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