#!/usr/bin/python3
#
# 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 sys

from optparse import OptionParser, OptionGroup
from os.path import expanduser
from subprocess import 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 list(SUPPORTED_ENTITIES.keys()):
        logging.info('%20s : %s' % (s, SUPPORTED_ENTITIES[s]))

    return


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

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

    if doAll:
        return entities

    for e in options.entities.split(','):
        if e in list(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, 0o700)
    if not os.path.isdir(options.export_dir + '/' + REPORTS_DIR):
        os.mkdir(options.export_dir + '/' + REPORTS_DIR, 0o700)

# 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, 0o700)
    _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 list(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)
