#! /usr/bin/python2.4 # logging requires python >= 2.3 # exception traceback needs >= 2.4 # open or close a snapshot for backup # 1st argument is open|close # 2nd argument is a path to backup # starts or stops a snapshot under /evms/snapshot/path # works even if path has components # path must be a mount point # Requirements for all operations # preexisting region to serve as backing store for snapshot # For open: # Neither snapshot nor its volume are in use # For close: # snapshot and volume in use, as established by open # (c) 2005 Ross Boylan RossBoylan@stanfordalumni.org # Released under the GNU Public License, v2 or later. # Use at your own risk. # v 0.5 18 May 2005 # v 0.6 19 May 2005 first apparently working version # See evms-devel email thread at: # http://marc.theaimsgroup.com/?l=evms-devel&m=111656973718080&w=2 import logging, os, os.path, pexpect, re, signal, sys, traceback, types snapName = "BackSnap" # name of snapshot object snapRegion = "lvm/LITTLEBOY/Backup Snapshot Region" # region on which # to create it snapVolume = "Backup Snapshot" # name of volume on which to mount it def devForPath(thePath): """Return a string containing the device name on which thePath is mounted. Return None if thePath is not the exact path of a mounted device.""" if not os.path.isdir(thePath): return None fd = os.popen("mount") if not fd: return None pattern = re.compile(r"^(/\S+)\son\s%s\stype"%thePath) while True: line = fd.readline() if not line: return None # reached EOF m = pattern.match(line) if m: return m.group(1) class Manipulator: "Manipulate EVMS client" def __init__(self, theLog, theSnapName, theSnapRegion, theSnapVolume): # theLog is of type logging.Logger self.log = theLog self.snapName = theSnapName self.snapRegion = theSnapRegion self.snapVolume = theSnapVolume self.timeError = [ pexpect.TIMEOUT ] self.anyError = [ pexpect.TIMEOUT, pexpect.EOF ] self.prompt = 'EVMS: ' self.snapFSRoot = '/evms/snapshot' # root on file system def snapPath(self, thePath): "Return path on which the snapshot for thePath will be mounted" # thePath assumed absolute return self.snapFSRoot+thePath def deviceName(self): "Device name as which the snapshot appears" return '/dev/evms/'+self.snapVolume def evmsPrompt(self): "Return true if we see the prompt" i = self.evms.expect_exact([self.prompt, pexpect.TIMEOUT, pexpect.EOF]) return not i def evmsExec(self, cmd, good, bad, badSeverity, badMessage): "have the evms CLI execute the indicated cmd." # good = list of good conditons, bad = list of bads # exit process on failure, with logging # return match object on success self.evms.sendline(cmd) self.log.debug(cmd) if type(good) is types.StringType: good = [good] if type(bad) is types.StringType: bad = [bad] goodnbad = good+bad i = self.evms.expect_exact(goodnbad) if i >= len(good): "Failure" self.log.log(badSeverity, badMessage) self.exit(badSeverity) m = self.evms.match if self.evms.after.endswith(self.prompt): return m if not self.evmsPrompt(): self.log.log(badSeverity, 'Could not get EVMS prompt after command %s'%cmd) self.exit(badSeverity) return m def exit(self, code): "Fail gracefully" try: "The next commands may be sent too rapidly" if exit: # assume this already sent for normal (0) exit self.evms.sendline('Quit') else: self.log.info('Successful completion') self.evms.sendeof() self.evms.kill(signal.SIGKILL) finally: return sys.exit(code) def launchEVMS(self, thePath): "start the EVMS client. Return the device name associated with thePath" # or fail theDev = devForPath(thePath) if not theDev: self.log.critical("Couldn't find device for path %s"%thePath) sys.exit(logging.CRITICAL) self.evms = pexpect.spawn("evms", timeout=45) # it can take awhile to start self.evms.setlog(sys.stdout) if not self.evmsPrompt(): self.log.critical("Unable to launch evms command line interface") sys.exit(logging.CRITICAL) self.evms.timeout = 15.0 return theDev def unexpectedError(self): "Deal with an unforseen error" self.log.critical('An unexpected error has occurred.') self.log.critical(traceback.format_exc()) # format_exc is a 2.4 feature self.exit(logging.CRITICAL) def quit(self, failureMessage): "close the EVMS client" self.evms.sendline('Quit') self.evms.timeout = 30.0 i = self.evms.expect_exact([pexpect.EOF, pexpect.TIMEOUT]) if i: self.log.warning(failureMessage) self.exit(logging.WARNING) self.exit(0) class OpenSnap (Manipulator): "Set up for backup by manipulating evms" def __init__(self, theLog, theSnapName, theSnapRegion, theSnapVolume): # theLog is of type logging.Logger Manipulator.__init__(self, theLog, theSnapName, theSnapRegion, theSnapVolume) def checkObjectsReady(self): "Return true if EVMS objects are available for use" # otherwise fail return self.checkRegion() and self.checkSnap() def checkRegion(self): "Check that the region is available to back the snapshot" self.evms.sendline('Query: Regions,Region="%s"'%self.snapRegion) fail = False try: self.evms.expect_exact('Region Name: %s'%self.snapRegion) self.evms.expect_exact('Region Type: Data') except pexpect.TIMEOUT: fail = True except pexpect.EOF: fail = True if fail: self.log.critical('Required Region %s seems not to exist'%self.snapRegion) if not self.evmsPrompt(): self.log.error('EVMS has vanished. No action taken.') fail = True if fail: self.exit(logging.CRITICAL) return True def checkSnap(self): "check snapshot object not in use" self.evms.sendline('Query: Objects, Object = "%s", Plugin = Snapshot'%self.snapName) i = self.evms.expect_exact(['The specified item could not be found.', self.prompt, pexpect.TIMEOUT, pexpect.EOF]) if i == 0: # good: we don't want the object to exist if self.evmsPrompt(): return True # if no prompt, fall through if i == 1: self.log.critical('Snapshot object "%s" is already in use. Free it first.'% self.snapName) self.exit(logging.CRITICAL) # so we have TIMEOUT or EOF or lack of prompt self.log.error('EVMS seems to have quit in early processing. Nothing done.') self.exit(logging.ERROR) def assureTargetPath(self, theSourcePath): "Assure that target path for backup exists or fail" path = self.snapPath(theSourcePath) if os.path.exists(path): if os.path.isdir(path): return True self.log.error('Mount point "%s" for snapshot exists but is not a dir--Aborting'% path) self.exit(logging.ERROR) try: os.makedirs(path) except: self.log.error('Attempt to create snapshot mount point "%s" failed--Aborting'% path) self.exit(logging.ERROR) return True def go(self, thePath): "Setup snapshot of thePath" theDev = self.launchEVMS(thePath) try: self.checkObjectsReady() self.assureTargetPath(thePath) cmd ='Create: Object,Snapshot={original="%s",snapshot="%s"},"%s"'%( theDev, self.snapName, self.snapRegion) self.evmsExec(cmd, ['The create command created object: %s'%self.snapName], self.anyError, logging.CRITICAL, '%s failed to create snapshot object'%cmd) cmd = 'Create: Volume,"%s", Name="%s"'%(self.snapName, self.snapVolume) self.evmsExec(cmd, self.prompt, # yes, command has no output ['Error', 'could not', pexpect.EOF, pexpect.TIMEOUT], logging.CRITICAL, 'Snapshot "%s" created, but %s failed'%(self.snapName, cmd)) cmd = 'Mount: "%s", "%s"'%(self.deviceName(), self.snapPath(thePath)) self.evmsExec(cmd, self.prompt, ['Error', 'could not', pexpect.EOF, pexpect.TIMEOUT ], logging.ERROR, 'Snapshot and volume created and active, but mount fails') self.quit('EVMS created and mounted snapshot, but exit was dirty.') except SystemExit: # avoid an embarrassing loop where we catch our attempts to exit raise except: self.unexpectedError() class CloseSnap (Manipulator): "Close snapshot and perform other end of backup actions" def __init__(self, theLog, theSnapName, theSnapRegion, theSnapVolume): # theLog is of type logging.Logger Manipulator.__init__(self, theLog, theSnapName, theSnapRegion, theSnapVolume) def killVolume(self): "Unmount and then delete the volume the snapshot is mounted on" # if volume not found for unmounting, we proceed self.evmsExec('Unmount: "%s"'%self.deviceName(), [self.prompt, 'Error', 'could not be'], self.anyError, logging.ERROR, 'Could not unmount %s--Aborting'%self.deviceName()) # special handling cmd = 'Delete: "%s"'%self.deviceName() self.evms.sendline(cmd) self.log.debug(cmd) i = self.evms.expect_exact(['*1 = Continue']+self.anyError) if i == 0: self.evms.expect_exact( ['Please enter the number corresponding to your choice: '] + self.anyError) if i == 0: self.evmsExec('1', self.prompt, self.anyError, logging.ERROR, "I unmounted '%s' but I don't think I deleted it'--Aborting"% self.deviceName()) return #if we get here, something's wrong self.log.error( 'Unmounted "%s" but ran into trouble while deleting it--Aborting'% self.deviceName()) self.exit(logging.ERROR) def killSnapshot(self): "kill snapshot object itself" # Prerequisite: volume is deleted self.evmsExec('Delete: "%s"'%self.snapName, self.prompt, ['Error', 'could not be'] + self.anyError, logging.ERROR, 'Volume "%s" killed, but could not delete snapshot object "%s"'%( self.deviceName(), self.snapName)) def go(self, thePath): "Shutdown the snapshot, deleting it but not underlying store" theDev = self.launchEVMS(thePath) try: # I could be more careful that all is in order first self.killVolume() self.killSnapshot() self.quit('Snapshot and associated volume deleted, but dirty exit') except SystemExit: # avoid an embarrassing loop where we catch our attempts to exit raise except: self.unexpectedError() if __name__ == '__main__': if len(sys.argv) != 3 or (sys.argv[1] != 'open' and sys.argv[1] != 'close'): os.stderr.writeLine("Useage: snapshot.py {open|close} /path/to/snapshot") os.exit(logging.CRITICAL) logging.basicConfig() log = logging.getLogger() log.setLevel(logging.DEBUG) if sys.argv[1] == 'open': manip = OpenSnap(log, snapName, snapRegion, snapVolume) else: manip = CloseSnap(log, snapName, snapRegion, snapVolume) thePath = sys.argv[2] manip.go(thePath)