summaryrefslogtreecommitdiff
path: root/bin
diff options
authorRod Smith <rod.smith@canonical.com>2014-10-10 07:38:59 +0000
committerDaniel Manrique <>2014-10-10 07:38:59 +0000
commitdb1578b80a7ff1cb6cb50e140d8900e4dff3c7fa (patch)
tree6079d4e641973ea06867d05c4af37bc8fd2058c7 /bin
parent70357a4a47fd04c75486580f6534239527cced91 (diff)
parenta1832f5ec8977450cf45505eaa64755eecf327b5 (diff)
"automatic merge by tarmac [r=zkrynicki][bug=1376449][author=rodsmith]"
Diffstat (limited to 'bin')
-rwxr-xr-xbin/disk_smart100
1 files changed, 69 insertions, 31 deletions
diff --git a/bin/disk_smart b/bin/disk_smart
index 3606e49..1aea483 100755
--- a/bin/disk_smart
+++ b/bin/disk_smart
@@ -35,6 +35,10 @@ USB/eSATA/eSAS attached storage devices.
Changelog:
+v1.3: Fix detection of SMART availability & activate SMART if available
+ but deactivated. Also use smartctl return value rather than string-
+ matching to determine if a test has failed; this should be more
+ robust, as output strings vary between disks.
v1.2: Handle multiple output formats for "smartctl -l"
v1.1: Put delay before first attempt to acces log, rather than after
v1.0: added debugger class and code to allow for verbose debug output if needed
@@ -60,8 +64,10 @@ import os
import sys
import time
import logging
+import shlex
-from subprocess import Popen, PIPE
+from subprocess import Popen, PIPE, check_call, check_output
+from subprocess import CalledProcessError
from argparse import ArgumentParser
@@ -84,6 +90,24 @@ class ListHandler(logging.StreamHandler):
logging.StreamHandler.emit(self, record)
+def enable_smart(disk):
+ """ Enable SMART on the specified disk
+ :param disk:
+ disk device filename (e.g., /dev/sda)
+ :returns:
+ True if enabling smart was successful, False otherwise
+ """
+ logging.debug('SMART disabled; attempting to enable it.')
+ command = 'smartctl -s on %s' % disk
+ try:
+ check_call(shlex.split(command))
+ return True
+ except CalledProcessError:
+ return False
+
+
+# Returns True if SMART is enabled. If not enabled, attempt to
+# enable it and return result of that attempt.
def is_smart_enabled(disk):
# Check with smartctl to see if SMART is available and enabled on the disk
command = 'smartctl -i %s' % disk
@@ -94,9 +118,19 @@ def is_smart_enabled(disk):
logging.debug('SMART Info for disk %s', disk)
logging.debug(diskinfo)
- return (len(diskinfo) > 2
- and 'Enabled' in diskinfo[-2]
- and 'Available' in diskinfo[-3])
+ # Return True if the output (diskinfo) includes BOTH
+ # "SMART support is.*Available" AND "SMART support is.*Enabled".
+ # If SMART is available but not enabled, try to enable it.
+ if len(diskinfo) > 2:
+ if any("SMART support is" in s and "Available" in s
+ for s in diskinfo):
+ if any("SMART support is" in s and "Enabled" in s
+ for s in diskinfo):
+ return True
+ else:
+ return enable_smart(disk)
+ else:
+ return False
def run_smart_test(disk, type='short'):
@@ -114,20 +148,23 @@ def run_smart_test(disk, type='short'):
def get_smart_entries(disk, type='selftest'):
entries = []
- command = 'smartctl -l %s %s' % (type, disk)
- stdout = Popen(command, stdout=PIPE, shell=True).stdout
+ try:
+ stdout = check_output(['smartctl', '-l', type, disk],
+ universal_newlines=True)
+ returncode = 0
+ except CalledProcessError as err:
+ stdout = err.output
+ returncode = err.returncode
# Skip intro lines
- while True:
- line = stdout.readline().decode()
- if not line:
- raise Exception('Failed to parse SMART log entries')
-
- if line.startswith('SMART'):
+ stdout_lines = iter(stdout.splitlines())
+ for line in stdout_lines:
+ if (line.startswith('SMART') or
+ line.startswith('No self-tests have been logged')):
break
# Get lengths from header
- line = stdout.readline().decode()
+ line = next(stdout_lines)
if not line.startswith('Num'):
entries.append('No entries found in log yet')
return entries
@@ -139,8 +176,7 @@ def get_smart_entries(disk, type='selftest'):
# Get remaining lines
entries = []
- for line_bytes in stdout.readlines():
- line = line_bytes.decode()
+ for line in stdout_lines:
if line.startswith('#'):
entry = {}
for i, column in enumerate(columns):
@@ -148,10 +184,9 @@ def get_smart_entries(disk, type='selftest'):
# Convert some columns to integers
entry['number'] = int(entry['number'][1:])
- entry['lifetime'] = int(entry['lifetime'])
entries.append(entry)
- return entries
+ return entries, returncode
# Returns True if an "in-progress" message is found in the smartctl
@@ -161,7 +196,8 @@ def in_progress(current_entries):
statuses = [entry for entry in current_entries
if isinstance(entry, dict)
and 'status' in entry
- and entry['status'] == 'Self-test routine in progress']
+ and (entry['status'] == 'Self-test routine in progress'
+ or "Self test in progress" in entry['status'])]
if statuses:
for entry in statuses:
logging.debug('%s %s %s %s' % (entry['number'],
@@ -173,7 +209,7 @@ def in_progress(current_entries):
return False
-# Wait for SMART test to complete; no return value.
+# Wait for SMART test to complete; return status and return code.
# Note that different disks return different types of values.
# Some return no status reports while a test is ongoing; others
# show a status line at the START of the list of tests, and
@@ -181,7 +217,7 @@ def in_progress(current_entries):
# (and then move it to the top once the tests are done).
def poll_for_status(args, disk, previous_entries):
# Priming read... this is here in case our test is finished or fails
- # immediate after it begins.
+ # immediate after it beginsAccording to.
logging.debug('Polling selftest.log for status')
keep_going = True
@@ -189,7 +225,7 @@ def poll_for_status(args, disk, previous_entries):
# Poll every sleep seconds until test is complete$
time.sleep(args.sleep)
- current_entries = get_smart_entries(disk)
+ current_entries, returncode = get_smart_entries(disk)
if current_entries != previous_entries:
if not in_progress(current_entries):
keep_going = False
@@ -202,9 +238,9 @@ def poll_for_status(args, disk, previous_entries):
args.timeout -= args.sleep
if isinstance(current_entries[0], str):
- return current_entries[0]
+ return current_entries[0], returncode
else:
- return current_entries[0]['status']
+ return current_entries[0]['status'], returncode
def main():
@@ -253,8 +289,8 @@ def main():
return 0
# Initiate a self test and start polling until the test is done
- previous_entries = get_smart_entries(disk)
- logging.info("Starting SMART self-test on %s" % disk)
+ previous_entries, returncode = get_smart_entries(disk)
+ logging.info("Starting SMART self-test on %s", disk)
if run_smart_test(disk) != 0:
logging.error("Error reported during smartctl test")
return 1
@@ -263,15 +299,17 @@ def main():
# Abort the previous instance
# so that polling can identify the difference
run_smart_test(disk)
- previous_entries = get_smart_entries(disk)
+ previous_entries, returncode = get_smart_entries(disk)
- status = poll_for_status(args, disk, previous_entries)
+ status, returncode = poll_for_status(args, disk, previous_entries)
- if status != 'Completed without error':
- log = get_smart_entries(disk)
+ if returncode != 0:
+ log, returncode = get_smart_entries(disk)
logging.error("FAIL: SMART Self-Test appears to have failed for some reason. "
- "Run 'sudo smartctl -l selftest %s' to see the SMART log" % disk)
- logging.debug("Last self-test run status: %s" % status)
+ "Run 'sudo smartctl -l selftest %s' to see the SMART log",
+ disk)
+ logging.debug("Last smartctl return code: %d", returncode)
+ logging.debug("Last smartctl run status: %s", status)
return 1
else:
logging.info("PASS: SMART Self-Test completed without error")