#!/usr/bin/python """Clean-up after people's deleted VMs. The Invirt janitor goes through and finds virtual disk images that users have requested we delete. For their privacy, it writes over the entire disk with /dev/zero, then removes the logical volume, restoring the space to the pool. A request is indicated to the janitor by creating a file in /var/lib/invirt-remote/cleanup/ corresponding to the name of the LV to delete. The janitor notices these requests using inotify. """ import os import subprocess import syslog import traceback import pyinotify _JANITOR_DIR = '/var/lib/invirt-remote/cleanup' def cleanup(): """Actually cleanup deleted LVs. When triggered, continue to iterate over cleanup queue files, deleting LVs one at a time, until there are no more pending cleanups. """ while True: lvs = os.listdir(_JANITOR_DIR) if not lvs: break lv = lvs.pop() lv_path = '/dev/xenvg/%s' % lv try: # If the LV name doesn't start with old_, we probably # don't actually want to be deleting it. # # Put it in the try block because we still want to delete # the state file. if not lv.startswith('old_'): continue # If the LV doesn't exist, for whatever reason, don't # proceed because the dd will simply fill the devfs # by creating a regular file and filling it with zeros. if not os.path.exists(lv_path): continue syslog.syslog(syslog.LOG_INFO, "Cleaning up LV '%s'" % lv_path) # In a perfect world, this should be erroring out with # ENOSPC, so we ignore errors subprocess.call(['/usr/bin/ionice', '-c', '2', '-n', '7', '/usr/bin/nice', '/bin/dd', 'if=/dev/zero', 'of=%s' % lv_path, 'bs=1M']) # Ignore any errors here, because there's really just not # anything we can do. subprocess.call(['/sbin/lvchange', '-a', 'n', lv_path]) subprocess.call(['/sbin/lvchange', '-a', 'ey', lv_path]) subprocess.check_call(['/sbin/lvremove', '--force', lv_path]) syslog.syslog(syslog.LOG_INFO, "Successfully cleaned up LV '%s'" % lv_path) except: syslog.syslog(syslog.LOG_ERR, "Error cleaning up LV '%s':" % lv_path) for line in traceback.format_exc().split('\n'): syslog.syslog(syslog.LOG_ERR, line) finally: # Regardless of what happens, we always want to remove the # cleanup queue file, because even if there's an error, we # don't want to waste time wiping the same disk repeatedly os.unlink(os.path.join(_JANITOR_DIR, lv)) class Janitor(pyinotify.ProcessEvent): """Process inotify events by wiping and deleting LVs. The Janitor class receives inotify events when a new file is created in the state directory. """ def process_IN_CREATE(self, event): """Handle a created file or directory. When an IN_CREATE event comes in, trigger a cleanup. """ cleanup() def main(): """Initialize the inotifications and start the main loop.""" syslog.openlog('invirt-janitor', syslog.LOG_PID, syslog.LOG_DAEMON) watch_manager = pyinotify.WatchManager() janitor = Janitor() notifier = pyinotify.Notifier(watch_manager, janitor) watch_manager.add_watch(_JANITOR_DIR, pyinotify.EventsCodes.ALL_FLAGS['IN_CREATE']) # Before inotifying, run any pending cleanups; otherwise we won't # get notified for them. cleanup() while True: notifier.process_events() if notifier.check_events(): notifier.read_events() if __name__ == '__main__': main()