source: trunk/RetroStatus/RetroStatus.py @ 304

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

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

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