source: trunk/RetroStatus/RetroStatus.py @ 343

Last change on this file since 343 was 343, checked in by Nicholas Riley, 12 years ago

RetroStatus?.py: Remove RSS link. Permit Growl notification to fail.

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