source: trunk/RetroStatus/RetroStatus.py@ 201

Last change on this file since 201 was 201, checked in by Nicholas Riley, 17 years ago

RetroStatus

File size: 9.7 KB
Line 
1#!/usr/bin/pythonw
2
3from appscript import app, k
4from aemreceive.sfba import *
5from Foundation import NSLog, NSUserDefaults, NSDate
6from subprocess import call
7from traceback import print_exc
8from cStringIO import StringIO
9from datetime import datetime
10import RSS
11import email, smtplib
12from email.MIMEMessage import MIMEMessage
13from email.MIMEText import MIMEText
14from email.Utils import formataddr, formatdate
15from formatflowed import convertToFlowed
16import os, sys
17import objc # XXX should switch to public DADiskMount/Eject functions
18objc.loadBundle('DiskManagement', globals(),
19 '/System/Library/PrivateFrameworks/DiskManagement.framework')
20
21EVENTS_DIR = os.path.expanduser('~/Sites/RetroStatus')
22EVENTS_MAX = 100
23
24FROM_NAME, FROM_ADDR = 'Retrospect backup server', 'retrospect' + '@rileys.us'
25TO_ADDR = ['%s@rileys.us' for %s in ['julia', 'gary']]
26SMTP_SERVER = 'calamity.bos.sabi.net'
27
28DEBUG = True
29
30# XXX implement Web page with current status
31
32### notifications
33
34GROWL_APP_NAME = 'RetroStatus'
35NOTIFICATION_BACKUP = 'Backup'
36NOTIFICATION_MEDIA = 'Media change'
37NOTIFICATIONS_ALL = [NOTIFICATION_BACKUP, NOTIFICATION_MEDIA]
38EVENTS_FILE = os.path.join(EVENTS_DIR, 'index.rdf')
39EVENTS_FILE_TEMP = EVENTS_FILE + '~'
40
41growl = app('GrowlHelperApp')
42events = RSS.TrackingChannel()
43
44try: os.mkdir(EVENTS_DIR)
45except: pass
46events.setMD((RSS.ns.rss10, 'channel'),
47 {(RSS.ns.rss10, 'title'): 'Retrospect status',
48 (RSS.ns.rss10, 'link'): os.path.basename(EVENTS_FILE)})
49
50def growlNotify(name, title, description, **kw):
51 # XXX restore coalescing support from LocationDo
52 params = dict(with_name=name,
53 title=title,
54 description=unicode(description),
55 application_name=GROWL_APP_NAME,
56 icon_of_application='Retrospect')
57 params.update(kw)
58 if DEBUG:
59 NSLog('%s: %s' % (title, description))
60 growl.notify(**params)
61
62def addEvent(title, description, preformatted=False):
63 description = description.replace('&', '&amp;').replace('<', '&lt;')
64 if preformatted:
65 description = '<pre>%s</pre>' % description
66 else:
67 description = description.replace('\n\n', '<p>').replace('\n', '<br>')
68 # XXX provide valid URL for event details
69 events.addItem({(RSS.ns.dc, 'date'): datetime.today().isoformat(),
70 (RSS.ns.rss10, 'title'): title,
71 (RSS.ns.rss10, 'link'): datetime.today().isoformat(),
72 (RSS.ns.rss10, 'description'): description})
73 events.truncateToLength(EVENTS_MAX)
74 file(EVENTS_FILE_TEMP, 'w').write(str(events))
75 os.rename(EVENTS_FILE_TEMP, EVENTS_FILE)
76
77def sendMail(subject, text):
78 message = MIMEText(convertToFlowed(text), 'plain; format=flowed')
79 message.set_unixfrom(FROM_ADDR)
80 message['From'] = formataddr((FROM_NAME, FROM_ADDR))
81 message['To'] = TO_ADDR
82 message['Date'] = formatdate(localtime=True)
83 message['Subject'] = subject
84 smtp = smtplib.SMTP(SMTP_SERVER)
85 smtp.sendmail(message['From'], TO_ADDR, message.as_string())
86
87### disk management
88
89DEFAULT_LAST_USED_NAME = 'Last used backup volume name'
90DEFAULT_LAST_USED_DATE = 'Date of last backup volume use'
91
92defaults = NSUserDefaults.standardUserDefaults()
93diskManager = DMManager.sharedManager()
94diskManager.generateConnection()
95
96def diskWithName(name):
97 disks = diskManager.disks()
98 for disk in disks:
99 if disk.volumeName() == name:
100 return disk
101
102def lastUsedDisk():
103 diskName = defaults.get(DEFAULT_LAST_USED_NAME)
104 if diskName:
105 return diskWithName(diskName)
106
107def diskIsMounted(disk):
108 mountPoint = disk.mountPoint()
109 return (mountPoint and os.path.exists(mountPoint))
110
111def ejectDisk(disk):
112 if not diskIsMounted(disk):
113 return True
114 if not diskManager.ejectDisk_synchronous_(disk, True):
115 return False
116 if diskIsMounted(disk):
117 growlNotify(NOTIFICATION_MEDIA, 'Eject failed',
118 'Using diskutil to eject %s' % disk)
119 return call('diskutil', 'eject', disk.diskIdentifier()) is 0
120 return True
121
122def mountDisk(disk):
123 if diskIsMounted(disk):
124 return True
125 if not diskManager.mountDisk_includeChildren_synchronous_(disk, False, True):
126 return False
127 if not diskIsMounted(disk):
128 growlNotify(NOTIFICATION_MEDIA, 'Mount failed',
129 'Using diskutil to mount %s' % disk)
130 return call('diskutil', 'mount', disk.diskIdentifier()) is 0
131 return True
132
133### Retrospect event handlers
134
135def retrospectstart(autolaunchboolean): pass
136def scriptstart(scriptname, startdate): pass
137def volumestart(volumename): pass
138def volumeend(volumename, kbcopied, filecount, durationinseconds, backupdate,
139 enddate, startdate, destinationname, clientname, scriptname,
140 backuptypestring, fileerrorcount, volumeerrorcode,
141 volumeerrormessage, zonename='', remotename='',
142 subvolumediskname=''):
143 growlNotify(NOTIFICATION_BACKUP, scriptname,
144 '%s\n%s' % (volumename, volumeerrormessage))
145 addEvent(volumename, '%s\n\n%d file(s) copied; %d had errors.' %
146 (volumeerrormessage, filecount, fileerrorcount))
147
148def mediarequest(mediatypestring, requestedmembername, mediaisknownboolean):
149 disk = diskWithName(requestedmembername)
150 if disk and mountDisk(disk):
151 defaults[DEFAULT_LAST_USED_NAME] = requestedmembername
152 defaults[DEFAULT_LAST_USED_DATE] = NSDate.date()
153 growlNotify(NOTIFICATION_MEDIA, requestedmembername,
154 'Disk is in use: please DO NOT swap')
155 else:
156 sendMail("IMPORTANT: Can't find disk %s" % requestedmembername,
157 "Retrospect can't find the disk '%s'. Please eject the old disk, if any, and check that the correct disk is inserted.")
158
159def mediarequesttimedout(numberofsecondswaited): pass
160
161def scriptend(scriptname, scripterrormessage, errorcount):
162 disk = lastUsedDisk()
163 if disk and ejectDisk(disk):
164 growlNotify(NOTIFICATION_MEDIA, disk.volumeName(),
165 'Disk no longer in use: OK to swap')
166 addEvent('Completed: scriptname',
167 '%s; %s error(s)' % (scripterrormessage, errorcount))
168
169def scriptcheckfailed(scriptname, failuremessage, scheduledate):
170 disk = lastUsedDisk()
171 if not disk or not diskIsMounted(disk):
172 sendMail("Please swap backup media before %s" % scheduledate.ctime(),
173 "%s\nEject the old disk, if any, and check that the correct disk is inserted." % failuremessage.replace('\r', '\n'))
174
175def retrospectquit():
176 disk = lastUsedDisk()
177 if not disk or not diskIsMounted(disk):
178 sendMail("Please swap backup media",
179 "Eject the old disk, if any, and check that the correct disk is inserted.")
180 stopeventloop()
181 sys.exit(0)
182
183def handleEvent(eventName, params):
184 eventHandler = globals().get(eventName)
185 params = dict(zip(params[::2], params[1::2]))
186 for k in params.keys():
187 if params[k] == '': del params[k]
188 eventStr = '%s(%s)' % (eventName, ', '.join(['%s=%r' % (k, v) for k, v
189 in params.iteritems()]))
190 if not eventHandler:
191 addEvent('Unhandled Retrospect event', eventStr)
192 if DEBUG: NSLog('unhandled event: ' + eventStr)
193 return
194 try:
195 eventHandler(**params)
196 except:
197 s = StringIO()
198 print_exc(file=s)
199 addEvent('Retrospect event handler failed',
200 '%s\n\n%s' % (eventStr, s.getval()), True)
201 if DEBUG:
202 NSLog('event handler failed for %s:\n%s' % (eventStr, s.getval())
203
204installeventhandler(handleEvent, 'ascrpsbr',
205 ('snam', 'eventName', kAE.typeUnicodeText),
206 ('usrf', 'params', ArgListOf(kAE.typeWildCard)))
207
208if __name__ == '__main__':
209 growl.register(
210 as_application=GROWL_APP_NAME,
211 all_notifications=NOTIFICATIONS_ALL,
212 default_notifications=NOTIFICATIONS_ALL)
213
214 starteventloop()
215
216# example events:
217
218# retrospectstart(autolaunchboolean=False)
219# scriptstart(scriptname='Boston Backup', startdate=datetime.datetime(2005, 11, 23, 2, 31, 51))
220# volumestart(volumename='Backup Clients:Babs')
221# volumeend(startdate=datetime.datetime(2005, 11, 23, 2, 31, 51), kbcopied=0, enddate=datetime.datetime(2005, 11, 23, 2, 32, 5), backuptypestring='Normal', volumename='Backup Clients:Babs', clientname='Babs', destinationname='Boston backup A', scriptname='Boston Backup', volumeerrormessage='client is not visible on network', backupdate=datetime.datetime(2005, 11, 23, 2, 32, 5), durationinseconds=0, volumeerrorcode=-1028, fileerrorcount=0, remotename='Babs', filecount=0, zonename='10.0.0.9')
222# volumestart(volumename=' Local Desktop:Bookworm')
223# mediarequest: appears even if media is available
224# mediarequest(mediatypestring='disk', requestedmembername='1-Boston backup A', mediaisknownboolean=True)
225# mediarequest(mediatypestring='disk', requestedmembername='1-Boston backup A', mediaisknownboolean=True)
226# volumeend(startdate=datetime.datetime(2005, 11, 23, 2, 31, 51), kbcopied=2291359, enddate=datetime.datetime(2005, 11, 23, 2, 56, 23), backuptypestring='Normal', volumename=' Local Desktop:Bookworm', clientname='Local Desktop', destinationname='Boston backup A', scriptname='Boston Backup', volumeerrormessage='successful', backupdate=datetime.datetime(2005, 11, 23, 2, 31, 51), durationinseconds=1066, volumeerrorcode=0, fileerrorcount=15, remotename='Local Desktop', filecount=72717)
227# scriptend(scriptname='Boston Backup', scripterrormessage='client is not visible on network', errorcount=18)
228# scriptcheckfailed(scriptname='Boston Backup', failuremessage='Appending to disk \xd21-Boston backup B\xd3\r\t149.0 G available (80%) of 186.2 G total capacity', scheduledate=datetime.datetime(2005, 11, 24, 1, 0))
229# retrospectquit()
Note: See TracBrowser for help on using the repository browser.