#! /usr/bin/env python

'''Cleanup after jobs/clients that have vanished without pruning.

For active jobs, pruning isn't an issue if you use auto-pruning (i.e. a
job-specific prune runs at the completion of each job run).  However, when
you remove a job, no auto-prune will ever be run for it again, which means
that all of the volumes that exist at the time the job is removed will
never be cleaned up properly.  Further, if you've actually deleted or
renamed the job and/or client, you will be unable to manually run such a
prune from within bconsole, as it won't be aware of the client/job.

It must be run as a user with sufficent permissions to access bconsole.conf.

'''

import sys
import subprocess
import logging
import optparse
import bacula_tools

# pylint: disable=unused-argument


def lame_progress_spinner(iterable, **kwargs):
    '''Add spinner output to consumption of iterator items.

    When you're printing stuff out with a long-running process, it's nice
    to see that you're making progress.  The **kwargs argument is for
    compatibility with the clint constructor, which will let you select the
    spinner style.  It is ignored here.

    '''
    spinlist = ['|', '/', '-', '\\']
    index = 1
    sys.stdout.write(spinlist[0])
    for item in iterable:
        yield item
        sys.stdout.write('\b' + spinlist[index])
        sys.stdout.flush()
        index = (index + 1) % len(spinlist)

try:
    # pylint: disable=import-error
    from clint.textui.progress import bar as progress_bar
except ImportError:
    progress_bar = lame_progress_spinner


class BConsole(subprocess.Popen):

    '''Wrapper around the bconsole command.  Will some day be refactored to use
    the bacula_tools class.
    '''
    prompt = '@# bconsole prompt\n'

    def __init__(self, options):
        '''Constructor.'''
        cmd = ['bconsole']
        if options.config_file:
            cmd.extend(['-c', options.config_file])
        subprocess.Popen.__init__(self, cmd, universal_newlines=True,
                                  stdin=subprocess.PIPE, stdout=subprocess.PIPE)
        logging.debug(str(self.execute('version')))
        return

    def execute(self, cmd):
        '''Dispatch commands to bconsole and return the result.
        Poor man's expect.'''
        retval = []
        logging.info('executing cmd: "%s"', cmd)
        self.stdin.write(cmd + '\n.\n' + self.prompt)
        while True:
            line = self.stdout.readline()
            logging.debug('got line: "%s"', line)
            if line == self.prompt:
                break
            retval.append(line)
        return retval

    def data_cleaner(self, raw):
        '''Convert columnar, pipe-separated data into a list of lists.
        '''
        data = []
        logging.info(str(raw))
        for line in raw:
            if not line[0] == '|':
                continue
            bits = [x.strip() for x in line.split('|')]
            # bits[1] is numeric, so 'Id' indicates a header row
            if 'Id' in bits[1]:
                continue
            data.append(bits)
        return sorted(data)

    def get_pools(self):
        'List all of the pools.'
        raw = self.execute('list pool')
        return self.data_cleaner(raw)

    def get_media(self, pool):
        'List all the media in the given pool.'
        raw = self.execute('list media pool=%s' % pool)
        return self.data_cleaner(raw)


def prune_commands(medialist):
    '''Convert a list of media into a list of commands for pruning the media.
    Ignore any media that is already pruned, as well as some other special
    categories.

    '''
    retval = []
    if medialist:
        for volume in medialist:
            if volume[3] in ('Purged', 'Error', 'Archive', 'Recycle',
                             'Disabled', 'Busy', 'Cleaning'):
                continue
            retval.append('prune volume=%s yes' % volume[2])
    return retval


def clean(options, pool_filter=None):
    '''Iterate through all pools that match the given filter, pruning all the
    media in each one.

    '''
    bconsole = BConsole(options)
    pools = bconsole.get_pools()
    commands = []
    for pool in pools:
        poolname = pool[2]
        if pool_filter:
            # This checks to see if True is returned for any 'x in poolname'
            acceptable = True in [x in poolname for x in pool_filter]
        else:
            acceptable = True  # Not trying to filter at all
        if not acceptable:
            continue
        print "searching through %20s" % poolname,
        media = bconsole.get_media(poolname)
        sys.stdout.flush()
        commands.append((poolname, prune_commands(media)))
        print " found %d(%d) records" % (len(commands[-1][-1]), len(media))

    # Actually do the pruning here
    for poolname, pool in commands:
        for cmd in progress_bar(pool,
                                label="  cleaning the %s pool " % poolname):
            bconsole.execute(cmd)
    return


def main():
    '''Main entry point.'''

    parser = optparse.OptionParser(description='Prune Bacula volumes.',
                                   usage='usage: %prog [options] [pools]',
                                   epilog='''By default prune the media in all of the pools.  If you pass pool names on the command-line, the only pools that will be pruned are those whose names CONTAIN the passed-in names, e.g. if you pass "pool1" in, then it will prune pool1, pool10, pool11, etc.''')
    parser.add_option('-d', '--debug', action='store_true', default=False)
    parser.add_option('-c', '--config-file')
    options, args = parser.parse_args()
    if options.debug:
        bacula_tools.set_debug()
    else:
        logging.basicConfig(level=logging.WARNING)
    clean(options, args)

if __name__ == '__main__':
    main()
