#!/usr/bin/python
#
# Utility for exporting Sat5 entity-data
#
# Copyright (c) 2014--2015 Red Hat, Inc.
#
#
# This software is licensed to you under the GNU General Public License,
# version 2 (GPLv2). There is NO WARRANTY for this software, express or
# implied, including the implied warranties of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
# along with this software; if not, see
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
#
# Red Hat trademarks are not licensed under GPLv2. No permission is
# granted to use or replicate Red Hat trademarks that are incorporated
# in this software or its documentation.
#

"""
spacewalk-export - a tool for preparing to move data from an existing Satellite-5 instance
to a Satellite-6 instance
"""

import csv
import logging
import os
import re
import sys

from optparse import OptionParser, OptionGroup
from os.path import expanduser
from string import lower, split
from subprocess import Popen, call

home = expanduser("~")
DEFAULT_EXPORT_DIR = home + '/spacewalk-export-dir'
DEFAULT_EXPORT_PACKAGE = 'spacewalk_export.tar.gz'
REPORTS_DIR = 'exports'

SUPPORTED_ENTITIES = {
    'activation-keys': 'Activation keys',
    'channels': 'Custom/cloned channels and repositories for all organizations',
    'config-files-latest': 'Latest revision of all configuration files',
    'kickstart-scripts': 'Kickstart scripts for all organizations',
    'repositories': 'Defined repositories',
    'system-groups': 'System-groups for all organizations',
    'system-profiles': 'System profiles for all organizations',
    'users': 'Users and Organizations'
}

#
# Some sw-reports use org_id and some use organization-id
# Map report-to-org-id so we can make generic decisions later
#
REPORT_TO_ORG = {
    'activation-keys': 'org_id',
    'channels': 'org_id',
    'config-files-latest': 'org_id',
    'kickstart-scripts': 'org_id',
    'repositories': 'org_id',
    'system-groups': 'org_id',
    'system-profiles': 'organization_id',
    'users': 'organization_id'
}


def setupOptions():
    usage = 'usage: %prog [options]'
    parser = OptionParser(usage=usage)

    locGroup = OptionGroup(parser, "Locations", "Where do you want to export to?")
    locGroup.add_option('--export-dir', action='store', dest='export_dir',
                        metavar='DIR', default=DEFAULT_EXPORT_DIR,
                        help='Specify directory to store exports in (will be created if not found) - defaults to ' + DEFAULT_EXPORT_DIR)
    locGroup.add_option('--export-package', action='store', dest='export_package',
                        metavar='FILE', default=DEFAULT_EXPORT_PACKAGE,
                        help='Specify filename to use for final packaged-exports tarfile - defaults to ' + DEFAULT_EXPORT_PACKAGE)
    parser.add_option_group(locGroup)

    entGroup = OptionGroup(parser, "Entities", "What do you want to export?")
    entGroup.add_option('--list-entities', action='store_true', dest='list',
                        default=False, help='List supported entities')
    entGroup.add_option('--entities', action='store', dest='entities',
                        metavar='entity[,entity...]', default='all',
                        help='Specify comma-separated list of entities to export (default is all)')
    entGroup.add_option('--org', action='append', type='int', dest='org_ids',
                        metavar='ORG-ID', help='Specify an org-id whose data we will export')
    entGroup.add_option('--dump-repos', action='store_true', dest='dump_repos',
                        default=False, help='Dump contents of file: repositories')
    parser.add_option_group(entGroup)

    chanGroup = OptionGroup(parser, "Channels")
    chanGroup.add_option('--ext-pkgs', action='store_true', dest='extpkgs',
                         default=False, help='Channel-output contains only external packages')
    chanGroup.add_option('--skip-repogen', action='store_true', dest='skipregen',
                         default=False, help='Omit repodata generation for exported channels')
    chanGroup.add_option('--no-size', action='store_true', dest='nosize',
                         default=False, help='Do not check package size')
    parser.add_option_group(chanGroup)

    utilGroup = OptionGroup(parser, "Utility")
    utilGroup.add_option('--clean', action='store_true', default=False, dest='clean',
                         help='How do I clean up from previous runs?')
    utilGroup.add_option('--debug', action='store_true', default=False, dest='debug',
                         help='Log debugging output')
    utilGroup.add_option('--quiet', action='store_true', default=False, dest='quiet',
                         help='Log only errors')
    parser.add_option_group(utilGroup)

    return parser


def setupLogging(opt):
    # determine the logging level
    if opt.debug:
        level = logging.DEBUG
    elif opt.quiet:
        level = logging.ERROR
    else:
        level = logging.INFO
    # configure logging
    logging.basicConfig(level=level, format='%(levelname)s: %(message)s')
    return


def listSupported():
    logging.info('Currently-supported entities include:')
    for s in SUPPORTED_ENTITIES.keys():
        logging.info('%20s : %s' % (s, SUPPORTED_ENTITIES[s]))

    return


def setupEntities(options):
    entities = {}
    doAll = options.entities == 'all'

    for s in SUPPORTED_ENTITIES.keys():
        entities[s] = doAll

    if doAll:
        return entities

    for e in split(options.entities, ','):
        if e in entities.keys():
            entities[e] = True
        else:
            logging.error('ERROR: unsupported entity ' + e + ', skipping...')

    return entities


def setupOutputDir(options):
    if not os.path.isdir(options.export_dir):
        os.mkdir(options.export_dir, 0700)
    if not os.path.isdir(options.export_dir + '/' + REPORTS_DIR):
        os.mkdir(options.export_dir + '/' + REPORTS_DIR, 0700)

# Did we get an orgs-list? If so, return an array of --where entries


def _my_call(*args, **kwargs):
    rc = call(*args, **kwargs)
    if rc:
        logging.error('Error response from %s : [%s]' % (args[0][0], rc))
    return rc

def _generateWhere(options, reportname):
    where = []

    if options.org_ids:
        for org in options.org_ids:
            where.append('--where-%s=%s' % (REPORT_TO_ORG[reportname], org))

    return where


def _issueReport(options, reportname):
    report_file = '%s/%s/%s.csv' % (options.export_dir, REPORTS_DIR, reportname)
    where_clause = _generateWhere(options, reportname)
    logging.debug('...WHERE = %s' % where_clause)
    if len(where_clause) == 0:
        _my_call(['/usr/bin/spacewalk-report', reportname], stdout=open(report_file, 'w'))
    else:
        _my_call(['/usr/bin/spacewalk-report'] + where_clause + [reportname],
                stdout=open(report_file, 'w'))
    return report_file


def channelsDump(options):
    logging.info('Processing channels...')
    _issueReport(options, 'channels')
    # /usr/bin/spacewalk-export-channels -d options.export_dir + '/' + REPORTS_DIR + '/' + 'CHANNELS' -f FORCE
    # if extpkgs -e; if skipregen -s; if nosize -S
    extra_args = []
    if options.extpkgs:
        extra_args.append('-e')
    if options.skipregen:
        extra_args.append('-s')
    if options.nosize:
        extra_args.append('-S')
    if options.org_ids:
        # Go from list-of-ints to list-of-args-to-export - ['-o', 'oid1', '-o', 'oid2', ...]
        oid_args = ' '.join(['-o ' + i for i in map(str, options.org_ids)]).split(' ')
        extra_args = extra_args + oid_args

    logging.debug('...EXTRA = %s' % extra_args)

    channel_export_dir = options.export_dir + '/' + REPORTS_DIR + '/' + 'CHANNELS'
    if not os.path.isdir(channel_export_dir):
        os.mkdir(channel_export_dir, 0700)
    _my_call(['/usr/bin/spacewalk-export-channels', '-d', channel_export_dir, '-f', 'FORCE'] + extra_args)
    return


def usersDump(options):
    logging.info('Processing users...')
    _issueReport(options, 'users')
    return


def systemgroupsDump(options):
    logging.info('Processing system-groups...')
    _issueReport(options, 'system-groups')
    return


def systemprofilesDump(options):
    logging.info('Processing system-profiles...')
    _issueReport(options, 'system-profiles')
    return


def activationkeysDump(options):
    logging.info('Processing activation-keys...')
    _issueReport(options, 'activation-keys')
    return


def repositoriesDump(options):
    logging.info('Processing repositories...')
    repo_file = _issueReport(options, 'repositories')
    if (options.dump_repos):
        logging.info('...repository dump requested')
        # Go thru the CSV we just dumped and look for file: repos
        handle = open(repo_file, 'r')
        repositories = csv.DictReader(handle)
        for entry in repositories:
            # file: protocol is file:// host/ path
            # Look for file::///<repo-location>
            if entry['source_url'].lower().startswith('file://'):
                logging.debug('Found file-repository : ' + entry['source_url'])
                # Strip off 'file://' to get absolute path
                repo_loc = entry['source_url'][7:]
                # Get the leading directory
                repo_dir = repo_loc.rsplit('/', 1)[0]
                # Get the repository directoryname
                repo_basename = repo_loc.rsplit('/', 1)[-1]
                # Tarfile name is 'repository_<repo-label>_contents.tar.gz'
                repo_tarname = 'repository_' + entry['repo_label'] + '_contents.tar.gz'
                logging.info('...storing file-repo %s into %s' % (repo_loc, repo_tarname))
                # Tar it up into the export-dir
                if options.debug:
                    _my_call(['/bin/tar', '-c', '-v', '-z',
                            '-C', repo_dir,
                            '-f', '%s/%s/%s' % (options.export_dir, REPORTS_DIR, repo_tarname),
                            repo_basename])
                else:
                    _my_call(['/bin/tar', '-c', '-z',
                            '-C', repo_dir,
                            '-f', '%s/%s/%s' % (options.export_dir, REPORTS_DIR, repo_tarname),
                            repo_basename])
        handle.close()
    return


def kickstartscriptsDump(options):
    logging.info('Processing kickstart-scripts...')
    _issueReport(options, 'kickstart-scripts')
    return


def configfileslatestDump(options):
    logging.info('Processing config-files...')
    _issueReport(options, 'config-files-latest')
    return


def prepareExport(options):
    if options.debug:
        rc = _my_call(['/bin/tar', '-c', '-v', '-z',
              '--owner', 'apache', '--group', 'apache',
              '-C', options.export_dir,
              '-f', '%s/%s' % (options.export_dir, options.export_package),
              REPORTS_DIR])
    else:
        rc = _my_call(['/bin/tar', '-c', '-z',
              '--owner', 'apache', '--group', 'apache',
              '-C', options.export_dir,
              '-f', '%s/%s' % (options.export_dir, options.export_package),
              REPORTS_DIR])

    if rc:
        logging.error('Error attempting to create export-file at %s/%s' %
                (options.export_dir, options.export_package))
        sys.exit(1)
    else:
        logging.info('Export-file created at %s/%s' %
                (options.export_dir, options.export_package))


def cleanup(options):
    logging.info('To clean up, issue the following command:')
    logging.info('sudo rm -rf %s' % (options.export_dir))
    logging.info('NOTE:  No, I will not do it for you!')
    return


def checkSuperUser():
    if os.geteuid() != 0:
        print "You must be root to run this!"
        sys.exit(1)


if __name__ == '__main__':
    parser = setupOptions()
    (options, args) = parser.parse_args()
    setupLogging(options)
    logging.debug('OPTIONS = %s' % options)

    if (options.list):
        listSupported()
        sys.exit(0)

    checkSuperUser()

    if (options.clean):
        cleanup(options)
        sys.exit(0)

    entities = setupEntities(options)
    setupOutputDir(options)

    for entity in entities.keys():
        if (entities[entity]):
            logging.debug('DUMPING ' + entity)
            # dump-function name is <entity>Dump(options)
            # BUT - entity-names can have dashes, function names do NOT
            globals()[entity.lower().replace('-', '') + 'Dump'](options)
        else:
            logging.debug('SKIPPING ' + entity)

    prepareExport(options)
