#!/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

            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()