pySMART API documentation

# 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.

View Source

      1# Copyright (C) 2014 Marc Herndon
      2#
      3# This program is free software; you can redistribute it and/or
      4# modify it under the terms of the GNU General Public License,
      5# version 2, as published by the Free Software Foundation.
      6#
      7# This program is distributed in the hope that it will be useful,
      8# but WITHOUT ANY WARRANTY; without even the implied warranty of
      9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     10# GNU General Public License for more details.
     11#
     12# You should have received a copy of the GNU General Public License
     13# along with this program; if not, write to the Free Software
     14# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
     15# MA  02110-1301, USA.
     16#
     17################################################################
     18"""
     19Copyright (C) 2014 Marc Herndon
     20
     21pySMART is a simple Python wrapper for the `smartctl` component of
     22`smartmontools`. It works under Linux and Windows, as long as smartctl is on
     23the system path. Running with administrative (root) privilege is strongly
     24recommended, as smartctl cannot accurately detect all device types or parse
     25all SMART information without full permissions.
     26
     27With only a device's name (ie: /dev/sda, pd0), the API will create a
     28`Device` object, populated with all relevant information about
     29that device. The documented API can then be used to query this object for
     30information, initiate device self-tests, and perform other functions.
     31
     32Usage
     33-----
     34The most common way to use pySMART is to create a logical representation of the
     35physical storage device that you would like to work with, as shown:
     36
     37    #!bash
     38    >>> from pySMART import Device
     39    >>> sda = Device('/dev/sda')
     40    >>> sda
     41    <SATA device on /dev/sda mod:WDC WD5000AAKS-60Z1A0 sn:WD-WCAWFxxxxxxx>
     42
     43`Device` class members can be accessed directly, and a number of helper methods
     44are provided to retrieve information in bulk.  Some examples are shown below:
     45
     46    #!bash
     47    >>> sda.assessment  # Query the SMART self-assessment
     48    'PASS'
     49    >>> sda.attributes[9]  # Query a single SMART attribute
     50    <SMART Attribute 'Power_On_Hours' 068/000 raw:23644>
     51    >>> sda.all_attributes()  # Print the entire SMART attribute table
     52    ID# ATTRIBUTE_NAME          CUR WST THR TYPE     UPDATED WHEN_FAIL    RAW
     53      1 Raw_Read_Error_Rate     200 200 051 Pre-fail Always  -           0
     54      3 Spin_Up_Time            141 140 021 Pre-fail Always  -           3908
     55      4 Start_Stop_Count        098 098 000 Old_age  Always  -           2690
     56      5 Reallocated_Sector_Ct   200 200 140 Pre-fail Always  -           0
     57        ... # Edited for brevity
     58    199 UDMA_CRC_Error_Count    200 200 000 Old_age  Always  -           0
     59    200 Multi_Zone_Error_Rate   200 200 000 Old_age  Offline -           0
     60    >>> sda.tests[0]  # Query the most recent self-test result
     61    <SMART Self-test [Short offline|Completed without error] hrs:23734 lba:->
     62    >>> sda.all_selftests()  # Print the entire self-test log
     63    ID Test_Description Status                        Left Hours  1st_Error@lba
     64     1 Short offline    Completed without error       00%  23734  -
     65     2 Short offline    Completed without error       00%  23734  -
     66       ... # Edited for brevity
     67     7 Short offline    Completed without error       00%  23726  -
     68     8 Short offline    Completed without error       00%  1      -
     69
     70Alternatively, the package provides a `DeviceList` class. When instantiated,
     71this will auto-detect all local storage devices and create a list containing
     72one `Device` object for each detected storage device.
     73
     74    #!bash
     75    >>> from pySMART import DeviceList
     76    >>> devlist = DeviceList()
     77    >>> devlist
     78    <DeviceList contents:
     79    <SAT device on /dev/sdb mod:WDC WD20EADS-00R6B0 sn:WD-WCAVYxxxxxxx>
     80    <SAT device on /dev/sdc mod:WDC WD20EADS-00S2B0 sn:WD-WCAVYxxxxxxx>
     81    <CSMI device on /dev/csmi0,0 mod:WDC WD5000AAKS-60Z1A0 sn:WD-WCAWFxxxxxxx>
     82    >
     83    >>> devlist.devices[0].attributes[5]  # Access Device data as above
     84    <SMART Attribute 'Reallocated_Sector_Ct' 173/140 raw:214>
     85
     86In the above cases if a new DeviceList is empty or a specific Device reports an
     87"UNKNOWN INTERFACE", you are likely running without administrative privileges.
     88On POSIX systems, you can request smartctl is run as a superuser by setting the
     89sudo attribute of the global SMARTCTL object to True. Note this may cause you
     90to be prompted for a password.
     91
     92    #!bash
     93    >>> from pySMART import DeviceList
     94    >>> from pySMART import Device
     95    >>> sda = Device('/dev/sda')
     96    >>> sda
     97    <UNKNOWN INTERFACE device on /dev/sda mod:None sn:None>
     98    >>> devlist = DeviceList()
     99    >>> devlist
    100    <DeviceList contents:
    101    >
    102    >>> from pySMART import SMARTCTL
    103    >>> SMARTCTL.sudo = True
    104    >>> sda = Device('/dev/sda')
    105    >>> sda
    106    [sudo] password for user:
    107    <SAT device on /dev/sda mod:ST10000DM0004-1ZC101 sn:ZA20VNPT>
    108    >>> devlist = DeviceList()
    109    >>> devlist
    110    <DeviceList contents:
    111    <NVME device on /dev/nvme0 mod:Sabrent Rocket 4.0 1TB sn:03850709185D88300410>
    112    <NVME device on /dev/nvme1 mod:Samsung SSD 970 EVO Plus 2TB sn:S59CNM0RB05028D>
    113    <NVME device on /dev/nvme2 mod:Samsung SSD 970 EVO Plus 2TB sn:S59CNM0RB05113H>
    114    <SAT device on /dev/sda mod:ST10000DM0004-1ZC101 sn:ZA20VNPT>
    115    <SAT device on /dev/sdb mod:ST10000DM0004-1ZC101 sn:ZA22W366>
    116    <SAT device on /dev/sdc mod:ST10000DM0004-1ZC101 sn:ZA22SPLG>
    117    <SAT device on /dev/sdd mod:ST10000DM0004-1ZC101 sn:ZA2215HL>
    118    >
    119
    120In general, it is recommended to run the base script with enough privileges to
    121execute smartctl, but this is not possible in all cases, so this workaround is
    122provided as a convenience. However, note that using sudo inside other
    123non-terminal projects may cause dev-bugs/issues.
    124
    125
    126Using the pySMART wrapper, Python applications be be rapidly developed to take
    127advantage of the powerful features of smartmontools.
    128
    129Acknowledgements
    130----------------
    131I would like to thank the entire team behind smartmontools for creating and
    132maintaining such a fantastic product.
    133
    134In particular I want to thank Christian Franke, who maintains the Windows port
    135of the software.  For several years I have written Windows batch files that
    136rely on smartctl.exe to automate evaluation and testing of large pools of
    137storage devices under Windows.  Without his work, my job would have been
    138significantly more miserable. :)
    139
    140Having recently migrated my development from Batch to Python for Linux
    141portability, I thought a simple wrapper for smartctl would save time in the
    142development of future automated test tools.
    143"""
    144# autopep8: off
    145from .testentry import TestEntry
    146from .attribute import Attribute
    147from . import utils
    148utils.configure_trace_logging()
    149from .smartctl import SMARTCTL
    150from .device_list import DeviceList
    151from .device import Device, smart_health_assement
    152# autopep8: on
    153
    154
    155__version__ = '1.2.2'
    156__all__ = [
    157    'TestEntry', 'Attribute', 'utils', 'SMARTCTL', 'DeviceList', 'Device',
    158    'smart_health_assement'
    159]

class TestEntry: View Source

     27class TestEntry(object):
     28    """
     29    Contains all of the information associated with a single SMART Self-test
     30    log entry. This data is intended to exactly mirror that obtained through
     31    smartctl.
     32    """
     33
     34    def __init__(self, format, num: Optional[int], test_type, status, hours, lba, remain=None, segment=None, sense=None, asc=None,
     35                 ascq=None):
     36        self._format = format
     37        """
     38        **(str):** Indicates whether this entry was taken from an 'ata' or
     39        'scsi' self-test log. Used to display the content properly.
     40        """
     41        self.num: Optional[int] = num
     42        """
     43        **(int):** Entry's position in the log from 1 (most recent) to 21
     44        (least recent).  ATA logs save the last 21 entries while SCSI logs
     45        only save the last 20.
     46        """
     47        self.type = test_type
     48        """
     49        **(str):** Type of test run.  Generally short, long (extended), or
     50        conveyance, plus offline (background) or captive (foreground).
     51        """
     52        self.status = status
     53        """
     54        **(str):** Self-test's status message, for example 'Completed without
     55        error' or 'Completed: read failure'.
     56        """
     57        self.hours = hours
     58        """
     59        **(str):** The device's power-on hours at the time the self-test
     60        was initiated.
     61        """
     62        self.LBA = lba
     63        """
     64        **(str):** Indicates the first LBA at which an error was encountered
     65        during this self-test. Presented as a decimal value for ATA/SATA
     66        devices and in hexadecimal notation for SAS/SCSI devices.
     67        """
     68        self.remain = remain
     69        """
     70        **(str):** Percentage value indicating how much of the self-test is
     71        left to perform. '00%' indicates a complete test, while any other
     72        value could indicate a test in progress or one that failed prior to
     73        completion. Only reported by ATA devices.
     74        """
     75        self.segment = segment
     76        """
     77        **(str):** A manufacturer-specific self-test segment number reported
     78        by SCSI devices on self-test failure. Set to '-' otherwise.
     79        """
     80        self.sense = sense
     81        """
     82        **(str):** SCSI sense key reported on self-test failure. Set to '-'
     83        otherwise.
     84        """
     85        self.ASC = asc
     86        """
     87        **(str):** SCSI 'Additonal Sense Code' reported on self-test failure.
     88        Set to '-' otherwise.
     89        """
     90        self.ASCQ = ascq
     91        """
     92        **(str):** SCSI 'Additonal Sense Code Quaifier' reported on self-test
     93        failure. Set to '-' otherwise.
     94        """
     95
     96    def __getstate__(self):
     97        return {
     98            'num': self.num,
     99            'type': self.type,
    100            'status': self.status,
    101            'hours': self.hours,
    102            'lba': self.LBA,
    103            'remain': self.remain,
    104            'segment': self.segment,
    105            'sense': self.sense,
    106            'asc': self.ASC,
    107            'ascq': self.ASCQ
    108        }
    109
    110    def __repr__(self):
    111        """Define a basic representation of the class object."""
    112        return "<SMART Self-test [%s|%s] hrs:%s LBA:%s>" % (
    113            self.type, self.status, self.hours, self.LBA)
    114
    115    def __str__(self):
    116        """
    117        Define a formatted string representation of the object's content.
    118        Looks nearly identical to the output of smartctl, without overflowing
    119        80-character lines.
    120        """
    121        if self._format == 'ata':
    122            return "{0:>2} {1:17}{2:30}{3:5}{4:7}{5:17}".format(
    123                self.num, self.type, self.status, self.remain, self.hours,
    124                self.LBA)
    125        else:
    126            # 'Segment' could not be fit on the 80-char line. It's of limited
    127            # utility anyway due to it's manufacturer-proprietary nature...
    128            return ("{0:>2} {1:17}{2:23}{3:7}{4:14}[{5:4}{6:5}{7:4}]".format(
    129                self.num,
    130                self.type,
    131                self.status,
    132                self.hours,
    133                self.LBA,
    134                self.sense,
    135                self.ASC,
    136                self.ASCQ
    137            ))

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.

TestEntry( format, num: Union[int, NoneType], test_type, status, hours, lba, remain=None, segment=None, sense=None, asc=None, ascq=None) View Source

    34    def __init__(self, format, num: Optional[int], test_type, status, hours, lba, remain=None, segment=None, sense=None, asc=None,
    35                 ascq=None):
    36        self._format = format
    37        """
    38        **(str):** Indicates whether this entry was taken from an 'ata' or
    39        'scsi' self-test log. Used to display the content properly.
    40        """
    41        self.num: Optional[int] = num
    42        """
    43        **(int):** Entry's position in the log from 1 (most recent) to 21
    44        (least recent).  ATA logs save the last 21 entries while SCSI logs
    45        only save the last 20.
    46        """
    47        self.type = test_type
    48        """
    49        **(str):** Type of test run.  Generally short, long (extended), or
    50        conveyance, plus offline (background) or captive (foreground).
    51        """
    52        self.status = status
    53        """
    54        **(str):** Self-test's status message, for example 'Completed without
    55        error' or 'Completed: read failure'.
    56        """
    57        self.hours = hours
    58        """
    59        **(str):** The device's power-on hours at the time the self-test
    60        was initiated.
    61        """
    62        self.LBA = lba
    63        """
    64        **(str):** Indicates the first LBA at which an error was encountered
    65        during this self-test. Presented as a decimal value for ATA/SATA
    66        devices and in hexadecimal notation for SAS/SCSI devices.
    67        """
    68        self.remain = remain
    69        """
    70        **(str):** Percentage value indicating how much of the self-test is
    71        left to perform. '00%' indicates a complete test, while any other
    72        value could indicate a test in progress or one that failed prior to
    73        completion. Only reported by ATA devices.
    74        """
    75        self.segment = segment
    76        """
    77        **(str):** A manufacturer-specific self-test segment number reported
    78        by SCSI devices on self-test failure. Set to '-' otherwise.
    79        """
    80        self.sense = sense
    81        """
    82        **(str):** SCSI sense key reported on self-test failure. Set to '-'
    83        otherwise.
    84        """
    85        self.ASC = asc
    86        """
    87        **(str):** SCSI 'Additonal Sense Code' reported on self-test failure.
    88        Set to '-' otherwise.
    89        """
    90        self.ASCQ = ascq
    91        """
    92        **(str):** SCSI 'Additonal Sense Code Quaifier' reported on self-test
    93        failure. Set to '-' otherwise.
    94        """

num: Union[int, NoneType]

(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.

type

(str): Type of test run. Generally short, long (extended), or conveyance, plus offline (background) or captive (foreground).

status

(str): Self-test's status message, for example 'Completed without error' or 'Completed: read failure'.

hours

(str): The device's power-on hours at the time the self-test was initiated.

LBA

(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.

remain

(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.

segment

(str): A manufacturer-specific self-test segment number reported by SCSI devices on self-test failure. Set to '-' otherwise.

sense

(str): SCSI sense key reported on self-test failure. Set to '-' otherwise.

ASC

(str): SCSI 'Additonal Sense Code' reported on self-test failure. Set to '-' otherwise.

ASCQ

(str): SCSI 'Additonal Sense Code Quaifier' reported on self-test failure. Set to '-' otherwise.

class Attribute: View Source

     28class Attribute(object):
     29    """
     30    Contains all of the information associated with a single SMART attribute
     31    in a `Device`'s SMART table. This data is intended to exactly mirror that
     32    obtained through smartctl.
     33    """
     34
     35    def __init__(self, num: int, name, flags: int, value, worst, thresh, attr_type, updated, when_failed, raw):
     36        self.num: int = num
     37        """**(int):** Attribute's ID as a decimal value (1-255)."""
     38        self.name: str = name
     39        """
     40        **(str):** Attribute's name, as reported by smartmontools' drive.db.
     41        """
     42        self.flags: int = flags
     43        """**(int):** Attribute flags as a bit value (ie: 0x0032)."""
     44        self._value: str = value
     45        """**(str):** Attribute's current normalized value."""
     46        self._worst: str = worst
     47        """**(str):** Worst recorded normalized value for this attribute."""
     48        self._thresh: str = thresh
     49        """**(str):** Attribute's failure threshold."""
     50        self.type: str = attr_type
     51        """**(str):** Attribute's type, generally 'pre-fail' or 'old-age'."""
     52        self.updated: str = updated
     53        """
     54        **(str):** When is this attribute updated? Generally 'Always' or
     55        'Offline'
     56        """
     57        self.when_failed: str = when_failed
     58        """
     59        **(str):** When did this attribute cross below
     60        `pySMART.attribute.Attribute.thresh`? Reads '-' when not failed.
     61        Generally either 'FAILING_NOW' or 'In_the_Past' otherwise.
     62        """
     63        self.raw = raw
     64        """**(str):** Attribute's current raw (non-normalized) value."""
     65
     66    @property
     67    def value_str(self) -> str:
     68        """Gets the attribute value
     69
     70        Returns:
     71            str: The attribute value in string format
     72        """
     73        return self._value
     74
     75    @property
     76    def value_int(self) -> int:
     77        """Gets the attribute value
     78
     79        Returns:
     80            int: The attribute value in integer format.
     81        """
     82        return int(self._value)
     83
     84    @property
     85    def value(self) -> str:
     86        """Gets the attribue value
     87
     88        Returns:
     89            str: The attribute value in string format
     90        """
     91        return self.value_str
     92
     93    @property
     94    def worst(self) -> int:
     95        """Gets the worst value
     96
     97        Returns:
     98            int: The attribute worst field in integer format
     99        """
    100        return int(self._worst)
    101
    102    @property
    103    def thresh(self) -> Optional[int]:
    104        """Gets the threshold value
    105
    106        Returns:
    107            int: The attribute threshold field in integer format
    108        """
    109        return None if self._thresh == '---' else int(self._thresh)
    110
    111    @property
    112    def raw_int(self) -> int:
    113        """Gets the raw value converted to int
    114        NOTE: Some values may not be correctly converted!
    115
    116        Returns:
    117            int: The attribute raw-value field in integer format.
    118            None: In case the raw string failed to be parsed
    119        """
    120        try:
    121            return int(re.search(r'\d+', self.raw).group())
    122        except:
    123            return None
    124
    125    def __repr__(self):
    126        """Define a basic representation of the class object."""
    127        return "<SMART Attribute %r %s/%s raw:%s>" % (
    128            self.name, self.value, self.thresh, self.raw)
    129
    130    def __str__(self):
    131        """
    132        Define a formatted string representation of the object's content.
    133        In the interest of not overflowing 80-character lines this does not
    134        print the value of `pySMART.attribute.Attribute.flags_hex`.
    135        """
    136        return "{0:>3} {1:23}{2:>4}{3:>4}{4:>4} {5:9}{6:8}{7:12}{8}".format(
    137            self.num,
    138            self.name,
    139            self.value,
    140            self.worst,
    141            self.thresh,
    142            self.type,
    143            self.updated,
    144            self.when_failed,
    145            self.raw
    146        )
    147
    148    def __getstate__(self):
    149        return {
    150            'num': self.num,
    151            'flags': self.flags,
    152            'raw': self.raw,
    153            'value': self.value,
    154            'worst': self.worst,
    155            'threshold': self.thresh,
    156            'type': self.type,
    157            'updated': self.updated,
    158            'when_failed': self.when_failed,
    159        }

Contains all of the information associated with a single SMART attribute in a `Device`'s SMART table. This data is intended to exactly mirror that obtained through smartctl.

Attribute( num: int, name, flags: int, value, worst, thresh, attr_type, updated, when_failed, raw) View Source

    35    def __init__(self, num: int, name, flags: int, value, worst, thresh, attr_type, updated, when_failed, raw):
    36        self.num: int = num
    37        """**(int):** Attribute's ID as a decimal value (1-255)."""
    38        self.name: str = name
    39        """
    40        **(str):** Attribute's name, as reported by smartmontools' drive.db.
    41        """
    42        self.flags: int = flags
    43        """**(int):** Attribute flags as a bit value (ie: 0x0032)."""
    44        self._value: str = value
    45        """**(str):** Attribute's current normalized value."""
    46        self._worst: str = worst
    47        """**(str):** Worst recorded normalized value for this attribute."""
    48        self._thresh: str = thresh
    49        """**(str):** Attribute's failure threshold."""
    50        self.type: str = attr_type
    51        """**(str):** Attribute's type, generally 'pre-fail' or 'old-age'."""
    52        self.updated: str = updated
    53        """
    54        **(str):** When is this attribute updated? Generally 'Always' or
    55        'Offline'
    56        """
    57        self.when_failed: str = when_failed
    58        """
    59        **(str):** When did this attribute cross below
    60        `pySMART.attribute.Attribute.thresh`? Reads '-' when not failed.
    61        Generally either 'FAILING_NOW' or 'In_the_Past' otherwise.
    62        """
    63        self.raw = raw
    64        """**(str):** Attribute's current raw (non-normalized) value."""

num: int

(int): Attribute's ID as a decimal value (1-255).

name: str

(str): Attribute's name, as reported by smartmontools' drive.db.

flags: int

(int): Attribute flags as a bit value (ie: 0x0032).

type: str

(str): Attribute's type, generally 'pre-fail' or 'old-age'.

updated: str

(str): When is this attribute updated? Generally 'Always' or 'Offline'

when_failed: str

(str): When did this attribute cross below `pySMART.Attribute.thresh`? Reads '-' when not failed. Generally either 'FAILING_NOW' or 'In_the_Past' otherwise.

raw

(str): Attribute's current raw (non-normalized) value.

value_str: str

Gets the attribute value

Returns: str: The attribute value in string format

value_int: int

Gets the attribute value

Returns: int: The attribute value in integer format.

value: str

Gets the attribue value

Returns: str: The attribute value in string format

worst: int

Gets the worst value

Returns: int: The attribute worst field in integer format

thresh: Union[int, NoneType]

Gets the threshold value

Returns: int: The attribute threshold field in integer format

raw_int: int

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

SMARTCTL = <pySMART.smartctl.Smartctl object>
class DeviceList: View Source

     37class DeviceList(object):
     38    """
     39    Represents a list of all the storage devices connected to this computer.
     40    """
     41
     42    def __init__(self, init: bool = True, smartctl=SMARTCTL):
     43        """Instantiates and optionally initializes the `DeviceList`.
     44
     45        Args:
     46            init (bool, optional): By default, `pySMART.device_list.DeviceList.devices`
     47                is populated with `Device` objects during instantiation. Setting init
     48                to False will skip initialization and create an empty
     49                `pySMART.device_list.DeviceList` object instead. Defaults to True.
     50            smartctl ([type], optional): This stablish the smartctl wrapper.
     51                Defaults the global `SMARTCTL` object and should be only
     52                overwritten on tests.
     53        """
     54
     55        self.devices: List[Device] = []
     56        """
     57        **(list of `Device`):** Contains all storage devices detected during
     58        instantiation, as `Device` objects.
     59        """
     60        self.smartctl: Smartctl = smartctl
     61        """The smartctl wrapper
     62        """
     63        if init:
     64            self._initialize()
     65
     66    def __repr__(self):
     67        """Define a basic representation of the class object."""
     68        rep = "<DeviceList contents:\n"
     69        for device in self.devices:
     70            rep += str(device) + '\n'
     71        return rep + '>'
     72        # return "<DeviceList contents:%r>" % (self.devices)
     73
     74    def _cleanup(self):
     75        """
     76        Removes duplicate ATA devices that correspond to an existing CSMI
     77        device. Also removes any device with no capacity value, as this
     78        indicates removable storage, ie: CD/DVD-ROM, ZIP, etc.
     79        """
     80        # We can't operate directly on the list while we're iterating
     81        # over it, so we collect indeces to delete and remove them later
     82        to_delete = []
     83        # Enumerate the list to get tuples containing indeces and values
     84        for index, device in enumerate(self.devices):
     85            if device.interface == 'csmi':
     86                for otherindex, otherdevice in enumerate(self.devices):
     87                    if (otherdevice.interface == 'ata' or
     88                            otherdevice.interface == 'sata'):
     89                        if device.serial == otherdevice.serial:
     90                            to_delete.append(otherindex)
     91                            device._sd_name = otherdevice.name
     92            if device.capacity is None and index not in to_delete:
     93                to_delete.append(index)
     94        # Recreate the self.devices list without the marked indeces
     95        self.devices[:] = [v for i, v in enumerate(self.devices)
     96                           if i not in to_delete]
     97
     98    def _initialize(self):
     99        """
    100        Scans system busses for attached devices and add them to the
    101        `DeviceList` as `Device` objects.
    102        """
    103
    104        for line in self.smartctl.scan():
    105            if not ('failed:' in line or line == ''):
    106                groups = re.compile(
    107                    '^(\S+)\s+-d\s+(\S+)').match(line).groups()
    108                name = groups[0]
    109                interface = groups[1]
    110                self.devices.append(
    111                    Device(name, interface=interface, smartctl=self.smartctl))
    112
    113        # Remove duplicates and unwanted devices (optical, etc.) from the list
    114        self._cleanup()
    115        # Sort the list alphabetically by device name
    116        self.devices.sort(key=lambda device: device.name)
    117
    118    def __getitem__(self, index: int) -> Device:
    119        """Returns an element from self.devices
    120
    121        Args:
    122            index (int): An index of self.devices
    123
    124        Returns:
    125            Device: Returns a Device that is located on the asked index
    126        """
    127        return self.devices[index]

Represents a list of all the storage devices connected to this computer.

DeviceList(init: bool = True, smartctl=<pySMART.smartctl.Smartctl object>) View Source

    42    def __init__(self, init: bool = True, smartctl=SMARTCTL):
    43        """Instantiates and optionally initializes the `DeviceList`.
    44
    45        Args:
    46            init (bool, optional): By default, `pySMART.device_list.DeviceList.devices`
    47                is populated with `Device` objects during instantiation. Setting init
    48                to False will skip initialization and create an empty
    49                `pySMART.device_list.DeviceList` object instead. Defaults to True.
    50            smartctl ([type], optional): This stablish the smartctl wrapper.
    51                Defaults the global `SMARTCTL` object and should be only
    52                overwritten on tests.
    53        """
    54
    55        self.devices: List[Device] = []
    56        """
    57        **(list of `Device`):** Contains all storage devices detected during
    58        instantiation, as `Device` objects.
    59        """
    60        self.smartctl: Smartctl = smartctl
    61        """The smartctl wrapper
    62        """
    63        if init:
    64            self._initialize()

Instantiates and optionally initializes the `DeviceList`.

Args: init (bool, optional): By default, `pySMART.DeviceList.devices` is populated with `Device` objects during instantiation. Setting init to False will skip initialization and create an empty `pySMART.DeviceList` object instead. Defaults to True. smartctl ([type], optional): This stablish the smartctl wrapper. Defaults the global `SMARTCTL` object and should be only overwritten on tests.

devices: List[pySMART.Device]

(list of `Device`): Contains all storage devices detected during instantiation, as `Device` objects.

smartctl: pySMART.smartctl.Smartctl

The smartctl wrapper

class Device: View Source

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

Device( name: str, interface: Union[str, NoneType] = None, abridged: bool = False, smart_options: Union[str, List[str], NoneType] = None, smartctl: pySMART.smartctl.Smartctl = <pySMART.smartctl.Smartctl object>) View Source

     88    def __init__(self, name: str, interface: Optional[str] = None, abridged: bool = False, smart_options: Union[str, List[str], None] = None, smartctl: Smartctl = SMARTCTL):
     89        """Instantiates and initializes the `pySMART.device.Device`."""
     90        if not (
     91                interface is None or
     92                smartctl_isvalid_type(interface.lower())
     93        ):
     94            raise ValueError(
     95                'Unknown interface: {0} specified for {1}'.format(interface, name))
     96        self.abridged = abridged or interface == 'UNKNOWN INTERFACE'
     97        if smart_options is not None:
     98            if isinstance(smart_options,  str):
     99                smart_options = smart_options.split(' ')
    100            smartctl.add_options(smart_options)
    101        self.smartctl = smartctl
    102        """
    103        """
    104        self.name: str = name.replace('/dev/', '').replace('nvd', 'nvme')
    105        """
    106        **(str):** Device's hardware ID, without the '/dev/' prefix.
    107        (ie: sda (Linux), pd0 (Windows))
    108        """
    109        self.model: Optional[str] = None
    110        """**(str):** Device's model number."""
    111        self.serial: Optional[str] = None
    112        """**(str):** Device's serial number."""
    113        self.vendor: Optional[str] = None
    114        """**(str):** Device's vendor (if any)."""
    115        self._interface: Optional[str] = None if interface == 'UNKNOWN INTERFACE' else interface
    116        """
    117        **(str):** Device's interface type. Must be one of:
    118            * **ATA** - Advanced Technology Attachment
    119            * **SATA** - Serial ATA
    120            * **SCSI** - Small Computer Systems Interface
    121            * **SAS** - Serial Attached SCSI
    122            * **SAT** - SCSI-to-ATA Translation (SATA device plugged into a
    123            SAS port)
    124            * **CSMI** - Common Storage Management Interface (Intel ICH /
    125            Matrix RAID)
    126        Generally this should not be specified to allow auto-detection to
    127        occur. Otherwise, this value overrides the auto-detected type and could
    128        produce unexpected or no data.
    129        """
    130        self._capacity: Optional[int] = None
    131        """**(str):** Device's user capacity as reported directly by smartctl (RAW)."""
    132        self._capacity_human: Optional[str] = None
    133        """**(str):** Device's user capacity (human readable) as reported directly by smartctl (RAW)."""
    134        self.firmware: Optional[str] = None
    135        """**(str):** Device's firmware version."""
    136        self.smart_capable: bool = 'nvme' in self.name
    137        """
    138        **(bool):** True if the device has SMART Support Available.
    139        False otherwise. This is useful for VMs amongst other things.
    140        """
    141        self.smart_enabled: bool = 'nvme' in self.name
    142        """
    143        **(bool):** True if the device supports SMART (or SCSI equivalent) and
    144        has the feature set enabled. False otherwise.
    145        """
    146        self.assessment: Optional[str] = None
    147        """
    148        **(str):** SMART health self-assessment as reported by the device.
    149        """
    150        self.messages: List[str] = []
    151        """
    152        **(list of str):** Contains any SMART warnings or other error messages
    153        reported by the device (ie: ascq codes).
    154        """
    155        self.is_ssd: bool = True if 'nvme' in self.name else False
    156        """
    157        **(bool):** True if this device is a Solid State Drive.
    158        False otherwise.
    159        """
    160        self.rotation_rate: Optional[int] = None
    161        """
    162        **(int):** The Roatation Rate of the Drive if it is not a SSD.
    163        The Metric is RPM.
    164        """
    165        self.attributes: List[Optional[Attribute]] = [None] * 256
    166        """
    167        **(list of `Attribute`):** Contains the complete SMART table
    168        information for this device, as provided by smartctl. Indexed by
    169        attribute #, values are set to 'None' for attributes not suported by
    170        this device.
    171        """
    172        self.test_capabilities = {
    173            'offline': False,  # SMART execute Offline immediate (ATA only)
    174            'short': 'nvme' not in self.name,  # SMART short Self-test
    175            'long': 'nvme' not in self.name,  # SMART long Self-test
    176            'conveyance': False,  # SMART Conveyance Self-Test (ATA only)
    177            'selective': False,  # SMART Selective Self-Test (ATA only)
    178        }
    179        # Note have not included 'offline' test for scsi as it runs in the foregorund
    180        # mode. While this may be beneficial to us in someways it is against the
    181        # general layout and pattern that the other tests issued using pySMART are
    182        # followed hence not doing it currently
    183        """
    184        **(dict): ** This dictionary contains key == 'Test Name' and
    185        value == 'True/False' of self-tests that this device is capable of.
    186        """
    187        # Note: The above are just default values and can/will be changed
    188        # upon update() when the attributes and type of the disk is actually
    189        # determined.
    190        self.tests: List[TestEntry] = []
    191        """
    192        **(list of `TestEntry`):** Contains the complete SMART self-test log
    193        for this device, as provided by smartctl.
    194        """
    195        self._test_running = False
    196        """
    197        **(bool):** True if a self-test is currently being run.
    198        False otherwise.
    199        """
    200        self._test_ECD = None
    201        """
    202        **(str):** Estimated completion time of the running SMART selftest.
    203        Not provided by SAS/SCSI devices.
    204        """
    205        self._test_progress = None
    206        """
    207        **(int):** Estimate progress percantage of the running SMART selftest.
    208        """
    209        self.diagnostics: Diagnostics = Diagnostics()
    210        """
    211        **Diagnostics** Contains parsed and processed diagnostic information
    212        extracted from the SMART information. Currently only populated for
    213        SAS and SCSI devices, since ATA/SATA SMART attributes are manufacturer
    214        proprietary.
    215        """
    216        self.temperature: Optional[int] = None
    217        """
    218        **(int or None): Since SCSI disks do not report attributes like ATA ones
    219        we need to grep/regex the shit outta the normal "smartctl -a" output.
    220        In case the device have more than one temperature sensor the first value
    221        will be stored here too.
    222        Note: Temperatures are always in Celsius (if possible).
    223        """
    224        self.temperatures: Dict[int, int] = {}
    225        """
    226        **(dict of int): NVMe disks usually report multiple temperatures, which
    227        will be stored here if available. Keys are sensor numbers as reported in
    228        output data.
    229        Note: Temperatures are always in Celsius (if possible).
    230        """
    231        self.logical_sector_size: Optional[int] = None
    232        """
    233        **(int):** The logical sector size of the device (or LBA).
    234        """
    235        self.physical_sector_size: Optional[int] = None
    236        """
    237        **(int):** The physical sector size of the device.
    238        """
    239        self.if_attributes: Union[None, NvmeAttributes] = None
    240        """
    241        **(NvmeAttributes):** This object may vary for each device interface attributes.
    242        It will store all data obtained from smartctl
    243        """
    244
    245        if self.name is None:
    246            warnings.warn(
    247                "\nDevice '{0}' does not exist! This object should be destroyed.".format(
    248                    name)
    249            )
    250            return
    251        # If no interface type was provided, scan for the device
    252        # Lets do this only for the non-abridged case
    253        # (we can work with no interface for abridged case)
    254        elif self._interface is None and not self.abridged:
    255            logger.trace(
    256                "Determining interface of disk: {0}".format(self.name))
    257            raw, returncode = self.smartctl.generic_call(
    258                ['-d', 'test', self.dev_reference])
    259
    260            if len(raw) > 0:
    261                # I do not like this parsing logic but it works for now!
    262                # just for reference _stdout.split('\n') gets us
    263                # something like
    264                # [
    265                #     ...copyright string...,
    266                #     '',
    267                #     "/dev/ada2: Device of type 'atacam' [ATA] detected",
    268                #     "/dev/ada2: Device of type 'atacam' [ATA] opened",
    269                #     ''
    270                # ]
    271                # The above example should be enough for anyone to understand the line below
    272                try:
    273                    self._interface = raw[-2].split("'")[1]
    274                    if self._interface == "nvme":  # if nvme set SMART to true
    275                        self.smart_capable = True
    276                        self.smart_enabled = True
    277                except:
    278                    # for whatever reason we could not get the interface type
    279                    # we should mark this as an `abbridged` case and move on
    280                    self._interface = None
    281                    self.abbridged = True
    282                # TODO: Uncomment the classify call if we ever find out that we need it
    283                # Disambiguate the generic interface to a specific type
    284                # self._classify()
    285            else:
    286                warnings.warn(
    287                    "\nDevice '{0}' does not exist! This object should be destroyed.".format(
    288                        name)
    289                )
    290                return
    291        # If a valid device was detected, populate its information
    292        # OR if in unabridged mode, then do it even without interface info
    293        if self._interface is not None or self.abridged:
    294            self.update()

Instantiates and initializes the `pySMART.Device`.

name: str

(str): Device's hardware ID, without the '/dev/' prefix. (ie: sda (Linux), pd0 (Windows))

model: Union[str, NoneType]

(str): Device's model number.

serial: Union[str, NoneType]

(str): Device's serial number.

vendor: Union[str, NoneType]

(str): Device's vendor (if any).

firmware: Union[str, NoneType]

(str): Device's firmware version.

smart_capable: bool

(bool): True if the device has SMART Support Available. False otherwise. This is useful for VMs amongst other things.

smart_enabled: bool

(bool): True if the device supports SMART (or SCSI equivalent) and has the feature set enabled. False otherwise.

assessment: Union[str, NoneType]

(str): SMART health self-assessment as reported by the device.

messages: List[str]

(list of str): Contains any SMART warnings or other error messages reported by the device (ie: ascq codes).

is_ssd: bool

(bool): True if this device is a Solid State Drive. False otherwise.

rotation_rate: Union[int, NoneType]

(int): The Roatation Rate of the Drive if it is not a SSD. The Metric is RPM.

attributes: List[Union[pySMART.Attribute, NoneType]]

(list of `Attribute`): Contains the complete SMART table information for this device, as provided by smartctl. Indexed by attribute #, values are set to 'None' for attributes not suported by this device.

test_capabilities

*(dict): * This dictionary contains key == 'Test Name' and value == 'True/False' of self-tests that this device is capable of.

tests: List[pySMART.TestEntry]

(list of `TestEntry`): Contains the complete SMART self-test log for this device, as provided by smartctl.

diagnostics: pySMART.diagnostics.Diagnostics

Diagnostics Contains parsed and processed diagnostic information extracted from the SMART information. Currently only populated for SAS and SCSI devices, since ATA/SATA SMART attributes are manufacturer proprietary.

temperature: Union[int, NoneType]

**(int or None): Since SCSI disks do not report attributes like ATA ones we need to grep/regex the shit outta the normal "smartctl -a" output. In case the device have more than one temperature sensor the first value will be stored here too. Note: Temperatures are always in Celsius (if possible).

temperatures: Dict[int, int]

**(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).

logical_sector_size: Union[int, NoneType]

(int): The logical sector size of the device (or LBA).

physical_sector_size: Union[int, NoneType]

(int): The physical sector size of the device.

if_attributes: Union[NoneType, pySMART.interface.nvme.NvmeAttributes]

(NvmeAttributes): This object may vary for each device interface attributes. It will store all data obtained from smartctl

dev_interface: Union[str, NoneType]

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.

smartctl_interface: Union[str, NoneType]

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.

interface: Union[str, NoneType]

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.

dev_reference: str

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.

capacity: Union[str, NoneType]

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

diags: Dict[str, str]

Gets the old/deprecated version of SCSI/SAS diags atribute.

size_raw: Union[str, NoneType]

Returns the capacity in the raw smartctl format.

Returns: str: The capacity in the raw smartctl format

size: int

Returns the capacity in bytes

Returns: int: The capacity in bytes

sector_size: int

Returns the sector size of the device.

Returns: int: The sector size of the device in Bytes. If undefined, we'll assume 512B

def smart_toggle(self, action: str) -> Tuple[bool, List[str]]: View Source

    455    def smart_toggle(self, action: str) -> Tuple[bool, List[str]]:
    456        """
    457        A basic function to enable/disable SMART on device.
    458
    459        # Args:
    460        * **action (str):** Can be either 'on'(for enabling) or 'off'(for disabling).
    461
    462        # Returns"
    463        * **(bool):** Return True (if action succeded) else False
    464        * **(List[str]):** None if option succeded else contains the error message.
    465        """
    466        # Lets make the action verb all lower case
    467        if self._interface == 'nvme':
    468            return False, ['NVME devices do not currently support toggling SMART enabled']
    469        action_lower = action.lower()
    470        if action_lower not in ['on', 'off']:
    471            return False, ['Unsupported action {0}'.format(action)]
    472        # Now lets check if the device's smart enabled status is already that of what
    473        # the supplied action is intending it to be. If so then just return successfully
    474        if self.smart_enabled:
    475            if action_lower == 'on':
    476                return True, []
    477        else:
    478            if action_lower == 'off':
    479                return True, []
    480        if self._interface is not None:
    481            raw, returncode = self.smartctl.generic_call(
    482                ['-s', action_lower, '-d', self._interface, self.dev_reference])
    483        else:
    484            raw, returncode = self.smartctl.generic_call(
    485                ['-s', action_lower, self.dev_reference])
    486
    487        if returncode != 0:
    488            return False, raw
    489        # if everything worked out so far lets perform an update() and check the result
    490        self.update()
    491        if action_lower == 'off' and self.smart_enabled:
    492            return False, ['Failed to turn SMART off.']
    493        if action_lower == 'on' and not self.smart_enabled:
    494            return False, ['Failed to turn SMART on.']
    495        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.

def all_attributes(self, print_fn=<built-in function print>): View Source

    497    def all_attributes(self, print_fn=print):
    498        """
    499        Prints the entire SMART attribute table, in a format similar to
    500        the output of smartctl.
    501        allows usage of custom print function via parameter print_fn by default uses print
    502        """
    503        header_printed = False
    504        for attr in self.attributes:
    505            if attr is not None:
    506                if not header_printed:
    507                    print_fn("{0:>3} {1:24}{2:4}{3:4}{4:4}{5:9}{6:8}{7:12}{8}"
    508                             .format('ID#', 'ATTRIBUTE_NAME', 'CUR', 'WST', 'THR', 'TYPE', 'UPDATED', 'WHEN_FAIL',
    509                                     'RAW'))
    510                    header_printed = True
    511                print_fn(attr)
    512        if not header_printed:
    513            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

def all_selftests(self): View Source

    515    def all_selftests(self):
    516        """
    517        Prints the entire SMART self-test log, in a format similar to
    518        the output of smartctl.
    519        """
    520        if self.tests:
    521            all_tests = []
    522            if smartctl_type(self._interface) == 'scsi':
    523                header = "{0:3}{1:17}{2:23}{3:7}{4:14}{5:15}".format(
    524                    'ID',
    525                    'Test Description',
    526                    'Status',
    527                    'Hours',
    528                    '1st_Error@LBA',
    529                    '[SK  ASC  ASCQ]'
    530                )
    531            else:
    532                header = ("{0:3}{1:17}{2:30}{3:5}{4:7}{5:17}".format(
    533                    'ID',
    534                    'Test_Description',
    535                    'Status',
    536                    'Left',
    537                    'Hours',
    538                    '1st_Error@LBA'))
    539            all_tests.append(header)
    540            for test in self.tests:
    541                all_tests.append(str(test))
    542
    543            return all_tests
    544        else:
    545            no_tests = 'No self-tests have been logged for this device.'
    546            return no_tests

Prints the entire SMART self-test log, in a format similar to the output of smartctl.

def get_selftest_result(self, output=None): View Source

    632    def get_selftest_result(self, output=None):
    633        """
    634        Refreshes a device's `pySMART.device.Device.tests` attribute to obtain
    635        the latest test results. If a new test result is obtained, its content
    636        is returned.
    637
    638        # Args:
    639        * **output (str, optional):** If set to 'str', the string
    640        representation of the most recent test result will be returned, instead
    641        of a `Test_Entry` object.
    642
    643        # Returns:
    644        * **(int):** Return status code. One of the following:
    645            * 0 - Success. Object (or optionally, string rep) is attached.
    646            * 1 - Self-test in progress. Must wait for it to finish.
    647            * 2 - No new test results.
    648            * 3 - The Self-test was Aborted by host
    649        * **(`Test_Entry` or str):** Most recent `Test_Entry` object (or
    650        optionally it's string representation) if new data exists.  Status
    651        message string on failure.
    652        * **(int):** Estimate progress percantage of the running SMART selftest, if known.
    653        Otherwise 'None'.
    654        """
    655        # SCSI self-test logs hold 20 entries while ATA logs hold 21
    656        if smartctl_type(self._interface) == 'scsi':
    657            maxlog = 20
    658        else:
    659            maxlog = 21
    660        # If we looked only at the most recent test result we could be fooled
    661        # by two short tests run close together (within the same hour)
    662        # appearing identical. Comparing the length of the log adds some
    663        # confidence until it maxes, as above. Comparing the least-recent test
    664        # result greatly diminishes the chances that two sets of two tests each
    665        # were run within an hour of themselves, but with 16-17 other tests run
    666        # in between them.
    667        if self.tests:
    668            _first_entry = self.tests[0]
    669            _len = len(self.tests)
    670            _last_entry = self.tests[_len - 1]
    671        else:
    672            _len = 0
    673        self.update()
    674        # Since I have changed the update() parsing to DTRT to pickup currently
    675        # running selftests we can now purely rely on that for self._test_running
    676        # Thus check for that variable first and return if it is True with appropos message.
    677        if self._test_running is True:
    678            return 1, 'Self-test in progress. Please wait.', self._test_progress
    679        # Check whether the list got longer (ie: new entry)
    680        # If so return the newest test result
    681        # If not, because it's max size already, check for new entries
    682        if (
    683                (len(self.tests) != _len) or
    684                (
    685                    len == maxlog and
    686                    (
    687                        _first_entry.type != self.tests[0].type or
    688                        _first_entry.hours != self.tests[0].hours or
    689                        _last_entry.type != self.tests[len(self.tests) - 1].type or
    690                        _last_entry.hours != self.tests[len(
    691                            self.tests) - 1].hours
    692                    )
    693                )
    694        ):
    695            return (
    696                0 if 'Aborted' not in self.tests[0].status else 3,
    697                str(self.tests[0]) if output == 'str' else self.tests[0],
    698                None
    699            )
    700        else:
    701            return 2, 'No new self-test results found.', None

Refreshes a device's `pySMART.Device.tests` attribute to obtain the latest test results. If a new test result is obtained, its content is returned.

# Args:

- output (str, optional): If set to 'str', the string representation of the most recent test result will be returned, instead of a `Test_Entry` object.

# Returns:

- (int): Return status code. One of the following:
  - 0 - Success. Object (or optionally, string rep) is attached.
  - 1 - Self-test in progress. Must wait for it to finish.
  - 2 - No new test results.
  - 3 - The Self-test was Aborted by host
- (`Test_Entry` or str): Most recent `Test_Entry` object (or optionally it's string representation) if new data exists. Status message string on failure.
- (int): Estimate progress percantage of the running SMART selftest, if known. Otherwise 'None'.

def abort_selftest(self): View Source

    703    def abort_selftest(self):
    704        """
    705        Aborts non-captive SMART Self Tests.   Note that this command
    706        will  abort the Offline Immediate Test routine only if your disk
    707        has the "Abort Offline collection upon new command"  capability.
    708
    709        # Args: Nothing (just aborts directly)
    710
    711        # Returns:
    712        * **(int):** The returncode of calling `smartctl -X device_path`
    713        """
    714        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`

def run_selftest(self, test_type, ETA_type='date'): View Source

    716    def run_selftest(self, test_type, ETA_type='date'):
    717        """
    718        Instructs a device to begin a SMART self-test. All tests are run in
    719        'offline' / 'background' mode, allowing normal use of the device while
    720        it is being tested.
    721
    722        # Args:
    723        * **test_type (str):** The type of test to run. Accepts the following
    724        (not case sensitive):
    725            * **short** - Brief electo-mechanical functionality check.
    726            Generally takes 2 minutes or less.
    727            * **long** - Thorough electro-mechanical functionality check,
    728            including complete recording media scan. Generally takes several
    729            hours.
    730            * **conveyance** - Brief test used to identify damage incurred in
    731            shipping. Generally takes 5 minutes or less. **This test is not
    732            supported by SAS or SCSI devices.**
    733            * **offline** - Runs SMART Immediate Offline Test. The effects of
    734            this test are visible only in that it updates the SMART Attribute
    735            values, and if errors are found they will appear in the SMART error
    736            log, visible with the '-l error' option to smartctl. **This test is
    737            not supported by SAS or SCSI devices in pySMART use cli smartctl for
    738            running 'offline' selftest (runs in foreground) on scsi devices.**
    739            * **ETA_type** - Format to return the estimated completion time/date
    740            in. Default is 'date'. One could otherwise specidy 'seconds'.
    741            Again only for ATA devices.
    742
    743        # Returns:
    744        * **(int):** Return status code.  One of the following:
    745            * 0 - Self-test initiated successfully
    746            * 1 - Previous self-test running. Must wait for it to finish.
    747            * 2 - Unknown or unsupported (by the device) test type requested.
    748            * 3 - Unspecified smartctl error. Self-test not initiated.
    749        * **(str):** Return status message.
    750        * **(str)/(float):** Estimated self-test completion time if a test is started.
    751        The optional argument of 'ETA_type' (see above) controls the return type.
    752        if 'ETA_type' == 'date' then a date string is returned else seconds(float)
    753        is returned.
    754        Note: The self-test completion time can only be obtained for ata devices.
    755        Otherwise 'None'.
    756        """
    757        # Lets call get_selftest_result() here since it does an update() and
    758        # checks for an existing selftest is running or not, this way the user
    759        # can issue a test from the cli and this can still pick that up
    760        # Also note that we do not need to obtain the results from this as the
    761        # data is already stored in the Device class object's variables
    762        self.get_selftest_result()
    763        if self._test_running:
    764            return 1, 'Self-test in progress. Please wait.', self._test_ECD
    765        test_type = test_type.lower()
    766        interface = smartctl_type(self._interface)
    767        try:
    768            if not self.test_capabilities[test_type]:
    769                return (
    770                    2,
    771                    "Device {0} does not support the '{1}' test ".format(
    772                        self.name, test_type),
    773                    None
    774                )
    775        except KeyError:
    776            return 2, "Unknown test type '{0}' requested.".format(test_type), None
    777
    778        raw, rc = self.smartctl.test_start(
    779            interface, test_type, self.dev_reference)
    780        _success = False
    781        _running = False
    782        for line in raw:
    783            if 'has begun' in line:
    784                _success = True
    785                self._test_running = True
    786            if 'aborting current test' in line:
    787                _running = True
    788                try:
    789                    self._test_progress = 100 - \
    790                        int(line.split('(')[-1].split('%')[0])
    791                except ValueError:
    792                    pass
    793
    794            if _success and 'complete after' in line:
    795                self._test_ECD = line[25:].rstrip()
    796                if ETA_type == 'seconds':
    797                    self._test_ECD = mktime(
    798                        strptime(self._test_ECD, '%a %b %d %H:%M:%S %Y')) - time()
    799                self._test_progress = 0
    800        if _success:
    801            return 0, 'Self-test started successfully', self._test_ECD
    802        else:
    803            if _running:
    804                return 1, 'Self-test already in progress. Please wait.', self._test_ECD
    805            else:
    806                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'.

def run_selftest_and_wait(self, test_type, output=None, polling=5, progress_handler=None): View Source

    808    def run_selftest_and_wait(self, test_type, output=None, polling=5, progress_handler=None):
    809        """
    810        This is essentially a wrapper around run_selftest() such that we
    811        call self.run_selftest() and wait on the running selftest till
    812        it finished before returning.
    813        The above holds true for all pySMART supported tests with the
    814        exception of the 'offline' test (ATA only) as it immediately
    815        returns, since the entire test only affects the smart error log
    816        (if any errors found) and updates the SMART attributes. Other
    817        than that it is not visibile anywhere else, so we start it and
    818        simply return.
    819        # Args:
    820        * **test_type (str):** The type of test to run. Accepts the following
    821        (not case sensitive):
    822            * **short** - Brief electo-mechanical functionality check.
    823            Generally takes 2 minutes or less.
    824            * **long** - Thorough electro-mechanical functionality check,
    825            including complete recording media scan. Generally takes several
    826            hours.
    827            * **conveyance** - Brief test used to identify damage incurred in
    828            shipping. Generally takes 5 minutes or less. **This test is not
    829            supported by SAS or SCSI devices.**
    830            * **offline** - Runs SMART Immediate Offline Test. The effects of
    831            this test are visible only in that it updates the SMART Attribute
    832            values, and if errors are found they will appear in the SMART error
    833            log, visible with the '-l error' option to smartctl. **This test is
    834            not supported by SAS or SCSI devices in pySMART use cli smartctl for
    835            running 'offline' selftest (runs in foreground) on scsi devices.**
    836        * **output (str, optional):** If set to 'str', the string
    837            representation of the most recent test result will be returned,
    838            instead of a `Test_Entry` object.
    839        * **polling (int, default=5):** The time duration to sleep for between
    840            checking for test_results and progress.
    841        * **progress_handler (function, optional):** This if provided is called
    842            with self._test_progress as the supplied argument everytime a poll to
    843            check the status of the selftest is done.
    844        # Returns:
    845        * **(int):** Return status code.  One of the following:
    846            * 0 - Self-test executed and finished successfully
    847            * 1 - Previous self-test running. Must wait for it to finish.
    848            * 2 - Unknown or illegal test type requested.
    849            * 3 - The Self-test was Aborted by host
    850            * 4 - Unspecified smartctl error. Self-test not initiated.
    851        * **(`Test_Entry` or str):** Most recent `Test_Entry` object (or
    852        optionally it's string representation) if new data exists.  Status
    853        message string on failure.
    854        """
    855        test_initiation_result = self.run_selftest(test_type)
    856        if test_initiation_result[0] != 0:
    857            return test_initiation_result[:2]
    858        if test_type == 'offline':
    859            self._test_running = False
    860        # if not then the test initiated correctly and we can start the polling.
    861        # For now default 'polling' value is 5 seconds if not specified by the user
    862
    863        # Do an initial check, for good measure.
    864        # In the probably impossible case that self._test_running is instantly False...
    865        selftest_results = self.get_selftest_result(output=output)
    866        while self._test_running:
    867            if selftest_results[0] != 1:
    868                # the selftest is run and finished lets return with the results
    869                break
    870            # Otherwise see if we are provided with the progress_handler to update progress
    871            if progress_handler is not None:
    872                progress_handler(
    873                    selftest_results[2] if selftest_results[2] is not None else 50)
    874            # Now sleep 'polling' seconds before checking the progress again
    875            sleep(polling)
    876
    877            # Check after the sleep to ensure we return the right result, and not an old one.
    878            selftest_results = self.get_selftest_result(output=output)
    879
    880        # Now if (selftes_results[0] == 2) i.e No new selftest (because the same
    881        # selftest was run twice within the last hour) but we know for a fact that
    882        # we just ran a new selftest then just return the latest entry in self.tests
    883        if selftest_results[0] == 2:
    884            selftest_return_value = 0 if 'Aborted' not in self.tests[0].status else 3
    885            return selftest_return_value, str(self.tests[0]) if output == 'str' else self.tests[0]
    886        return selftest_results[:2]

This is essentially a wrapper around run_selftest() such that we call self.run_selftest() and wait on the running selftest till it finished before returning. The above holds true for all pySMART supported tests with the exception of the 'offline' test (ATA only) as it immediately returns, since the entire test only affects the smart error log (if any errors found) and updates the SMART attributes. Other than that it is not visibile anywhere else, so we start it and simply return.

# Args:

- test_type (str): The type of test to run. Accepts the following (not case sensitive):
  - short - Brief electo-mechanical functionality check. Generally takes 2 minutes or less.
  - long - Thorough electro-mechanical functionality check, including complete recording media scan. Generally takes several hours.
  - conveyance - Brief test used to identify damage incurred in shipping. Generally takes 5 minutes or less. This test is not supported by SAS or SCSI devices.
  - offline - Runs SMART Immediate Offline Test. The effects of this test are visible only in that it updates the SMART Attribute values, and if errors are found they will appear in the SMART error log, visible with the '-l error' option to smartctl. This test is not supported by SAS or SCSI devices in pySMART use cli smartctl for running 'offline' selftest (runs in foreground) on scsi devices.
- output (str, optional): If set to 'str', the string representation of the most recent test result will be returned, instead of a `Test_Entry` object.
- polling (int, default=5): The time duration to sleep for between checking for test_results and progress.

- progress_handler (function, optional): This if provided is called with self._test_progress as the supplied argument everytime a poll to check the status of the selftest is done.

  # Returns:

- (int): Return status code. One of the following:

  - 0 - Self-test executed and finished successfully
  - 1 - Previous self-test running. Must wait for it to finish.
  - 2 - Unknown or illegal test type requested.
  - 3 - The Self-test was Aborted by host
  - 4 - Unspecified smartctl error. Self-test not initiated.
- (`Test_Entry` or str): Most recent `Test_Entry` object (or optionally it's string representation) if new data exists. Status message string on failure.

def update(self): View Source

     888    def update(self):
     889        """
     890        Queries for device information using smartctl and updates all
     891        class members, including the SMART attribute table and self-test log.
     892        Can be called at any time to refresh the `pySMART.device.Device`
     893        object's data content.
     894        """
     895        # set temperature back to None so that if update() is called more than once
     896        # any logic that relies on self.temperature to be None to rescan it works.it
     897        self.temperature = None
     898        # same for temperatures
     899        self.temperatures = {}
     900        if self.abridged:
     901            interface = None
     902            raw = self.smartctl.info(self.dev_reference)
     903
     904        else:
     905            interface = smartctl_type(self._interface)
     906            raw = self.smartctl.all(
     907                self.dev_reference, interface)
     908
     909        parse_self_tests = False
     910        parse_running_test = False
     911        parse_ascq = False
     912        message = ''
     913        self.tests = []
     914        self._test_running = False
     915        self._test_progress = None
     916        # Lets skip the first couple of non-useful lines
     917        _stdout = raw[4:]
     918
     919        #######################################
     920        #           Encoding fixing           #
     921        #######################################
     922        # In some scenarios, smartctl returns some lines with a different/strange encoding
     923        # This is a workaround to fix that
     924        for i, line in enumerate(_stdout):
     925            # character ' ' (U+202F) should be removed
     926            _stdout[i] = line.replace('\u202f', '')
     927
     928        #######################################
     929        #   Dedicated interface attributes    #
     930        #######################################
     931
     932        if interface == 'nvme':
     933            self.if_attributes = NvmeAttributes(iter(_stdout))
     934        else:
     935            self.if_attributes = None
     936
     937        #######################################
     938        #    Global / generic  attributes     #
     939        #######################################
     940        stdout_iter = iter(_stdout)
     941        for line in stdout_iter:
     942            if line.strip() == '':  # Blank line stops sub-captures
     943                if parse_self_tests is True:
     944                    parse_self_tests = False
     945                if parse_ascq:
     946                    parse_ascq = False
     947                    self.messages.append(message)
     948            if parse_ascq:
     949                message += ' ' + line.lstrip().rstrip()
     950            if parse_self_tests:
     951                num = line[0:3]
     952                if '#' not in num:
     953                    continue
     954
     955                # Detect Test Format
     956
     957                ## SCSI/SAS FORMAT ##
     958                # Example smartctl output
     959                # SMART Self-test log
     960                # Num  Test              Status                 segment  LifeTime  LBA_first_err [SK ASC ASQ]
     961                #      Description                              number   (hours)
     962                # # 1  Background short  Completed                   -   33124                 - [-   -    -]
     963                format_scsi = re.compile(
     964                    r'^[#\s]*([^\s]+)\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s+\[([^\s]+)\s+([^\s]+)\s+([^\s]+)\]$').match(line)
     965
     966                if format_scsi is not None:
     967                    format = 'scsi'
     968                    parsed = format_scsi.groups()
     969                    num = int(parsed[0])
     970                    test_type = parsed[1]
     971                    status = parsed[2]
     972                    segment = parsed[3]
     973                    hours = parsed[4]
     974                    lba = parsed[5]
     975                    sense = parsed[6]
     976                    asc = parsed[7]
     977                    ascq = parsed[8]
     978                    self.tests.append(TestEntry(
     979                        format,
     980                        num,
     981                        test_type,
     982                        status,
     983                        hours,
     984                        lba,
     985                        segment=segment,
     986                        sense=sense,
     987                        asc=asc,
     988                        ascq=ascq
     989                    ))
     990                else:
     991                    ## ATA FORMAT ##
     992                    # Example smartctl output:
     993                    # SMART Self-test log structure revision number 1
     994                    # Num  Test_Description    Status                  Remaining  LifeTime(hours)  LBA_of_first_error
     995                    # # 1  Extended offline    Completed without error       00%     46660         -
     996                    format = 'ata'
     997                    parsed = re.compile(
     998                        r'^[#\s]*([^\s]+)\s{2,}(.*[^\s])\s{2,}(.*[^\s])\s{1,}(.*[^\s])\s{2,}(.*[^\s])\s{2,}(.*[^\s])$').match(line).groups()
     999                    num = parsed[0]
    1000                    test_type = parsed[1]
    1001                    status = parsed[2]
    1002                    remain = parsed[3]
    1003                    hours = parsed[4]
    1004                    lba = parsed[5]
    1005
    1006                    try:
    1007                        num = int(num)
    1008                    except:
    1009                        num = None
    1010
    1011                    self.tests.append(
    1012                        TestEntry(format, num, test_type, status,
    1013                                  hours, lba, remain=remain)
    1014                    )
    1015            # Basic device information parsing
    1016            if any_in(line, 'Device Model', 'Product', 'Model Number'):
    1017                self.model = line.split(':')[1].lstrip().rstrip()
    1018                self._guess_smart_type(line.lower())
    1019                continue
    1020
    1021            if 'Model Family' in line:
    1022                self._guess_smart_type(line.lower())
    1023                continue
    1024
    1025            if 'LU WWN' in line:
    1026                self._guess_smart_type(line.lower())
    1027                continue
    1028
    1029            if any_in(line, 'Serial Number', 'Serial number'):
    1030                self.serial = line.split(':')[1].split()[0].rstrip()
    1031                continue
    1032
    1033            vendor = re.compile(r'^Vendor:\s+(\w+)').match(line)
    1034            if vendor is not None:
    1035                self.vendor = vendor.groups()[0]
    1036
    1037            if any_in(line, 'Firmware Version', 'Revision'):
    1038                self.firmware = line.split(':')[1].strip()
    1039
    1040            if any_in(line, 'User Capacity', 'Total NVM Capacity', 'Namespace 1 Size/Capacity'):
    1041                # TODO: support for multiple NVMe namespaces
    1042                m = re.match(
    1043                    r'.*:\s+([\d,.]+)\s\D*\[?([^\]]+)?\]?', line.strip())
    1044
    1045                if m is not None:
    1046                    tmp = m.groups()
    1047                    self._capacity = int(
    1048                        tmp[0].strip().replace(',', '').replace('.', ''))
    1049
    1050                    if len(tmp) == 2 and tmp[1] is not None:
    1051                        self._capacity_human = tmp[1].strip().replace(',', '.')
    1052
    1053            if 'SMART support' in line:
    1054                # self.smart_capable = 'Available' in line
    1055                # self.smart_enabled = 'Enabled' in line
    1056                # Since this line repeats twice the above method is flawed
    1057                # Lets try the following instead, it is a bit redundant but
    1058                # more robust.
    1059                if any_in(line, 'Unavailable', 'device lacks SMART capability'):
    1060                    self.smart_capable = False
    1061                    self.smart_enabled = False
    1062                elif 'Enabled' in line:
    1063                    self.smart_enabled = True
    1064                elif 'Disabled' in line:
    1065                    self.smart_enabled = False
    1066                elif any_in(line, 'Available', 'device has SMART capability'):
    1067                    self.smart_capable = True
    1068                continue
    1069
    1070            if 'does not support SMART' in line:
    1071                self.smart_capable = False
    1072                self.smart_enabled = False
    1073                continue
    1074
    1075            if 'Rotation Rate' in line:
    1076                if 'Solid State Device' in line:
    1077                    self.is_ssd = True
    1078                elif 'rpm' in line:
    1079                    self.is_ssd = False
    1080                    try:
    1081                        self.rotation_rate = int(
    1082                            line.split(':')[1].lstrip().rstrip()[:-4])
    1083                    except ValueError:
    1084                        # Cannot parse the RPM? Assigning None instead
    1085                        self.rotation_rate = None
    1086                continue
    1087
    1088            if 'SMART overall-health self-assessment' in line:  # ATA devices
    1089                if line.split(':')[1].strip() == 'PASSED':
    1090                    self.assessment = 'PASS'
    1091                else:
    1092                    self.assessment = 'FAIL'
    1093                continue
    1094
    1095            if 'SMART Health Status' in line:  # SCSI devices
    1096                if line.split(':')[1].strip() == 'OK':
    1097                    self.assessment = 'PASS'
    1098                else:
    1099                    self.assessment = 'FAIL'
    1100                    parse_ascq = True  # Set flag to capture status message
    1101                    message = line.split(':')[1].lstrip().rstrip()
    1102                continue
    1103
    1104            # Parse SMART test capabilities (ATA only)
    1105            # Note: SCSI does not list this but and allows for only 'offline', 'short' and 'long'
    1106            if 'SMART execute Offline immediate' in line:
    1107                self.test_capabilities['offline'] = 'No' not in line
    1108                continue
    1109
    1110            if 'Conveyance Self-test supported' in line:
    1111                self.test_capabilities['conveyance'] = 'No' not in line
    1112                continue
    1113
    1114            if 'Selective Self-test supported' in line:
    1115                self.test_capabilities['selective'] = 'No' not in line
    1116                continue
    1117
    1118            if 'Self-test supported' in line:
    1119                self.test_capabilities['short'] = 'No' not in line
    1120                self.test_capabilities['long'] = 'No' not in line
    1121                continue
    1122
    1123            # SMART Attribute table parsing
    1124            if all_in(line, '0x0', '_') and not interface == 'nvme':
    1125                # Replace multiple space separators with a single space, then
    1126                # tokenize the string on space delimiters
    1127                line_ = ' '.join(line.split()).split(' ')
    1128                if '' not in line_:
    1129                    self.attributes[int(line_[0])] = Attribute(
    1130                        int(line_[0]), line_[1], int(line[2], base=16), line_[3], line_[4], line_[5], line_[6], line_[7], line_[8], line_[9])
    1131            # For some reason smartctl does not show a currently running test
    1132            # for 'ATA' in the Test log so I just have to catch it this way i guess!
    1133            # For 'scsi' I still do it since it is the only place I get % remaining in scsi
    1134            if 'Self-test execution status' in line:
    1135                if 'progress' in line:
    1136                    self._test_running = True
    1137                    # for ATA the "%" remaining is on the next line
    1138                    # thus set the parse_running_test flag and move on
    1139                    parse_running_test = True
    1140                elif '%' in line:
    1141                    # for scsi the progress is on the same line
    1142                    # so we can just parse it and move on
    1143                    self._test_running = True
    1144                    try:
    1145                        self._test_progress = 100 - \
    1146                            int(line.split('%')[0][-3:].strip())
    1147                    except ValueError:
    1148                        pass
    1149                continue
    1150            if parse_running_test is True:
    1151                try:
    1152                    self._test_progress = 100 - \
    1153                        int(line.split('%')[0][-3:].strip())
    1154                except ValueError:
    1155                    pass
    1156                parse_running_test = False
    1157
    1158            if all_in(line, 'Description', '(hours)'):
    1159                parse_self_tests = True  # Set flag to capture test entries
    1160
    1161            #######################################
    1162            #              SCSI only              #
    1163            #######################################
    1164            #
    1165            # Everything from here on is parsing SCSI information that takes
    1166            # the place of similar ATA SMART information
    1167            if 'used endurance' in line:
    1168                pct = int(line.split(':')[1].strip()[:-1])
    1169                self.diagnostics.Life_Left = 100 - pct
    1170                continue
    1171
    1172            if 'Specified cycle count' in line:
    1173                self.diagnostics.Start_Stop_Spec = int(
    1174                    line.split(':')[1].strip())
    1175                continue
    1176
    1177            if 'Accumulated start-stop cycles' in line:
    1178                self.diagnostics.Start_Stop_Cycles = int(
    1179                    line.split(':')[1].strip())
    1180                if self.diagnostics.Start_Stop_Spec != 0:
    1181                    self.diagnostics.Start_Stop_Pct_Left = int(round(
    1182                        100 - (self.diagnostics.Start_Stop_Cycles /
    1183                               self.diagnostics.Start_Stop_Spec), 0))
    1184                continue
    1185
    1186            if 'Specified load-unload count' in line:
    1187                self.diagnostics.Load_Cycle_Spec = int(
    1188                    line.split(':')[1].strip())
    1189                continue
    1190
    1191            if 'Accumulated load-unload cycles' in line:
    1192                self.diagnostics.Load_Cycle_Count = int(
    1193                    line.split(':')[1].strip())
    1194                if self.diagnostics.Load_Cycle_Spec != 0:
    1195                    self.diagnostics.Load_Cycle_Pct_Left = int(round(
    1196                        100 - (self.diagnostics.Load_Cycle_Count /
    1197                               self.diagnostics.Load_Cycle_Spec), 0))
    1198                continue
    1199
    1200            if 'Elements in grown defect list' in line:
    1201                self.diagnostics.Reallocated_Sector_Ct = int(
    1202                    line.split(':')[1].strip())
    1203                continue
    1204
    1205            if 'read:' in line:
    1206                line_ = ' '.join(line.split()).split(' ')
    1207                if line_[1] == '0' and line_[2] == '0' and line_[3] == '0' and line_[4] == '0':
    1208                    self.diagnostics.Corrected_Reads = 0
    1209                elif line_[4] == '0':
    1210                    self.diagnostics.Corrected_Reads = int(
    1211                        line_[1]) + int(line_[2]) + int(line_[3])
    1212                else:
    1213                    self.diagnostics.Corrected_Reads = int(line_[4])
    1214                self.diagnostics._Reads_GB = float(line_[6].replace(',', '.'))
    1215                self.diagnostics._Uncorrected_Reads = int(line_[7])
    1216                continue
    1217
    1218            if 'write:' in line:
    1219                line_ = ' '.join(line.split()).split(' ')
    1220                if (line_[1] == '0' and line_[2] == '0' and
    1221                        line_[3] == '0' and line_[4] == '0'):
    1222                    self.diagnostics.Corrected_Writes = 0
    1223                elif line_[4] == '0':
    1224                    self.diagnostics.Corrected_Writes = int(
    1225                        line_[1]) + int(line_[2]) + int(line_[3])
    1226                else:
    1227                    self.diagnostics.Corrected_Writes = int(line_[4])
    1228                self.diagnostics._Writes_GB = float(line_[6].replace(',', '.'))
    1229                self.diagnostics._Uncorrected_Writes = int(line_[7])
    1230                continue
    1231
    1232            if 'verify:' in line:
    1233                line_ = ' '.join(line.split()).split(' ')
    1234                if (line_[1] == '0' and line_[2] == '0' and
    1235                        line_[3] == '0' and line_[4] == '0'):
    1236                    self.diagnostics.Corrected_Verifies = 0
    1237                elif line_[4] == '0':
    1238                    self.diagnostics.Corrected_Verifies = int(
    1239                        line_[1]) + int(line_[2]) + int(line_[3])
    1240                else:
    1241                    self.diagnostics.Corrected_Verifies = int(line_[4])
    1242                self.diagnostics._Verifies_GB = float(
    1243                    line_[6].replace(',', '.'))
    1244                self.diagnostics._Uncorrected_Verifies = int(line_[7])
    1245                continue
    1246
    1247            if 'non-medium error count' in line:
    1248                self.diagnostics.Non_Medium_Errors = int(
    1249                    line.split(':')[1].strip())
    1250                continue
    1251
    1252            if 'Accumulated power on time' in line:
    1253                self.diagnostics.Power_On_Hours = int(
    1254                    line.split(':')[1].split(' ')[1])
    1255                continue
    1256
    1257            if 'Current Drive Temperature' in line or ('Temperature:' in
    1258                                                       line and interface == 'nvme'):
    1259                try:
    1260                    self.temperature = int(
    1261                        line.split(':')[-1].strip().split()[0])
    1262
    1263                    if 'fahrenheit' in line.lower():
    1264                        self.temperature = int((self.temperature - 32) * 5 / 9)
    1265
    1266                except ValueError:
    1267                    pass
    1268
    1269                continue
    1270
    1271            if 'Temperature Sensor ' in line:
    1272                try:
    1273                    match = re.search(
    1274                        r'Temperature\sSensor\s([0-9]+):\s+(-?[0-9]+)', line)
    1275                    if match:
    1276                        (tempsensor_number_s, tempsensor_value_s) = match.group(1, 2)
    1277                        tempsensor_number = int(tempsensor_number_s)
    1278                        tempsensor_value = int(tempsensor_value_s)
    1279
    1280                        if 'fahrenheit' in line.lower():
    1281                            tempsensor_value = int(
    1282                                (tempsensor_value - 32) * 5 / 9)
    1283
    1284                        self.temperatures[tempsensor_number] = tempsensor_value
    1285                        if self.temperature is None or tempsensor_number == 0:
    1286                            self.temperature = tempsensor_value
    1287                except ValueError:
    1288                    pass
    1289
    1290                continue
    1291
    1292            #######################################
    1293            #            Common values            #
    1294            #######################################
    1295
    1296            # Sector sizes
    1297            if 'Sector Sizes' in line:  # ATA
    1298                m = re.match(
    1299                    r'.* (\d+) bytes logical,\s*(\d+) bytes physical', line)
    1300                if m:
    1301                    self.logical_sector_size = int(m.group(1))
    1302                    self.physical_sector_size = int(m.group(2))
    1303                    # set diagnostics block size to physical sector size
    1304                    self.diagnostics._block_size = self.physical_sector_size
    1305                continue
    1306            if 'Logical block size:' in line:  # SCSI 1/2
    1307                self.logical_sector_size = int(
    1308                    line.split(':')[1].strip().split(' ')[0])
    1309                # set diagnostics block size to logical sector size
    1310                self.diagnostics._block_size = self.logical_sector_size
    1311                continue
    1312            if 'Physical block size:' in line:  # SCSI 2/2
    1313                self.physical_sector_size = int(
    1314                    line.split(':')[1].strip().split(' ')[0])
    1315                continue
    1316            if 'Namespace 1 Formatted LBA Size' in line:  # NVMe
    1317                # Note: we will assume that there is only one namespace
    1318                self.logical_sector_size = int(
    1319                    line.split(':')[1].strip().split(' ')[0])
    1320                continue
    1321
    1322        if not self.abridged:
    1323            if not interface == 'scsi':
    1324                # Parse the SMART table for below-threshold attributes and create
    1325                # corresponding warnings for non-SCSI disks
    1326                self._make_smart_warnings()
    1327            else:
    1328                # If not obtained Power_On_Hours above, make a direct attempt to extract power on
    1329                # hours from the background scan results log.
    1330                if self.diagnostics.Power_On_Hours is None:
    1331                    raw, returncode = self.smartctl.generic_call(
    1332                        [
    1333                            '-d',
    1334                            'scsi',
    1335                            '-l',
    1336                            'background',
    1337                            self.dev_reference
    1338                        ])
    1339
    1340                    for line in raw:
    1341                        if 'power on time' in line:
    1342                            self.diagnostics.Power_On_Hours = int(
    1343                                line.split(':')[1].split(' ')[1])
    1344        # map temperature
    1345        if self.temperature is None:
    1346            # in this case the disk is probably ata
    1347            try:
    1348                # Some disks report temperature to attribute number 190 ('Airflow_Temperature_Cel')
    1349                # see https://bugs.freenas.org/issues/20860
    1350                temp_attr = self.attributes[194] or self.attributes[190]
    1351                self.temperature = int(temp_attr.raw)
    1352            except (ValueError, AttributeError):
    1353                pass
    1354        # Now that we have finished the update routine, if we did not find a runnning selftest
    1355        # nuke the self._test_ECD and self._test_progress
    1356        if self._test_running is False:
    1357            self._test_ECD = None
    1358            self._test_progress = None

Queries for device information using smartctl and updates all class members, including the SMART attribute table and self-test log. Can be called at any time to refresh the `pySMART.Device` object's data content.

def smart_health_assement( disk_name: str, interface: Union[str, NoneType] = None, smartctl: pySMART.smartctl.Smartctl = <pySMART.smartctl.Smartctl object>) -> Union[str, NoneType]: View Source

    49def smart_health_assement(disk_name: str, interface: Optional[str] = None, smartctl: Smartctl = SMARTCTL) -> Optional[str]:
    50    """
    51    This function gets the SMART Health Status of the disk (IF the disk
    52    is SMART capable and smart is enabled on it else returns None).
    53    This function is to be used only in abridged mode and not otherwise,
    54    since in non-abridged mode update gets this information anyways.
    55
    56    Args:
    57        disk_name (str): name of the disk
    58        interface (str, optional): interface type of the disk (e.g. 'sata', 'scsi', 'nvme',... Defaults to None.)
    59
    60    Returns:
    61        str: SMART Health Status of the disk. Returns None if the disk is not SMART capable or smart is not enabled on it.
    62             Possible values are 'PASS', 'FAIL' or None.
    63    """
    64    assessment = None
    65    raw = smartctl.health(os.path.join(
    66        '/dev/', disk_name.replace('nvd', 'nvme')), interface)
    67    line = raw[4]  # We only need this line
    68    if 'SMART overall-health self-assessment' in line:  # ATA devices
    69        if line.split(':')[1].strip() == 'PASSED':
    70            assessment = 'PASS'
    71        else:
    72            assessment = 'FAIL'
    73    if 'SMART Health Status' in line:  # SCSI devices
    74        if line.split(':')[1].strip() == 'OK':
    75            assessment = 'PASS'
    76        else:
    77            assessment = 'FAIL'
    78    return assessment

This function gets the SMART Health Status of the disk (IF the disk is SMART capable and smart is enabled on it else returns None). This function is to be used only in abridged mode and not otherwise, since in non-abridged mode update gets this information anyways.

Args: disk_name (str): name of the disk interface (str, optional): interface type of the disk (e.g. 'sata', 'scsi', 'nvme',... Defaults to None.)

Returns: str: SMART Health Status of the disk. Returns None if the disk is not SMART capable or smart is not enabled on it. Possible values are 'PASS', 'FAIL' or None.