#!/usr/bin/pythonw from appscript import app, k from aemreceive.sfba import * from Foundation import NSLog, NSUserDefaults, NSDate from subprocess import call from traceback import print_exc from cStringIO import StringIO from datetime import datetime import RSS import email, smtplib from email.MIMEMessage import MIMEMessage from email.MIMEText import MIMEText from email.Utils import formataddr, formatdate from formatflowed import convertToFlowed import os, sys import objc # XXX should switch to public DADiskMount/Eject functions objc.loadBundle('DiskManagement', globals(), '/System/Library/PrivateFrameworks/DiskManagement.framework') EVENTS_DIR = os.path.expanduser('~/Sites/RetroStatus') EVENTS_MAX = 100 FROM_NAME, FROM_ADDR = 'Retrospect backup server', 'retrospect' + '@rileys.us' TO_ADDR = ['%s@rileys.us' for %s in ['julia', 'gary']] SMTP_SERVER = 'calamity.bos.sabi.net' DEBUG = True # XXX implement Web page with current status ### notifications GROWL_APP_NAME = 'RetroStatus' NOTIFICATION_BACKUP = 'Backup' NOTIFICATION_MEDIA = 'Media change' NOTIFICATIONS_ALL = [NOTIFICATION_BACKUP, NOTIFICATION_MEDIA] EVENTS_FILE = os.path.join(EVENTS_DIR, 'index.rdf') EVENTS_FILE_TEMP = EVENTS_FILE + '~' growl = app('GrowlHelperApp') events = RSS.TrackingChannel() try: os.mkdir(EVENTS_DIR) except: pass events.setMD((RSS.ns.rss10, 'channel'), {(RSS.ns.rss10, 'title'): 'Retrospect status', (RSS.ns.rss10, 'link'): os.path.basename(EVENTS_FILE)}) def growlNotify(name, title, description, **kw): # XXX restore coalescing support from LocationDo params = dict(with_name=name, title=title, description=unicode(description), application_name=GROWL_APP_NAME, icon_of_application='Retrospect') params.update(kw) if DEBUG: NSLog('%s: %s' % (title, description)) growl.notify(**params) def addEvent(title, description, preformatted=False): description = description.replace('&', '&').replace('<', '<') if preformatted: description = '
%s
' % description else: description = description.replace('\n\n', '

').replace('\n', '
') # XXX provide valid URL for event details events.addItem({(RSS.ns.dc, 'date'): datetime.today().isoformat(), (RSS.ns.rss10, 'title'): title, (RSS.ns.rss10, 'link'): datetime.today().isoformat(), (RSS.ns.rss10, 'description'): description}) events.truncateToLength(EVENTS_MAX) file(EVENTS_FILE_TEMP, 'w').write(str(events)) os.rename(EVENTS_FILE_TEMP, EVENTS_FILE) def sendMail(subject, text): message = MIMEText(convertToFlowed(text), 'plain; format=flowed') message.set_unixfrom(FROM_ADDR) message['From'] = formataddr((FROM_NAME, FROM_ADDR)) message['To'] = TO_ADDR message['Date'] = formatdate(localtime=True) message['Subject'] = subject smtp = smtplib.SMTP(SMTP_SERVER) smtp.sendmail(message['From'], TO_ADDR, message.as_string()) ### disk management DEFAULT_LAST_USED_NAME = 'Last used backup volume name' DEFAULT_LAST_USED_DATE = 'Date of last backup volume use' defaults = NSUserDefaults.standardUserDefaults() diskManager = DMManager.sharedManager() diskManager.generateConnection() def diskWithName(name): disks = diskManager.disks() for disk in disks: if disk.volumeName() == name: return disk def lastUsedDisk(): diskName = defaults.get(DEFAULT_LAST_USED_NAME) if diskName: return diskWithName(diskName) def diskIsMounted(disk): mountPoint = disk.mountPoint() return (mountPoint and os.path.exists(mountPoint)) def ejectDisk(disk): if not diskIsMounted(disk): return True if not diskManager.ejectDisk_synchronous_(disk, True): return False if diskIsMounted(disk): growlNotify(NOTIFICATION_MEDIA, 'Eject failed', 'Using diskutil to eject %s' % disk) return call('diskutil', 'eject', disk.diskIdentifier()) is 0 return True def mountDisk(disk): if diskIsMounted(disk): return True if not diskManager.mountDisk_includeChildren_synchronous_(disk, False, True): return False if not diskIsMounted(disk): growlNotify(NOTIFICATION_MEDIA, 'Mount failed', 'Using diskutil to mount %s' % disk) return call('diskutil', 'mount', disk.diskIdentifier()) is 0 return True ### Retrospect event handlers def retrospectstart(autolaunchboolean): pass def scriptstart(scriptname, startdate): pass def volumestart(volumename): pass def volumeend(volumename, kbcopied, filecount, durationinseconds, backupdate, enddate, startdate, destinationname, clientname, scriptname, backuptypestring, fileerrorcount, volumeerrorcode, volumeerrormessage, zonename='', remotename='', subvolumediskname=''): growlNotify(NOTIFICATION_BACKUP, scriptname, '%s\n%s' % (volumename, volumeerrormessage)) addEvent(volumename, '%s\n\n%d file(s) copied; %d had errors.' % (volumeerrormessage, filecount, fileerrorcount)) def mediarequest(mediatypestring, requestedmembername, mediaisknownboolean): disk = diskWithName(requestedmembername) if disk and mountDisk(disk): defaults[DEFAULT_LAST_USED_NAME] = requestedmembername defaults[DEFAULT_LAST_USED_DATE] = NSDate.date() growlNotify(NOTIFICATION_MEDIA, requestedmembername, 'Disk is in use: please DO NOT swap') else: sendMail("IMPORTANT: Can't find disk %s" % requestedmembername, "Retrospect can't find the disk '%s'. Please eject the old disk, if any, and check that the correct disk is inserted.") def mediarequesttimedout(numberofsecondswaited): pass def scriptend(scriptname, scripterrormessage, errorcount): disk = lastUsedDisk() if disk and ejectDisk(disk): growlNotify(NOTIFICATION_MEDIA, disk.volumeName(), 'Disk no longer in use: OK to swap') addEvent('Completed: scriptname', '%s; %s error(s)' % (scripterrormessage, errorcount)) def scriptcheckfailed(scriptname, failuremessage, scheduledate): disk = lastUsedDisk() if not disk or not diskIsMounted(disk): sendMail("Please swap backup media before %s" % scheduledate.ctime(), "%s\nEject the old disk, if any, and check that the correct disk is inserted." % failuremessage.replace('\r', '\n')) def retrospectquit(): disk = lastUsedDisk() if not disk or not diskIsMounted(disk): sendMail("Please swap backup media", "Eject the old disk, if any, and check that the correct disk is inserted.") stopeventloop() sys.exit(0) def handleEvent(eventName, params): eventHandler = globals().get(eventName) params = dict(zip(params[::2], params[1::2])) for k in params.keys(): if params[k] == '': del params[k] eventStr = '%s(%s)' % (eventName, ', '.join(['%s=%r' % (k, v) for k, v in params.iteritems()])) if not eventHandler: addEvent('Unhandled Retrospect event', eventStr) if DEBUG: NSLog('unhandled event: ' + eventStr) return try: eventHandler(**params) except: s = StringIO() print_exc(file=s) addEvent('Retrospect event handler failed', '%s\n\n%s' % (eventStr, s.getval()), True) if DEBUG: NSLog('event handler failed for %s:\n%s' % (eventStr, s.getval()) installeventhandler(handleEvent, 'ascrpsbr', ('snam', 'eventName', kAE.typeUnicodeText), ('usrf', 'params', ArgListOf(kAE.typeWildCard))) if __name__ == '__main__': growl.register( as_application=GROWL_APP_NAME, all_notifications=NOTIFICATIONS_ALL, default_notifications=NOTIFICATIONS_ALL) starteventloop() # example events: # retrospectstart(autolaunchboolean=False) # scriptstart(scriptname='Boston Backup', startdate=datetime.datetime(2005, 11, 23, 2, 31, 51)) # volumestart(volumename='Backup Clients:Babs') # 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') # volumestart(volumename=' Local Desktop:Bookworm') # mediarequest: appears even if media is available # mediarequest(mediatypestring='disk', requestedmembername='1-Boston backup A', mediaisknownboolean=True) # mediarequest(mediatypestring='disk', requestedmembername='1-Boston backup A', mediaisknownboolean=True) # 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) # scriptend(scriptname='Boston Backup', scripterrormessage='client is not visible on network', errorcount=18) # 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)) # retrospectquit()