source: trunk/RetroStatus/RetroStatus.py @ 201

Last change on this file since 201 was 201, checked in by Nicholas Riley, 14 years ago

RetroStatus?

File size: 9.7 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
15from formatflowed import convertToFlowed
16import os, sys
17import objc # XXX should switch to public DADiskMount/Eject functions
18objc.loadBundle('DiskManagement', globals(),
19                '/System/Library/PrivateFrameworks/DiskManagement.framework')
20
21EVENTS_DIR = os.path.expanduser('~/Sites/RetroStatus')
22EVENTS_MAX = 100
23
24FROM_NAME, FROM_ADDR = 'Retrospect backup server', 'retrospect' + '@rileys.us'
25TO_ADDR = ['%s@rileys.us' for %s in ['julia', 'gary']]
26SMTP_SERVER = 'calamity.bos.sabi.net'
27
28DEBUG = True
29
30# XXX implement Web page with current status
31
32### notifications
33
34GROWL_APP_NAME = 'RetroStatus'
35NOTIFICATION_BACKUP = 'Backup'
36NOTIFICATION_MEDIA = 'Media change'
37NOTIFICATIONS_ALL = [NOTIFICATION_BACKUP, NOTIFICATION_MEDIA]
38EVENTS_FILE = os.path.join(EVENTS_DIR, 'index.rdf')
39EVENTS_FILE_TEMP = EVENTS_FILE + '~'
40
41growl = app('GrowlHelperApp')
42events = RSS.TrackingChannel()
43
44try: os.mkdir(EVENTS_DIR)
45except: pass
46events.setMD((RSS.ns.rss10, 'channel'),
47             {(RSS.ns.rss10, 'title'): 'Retrospect status',
48              (RSS.ns.rss10, 'link'): os.path.basename(EVENTS_FILE)})
49
50def growlNotify(name, title, description, **kw):
51    # XXX restore coalescing support from LocationDo
52    params = dict(with_name=name,
53                  title=title,
54                  description=unicode(description),
55                  application_name=GROWL_APP_NAME,
56                  icon_of_application='Retrospect')
57    params.update(kw)
58    if DEBUG:
59        NSLog('%s: %s' % (title, description))
60    growl.notify(**params)
61
62def addEvent(title, description, preformatted=False):
63    description = description.replace('&', '&amp;').replace('<', '&lt;')
64    if preformatted:
65        description = '<pre>%s</pre>' % description
66    else:
67        description = description.replace('\n\n', '<p>').replace('\n', '<br>')
68    # XXX provide valid URL for event details
69    events.addItem({(RSS.ns.dc, 'date'): datetime.today().isoformat(),
70                    (RSS.ns.rss10, 'title'): title,
71                    (RSS.ns.rss10, 'link'): datetime.today().isoformat(),
72                    (RSS.ns.rss10, 'description'): description})
73    events.truncateToLength(EVENTS_MAX)
74    file(EVENTS_FILE_TEMP, 'w').write(str(events))
75    os.rename(EVENTS_FILE_TEMP, EVENTS_FILE)
76
77def sendMail(subject, text):
78    message = MIMEText(convertToFlowed(text), 'plain; format=flowed')
79    message.set_unixfrom(FROM_ADDR)
80    message['From'] = formataddr((FROM_NAME, FROM_ADDR))
81    message['To'] = TO_ADDR
82    message['Date'] = formatdate(localtime=True)
83    message['Subject'] = subject
84    smtp = smtplib.SMTP(SMTP_SERVER)
85    smtp.sendmail(message['From'], TO_ADDR, message.as_string())
86
87### disk management
88
89DEFAULT_LAST_USED_NAME = 'Last used backup volume name'
90DEFAULT_LAST_USED_DATE = 'Date of last backup volume use'
91
92defaults = NSUserDefaults.standardUserDefaults()
93diskManager = DMManager.sharedManager()
94diskManager.generateConnection()
95
96def diskWithName(name):
97    disks = diskManager.disks()
98    for disk in disks:
99        if disk.volumeName() == name:
100            return disk
101
102def lastUsedDisk():
103    diskName = defaults.get(DEFAULT_LAST_USED_NAME)
104    if diskName:
105        return diskWithName(diskName)
106
107def diskIsMounted(disk):
108    mountPoint = disk.mountPoint()
109    return (mountPoint and os.path.exists(mountPoint))
110
111def ejectDisk(disk):
112    if not diskIsMounted(disk):
113        return True
114    if not diskManager.ejectDisk_synchronous_(disk, True):
115        return False
116    if diskIsMounted(disk):
117        growlNotify(NOTIFICATION_MEDIA, 'Eject failed',
118                    'Using diskutil to eject %s' % disk)
119        return call('diskutil', 'eject', disk.diskIdentifier()) is 0
120    return True
121
122def mountDisk(disk):
123    if diskIsMounted(disk):
124        return True
125    if not diskManager.mountDisk_includeChildren_synchronous_(disk, False, True):
126        return False
127    if not diskIsMounted(disk):
128        growlNotify(NOTIFICATION_MEDIA, 'Mount failed',
129                    'Using diskutil to mount %s' % disk)
130        return call('diskutil', 'mount', disk.diskIdentifier()) is 0
131    return True
132
133### Retrospect event handlers
134
135def retrospectstart(autolaunchboolean): pass
136def scriptstart(scriptname, startdate): pass
137def volumestart(volumename): 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(volumename, '%s\n\n%d file(s) copied; %d had errors.' %
146             (volumeerrormessage, 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.")
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('Completed: scriptname',
167             '%s; %s error(s)' % (scripterrormessage, errorcount))
168
169def scriptcheckfailed(scriptname, failuremessage, scheduledate):
170    disk = lastUsedDisk()
171    if not disk or not diskIsMounted(disk):
172        sendMail("Please swap backup media before %s" % scheduledate.ctime(),
173                 "%s\nEject the old disk, if any, and check that the correct disk is inserted." % failuremessage.replace('\r', '\n'))
174
175def retrospectquit():
176    disk = lastUsedDisk()
177    if not disk or not diskIsMounted(disk):
178        sendMail("Please swap backup media",
179                 "Eject the old disk, if any, and check that the correct disk is inserted.")
180    stopeventloop()
181    sys.exit(0)
182
183def handleEvent(eventName, params):
184    eventHandler = globals().get(eventName)
185    params = dict(zip(params[::2], params[1::2]))
186    for k in params.keys():
187        if params[k] == '': del params[k]
188    eventStr = '%s(%s)' % (eventName, ', '.join(['%s=%r' % (k, v) for k, v
189                                                 in params.iteritems()]))
190    if not eventHandler:
191        addEvent('Unhandled Retrospect event', eventStr)
192        if DEBUG: NSLog('unhandled event: ' + eventStr)
193        return
194    try:
195        eventHandler(**params)
196    except:
197        s = StringIO()
198        print_exc(file=s)
199        addEvent('Retrospect event handler failed',
200                 '%s\n\n%s' % (eventStr, s.getval()), True)
201        if DEBUG:
202            NSLog('event handler failed for %s:\n%s' % (eventStr, s.getval())
203
204installeventhandler(handleEvent, 'ascrpsbr',
205                    ('snam', 'eventName', kAE.typeUnicodeText),
206                    ('usrf', 'params', ArgListOf(kAE.typeWildCard)))
207
208if __name__ == '__main__':
209    growl.register(
210        as_application=GROWL_APP_NAME,
211        all_notifications=NOTIFICATIONS_ALL,
212        default_notifications=NOTIFICATIONS_ALL)
213
214    starteventloop()
215
216# example events:
217
218# retrospectstart(autolaunchboolean=False)
219# scriptstart(scriptname='Boston Backup', startdate=datetime.datetime(2005, 11, 23, 2, 31, 51))
220# volumestart(volumename='Backup Clients:Babs')
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# mediarequest: appears even if media is available
224# mediarequest(mediatypestring='disk', requestedmembername='1-Boston backup A', mediaisknownboolean=True)
225# mediarequest(mediatypestring='disk', requestedmembername='1-Boston backup A', mediaisknownboolean=True)
226# 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)
227# scriptend(scriptname='Boston Backup', scripterrormessage='client is not visible on network', errorcount=18)
228# 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))
229# retrospectquit()
Note: See TracBrowser for help on using the repository browser.