init
This commit is contained in:
commit
545473f1df
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.idea
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
39
README.md
Normal file
39
README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Smart and wipe disks
|
||||
|
||||
**Project target : ultimately provide a way to diagnose and clear old disks before use.**
|
||||
|
||||
It should be :
|
||||
|
||||
- based on classic GNU/LINUX tools (smartctl, dd)
|
||||
- safe to use (never touch a mounted partition)
|
||||
- automatized for standalone operations
|
||||
|
||||
---
|
||||
|
||||
For now the project runs through a shell script that needs to be converted to python.
|
||||
|
||||
```
|
||||
$ pip install -r requirements.txt
|
||||
|
||||
# sudo/root is required to operate smartctl
|
||||
|
||||
$ sudo ./bin/main.sh
|
||||
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
It needs to improve based on the sources in the src dir :
|
||||
|
||||
- [pySMART](https://pypi.org/project/pySMART/)
|
||||
- [py_smartjson](https://github.com/kroy-the-rabbit/py_smartjson)
|
||||
- [smart_status](https://github.com/ixs/smart_status)
|
||||
- [pyWype](https://github.com/marshki/pyWype)
|
||||
|
||||
|
||||
Known dependencies
|
||||
|
||||
```
|
||||
- pip install pySMART
|
||||
- pip install jsonpickle
|
||||
```
|
131
bin/main.sh
Executable file
131
bin/main.sh
Executable file
@ -0,0 +1,131 @@
|
||||
#! /bin/bash
|
||||
#set -e
|
||||
APP_PATH=$( cd $(dirname $0) && pwd)
|
||||
SRC_PATH=$( cd $(dirname $0)/../src && pwd)
|
||||
cd $APP_PATH
|
||||
clear
|
||||
echo -e "## DEVICES\n"
|
||||
DEVICES=$(lsblk /dev/sd? --nodeps --output NAME,MODEL,VENDOR,SIZE,TYPE,STATE)
|
||||
echo "$DEVICES"
|
||||
MOUNT_ROOT=$(mount |grep " / " |awk '{print $1}' |sed -r 's=/dev/(...)[0-9]$=\1=')
|
||||
declare -a NOT_ROOT
|
||||
while read device others; do
|
||||
[[ "$device" != $MOUNT_ROOT ]] && [[ "$device" != "NAME" ]] && NOT_ROOT+=( $device )
|
||||
done <<< "$DEVICES"
|
||||
|
||||
NOT_ROOT_STR=$( echo ${NOT_ROOT[@]} )
|
||||
echo -e "\n## ROOT MOUNTED DEVICE\n$MOUNT_ROOT"
|
||||
echo -e "\n## OTHER DEVICES\n${NOT_ROOT_STR}\n"
|
||||
|
||||
|
||||
echo -e "\n## SMARTCTL TESTS"
|
||||
read -e -p "Do you want to run tests? [y/N] " -n 1
|
||||
REPLY=${REPLY:-N}
|
||||
if [[ "N" != "${REPLY^^}" ]] ; then
|
||||
echo -e "\n## SMARTCTL DISKS SELECTION"
|
||||
echo -e "Do you want to USE ALL non root devices (empty reply)? \nOr else please type specific DEVICES NAMES to include (ex: '${NOT_ROOT_STR}')?"
|
||||
read -e -p "Type your answer: "
|
||||
CMD="$SRC_PATH/smartjson.py long "
|
||||
if [[ -z "$REPLY" ]] ; then
|
||||
CMD+=" -e $MOUNT_ROOT"
|
||||
else
|
||||
|
||||
DEVICES=${REPLY}
|
||||
for i in $DEVICES; do
|
||||
CMD+=" -d $i"
|
||||
done
|
||||
fi
|
||||
|
||||
echo -e "\n## READY"
|
||||
read -e -i Y -n 1 -p "About to run command '$CMD'. OK [Y/n]? "
|
||||
|
||||
# $CMD
|
||||
START=$(date "+%s")
|
||||
set +e
|
||||
while true; do
|
||||
clear
|
||||
echo -e "\n## CHECKING STATUS"
|
||||
|
||||
date
|
||||
echo -e "\nRunning since: $(( $( date +%s ) - $START )) seconds.\n"
|
||||
$SRC_PATH/smartjson.py status && break
|
||||
echo -e "\n## MANUAL HALT"
|
||||
read -e -i Y -p "Stop ?" -t 15
|
||||
[[ ${REPLY^^} == "Y" ]] && break
|
||||
done
|
||||
|
||||
fi
|
||||
|
||||
echo -e "\n## SMARTCTL DISKS STATUS"
|
||||
$SRC_PATH/smartjson.py list
|
||||
|
||||
|
||||
echo -e "\n## DISKS BACKGROUND ERASE\nCaution: this might cause data loss."
|
||||
read -e -p "Do you want to run a background erasure? [y/N] " -n 1
|
||||
REPLY=${REPLY:-N}
|
||||
if [[ "N" != "${REPLY^^}" ]] ; then
|
||||
ERASE_DEVICES=""
|
||||
while [[ -z "$ERASE_DEVICES" ]] ; do
|
||||
echo "Please provide device names. Ex: 'sda sdc'"
|
||||
read -e -p "Devices to erase: " -i "${NOT_ROOT_STR}" ERASE_DEVICES
|
||||
declare -A DD_CMD
|
||||
ERROR="false"
|
||||
for f in $ERASE_DEVICES; do
|
||||
if [[ ! -b /dev/$f ]] || [[ "$MOUNT_ROOT" == $f ]] ; then
|
||||
echo "ERROR. $f is not a valid device."
|
||||
ERROR="true"
|
||||
break
|
||||
fi
|
||||
DD_CMD["$f"]="dd if=/dev/zero of=/dev/$f bs=512K"
|
||||
done
|
||||
if [[ "$ERROR" == "true" ]]; then
|
||||
ERASE_DEVICES=""
|
||||
continue
|
||||
fi
|
||||
done
|
||||
echo -e "\n## CONFIRMATION\nYou are about to run the following commands."
|
||||
for i in ${!DD_CMD[@]}; do
|
||||
echo "Disk $i: ${DD_CMD[$i]}"
|
||||
done
|
||||
read -e -p "Please type 'Yes' to validate: " VALIDATE
|
||||
if [[ "YES" != "${VALIDATE^^}" ]]; then
|
||||
echo "EXIT"
|
||||
exit
|
||||
else
|
||||
echo -e "\n## RUNNING ERASURE\n"
|
||||
declare -A DD_PID
|
||||
for i in ${!DD_CMD[@]}; do
|
||||
${DD_CMD[$i]}&
|
||||
DD_PID[$i]=$!
|
||||
echo "$i PID ${DD_PID[$i]}..."
|
||||
sleep 1
|
||||
done
|
||||
sleep 3
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ -n ${DD_PID[@]} ]] && while true; do
|
||||
clear
|
||||
echo -e "\n## CHECKING STATUS"
|
||||
FINISHED_COUNT=O
|
||||
|
||||
if [[ ${#DD_PID[@]} -ne 0 ]] ; then
|
||||
for i in ${!DD_PID[@]}; do
|
||||
PID=${DD_PID[$i]}
|
||||
ps -f -p $PID &>/dev/null
|
||||
R=$?
|
||||
if [[ 0 -ne $R ]] ; then
|
||||
MSG="Finished"
|
||||
let $(( FINISHED_COUNT++ ))
|
||||
else
|
||||
MSG="Process running"
|
||||
fi
|
||||
echo "DISK $i PID $i : $MSG"
|
||||
done
|
||||
fi
|
||||
[[ ${#DD_PID[@]} -eq $FINISHED_COUNT ]] && break
|
||||
echo -e "\n## MANUAL HALT"
|
||||
read -e -i Y -p "Stop ?" -t 15
|
||||
[[ ${REPLY^^} == "Y" ]] && break
|
||||
done
|
||||
|
1
src/.gitignore
vendored
Normal file
1
src/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
__pycache__/
|
18
src/SmartDevice.py
Normal file
18
src/SmartDevice.py
Normal file
@ -0,0 +1,18 @@
|
||||
class SmartDevice:
|
||||
c_TEMP=194
|
||||
c_REALLOC_SECTORS=5
|
||||
c_CURRENT_PENDING_SECTORS=197
|
||||
c_HOURS=9
|
||||
def __init__(self, device):
|
||||
self.dev = device.name
|
||||
self.model = device.model
|
||||
self.serial = device.serial
|
||||
self.temp = device.attributes[self.c_TEMP].raw
|
||||
self.reallocated_sectors = device.attributes[self.c_REALLOC_SECTORS].raw
|
||||
self.capacity = device.capacity
|
||||
self.firmware = device.firmware
|
||||
self.smart_status = device.assessment
|
||||
self.ssd = device.is_ssd
|
||||
self.hours = device.attributes[self.c_HOURS].raw
|
||||
self.full_attributes = device.attributes
|
||||
|
177
src/py_wype.py
Executable file
177
src/py_wype.py
Executable file
@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Disk-wiping utility for GNU/Linux, written in Python 2 & 3.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
|
||||
try:
|
||||
input = raw_input
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
def is_linux():
|
||||
"""Check if system is 'Linux'
|
||||
"""
|
||||
|
||||
if 'Linux' not in platform.system():
|
||||
print("This program was designed for GNU/Linux. Exiting.")
|
||||
sys.exit()
|
||||
|
||||
def root_user_check():
|
||||
"""Check if current UID is 0.
|
||||
"""
|
||||
|
||||
if os.getuid() != 0:
|
||||
print("This program requires ROOT privileges. Exiting.")
|
||||
sys.exit()
|
||||
|
||||
def list_mounted_devices():
|
||||
"""List mounted device(s) / partition(s).
|
||||
"""
|
||||
|
||||
print(22 * "-", "DEVICES & PARTITIONS", 22 * "-")
|
||||
|
||||
return os.system('lsblk /dev/sd* --nodeps --output NAME,MODEL,VENDOR,SIZE,TYPE,STATE')
|
||||
|
||||
def define_device_to_wipe():
|
||||
"""Prompt user to define device or partition to wipe.
|
||||
"""
|
||||
|
||||
while True:
|
||||
try:
|
||||
device = input(
|
||||
"Enter letter [number] of device/partition to wipe,"
|
||||
"\ne.g. to wipe '/dev/sdb1' enter 'b1': ")
|
||||
|
||||
if not re.match("^[a-z][0-9]?$", device):
|
||||
raise ValueError()
|
||||
return device
|
||||
|
||||
except ValueError:
|
||||
print("Sorry, that's not a valid device or partition. Try again.")
|
||||
|
||||
def append_device_to_wipe():
|
||||
"""Append user-defined device/partition to /dev/sd.
|
||||
"""
|
||||
|
||||
letter = define_device_to_wipe()
|
||||
|
||||
return '/dev/sd' + letter
|
||||
|
||||
def number_of_wipes():
|
||||
"""Prompt user for number of wipes to perform.
|
||||
"""
|
||||
|
||||
while True:
|
||||
try:
|
||||
wipes = int(input("How many times do you want to wipe the device or partition?: "))
|
||||
|
||||
if wipes <= 0:
|
||||
raise ValueError()
|
||||
return wipes
|
||||
|
||||
except ValueError:
|
||||
print("Sorry, that's not a valid number. Try again: ")
|
||||
|
||||
def confirm_wipe():
|
||||
"""Prompt user to confirm disk erasure.
|
||||
"""
|
||||
|
||||
print("WARNING!!! WRITING CHANGES TO DISK WILL RESULT IN IRRECOVERABLE DATA LOSS.")
|
||||
|
||||
while True:
|
||||
try:
|
||||
reply = input("Do you want to proceed? (Yes/No): ").lower().strip()
|
||||
|
||||
if reply == 'yes':
|
||||
return True
|
||||
if reply == 'no':
|
||||
print("Exiting pyWype.")
|
||||
sys.exit()
|
||||
|
||||
except ValueError:
|
||||
print("Sorry, that's not a valid entry. Try again: ")
|
||||
|
||||
def write_zeros_to_device():
|
||||
"""Write zeros to device/partition.
|
||||
"""
|
||||
|
||||
append = append_device_to_wipe()
|
||||
num = number_of_wipes()
|
||||
confirm_wipe()
|
||||
|
||||
for i in range(num):
|
||||
print("Processing pass count {} of {} ... ".format(i + 1, num))
|
||||
os.system(('dd if=/dev/zero |pv --progress --time --rate --bytes|'
|
||||
'dd of={} bs=1024'.format(append)))
|
||||
|
||||
def write_random_to_device():
|
||||
"""Write random zeros and ones to device/partition.
|
||||
"""
|
||||
|
||||
append = append_device_to_wipe()
|
||||
num = number_of_wipes()
|
||||
confirm_wipe()
|
||||
|
||||
for i in range(num):
|
||||
print("Processing pass count {} of {} ... ".format(i + 1, num))
|
||||
os.system(('dd if=/dev/urandom |pv --progress --time --rate --bytes|'
|
||||
'dd of={} bs=1024'.format(append)))
|
||||
|
||||
def menu():
|
||||
"""Menu prompt for use to select program option.
|
||||
"""
|
||||
|
||||
list_mounted_devices()
|
||||
|
||||
while True:
|
||||
try:
|
||||
print(30 * "-", "MENU", 30 * "-")
|
||||
print("1. Overwrite device or partition with 0's \n(faster, less secure).")
|
||||
print("2. Overwrite device or partition with random 0\'s & 1\'s"
|
||||
"\n(slower, more secure).")
|
||||
print("3. Quit.")
|
||||
|
||||
choice = input("Select an option (1, 2 or 3): ")
|
||||
|
||||
if choice not in ('1', '2', '3'):
|
||||
raise ValueError()
|
||||
return choice
|
||||
|
||||
except ValueError:
|
||||
print("Sorry, that's not a valid number. Try again: ")
|
||||
|
||||
def interactive_mode():
|
||||
"""Display menu-driven options and run function based on selection.
|
||||
"""
|
||||
|
||||
while True:
|
||||
choice = menu()
|
||||
|
||||
if choice == '3':
|
||||
sys.exit()
|
||||
elif choice == '1':
|
||||
write_zeros_to_device()
|
||||
elif choice == '2':
|
||||
write_random_to_device()
|
||||
|
||||
def wipe_device():
|
||||
"""Program to wipe drive.
|
||||
"""
|
||||
|
||||
is_linux()
|
||||
root_user_check()
|
||||
interactive_mode()
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(28 * '-', " pyWype ", 28 * '-')
|
||||
print("PYTHON DISK & PARTITION WIPING UTILITY FOR GNU/LINUX."
|
||||
"\nTHIS UTILITY WILL IRRECOVERABLY WIPE DATA FROM DRIVE.\nPROCEED WITH CAUTION.")
|
||||
|
||||
wipe_device()
|
549
src/smart_status.py
Executable file
549
src/smart_status.py
Executable file
@ -0,0 +1,549 @@
|
||||
#! /usr/bin/env python3
|
||||
# Source : https://github.com/ixs/smart_status
|
||||
# smartmontools disk status
|
||||
#
|
||||
# Copyright (c) 2015 Andreas Thienemann <andreas@bawue.net>
|
||||
#
|
||||
# Use all available SMART data to ascertain whether a disk is probably okay or not.
|
||||
# As customer available SMART attributes are basically unusable to predict failure,
|
||||
# the script will schedule selftests in order to discover disk (hopefully) before
|
||||
# they result in loss of data.
|
||||
#
|
||||
# Licensed under the GPL v3.0 or any later version
|
||||
#
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import pprint
|
||||
import traceback
|
||||
import stat
|
||||
import argparse
|
||||
|
||||
class bcolors:
|
||||
HEADER = '\033[95m'
|
||||
OKBLUE = '\033[94m'
|
||||
OKGREEN = '\033[92m'
|
||||
WARNING = '\033[93m'
|
||||
FAIL = '\033[91m'
|
||||
ENDC = '\033[0m'
|
||||
|
||||
class smart_status:
|
||||
def __init__(self):
|
||||
# The errorcode decoder map for smartctl taken from the manpage
|
||||
self.error_map = (
|
||||
'Command line did not parse.',
|
||||
'Device open failed, device did not return an IDENTIFY DEVICE structure, or device is in a low-power mode',
|
||||
'Some SMART or other ATA command to the disk failed, or there was a checksum error in a SMART data structure',
|
||||
'SMART status check returned "DISK FAILING"',
|
||||
'We found prefail Attributes <= threshold.',
|
||||
'SMART status check returned "DISK OK" but we found that some (usage or prefail) Attributes have been <= threshold at some time in the past.',
|
||||
'The device error log contains records of errors.',
|
||||
'The device self-test log contains records of errors. [ATA only] Failed self-tests outdated by a newer successful extended self-test are ignored.'
|
||||
)
|
||||
|
||||
self.cfg = dict()
|
||||
self.cfg['smartctl_bin'] = 'smartctl'
|
||||
self.cfg['strict'] = False
|
||||
self.cfg['smartctl_test_threshold'] = 0
|
||||
self.cfg['smartctl_test_frequency'] = 0
|
||||
self.cfg['verbose'] = False
|
||||
self.cfg['color'] = False
|
||||
self.cfg['disks'] = list()
|
||||
|
||||
def colorize(self, mode):
|
||||
if mode == False:
|
||||
bcolors.HEADER = ''
|
||||
bcolors.WARNING = ''
|
||||
bcolors.OKGREEN = ''
|
||||
bcolors.OKBLUE = ''
|
||||
bcolors.FAIL = ''
|
||||
bcolors.ENDC = ''
|
||||
|
||||
|
||||
def find_disks(self):
|
||||
disks = list()
|
||||
|
||||
for dev in sorted(os.listdir('/sys/block')):
|
||||
try:
|
||||
with open('/sys/block/{}/device/type'.format(dev)) as f:
|
||||
if f.read().strip() == '0':
|
||||
disks.append('/dev/{}'.format(dev))
|
||||
except:
|
||||
continue
|
||||
|
||||
return disks
|
||||
|
||||
def schedule_selftest(self, dev, report = False):
|
||||
(smart_health, smart_selftest, smart_log, smart_attr) = self.fetch_smart(dev, report)
|
||||
|
||||
if not self.judge_selftest(dev, smart_selftest, report = report):
|
||||
if report:
|
||||
print( "{col}{dev} Cannot schedule SMART selftest.{cls}".format(col = bcolors.FAIL, dev = dev, cls=bcolors.ENDC) )
|
||||
return False
|
||||
if self.judge_selftest_log(dev, smart_log, smart_attr, report = report)[1]:
|
||||
if report:
|
||||
print( "{col}{dev} SMART selftest ran recently. Not scheduling a new one.{cls}".format(col = bcolors.OKBLUE, dev = dev, cls=bcolors.ENDC) )
|
||||
return False
|
||||
else:
|
||||
if report:
|
||||
print( "{col}{dev} Scheduling SMART selftest.{cls}".format(col = bcolors.HEADER, dev = dev, cls=bcolors.ENDC) )
|
||||
|
||||
output = subprocess.check_output([self.cfg['smartctl_bin'], '-t', 'long', dev], universal_newlines=True)
|
||||
if 'Drive command "Execute SMART Extended self-test routine immediately in off-line mode" successful.' not in output:
|
||||
if report:
|
||||
print( "{col}{dev} Scheduling SMART selftest failed.{cls}".format(col = bcolors.FAIL, dev = dev, cls=bcolors.ENDC) )
|
||||
return False
|
||||
elif 'Testing has begun.' in output:
|
||||
for l in output.split("\n"):
|
||||
if l.startswith("Please wait "):
|
||||
duration = l.split()[2]
|
||||
continue
|
||||
if l.startswith("Test will complete after "):
|
||||
eta = l[len("Test will complete after "):]
|
||||
if report:
|
||||
print( "{col}{dev} Scheduling SMART selftest successful. Expected duration {duration} min, ETA: {eta}.{cls}".format(col = bcolors.OKBLUE, dev = dev, duration = duration, eta = eta, cls=bcolors.ENDC) )
|
||||
return True
|
||||
|
||||
|
||||
def judge_health(self, dev, smart_health, report = False):
|
||||
# Overall health
|
||||
try:
|
||||
if smart_health == "PASSED":
|
||||
healthy = True
|
||||
col = bcolors.HEADER
|
||||
else:
|
||||
col = bcolors.FAIL
|
||||
healthy = False
|
||||
if report:
|
||||
print( "{col}{dev} SMART Health status is {health}. (This value cannot necessarily be trusted){cls}".format(col = col, dev = dev, health = smart_health, cls=bcolors.ENDC) )
|
||||
except:
|
||||
if report:
|
||||
print( "{col}{dev} SMART Health status cannot be determined.{cls}".format(col=bcolors.FAIL, dev = dev, health = smart_health, cls=bcolors.ENDC) )
|
||||
healthy = None
|
||||
return healthy
|
||||
|
||||
|
||||
def judge_attributes(self, dev, smart_attr, report = False):
|
||||
healthy = None
|
||||
try:
|
||||
# Smart Attributes to watch
|
||||
# for a in ('Reallocated_Sector_Ct', 'Reported_Uncorrect', 'Command_Timeout', 'Current_Pending_Sector', 'Offline_Uncorrectable'):
|
||||
# try:
|
||||
# print a, smart_attr[a]['raw_value']
|
||||
# except:
|
||||
# print
|
||||
if int(smart_attr['Current_Pending_Sector']['raw_value']) > 0:
|
||||
if report:
|
||||
print( "{col}{dev} SMART Attribute Current_Pending_Sector indicates failing disk.{cls}".format(col=bcolors.FAIL, dev = dev, cls=bcolors.ENDC) )
|
||||
healthy = False
|
||||
else:
|
||||
healthy = True
|
||||
except:
|
||||
pass
|
||||
return healthy
|
||||
|
||||
def judge_selftest(self, dev, smart_selftest, report = False):
|
||||
"""Judge whether we can schedule a selftest
|
||||
"""
|
||||
|
||||
try:
|
||||
(selftest_num, selftest_txt) = smart_selftest
|
||||
|
||||
if selftest_num == 0:
|
||||
if report:
|
||||
print( "{col}{dev} No SMART selftest is currently running.{cls}".format(col=bcolors.OKBLUE, dev = dev, txt = selftest_txt, cls=bcolors.ENDC) )
|
||||
return True
|
||||
elif selftest_num >= 240 and selftest_num <= 250:
|
||||
if report:
|
||||
print( "{col}{dev} SMART selftest is currently running: {txt}.{cls}".format(col=bcolors.OKBLUE, dev = dev, txt = selftest_txt, cls=bcolors.ENDC) )
|
||||
return False
|
||||
elif selftest_num == 25:
|
||||
if report:
|
||||
print( "{col}{dev} Last SMART selftest had a problem: {txt}.{cls}".format(col=bcolors.WARNING, dev = dev, txt = selftest_txt, cls=bcolors.ENDC) )
|
||||
return True
|
||||
else:
|
||||
if report:
|
||||
print( "{col}{dev} SMART selftest had a problem: {txt}.{cls}".format(col=bcolors.FAIL, dev = dev, txt = selftest_txt, cls=bcolors.ENDC) )
|
||||
return True
|
||||
except:
|
||||
if report:
|
||||
print( "{col}{dev} cannot determine selftest status.{cls}".format(col = bcolors.WARNING, dev = dev, cls = bcolors.ENDC) )
|
||||
return False
|
||||
|
||||
|
||||
def judge_selftest_log(self, dev, smart_log, smart_attr, report = False):
|
||||
"""
|
||||
returns (selftest ok, selftest current)
|
||||
"""
|
||||
healthy = True
|
||||
current = None
|
||||
try:
|
||||
uptime = int(smart_attr['Power_On_Hours']['raw_value'])
|
||||
except:
|
||||
if report:
|
||||
#print "{col}{dev} cannot determine power on hours.{cls}".format(col=bcolors.WARNING, dev=dev, cls=bcolors.ENDC)
|
||||
pass
|
||||
pass
|
||||
|
||||
try:
|
||||
# Iterate over the log entrys and ignore useless/invalid logs
|
||||
for entry in sorted(smart_log):
|
||||
if smart_log[entry]['Status'] in ('Self-test routine in progress', 'Interrupted (host reset)' and 'Aborted by host'):
|
||||
continue
|
||||
else:
|
||||
last_test = int(smart_log[entry]['LifeTime(hours)'])
|
||||
test_type = smart_log[entry]['Test_Description']
|
||||
test_state = smart_log[entry]['Status']
|
||||
test_diff = uptime - last_test
|
||||
break
|
||||
if test_diff < self.cfg['smartctl_test_frequency'] * 24 and test_state == 'Completed without error':
|
||||
if self.cfg['smartctl_test_frequency'] == 0:
|
||||
col = bcolors.HEADER
|
||||
else:
|
||||
col = bcolors.OKGREEN
|
||||
current = True
|
||||
elif test_diff >= self.cfg['smartctl_test_frequency'] * 24 * 2 and test_state == 'Completed without error':
|
||||
if self.cfg['smartctl_test_frequency'] == 0:
|
||||
col = bcolors.HEADER
|
||||
else:
|
||||
col = bcolors.FAIL
|
||||
current = False
|
||||
elif test_diff >= self.cfg['smartctl_test_frequency'] * 24 and test_state == 'Completed without error':
|
||||
if self.cfg['smartctl_test_frequency'] == 0:
|
||||
col = bcolors.HEADER
|
||||
else:
|
||||
col = bcolors.WARNING
|
||||
current = False
|
||||
elif test_state.startswith('Self-test routine in'):
|
||||
col = ''
|
||||
healthy = None
|
||||
current = True
|
||||
else:
|
||||
col = bcolors.FAIL
|
||||
healthy = False
|
||||
current = False
|
||||
if report:
|
||||
hrs = uptime - last_test
|
||||
if hrs < 1:
|
||||
tspec = '1 hour'
|
||||
elif hrs <= 24:
|
||||
tspec = '{} hours'.format(hrs)
|
||||
elif hrs > 24 and hrs < 24 * 2:
|
||||
tspec = '{} day {} hours'.format(hrs / 24, hrs % 24)
|
||||
elif hrs >= 24 * 2 and hrs < 24 * 14:
|
||||
tspec = '{} days {} hours'.format(hrs / 24, hrs % 24)
|
||||
else:
|
||||
tspec = '{} weeks {} days {} hours'.format(hrs / 24 / 7, hrs / 24, hrs % 24)
|
||||
|
||||
print( "{col}{dev} last {type} selftest {state} and finished {tspec} ago.{cls}".format(col = col, dev = dev, tspec = tspec, type = test_type.lower(), state = test_state.lower(), cls = bcolors.ENDC) )
|
||||
except Exception as err:
|
||||
if report:
|
||||
print( "{col}{dev} never finished a SMART selftest.{cls}".format(col = bcolors.WARNING, dev = dev, cls = bcolors.ENDC) )
|
||||
return (healthy, current)
|
||||
|
||||
|
||||
def verify_smart(self, dev, report = False):
|
||||
"""Verify the SMART status of a disk and return True or False depending on state.
|
||||
This is a guesstimate as SMART is basically unreliable"""
|
||||
|
||||
health = []
|
||||
|
||||
(smart_health, smart_selftest, smart_log, smart_attr) = self.fetch_smart(dev, report)
|
||||
try:
|
||||
# Overall health
|
||||
health.append(self.judge_health(dev, smart_health, report = report))
|
||||
|
||||
# Attribute health
|
||||
health.append(self.judge_attributes(dev, smart_attr, report = report))
|
||||
|
||||
# Smart Selftest capability
|
||||
self.judge_selftest(dev, smart_selftest, report = report)
|
||||
|
||||
# Selftest log
|
||||
health.append(self.judge_selftest_log(dev, smart_log, smart_attr, report = report)[0])
|
||||
|
||||
except Exception as err:
|
||||
print( traceback.format_exc() )
|
||||
raise(err)
|
||||
|
||||
if None in health and self.cfg['strict'] == True:
|
||||
return None
|
||||
elif False in health:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def fetch_smart(self, dev, report = False):
|
||||
"""Verify the disk is still safe to use according to smartctl output.
|
||||
Yes, this is only a best effort... SMART is not trustworthy.
|
||||
"""
|
||||
try:
|
||||
output = subprocess.check_output([self.cfg['smartctl_bin'], '-H', '-c', '-A', '-l', 'selftest', dev], universal_newlines=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
ret = e.returncode
|
||||
output = e.output
|
||||
# Decode bitmasked return code
|
||||
msg = list()
|
||||
for i in range(0,len(self.error_map)):
|
||||
if ((ret & 2**i) >> i) != 0:
|
||||
msg.append(self.error_map[i])
|
||||
for m in msg:
|
||||
if report and self.error_map.index(m) in (2,) and smart.cfg['strict'] == False:
|
||||
col = bcolors.WARNING
|
||||
else:
|
||||
col = bcolors.FAIL
|
||||
if report:
|
||||
print( "{col}{dev} smartctl output: {msg}{cls}".format(col=col, dev=dev, msg=m, cls=bcolors.ENDC) )
|
||||
|
||||
|
||||
if report:
|
||||
if 'SMART Attributes Data Structure revision number' not in output:
|
||||
print( "{col}{dev} does not support SMART attributes.{cls}".format(col=bcolors.WARNING, dev=dev, cls=bcolors.ENDC) )
|
||||
if 'SMART Self-test log structure revision number' not in output:
|
||||
print( "{col}{dev} does not support SMART selftest.{cls}".format(col=bcolors.WARNING, dev=dev, cls=bcolors.ENDC) )
|
||||
|
||||
# Simple smartctl output parser
|
||||
# Attributes we can split by whitespace
|
||||
# Log entries we need to parse by looking at str.find() based using the header as a template
|
||||
section = None
|
||||
attrs = dict()
|
||||
logs = dict()
|
||||
health = None
|
||||
selftest = list()
|
||||
linecont = False # Is the next line a continuation of the current item? Important for capabilities
|
||||
for l in output.split("\n"):
|
||||
attr = dict()
|
||||
log = list()
|
||||
|
||||
# section end
|
||||
if section is not None and l == "":
|
||||
section = None
|
||||
continue
|
||||
|
||||
# Overall health
|
||||
if l.startswith("SMART overall-health self-assessment test result"):
|
||||
health = l.split(':')[1].strip()
|
||||
|
||||
# Capabilities, we're only caring for the selftest status
|
||||
if l.startswith("General SMART Values"):
|
||||
section = 'cap'
|
||||
continue
|
||||
if section == 'cap':
|
||||
if l.startswith('Self-test execution status'):
|
||||
selftest.append(l)
|
||||
linecont = 'selftest'
|
||||
continue
|
||||
|
||||
if linecont is not None and l.startswith("\t"):
|
||||
if linecont == 'selftest':
|
||||
selftest.append(l)
|
||||
continue
|
||||
else:
|
||||
linecont = None
|
||||
|
||||
|
||||
# Attr
|
||||
if l.startswith("Vendor Specific SMART Attributes with Thresholds"):
|
||||
section = 'attr'
|
||||
continue
|
||||
|
||||
if section == 'attr':
|
||||
if l.startswith("ID#"):
|
||||
continue
|
||||
else:
|
||||
attr = dict(zip(('id', 'name', 'flag', 'value', 'worst', 'thresh', 'type', 'updated', 'when_failed', 'raw_value'), l.split(None, 9)))
|
||||
attrs[attr['name']] = attr
|
||||
|
||||
|
||||
# Log
|
||||
if l.startswith("SMART Self-test log structure revision number"):
|
||||
section = 'log'
|
||||
continue
|
||||
|
||||
if section == 'log':
|
||||
if l.startswith("Num"):
|
||||
log_header = l
|
||||
log_item_pos = map(log_header.find, log_header.split())
|
||||
continue
|
||||
elif l.startswith('No self-tests have been logged.'):
|
||||
section = None
|
||||
continue
|
||||
|
||||
else:
|
||||
try :
|
||||
log_item_pos = list(log_item_pos)
|
||||
for i in range(0, len(log_item_pos)):
|
||||
if i == 3:
|
||||
s = log_item_pos[i] + 5 # Special handling for the status where the table header doesn't line up with the table data
|
||||
else:
|
||||
s = log_item_pos[i]
|
||||
if i < len(log_item_pos) - 1:
|
||||
if i == 2:
|
||||
e = log_item_pos[i + 1] + 5 # Special handling for the status where the table header doesn't line up with the table data
|
||||
else:
|
||||
e = log_item_pos[i + 1]
|
||||
else:
|
||||
e = len(l)
|
||||
log.append(l[s:e].strip())
|
||||
logs[log[0]] = dict(zip(log_header.split(), log))
|
||||
except UnboundLocalError as exc :
|
||||
print(f"Device {dev} doesn't offer logs capacity")
|
||||
|
||||
# Fixup the selftest status
|
||||
try:
|
||||
m = re.search('\([ ]*(?P<num_status>\d+)\)\s(?P<text_status>.*)', selftest[0])
|
||||
num = int(m.group('num_status'))
|
||||
txt = ([m.group('text_status')])
|
||||
txt.extend(map(str.strip, selftest[1:]))
|
||||
txt = " ".join(txt)
|
||||
selftest = (num, txt)
|
||||
except:
|
||||
selftest = None
|
||||
|
||||
return health, selftest, logs, attrs
|
||||
|
||||
|
||||
|
||||
def check_single_dev(dev, report = True):
|
||||
try:
|
||||
res = smart.verify_smart(dev, report)
|
||||
|
||||
return res
|
||||
|
||||
except Exception as err:
|
||||
pass
|
||||
print( "{0} Error getting SMART data".format(dev) )
|
||||
print( traceback.format_exc() )
|
||||
|
||||
def parse_opts():
|
||||
parser = argparse.ArgumentParser(description="""Hard drives use Self-Monitoring, Analysis and Reporting Technology (SMART) to export data about the health of a disk device.
|
||||
{prog} is a tool to parse this data and tries to detect pending or post disk failures and report on disk status.
|
||||
Unfortunately SMART failure prediction is rarely reliable.
|
||||
Reporting on actual disk failures however generally works.""".format(prog=os.path.basename(sys.argv[0])))
|
||||
group_op_sel = parser.add_mutually_exclusive_group(required=True)
|
||||
group_op_sel.add_argument("-a", "--autodetect", "--all", action='store_true', help="Autodetect disks and scan.")
|
||||
group_op_sel.add_argument("-d", "--disks", action='append', nargs=1, help="Only handle specific disk device.")
|
||||
group_op_sel.add_argument("-b", "--smartctl", help="Overide smartctl binary location if not in path.", default = 'smartctl')
|
||||
group_nag = parser.add_argument_group('Nagios', description="Format output to be usable as a Nagios compatible plugin.")
|
||||
group_nag.add_argument("-n", "--nagios", action='store_true', help="Return data in a form usable as a nagios check.")
|
||||
group_nag.add_argument("-u", "--unknown", choices=['warning', 'critical'], help="Change alert level of unknown smart status.")
|
||||
group_nag.add_argument("-w", "--warning", choices=['unknown', 'critical'], help="Change alert level of warning smart status.")
|
||||
parser.add_argument("-i", "--ignore", action='append', nargs="+", help="Ignore specific disk devices. Helpful when scanning for all disks.", default = [])
|
||||
parser.add_argument("-s", "--schedule", type=int, help="Frequency in days after which a selftest is considered out of date and will be rescheduled.")
|
||||
parser.add_argument("-t", "--threshold", type=int, help="Frequency in days after which a selftest is considered out of date and will be warned about but not rescheduled.")
|
||||
parser.add_argument("-v", "--verbose", action='store_true', help="Print more status information.")
|
||||
parser.add_argument("-x", "--strict", action='store_true', help="Strict checking. Report a device not supporting SMART attributes or selftest as unknown/error instead of relying on the unreliable general SMART health feedback.", default = False)
|
||||
parser.add_argument("-c", "--color", "--colour", action='store_true', help="Colorize output.", default = False)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
if __name__ == '__main__':
|
||||
smart = smart_status()
|
||||
args = parse_opts()
|
||||
|
||||
smart.cfg['smartctl_bin'] = args.smartctl
|
||||
|
||||
if args.autodetect:
|
||||
smart.cfg['disks'] = smart.find_disks()
|
||||
|
||||
if args.strict:
|
||||
smart.cfg['strict'] = True
|
||||
|
||||
if args.schedule == None:
|
||||
smart.cfg['smartctl_test_frequency'] = 0
|
||||
else:
|
||||
smart.cfg['smartctl_test_frequency'] = args.schedule
|
||||
|
||||
if args.schedule == None:
|
||||
smart.cfg['smartctl_test_threshold'] = 0
|
||||
else:
|
||||
smart.cfg['smartctl_test_threshold'] = args.schedule
|
||||
|
||||
if not args.color:
|
||||
smart.colorize(False)
|
||||
|
||||
if args.verbose:
|
||||
smart.cfg['verbose'] = True
|
||||
|
||||
try:
|
||||
if args.disks :
|
||||
map(lambda x: x[0], args.disks)
|
||||
smart.cfg['disks'] = sorted(list(set(map(lambda x: x[0], args.disks)) - set(map(lambda x: x[0], args.ignore))))
|
||||
except Exception as e:
|
||||
print( traceback.format_exc() )
|
||||
pass
|
||||
|
||||
if len(smart.cfg['disks']) > 0 and not args.nagios:
|
||||
col = list()
|
||||
msg = list()
|
||||
ret = list()
|
||||
sched = list()
|
||||
for disk in smart.cfg['disks']:
|
||||
try:
|
||||
if not stat.S_ISBLK(os.stat(disk).st_mode):
|
||||
raise()
|
||||
except:
|
||||
msg.append("Invalid device")
|
||||
ret.append(255)
|
||||
|
||||
if smart.cfg['verbose']:
|
||||
print( "Checking {}:".format(disk) )
|
||||
res = check_single_dev(disk, report = smart.cfg['verbose'])
|
||||
|
||||
if res == True:
|
||||
col.append(bcolors.OKGREEN)
|
||||
msg.append("Disk healthy")
|
||||
ret.append(0)
|
||||
elif res == None:
|
||||
col.append(bcolors.WARNING)
|
||||
msg.append("Insufficient SMART support")
|
||||
ret.append(2)
|
||||
else:
|
||||
col.append(bcolors.FAIL)
|
||||
msg.append("Disk failing")
|
||||
ret.append(1)
|
||||
|
||||
if smart.cfg['smartctl_test_frequency'] > 0:
|
||||
if smart.cfg['verbose']:
|
||||
print( "Scheduling selftest {}:".format(disk) )
|
||||
if smart.schedule_selftest(disk, report = smart.cfg['verbose']):
|
||||
sched.append('New selftest scheduled.')
|
||||
else:
|
||||
sched.append('')
|
||||
else:
|
||||
sched.append('')
|
||||
|
||||
for i in range(0, len(smart.cfg['disks'])):
|
||||
print( "{disk}: {col}{msg}{cls} {sched}".format(col=col[i], msg=msg[i], disk=smart.cfg['disks'][i], cls=bcolors.ENDC, sched = sched[i]) )
|
||||
sys.exit(max(ret))
|
||||
|
||||
elif 'disks' in args and args.nagios:
|
||||
res = dict()
|
||||
for disk in smart.cfg['disks']:
|
||||
res[disk] = check_single_dev(disk, report = smart.cfg['verbose'])
|
||||
if smart.cfg['smartctl_test_frequency'] > 0:
|
||||
smart.schedule_selftest(disk, report = smart.cfg['verbose'])
|
||||
|
||||
# Format nagios line
|
||||
line = ''
|
||||
for disk in sorted(res):
|
||||
if res[disk] == True:
|
||||
status = 'Ok'
|
||||
elif res[disk] == None:
|
||||
status = 'Unkn'
|
||||
elif res[disk] == False:
|
||||
status = 'Err'
|
||||
line += "{}: {}, ".format(disk, status)
|
||||
line = line[:-2]
|
||||
|
||||
if False in res.values():
|
||||
print( 'CRITICAL: smart_status reports {} disk(s) as having errors. {}'.format(res.values().count(False), line) )
|
||||
sys.exit(2)
|
||||
else:
|
||||
print( 'OK: smart_status reports {} disk(s) as okay. {}'.format(res.values().count(True), line) )
|
||||
sys.exit(0)
|
||||
|
80
src/smartjson.py
Executable file
80
src/smartjson.py
Executable file
@ -0,0 +1,80 @@
|
||||
#! /usr/bin/python3
|
||||
|
||||
from SmartDevice import SmartDevice
|
||||
from pySMART import Device, DeviceList
|
||||
|
||||
import jsonpickle
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
actionList = ["list","status","short","long","abort"]
|
||||
|
||||
parser = argparse.ArgumentParser(description='Handle smart disks.')
|
||||
parser.add_argument('action', help="What to do: {}".format(actionList))
|
||||
parser.add_argument('-d','--disk', required=False, help="Use a single disk ex: 'sdb'")
|
||||
parser.add_argument('-e','--exclude', required=False, help="Exclude a single disk ex: 'sda'")
|
||||
|
||||
args = parser.parse_args()
|
||||
disk = args.disk
|
||||
exclude = args.exclude
|
||||
action = args.action
|
||||
|
||||
devices = {}
|
||||
|
||||
|
||||
|
||||
if disk:
|
||||
dev = Device(disk)
|
||||
d = SmartDevice(dev)
|
||||
devices[dev] = {
|
||||
"model" : d.model,
|
||||
"smart_status" : d.smart_status,
|
||||
"hours": d.hours
|
||||
}
|
||||
else:
|
||||
devlist = DeviceList()
|
||||
for dev in devlist.devices:
|
||||
try:
|
||||
d = SmartDevice(dev)
|
||||
except Exception as e:
|
||||
print("Failed to convert to device",e)
|
||||
continue
|
||||
if d.dev == exclude :
|
||||
continue
|
||||
devices[dev] = {
|
||||
"model" : d.model,
|
||||
"smart_status" : d.smart_status,
|
||||
"hours": d.hours
|
||||
}
|
||||
|
||||
if action == "list" :
|
||||
|
||||
print (jsonpickle.encode(devices,indent=2))
|
||||
|
||||
elif action == "long" :
|
||||
for dev in devices:
|
||||
dev.run_selftest("long")
|
||||
|
||||
elif action == "short" :
|
||||
for dev in devices:
|
||||
dev.run_selftest("short")
|
||||
|
||||
elif action == "status" :
|
||||
code = 0
|
||||
msgList = []
|
||||
for dev in devices:
|
||||
r = dev.get_selftest_result()
|
||||
d = SmartDevice(dev)
|
||||
msg = "{} / {} : {}".format(d.dev,d.serial, r[1])
|
||||
if r[0] == 1 or r[0] == 3:
|
||||
code = 1
|
||||
if r[2] :
|
||||
msg += " {}% done".format(r[2])
|
||||
msgList.append(msg)
|
||||
print( "\n".join(msgList) )
|
||||
sys.exit(code)
|
||||
|
||||
elif action == "json":
|
||||
print( jsonpickle.encode(devices, indent=2) )
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user