source: trunk/RetroStatus/RetroStatus.py@ 507

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

setup.py: Note that the bundle bit needs setting.

RetroStatus.py: Launch/quit Apple Backup, to work around its periodic
failure when left running all the time.

File size: 10.2 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):
138 try:
139 app(id='com.apple.Backup').quit(timeout=5)
140 except CommandError:
141 addEvent('Apple Backup failed to quit', 'Please check on it.')
142
143def scriptstart(scriptname, startdate): pass
144# yes, this really should be "remotetname"
145def volumestart(volumename, clientname='', remotetname=''): pass
146def volumeend(volumename, kbcopied, filecount, durationinseconds, backupdate,
147 enddate, startdate, destinationname, clientname, scriptname,
148 backuptypestring, fileerrorcount, volumeerrorcode,
149 volumeerrormessage, zonename='', remotename='',
150 subvolumediskname=''):
151 growlNotify(NOTIFICATION_BACKUP, scriptname,
152 '%s\n%s' % (volumename, volumeerrormessage))
153 addEvent('%s: %s' % (volumename, volumeerrormessage),
154 '%d file(s) copied; %d had errors.' % (filecount, fileerrorcount))
155
156def mediarequest(mediatypestring, requestedmembername, mediaisknownboolean):
157 disk = diskWithName(requestedmembername)
158 if disk and mountDisk(disk):
159 defaults[DEFAULT_LAST_USED_NAME] = requestedmembername
160 defaults[DEFAULT_LAST_USED_DATE] = NSDate.date()
161 growlNotify(NOTIFICATION_MEDIA, requestedmembername,
162 'Disk is in use: please DO NOT swap')
163 else:
164 sendMail("IMPORTANT: Can't find disk %s" % requestedmembername,
165 "Retrospect can't find the disk '%s'. Please eject the old disk, if any, and check that the correct disk is inserted." % requestedmembername)
166
167def mediarequesttimedout(numberofsecondswaited): pass
168
169def scriptend(scriptname, scripterrormessage, errorcount):
170 disk = lastUsedDisk()
171 if disk and ejectDisk(disk):
172 growlNotify(NOTIFICATION_MEDIA, disk.volumeName(),
173 'Disk no longer in use: OK to swap')
174 addEvent(scriptname + ': script completed',
175 '%s; %s error(s)' % (scripterrormessage, errorcount))
176
177def scriptcheckfailed(scriptname, failuremessage, scheduledate):
178 failuremessage = failuremessage.replace('\r', '\n').decode('macroman')
179 m = re.search(u'\u201c([^\u201d]+)\u201d', failuremessage)
180 if not m:
181 addEvent(scriptname + ': check failed', failuremessage)
182 return
183 if not diskWithName(m.group(1)):
184 sendMail("Please swap backup media before %s" % scheduledate.ctime(),
185 failuremessage + "\n\nEject the old disk, if any, and check that the correct disk is inserted.")
186
187def retrospectquit():
188 # can use launch() in newer appscript versions without CommandError
189 app(id='com.apple.Backup').activate()
190 stopeventloop()
191
192def handleEvent(eventName, params):
193 eventHandler = globals().get(eventName)
194 params = dict(zip(params[::2], params[1::2]))
195 for k in params.keys():
196 if params[k] == '': del params[k]
197 eventStr = '%s(%s)' % (eventName, ', '.join(['%s=%r' % (k, v) for k, v
198 in params.iteritems()]))
199 if not eventHandler:
200 addEvent('%s: unhandled event' % eventName, eventStr)
201 if DEBUG: NSLog('unhandled event: ' + eventStr)
202 return
203 try:
204 eventHandler(**params)
205 except:
206 s = StringIO()
207 print_exc(file=s)
208 addEvent('%s: event handler failed' % eventName,
209 '%s\n\n%s' % (eventStr, s.getvalue()), True)
210 if DEBUG:
211 NSLog('event handler failed for %s:\n%s' % (eventStr, s.getvalue()))
212
213installeventhandler(handleEvent, 'ascrpsbr',
214 ('snam', 'eventName', kAE.typeUnicodeText),
215 ('usrf', 'params', ArgListOf(kAE.typeWildCard)))
216
217if __name__ == '__main__':
218 growl.register(
219 as_application=GROWL_APP_NAME,
220 all_notifications=NOTIFICATIONS_ALL,
221 default_notifications=NOTIFICATIONS_ALL)
222
223 starteventloop()
224
225# example events:
226
227# retrospectstart(autolaunchboolean=False)
228# scriptstart(scriptname='Boston Backup', startdate=datetime.datetime(2005, 11, 23, 2, 31, 51))
229# volumestart(volumename='Backup Clients:Babs')
230
231# 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')
232# volumestart(volumename=' Local Desktop:Bookworm')
233# volumestart(volumename='Backup Clients:Concord:Mac OS X', remotetname='Backup Clients:Concord', clientname='Backup Clients:Concord')
234# mediarequest: appears even if media is available
235# mediarequest(mediatypestring='disk', requestedmembername='1-Boston backup A', mediaisknownboolean=True)
236# mediarequest(mediatypestring='disk', requestedmembername='1-Boston backup A', mediaisknownboolean=True)
237# 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)
238# scriptend(scriptname='Boston Backup', scripterrormessage='client is not visible on network', errorcount=18)
239# 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))
240# retrospectquit()
Note: See TracBrowser for help on using the repository browser.