source: trunk/RetroStatus/RetroStatus.py@ 353

Last change on this file since 353 was 344, checked in by Nicholas Riley, 17 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
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
[344]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
[201]143def scriptstart(scriptname, startdate): pass
[204]144# yes, this really should be "remotetname"
145def volumestart(volumename, clientname='', remotetname=''): pass
[201]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))
[204]153 addEvent('%s: %s' % (volumename, volumeerrormessage),
154 '%d file(s) copied; %d had errors.' % (filecount, fileerrorcount))
[201]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,
[209]165 "Retrospect can't find the disk '%s'. Please eject the old disk, if any, and check that the correct disk is inserted." % requestedmembername)
[201]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')
[204]174 addEvent(scriptname + ': script completed',
[201]175 '%s; %s error(s)' % (scripterrormessage, errorcount))
176
177def scriptcheckfailed(scriptname, failuremessage, scheduledate):
[204]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)):
[201]184 sendMail("Please swap backup media before %s" % scheduledate.ctime(),
[204]185 failuremessage + "\n\nEject the old disk, if any, and check that the correct disk is inserted.")
[201]186
187def retrospectquit():
[344]188 # can use launch() in newer appscript versions without CommandError
189 app(id='com.apple.Backup').activate()
[201]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:
[204]200 addEvent('%s: unhandled event' % eventName, eventStr)
[201]201 if DEBUG: NSLog('unhandled event: ' + eventStr)
202 return
203 try:
204 eventHandler(**params)
205 except:
206 s = StringIO()
207 print_exc(file=s)
[204]208 addEvent('%s: event handler failed' % eventName,
209 '%s\n\n%s' % (eventStr, s.getvalue()), True)
[201]210 if DEBUG:
[204]211 NSLog('event handler failed for %s:\n%s' % (eventStr, s.getvalue()))
[201]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')
[204]230
[201]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')
[204]233# volumestart(volumename='Backup Clients:Concord:Mac OS X', remotetname='Backup Clients:Concord', clientname='Backup Clients:Concord')
[201]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.