pySMART
Copyright (C) 2014 Marc Herndon
pySMART is a simple Python wrapper for the smartctl
component of
smartmontools
. It works under Linux and Windows, as long as smartctl is on
the system path. Running with administrative (root) privilege is strongly
recommended, as smartctl cannot accurately detect all device types or parse
all SMART information without full permissions.
With only a device's name (ie: /dev/sda, pd0), the API will create a
Device
object, populated with all relevant information about
that device. The documented API can then be used to query this object for
information, initiate device self-tests, and perform other functions.
Usage
The most common way to use pySMART is to create a logical representation of the physical storage device that you would like to work with, as shown:
#!bash
>>> from pySMART import Device
>>> sda = Device('/dev/sda')
>>> sda
<SATA device on /dev/sda mod:WDC WD5000AAKS-60Z1A0 sn:WD-WCAWFxxxxxxx>
Device
class members can be accessed directly, and a number of helper methods
are provided to retrieve information in bulk. Some examples are shown below:
#!bash
>>> sda.assessment # Query the SMART self-assessment
'PASS'
>>> sda.attributes[9] # Query a single SMART attribute
<SMART Attribute 'Power_On_Hours' 068/000 raw:23644>
>>> sda.all_attributes() # Print the entire SMART attribute table
ID# ATTRIBUTE_NAME CUR WST THR TYPE UPDATED WHEN_FAIL RAW
1 Raw_Read_Error_Rate 200 200 051 Pre-fail Always - 0
3 Spin_Up_Time 141 140 021 Pre-fail Always - 3908
4 Start_Stop_Count 098 098 000 Old_age Always - 2690
5 Reallocated_Sector_Ct 200 200 140 Pre-fail Always - 0
... # Edited for brevity
199 UDMA_CRC_Error_Count 200 200 000 Old_age Always - 0
200 Multi_Zone_Error_Rate 200 200 000 Old_age Offline - 0
>>> sda.tests[0] # Query the most recent self-test result
<SMART Self-test [Short offline|Completed without error] hrs:23734 lba:->
>>> sda.all_selftests() # Print the entire self-test log
ID Test_Description Status Left Hours 1st_Error@lba
1 Short offline Completed without error 00% 23734 -
2 Short offline Completed without error 00% 23734 -
... # Edited for brevity
7 Short offline Completed without error 00% 23726 -
8 Short offline Completed without error 00% 1 -
Alternatively, the package provides a DeviceList
class. When instantiated,
this will auto-detect all local storage devices and create a list containing
one Device
object for each detected storage device.
#!bash
>>> from pySMART import DeviceList
>>> devlist = DeviceList()
>>> devlist
<DeviceList contents:
<SAT device on /dev/sdb mod:WDC WD20EADS-00R6B0 sn:WD-WCAVYxxxxxxx>
<SAT device on /dev/sdc mod:WDC WD20EADS-00S2B0 sn:WD-WCAVYxxxxxxx>
<CSMI device on /dev/csmi0,0 mod:WDC WD5000AAKS-60Z1A0 sn:WD-WCAWFxxxxxxx>
>
>>> devlist.devices[0].attributes[5] # Access Device data as above
<SMART Attribute 'Reallocated_Sector_Ct' 173/140 raw:214>
In the above cases if a new DeviceList is empty or a specific Device reports an "UNKNOWN INTERFACE", you are likely running without administrative privileges. On POSIX systems, you can request smartctl is run as a superuser by setting the sudo attribute of the global SMARTCTL object to True. Note this may cause you to be prompted for a password.
#!bash
>>> from pySMART import DeviceList
>>> from pySMART import Device
>>> sda = Device('/dev/sda')
>>> sda
<UNKNOWN INTERFACE device on /dev/sda mod:None sn:None>
>>> devlist = DeviceList()
>>> devlist
<DeviceList contents:
>
>>> from pySMART import SMARTCTL
>>> SMARTCTL.sudo = True
>>> sda = Device('/dev/sda')
>>> sda
[sudo] password for user:
<SAT device on /dev/sda mod:ST10000DM0004-1ZC101 sn:ZA20VNPT>
>>> devlist = DeviceList()
>>> devlist
<DeviceList contents:
<NVME device on /dev/nvme0 mod:Sabrent Rocket 4.0 1TB sn:03850709185D88300410>
<NVME device on /dev/nvme1 mod:Samsung SSD 970 EVO Plus 2TB sn:S59CNM0RB05028D>
<NVME device on /dev/nvme2 mod:Samsung SSD 970 EVO Plus 2TB sn:S59CNM0RB05113H>
<SAT device on /dev/sda mod:ST10000DM0004-1ZC101 sn:ZA20VNPT>
<SAT device on /dev/sdb mod:ST10000DM0004-1ZC101 sn:ZA22W366>
<SAT device on /dev/sdc mod:ST10000DM0004-1ZC101 sn:ZA22SPLG>
<SAT device on /dev/sdd mod:ST10000DM0004-1ZC101 sn:ZA2215HL>
>
In general, it is recommended to run the base script with enough privileges to execute smartctl, but this is not possible in all cases, so this workaround is provided as a convenience. However, note that using sudo inside other non-terminal projects may cause dev-bugs/issues.
Using the pySMART wrapper, Python applications be be rapidly developed to take advantage of the powerful features of smartmontools.
Acknowledgements
I would like to thank the entire team behind smartmontools for creating and maintaining such a fantastic product.
In particular I want to thank Christian Franke, who maintains the Windows port of the software. For several years I have written Windows batch files that rely on smartctl.exe to automate evaluation and testing of large pools of storage devices under Windows. Without his work, my job would have been significantly more miserable. :)
Having recently migrated my development from Batch to Python for Linux portability, I thought a simple wrapper for smartctl would save time in the development of future automated test tools.
1# Copyright (C) 2014 Marc Herndon 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""" 19Copyright (C) 2014 Marc Herndon 20 21pySMART is a simple Python wrapper for the `smartctl` component of 22`smartmontools`. It works under Linux and Windows, as long as smartctl is on 23the system path. Running with administrative (root) privilege is strongly 24recommended, as smartctl cannot accurately detect all device types or parse 25all SMART information without full permissions. 26 27With only a device's name (ie: /dev/sda, pd0), the API will create a 28`Device` object, populated with all relevant information about 29that device. The documented API can then be used to query this object for 30information, initiate device self-tests, and perform other functions. 31 32Usage 33----- 34The most common way to use pySMART is to create a logical representation of the 35physical storage device that you would like to work with, as shown: 36 37 #!bash 38 >>> from pySMART import Device 39 >>> sda = Device('/dev/sda') 40 >>> sda 41 <SATA device on /dev/sda mod:WDC WD5000AAKS-60Z1A0 sn:WD-WCAWFxxxxxxx> 42 43`Device` class members can be accessed directly, and a number of helper methods 44are provided to retrieve information in bulk. Some examples are shown below: 45 46 #!bash 47 >>> sda.assessment # Query the SMART self-assessment 48 'PASS' 49 >>> sda.attributes[9] # Query a single SMART attribute 50 <SMART Attribute 'Power_On_Hours' 068/000 raw:23644> 51 >>> sda.all_attributes() # Print the entire SMART attribute table 52 ID# ATTRIBUTE_NAME CUR WST THR TYPE UPDATED WHEN_FAIL RAW 53 1 Raw_Read_Error_Rate 200 200 051 Pre-fail Always - 0 54 3 Spin_Up_Time 141 140 021 Pre-fail Always - 3908 55 4 Start_Stop_Count 098 098 000 Old_age Always - 2690 56 5 Reallocated_Sector_Ct 200 200 140 Pre-fail Always - 0 57 ... # Edited for brevity 58 199 UDMA_CRC_Error_Count 200 200 000 Old_age Always - 0 59 200 Multi_Zone_Error_Rate 200 200 000 Old_age Offline - 0 60 >>> sda.tests[0] # Query the most recent self-test result 61 <SMART Self-test [Short offline|Completed without error] hrs:23734 lba:-> 62 >>> sda.all_selftests() # Print the entire self-test log 63 ID Test_Description Status Left Hours 1st_Error@lba 64 1 Short offline Completed without error 00% 23734 - 65 2 Short offline Completed without error 00% 23734 - 66 ... # Edited for brevity 67 7 Short offline Completed without error 00% 23726 - 68 8 Short offline Completed without error 00% 1 - 69 70Alternatively, the package provides a `DeviceList` class. When instantiated, 71this will auto-detect all local storage devices and create a list containing 72one `Device` object for each detected storage device. 73 74 #!bash 75 >>> from pySMART import DeviceList 76 >>> devlist = DeviceList() 77 >>> devlist 78 <DeviceList contents: 79 <SAT device on /dev/sdb mod:WDC WD20EADS-00R6B0 sn:WD-WCAVYxxxxxxx> 80 <SAT device on /dev/sdc mod:WDC WD20EADS-00S2B0 sn:WD-WCAVYxxxxxxx> 81 <CSMI device on /dev/csmi0,0 mod:WDC WD5000AAKS-60Z1A0 sn:WD-WCAWFxxxxxxx> 82 > 83 >>> devlist.devices[0].attributes[5] # Access Device data as above 84 <SMART Attribute 'Reallocated_Sector_Ct' 173/140 raw:214> 85 86In the above cases if a new DeviceList is empty or a specific Device reports an 87"UNKNOWN INTERFACE", you are likely running without administrative privileges. 88On POSIX systems, you can request smartctl is run as a superuser by setting the 89sudo attribute of the global SMARTCTL object to True. Note this may cause you 90to be prompted for a password. 91 92 #!bash 93 >>> from pySMART import DeviceList 94 >>> from pySMART import Device 95 >>> sda = Device('/dev/sda') 96 >>> sda 97 <UNKNOWN INTERFACE device on /dev/sda mod:None sn:None> 98 >>> devlist = DeviceList() 99 >>> devlist 100 <DeviceList contents: 101 > 102 >>> from pySMART import SMARTCTL 103 >>> SMARTCTL.sudo = True 104 >>> sda = Device('/dev/sda') 105 >>> sda 106 [sudo] password for user: 107 <SAT device on /dev/sda mod:ST10000DM0004-1ZC101 sn:ZA20VNPT> 108 >>> devlist = DeviceList() 109 >>> devlist 110 <DeviceList contents: 111 <NVME device on /dev/nvme0 mod:Sabrent Rocket 4.0 1TB sn:03850709185D88300410> 112 <NVME device on /dev/nvme1 mod:Samsung SSD 970 EVO Plus 2TB sn:S59CNM0RB05028D> 113 <NVME device on /dev/nvme2 mod:Samsung SSD 970 EVO Plus 2TB sn:S59CNM0RB05113H> 114 <SAT device on /dev/sda mod:ST10000DM0004-1ZC101 sn:ZA20VNPT> 115 <SAT device on /dev/sdb mod:ST10000DM0004-1ZC101 sn:ZA22W366> 116 <SAT device on /dev/sdc mod:ST10000DM0004-1ZC101 sn:ZA22SPLG> 117 <SAT device on /dev/sdd mod:ST10000DM0004-1ZC101 sn:ZA2215HL> 118 > 119 120In general, it is recommended to run the base script with enough privileges to 121execute smartctl, but this is not possible in all cases, so this workaround is 122provided as a convenience. However, note that using sudo inside other 123non-terminal projects may cause dev-bugs/issues. 124 125 126Using the pySMART wrapper, Python applications be be rapidly developed to take 127advantage of the powerful features of smartmontools. 128 129Acknowledgements 130---------------- 131I would like to thank the entire team behind smartmontools for creating and 132maintaining such a fantastic product. 133 134In particular I want to thank Christian Franke, who maintains the Windows port 135of the software. For several years I have written Windows batch files that 136rely on smartctl.exe to automate evaluation and testing of large pools of 137storage devices under Windows. Without his work, my job would have been 138significantly more miserable. :) 139 140Having recently migrated my development from Batch to Python for Linux 141portability, I thought a simple wrapper for smartctl would save time in the 142development of future automated test tools. 143""" 144# autopep8: off 145from .testentry import TestEntry 146from .interface.ata.attribute import Attribute 147from . import utils 148utils.configure_trace_logging() 149from .smartctl import SMARTCTL 150from .device_list import DeviceList 151from .device import Device, smart_health_assement 152from .version import __version__,__version_tuple__ 153# autopep8: on 154 155 156__all__ = [ 157 '__version__', '__version_tuple__', 158 'TestEntry', 'Attribute', 'utils', 'SMARTCTL', 'DeviceList', 'Device', 159 'smart_health_assement' 160]
27class TestEntry(object): 28 """ 29 Contains all of the information associated with a single SMART Self-test 30 log entry. This data is intended to exactly mirror that obtained through 31 smartctl. 32 """ 33 34 def __init__(self, format, num: Optional[int], test_type, status, hours, lba, 35 remain=None, 36 segment=None, 37 sense=None, 38 asc=None, 39 ascq=None, 40 nsid=None, 41 sct=None, 42 code=None): 43 44 self._format = format 45 """ 46 **(str):** Indicates whether this entry was taken from an 'ata' or 47 'scsi' self-test log. Used to display the content properly. 48 """ 49 self.num: Optional[int] = num 50 """ 51 **(int):** Entry's position in the log from 1 (most recent) to 21 52 (least recent). ATA logs save the last 21 entries while SCSI logs 53 only save the last 20. 54 """ 55 self.type = test_type 56 """ 57 **(str):** Type of test run. Generally short, long (extended), or 58 conveyance, plus offline (background) or captive (foreground). 59 """ 60 self.status = status 61 """ 62 **(str):** Self-test's status message, for example 'Completed without 63 error' or 'Completed: read failure'. 64 """ 65 self.hours = hours 66 """ 67 **(str):** The device's power-on hours at the time the self-test 68 was initiated. 69 """ 70 self.LBA = lba 71 """ 72 **(str):** Indicates the first LBA at which an error was encountered 73 during this self-test. Presented as a decimal value for ATA/SATA 74 devices and in hexadecimal notation for SAS/SCSI devices. 75 """ 76 self.remain = remain 77 """ 78 **(str):** Percentage value indicating how much of the self-test is 79 left to perform. '00%' indicates a complete test, while any other 80 value could indicate a test in progress or one that failed prior to 81 completion. Only reported by ATA devices. 82 """ 83 self.segment = segment 84 """ 85 **(str):** A manufacturer-specific self-test segment number reported 86 by SCSI devices on self-test failure. Set to '-' otherwise. 87 """ 88 self.sense = sense 89 """ 90 **(str):** SCSI sense key reported on self-test failure. Set to '-' 91 otherwise. 92 """ 93 self.ASC = asc 94 """ 95 **(str):** SCSI 'Additonal Sense Code' reported on self-test failure. 96 Set to '-' otherwise. 97 """ 98 self.ASCQ = ascq 99 """ 100 **(str):** SCSI 'Additonal Sense Code Quaifier' reported on self-test 101 failure. Set to '-' otherwise. 102 """ 103 self.nsid = nsid 104 """ 105 **(str):** NVMe 'Name Space Identifier' reported on self-test failure. 106 Set to '-' if no namespace is defined. 107 """ 108 self.sct = sct 109 """ 110 **(str):** NVMe 'Status Code Type' reported on self-test failure. 111 Set to '-' if undefined. 112 """ 113 self.code = code 114 """ 115 **(str):** NVMe 'Status Code' reported on self-test failure. 116 Set to '-' if undefined. 117 """ 118 119 def __getstate__(self): 120 return { 121 'num': self.num, 122 'type': self.type, 123 'status': self.status, 124 'hours': self.hours, 125 'lba': self.LBA, 126 'remain': self.remain, 127 'segment': self.segment, 128 'sense': self.sense, 129 'asc': self.ASC, 130 'ascq': self.ASCQ, 131 'nsid': self.nsid, 132 'sct': self.sct, 133 'code': self.code 134 } 135 136 def __repr__(self): 137 """Define a basic representation of the class object.""" 138 return "<SMART Self-test [%s|%s] hrs:%s LBA:%s>" % ( 139 self.type, self.status, self.hours, self.LBA) 140 141 def __str__(self): 142 """ 143 Define a formatted string representation of the object's content. 144 Looks nearly identical to the output of smartctl, without overflowing 145 80-character lines. 146 """ 147 if self._format == 'ata': 148 return "{0:>2} {1:17}{2:30}{3:5}{4:7}{5:17}".format( 149 self.num, self.type, self.status, self.remain, self.hours, 150 self.LBA) 151 elif self._format == 'scsi': 152 # 'Segment' could not be fit on the 80-char line. It's of limited 153 # utility anyway due to it's manufacturer-proprietary nature... 154 return ("{0:>2} {1:17}{2:23}{3:7}{4:14}[{5:4}{6:5}{7:4}]".format( 155 self.num, 156 self.type, 157 self.status, 158 self.hours, 159 self.LBA, 160 self.sense, 161 self.ASC, 162 self.ASCQ 163 )) 164 elif self._format == 'nvme': 165 ## NVME FORMAT ## 166 # Example smartctl output 167 # Self-test Log (NVMe Log 0x06) 168 # Self-test status: Extended self-test in progress (28% completed) 169 # Num Test_Description Status Power_on_Hours Failing_LBA NSID Seg SCT Code 170 # 0 Extended Completed without error 3441 - - - - - 171 return ("{0:^4} {1:<18}{2:<29}{3:>14}{4:>13}{5:>6}{6:>4}{7:>4}{8:>5}".format( 172 self.num, 173 self.type, 174 self.status, 175 self.hours, 176 self.LBA if self.LBA is not None else '-', 177 self.nsid if self.LBA is not None else '-', 178 self.segment if self.segment is not None else '-', 179 self.sct if self.LBA is not None else '-', 180 self.code if self.LBA is not None else '-' 181 )) 182 else: 183 return "Unknown test format: %s" % self._format
Contains all of the information associated with a single SMART Self-test log entry. This data is intended to exactly mirror that obtained through smartctl.
34 def __init__(self, format, num: Optional[int], test_type, status, hours, lba, 35 remain=None, 36 segment=None, 37 sense=None, 38 asc=None, 39 ascq=None, 40 nsid=None, 41 sct=None, 42 code=None): 43 44 self._format = format 45 """ 46 **(str):** Indicates whether this entry was taken from an 'ata' or 47 'scsi' self-test log. Used to display the content properly. 48 """ 49 self.num: Optional[int] = num 50 """ 51 **(int):** Entry's position in the log from 1 (most recent) to 21 52 (least recent). ATA logs save the last 21 entries while SCSI logs 53 only save the last 20. 54 """ 55 self.type = test_type 56 """ 57 **(str):** Type of test run. Generally short, long (extended), or 58 conveyance, plus offline (background) or captive (foreground). 59 """ 60 self.status = status 61 """ 62 **(str):** Self-test's status message, for example 'Completed without 63 error' or 'Completed: read failure'. 64 """ 65 self.hours = hours 66 """ 67 **(str):** The device's power-on hours at the time the self-test 68 was initiated. 69 """ 70 self.LBA = lba 71 """ 72 **(str):** Indicates the first LBA at which an error was encountered 73 during this self-test. Presented as a decimal value for ATA/SATA 74 devices and in hexadecimal notation for SAS/SCSI devices. 75 """ 76 self.remain = remain 77 """ 78 **(str):** Percentage value indicating how much of the self-test is 79 left to perform. '00%' indicates a complete test, while any other 80 value could indicate a test in progress or one that failed prior to 81 completion. Only reported by ATA devices. 82 """ 83 self.segment = segment 84 """ 85 **(str):** A manufacturer-specific self-test segment number reported 86 by SCSI devices on self-test failure. Set to '-' otherwise. 87 """ 88 self.sense = sense 89 """ 90 **(str):** SCSI sense key reported on self-test failure. Set to '-' 91 otherwise. 92 """ 93 self.ASC = asc 94 """ 95 **(str):** SCSI 'Additonal Sense Code' reported on self-test failure. 96 Set to '-' otherwise. 97 """ 98 self.ASCQ = ascq 99 """ 100 **(str):** SCSI 'Additonal Sense Code Quaifier' reported on self-test 101 failure. Set to '-' otherwise. 102 """ 103 self.nsid = nsid 104 """ 105 **(str):** NVMe 'Name Space Identifier' reported on self-test failure. 106 Set to '-' if no namespace is defined. 107 """ 108 self.sct = sct 109 """ 110 **(str):** NVMe 'Status Code Type' reported on self-test failure. 111 Set to '-' if undefined. 112 """ 113 self.code = code 114 """ 115 **(str):** NVMe 'Status Code' reported on self-test failure. 116 Set to '-' if undefined. 117 """
(int): Entry's position in the log from 1 (most recent) to 21 (least recent). ATA logs save the last 21 entries while SCSI logs only save the last 20.
(str): Type of test run. Generally short, long (extended), or conveyance, plus offline (background) or captive (foreground).
(str): Self-test's status message, for example 'Completed without error' or 'Completed: read failure'.
(str): Indicates the first LBA at which an error was encountered during this self-test. Presented as a decimal value for ATA/SATA devices and in hexadecimal notation for SAS/SCSI devices.
(str): Percentage value indicating how much of the self-test is left to perform. '00%' indicates a complete test, while any other value could indicate a test in progress or one that failed prior to completion. Only reported by ATA devices.
(str): A manufacturer-specific self-test segment number reported by SCSI devices on self-test failure. Set to '-' otherwise.
(str): SCSI 'Additonal Sense Code Quaifier' reported on self-test failure. Set to '-' otherwise.
28class Attribute(object): 29 """ 30 Contains all of the information associated with a single SMART attribute 31 in a `Device`'s SMART table. This data is intended to exactly mirror that 32 obtained through smartctl. 33 """ 34 35 def __init__(self, num: int, name, flags: int, value, worst, thresh, attr_type, updated, when_failed, raw): 36 self.num: int = num 37 """**(int):** Attribute's ID as a decimal value (1-255).""" 38 self.name: str = name 39 """ 40 **(str):** Attribute's name, as reported by smartmontools' drive.db. 41 """ 42 self.flags: int = flags 43 """**(int):** Attribute flags as a bit value (ie: 0x0032).""" 44 self._value: str = value 45 """**(str):** Attribute's current normalized value.""" 46 self._worst: str = worst 47 """**(str):** Worst recorded normalized value for this attribute.""" 48 self._thresh: str = thresh 49 """**(str):** Attribute's failure threshold.""" 50 self.type: str = attr_type 51 """**(str):** Attribute's type, generally 'pre-fail' or 'old-age'.""" 52 self.updated: str = updated 53 """ 54 **(str):** When is this attribute updated? Generally 'Always' or 55 'Offline' 56 """ 57 self.when_failed: str = when_failed 58 """ 59 **(str):** When did this attribute cross below 60 `pySMART.attribute.Attribute.thresh`? Reads '-' when not failed. 61 Generally either 'FAILING_NOW' or 'In_the_Past' otherwise. 62 """ 63 self.raw = raw 64 """**(str):** Attribute's current raw (non-normalized) value.""" 65 66 @property 67 def value_str(self) -> str: 68 """Gets the attribute value 69 70 Returns: 71 str: The attribute value in string format 72 """ 73 return self._value 74 75 @property 76 def value_int(self) -> int: 77 """Gets the attribute value 78 79 Returns: 80 int: The attribute value in integer format. 81 """ 82 return int(self._value) 83 84 @property 85 def value(self) -> str: 86 """Gets the attribue value 87 88 Returns: 89 str: The attribute value in string format 90 """ 91 return self.value_str 92 93 @property 94 def worst(self) -> int: 95 """Gets the worst value 96 97 Returns: 98 int: The attribute worst field in integer format 99 """ 100 return int(self._worst) 101 102 @property 103 def thresh(self) -> Optional[int]: 104 """Gets the threshold value 105 106 Returns: 107 int: The attribute threshold field in integer format 108 """ 109 return None if self._thresh == '---' else int(self._thresh) 110 111 @property 112 def raw_int(self) -> Optional[int]: 113 """Gets the raw value converted to int 114 NOTE: Some values may not be correctly converted! 115 116 Returns: 117 int: The attribute raw-value field in integer format. 118 None: In case the raw string failed to be parsed 119 """ 120 try: 121 return int(re.search(r'\d+', self.raw).group()) 122 except: 123 return None 124 125 def __repr__(self): 126 """Define a basic representation of the class object.""" 127 return "<SMART Attribute %r %s/%s raw:%s>" % ( 128 self.name, self.value, self.thresh, self.raw) 129 130 def __str__(self): 131 """ 132 Define a formatted string representation of the object's content. 133 In the interest of not overflowing 80-character lines this does not 134 print the value of `pySMART.attribute.Attribute.flags_hex`. 135 """ 136 return "{0:>3} {1:23}{2:>4}{3:>4}{4:>4} {5:9}{6:8}{7:12}{8}".format( 137 self.num, 138 self.name, 139 self.value, 140 self.worst, 141 self.thresh, 142 self.type, 143 self.updated, 144 self.when_failed, 145 self.raw 146 ) 147 148 def __getstate__(self): 149 return { 150 'name': self.name, 151 'num': self.num, 152 'flags': self.flags, 153 'raw': self.raw, 154 'value': self.value, 155 'worst': self.worst, 156 'thresh': self.thresh, 157 'type': self.type, 158 'updated': self.updated, 159 'when_failed': self.when_failed, 160 # Raw values 161 '_value': self._value, 162 '_worst': self._worst, 163 '_thresh': self._thresh, 164 'raw_int': self.raw_int, 165 166 }
Contains all of the information associated with a single SMART attribute
in a Device
's SMART table. This data is intended to exactly mirror that
obtained through smartctl.
35 def __init__(self, num: int, name, flags: int, value, worst, thresh, attr_type, updated, when_failed, raw): 36 self.num: int = num 37 """**(int):** Attribute's ID as a decimal value (1-255).""" 38 self.name: str = name 39 """ 40 **(str):** Attribute's name, as reported by smartmontools' drive.db. 41 """ 42 self.flags: int = flags 43 """**(int):** Attribute flags as a bit value (ie: 0x0032).""" 44 self._value: str = value 45 """**(str):** Attribute's current normalized value.""" 46 self._worst: str = worst 47 """**(str):** Worst recorded normalized value for this attribute.""" 48 self._thresh: str = thresh 49 """**(str):** Attribute's failure threshold.""" 50 self.type: str = attr_type 51 """**(str):** Attribute's type, generally 'pre-fail' or 'old-age'.""" 52 self.updated: str = updated 53 """ 54 **(str):** When is this attribute updated? Generally 'Always' or 55 'Offline' 56 """ 57 self.when_failed: str = when_failed 58 """ 59 **(str):** When did this attribute cross below 60 `pySMART.attribute.Attribute.thresh`? Reads '-' when not failed. 61 Generally either 'FAILING_NOW' or 'In_the_Past' otherwise. 62 """ 63 self.raw = raw 64 """**(str):** Attribute's current raw (non-normalized) value."""
(str): When did this attribute cross below
pySMART.attribute.Attribute.thresh
? Reads '-' when not failed.
Generally either 'FAILING_NOW' or 'In_the_Past' otherwise.
66 @property 67 def value_str(self) -> str: 68 """Gets the attribute value 69 70 Returns: 71 str: The attribute value in string format 72 """ 73 return self._value
Gets the attribute value
Returns: str: The attribute value in string format
75 @property 76 def value_int(self) -> int: 77 """Gets the attribute value 78 79 Returns: 80 int: The attribute value in integer format. 81 """ 82 return int(self._value)
Gets the attribute value
Returns: int: The attribute value in integer format.
84 @property 85 def value(self) -> str: 86 """Gets the attribue value 87 88 Returns: 89 str: The attribute value in string format 90 """ 91 return self.value_str
Gets the attribue value
Returns: str: The attribute value in string format
93 @property 94 def worst(self) -> int: 95 """Gets the worst value 96 97 Returns: 98 int: The attribute worst field in integer format 99 """ 100 return int(self._worst)
Gets the worst value
Returns: int: The attribute worst field in integer format
102 @property 103 def thresh(self) -> Optional[int]: 104 """Gets the threshold value 105 106 Returns: 107 int: The attribute threshold field in integer format 108 """ 109 return None if self._thresh == '---' else int(self._thresh)
Gets the threshold value
Returns: int: The attribute threshold field in integer format
111 @property 112 def raw_int(self) -> Optional[int]: 113 """Gets the raw value converted to int 114 NOTE: Some values may not be correctly converted! 115 116 Returns: 117 int: The attribute raw-value field in integer format. 118 None: In case the raw string failed to be parsed 119 """ 120 try: 121 return int(re.search(r'\d+', self.raw).group()) 122 except: 123 return None
Gets the raw value converted to int NOTE: Some values may not be correctly converted!
Returns: int: The attribute raw-value field in integer format. None: In case the raw string failed to be parsed
37class DeviceList(object): 38 """ 39 Represents a list of all the storage devices connected to this computer. 40 """ 41 42 def __init__(self, init: bool = True, smartctl=SMARTCTL, catch_errors: bool = False): 43 """Instantiates and optionally initializes the `DeviceList`. 44 45 Args: 46 init (bool, optional): By default, `pySMART.device_list.DeviceList.devices` 47 is populated with `Device` objects during instantiation. Setting init 48 to False will skip initialization and create an empty 49 `pySMART.device_list.DeviceList` object instead. Defaults to True. 50 smartctl ([type], optional): This stablish the smartctl wrapper. 51 Defaults the global `SMARTCTL` object and should be only 52 overwritten on tests. 53 catch_errors (bool, optional): If True, individual device-parsing errors will be caught 54 """ 55 56 self.devices: List[Device] = [] 57 """ 58 **(list of `Device`):** Contains all storage devices detected during 59 instantiation, as `Device` objects. 60 """ 61 self.smartctl: Smartctl = smartctl 62 """The smartctl wrapper 63 """ 64 if init: 65 self.initialize(catch_errors) 66 67 def __repr__(self): 68 """Define a basic representation of the class object.""" 69 rep = "<DeviceList contents:\n" 70 for device in self.devices: 71 rep += str(device) + '\n' 72 return rep + '>' 73 # return "<DeviceList contents:%r>" % (self.devices) 74 75 def _cleanup(self): 76 """ 77 Removes duplicate ATA devices that correspond to an existing CSMI 78 device. Also removes any device with no capacity value, as this 79 indicates removable storage, ie: CD/DVD-ROM, ZIP, etc. 80 """ 81 # We can't operate directly on the list while we're iterating 82 # over it, so we collect indeces to delete and remove them later 83 to_delete = [] 84 # Enumerate the list to get tuples containing indeces and values 85 for index, device in enumerate(self.devices): 86 if device.interface == 'csmi': 87 for otherindex, otherdevice in enumerate(self.devices): 88 if (otherdevice.interface == 'ata' or 89 otherdevice.interface == 'sata'): 90 if device.serial == otherdevice.serial: 91 to_delete.append(otherindex) 92 device._sd_name = otherdevice.name 93 if device.capacity is None and index not in to_delete: 94 to_delete.append(index) 95 # Recreate the self.devices list without the marked indeces 96 self.devices[:] = [v for i, v in enumerate(self.devices) 97 if i not in to_delete] 98 99 def initialize(self, catch_errors: bool = False): 100 """ 101 Scans system busses for attached devices and add them to the 102 `DeviceList` as `Device` objects. 103 If device list is already populated, it will be cleared first. 104 105 Args: 106 catch_errors (bool, optional): If True, individual device-parsing errors will be caught 107 """ 108 109 # Clear the list if it's already populated 110 if len(self.devices): 111 self.devices = [] 112 113 # Scan for devices 114 for line in self.smartctl.scan(): 115 if not ('failed:' in line or line == ''): 116 groups = re.compile( 117 r'^(\S+)\s+-d\s+(\S+)').match(line).groups() 118 name = groups[0] 119 interface = groups[1] 120 121 try: 122 # Add the device to the list 123 self.devices.append( 124 Device(name, interface=interface, smartctl=self.smartctl)) 125 126 except Exception as e: 127 if catch_errors: 128 # Print the exception 129 import logging 130 131 logging.exception(f"Error parsing device {name}") 132 133 else: 134 # Reraise the exception 135 raise e 136 137 # Remove duplicates and unwanted devices (optical, etc.) from the list 138 self._cleanup() 139 # Sort the list alphabetically by device name 140 self.devices.sort(key=lambda device: device.name) 141 142 def __getitem__(self, index: int) -> Device: 143 """Returns an element from self.devices 144 145 Args: 146 index (int): An index of self.devices 147 148 Returns: 149 Device: Returns a Device that is located on the asked index 150 """ 151 return self.devices[index]
Represents a list of all the storage devices connected to this computer.
42 def __init__(self, init: bool = True, smartctl=SMARTCTL, catch_errors: bool = False): 43 """Instantiates and optionally initializes the `DeviceList`. 44 45 Args: 46 init (bool, optional): By default, `pySMART.device_list.DeviceList.devices` 47 is populated with `Device` objects during instantiation. Setting init 48 to False will skip initialization and create an empty 49 `pySMART.device_list.DeviceList` object instead. Defaults to True. 50 smartctl ([type], optional): This stablish the smartctl wrapper. 51 Defaults the global `SMARTCTL` object and should be only 52 overwritten on tests. 53 catch_errors (bool, optional): If True, individual device-parsing errors will be caught 54 """ 55 56 self.devices: List[Device] = [] 57 """ 58 **(list of `Device`):** Contains all storage devices detected during 59 instantiation, as `Device` objects. 60 """ 61 self.smartctl: Smartctl = smartctl 62 """The smartctl wrapper 63 """ 64 if init: 65 self.initialize(catch_errors)
Instantiates and optionally initializes the DeviceList
.
Args:
init (bool, optional): By default, DeviceList.devices
is populated with Device
objects during instantiation. Setting init
to False will skip initialization and create an empty
DeviceList
object instead. Defaults to True.
smartctl ([type], optional): This stablish the smartctl wrapper.
Defaults the global SMARTCTL
object and should be only
overwritten on tests.
catch_errors (bool, optional): If True, individual device-parsing errors will be caught
99 def initialize(self, catch_errors: bool = False): 100 """ 101 Scans system busses for attached devices and add them to the 102 `DeviceList` as `Device` objects. 103 If device list is already populated, it will be cleared first. 104 105 Args: 106 catch_errors (bool, optional): If True, individual device-parsing errors will be caught 107 """ 108 109 # Clear the list if it's already populated 110 if len(self.devices): 111 self.devices = [] 112 113 # Scan for devices 114 for line in self.smartctl.scan(): 115 if not ('failed:' in line or line == ''): 116 groups = re.compile( 117 r'^(\S+)\s+-d\s+(\S+)').match(line).groups() 118 name = groups[0] 119 interface = groups[1] 120 121 try: 122 # Add the device to the list 123 self.devices.append( 124 Device(name, interface=interface, smartctl=self.smartctl)) 125 126 except Exception as e: 127 if catch_errors: 128 # Print the exception 129 import logging 130 131 logging.exception(f"Error parsing device {name}") 132 133 else: 134 # Reraise the exception 135 raise e 136 137 # Remove duplicates and unwanted devices (optical, etc.) from the list 138 self._cleanup() 139 # Sort the list alphabetically by device name 140 self.devices.sort(key=lambda device: device.name)
Scans system busses for attached devices and add them to the
DeviceList
as Device
objects.
If device list is already populated, it will be cleared first.
Args: catch_errors (bool, optional): If True, individual device-parsing errors will be caught
81class Device(object): 82 """ 83 Represents any device attached to an internal storage interface, such as a 84 hard drive or DVD-ROM, and detected by smartmontools. Includes eSATA 85 (considered SATA) but excludes other external devices (USB, Firewire). 86 """ 87 88 def __init__(self, name: str, interface: Optional[str] = None, abridged: bool = False, smart_options: Union[str, List[str], None] = None, smartctl: Smartctl = SMARTCTL): 89 """Instantiates and initializes the `pySMART.device.Device`.""" 90 if not ( 91 interface is None or 92 smartctl_isvalid_type(interface.lower()) 93 ): 94 raise ValueError( 95 'Unknown interface: {0} specified for {1}'.format(interface, name)) 96 self.abridged = abridged or interface == 'UNKNOWN INTERFACE' 97 if smart_options is not None: 98 if isinstance(smart_options, str): 99 smart_options = smart_options.split(' ') 100 smartctl.add_options(smart_options) 101 self.smartctl = smartctl 102 """ 103 """ 104 self.name: str = name.replace('/dev/', '').replace('nvd', 'nvme') 105 """ 106 **(str):** Device's hardware ID, without the '/dev/' prefix. 107 (ie: sda (Linux), pd0 (Windows)) 108 """ 109 self.family: Optional[str] = None 110 """**(str):** Device's family (if any).""" 111 self.model: Optional[str] = None 112 """**(str):** Device's model number (if any).""" 113 self.serial: Optional[str] = None 114 """**(str):** Device's serial number (if any).""" 115 self._vendor: Optional[str] = None 116 """**(str):** Device's vendor (if any).""" 117 self._interface: Optional[str] = None if interface == 'UNKNOWN INTERFACE' else interface 118 """ 119 **(str):** Device's interface type. Must be one of: 120 * **ATA** - Advanced Technology Attachment 121 * **SATA** - Serial ATA 122 * **SCSI** - Small Computer Systems Interface 123 * **SAS** - Serial Attached SCSI 124 * **SAT** - SCSI-to-ATA Translation (SATA device plugged into a 125 SAS port) 126 * **CSMI** - Common Storage Management Interface (Intel ICH / 127 Matrix RAID) 128 Generally this should not be specified to allow auto-detection to 129 occur. Otherwise, this value overrides the auto-detected type and could 130 produce unexpected or no data. 131 """ 132 self._capacity: Optional[int] = None 133 """**(str):** Device's user capacity as reported directly by smartctl (RAW).""" 134 self._capacity_human: Optional[str] = None 135 """**(str):** Device's user capacity (human readable) as reported directly by smartctl (RAW).""" 136 self.firmware: Optional[str] = None 137 """**(str):** Device's firmware version.""" 138 self.smart_capable: bool = 'nvme' in self.name 139 """ 140 **(bool):** True if the device has SMART Support Available. 141 False otherwise. This is useful for VMs amongst other things. 142 """ 143 self.smart_enabled: bool = 'nvme' in self.name 144 """ 145 **(bool):** True if the device supports SMART (or SCSI equivalent) and 146 has the feature set enabled. False otherwise. 147 """ 148 self.assessment: Optional[str] = None 149 """ 150 **(str):** SMART health self-assessment as reported by the device. 151 """ 152 self.messages: List[str] = [] 153 """ 154 **(list of str):** Contains any SMART warnings or other error messages 155 reported by the device (ie: ascq codes). 156 """ 157 self.is_ssd: bool = True if 'nvme' in self.name else False 158 """ 159 **(bool):** True if this device is a Solid State Drive. 160 False otherwise. 161 """ 162 self.rotation_rate: Optional[int] = None 163 """ 164 **(int):** The Roatation Rate of the Drive if it is not a SSD. 165 The Metric is RPM. 166 """ 167 self.test_capabilities = { 168 'offline': False, # SMART execute Offline immediate (ATA only) 169 'short': 'nvme' not in self.name, # SMART short Self-test 170 'long': 'nvme' not in self.name, # SMART long Self-test 171 'conveyance': False, # SMART Conveyance Self-Test (ATA only) 172 'selective': False, # SMART Selective Self-Test (ATA only) 173 } 174 # Note have not included 'offline' test for scsi as it runs in the foregorund 175 # mode. While this may be beneficial to us in someways it is against the 176 # general layout and pattern that the other tests issued using pySMART are 177 # followed hence not doing it currently 178 """ 179 **(dict): ** This dictionary contains key == 'Test Name' and 180 value == 'True/False' of self-tests that this device is capable of. 181 """ 182 # Note: The above are just default values and can/will be changed 183 # upon update() when the attributes and type of the disk is actually 184 # determined. 185 self.test_polling_time = { 186 'short': 10, 187 'long': 1000, 188 'conveyance': 20, 189 } 190 """ 191 **(dict): ** This dictionary contains key == 'Test Name' and 192 value == int of approximate times to run each test type that this 193 device is capable of. 194 """ 195 # Note: The above are just default values and can/will be changed 196 # upon update() when the attributes and type of the disk is actually 197 # determined. 198 self.tests: List[TestEntry] = [] 199 """ 200 **(list of `TestEntry`):** Contains the complete SMART self-test log 201 for this device, as provided by smartctl. 202 """ 203 self._test_running = False 204 """ 205 **(bool):** True if a self-test is currently being run. 206 False otherwise. 207 """ 208 self._test_ECD = None 209 """ 210 **(str):** Estimated completion time of the running SMART selftest. 211 Not provided by SAS/SCSI devices. 212 """ 213 self._test_progress = None 214 """ 215 **(int):** Estimate progress percantage of the running SMART selftest. 216 """ 217 self.diagnostics: Diagnostics = Diagnostics() 218 """ 219 **Diagnostics** Contains parsed and processed diagnostic information 220 extracted from the SMART information. Currently only populated for 221 SAS and SCSI devices, since ATA/SATA SMART attributes are manufacturer 222 proprietary. 223 """ 224 self._temperature: Optional[int] = None 225 """ 226 **(int or None): Since SCSI disks do not report attributes like ATA ones 227 we need to grep/regex the shit outta the normal "smartctl -a" output. 228 In case the device have more than one temperature sensor the first value 229 will be stored here too. 230 Note: Temperatures are always in Celsius (if possible). 231 """ 232 self.temperatures: Dict[int, int] = {} 233 """ 234 **(dict of int): NVMe disks usually report multiple temperatures, which 235 will be stored here if available. Keys are sensor numbers as reported in 236 output data. 237 Note: Temperatures are always in Celsius (if possible). 238 """ 239 self.logical_sector_size: Optional[int] = None 240 """ 241 **(int):** The logical sector size of the device (or LBA). 242 """ 243 self.physical_sector_size: Optional[int] = None 244 """ 245 **(int):** The physical sector size of the device. 246 """ 247 self.if_attributes: Union[None, NvmeAttributes, AtaAttributes] = None 248 """ 249 **(NvmeAttributes):** This object may vary for each device interface attributes. 250 It will store all data obtained from smartctl 251 """ 252 253 if self.name is None: 254 warnings.warn( 255 "\nDevice '{0}' does not exist! This object should be destroyed.".format( 256 name) 257 ) 258 return 259 # If no interface type was provided, scan for the device 260 # Lets do this only for the non-abridged case 261 # (we can work with no interface for abridged case) 262 elif self._interface is None and not self.abridged: 263 logger.trace( 264 "Determining interface of disk: {0}".format(self.name)) 265 raw, returncode = self.smartctl.generic_call( 266 ['-d', 'test', self.dev_reference]) 267 268 if len(raw) > 0: 269 # I do not like this parsing logic but it works for now! 270 # just for reference _stdout.split('\n') gets us 271 # something like 272 # [ 273 # ...copyright string..., 274 # '', 275 # "/dev/ada2: Device of type 'atacam' [ATA] detected", 276 # "/dev/ada2: Device of type 'atacam' [ATA] opened", 277 # '' 278 # ] 279 # The above example should be enough for anyone to understand the line below 280 try: 281 for line in reversed(raw): 282 if "opened" in line: 283 self._interface = line.split("'")[1] 284 285 if self._interface == "nvme": # if nvme set SMART to true 286 self.smart_capable = True 287 self.smart_enabled = True 288 289 break 290 except: 291 # for whatever reason we could not get the interface type 292 # we should mark this as an `abbridged` case and move on 293 self._interface = None 294 self.abbridged = True 295 # TODO: Uncomment the classify call if we ever find out that we need it 296 # Disambiguate the generic interface to a specific type 297 # self._classify() 298 else: 299 warnings.warn( 300 "\nDevice '{0}' does not exist! This object should be destroyed.".format( 301 name) 302 ) 303 return 304 # If a valid device was detected, populate its information 305 # OR if in unabridged mode, then do it even without interface info 306 if self._interface is not None or self.abridged: 307 self.update() 308 309 @property 310 def attributes(self) -> List[Optional[Attribute]]: 311 """Returns the SMART attributes of the device. 312 Note: This is only filled with ATA/SATA attributes. SCSI/SAS/NVMe devices will have empty lists!! 313 @deprecated: Use `if_attributes` instead. 314 315 Returns: 316 list of `Attribute`: The SMART attributes of the device. 317 """ 318 if self.if_attributes is None or not isinstance(self.if_attributes, AtaAttributes): 319 return [None] * 256 320 else: 321 return self.if_attributes.legacyAttributes 322 323 @property 324 def dev_interface(self) -> Optional[str]: 325 """Returns the internal interface type of the device. 326 It may not be the same as the interface type as used by smartctl. 327 328 Returns: 329 str: The interface type of the device. (example: ata, scsi, nvme) 330 None if the interface type could not be determined. 331 """ 332 # Try to get the fine-tuned interface type 333 fineType = self._classify() 334 335 # If return still contains a megaraid, just asume it's type 336 if 'megaraid' in fineType: 337 # If any attributes is not None and has at least non None value, then it is a sat+megaraid device 338 if isinstance(self.if_attributes, AtaAttributes): 339 return 'ata' 340 else: 341 return 'sas' 342 343 return fineType 344 345 @property 346 def temperature(self) -> Optional[int]: 347 """Returns the temperature of the device. 348 349 Returns: 350 int: The temperature of the device in Celsius. 351 None if the temperature could not be determined. 352 """ 353 if self.if_attributes is None: 354 return self._temperature 355 else: 356 return self.if_attributes.temperature or self._temperature 357 358 @property 359 def smartctl_interface(self) -> Optional[str]: 360 """Returns the interface type of the device as it is used in smartctl. 361 362 Returns: 363 str: The interface type of the device. (example: ata, scsi, nvme) 364 None if the interface type could not be determined. 365 """ 366 return self._interface 367 368 @property 369 def interface(self) -> Optional[str]: 370 """Returns the interface type of the device as it is used in smartctl. 371 372 Returns: 373 str: The interface type of the device. (example: ata, scsi, nvme) 374 None if the interface type could not be determined. 375 """ 376 return self.smartctl_interface 377 378 @property 379 def dev_reference(self) -> str: 380 """The reference to the device as provided by smartctl. 381 - On unix-like systems, this is the path to the device. (example /dev/<name>) 382 - On MacOS, this is the name of the device. (example <name>) 383 - On Windows, this is the drive letter of the device. (example <drive letter>) 384 385 Returns: 386 str: The reference to the device as provided by smartctl. 387 """ 388 389 # detect if we are on MacOS 390 if 'IOService' in self.name: 391 return self.name 392 393 # otherwise asume we are on unix-like systems 394 return os.path.join('/dev/', self.name) 395 396 @property 397 def vendor(self) -> Optional[str]: 398 """Returns the vendor of the device. 399 400 Returns: 401 str: The vendor of the device. 402 """ 403 if self._vendor: 404 return self._vendor 405 406 # If family is present, try to stract from family. Skip anything but letters. 407 elif self.family: 408 filter = re.search(r'^[a-zA-Z]+', self.family.strip()) 409 if filter: 410 return filter.group(0) 411 412 # If model is present, try to stract from model. Skip anything but letters. 413 elif self.model: 414 filter = re.search(r'^[a-zA-Z]+', self.model.strip()) 415 if filter: 416 return filter.group(0) 417 418 # If all else fails, return None 419 return None 420 421 @property 422 def capacity(self) -> Optional[str]: 423 """Returns the capacity in the raw smartctl format. 424 This may be deprecated in the future and its only retained for compatibility. 425 426 Returns: 427 str: The capacity in the raw smartctl format 428 """ 429 return self._capacity_human 430 431 @property 432 def diags(self) -> Dict[str, str]: 433 """Gets the old/deprecated version of SCSI/SAS diags atribute. 434 """ 435 return self.diagnostics.get_classic_format() 436 437 @property 438 def size_raw(self) -> Optional[str]: 439 """Returns the capacity in the raw smartctl format. 440 441 Returns: 442 str: The capacity in the raw smartctl format 443 """ 444 return self._capacity_human 445 446 @property 447 def size(self) -> int: 448 """Returns the capacity in bytes 449 450 Returns: 451 int: The capacity in bytes 452 """ 453 import humanfriendly 454 455 if self._capacity is not None: 456 return self._capacity 457 elif self._capacity_human is not None: 458 return humanfriendly.parse_size(self._capacity_human) 459 else: 460 return 0 461 462 @property 463 def sector_size(self) -> int: 464 """Returns the sector size of the device. 465 466 Returns: 467 int: The sector size of the device in Bytes. If undefined, we'll assume 512B 468 """ 469 if self.logical_sector_size is not None: 470 return self.logical_sector_size 471 elif self.physical_sector_size is not None: 472 return self.physical_sector_size 473 else: 474 return 512 475 476 def __repr__(self): 477 """Define a basic representation of the class object.""" 478 return "<{0} device on /dev/{1} mod:{2} sn:{3}>".format( 479 self._interface.upper() if self._interface else 'UNKNOWN INTERFACE', 480 self.name, 481 self.model, 482 self.serial 483 ) 484 485 def __getstate__(self, all_info=True): 486 """ 487 Allows us to send a pySMART Device object over a serializable 488 medium which uses json (or the likes of json) payloads 489 """ 490 return None 491 state_dict = { 492 'interface': self._interface if self._interface else 'UNKNOWN INTERFACE', 493 'model': self.model, 494 'firmware': self.firmware, 495 'smart_capable': self.smart_capable, 496 'smart_enabled': self.smart_enabled, 497 'smart_status': self.assessment, 498 'messages': self.messages, 499 'test_capabilities': self.test_capabilities.copy(), 500 'tests': [t.__getstate__() for t in self.tests] if self.tests else [], 501 'diagnostics': self.diagnostics.__getstate__(all_info), 502 'temperature': self.temperature, 503 'attributes': [attr.__getstate__() if attr else None for attr in self.attributes], 504 'if_attributes': self.if_attributes.__getstate__(all_info) if self.if_attributes else None, 505 } 506 if all_info: 507 state_dict.update({ 508 'name': self.name, 509 'path': self.dev_reference, 510 'serial': self.serial, 511 'is_ssd': self.is_ssd, 512 'rotation_rate': self.rotation_rate, 513 'capacity': self._capacity_human 514 }) 515 return state_dict 516 517 def __setstate__(self, state): 518 state['assessment'] = state['smart_status'] 519 del state['smart_status'] 520 self.__dict__.update(state) 521 522 def smart_toggle(self, action: str) -> Tuple[bool, List[str]]: 523 """ 524 A basic function to enable/disable SMART on device. 525 526 # Args: 527 * **action (str):** Can be either 'on'(for enabling) or 'off'(for disabling). 528 529 # Returns" 530 * **(bool):** Return True (if action succeded) else False 531 * **(List[str]):** None if option succeded else contains the error message. 532 """ 533 # Lets make the action verb all lower case 534 if self._interface == 'nvme': 535 return False, ['NVME devices do not currently support toggling SMART enabled'] 536 action_lower = action.lower() 537 if action_lower not in ['on', 'off']: 538 return False, ['Unsupported action {0}'.format(action)] 539 # Now lets check if the device's smart enabled status is already that of what 540 # the supplied action is intending it to be. If so then just return successfully 541 if self.smart_enabled: 542 if action_lower == 'on': 543 return True, [] 544 else: 545 if action_lower == 'off': 546 return True, [] 547 if self._interface is not None: 548 raw, returncode = self.smartctl.generic_call( 549 ['-s', action_lower, '-d', self._interface, self.dev_reference]) 550 else: 551 raw, returncode = self.smartctl.generic_call( 552 ['-s', action_lower, self.dev_reference]) 553 554 if returncode != 0: 555 return False, raw 556 # if everything worked out so far lets perform an update() and check the result 557 self.update() 558 if action_lower == 'off' and self.smart_enabled: 559 return False, ['Failed to turn SMART off.'] 560 if action_lower == 'on' and not self.smart_enabled: 561 return False, ['Failed to turn SMART on.'] 562 return True, [] 563 564 def all_attributes(self, print_fn=print): 565 """ 566 Prints the entire SMART attribute table, in a format similar to 567 the output of smartctl. 568 allows usage of custom print function via parameter print_fn by default uses print 569 """ 570 header_printed = False 571 for attr in self.attributes: 572 if attr is not None: 573 if not header_printed: 574 print_fn("{0:>3} {1:24}{2:4}{3:4}{4:4}{5:9}{6:8}{7:12}{8}" 575 .format('ID#', 'ATTRIBUTE_NAME', 'CUR', 'WST', 'THR', 'TYPE', 'UPDATED', 'WHEN_FAIL', 576 'RAW')) 577 header_printed = True 578 print_fn(attr) 579 if not header_printed: 580 print_fn('This device does not support SMART attributes.') 581 582 def all_selftests(self): 583 """ 584 Prints the entire SMART self-test log, in a format similar to 585 the output of smartctl. 586 """ 587 if self.tests: 588 all_tests = [] 589 if smartctl_type(self._interface) == 'scsi': 590 header = "{0:3}{1:17}{2:23}{3:7}{4:14}{5:15}".format( 591 'ID', 592 'Test Description', 593 'Status', 594 'Hours', 595 '1st_Error@LBA', 596 '[SK ASC ASCQ]' 597 ) 598 else: 599 header = ("{0:3}{1:17}{2:30}{3:5}{4:7}{5:17}".format( 600 'ID', 601 'Test_Description', 602 'Status', 603 'Left', 604 'Hours', 605 '1st_Error@LBA')) 606 all_tests.append(header) 607 for test in self.tests: 608 all_tests.append(str(test)) 609 610 return all_tests 611 else: 612 no_tests = 'No self-tests have been logged for this device.' 613 return no_tests 614 615 def _classify(self) -> str: 616 """ 617 Disambiguates generic device types ATA and SCSI into more specific 618 ATA, SATA, SAS, SAT and SCSI. 619 """ 620 621 fine_interface = self._interface or '' 622 # SCSI devices might be SCSI, SAS or SAT 623 # ATA device might be ATA or SATA 624 if fine_interface in ['scsi', 'ata'] or 'megaraid' in fine_interface: 625 if 'megaraid' in fine_interface: 626 if not 'sat+' in fine_interface: 627 test = 'sat'+fine_interface 628 else: 629 test = fine_interface 630 else: 631 test = 'sat' if fine_interface == 'scsi' else 'sata' 632 # Look for a SATA PHY to detect SAT and SATA 633 raw, returncode = self.smartctl.try_generic_call([ 634 '-d', 635 smartctl_type(test), 636 '-l', 637 'sataphy', 638 self.dev_reference]) 639 640 if returncode == 0 and 'GP Log 0x11' in raw[3]: 641 fine_interface = test 642 # If device type is still SCSI (not changed to SAT above), then 643 # check for a SAS PHY 644 if fine_interface in ['scsi'] or 'megaraid' in fine_interface: 645 raw, returncode = self.smartctl.try_generic_call([ 646 '-d', 647 smartctl_type(fine_interface), 648 '-l', 649 'sasphy', 650 self.dev_reference]) 651 if returncode == 0 and 'SAS SSP' in raw[4]: 652 fine_interface = 'sas' 653 # Some older SAS devices do not support the SAS PHY log command. 654 # For these, see if smartmontools reports a transport protocol. 655 else: 656 raw = self.smartctl.all(self.dev_reference, fine_interface) 657 658 for line in raw: 659 if 'Transport protocol' in line and 'SAS' in line: 660 fine_interface = 'sas' 661 662 return fine_interface 663 664 def _guess_smart_type(self, line): 665 """ 666 This function is not used in the generic wrapper, however the header 667 is defined so that it can be monkey-patched by another application. 668 """ 669 pass 670 671 def _make_smart_warnings(self): 672 """ 673 Parses an ATA/SATA SMART table for attributes with the 'when_failed' 674 value set. Generates an warning message for any such attributes and 675 updates the self-assessment value if necessary. 676 """ 677 if smartctl_type(self._interface) == 'scsi': 678 return 679 for attr in self.attributes: 680 if attr is not None: 681 if attr.when_failed == 'In_the_past': 682 warn_str = "{0} failed in the past with value {1}. [Threshold: {2}]".format( 683 attr.name, attr.worst, attr.thresh) 684 self.messages.append(warn_str) 685 if not self.assessment == 'FAIL': 686 self.assessment = 'WARN' 687 elif attr.when_failed == 'FAILING_NOW': 688 warn_str = "{0} is failing now with value {1}. [Threshold: {2}]".format( 689 attr.name, attr.value, attr.thresh) 690 self.assessment = 'FAIL' 691 self.messages.append(warn_str) 692 elif not attr.when_failed == '-': 693 warn_str = "{0} says it failed '{1}'. [V={2},W={3},T={4}]".format( 694 attr.name, attr.when_failed, attr.value, attr.worst, attr.thresh) 695 self.messages.append(warn_str) 696 if not self.assessment == 'FAIL': 697 self.assessment = 'WARN' 698 699 def get_selftest_result(self, output=None): 700 """ 701 Refreshes a device's `pySMART.device.Device.tests` attribute to obtain 702 the latest test results. If a new test result is obtained, its content 703 is returned. 704 705 # Args: 706 * **output (str, optional):** If set to 'str', the string 707 representation of the most recent test result will be returned, instead 708 of a `Test_Entry` object. 709 710 # Returns: 711 * **(int):** Return status code. One of the following: 712 * 0 - Success. Object (or optionally, string rep) is attached. 713 * 1 - Self-test in progress. Must wait for it to finish. 714 * 2 - No new test results. 715 * 3 - The Self-test was Aborted by host 716 * **(`Test_Entry` or str):** Most recent `Test_Entry` object (or 717 optionally it's string representation) if new data exists. Status 718 message string on failure. 719 * **(int):** Estimate progress percantage of the running SMART selftest, if known. 720 Otherwise 'None'. 721 """ 722 # SCSI self-test logs hold 20 entries while ATA logs hold 21 723 if smartctl_type(self._interface) == 'scsi': 724 maxlog = 20 725 else: 726 maxlog = 21 727 # If we looked only at the most recent test result we could be fooled 728 # by two short tests run close together (within the same hour) 729 # appearing identical. Comparing the length of the log adds some 730 # confidence until it maxes, as above. Comparing the least-recent test 731 # result greatly diminishes the chances that two sets of two tests each 732 # were run within an hour of themselves, but with 16-17 other tests run 733 # in between them. 734 if self.tests: 735 _first_entry = self.tests[0] 736 _len = len(self.tests) 737 _last_entry = self.tests[_len - 1] 738 else: 739 _len = 0 740 self.update() 741 # Since I have changed the update() parsing to DTRT to pickup currently 742 # running selftests we can now purely rely on that for self._test_running 743 # Thus check for that variable first and return if it is True with appropos message. 744 if self._test_running is True: 745 return 1, 'Self-test in progress. Please wait.', self._test_progress 746 # Check whether the list got longer (ie: new entry) 747 # If so return the newest test result 748 # If not, because it's max size already, check for new entries 749 if ( 750 (len(self.tests) != _len) or 751 ( 752 len == maxlog and 753 ( 754 _first_entry.type != self.tests[0].type or 755 _first_entry.hours != self.tests[0].hours or 756 _last_entry.type != self.tests[len(self.tests) - 1].type or 757 _last_entry.hours != self.tests[len( 758 self.tests) - 1].hours 759 ) 760 ) 761 ): 762 return ( 763 0 if 'Aborted' not in self.tests[0].status else 3, 764 str(self.tests[0]) if output == 'str' else self.tests[0], 765 None 766 ) 767 else: 768 return 2, 'No new self-test results found.', None 769 770 def abort_selftest(self): 771 """ 772 Aborts non-captive SMART Self Tests. Note that this command 773 will abort the Offline Immediate Test routine only if your disk 774 has the "Abort Offline collection upon new command" capability. 775 776 # Args: Nothing (just aborts directly) 777 778 # Returns: 779 * **(int):** The returncode of calling `smartctl -X device_path` 780 """ 781 return self.smartctl.test_stop(smartctl_type(self._interface), self.dev_reference) 782 783 def run_selftest(self, test_type, ETA_type='date'): 784 """ 785 Instructs a device to begin a SMART self-test. All tests are run in 786 'offline' / 'background' mode, allowing normal use of the device while 787 it is being tested. 788 789 # Args: 790 * **test_type (str):** The type of test to run. Accepts the following 791 (not case sensitive): 792 * **short** - Brief electo-mechanical functionality check. 793 Generally takes 2 minutes or less. 794 * **long** - Thorough electro-mechanical functionality check, 795 including complete recording media scan. Generally takes several 796 hours. 797 * **conveyance** - Brief test used to identify damage incurred in 798 shipping. Generally takes 5 minutes or less. **This test is not 799 supported by SAS or SCSI devices.** 800 * **offline** - Runs SMART Immediate Offline Test. The effects of 801 this test are visible only in that it updates the SMART Attribute 802 values, and if errors are found they will appear in the SMART error 803 log, visible with the '-l error' option to smartctl. **This test is 804 not supported by SAS or SCSI devices in pySMART use cli smartctl for 805 running 'offline' selftest (runs in foreground) on scsi devices.** 806 * **ETA_type** - Format to return the estimated completion time/date 807 in. Default is 'date'. One could otherwise specidy 'seconds'. 808 Again only for ATA devices. 809 810 # Returns: 811 * **(int):** Return status code. One of the following: 812 * 0 - Self-test initiated successfully 813 * 1 - Previous self-test running. Must wait for it to finish. 814 * 2 - Unknown or unsupported (by the device) test type requested. 815 * 3 - Unspecified smartctl error. Self-test not initiated. 816 * **(str):** Return status message. 817 * **(str)/(float):** Estimated self-test completion time if a test is started. 818 The optional argument of 'ETA_type' (see above) controls the return type. 819 if 'ETA_type' == 'date' then a date string is returned else seconds(float) 820 is returned. 821 Note: The self-test completion time can only be obtained for ata devices. 822 Otherwise 'None'. 823 """ 824 # Lets call get_selftest_result() here since it does an update() and 825 # checks for an existing selftest is running or not, this way the user 826 # can issue a test from the cli and this can still pick that up 827 # Also note that we do not need to obtain the results from this as the 828 # data is already stored in the Device class object's variables 829 self.get_selftest_result() 830 if self._test_running: 831 return 1, 'Self-test in progress. Please wait.', self._test_ECD 832 test_type = test_type.lower() 833 interface = smartctl_type(self._interface) 834 try: 835 if not self.test_capabilities[test_type]: 836 return ( 837 2, 838 "Device {0} does not support the '{1}' test ".format( 839 self.name, test_type), 840 None 841 ) 842 except KeyError: 843 return 2, "Unknown test type '{0}' requested.".format(test_type), None 844 845 raw, rc = self.smartctl.test_start( 846 interface, test_type, self.dev_reference) 847 _success = False 848 _running = False 849 for line in raw: 850 if 'has begun' in line: 851 _success = True 852 self._test_running = True 853 if 'aborting current test' in line: 854 _running = True 855 try: 856 self._test_progress = 100 - \ 857 int(line.split('(')[-1].split('%')[0]) 858 except ValueError: 859 pass 860 861 if _success and 'complete after' in line: 862 self._test_ECD = line[25:].rstrip() 863 if ETA_type == 'seconds': 864 self._test_ECD = mktime( 865 strptime(self._test_ECD, '%a %b %d %H:%M:%S %Y')) - time() 866 self._test_progress = 0 867 if _success: 868 return 0, 'Self-test started successfully', self._test_ECD 869 else: 870 if _running: 871 return 1, 'Self-test already in progress. Please wait.', self._test_ECD 872 else: 873 return 3, 'Unspecified Error. Self-test not started.', None 874 875 def run_selftest_and_wait(self, test_type, output=None, polling=5, progress_handler=None): 876 """ 877 This is essentially a wrapper around run_selftest() such that we 878 call self.run_selftest() and wait on the running selftest till 879 it finished before returning. 880 The above holds true for all pySMART supported tests with the 881 exception of the 'offline' test (ATA only) as it immediately 882 returns, since the entire test only affects the smart error log 883 (if any errors found) and updates the SMART attributes. Other 884 than that it is not visibile anywhere else, so we start it and 885 simply return. 886 # Args: 887 * **test_type (str):** The type of test to run. Accepts the following 888 (not case sensitive): 889 * **short** - Brief electo-mechanical functionality check. 890 Generally takes 2 minutes or less. 891 * **long** - Thorough electro-mechanical functionality check, 892 including complete recording media scan. Generally takes several 893 hours. 894 * **conveyance** - Brief test used to identify damage incurred in 895 shipping. Generally takes 5 minutes or less. **This test is not 896 supported by SAS or SCSI devices.** 897 * **offline** - Runs SMART Immediate Offline Test. The effects of 898 this test are visible only in that it updates the SMART Attribute 899 values, and if errors are found they will appear in the SMART error 900 log, visible with the '-l error' option to smartctl. **This test is 901 not supported by SAS or SCSI devices in pySMART use cli smartctl for 902 running 'offline' selftest (runs in foreground) on scsi devices.** 903 * **output (str, optional):** If set to 'str', the string 904 representation of the most recent test result will be returned, 905 instead of a `Test_Entry` object. 906 * **polling (int, default=5):** The time duration to sleep for between 907 checking for test_results and progress. 908 * **progress_handler (function, optional):** This if provided is called 909 with self._test_progress as the supplied argument everytime a poll to 910 check the status of the selftest is done. 911 # Returns: 912 * **(int):** Return status code. One of the following: 913 * 0 - Self-test executed and finished successfully 914 * 1 - Previous self-test running. Must wait for it to finish. 915 * 2 - Unknown or illegal test type requested. 916 * 3 - The Self-test was Aborted by host 917 * 4 - Unspecified smartctl error. Self-test not initiated. 918 * **(`Test_Entry` or str):** Most recent `Test_Entry` object (or 919 optionally it's string representation) if new data exists. Status 920 message string on failure. 921 """ 922 test_initiation_result = self.run_selftest(test_type) 923 if test_initiation_result[0] != 0: 924 return test_initiation_result[:2] 925 if test_type == 'offline': 926 self._test_running = False 927 # if not then the test initiated correctly and we can start the polling. 928 # For now default 'polling' value is 5 seconds if not specified by the user 929 930 # Do an initial check, for good measure. 931 # In the probably impossible case that self._test_running is instantly False... 932 selftest_results = self.get_selftest_result(output=output) 933 while self._test_running: 934 if selftest_results[0] != 1: 935 # the selftest is run and finished lets return with the results 936 break 937 # Otherwise see if we are provided with the progress_handler to update progress 938 if progress_handler is not None: 939 progress_handler( 940 selftest_results[2] if selftest_results[2] is not None else 50) 941 # Now sleep 'polling' seconds before checking the progress again 942 sleep(polling) 943 944 # Check after the sleep to ensure we return the right result, and not an old one. 945 selftest_results = self.get_selftest_result(output=output) 946 947 # Now if (selftes_results[0] == 2) i.e No new selftest (because the same 948 # selftest was run twice within the last hour) but we know for a fact that 949 # we just ran a new selftest then just return the latest entry in self.tests 950 if selftest_results[0] == 2: 951 selftest_return_value = 0 if 'Aborted' not in self.tests[0].status else 3 952 return selftest_return_value, str(self.tests[0]) if output == 'str' else self.tests[0] 953 return selftest_results[:2] 954 955 def update(self): 956 """ 957 Queries for device information using smartctl and updates all 958 class members, including the SMART attribute table and self-test log. 959 Can be called at any time to refresh the `pySMART.device.Device` 960 object's data content. 961 """ 962 # set temperature back to None so that if update() is called more than once 963 # any logic that relies on self.temperature to be None to rescan it works.it 964 self._temperature = None 965 # same for temperatures 966 self.temperatures = {} 967 if self.abridged: 968 interface = None 969 raw = self.smartctl.info(self.dev_reference) 970 971 else: 972 interface = smartctl_type(self._interface) 973 raw = self.smartctl.all( 974 self.dev_reference, interface) 975 976 parse_self_tests = False 977 parse_running_test = False 978 parse_ascq = False 979 polling_minute_type = None 980 message = '' 981 self.tests = [] 982 self._test_running = False 983 self._test_progress = None 984 # Lets skip the first couple of non-useful lines 985 _stdout = raw[4:] 986 987 ####################################### 988 # Encoding fixing # 989 ####################################### 990 # In some scenarios, smartctl returns some lines with a different/strange encoding 991 # This is a workaround to fix that 992 for i, line in enumerate(_stdout): 993 # character ' ' (U+202F) should be removed 994 _stdout[i] = line.replace('\u202f', '') 995 996 ####################################### 997 # Dedicated interface attributes # 998 ####################################### 999 if AtaAttributes.has_compatible_data(iter(_stdout)): 1000 self.if_attributes = AtaAttributes(iter(_stdout)) 1001 1002 elif self.dev_interface == 'nvme': 1003 self.if_attributes = NvmeAttributes(iter(_stdout)) 1004 1005 # Get Tests 1006 for test in self.if_attributes.tests: 1007 self.tests.append(TestEntry('nvme', test.num, test.description, test.status, test.powerOnHours, 1008 test.failingLBA, nsid=test.nsid, segment=test.seg, sct=test.sct, code=test.code, remain=100-test.progress)) 1009 1010 # Set running test 1011 if any(test.status == 'Running' for test in self.if_attributes.tests): 1012 self._test_running = True 1013 self._test_progress = self.if_attributes.tests[0].progress 1014 else: 1015 self._test_running = False 1016 self._test_progress = None 1017 1018 else: 1019 self.if_attributes = None 1020 1021 ####################################### 1022 # Global / generic attributes # 1023 ####################################### 1024 stdout_iter = iter(_stdout) 1025 for line in stdout_iter: 1026 if line.strip() == '': # Blank line stops sub-captures 1027 if parse_self_tests is True: 1028 parse_self_tests = False 1029 if parse_ascq: 1030 parse_ascq = False 1031 self.messages.append(message) 1032 if parse_ascq: 1033 message += ' ' + line.lstrip().rstrip() 1034 if parse_self_tests: 1035 # Detect Test Format 1036 1037 ## SCSI/SAS FORMAT ## 1038 # Example smartctl output 1039 # SMART Self-test log 1040 # Num Test Status segment LifeTime LBA_first_err [SK ASC ASQ] 1041 # Description number (hours) 1042 # # 1 Background short Completed - 33124 - [- - -] 1043 format_scsi = re.compile( 1044 r'^[#\s]*(\d+)\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s+\[([^\s]+)\s+([^\s]+)\s+([^\s]+)\]$').match(line) 1045 1046 ## ATA FORMAT ## 1047 # Example smartctl output: 1048 # SMART Self-test log structure revision number 1 1049 # Num Test_Description Status Remaining LifeTime(hours) LBA_of_first_error 1050 # # 1 Extended offline Completed without error 00% 46660 - 1051 format_ata = re.compile( 1052 r'^[#\s]*(\d+)\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{1,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])$').match(line) 1053 1054 if format_scsi is not None: 1055 format = 'scsi' 1056 parsed = format_scsi.groups() 1057 num = int(parsed[0]) 1058 test_type = parsed[1] 1059 status = parsed[2] 1060 segment = parsed[3] 1061 hours = parsed[4] 1062 lba = parsed[5] 1063 sense = parsed[6] 1064 asc = parsed[7] 1065 ascq = parsed[8] 1066 self.tests.append(TestEntry( 1067 format, 1068 num, 1069 test_type, 1070 status, 1071 hours, 1072 lba, 1073 segment=segment, 1074 sense=sense, 1075 asc=asc, 1076 ascq=ascq 1077 )) 1078 elif format_ata is not None: 1079 ## ATA FORMAT ## 1080 format = 'ata' 1081 parsed = format_ata.groups() 1082 num = parsed[0] 1083 test_type = parsed[1] 1084 status = parsed[2] 1085 remain = parsed[3] 1086 hours = parsed[4] 1087 lba = parsed[5] 1088 1089 try: 1090 num = int(num) 1091 except: 1092 num = None 1093 1094 self.tests.append( 1095 TestEntry(format, num, test_type, status, 1096 hours, lba, remain=remain) 1097 ) 1098 else: 1099 pass 1100 1101 # Basic device information parsing 1102 if any_in(line, 'Device Model', 'Product', 'Model Number'): 1103 self.model = line.split(':')[1].lstrip().rstrip() 1104 self._guess_smart_type(line.lower()) 1105 continue 1106 1107 if 'Model Family' in line: 1108 self.family = line.split(':')[1].strip() 1109 self._guess_smart_type(line.lower()) 1110 continue 1111 1112 if 'LU WWN' in line: 1113 self._guess_smart_type(line.lower()) 1114 continue 1115 1116 if any_in(line, 'Serial Number', 'Serial number'): 1117 try: 1118 self.serial = line.split(':')[1].split()[0].rstrip() 1119 except IndexError: 1120 # Serial reported empty 1121 self.serial = "" 1122 continue 1123 1124 vendor = re.compile(r'^Vendor:\s+(\w+)').match(line) 1125 if vendor is not None: 1126 self._vendor = vendor.groups()[0] 1127 1128 if any_in(line, 'Firmware Version', 'Revision'): 1129 self.firmware = line.split(':')[1].strip() 1130 1131 if any_in(line, 'User Capacity', 'Total NVM Capacity', 'Namespace 1 Size/Capacity'): 1132 # TODO: support for multiple NVMe namespaces 1133 m = re.match( 1134 r'.*:\s+([\d,. \u2019\u00a0]+)\s\D*\[?([^\]]+)?\]?', line.strip()) 1135 1136 if m is not None: 1137 tmp = m.groups() 1138 if tmp[0] == ' ': 1139 # This capacity is set to 0, skip it 1140 continue 1141 1142 self._capacity = int( 1143 tmp[0].strip().replace(',', '').replace('.', '').replace(' ', '').replace('\u2019', '').replace('\u00a0', '')) 1144 1145 if len(tmp) == 2 and tmp[1] is not None: 1146 self._capacity_human = tmp[1].strip().replace(',', '.') 1147 1148 if 'SMART support' in line: 1149 # self.smart_capable = 'Available' in line 1150 # self.smart_enabled = 'Enabled' in line 1151 # Since this line repeats twice the above method is flawed 1152 # Lets try the following instead, it is a bit redundant but 1153 # more robust. 1154 if any_in(line, 'Unavailable', 'device lacks SMART capability'): 1155 self.smart_capable = False 1156 self.smart_enabled = False 1157 elif 'Enabled' in line: 1158 self.smart_enabled = True 1159 elif 'Disabled' in line: 1160 self.smart_enabled = False 1161 elif any_in(line, 'Available', 'device has SMART capability'): 1162 self.smart_capable = True 1163 continue 1164 1165 if 'does not support SMART' in line: 1166 self.smart_capable = False 1167 self.smart_enabled = False 1168 continue 1169 1170 if 'Rotation Rate' in line: 1171 if 'Solid State Device' in line: 1172 self.is_ssd = True 1173 elif 'rpm' in line: 1174 self.is_ssd = False 1175 try: 1176 self.rotation_rate = int( 1177 line.split(':')[1].lstrip().rstrip()[:-4]) 1178 except ValueError: 1179 # Cannot parse the RPM? Assigning None instead 1180 self.rotation_rate = None 1181 continue 1182 1183 if 'SMART overall-health self-assessment' in line: # ATA devices 1184 if line.split(':')[1].strip() == 'PASSED': 1185 self.assessment = 'PASS' 1186 else: 1187 self.assessment = 'FAIL' 1188 continue 1189 1190 if 'SMART Health Status' in line: # SCSI devices 1191 if line.split(':')[1].strip() == 'OK': 1192 self.assessment = 'PASS' 1193 else: 1194 self.assessment = 'FAIL' 1195 parse_ascq = True # Set flag to capture status message 1196 message = line.split(':')[1].lstrip().rstrip() 1197 continue 1198 1199 # Parse SMART test capabilities (ATA only) 1200 # Note: SCSI does not list this but and allows for only 'offline', 'short' and 'long' 1201 if 'SMART execute Offline immediate' in line: 1202 self.test_capabilities['offline'] = 'No' not in line 1203 continue 1204 1205 if 'Conveyance Self-test supported' in line: 1206 self.test_capabilities['conveyance'] = 'No' not in line 1207 continue 1208 1209 if 'Selective Self-test supported' in line: 1210 self.test_capabilities['selective'] = 'No' not in line 1211 continue 1212 1213 if 'Self-test supported' in line: 1214 self.test_capabilities['short'] = 'No' not in line 1215 self.test_capabilities['long'] = 'No' not in line 1216 continue 1217 1218 # Parse SMART test capabilities (NVMe only) 1219 if 'Optional Admin Commands' in line: 1220 if 'Self_Test' in line: 1221 self.test_capabilities['short'] = True 1222 self.test_capabilities['long'] = True 1223 1224 if 'Short self-test routine' in line: 1225 polling_minute_type = 'short' 1226 continue 1227 if 'Extended self-test routine' in line: 1228 polling_minute_type = 'long' 1229 continue 1230 if 'Conveyance self-test routine' in line: 1231 polling_minute_type = 'conveyance' 1232 continue 1233 if 'recommended polling time:' in line: 1234 self.test_polling_time[polling_minute_type] = float( 1235 re.sub("[^0-9]", "", line) 1236 ) 1237 continue 1238 1239 # For some reason smartctl does not show a currently running test 1240 # for 'ATA' in the Test log so I just have to catch it this way i guess! 1241 # For 'scsi' I still do it since it is the only place I get % remaining in scsi 1242 if 'Self-test execution status' in line: 1243 if 'progress' in line: 1244 self._test_running = True 1245 # for ATA the "%" remaining is on the next line 1246 # thus set the parse_running_test flag and move on 1247 parse_running_test = True 1248 elif '%' in line: 1249 # for scsi the progress is on the same line 1250 # so we can just parse it and move on 1251 self._test_running = True 1252 try: 1253 self._test_progress = 100 - \ 1254 int(line.split('%')[0][-3:].strip()) 1255 except ValueError: 1256 pass 1257 continue 1258 if parse_running_test is True: 1259 try: 1260 self._test_progress = 100 - \ 1261 int(line.split('%')[0][-3:].strip()) 1262 except ValueError: 1263 pass 1264 parse_running_test = False 1265 1266 if "Self-test log" in line: 1267 parse_self_tests = True # Set flag to capture test entries 1268 continue 1269 1270 ####################################### 1271 # SCSI only # 1272 ####################################### 1273 # 1274 # Everything from here on is parsing SCSI information that takes 1275 # the place of similar ATA SMART information 1276 if 'used endurance' in line: 1277 pct = int(line.split(':')[1].strip()[:-1]) 1278 self.diagnostics.Life_Left = 100 - pct 1279 continue 1280 1281 if 'Specified cycle count' in line: 1282 self.diagnostics.Start_Stop_Spec = int( 1283 line.split(':')[1].strip()) 1284 continue 1285 1286 if 'Accumulated start-stop cycles' in line: 1287 self.diagnostics.Start_Stop_Cycles = int( 1288 line.split(':')[1].strip()) 1289 if self.diagnostics.Start_Stop_Spec and self.diagnostics.Start_Stop_Spec != 0: 1290 self.diagnostics.Start_Stop_Pct_Left = int(round( 1291 100 - (self.diagnostics.Start_Stop_Cycles / 1292 self.diagnostics.Start_Stop_Spec), 0)) 1293 continue 1294 1295 if 'Specified load-unload count' in line: 1296 self.diagnostics.Load_Cycle_Spec = int( 1297 line.split(':')[1].strip()) 1298 continue 1299 1300 if 'Accumulated load-unload cycles' in line: 1301 self.diagnostics.Load_Cycle_Count = int( 1302 line.split(':')[1].strip()) 1303 if self.diagnostics.Load_Cycle_Spec and self.diagnostics.Load_Cycle_Spec != 0: 1304 self.diagnostics.Load_Cycle_Pct_Left = int(round( 1305 100 - (self.diagnostics.Load_Cycle_Count / 1306 self.diagnostics.Load_Cycle_Spec), 0)) 1307 continue 1308 1309 if 'Elements in grown defect list' in line: 1310 self.diagnostics.Reallocated_Sector_Ct = int( 1311 line.split(':')[1].strip()) 1312 continue 1313 1314 if 'read:' in line: 1315 line_ = ' '.join(line.split()).split(' ') 1316 if line_[1] == '0' and line_[2] == '0' and line_[3] == '0' and line_[4] == '0': 1317 self.diagnostics.Corrected_Reads = 0 1318 elif line_[4] == '0': 1319 self.diagnostics.Corrected_Reads = int( 1320 line_[1]) + int(line_[2]) + int(line_[3]) 1321 else: 1322 self.diagnostics.Corrected_Reads = int(line_[4]) 1323 self.diagnostics._Reads_GB = float(line_[6].replace(',', '.')) 1324 self.diagnostics._Uncorrected_Reads = int(line_[7]) 1325 continue 1326 1327 if 'write:' in line: 1328 line_ = ' '.join(line.split()).split(' ') 1329 if (line_[1] == '0' and line_[2] == '0' and 1330 line_[3] == '0' and line_[4] == '0'): 1331 self.diagnostics.Corrected_Writes = 0 1332 elif line_[4] == '0': 1333 self.diagnostics.Corrected_Writes = int( 1334 line_[1]) + int(line_[2]) + int(line_[3]) 1335 else: 1336 self.diagnostics.Corrected_Writes = int(line_[4]) 1337 self.diagnostics._Writes_GB = float(line_[6].replace(',', '.')) 1338 self.diagnostics._Uncorrected_Writes = int(line_[7]) 1339 continue 1340 1341 if 'verify:' in line: 1342 line_ = ' '.join(line.split()).split(' ') 1343 if (line_[1] == '0' and line_[2] == '0' and 1344 line_[3] == '0' and line_[4] == '0'): 1345 self.diagnostics.Corrected_Verifies = 0 1346 elif line_[4] == '0': 1347 self.diagnostics.Corrected_Verifies = int( 1348 line_[1]) + int(line_[2]) + int(line_[3]) 1349 else: 1350 self.diagnostics.Corrected_Verifies = int(line_[4]) 1351 self.diagnostics._Verifies_GB = float( 1352 line_[6].replace(',', '.')) 1353 self.diagnostics._Uncorrected_Verifies = int(line_[7]) 1354 continue 1355 1356 if 'non-medium error count' in line: 1357 self.diagnostics.Non_Medium_Errors = int( 1358 line.split(':')[1].strip()) 1359 continue 1360 1361 if 'Accumulated power on time' in line: 1362 self.diagnostics.Power_On_Hours = int( 1363 line.split(':')[1].split(' ')[1]) 1364 continue 1365 1366 if 'Current Drive Temperature' in line or ('Temperature:' in 1367 line and interface == 'nvme'): 1368 try: 1369 self._temperature = int( 1370 line.split(':')[-1].strip().split()[0]) 1371 1372 if 'fahrenheit' in line.lower(): 1373 self._temperature = int( 1374 (self.temperature - 32) * 5 / 9) 1375 1376 except ValueError: 1377 pass 1378 1379 continue 1380 1381 if 'Temperature Sensor ' in line: 1382 try: 1383 match = re.search( 1384 r'Temperature\sSensor\s([0-9]+):\s+(-?[0-9]+)', line) 1385 if match: 1386 (tempsensor_number_s, tempsensor_value_s) = match.group(1, 2) 1387 tempsensor_number = int(tempsensor_number_s) 1388 tempsensor_value = int(tempsensor_value_s) 1389 1390 if 'fahrenheit' in line.lower(): 1391 tempsensor_value = int( 1392 (tempsensor_value - 32) * 5 / 9) 1393 1394 self.temperatures[tempsensor_number] = tempsensor_value 1395 if self.temperature is None or tempsensor_number == 0: 1396 self._temperature = tempsensor_value 1397 except ValueError: 1398 pass 1399 1400 continue 1401 1402 ####################################### 1403 # Common values # 1404 ####################################### 1405 1406 # Sector sizes 1407 if 'Sector Sizes' in line: # ATA 1408 m = re.match( 1409 r'.* (\d+) bytes logical,\s*(\d+) bytes physical', line) 1410 if m: 1411 self.logical_sector_size = int(m.group(1)) 1412 self.physical_sector_size = int(m.group(2)) 1413 # set diagnostics block size to physical sector size 1414 self.diagnostics._block_size = self.physical_sector_size 1415 continue 1416 if 'Logical block size:' in line: # SCSI 1/2 1417 self.logical_sector_size = int( 1418 line.split(':')[1].strip().split(' ')[0]) 1419 # set diagnostics block size to logical sector size 1420 self.diagnostics._block_size = self.logical_sector_size 1421 continue 1422 if 'Physical block size:' in line: # SCSI 2/2 1423 self.physical_sector_size = int( 1424 line.split(':')[1].strip().split(' ')[0]) 1425 continue 1426 if 'Namespace 1 Formatted LBA Size' in line: # NVMe 1427 # Note: we will assume that there is only one namespace 1428 self.logical_sector_size = int( 1429 line.split(':')[1].strip().split(' ')[0]) 1430 continue 1431 1432 if not self.abridged: 1433 if not interface == 'scsi': 1434 # Parse the SMART table for below-threshold attributes and create 1435 # corresponding warnings for non-SCSI disks 1436 self._make_smart_warnings() 1437 else: 1438 # If not obtained Power_On_Hours above, make a direct attempt to extract power on 1439 # hours from the background scan results log. 1440 if self.smart_enabled and self.diagnostics.Power_On_Hours is None: 1441 raw, returncode = self.smartctl.generic_call( 1442 [ 1443 '-d', 1444 'scsi', 1445 '-l', 1446 'background', 1447 self.dev_reference 1448 ]) 1449 1450 for line in raw: 1451 if 'power on time' in line: 1452 self.diagnostics.Power_On_Hours = int( 1453 line.split(':')[1].split(' ')[1]) 1454 1455 # Now that we have finished the update routine, if we did not find a runnning selftest 1456 # nuke the self._test_ECD and self._test_progress 1457 if self._test_running is False: 1458 self._test_ECD = None 1459 self._test_progress = None
Represents any device attached to an internal storage interface, such as a hard drive or DVD-ROM, and detected by smartmontools. Includes eSATA (considered SATA) but excludes other external devices (USB, Firewire).
88 def __init__(self, name: str, interface: Optional[str] = None, abridged: bool = False, smart_options: Union[str, List[str], None] = None, smartctl: Smartctl = SMARTCTL): 89 """Instantiates and initializes the `pySMART.device.Device`.""" 90 if not ( 91 interface is None or 92 smartctl_isvalid_type(interface.lower()) 93 ): 94 raise ValueError( 95 'Unknown interface: {0} specified for {1}'.format(interface, name)) 96 self.abridged = abridged or interface == 'UNKNOWN INTERFACE' 97 if smart_options is not None: 98 if isinstance(smart_options, str): 99 smart_options = smart_options.split(' ') 100 smartctl.add_options(smart_options) 101 self.smartctl = smartctl 102 """ 103 """ 104 self.name: str = name.replace('/dev/', '').replace('nvd', 'nvme') 105 """ 106 **(str):** Device's hardware ID, without the '/dev/' prefix. 107 (ie: sda (Linux), pd0 (Windows)) 108 """ 109 self.family: Optional[str] = None 110 """**(str):** Device's family (if any).""" 111 self.model: Optional[str] = None 112 """**(str):** Device's model number (if any).""" 113 self.serial: Optional[str] = None 114 """**(str):** Device's serial number (if any).""" 115 self._vendor: Optional[str] = None 116 """**(str):** Device's vendor (if any).""" 117 self._interface: Optional[str] = None if interface == 'UNKNOWN INTERFACE' else interface 118 """ 119 **(str):** Device's interface type. Must be one of: 120 * **ATA** - Advanced Technology Attachment 121 * **SATA** - Serial ATA 122 * **SCSI** - Small Computer Systems Interface 123 * **SAS** - Serial Attached SCSI 124 * **SAT** - SCSI-to-ATA Translation (SATA device plugged into a 125 SAS port) 126 * **CSMI** - Common Storage Management Interface (Intel ICH / 127 Matrix RAID) 128 Generally this should not be specified to allow auto-detection to 129 occur. Otherwise, this value overrides the auto-detected type and could 130 produce unexpected or no data. 131 """ 132 self._capacity: Optional[int] = None 133 """**(str):** Device's user capacity as reported directly by smartctl (RAW).""" 134 self._capacity_human: Optional[str] = None 135 """**(str):** Device's user capacity (human readable) as reported directly by smartctl (RAW).""" 136 self.firmware: Optional[str] = None 137 """**(str):** Device's firmware version.""" 138 self.smart_capable: bool = 'nvme' in self.name 139 """ 140 **(bool):** True if the device has SMART Support Available. 141 False otherwise. This is useful for VMs amongst other things. 142 """ 143 self.smart_enabled: bool = 'nvme' in self.name 144 """ 145 **(bool):** True if the device supports SMART (or SCSI equivalent) and 146 has the feature set enabled. False otherwise. 147 """ 148 self.assessment: Optional[str] = None 149 """ 150 **(str):** SMART health self-assessment as reported by the device. 151 """ 152 self.messages: List[str] = [] 153 """ 154 **(list of str):** Contains any SMART warnings or other error messages 155 reported by the device (ie: ascq codes). 156 """ 157 self.is_ssd: bool = True if 'nvme' in self.name else False 158 """ 159 **(bool):** True if this device is a Solid State Drive. 160 False otherwise. 161 """ 162 self.rotation_rate: Optional[int] = None 163 """ 164 **(int):** The Roatation Rate of the Drive if it is not a SSD. 165 The Metric is RPM. 166 """ 167 self.test_capabilities = { 168 'offline': False, # SMART execute Offline immediate (ATA only) 169 'short': 'nvme' not in self.name, # SMART short Self-test 170 'long': 'nvme' not in self.name, # SMART long Self-test 171 'conveyance': False, # SMART Conveyance Self-Test (ATA only) 172 'selective': False, # SMART Selective Self-Test (ATA only) 173 } 174 # Note have not included 'offline' test for scsi as it runs in the foregorund 175 # mode. While this may be beneficial to us in someways it is against the 176 # general layout and pattern that the other tests issued using pySMART are 177 # followed hence not doing it currently 178 """ 179 **(dict): ** This dictionary contains key == 'Test Name' and 180 value == 'True/False' of self-tests that this device is capable of. 181 """ 182 # Note: The above are just default values and can/will be changed 183 # upon update() when the attributes and type of the disk is actually 184 # determined. 185 self.test_polling_time = { 186 'short': 10, 187 'long': 1000, 188 'conveyance': 20, 189 } 190 """ 191 **(dict): ** This dictionary contains key == 'Test Name' and 192 value == int of approximate times to run each test type that this 193 device is capable of. 194 """ 195 # Note: The above are just default values and can/will be changed 196 # upon update() when the attributes and type of the disk is actually 197 # determined. 198 self.tests: List[TestEntry] = [] 199 """ 200 **(list of `TestEntry`):** Contains the complete SMART self-test log 201 for this device, as provided by smartctl. 202 """ 203 self._test_running = False 204 """ 205 **(bool):** True if a self-test is currently being run. 206 False otherwise. 207 """ 208 self._test_ECD = None 209 """ 210 **(str):** Estimated completion time of the running SMART selftest. 211 Not provided by SAS/SCSI devices. 212 """ 213 self._test_progress = None 214 """ 215 **(int):** Estimate progress percantage of the running SMART selftest. 216 """ 217 self.diagnostics: Diagnostics = Diagnostics() 218 """ 219 **Diagnostics** Contains parsed and processed diagnostic information 220 extracted from the SMART information. Currently only populated for 221 SAS and SCSI devices, since ATA/SATA SMART attributes are manufacturer 222 proprietary. 223 """ 224 self._temperature: Optional[int] = None 225 """ 226 **(int or None): Since SCSI disks do not report attributes like ATA ones 227 we need to grep/regex the shit outta the normal "smartctl -a" output. 228 In case the device have more than one temperature sensor the first value 229 will be stored here too. 230 Note: Temperatures are always in Celsius (if possible). 231 """ 232 self.temperatures: Dict[int, int] = {} 233 """ 234 **(dict of int): NVMe disks usually report multiple temperatures, which 235 will be stored here if available. Keys are sensor numbers as reported in 236 output data. 237 Note: Temperatures are always in Celsius (if possible). 238 """ 239 self.logical_sector_size: Optional[int] = None 240 """ 241 **(int):** The logical sector size of the device (or LBA). 242 """ 243 self.physical_sector_size: Optional[int] = None 244 """ 245 **(int):** The physical sector size of the device. 246 """ 247 self.if_attributes: Union[None, NvmeAttributes, AtaAttributes] = None 248 """ 249 **(NvmeAttributes):** This object may vary for each device interface attributes. 250 It will store all data obtained from smartctl 251 """ 252 253 if self.name is None: 254 warnings.warn( 255 "\nDevice '{0}' does not exist! This object should be destroyed.".format( 256 name) 257 ) 258 return 259 # If no interface type was provided, scan for the device 260 # Lets do this only for the non-abridged case 261 # (we can work with no interface for abridged case) 262 elif self._interface is None and not self.abridged: 263 logger.trace( 264 "Determining interface of disk: {0}".format(self.name)) 265 raw, returncode = self.smartctl.generic_call( 266 ['-d', 'test', self.dev_reference]) 267 268 if len(raw) > 0: 269 # I do not like this parsing logic but it works for now! 270 # just for reference _stdout.split('\n') gets us 271 # something like 272 # [ 273 # ...copyright string..., 274 # '', 275 # "/dev/ada2: Device of type 'atacam' [ATA] detected", 276 # "/dev/ada2: Device of type 'atacam' [ATA] opened", 277 # '' 278 # ] 279 # The above example should be enough for anyone to understand the line below 280 try: 281 for line in reversed(raw): 282 if "opened" in line: 283 self._interface = line.split("'")[1] 284 285 if self._interface == "nvme": # if nvme set SMART to true 286 self.smart_capable = True 287 self.smart_enabled = True 288 289 break 290 except: 291 # for whatever reason we could not get the interface type 292 # we should mark this as an `abbridged` case and move on 293 self._interface = None 294 self.abbridged = True 295 # TODO: Uncomment the classify call if we ever find out that we need it 296 # Disambiguate the generic interface to a specific type 297 # self._classify() 298 else: 299 warnings.warn( 300 "\nDevice '{0}' does not exist! This object should be destroyed.".format( 301 name) 302 ) 303 return 304 # If a valid device was detected, populate its information 305 # OR if in unabridged mode, then do it even without interface info 306 if self._interface is not None or self.abridged: 307 self.update()
Instantiates and initializes the Device
.
(bool): True if the device has SMART Support Available. False otherwise. This is useful for VMs amongst other things.
(bool): True if the device supports SMART (or SCSI equivalent) and has the feature set enabled. False otherwise.
(list of str): Contains any SMART warnings or other error messages reported by the device (ie: ascq codes).
(int): The Roatation Rate of the Drive if it is not a SSD. The Metric is RPM.
*(dict): * This dictionary contains key == 'Test Name' and value == 'True/False' of self-tests that this device is capable of.
*(dict): * This dictionary contains key == 'Test Name' and value == int of approximate times to run each test type that this device is capable of.
(list of TestEntry
): Contains the complete SMART self-test log
for this device, as provided by smartctl.
Diagnostics Contains parsed and processed diagnostic information extracted from the SMART information. Currently only populated for SAS and SCSI devices, since ATA/SATA SMART attributes are manufacturer proprietary.
**(dict of int): NVMe disks usually report multiple temperatures, which will be stored here if available. Keys are sensor numbers as reported in output data. Note: Temperatures are always in Celsius (if possible).
(NvmeAttributes): This object may vary for each device interface attributes. It will store all data obtained from smartctl
309 @property 310 def attributes(self) -> List[Optional[Attribute]]: 311 """Returns the SMART attributes of the device. 312 Note: This is only filled with ATA/SATA attributes. SCSI/SAS/NVMe devices will have empty lists!! 313 @deprecated: Use `if_attributes` instead. 314 315 Returns: 316 list of `Attribute`: The SMART attributes of the device. 317 """ 318 if self.if_attributes is None or not isinstance(self.if_attributes, AtaAttributes): 319 return [None] * 256 320 else: 321 return self.if_attributes.legacyAttributes
Returns the SMART attributes of the device.
Note: This is only filled with ATA/SATA attributes. SCSI/SAS/NVMe devices will have empty lists!!
@deprecated: Use if_attributes
instead.
Returns:
list of Attribute
: The SMART attributes of the device.
323 @property 324 def dev_interface(self) -> Optional[str]: 325 """Returns the internal interface type of the device. 326 It may not be the same as the interface type as used by smartctl. 327 328 Returns: 329 str: The interface type of the device. (example: ata, scsi, nvme) 330 None if the interface type could not be determined. 331 """ 332 # Try to get the fine-tuned interface type 333 fineType = self._classify() 334 335 # If return still contains a megaraid, just asume it's type 336 if 'megaraid' in fineType: 337 # If any attributes is not None and has at least non None value, then it is a sat+megaraid device 338 if isinstance(self.if_attributes, AtaAttributes): 339 return 'ata' 340 else: 341 return 'sas' 342 343 return fineType
Returns the internal interface type of the device. It may not be the same as the interface type as used by smartctl.
Returns: str: The interface type of the device. (example: ata, scsi, nvme) None if the interface type could not be determined.
345 @property 346 def temperature(self) -> Optional[int]: 347 """Returns the temperature of the device. 348 349 Returns: 350 int: The temperature of the device in Celsius. 351 None if the temperature could not be determined. 352 """ 353 if self.if_attributes is None: 354 return self._temperature 355 else: 356 return self.if_attributes.temperature or self._temperature
Returns the temperature of the device.
Returns: int: The temperature of the device in Celsius. None if the temperature could not be determined.
358 @property 359 def smartctl_interface(self) -> Optional[str]: 360 """Returns the interface type of the device as it is used in smartctl. 361 362 Returns: 363 str: The interface type of the device. (example: ata, scsi, nvme) 364 None if the interface type could not be determined. 365 """ 366 return self._interface
Returns the interface type of the device as it is used in smartctl.
Returns: str: The interface type of the device. (example: ata, scsi, nvme) None if the interface type could not be determined.
368 @property 369 def interface(self) -> Optional[str]: 370 """Returns the interface type of the device as it is used in smartctl. 371 372 Returns: 373 str: The interface type of the device. (example: ata, scsi, nvme) 374 None if the interface type could not be determined. 375 """ 376 return self.smartctl_interface
Returns the interface type of the device as it is used in smartctl.
Returns: str: The interface type of the device. (example: ata, scsi, nvme) None if the interface type could not be determined.
378 @property 379 def dev_reference(self) -> str: 380 """The reference to the device as provided by smartctl. 381 - On unix-like systems, this is the path to the device. (example /dev/<name>) 382 - On MacOS, this is the name of the device. (example <name>) 383 - On Windows, this is the drive letter of the device. (example <drive letter>) 384 385 Returns: 386 str: The reference to the device as provided by smartctl. 387 """ 388 389 # detect if we are on MacOS 390 if 'IOService' in self.name: 391 return self.name 392 393 # otherwise asume we are on unix-like systems 394 return os.path.join('/dev/', self.name)
The reference to the device as provided by smartctl.
- On unix-like systems, this is the path to the device. (example /dev/
) - On MacOS, this is the name of the device. (example
) - On Windows, this is the drive letter of the device. (example
)
Returns: str: The reference to the device as provided by smartctl.
396 @property 397 def vendor(self) -> Optional[str]: 398 """Returns the vendor of the device. 399 400 Returns: 401 str: The vendor of the device. 402 """ 403 if self._vendor: 404 return self._vendor 405 406 # If family is present, try to stract from family. Skip anything but letters. 407 elif self.family: 408 filter = re.search(r'^[a-zA-Z]+', self.family.strip()) 409 if filter: 410 return filter.group(0) 411 412 # If model is present, try to stract from model. Skip anything but letters. 413 elif self.model: 414 filter = re.search(r'^[a-zA-Z]+', self.model.strip()) 415 if filter: 416 return filter.group(0) 417 418 # If all else fails, return None 419 return None
Returns the vendor of the device.
Returns: str: The vendor of the device.
421 @property 422 def capacity(self) -> Optional[str]: 423 """Returns the capacity in the raw smartctl format. 424 This may be deprecated in the future and its only retained for compatibility. 425 426 Returns: 427 str: The capacity in the raw smartctl format 428 """ 429 return self._capacity_human
Returns the capacity in the raw smartctl format. This may be deprecated in the future and its only retained for compatibility.
Returns: str: The capacity in the raw smartctl format
431 @property 432 def diags(self) -> Dict[str, str]: 433 """Gets the old/deprecated version of SCSI/SAS diags atribute. 434 """ 435 return self.diagnostics.get_classic_format()
Gets the old/deprecated version of SCSI/SAS diags atribute.
437 @property 438 def size_raw(self) -> Optional[str]: 439 """Returns the capacity in the raw smartctl format. 440 441 Returns: 442 str: The capacity in the raw smartctl format 443 """ 444 return self._capacity_human
Returns the capacity in the raw smartctl format.
Returns: str: The capacity in the raw smartctl format
446 @property 447 def size(self) -> int: 448 """Returns the capacity in bytes 449 450 Returns: 451 int: The capacity in bytes 452 """ 453 import humanfriendly 454 455 if self._capacity is not None: 456 return self._capacity 457 elif self._capacity_human is not None: 458 return humanfriendly.parse_size(self._capacity_human) 459 else: 460 return 0
Returns the capacity in bytes
Returns: int: The capacity in bytes
462 @property 463 def sector_size(self) -> int: 464 """Returns the sector size of the device. 465 466 Returns: 467 int: The sector size of the device in Bytes. If undefined, we'll assume 512B 468 """ 469 if self.logical_sector_size is not None: 470 return self.logical_sector_size 471 elif self.physical_sector_size is not None: 472 return self.physical_sector_size 473 else: 474 return 512
Returns the sector size of the device.
Returns: int: The sector size of the device in Bytes. If undefined, we'll assume 512B
522 def smart_toggle(self, action: str) -> Tuple[bool, List[str]]: 523 """ 524 A basic function to enable/disable SMART on device. 525 526 # Args: 527 * **action (str):** Can be either 'on'(for enabling) or 'off'(for disabling). 528 529 # Returns" 530 * **(bool):** Return True (if action succeded) else False 531 * **(List[str]):** None if option succeded else contains the error message. 532 """ 533 # Lets make the action verb all lower case 534 if self._interface == 'nvme': 535 return False, ['NVME devices do not currently support toggling SMART enabled'] 536 action_lower = action.lower() 537 if action_lower not in ['on', 'off']: 538 return False, ['Unsupported action {0}'.format(action)] 539 # Now lets check if the device's smart enabled status is already that of what 540 # the supplied action is intending it to be. If so then just return successfully 541 if self.smart_enabled: 542 if action_lower == 'on': 543 return True, [] 544 else: 545 if action_lower == 'off': 546 return True, [] 547 if self._interface is not None: 548 raw, returncode = self.smartctl.generic_call( 549 ['-s', action_lower, '-d', self._interface, self.dev_reference]) 550 else: 551 raw, returncode = self.smartctl.generic_call( 552 ['-s', action_lower, self.dev_reference]) 553 554 if returncode != 0: 555 return False, raw 556 # if everything worked out so far lets perform an update() and check the result 557 self.update() 558 if action_lower == 'off' and self.smart_enabled: 559 return False, ['Failed to turn SMART off.'] 560 if action_lower == 'on' and not self.smart_enabled: 561 return False, ['Failed to turn SMART on.'] 562 return True, []
A basic function to enable/disable SMART on device.
Args:
- action (str): Can be either 'on'(for enabling) or 'off'(for disabling).
Returns"
- (bool): Return True (if action succeded) else False
- (List[str]): None if option succeded else contains the error message.
564 def all_attributes(self, print_fn=print): 565 """ 566 Prints the entire SMART attribute table, in a format similar to 567 the output of smartctl. 568 allows usage of custom print function via parameter print_fn by default uses print 569 """ 570 header_printed = False 571 for attr in self.attributes: 572 if attr is not None: 573 if not header_printed: 574 print_fn("{0:>3} {1:24}{2:4}{3:4}{4:4}{5:9}{6:8}{7:12}{8}" 575 .format('ID#', 'ATTRIBUTE_NAME', 'CUR', 'WST', 'THR', 'TYPE', 'UPDATED', 'WHEN_FAIL', 576 'RAW')) 577 header_printed = True 578 print_fn(attr) 579 if not header_printed: 580 print_fn('This device does not support SMART attributes.')
Prints the entire SMART attribute table, in a format similar to the output of smartctl. allows usage of custom print function via parameter print_fn by default uses print
582 def all_selftests(self): 583 """ 584 Prints the entire SMART self-test log, in a format similar to 585 the output of smartctl. 586 """ 587 if self.tests: 588 all_tests = [] 589 if smartctl_type(self._interface) == 'scsi': 590 header = "{0:3}{1:17}{2:23}{3:7}{4:14}{5:15}".format( 591 'ID', 592 'Test Description', 593 'Status', 594 'Hours', 595 '1st_Error@LBA', 596 '[SK ASC ASCQ]' 597 ) 598 else: 599 header = ("{0:3}{1:17}{2:30}{3:5}{4:7}{5:17}".format( 600 'ID', 601 'Test_Description', 602 'Status', 603 'Left', 604 'Hours', 605 '1st_Error@LBA')) 606 all_tests.append(header) 607 for test in self.tests: 608 all_tests.append(str(test)) 609 610 return all_tests 611 else: 612 no_tests = 'No self-tests have been logged for this device.' 613 return no_tests
Prints the entire SMART self-test log, in a format similar to the output of smartctl.
699 def get_selftest_result(self, output=None): 700 """ 701 Refreshes a device's `pySMART.device.Device.tests` attribute to obtain 702 the latest test results. If a new test result is obtained, its content 703 is returned. 704 705 # Args: 706 * **output (str, optional):** If set to 'str', the string 707 representation of the most recent test result will be returned, instead 708 of a `Test_Entry` object. 709 710 # Returns: 711 * **(int):** Return status code. One of the following: 712 * 0 - Success. Object (or optionally, string rep) is attached. 713 * 1 - Self-test in progress. Must wait for it to finish. 714 * 2 - No new test results. 715 * 3 - The Self-test was Aborted by host 716 * **(`Test_Entry` or str):** Most recent `Test_Entry` object (or 717 optionally it's string representation) if new data exists. Status 718 message string on failure. 719 * **(int):** Estimate progress percantage of the running SMART selftest, if known. 720 Otherwise 'None'. 721 """ 722 # SCSI self-test logs hold 20 entries while ATA logs hold 21 723 if smartctl_type(self._interface) == 'scsi': 724 maxlog = 20 725 else: 726 maxlog = 21 727 # If we looked only at the most recent test result we could be fooled 728 # by two short tests run close together (within the same hour) 729 # appearing identical. Comparing the length of the log adds some 730 # confidence until it maxes, as above. Comparing the least-recent test 731 # result greatly diminishes the chances that two sets of two tests each 732 # were run within an hour of themselves, but with 16-17 other tests run 733 # in between them. 734 if self.tests: 735 _first_entry = self.tests[0] 736 _len = len(self.tests) 737 _last_entry = self.tests[_len - 1] 738 else: 739 _len = 0 740 self.update() 741 # Since I have changed the update() parsing to DTRT to pickup currently 742 # running selftests we can now purely rely on that for self._test_running 743 # Thus check for that variable first and return if it is True with appropos message. 744 if self._test_running is True: 745 return 1, 'Self-test in progress. Please wait.', self._test_progress 746 # Check whether the list got longer (ie: new entry) 747 # If so return the newest test result 748 # If not, because it's max size already, check for new entries 749 if ( 750 (len(self.tests) != _len) or 751 ( 752 len == maxlog and 753 ( 754 _first_entry.type != self.tests[0].type or 755 _first_entry.hours != self.tests[0].hours or 756 _last_entry.type != self.tests[len(self.tests) - 1].type or 757 _last_entry.hours != self.tests[len( 758 self.tests) - 1].hours 759 ) 760 ) 761 ): 762 return ( 763 0 if 'Aborted' not in self.tests[0].status else 3, 764 str(self.tests[0]) if output == 'str' else self.tests[0], 765 None 766 ) 767 else: 768 return 2, 'No new self-test results found.', None
Refreshes a device's Device.tests
attribute to obtain
the latest test results. If a new test result is obtained, its content
is returned.
Args:
- output (str, optional): If set to 'str', the string
representation of the most recent test result will be returned, instead
of a
Test_Entry
object.
Returns:
- (int): Return status code. One of the following:
- 0 - Success. Object (or optionally, string rep) is attached.
- 1 - Self-test in progress. Must wait for it to finish.
- 2 - No new test results.
- 3 - The Self-test was Aborted by host
- (
Test_Entry
or str): Most recentTest_Entry
object (or optionally it's string representation) if new data exists. Status message string on failure. - (int): Estimate progress percantage of the running SMART selftest, if known. Otherwise 'None'.
770 def abort_selftest(self): 771 """ 772 Aborts non-captive SMART Self Tests. Note that this command 773 will abort the Offline Immediate Test routine only if your disk 774 has the "Abort Offline collection upon new command" capability. 775 776 # Args: Nothing (just aborts directly) 777 778 # Returns: 779 * **(int):** The returncode of calling `smartctl -X device_path` 780 """ 781 return self.smartctl.test_stop(smartctl_type(self._interface), self.dev_reference)
Aborts non-captive SMART Self Tests. Note that this command will abort the Offline Immediate Test routine only if your disk has the "Abort Offline collection upon new command" capability.
Args: Nothing (just aborts directly)
Returns:
- (int): The returncode of calling
smartctl -X device_path
783 def run_selftest(self, test_type, ETA_type='date'): 784 """ 785 Instructs a device to begin a SMART self-test. All tests are run in 786 'offline' / 'background' mode, allowing normal use of the device while 787 it is being tested. 788 789 # Args: 790 * **test_type (str):** The type of test to run. Accepts the following 791 (not case sensitive): 792 * **short** - Brief electo-mechanical functionality check. 793 Generally takes 2 minutes or less. 794 * **long** - Thorough electro-mechanical functionality check, 795 including complete recording media scan. Generally takes several 796 hours. 797 * **conveyance** - Brief test used to identify damage incurred in 798 shipping. Generally takes 5 minutes or less. **This test is not 799 supported by SAS or SCSI devices.** 800 * **offline** - Runs SMART Immediate Offline Test. The effects of 801 this test are visible only in that it updates the SMART Attribute 802 values, and if errors are found they will appear in the SMART error 803 log, visible with the '-l error' option to smartctl. **This test is 804 not supported by SAS or SCSI devices in pySMART use cli smartctl for 805 running 'offline' selftest (runs in foreground) on scsi devices.** 806 * **ETA_type** - Format to return the estimated completion time/date 807 in. Default is 'date'. One could otherwise specidy 'seconds'. 808 Again only for ATA devices. 809 810 # Returns: 811 * **(int):** Return status code. One of the following: 812 * 0 - Self-test initiated successfully 813 * 1 - Previous self-test running. Must wait for it to finish. 814 * 2 - Unknown or unsupported (by the device) test type requested. 815 * 3 - Unspecified smartctl error. Self-test not initiated. 816 * **(str):** Return status message. 817 * **(str)/(float):** Estimated self-test completion time if a test is started. 818 The optional argument of 'ETA_type' (see above) controls the return type. 819 if 'ETA_type' == 'date' then a date string is returned else seconds(float) 820 is returned. 821 Note: The self-test completion time can only be obtained for ata devices. 822 Otherwise 'None'. 823 """ 824 # Lets call get_selftest_result() here since it does an update() and 825 # checks for an existing selftest is running or not, this way the user 826 # can issue a test from the cli and this can still pick that up 827 # Also note that we do not need to obtain the results from this as the 828 # data is already stored in the Device class object's variables 829 self.get_selftest_result() 830 if self._test_running: 831 return 1, 'Self-test in progress. Please wait.', self._test_ECD 832 test_type = test_type.lower() 833 interface = smartctl_type(self._interface) 834 try: 835 if not self.test_capabilities[test_type]: 836 return ( 837 2, 838 "Device {0} does not support the '{1}' test ".format( 839 self.name, test_type), 840 None 841 ) 842 except KeyError: 843 return 2, "Unknown test type '{0}' requested.".format(test_type), None 844 845 raw, rc = self.smartctl.test_start( 846 interface, test_type, self.dev_reference) 847 _success = False 848 _running = False 849 for line in raw: 850 if 'has begun' in line: 851 _success = True 852 self._test_running = True 853 if 'aborting current test' in line: 854 _running = True 855 try: 856 self._test_progress = 100 - \ 857 int(line.split('(')[-1].split('%')[0]) 858 except ValueError: 859 pass 860 861 if _success and 'complete after' in line: 862 self._test_ECD = line[25:].rstrip() 863 if ETA_type == 'seconds': 864 self._test_ECD = mktime( 865 strptime(self._test_ECD, '%a %b %d %H:%M:%S %Y')) - time() 866 self._test_progress = 0 867 if _success: 868 return 0, 'Self-test started successfully', self._test_ECD 869 else: 870 if _running: 871 return 1, 'Self-test already in progress. Please wait.', self._test_ECD 872 else: 873 return 3, 'Unspecified Error. Self-test not started.', None
Instructs a device to begin a SMART self-test. All tests are run in 'offline' / 'background' mode, allowing normal use of the device while it is being tested.
Args:
- test_type (str): The type of test to run. Accepts the following
(not case sensitive):
- short - Brief electo-mechanical functionality check. Generally takes 2 minutes or less.
- long - Thorough electro-mechanical functionality check, including complete recording media scan. Generally takes several hours.
- conveyance - Brief test used to identify damage incurred in shipping. Generally takes 5 minutes or less. This test is not supported by SAS or SCSI devices.
- offline - Runs SMART Immediate Offline Test. The effects of this test are visible only in that it updates the SMART Attribute values, and if errors are found they will appear in the SMART error log, visible with the '-l error' option to smartctl. This test is not supported by SAS or SCSI devices in pySMART use cli smartctl for running 'offline' selftest (runs in foreground) on scsi devices.
- ETA_type - Format to return the estimated completion time/date in. Default is 'date'. One could otherwise specidy 'seconds'. Again only for ATA devices.
Returns:
- (int): Return status code. One of the following:
- 0 - Self-test initiated successfully
- 1 - Previous self-test running. Must wait for it to finish.
- 2 - Unknown or unsupported (by the device) test type requested.
- 3 - Unspecified smartctl error. Self-test not initiated.
- (str): Return status message.
- (str)/(float): Estimated self-test completion time if a test is started. The optional argument of 'ETA_type' (see above) controls the return type. if 'ETA_type' == 'date' then a date string is returned else seconds(float) is returned. Note: The self-test completion time can only be obtained for ata devices. Otherwise 'None'.
875 def run_selftest_and_wait(self, test_type, output=None, polling=5, progress_handler=None): 876 """ 877 This is essentially a wrapper around run_selftest() such that we 878 call self.run_selftest() and wait on the running selftest till 879 it finished before returning. 880 The above holds true for all pySMART supported tests with the 881 exception of the 'offline' test (ATA only) as it immediately 882 returns, since the entire test only affects the smart error log 883 (if any errors found) and updates the SMART attributes. Other 884 than that it is not visibile anywhere else, so we start it and 885 simply return. 886 # Args: 887 * **test_type (str):** The type of test to run. Accepts the following 888 (not case sensitive): 889 * **short** - Brief electo-mechanical functionality check. 890 Generally takes 2 minutes or less. 891 * **long** - Thorough electro-mechanical functionality check, 892 including complete recording media scan. Generally takes several 893 hours. 894 * **conveyance** - Brief test used to identify damage incurred in 895 shipping. Generally takes 5 minutes or less. **This test is not 896 supported by SAS or SCSI devices.** 897 * **offline** - Runs SMART Immediate Offline Test. The effects of 898 this test are visible only in that it updates the SMART Attribute 899 values, and if errors are found they will appear in the SMART error 900 log, visible with the '-l error' option to smartctl. **This test is 901 not supported by SAS or SCSI devices in pySMART use cli smartctl for 902 running 'offline' selftest (runs in foreground) on scsi devices.** 903 * **output (str, optional):** If set to 'str', the string 904 representation of the most recent test result will be returned, 905 instead of a `Test_Entry` object. 906 * **polling (int, default=5):** The time duration to sleep for between 907 checking for test_results and progress. 908 * **progress_handler (function, optional):** This if provided is called 909 with self._test_progress as the supplied argument everytime a poll to 910 check the status of the selftest is done. 911 # Returns: 912 * **(int):** Return status code. One of the following: 913 * 0 - Self-test executed and finished successfully 914 * 1 - Previous self-test running. Must wait for it to finish. 915 * 2 - Unknown or illegal test type requested. 916 * 3 - The Self-test was Aborted by host 917 * 4 - Unspecified smartctl error. Self-test not initiated. 918 * **(`Test_Entry` or str):** Most recent `Test_Entry` object (or 919 optionally it's string representation) if new data exists. Status 920 message string on failure. 921 """ 922 test_initiation_result = self.run_selftest(test_type) 923 if test_initiation_result[0] != 0: 924 return test_initiation_result[:2] 925 if test_type == 'offline': 926 self._test_running = False 927 # if not then the test initiated correctly and we can start the polling. 928 # For now default 'polling' value is 5 seconds if not specified by the user 929 930 # Do an initial check, for good measure. 931 # In the probably impossible case that self._test_running is instantly False... 932 selftest_results = self.get_selftest_result(output=output) 933 while self._test_running: 934 if selftest_results[0] != 1: 935 # the selftest is run and finished lets return with the results 936 break 937 # Otherwise see if we are provided with the progress_handler to update progress 938 if progress_handler is not None: 939 progress_handler( 940 selftest_results[2] if selftest_results[2] is not None else 50) 941 # Now sleep 'polling' seconds before checking the progress again 942 sleep(polling) 943 944 # Check after the sleep to ensure we return the right result, and not an old one. 945 selftest_results = self.get_selftest_result(output=output) 946 947 # Now if (selftes_results[0] == 2) i.e No new selftest (because the same 948 # selftest was run twice within the last hour) but we know for a fact that 949 # we just ran a new selftest then just return the latest entry in self.tests 950 if selftest_results[0] == 2: 951 selftest_return_value = 0 if 'Aborted' not in self.tests[0].status else 3 952 return selftest_return_value, str(self.tests[0]) if output == 'str' else self.tests[0] 953 return selftest_results[:2]
This is essentially a wrapper around run_selftest() such that we call self.run_selftest() and wait on the running selftest till it finished before returning. The above holds true for all pySMART supported tests with the exception of the 'offline' test (ATA only) as it immediately returns, since the entire test only affects the smart error log (if any errors found) and updates the SMART attributes. Other than that it is not visibile anywhere else, so we start it and simply return.
Args:
- test_type (str): The type of test to run. Accepts the following
(not case sensitive):
- short - Brief electo-mechanical functionality check. Generally takes 2 minutes or less.
- long - Thorough electro-mechanical functionality check, including complete recording media scan. Generally takes several hours.
- conveyance - Brief test used to identify damage incurred in shipping. Generally takes 5 minutes or less. This test is not supported by SAS or SCSI devices.
- offline - Runs SMART Immediate Offline Test. The effects of this test are visible only in that it updates the SMART Attribute values, and if errors are found they will appear in the SMART error log, visible with the '-l error' option to smartctl. This test is not supported by SAS or SCSI devices in pySMART use cli smartctl for running 'offline' selftest (runs in foreground) on scsi devices.
- output (str, optional): If set to 'str', the string
representation of the most recent test result will be returned,
instead of a
Test_Entry
object. - polling (int, default=5): The time duration to sleep for between checking for test_results and progress.
progress_handler (function, optional): This if provided is called with self._test_progress as the supplied argument everytime a poll to check the status of the selftest is done.
Returns:
(int): Return status code. One of the following:
- 0 - Self-test executed and finished successfully
- 1 - Previous self-test running. Must wait for it to finish.
- 2 - Unknown or illegal test type requested.
- 3 - The Self-test was Aborted by host
- 4 - Unspecified smartctl error. Self-test not initiated.
- (
Test_Entry
or str): Most recentTest_Entry
object (or optionally it's string representation) if new data exists. Status message string on failure.
955 def update(self): 956 """ 957 Queries for device information using smartctl and updates all 958 class members, including the SMART attribute table and self-test log. 959 Can be called at any time to refresh the `pySMART.device.Device` 960 object's data content. 961 """ 962 # set temperature back to None so that if update() is called more than once 963 # any logic that relies on self.temperature to be None to rescan it works.it 964 self._temperature = None 965 # same for temperatures 966 self.temperatures = {} 967 if self.abridged: 968 interface = None 969 raw = self.smartctl.info(self.dev_reference) 970 971 else: 972 interface = smartctl_type(self._interface) 973 raw = self.smartctl.all( 974 self.dev_reference, interface) 975 976 parse_self_tests = False 977 parse_running_test = False 978 parse_ascq = False 979 polling_minute_type = None 980 message = '' 981 self.tests = [] 982 self._test_running = False 983 self._test_progress = None 984 # Lets skip the first couple of non-useful lines 985 _stdout = raw[4:] 986 987 ####################################### 988 # Encoding fixing # 989 ####################################### 990 # In some scenarios, smartctl returns some lines with a different/strange encoding 991 # This is a workaround to fix that 992 for i, line in enumerate(_stdout): 993 # character ' ' (U+202F) should be removed 994 _stdout[i] = line.replace('\u202f', '') 995 996 ####################################### 997 # Dedicated interface attributes # 998 ####################################### 999 if AtaAttributes.has_compatible_data(iter(_stdout)): 1000 self.if_attributes = AtaAttributes(iter(_stdout)) 1001 1002 elif self.dev_interface == 'nvme': 1003 self.if_attributes = NvmeAttributes(iter(_stdout)) 1004 1005 # Get Tests 1006 for test in self.if_attributes.tests: 1007 self.tests.append(TestEntry('nvme', test.num, test.description, test.status, test.powerOnHours, 1008 test.failingLBA, nsid=test.nsid, segment=test.seg, sct=test.sct, code=test.code, remain=100-test.progress)) 1009 1010 # Set running test 1011 if any(test.status == 'Running' for test in self.if_attributes.tests): 1012 self._test_running = True 1013 self._test_progress = self.if_attributes.tests[0].progress 1014 else: 1015 self._test_running = False 1016 self._test_progress = None 1017 1018 else: 1019 self.if_attributes = None 1020 1021 ####################################### 1022 # Global / generic attributes # 1023 ####################################### 1024 stdout_iter = iter(_stdout) 1025 for line in stdout_iter: 1026 if line.strip() == '': # Blank line stops sub-captures 1027 if parse_self_tests is True: 1028 parse_self_tests = False 1029 if parse_ascq: 1030 parse_ascq = False 1031 self.messages.append(message) 1032 if parse_ascq: 1033 message += ' ' + line.lstrip().rstrip() 1034 if parse_self_tests: 1035 # Detect Test Format 1036 1037 ## SCSI/SAS FORMAT ## 1038 # Example smartctl output 1039 # SMART Self-test log 1040 # Num Test Status segment LifeTime LBA_first_err [SK ASC ASQ] 1041 # Description number (hours) 1042 # # 1 Background short Completed - 33124 - [- - -] 1043 format_scsi = re.compile( 1044 r'^[#\s]*(\d+)\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s+\[([^\s]+)\s+([^\s]+)\s+([^\s]+)\]$').match(line) 1045 1046 ## ATA FORMAT ## 1047 # Example smartctl output: 1048 # SMART Self-test log structure revision number 1 1049 # Num Test_Description Status Remaining LifeTime(hours) LBA_of_first_error 1050 # # 1 Extended offline Completed without error 00% 46660 - 1051 format_ata = re.compile( 1052 r'^[#\s]*(\d+)\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{1,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])$').match(line) 1053 1054 if format_scsi is not None: 1055 format = 'scsi' 1056 parsed = format_scsi.groups() 1057 num = int(parsed[0]) 1058 test_type = parsed[1] 1059 status = parsed[2] 1060 segment = parsed[3] 1061 hours = parsed[4] 1062 lba = parsed[5] 1063 sense = parsed[6] 1064 asc = parsed[7] 1065 ascq = parsed[8] 1066 self.tests.append(TestEntry( 1067 format, 1068 num, 1069 test_type, 1070 status, 1071 hours, 1072 lba, 1073 segment=segment, 1074 sense=sense, 1075 asc=asc, 1076 ascq=ascq 1077 )) 1078 elif format_ata is not None: 1079 ## ATA FORMAT ## 1080 format = 'ata' 1081 parsed = format_ata.groups() 1082 num = parsed[0] 1083 test_type = parsed[1] 1084 status = parsed[2] 1085 remain = parsed[3] 1086 hours = parsed[4] 1087 lba = parsed[5] 1088 1089 try: 1090 num = int(num) 1091 except: 1092 num = None 1093 1094 self.tests.append( 1095 TestEntry(format, num, test_type, status, 1096 hours, lba, remain=remain) 1097 ) 1098 else: 1099 pass 1100 1101 # Basic device information parsing 1102 if any_in(line, 'Device Model', 'Product', 'Model Number'): 1103 self.model = line.split(':')[1].lstrip().rstrip() 1104 self._guess_smart_type(line.lower()) 1105 continue 1106 1107 if 'Model Family' in line: 1108 self.family = line.split(':')[1].strip() 1109 self._guess_smart_type(line.lower()) 1110 continue 1111 1112 if 'LU WWN' in line: 1113 self._guess_smart_type(line.lower()) 1114 continue 1115 1116 if any_in(line, 'Serial Number', 'Serial number'): 1117 try: 1118 self.serial = line.split(':')[1].split()[0].rstrip() 1119 except IndexError: 1120 # Serial reported empty 1121 self.serial = "" 1122 continue 1123 1124 vendor = re.compile(r'^Vendor:\s+(\w+)').match(line) 1125 if vendor is not None: 1126 self._vendor = vendor.groups()[0] 1127 1128 if any_in(line, 'Firmware Version', 'Revision'): 1129 self.firmware = line.split(':')[1].strip() 1130 1131 if any_in(line, 'User Capacity', 'Total NVM Capacity', 'Namespace 1 Size/Capacity'): 1132 # TODO: support for multiple NVMe namespaces 1133 m = re.match( 1134 r'.*:\s+([\d,. \u2019\u00a0]+)\s\D*\[?([^\]]+)?\]?', line.strip()) 1135 1136 if m is not None: 1137 tmp = m.groups() 1138 if tmp[0] == ' ': 1139 # This capacity is set to 0, skip it 1140 continue 1141 1142 self._capacity = int( 1143 tmp[0].strip().replace(',', '').replace('.', '').replace(' ', '').replace('\u2019', '').replace('\u00a0', '')) 1144 1145 if len(tmp) == 2 and tmp[1] is not None: 1146 self._capacity_human = tmp[1].strip().replace(',', '.') 1147 1148 if 'SMART support' in line: 1149 # self.smart_capable = 'Available' in line 1150 # self.smart_enabled = 'Enabled' in line 1151 # Since this line repeats twice the above method is flawed 1152 # Lets try the following instead, it is a bit redundant but 1153 # more robust. 1154 if any_in(line, 'Unavailable', 'device lacks SMART capability'): 1155 self.smart_capable = False 1156 self.smart_enabled = False 1157 elif 'Enabled' in line: 1158 self.smart_enabled = True 1159 elif 'Disabled' in line: 1160 self.smart_enabled = False 1161 elif any_in(line, 'Available', 'device has SMART capability'): 1162 self.smart_capable = True 1163 continue 1164 1165 if 'does not support SMART' in line: 1166 self.smart_capable = False 1167 self.smart_enabled = False 1168 continue 1169 1170 if 'Rotation Rate' in line: 1171 if 'Solid State Device' in line: 1172 self.is_ssd = True 1173 elif 'rpm' in line: 1174 self.is_ssd = False 1175 try: 1176 self.rotation_rate = int( 1177 line.split(':')[1].lstrip().rstrip()[:-4]) 1178 except ValueError: 1179 # Cannot parse the RPM? Assigning None instead 1180 self.rotation_rate = None 1181 continue 1182 1183 if 'SMART overall-health self-assessment' in line: # ATA devices 1184 if line.split(':')[1].strip() == 'PASSED': 1185 self.assessment = 'PASS' 1186 else: 1187 self.assessment = 'FAIL' 1188 continue 1189 1190 if 'SMART Health Status' in line: # SCSI devices 1191 if line.split(':')[1].strip() == 'OK': 1192 self.assessment = 'PASS' 1193 else: 1194 self.assessment = 'FAIL' 1195 parse_ascq = True # Set flag to capture status message 1196 message = line.split(':')[1].lstrip().rstrip() 1197 continue 1198 1199 # Parse SMART test capabilities (ATA only) 1200 # Note: SCSI does not list this but and allows for only 'offline', 'short' and 'long' 1201 if 'SMART execute Offline immediate' in line: 1202 self.test_capabilities['offline'] = 'No' not in line 1203 continue 1204 1205 if 'Conveyance Self-test supported' in line: 1206 self.test_capabilities['conveyance'] = 'No' not in line 1207 continue 1208 1209 if 'Selective Self-test supported' in line: 1210 self.test_capabilities['selective'] = 'No' not in line 1211 continue 1212 1213 if 'Self-test supported' in line: 1214 self.test_capabilities['short'] = 'No' not in line 1215 self.test_capabilities['long'] = 'No' not in line 1216 continue 1217 1218 # Parse SMART test capabilities (NVMe only) 1219 if 'Optional Admin Commands' in line: 1220 if 'Self_Test' in line: 1221 self.test_capabilities['short'] = True 1222 self.test_capabilities['long'] = True 1223 1224 if 'Short self-test routine' in line: 1225 polling_minute_type = 'short' 1226 continue 1227 if 'Extended self-test routine' in line: 1228 polling_minute_type = 'long' 1229 continue 1230 if 'Conveyance self-test routine' in line: 1231 polling_minute_type = 'conveyance' 1232 continue 1233 if 'recommended polling time:' in line: 1234 self.test_polling_time[polling_minute_type] = float( 1235 re.sub("[^0-9]", "", line) 1236 ) 1237 continue 1238 1239 # For some reason smartctl does not show a currently running test 1240 # for 'ATA' in the Test log so I just have to catch it this way i guess! 1241 # For 'scsi' I still do it since it is the only place I get % remaining in scsi 1242 if 'Self-test execution status' in line: 1243 if 'progress' in line: 1244 self._test_running = True 1245 # for ATA the "%" remaining is on the next line 1246 # thus set the parse_running_test flag and move on 1247 parse_running_test = True 1248 elif '%' in line: 1249 # for scsi the progress is on the same line 1250 # so we can just parse it and move on 1251 self._test_running = True 1252 try: 1253 self._test_progress = 100 - \ 1254 int(line.split('%')[0][-3:].strip()) 1255 except ValueError: 1256 pass 1257 continue 1258 if parse_running_test is True: 1259 try: 1260 self._test_progress = 100 - \ 1261 int(line.split('%')[0][-3:].strip()) 1262 except ValueError: 1263 pass 1264 parse_running_test = False 1265 1266 if "Self-test log" in line: 1267 parse_self_tests = True # Set flag to capture test entries 1268 continue 1269 1270 ####################################### 1271 # SCSI only # 1272 ####################################### 1273 # 1274 # Everything from here on is parsing SCSI information that takes 1275 # the place of similar ATA SMART information 1276 if 'used endurance' in line: 1277 pct = int(line.split(':')[1].strip()[:-1]) 1278 self.diagnostics.Life_Left = 100 - pct 1279 continue 1280 1281 if 'Specified cycle count' in line: 1282 self.diagnostics.Start_Stop_Spec = int( 1283 line.split(':')[1].strip()) 1284 continue 1285 1286 if 'Accumulated start-stop cycles' in line: 1287 self.diagnostics.Start_Stop_Cycles = int( 1288 line.split(':')[1].strip()) 1289 if self.diagnostics.Start_Stop_Spec and self.diagnostics.Start_Stop_Spec != 0: 1290 self.diagnostics.Start_Stop_Pct_Left = int(round( 1291 100 - (self.diagnostics.Start_Stop_Cycles / 1292 self.diagnostics.Start_Stop_Spec), 0)) 1293 continue 1294 1295 if 'Specified load-unload count' in line: 1296 self.diagnostics.Load_Cycle_Spec = int( 1297 line.split(':')[1].strip()) 1298 continue 1299 1300 if 'Accumulated load-unload cycles' in line: 1301 self.diagnostics.Load_Cycle_Count = int( 1302 line.split(':')[1].strip()) 1303 if self.diagnostics.Load_Cycle_Spec and self.diagnostics.Load_Cycle_Spec != 0: 1304 self.diagnostics.Load_Cycle_Pct_Left = int(round( 1305 100 - (self.diagnostics.Load_Cycle_Count / 1306 self.diagnostics.Load_Cycle_Spec), 0)) 1307 continue 1308 1309 if 'Elements in grown defect list' in line: 1310 self.diagnostics.Reallocated_Sector_Ct = int( 1311 line.split(':')[1].strip()) 1312 continue 1313 1314 if 'read:' in line: 1315 line_ = ' '.join(line.split()).split(' ') 1316 if line_[1] == '0' and line_[2] == '0' and line_[3] == '0' and line_[4] == '0': 1317 self.diagnostics.Corrected_Reads = 0 1318 elif line_[4] == '0': 1319 self.diagnostics.Corrected_Reads = int( 1320 line_[1]) + int(line_[2]) + int(line_[3]) 1321 else: 1322 self.diagnostics.Corrected_Reads = int(line_[4]) 1323 self.diagnostics._Reads_GB = float(line_[6].replace(',', '.')) 1324 self.diagnostics._Uncorrected_Reads = int(line_[7]) 1325 continue 1326 1327 if 'write:' in line: 1328 line_ = ' '.join(line.split()).split(' ') 1329 if (line_[1] == '0' and line_[2] == '0' and 1330 line_[3] == '0' and line_[4] == '0'): 1331 self.diagnostics.Corrected_Writes = 0 1332 elif line_[4] == '0': 1333 self.diagnostics.Corrected_Writes = int( 1334 line_[1]) + int(line_[2]) + int(line_[3]) 1335 else: 1336 self.diagnostics.Corrected_Writes = int(line_[4]) 1337 self.diagnostics._Writes_GB = float(line_[6].replace(',', '.')) 1338 self.diagnostics._Uncorrected_Writes = int(line_[7]) 1339 continue 1340 1341 if 'verify:' in line: 1342 line_ = ' '.join(line.split()).split(' ') 1343 if (line_[1] == '0' and line_[2] == '0' and 1344 line_[3] == '0' and line_[4] == '0'): 1345 self.diagnostics.Corrected_Verifies = 0 1346 elif line_[4] == '0': 1347 self.diagnostics.Corrected_Verifies = int( 1348 line_[1]) + int(line_[2]) + int(line_[3]) 1349 else: 1350 self.diagnostics.Corrected_Verifies = int(line_[4]) 1351 self.diagnostics._Verifies_GB = float( 1352 line_[6].replace(',', '.')) 1353 self.diagnostics._Uncorrected_Verifies = int(line_[7]) 1354 continue 1355 1356 if 'non-medium error count' in line: 1357 self.diagnostics.Non_Medium_Errors = int( 1358 line.split(':')[1].strip()) 1359 continue 1360 1361 if 'Accumulated power on time' in line: 1362 self.diagnostics.Power_On_Hours = int( 1363 line.split(':')[1].split(' ')[1]) 1364 continue 1365 1366 if 'Current Drive Temperature' in line or ('Temperature:' in 1367 line and interface == 'nvme'): 1368 try: 1369 self._temperature = int( 1370 line.split(':')[-1].strip().split()[0]) 1371 1372 if 'fahrenheit' in line.lower(): 1373 self._temperature = int( 1374 (self.temperature - 32) * 5 / 9) 1375 1376 except ValueError: 1377 pass 1378 1379 continue 1380 1381 if 'Temperature Sensor ' in line: 1382 try: 1383 match = re.search( 1384 r'Temperature\sSensor\s([0-9]+):\s+(-?[0-9]+)', line) 1385 if match: 1386 (tempsensor_number_s, tempsensor_value_s) = match.group(1, 2) 1387 tempsensor_number = int(tempsensor_number_s) 1388 tempsensor_value = int(tempsensor_value_s) 1389 1390 if 'fahrenheit' in line.lower(): 1391 tempsensor_value = int( 1392 (tempsensor_value - 32) * 5 / 9) 1393 1394 self.temperatures[tempsensor_number] = tempsensor_value 1395 if self.temperature is None or tempsensor_number == 0: 1396 self._temperature = tempsensor_value 1397 except ValueError: 1398 pass 1399 1400 continue 1401 1402 ####################################### 1403 # Common values # 1404 ####################################### 1405 1406 # Sector sizes 1407 if 'Sector Sizes' in line: # ATA 1408 m = re.match( 1409 r'.* (\d+) bytes logical,\s*(\d+) bytes physical', line) 1410 if m: 1411 self.logical_sector_size = int(m.group(1)) 1412 self.physical_sector_size = int(m.group(2)) 1413 # set diagnostics block size to physical sector size 1414 self.diagnostics._block_size = self.physical_sector_size 1415 continue 1416 if 'Logical block size:' in line: # SCSI 1/2 1417 self.logical_sector_size = int( 1418 line.split(':')[1].strip().split(' ')[0]) 1419 # set diagnostics block size to logical sector size 1420 self.diagnostics._block_size = self.logical_sector_size 1421 continue 1422 if 'Physical block size:' in line: # SCSI 2/2 1423 self.physical_sector_size = int( 1424 line.split(':')[1].strip().split(' ')[0]) 1425 continue 1426 if 'Namespace 1 Formatted LBA Size' in line: # NVMe 1427 # Note: we will assume that there is only one namespace 1428 self.logical_sector_size = int( 1429 line.split(':')[1].strip().split(' ')[0]) 1430 continue 1431 1432 if not self.abridged: 1433 if not interface == 'scsi': 1434 # Parse the SMART table for below-threshold attributes and create 1435 # corresponding warnings for non-SCSI disks 1436 self._make_smart_warnings() 1437 else: 1438 # If not obtained Power_On_Hours above, make a direct attempt to extract power on 1439 # hours from the background scan results log. 1440 if self.smart_enabled and self.diagnostics.Power_On_Hours is None: 1441 raw, returncode = self.smartctl.generic_call( 1442 [ 1443 '-d', 1444 'scsi', 1445 '-l', 1446 'background', 1447 self.dev_reference 1448 ]) 1449 1450 for line in raw: 1451 if 'power on time' in line: 1452 self.diagnostics.Power_On_Hours = int( 1453 line.split(':')[1].split(' ')[1]) 1454 1455 # Now that we have finished the update routine, if we did not find a runnning selftest 1456 # nuke the self._test_ECD and self._test_progress 1457 if self._test_running is False: 1458 self._test_ECD = None 1459 self._test_progress = None
Queries for device information using smartctl and updates all
class members, including the SMART attribute table and self-test log.
Can be called at any time to refresh the Device
object's data content.
49def smart_health_assement(disk_name: str, interface: Optional[str] = None, smartctl: Smartctl = SMARTCTL) -> Optional[str]: 50 """ 51 This function gets the SMART Health Status of the disk (IF the disk 52 is SMART capable and smart is enabled on it else returns None). 53 This function is to be used only in abridged mode and not otherwise, 54 since in non-abridged mode update gets this information anyways. 55 56 Args: 57 disk_name (str): name of the disk 58 interface (str, optional): interface type of the disk (e.g. 'sata', 'scsi', 'nvme',... Defaults to None.) 59 60 Returns: 61 str: SMART Health Status of the disk. Returns None if the disk is not SMART capable or smart is not enabled on it. 62 Possible values are 'PASS', 'FAIL' or None. 63 """ 64 assessment = None 65 raw = smartctl.health(os.path.join( 66 '/dev/', disk_name.replace('nvd', 'nvme')), interface) 67 line = raw[4] # We only need this line 68 if 'SMART overall-health self-assessment' in line: # ATA devices 69 if line.split(':')[1].strip() == 'PASSED': 70 assessment = 'PASS' 71 else: 72 assessment = 'FAIL' 73 if 'SMART Health Status' in line: # SCSI devices 74 if line.split(':')[1].strip() == 'OK': 75 assessment = 'PASS' 76 else: 77 assessment = 'FAIL' 78 return assessment
This function gets the SMART Health Status of the disk (IF the disk is SMART capable and smart is enabled on it else returns None). This function is to be used only in abridged mode and not otherwise, since in non-abridged mode update gets this information anyways.
Args: disk_name (str): name of the disk interface (str, optional): interface type of the disk (e.g. 'sata', 'scsi', 'nvme',... Defaults to None.)
Returns: str: SMART Health Status of the disk. Returns None if the disk is not SMART capable or smart is not enabled on it. Possible values are 'PASS', 'FAIL' or None.