source: trunk/RetroStatus/RetroStatus.py @ 202

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

RetroStatus?.py: Fixed TO_ADDR typos. Removed extraneous emails from quit.

File size: 9.5 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' % user for user 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
169# XXX does this get sent when it's supposed to?
170def scriptcheckfailed(scriptname, failuremessage, scheduledate):
171    disk = lastUsedDisk()
172    if not disk or not diskIsMounted(disk):
173        sendMail("Please swap backup media before %s" % scheduledate.ctime(),
174                 "%s\nEject the old disk, if any, and check that the correct disk is inserted." % failuremessage.replace('\r', '\n'))
175
176def retrospectquit():
177    stopeventloop()
178
179def handleEvent(eventName, params):
180    eventHandler = globals().get(eventName)
181    params = dict(zip(params[::2], params[1::2]))
182    for k in params.keys():
183        if params[k] == '': del params[k]
184    eventStr = '%s(%s)' % (eventName, ', '.join(['%s=%r' % (k, v) for k, v
185                                                 in params.iteritems()]))
186    if not eventHandler:
187        addEvent('Unhandled Retrospect event', eventStr)
188        if DEBUG: NSLog('unhandled event: ' + eventStr)
189        return
190    try:
191        eventHandler(**params)
192    except:
193        s = StringIO()
194        print_exc(file=s)
195        addEvent('Retrospect event handler failed',
196                 '%s\n\n%s' % (eventStr, s.getval()), True)
197        if DEBUG:
198            NSLog('event handler failed for %s:\n%s' % (eventStr, s.getval())
199
200installeventhandler(handleEvent, 'ascrpsbr',
201                    ('snam', 'eventName', kAE.typeUnicodeText),
202                    ('usrf', 'params', ArgListOf(kAE.typeWildCard)))
203
204if __name__ == '__main__':
205    growl.register(
206        as_application=GROWL_APP_NAME,
207        all_notifications=NOTIFICATIONS_ALL,
208        default_notifications=NOTIFICATIONS_ALL)
209
210    starteventloop()
211
212# example events:
213
214# retrospectstart(autolaunchboolean=False)
215# scriptstart(scriptname='Boston Backup', startdate=datetime.datetime(2005, 11, 23, 2, 31, 51))
216# volumestart(volumename='Backup Clients:Babs')
217# 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')
218# volumestart(volumename=' Local Desktop:Bookworm')
219# mediarequest: appears even if media is available
220# mediarequest(mediatypestring='disk', requestedmembername='1-Boston backup A', mediaisknownboolean=True)
221# mediarequest(mediatypestring='disk', requestedmembername='1-Boston backup A', mediaisknownboolean=True)
222# 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)
223# scriptend(scriptname='Boston Backup', scripterrormessage='client is not visible on network', errorcount=18)
224# 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))
225# retrospectquit()
Note: See TracBrowser for help on using the repository browser.