source: trunk/RetroStatus/RetroStatus.py @ 507

Last change on this file since 507 was 344, checked in by Nicholas Riley, 12 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.