source: trunk/RetroStatus/RetroStatus.py@ 343

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

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

File size: 9.9 KB
RevLine 
[201]1#!/usr/bin/pythonw
2
[343]3from appscript import app, k, CommandError
[201]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
[204]15import os, re, sys
[201]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'
[202]24TO_ADDR = ['%s@rileys.us' % user for user in ['julia', 'gary']]
[201]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))
[343]59 try:
60 growl.notify(**params)
61 except CommandError:
62 pass
[201]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,
[343]73 # (RSS.ns.rss10, 'link'): datetime.today().isoformat(),
[201]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):
[204]80 message = MIMEText(text.encode('utf8'), _charset='utf8')
[201]81 message.set_unixfrom(FROM_ADDR)
82 message['From'] = formataddr((FROM_NAME, FROM_ADDR))
[204]83 message['To'] = ', '.join(TO_ADDR)
[201]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
[204]139# yes, this really should be "remotetname"
140def volumestart(volumename, clientname='', remotetname=''): pass
[201]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))
[204]148 addEvent('%s: %s' % (volumename, volumeerrormessage),
149 '%d file(s) copied; %d had errors.' % (filecount, fileerrorcount))
[201]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,
[209]160 "Retrospect can't find the disk '%s'. Please eject the old disk, if any, and check that the correct disk is inserted." % requestedmembername)
[201]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')
[204]169 addEvent(scriptname + ': script completed',
[201]170 '%s; %s error(s)' % (scripterrormessage, errorcount))
171
172def scriptcheckfailed(scriptname, failuremessage, scheduledate):
[204]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)):
[201]179 sendMail("Please swap backup media before %s" % scheduledate.ctime(),
[204]180 failuremessage + "\n\nEject the old disk, if any, and check that the correct disk is inserted.")
[201]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:
[204]193 addEvent('%s: unhandled event' % eventName, eventStr)
[201]194 if DEBUG: NSLog('unhandled event: ' + eventStr)
195 return
196 try:
197 eventHandler(**params)
198 except:
199 s = StringIO()
200 print_exc(file=s)
[204]201 addEvent('%s: event handler failed' % eventName,
202 '%s\n\n%s' % (eventStr, s.getvalue()), True)
[201]203 if DEBUG:
[204]204 NSLog('event handler failed for %s:\n%s' % (eventStr, s.getvalue()))
[201]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')
[204]223
[201]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')
[204]226# volumestart(volumename='Backup Clients:Concord:Mac OS X', remotetname='Backup Clients:Concord', clientname='Backup Clients:Concord')
[201]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.