source: trunk/RetroStatus/RetroStatus.py@ 204

Last change on this file since 204 was 204, checked in by Nicholas Riley, 19 years ago

formatflowed.py: Removed; no longer used.

RetroStatus.py: Remove format=flowed support, it doesn't work nicely
with UTF-8 encoded email. Fix sendMail bugs; it works now. Handle
volumestart event on clients (parameter is misspelled). Standardize
RSS titles to be of the form "name: status". Fixed scriptcheckfailed
to pull out the requested disk and actually check for it (mounted or
not). Add an example of the aformentioned volumestart event variant.
Fixed repeated typo of StringIO.getvalue in event exception handler.

File size: 10.0 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
15import os, re, sys
16import objc # XXX should switch to public DADiskMount/Eject functions
17objc.loadBundle('DiskManagement', globals(),
18 '/System/Library/PrivateFrameworks/DiskManagement.framework')
19
20EVENTS_DIR = os.path.expanduser('~/Sites/RetroStatus')
21EVENTS_MAX = 100
22
23FROM_NAME, FROM_ADDR = 'Retrospect backup server', 'retrospect' + '@rileys.us'
24TO_ADDR = ['%s@rileys.us' % user for user in ['julia', 'gary']]
25SMTP_SERVER = 'calamity.bos.sabi.net'
26
27DEBUG = True
28
29# XXX implement Web page with current status
30
31### notifications
32
33GROWL_APP_NAME = 'RetroStatus'
34NOTIFICATION_BACKUP = 'Backup'
35NOTIFICATION_MEDIA = 'Media change'
36NOTIFICATIONS_ALL = [NOTIFICATION_BACKUP, NOTIFICATION_MEDIA]
37EVENTS_FILE = os.path.join(EVENTS_DIR, 'index.rdf')
38EVENTS_FILE_TEMP = EVENTS_FILE + '~'
39
40growl = app('GrowlHelperApp')
41events = RSS.TrackingChannel()
42
43try: os.mkdir(EVENTS_DIR)
44except: pass
45events.setMD((RSS.ns.rss10, 'channel'),
46 {(RSS.ns.rss10, 'title'): 'Retrospect status',
47 (RSS.ns.rss10, 'link'): os.path.basename(EVENTS_FILE)})
48
49def growlNotify(name, title, description, **kw):
50 # XXX restore coalescing support from LocationDo
51 params = dict(with_name=name,
52 title=title,
53 description=unicode(description),
54 application_name=GROWL_APP_NAME,
55 icon_of_application='Retrospect')
56 params.update(kw)
57 if DEBUG:
58 NSLog('%s: %s' % (title, description))
59 growl.notify(**params)
60
61def addEvent(title, description, preformatted=False):
62 description = description.replace('&', '&amp;').replace('<', '&lt;')
63 if preformatted:
64 description = '<pre>%s</pre>' % description
65 else:
66 description = description.replace('\n\n', '<p>').replace('\n', '<br>')
67 # XXX provide valid URL for event details
68 events.addItem({(RSS.ns.dc, 'date'): datetime.today().isoformat(),
69 (RSS.ns.rss10, 'title'): title,
70 (RSS.ns.rss10, 'link'): datetime.today().isoformat(),
71 (RSS.ns.rss10, 'description'): description})
72 events.truncateToLength(EVENTS_MAX)
73 file(EVENTS_FILE_TEMP, 'w').write(str(events))
74 os.rename(EVENTS_FILE_TEMP, EVENTS_FILE)
75
76def sendMail(subject, text):
77 message = MIMEText(text.encode('utf8'), _charset='utf8')
78 message.set_unixfrom(FROM_ADDR)
79 message['From'] = formataddr((FROM_NAME, FROM_ADDR))
80 message['To'] = ', '.join(TO_ADDR)
81 message['Date'] = formatdate(localtime=True)
82 message['Subject'] = subject
83 smtp = smtplib.SMTP(SMTP_SERVER)
84 smtp.sendmail(message['From'], TO_ADDR, message.as_string())
85
86### disk management
87
88DEFAULT_LAST_USED_NAME = 'Last used backup volume name'
89DEFAULT_LAST_USED_DATE = 'Date of last backup volume use'
90
91defaults = NSUserDefaults.standardUserDefaults()
92diskManager = DMManager.sharedManager()
93diskManager.generateConnection()
94
95def diskWithName(name):
96 disks = diskManager.disks()
97 for disk in disks:
98 if disk.volumeName() == name:
99 return disk
100
101def lastUsedDisk():
102 diskName = defaults.get(DEFAULT_LAST_USED_NAME)
103 if diskName:
104 return diskWithName(diskName)
105
106def diskIsMounted(disk):
107 mountPoint = disk.mountPoint()
108 return (mountPoint and os.path.exists(mountPoint))
109
110def ejectDisk(disk):
111 if not diskIsMounted(disk):
112 return True
113 if not diskManager.ejectDisk_synchronous_(disk, True):
114 return False
115 if diskIsMounted(disk):
116 growlNotify(NOTIFICATION_MEDIA, 'Eject failed',
117 'Using diskutil to eject %s' % disk)
118 return call('diskutil', 'eject', disk.diskIdentifier()) is 0
119 return True
120
121def mountDisk(disk):
122 if diskIsMounted(disk):
123 return True
124 if not diskManager.mountDisk_includeChildren_synchronous_(disk, False, True):
125 return False
126 if not diskIsMounted(disk):
127 growlNotify(NOTIFICATION_MEDIA, 'Mount failed',
128 'Using diskutil to mount %s' % disk)
129 return call('diskutil', 'mount', disk.diskIdentifier()) is 0
130 return True
131
132### Retrospect event handlers
133
134def retrospectstart(autolaunchboolean): pass
135def scriptstart(scriptname, startdate): pass
136# yes, this really should be "remotetname"
137def volumestart(volumename, clientname='', remotetname=''): 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('%s: %s' % (volumename, volumeerrormessage),
146 '%d file(s) copied; %d had errors.' % (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(scriptname + ': script completed',
167 '%s; %s error(s)' % (scripterrormessage, errorcount))
168
169# XXX does this get sent when it's supposed to?
170# XXX if not, we could ask for quit on script end if Retrospect was started
171# XXX by a script, and lengthen the lookahead interval
172def scriptcheckfailed(scriptname, failuremessage, scheduledate):
173 failuremessage = failuremessage.replace('\r', '\n').decode('macroman')
174 m = re.search(u'\u201c([^\u201d]+)\u201d', failuremessage)
175 if not m:
176 addEvent(scriptname + ': check failed', failuremessage)
177 return
178 if not diskWithName(m.group(1)):
179 sendMail("Please swap backup media before %s" % scheduledate.ctime(),
180 failuremessage + "\n\nEject the old disk, if any, and check that the correct disk is inserted.")
181
182def retrospectquit():
183 stopeventloop()
184
185def handleEvent(eventName, params):
186 eventHandler = globals().get(eventName)
187 params = dict(zip(params[::2], params[1::2]))
188 for k in params.keys():
189 if params[k] == '': del params[k]
190 eventStr = '%s(%s)' % (eventName, ', '.join(['%s=%r' % (k, v) for k, v
191 in params.iteritems()]))
192 if not eventHandler:
193 addEvent('%s: unhandled event' % eventName, eventStr)
194 if DEBUG: NSLog('unhandled event: ' + eventStr)
195 return
196 try:
197 eventHandler(**params)
198 except:
199 s = StringIO()
200 print_exc(file=s)
201 addEvent('%s: event handler failed' % eventName,
202 '%s\n\n%s' % (eventStr, s.getvalue()), True)
203 if DEBUG:
204 NSLog('event handler failed for %s:\n%s' % (eventStr, s.getvalue()))
205
206installeventhandler(handleEvent, 'ascrpsbr',
207 ('snam', 'eventName', kAE.typeUnicodeText),
208 ('usrf', 'params', ArgListOf(kAE.typeWildCard)))
209
210if __name__ == '__main__':
211 growl.register(
212 as_application=GROWL_APP_NAME,
213 all_notifications=NOTIFICATIONS_ALL,
214 default_notifications=NOTIFICATIONS_ALL)
215
216 starteventloop()
217
218# example events:
219
220# retrospectstart(autolaunchboolean=False)
221# scriptstart(scriptname='Boston Backup', startdate=datetime.datetime(2005, 11, 23, 2, 31, 51))
222# volumestart(volumename='Backup Clients:Babs')
223
224# 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')
225# volumestart(volumename=' Local Desktop:Bookworm')
226# volumestart(volumename='Backup Clients:Concord:Mac OS X', remotetname='Backup Clients:Concord', clientname='Backup Clients:Concord')
227# mediarequest: appears even if media is available
228# mediarequest(mediatypestring='disk', requestedmembername='1-Boston backup A', mediaisknownboolean=True)
229# mediarequest(mediatypestring='disk', requestedmembername='1-Boston backup A', mediaisknownboolean=True)
230# 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)
231# scriptend(scriptname='Boston Backup', scripterrormessage='client is not visible on network', errorcount=18)
232# 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))
233# retrospectquit()
Note: See TracBrowser for help on using the repository browser.