source: trunk/RetroStatus/RetroStatus.py@ 343

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

RetroStatus.py: Remove RSS link. Permit Growl notification to fail.

File size: 9.9 KB
Line 
1#!/usr/bin/pythonw
2
3from appscript import app, k, CommandError
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 try:
60 growl.notify(**params)
61 except CommandError:
62 pass
63
64def addEvent(title, description, preformatted=False):
65 description = description.replace('&', '&amp;').replace('<', '&lt;')
66 if preformatted:
67 description = '<pre>%s</pre>' % description
68 else:
69 description = description.replace('\n\n', '<p>').replace('\n', '<br>')
70 # XXX provide valid URL for event details
71 events.addItem({(RSS.ns.dc, 'date'): datetime.today().isoformat(),
72 (RSS.ns.rss10, 'title'): title,
73 # (RSS.ns.rss10, 'link'): datetime.today().isoformat(),
74 (RSS.ns.rss10, 'description'): description})
75 events.truncateToLength(EVENTS_MAX)
76 file(EVENTS_FILE_TEMP, 'w').write(str(events))
77 os.rename(EVENTS_FILE_TEMP, EVENTS_FILE)
78
79def sendMail(subject, text):
80 message = MIMEText(text.encode('utf8'), _charset='utf8')
81 message.set_unixfrom(FROM_ADDR)
82 message['From'] = formataddr((FROM_NAME, FROM_ADDR))
83 message['To'] = ', '.join(TO_ADDR)
84 message['Date'] = formatdate(localtime=True)
85 message['Subject'] = subject
86 smtp = smtplib.SMTP(SMTP_SERVER)
87 smtp.sendmail(message['From'], TO_ADDR, message.as_string())
88
89### disk management
90
91DEFAULT_LAST_USED_NAME = 'Last used backup volume name'
92DEFAULT_LAST_USED_DATE = 'Date of last backup volume use'
93
94defaults = NSUserDefaults.standardUserDefaults()
95diskManager = DMManager.sharedManager()
96diskManager.generateConnection()
97
98def diskWithName(name):
99 disks = diskManager.disks()
100 for disk in disks:
101 if disk.volumeName() == name:
102 return disk
103
104def lastUsedDisk():
105 diskName = defaults.get(DEFAULT_LAST_USED_NAME)
106 if diskName:
107 return diskWithName(diskName)
108
109def diskIsMounted(disk):
110 mountPoint = disk.mountPoint()
111 return (mountPoint and os.path.exists(mountPoint))
112
113def ejectDisk(disk):
114 if not diskIsMounted(disk):
115 return True
116 if not diskManager.ejectDisk_synchronous_(disk, True):
117 return False
118 if diskIsMounted(disk):
119 growlNotify(NOTIFICATION_MEDIA, 'Eject failed',
120 'Using diskutil to eject %s' % disk)
121 return call('diskutil', 'eject', disk.diskIdentifier()) is 0
122 return True
123
124def mountDisk(disk):
125 if diskIsMounted(disk):
126 return True
127 if not diskManager.mountDisk_includeChildren_synchronous_(disk, False, True):
128 return False
129 if not diskIsMounted(disk):
130 growlNotify(NOTIFICATION_MEDIA, 'Mount failed',
131 'Using diskutil to mount %s' % disk)
132 return call('diskutil', 'mount', disk.diskIdentifier()) is 0
133 return True
134
135### Retrospect event handlers
136
137def retrospectstart(autolaunchboolean): pass
138def scriptstart(scriptname, startdate): pass
139# yes, this really should be "remotetname"
140def volumestart(volumename, clientname='', remotetname=''): pass
141def volumeend(volumename, kbcopied, filecount, durationinseconds, backupdate,
142 enddate, startdate, destinationname, clientname, scriptname,
143 backuptypestring, fileerrorcount, volumeerrorcode,
144 volumeerrormessage, zonename='', remotename='',
145 subvolumediskname=''):
146 growlNotify(NOTIFICATION_BACKUP, scriptname,
147 '%s\n%s' % (volumename, volumeerrormessage))
148 addEvent('%s: %s' % (volumename, volumeerrormessage),
149 '%d file(s) copied; %d had errors.' % (filecount, fileerrorcount))
150
151def mediarequest(mediatypestring, requestedmembername, mediaisknownboolean):
152 disk = diskWithName(requestedmembername)
153 if disk and mountDisk(disk):
154 defaults[DEFAULT_LAST_USED_NAME] = requestedmembername
155 defaults[DEFAULT_LAST_USED_DATE] = NSDate.date()
156 growlNotify(NOTIFICATION_MEDIA, requestedmembername,
157 'Disk is in use: please DO NOT swap')
158 else:
159 sendMail("IMPORTANT: Can't find disk %s" % requestedmembername,
160 "Retrospect can't find the disk '%s'. Please eject the old disk, if any, and check that the correct disk is inserted." % requestedmembername)
161
162def mediarequesttimedout(numberofsecondswaited): pass
163
164def scriptend(scriptname, scripterrormessage, errorcount):
165 disk = lastUsedDisk()
166 if disk and ejectDisk(disk):
167 growlNotify(NOTIFICATION_MEDIA, disk.volumeName(),
168 'Disk no longer in use: OK to swap')
169 addEvent(scriptname + ': script completed',
170 '%s; %s error(s)' % (scripterrormessage, errorcount))
171
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.