1 |
apollock |
29 |
#!/usr/bin/python |
2 |
apollock |
30 |
# vi:set ts=2:et:sw=2 |
3 |
apollock |
29 |
# |
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 |
apollock |
30 |
import diskstats |
25 |
|
|
import syslog |
26 |
|
|
import optparse |
27 |
|
|
import ConfigParser |
28 |
|
|
import sys |
29 |
|
|
import os |
30 |
|
|
import time |
31 |
apollock |
33 |
import signal |
32 |
apollock |
30 |
|
33 |
|
|
spindown_cmd = "" |
34 |
|
|
spindown_cmd_args = "" |
35 |
apollock |
33 |
keep_running = True |
36 |
|
|
signals = {} |
37 |
apollock |
30 |
|
38 |
apollock |
37 |
class Error(Exception): |
39 |
|
|
pass |
40 |
|
|
|
41 |
|
|
|
42 |
apollock |
33 |
def signal_handler(signal, stack): |
43 |
|
|
global keep_running |
44 |
|
|
keep_running = False |
45 |
apollock |
30 |
|
46 |
apollock |
33 |
|
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 |
apollock |
30 |
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 |
apollock |
33 |
|
73 |
|
|
def log(message): |
74 |
|
|
pass |
75 |
|
|
|
76 |
|
|
|
77 |
apollock |
30 |
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 |
apollock |
34 |
except ConfigParser.MissingSectionHeaderError, e: |
87 |
|
|
print "Parser error: '%s'" % (e.message) |
88 |
|
|
sys.exit(1) |
89 |
apollock |
30 |
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 |
apollock |
33 |
global keep_running |
136 |
apollock |
30 |
debug(options, "Monitoring disks") |
137 |
apollock |
33 |
while keep_running: |
138 |
apollock |
30 |
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 |
apollock |
35 |
if (now - config[disk]["timestamp"]) >= int(config[disk]["wait"]): |
157 |
apollock |
30 |
debug(options, "Disk eligible for spinning down") |
158 |
|
|
# We can spin this disk down |
159 |
|
|
if not config[disk]["spun_down"]: |
160 |
apollock |
38 |
if spin_down(options, disk) == 0: |
161 |
apollock |
30 |
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 |
apollock |
31 |
debug(options, "Disk idle for %s seconds, but not for long enough (%s)" % (now - config[disk]["timestamp"], config[disk]["wait"])) |
170 |
apollock |
30 |
else: |
171 |
apollock |
39 |
debug(options, "Disk not idle (old msio: %s, current msio: %s)" % (config[disk]["last_msio"], msio)) |
172 |
apollock |
30 |
config[disk]["last_msio"] = msio |
173 |
|
|
config[disk]["timestamp"] = int(time.time()) |
174 |
apollock |
36 |
if config[disk]["spun_down"]: |
175 |
|
|
debug(options, "%s presumed spun back up by activity" % (disk)) |
176 |
|
|
config[disk]["spun_down"] = False |
177 |
apollock |
30 |
debug(options, "Sleeping") |
178 |
|
|
time.sleep(60) |
179 |
apollock |
33 |
debug(options, "Shutting down") |
180 |
apollock |
30 |
|
181 |
apollock |
29 |
def main(): |
182 |
apollock |
33 |
global options |
183 |
|
|
global signals |
184 |
|
|
signals = getsignals() |
185 |
|
|
signal.signal(signal.SIGTERM, signal_handler) |
186 |
|
|
signal.signal(signal.SIGINT, signal_handler) |
187 |
apollock |
30 |
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 |
apollock |
29 |
|
205 |
apollock |
30 |
config = load_config(options) |
206 |
|
|
monitor_disks(config, options) |
207 |
|
|
|
208 |
apollock |
29 |
if __name__ == "__main__": |
209 |
apollock |
30 |
main() |