#!/usr/bin/python2 -u
#
# Copyright (c) 2008--2018 Red Hat, Inc.
# Copyright (c) 2011 SUSE LLC
#
# 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.
#

import re
import StringIO
import simplejson as json
import shutil
import sys
import os
from optparse import OptionParser
import datetime

from spacewalk.satellite_tools.syncLib import initEMAIL_LOG

LOCK = None

log_path = '/var/log/rhn/reposync.log'


def systemExit(code, msg=None):
    "Exit with a code and optional message(s). Saved a few lines of code."
    sys.stderr.write(str(msg)+'\n')
    sys.exit(code)

try:
    from rhn import rhnLockfile
    from spacewalk.common import rhnLog
    from spacewalk.common.rhnConfig import CFG, initCFG
    from spacewalk.satellite_tools import reposync
    from spacewalk.satellite_tools.syncLib import log, log2disk
except KeyboardInterrupt:
    systemExit(0, "\nUser interrupted process.")
except ImportError:
    systemExit(1, "Unable to find code tree.\n"
               "Path not correct? " + str(sys.path))


def releaseLOCK():
    global LOCK
    if LOCK:
        LOCK.release()
        LOCK = None


def main():

    # quick check to see if you are a super-user.
    if os.getuid() != 0:
        systemExit(8, 'ERROR: must be root to execute.')

    global LOCK
    try:
        LOCK = rhnLockfile.Lockfile('/var/run/spacewalk-repo-sync.pid')
    except rhnLockfile.LockfileLockedException:
        systemExit(1, "ERROR: attempting to run more than one instance of "
                      "spacewalk-repo-sync Exiting.")

    parser = OptionParser()
    parser.add_option('-l', '--list', action='store_true', dest='list',
                      help='List the custom channels with the assosiated repositories.')
    parser.add_option('-s', '--show-packages', action='store_true', dest='show_packages',
                      help='List all packages in a specified channel.')
    parser.add_option('-u', '--url', action='append', dest='url',
                      default=[], help='The url of the repository. Can be used multiple times.')
    parser.add_option('-c', '--channel', action='append',
                      dest='channel_label',
                      help='The label of the channel to sync packages to. Can be used multiple times.')
    parser.add_option('-p', '--parent-channel', action='append',
                      dest='parent_label', default=[],
                      help='Synchronize the parent channel and all its child channels.')
    parser.add_option('-d', '--dry-run', action='store_true',
                      dest='dry_run',
                      help='Test run. No sync takes place.')
    parser.add_option('-n', '--latest', action='store_true',
                      dest='latest',
                      help='Sync latest packages only. Use carefully - you might need to fix some dependencies on your own.')
    parser.add_option('-g', '--config', action='store', dest='config',
               help='Configuration file')
    parser.add_option('-t', '--type', action='store', dest='repo_type',
                      help='Force type of repository ("yum", "uln" and "deb" are supported)')
    parser.add_option('-f', '--fail', action='store_true', dest='fail',
                      default=False,
                      help="If a package import fails, fail the entire operation")
    parser.add_option('-i', '--include', action='callback',
                      callback=reposync.set_filter_opt, type='str', nargs=1,
                      dest='filters', default=[],
                      help="Comma or space separated list of included packages or package groups.")
    parser.add_option('-e', '--exclude', action='callback',
                      callback=reposync.set_filter_opt,
                      type='str', nargs=1, dest='filters', default=[],
                      help="Comma or space separated list of excluded packages or package groups.")
    parser.add_option('', '--email', action="store_true", help="e-mail a report of what was synced/imported")
    parser.add_option('', '--traceback-mail', action="store",
                      help="alternative email address(es) for sync output (--email option)")
    parser.add_option('', '--no-errata', action='store_true', dest='no_errata',
                      default=False, help="Do not sync errata")
    parser.add_option('', '--sync-kickstart', action='store_true', dest='sync_kickstart',
                      default=False, help="Sync kickstartable tree")
    parser.add_option('', '--force-all-errata', action='store_true', dest='force_all_errata',
                      default=False, help="Process metadata of all errata, not only missing.")
    parser.add_option('', '--batch-size', action='store', help="max. batch size for package import (debug only)")
    parser.add_option('-v', '--verbose', action='count',
                      help="Verbose output. Possible to accumulate: -vvv")
    parser.add_option('', '--debug', action='store_true', dest='enable_debug',
                      default=False, help="Enable debugging output.")
    (options, args) = parser.parse_args()

    log_level = options.verbose
    if log_level is None:
        log_level = 0

    initCFG('server.satellite')
    CFG.set('DEBUG', log_level)
    CFG.set("TRACEBACK_MAIL", options.traceback_mail or CFG.TRACEBACK_MAIL)
    if options.email:
        initEMAIL_LOG()
    if options.enable_debug:
        rhnLog.initLOG('stdout', log_level)
    else:
        rhnLog.initLOG(log_path, log_level)
    log2disk(0, "Command: %s" % str(sys.argv))

    l_params=["no_errata", "sync_kickstart", "fail"]
    d_chan_repo=reposync.getChannelRepo()
    l_ch_custom=reposync.getCustomChannels()
    d_parent_child=reposync.getParentsChilds()
    d_ch_repo_sync={}
    l_no_ch_repo_sync=[]

    if options.list:
        log(0, "======================================")
        log(0, "|   Channel Label   |   Repository   |")
        log(0, "======================================")
        for ch in list(set(l_ch_custom) & set(d_chan_repo)):
            for repo in d_chan_repo[ch]:
                log(0, "%s | %s" %(ch,repo))
        for ch in list(set(l_ch_custom)-set(d_chan_repo)):
            log(0, "%s | No repository set" % ch)
        return 0

    if not options.channel_label and not options.parent_label and not options.config:
        systemExit(1, "--channel, --parent-channel or --config must be specifed.")

    if options.config:
        try:
            config_file = open(options.config).read()
            # strip  all whitespace
            config_file = re.sub(r'\s', '', config_file)
            config = json.load(StringIO.StringIO(config_file))

        except Exception as e:
            systemExit(1, "Configuration file is invalid, please check syntax. Error [%s]" % e )

        for key in l_params:
            if key in config and not getattr(options, key):
                setattr(options, key, config[key])

        # Channels
        if 'channel' in config:
            for ch,repo in config['channel'].iteritems():
                 d_ch_repo_sync[ch]=repo

        if 'parent_channel' in config:
            options.parent_label+=config['parent_channel']

    if options.channel_label and len(options.channel_label)>0:
        for channel in options.channel_label:
            d_ch_repo_sync[channel]=options.url

    if options.parent_label:
        for pch in options.parent_label:
            if pch in d_parent_child:
               for ch in [pch]+d_parent_child[pch]:
                    if ch in l_ch_custom and ch not in d_ch_repo_sync:
                        d_ch_repo_sync[ch]=[]
            else:
                systemExit(1, "Channel %s is not custom base channel." % pch)

    for ch in d_ch_repo_sync:
        if ch not in l_ch_custom:
            systemExit(1, "Channel %s is not custom or does not exist." % ch)
        if not d_ch_repo_sync[ch] and not ch in d_chan_repo:
            log(0, "Channel %s Channel has no URL associated, skipping sync" % ch)
            l_no_ch_repo_sync.append(ch)

    for ch in l_no_ch_repo_sync:
        del d_ch_repo_sync[ch]

    if options.dry_run:
        log(0, "======================================")
        log(0, "|   Channel Label   |   Repository   |")
        log(0, "======================================")

        for ch,repo in d_ch_repo_sync.items():
            if repo:
                log(0, " %s : %s" % (ch,", ".join(repo)))
            else:
                log(0, " %s : %s" % (ch,", ".join(d_chan_repo[ch])))

        log(0, "======================================")
        log(0, "|             Parameters             |")
        log(0, "======================================")
        for key in  l_params:
            log(0, " %s: %s" % (key,str(getattr(options, key))))
        return 0

    if options.batch_size:
        try:
            batch_size = int(options.batch_size)
            if batch_size <= 0:
                raise ValueError()
        except ValueError:
            systemExit(1, "Invalid batch size: %s" % options.batch_size)

    reposync.clear_ssl_cache()

    total_time = datetime.timedelta()
    ret_code = 0
    for ch,repo in d_ch_repo_sync.items():

        log(0, "======================================")
        log(0, "| Channel: %s" % ch)
        log(0, "======================================")
        log(0, "Sync of channel started.")
        log2disk(0, "Please check 'reposync/%s.log' for sync log of this channel." % ch, notimeYN=True)
        sync = reposync.RepoSync(channel_label=ch,
                      repo_type=options.repo_type,
                      url=repo,
                      fail=options.fail,
                      filters=options.filters,
                      no_errata=options.no_errata,
                      sync_kickstart=options.sync_kickstart,
                      latest=options.latest,
                      debug=options.enable_debug,
                      log_level=options.verbose,
                      force_all_errata=options.force_all_errata, show_packages_only=options.show_packages)
        if options.batch_size:
            sync.set_import_batch_size(options.batch_size)
        elapsed_time, channel_ret_code = sync.sync()
        if channel_ret_code != 0 and ret_code == 0:
            ret_code = channel_ret_code
        total_time += elapsed_time
        # Switch back to common log
        rhnLog.initLOG(log_path, log_level)
        log2disk(0, "Sync of channel completed.")

    log(0, "Total time: %s" % str(total_time).split('.')[0])
    if options.email:
        reposync.send_mail()
    releaseLOCK()
    return ret_code


if __name__ == '__main__':
    try:
        sys.exit(abs(main() or 0))
    except KeyboardInterrupt:
        systemExit(1, "\nProcess has been interrupted.")
    except SystemExit, e:
        releaseLOCK()
        sys.exit(e.code)
    except Exception, e:
        releaseLOCK()
        raise
