source: trunk/RetroStatus/RetroStatus.py@ 247

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

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

File size: 9.8 KB
RevLine 
[201]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
[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))
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):
[204]77 message = MIMEText(text.encode('utf8'), _charset='utf8')
[201]78 message.set_unixfrom(FROM_ADDR)
79 message['From'] = formataddr((FROM_NAME, FROM_ADDR))
[204]80 message['To'] = ', '.join(TO_ADDR)
[201]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
[204]136# yes, this really should be "remotetname"
137def volumestart(volumename, clientname='', remotetname=''): pass
[201]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))
[204]145 addEvent('%s: %s' % (volumename, volumeerrormessage),
146 '%d file(s) copied; %d had errors.' % (filecount, fileerrorcount))
[201]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,
[209]157 "Retrospect can't find the disk '%s'. Please eject the old disk, if any, and check that the correct disk is inserted." % requestedmembername)
[201]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')
[204]166 addEvent(scriptname + ': script completed',
[201]167 '%s; %s error(s)' % (scripterrormessage, errorcount))
168
169def scriptcheckfailed(scriptname, failuremessage, scheduledate):
[204]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)):
[201]176 sendMail("Please swap backup media before %s" % scheduledate.ctime(),
[204]177 failuremessage + "\n\nEject the old disk, if any, and check that the correct disk is inserted.")
[201]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:
[204]190 addEvent('%s: unhandled event' % eventName, eventStr)
[201]191 if DEBUG: NSLog('unhandled event: ' + eventStr)
192 return
193 try:
194 eventHandler(**params)
195 except:
196 s = StringIO()
197 print_exc(file=s)
[204]198 addEvent('%s: event handler failed' % eventName,
199 '%s\n\n%s' % (eventStr, s.getvalue()), True)
[201]200 if DEBUG:
[204]201 NSLog('event handler failed for %s:\n%s' % (eventStr, s.getvalue()))
[201]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')
[204]220
[201]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')
[204]223# volumestart(volumename='Backup Clients:Concord:Mac OS X', remotetname='Backup Clients:Concord', clientname='Backup Clients:Concord')
[201]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.