| 1 |
#!/usr/bin/python |
| 2 |
# vi:set ts=2:et:sw=2 |
| 3 |
# |
| 4 |
# Daemon to monitor /proc/diskstats and spin down USB drives determined to be |
| 5 |
# idle |
| 6 |
# |
| 7 |
# Author: Andrew Pollock <me@andrew.net.au> |
| 8 |
# Copyright: (c) 2008 Andrew Pollock <me@andrew.net.au> |
| 9 |
# |
| 10 |
|
| 11 |
# |
| 12 |
# Need a config file that contains the drives to watch and how long they must |
| 13 |
# be idle for before spinning down |
| 14 |
# |
| 15 |
# Need to determine if sg_start is installed, and if so, where |
| 16 |
# |
| 17 |
# Need to monitor field 13 of /proc/diskstats for each drive. Value unchanged |
| 18 |
# from last time means idle, value changed means not-idle and presumed spun-up |
| 19 |
# |
| 20 |
# Need to maintain a flag for when we've spun the drive down, so we don't keep |
| 21 |
# spinning it down every time we think it's idle |
| 22 |
# |
| 23 |
|
| 24 |
import diskstats |
| 25 |
import syslog |
| 26 |
import optparse |
| 27 |
import ConfigParser |
| 28 |
import sys |
| 29 |
import os |
| 30 |
import time |
| 31 |
import signal |
| 32 |
|
| 33 |
spindown_cmd = "" |
| 34 |
spindown_cmd_args = "" |
| 35 |
keep_running = True |
| 36 |
signals = {} |
| 37 |
|
| 38 |
class Error(Exception): |
| 39 |
pass |
| 40 |
|
| 41 |
|
| 42 |
def signal_handler(signal, stack): |
| 43 |
global keep_running |
| 44 |
keep_running = False |
| 45 |
|
| 46 |
|
| 47 |
def getsignals(): |
| 48 |
signals = {} |
| 49 |
for name in dir(signal): |
| 50 |
if name.startswith("SIG"): |
| 51 |
signals[getattr(signal, name)] = name |
| 52 |
return signals |
| 53 |
|
| 54 |
|
| 55 |
def search_path(filename, search_path): |
| 56 |
file_found = False |
| 57 |
paths = search_path.split(os.pathsep) |
| 58 |
for path in paths: |
| 59 |
if os.path.exists(os.path.join(path, filename)): |
| 60 |
file_found = True |
| 61 |
break |
| 62 |
if file_found: |
| 63 |
return os.path.abspath(os.path.join(path, filename)) |
| 64 |
else: |
| 65 |
return None |
| 66 |
|
| 67 |
|
| 68 |
def debug(options, message): |
| 69 |
if options.debug: |
| 70 |
print "%s: %s" % (time.asctime(time.localtime()), message) |
| 71 |
|
| 72 |
|
| 73 |
def log(message): |
| 74 |
pass |
| 75 |
|
| 76 |
|
| 77 |
def load_config(options): |
| 78 |
global spindown_cmd, spindown_cmd_args |
| 79 |
cf = ConfigParser.SafeConfigParser() |
| 80 |
try: |
| 81 |
debug(options, "Attempting to read %s" % options.config) |
| 82 |
cf.readfp(open(options.config)) |
| 83 |
except IOError, e: |
| 84 |
print "Got a '%s' trying to read %s" % (e.strerror, options.config) |
| 85 |
sys.exit(1) |
| 86 |
except ConfigParser.MissingSectionHeaderError, e: |
| 87 |
print "Parser error: '%s'" % (e.message) |
| 88 |
sys.exit(1) |
| 89 |
if cf.has_option("DEFAULT", "wait"): |
| 90 |
defaultwait = cf.get("DEFAULT", "wait") |
| 91 |
else: |
| 92 |
defaultwait = 600; |
| 93 |
config = {} |
| 94 |
# TODO: Need to deal with a malformed config file here |
| 95 |
for disk in cf.defaults()['disks'].split(","): |
| 96 |
if cf.has_option(disk, "wait"): |
| 97 |
wait = cf.get(disk, "wait") |
| 98 |
else: |
| 99 |
wait = defaultwait |
| 100 |
config[disk] = { 'wait': wait, 'last_msio': 0, 'spun_down': False, 'timestamp': 0 } |
| 101 |
if cf.has_option("DEFAULT", "spindown_cmd"): |
| 102 |
spindown_cmd = cf.get("DEFAULT", "spindown_cmd") |
| 103 |
if cf.has_option("DEFAULT", "spindown_cmd_args"): |
| 104 |
spindown_cmd_args = cf.get("DEFAULT", "spindown_cmd_args") |
| 105 |
if not spindown_cmd: |
| 106 |
# Try searching for sg_start |
| 107 |
spindown_cmd = search_path("sg_start", os.getenv("PATH")) |
| 108 |
if spindown_cmd: |
| 109 |
spindown_cmd_args = "--stop --pc=2" |
| 110 |
else: |
| 111 |
# We couldn't find anything to spin down the disks |
| 112 |
print "No disk spinning down command specified or found in $PATH" |
| 113 |
sys.exit(1) |
| 114 |
debug(options, "Configuration loaded:") |
| 115 |
debug(options, "spindown_cmd: %s" % (spindown_cmd)) |
| 116 |
debug(options, "spindown_cmd_args: %s" % (spindown_cmd_args)) |
| 117 |
if options.debug: |
| 118 |
for disk in config: |
| 119 |
debug(options, "%s: %s" % (disk, config[disk])) |
| 120 |
return config |
| 121 |
|
| 122 |
|
| 123 |
def spin_down(options, disk): |
| 124 |
if not options.noop: |
| 125 |
if spindown_cmd_args: |
| 126 |
return os.system("%s %s %s" % (spindown_cmd, spindown_cmd_args, disk)) >> 8 |
| 127 |
else: |
| 128 |
return os.system("%s %s" % (spindown_cmd, disk)) >> 8 |
| 129 |
else: |
| 130 |
debug(options, "Not really spinning down the disk") |
| 131 |
return 0 |
| 132 |
|
| 133 |
|
| 134 |
def monitor_disks(config, options): |
| 135 |
global keep_running |
| 136 |
debug(options, "Monitoring disks") |
| 137 |
while keep_running: |
| 138 |
for disk in config: |
| 139 |
debug(options, "Considering %s" % (disk)) |
| 140 |
ds = diskstats.DiskStats(disk) |
| 141 |
msio = None |
| 142 |
try: |
| 143 |
msio = ds.diskstat("msio") |
| 144 |
except diskstats.Error, e: |
| 145 |
# This disk doesn't exist at this time |
| 146 |
debug(options, "%s is not present" % (disk)) |
| 147 |
continue |
| 148 |
if config[disk]["last_msio"] == 0: |
| 149 |
debug(options, "First time we've considered this disk") |
| 150 |
config[disk]["last_msio"] = msio |
| 151 |
config[disk]["timestamp"] = int(time.time()) |
| 152 |
else: |
| 153 |
if msio == config[disk]["last_msio"]: |
| 154 |
debug(options, "Disk has been idle since last considered") |
| 155 |
now = int(time.time()) |
| 156 |
if (now - config[disk]["timestamp"]) >= int(config[disk]["wait"]): |
| 157 |
debug(options, "Disk eligible for spinning down") |
| 158 |
# We can spin this disk down |
| 159 |
if not config[disk]["spun_down"]: |
| 160 |
if spin_down(options, disk) == 0: |
| 161 |
debug(options, "Disk spun down") |
| 162 |
config[disk]["spun_down"] = True |
| 163 |
else: |
| 164 |
raise Error("Failed to spin down %s" % disk) |
| 165 |
else: |
| 166 |
debug(options, "Disk already spun down") |
| 167 |
else: |
| 168 |
# This disk is ineligible for spinning down at this time |
| 169 |
debug(options, "Disk idle for %s seconds, but not for long enough (%s)" % (now - config[disk]["timestamp"], config[disk]["wait"])) |
| 170 |
else: |
| 171 |
debug(options, "Disk not idle (old msio: %s, current msio: %s)" % (config[disk]["last_msio"], msio)) |
| 172 |
config[disk]["last_msio"] = msio |
| 173 |
config[disk]["timestamp"] = int(time.time()) |
| 174 |
if config[disk]["spun_down"]: |
| 175 |
debug(options, "%s presumed spun back up by activity" % (disk)) |
| 176 |
config[disk]["spun_down"] = False |
| 177 |
debug(options, "Sleeping") |
| 178 |
time.sleep(60) |
| 179 |
debug(options, "Shutting down") |
| 180 |
|
| 181 |
def main(): |
| 182 |
global options |
| 183 |
global signals |
| 184 |
signals = getsignals() |
| 185 |
signal.signal(signal.SIGTERM, signal_handler) |
| 186 |
signal.signal(signal.SIGINT, signal_handler) |
| 187 |
parser = optparse.OptionParser() |
| 188 |
parser.add_option("-n", "--dry-run", |
| 189 |
action="store_true", |
| 190 |
dest="noop", |
| 191 |
default=False, |
| 192 |
help="Don't do anything, just log what would be done") |
| 193 |
parser.add_option("-c", "--config", |
| 194 |
action="store", |
| 195 |
dest="config", |
| 196 |
default="/etc/usbspindownd.conf", |
| 197 |
help="Configuration file for usbspindownd") |
| 198 |
parser.add_option("-d", "--debug", |
| 199 |
action="store_true", |
| 200 |
dest="debug", |
| 201 |
default=False, |
| 202 |
help="Turn on extra debugging") |
| 203 |
(options, args) = parser.parse_args() |
| 204 |
|
| 205 |
config = load_config(options) |
| 206 |
monitor_disks(config, options) |
| 207 |
|
| 208 |
if __name__ == "__main__": |
| 209 |
main() |