source: trunk/RetroStatus/RetroStatus.py@ 339

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

RetroStatus.py: Fixed missing replacement; removed inapplicable comment.

File size: 9.8 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." % requestedmembername)
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
169def scriptcheckfailed(scriptname, failuremessage, scheduledate):
170 failuremessage = failuremessage.replace('\r', '\n').decode('macroman')
171 m = re.search(u'\u201c([^\u201d]+)\u201d', failuremessage)
172 if not m:
173 addEvent(scriptname + ': check failed', failuremessage)
174 return
175 if not diskWithName(m.group(1)):
176 sendMail("Please swap backup media before %s" % scheduledate.ctime(),
177 failuremessage + "\n\nEject the old disk, if any, and check that the correct disk is inserted.")
178
179def retrospectquit():
180 stopeventloop()
181
182def handleEvent(eventName, params):
183 eventHandler = globals().get(eventName)
184 params = dict(zip(params[::2], params[1::2]))
185 for k in params.keys():
186 if params[k] == '': del params[k]
187 eventStr = '%s(%s)' % (eventName, ', '.join(['%s=%r' % (k, v) for k, v
188 in params.iteritems()]))
189 if not eventHandler:
190 addEvent('%s: unhandled event' % eventName, eventStr)
191 if DEBUG: NSLog('unhandled event: ' + eventStr)
192 return
193 try:
194 eventHandler(**params)
195 except:
196 s = StringIO()
197 print_exc(file=s)
198 addEvent('%s: event handler failed' % eventName,
199 '%s\n\n%s' % (eventStr, s.getvalue()), True)
200 if DEBUG:
201 NSLog('event handler failed for %s:\n%s' % (eventStr, s.getvalue()))
202
203installeventhandler(handleEvent, 'ascrpsbr',
204 ('snam', 'eventName', kAE.typeUnicodeText),
205 ('usrf', 'params', ArgListOf(kAE.typeWildCard)))
206
207if __name__ == '__main__':
208 growl.register(
209 as_application=GROWL_APP_NAME,
210 all_notifications=NOTIFICATIONS_ALL,
211 default_notifications=NOTIFICATIONS_ALL)
212
213 starteventloop()
214
215# example events:
216
217# retrospectstart(autolaunchboolean=False)
218# scriptstart(scriptname='Boston Backup', startdate=datetime.datetime(2005, 11, 23, 2, 31, 51))
219# volumestart(volumename='Backup Clients:Babs')
220
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# volumestart(volumename='Backup Clients:Concord:Mac OS X', remotetname='Backup Clients:Concord', clientname='Backup Clients:Concord')
224# mediarequest: appears even if media is available
225# mediarequest(mediatypestring='disk', requestedmembername='1-Boston backup A', mediaisknownboolean=True)
226# mediarequest(mediatypestring='disk', requestedmembername='1-Boston backup A', mediaisknownboolean=True)
227# 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)
228# scriptend(scriptname='Boston Backup', scripterrormessage='client is not visible on network', errorcount=18)
229# 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))
230# retrospectquit()
Note: See TracBrowser for help on using the repository browser.