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