#!/usr/bin/python3
#
# Copyright (c) 2010--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.
#

import sys
import fnmatch
from optparse import OptionParser, Option
import re
from uyuni.common.cli import getUsernamePassword, xmlrpc_login, xmlrpc_logout

try:
    import xmlrpclib
except ImportError:
    import xmlrpc.client as xmlrpclib  # pylint: disable=F0401

try:
    import ConfigParser
except ImportError:
    import configparser as ConfigParser

DEFAULT_SERVER = "localhost"

DEFAULT_CONFIG = '/etc/rhn/spacewalk-common-channels.ini'

DEFAULT_REPO_TYPE = 'yum'

CHANNEL_ARCH = {
    'aarch64':      'channel-aarch64',
    'i386':         'channel-ia32',
    'ia64':         'channel-ia64',
    'sparc':        'channel-sparc',
    'sparc64':      'channel-sparc64',
    'alpha':        'channel-alpha',
    's390':         'channel-s390',
    's390x':        'channel-s390x',
    'iSeries':      'channel-iSeries',
    'pSeries':      'channel-pSeries',
    'x86_64':       'channel-x86_64',
    'ppc':          'channel-ppc',
    'ppc64':        'channel-ppc64',
    'ppc64le':      'channel-ppc64le',
    'amd64-deb':    'channel-amd64-deb',
    'ia32-deb':     'channel-ia32-deb',
    'ia64-deb':     'channel-ia64-deb',
    'sparc-deb':    'channel-sparc-deb',
    'alpha-deb':    'channel-alpha-deb',
    's390-deb':     'channel-s390-deb',
    'powerpc-deb':  'channel-powerpc-deb',
    'arm-deb':      'channel-arm-deb',
    'mips-deb':     'channel-mips-deb'

}
CHANNEL_NAME_TO_ARCH = {
    'AArch64': 'aarch64',
    'Alpha': 'alpha',
    'Alpha Debian': 'alpha-deb',
    'AMD64 Debian': 'amd64-deb',
    'ARM64 Debian': 'arm64-deb',
    'arm Debian': 'arm-deb',
    'ARM hard. FP':  'armhfp',
    'ARM soft. FP': 'arm',
    'IA-32':  'i386',
    'IA-32 Debian': 'ia32-deb',
    'IA-64': 'ia64',
    'IA-64 Debian': 'ia64-deb',
    'iSeries': 'iSeries',
    'mips Debian': 'mips-deb',
    'PowerPC Debian': 'powerpc-deb',
    'PPC': 'ppc',
    'PPC64LE': 'ppc64le',
    'pSeries': 'pSeries',
    's390': 's390',
    's390 Debian': 's390-deb',
    's390x': 's390x',
    'Sparc': 'sparc',
    'Sparc Debian': 'sparc-deb',
    'x86_64': 'x86_64'
}

SPLIT_PATTERN = '[ ,]+'


class ExtOptionParser(OptionParser):

    """extend OptionParser to print examples"""

    def __init__(self, examples=None, **kwargs):
        self.examples = examples
        OptionParser.__init__(self, **kwargs)

    def print_help(self):
        OptionParser.print_help(self)
        print("\n\n" + self.examples)


def connect(user, password, server):
    server_url = "http://%s/rpc/api" % server

    if options.verbose and options.verbose > 2:
        client_verbose = options.verbose - 2
    else:
        client_verbose = 0
    if options.verbose:
        sys.stdout.write("Connecting to %s\n" % server_url)
    client = xmlrpclib.Server(server_url, verbose=client_verbose)
    options.user, password = getUsernamePassword(user, password)
    key = xmlrpc_login(client, options.user, password)
    return client, key


def add_channels(channels, section, arch, client):
    base_channels = ['']
    optional = ['activationkey', 'base_channel_activationkey', 'gpgkey_url',
                'gpgkey_id', 'gpgkey_fingerprint', 'repo_url',
                'yum_repo_label', 'dist_map_release', 'repo_type']
    mandatory = ['label', 'name', 'summary', 'checksum', 'arch', 'section']

    config.set(section, 'arch', arch)
    config.set(section, 'section', section)
    if config.has_option(section, 'base_channels'):
        base_channels = re.split(SPLIT_PATTERN,
                                 config.get(section, 'base_channels'), 1)

    for base_channel in base_channels:
        config.set(section, 'base_channel', base_channel)
        channel = {'base_channel': config.get(section, 'base_channel')}

        if base_channel:
            if channel['base_channel'] in channels:
                # If channel exists at the INI file, use it.
                pass
            elif channel_exists(client, channel['base_channel']):
                # If not, if cannel exists on the server, convert it
                channel_base_server = channel_get_details(client, channel['base_channel'])
                channels[channel['base_channel']] = {'label': channel_base_server['label'],
                                                     'name' : channel_base_server['name'],
                                                     'summary': channel_base_server['summary'],
                                                     'arch': CHANNEL_NAME_TO_ARCH[channel_base_server['arch_name']],
                                                     'checksum': channel_base_server['checksum_label'],
                                                     'gpgkey_url': channel_base_server['gpg_key_url'],
                                                     'gpgkey_id': channel_base_server['gpg_key_id'],
                                                     'gpgkey_fingerprint': channel_base_server['gpg_key_fp'],
                                                     'section': section}
            else:
                # Otheriwse there isn't such base channel so skip also child
                continue
            # set base channel values so they can be used as macros
            for (k, v) in list(channels[channel['base_channel']].items()):
                config.set(section, 'base_channel_' + k, v)

        for k in optional:
            if config.has_option(section, k):
                channel[k] = config.get(section, k)
            else:
                channel[k] = ''
        for k in mandatory:
            channel[k] = config.get(section, k)
        channels[channel['label']] = channel


def channel_exists(client, channel_label):
    # check whether channel exists
    try:
        base_info = client.channel.software.isExisting(key, channel_label)
        return base_info
    # We should improve this as a connection failure would be the same
    # as not finding the channel
    except xmlrpclib.Fault as e:
        return None


def channel_get_details(client, channel_label):
    # get details for a channel
    try:
        base_info = client.channel.software.getDetails(key, channel_label)
        return base_info
    # We should improve this as a connection failure would be the same
    # as not finding the channel
    except xmlrpclib.Fault as e:
        return None

def get_existing_repos(client):
    result = {}
    try:
        user_repos = client.channel.software.listUserRepos(key)
        for repo in user_repos:
            result[repo['sourceUrl']] = repo['label']
    except xmlrpclib.Fault as e:
        return None
    return result


if __name__ == "__main__":
    # options parsing
    usage = "usage: %prog [options] <channel1 glob> [<channel2 glob> ... ]"
    examples = """Examples:

Create Fedora 12 channel, its child channels and activation key limited to 10 servers:
    %(prog)s -u admin -p pass -k 10 'fedora12*'

Create Centos 5 with child channels only on x86_64:
    %(prog)s -u admin -p pass -a x86_64 'centos5*'

Create only Centos 4 base channels for intel archs:
    %(prog)s -u admin -p pass -a i386,x86_64 'centos4'

Create Spacewalk client child channel for every (suitable) defined base channel:
    %(prog)s -u admin -p pass 'spacewalk-client*'

Create everything as well as unlimited activation key for every channel:
    %(prog)s -u admin -p pass -k unlimited '*'
\n""" % {'prog': sys.argv[0]}

    option_list = [
        Option("-c", "--config", help="configuration file",
               default=DEFAULT_CONFIG),
        Option("-u", "--user", help="username"),
        Option("-p", "--password", help="password"),
        Option("-s", "--server", help="your spacewalk server",
               default=DEFAULT_SERVER),
        Option("-k", "--keys", help="activation key usage limit -"
               + " 'unlimited' or number\n"
               + "(default: options is not set and activation keys"
               + " are not created at all)",
               dest="key_limit"),
        Option("-n", "--dry-run", help="perform a trial run with no changes made",
               action="store_true"),
        Option("-a", "--archs", help="list of architectures"),
        Option("-v", "--verbose", help="verbose", action="count"),
        Option("-l", "--list", help="print list of available channels",
               action="store_true"),
        Option("-d", "--default-channels", help="make base channels default channels for given OS version",
               action="store_true"),
    ]

    parser = ExtOptionParser(usage=usage, option_list=option_list, examples=examples)
    (options, args) = parser.parse_args()
    config = ConfigParser.ConfigParser()
    config.read(options.config)

    if options.list:
        print("Available channels:")
        channel_list = config.sections()
        if channel_list:
            for channel in sorted(channel_list):
                channel_archs = config.get(channel, 'archs')
                print(" %-20s %s" % (channel + ":", channel_archs))
        else:
            print(" [no channel available]")
        sys.exit(0)

    if not args:
        print(parser.print_help())
        parser.exit()

    try:
        client, key = connect(options.user, options.password, options.server)
        user_info = client.user.getDetails(key, options.user)
        org_id = user_info['org_id']
    except xmlrpclib.Fault as e:
        if e.faultCode == 2950:
            sys.stderr.write("Either the password or username is incorrect.\n")
            sys.exit(2)
        else:
            raise

    channels = {}

    sections = []
    # sort base channels first and child last
    for section in config.sections():
        if config.has_option(section, 'base_channels'):  # child
            sections.append(section)
        else:                                           # base
            sections.insert(0, section)
    for section in sections:
        archs = re.split(SPLIT_PATTERN, config.get(section, 'archs'))
        if options.archs:
            # filter out archs not set on commandline
            archs = [a for a in archs if a in options.archs]
        for arch in archs:
            add_channels(channels, section, arch, client)

    # list of base_channels to deal with
    base_channels = {}
    # list of child_channels for given base_channel
    child_channels = {}
    # filter out non-matching channels
    for pattern in args:
        matching_channels = [n for n in list(channels.keys())
                             if fnmatch.fnmatch(channels[n]['section'], pattern)]
        for name in matching_channels:
            attr = channels[name]
            if 'base_channel' in attr and attr['base_channel']:
                if attr['base_channel'] not in base_channels:
                    base_channels[attr['base_channel']] = False
                if attr['base_channel'] in child_channels:
                    child_channels[attr['base_channel']].append(name)
                else:
                    child_channels[attr['base_channel']] = [name]
            else:
                # this channel is base channel
                base_channels[name] = True
                if name not in child_channels:
                    child_channels[name] = []

    if not matching_channels:
        sys.stderr.write("No channels matching your selection.\n")
        sys.exit(2)

    existing_repo_urls = get_existing_repos(client)
    if existing_repo_urls is None:
        sys.stderr.write("Unable to get exsiting repositories from server.\n")
        sys.exit(2)

    for (base_channel_label, create_channel) in sorted(base_channels.items()):

        if create_channel:
            base_info = channels[base_channel_label]
            repo_type = base_info.get('repo_type', DEFAULT_REPO_TYPE) or DEFAULT_REPO_TYPE
            if options.verbose:
                sys.stdout.write("Base channel '%s' - creating...\n"
                                 % base_info['name'])
            if options.verbose and options.verbose > 1:
                sys.stdout.write(
                    "* label=%s, summary=%s, arch=%s, repo_type=%s, checksum=%s\n" % (
                        base_info['label'], base_info['summary'],
                        base_info['arch'], repo_type,
                        base_info['checksum']))

            if not options.dry_run:
                try:
                    # create base channel
                    client.channel.software.create(key,
                                                   base_info['label'], base_info['name'],
                                                   base_info['summary'], CHANNEL_ARCH[base_info['arch']],
                                                   '', base_info['checksum'],
                                                   {'url': base_info['gpgkey_url'],
                                                    'id': base_info['gpgkey_id'],
                                                    'fingerprint': base_info['gpgkey_fingerprint']})
                    if base_info['repo_url'] in existing_repo_urls:
                        # use existing repo
                        client.channel.software.associateRepo(key,
                                                              base_info['label'],
                                                              existing_repo_urls[base_info['repo_url']])
                    else:
                        client.channel.software.createRepo(key,
                                                           base_info['yum_repo_label'], repo_type,
                                                           base_info['repo_url'])
                        client.channel.software.associateRepo(key,
                                                              base_info['label'], base_info['yum_repo_label'])
                except xmlrpclib.Fault as e:
                    if e.faultCode == 1200:  # ignore if channel exists
                        sys.stdout.write("INFO: %s exists\n" % base_info['label'])
                    else:
                        sys.stderr.write("ERROR: %s: %s\n" % (
                            base_info['label'], e.faultString))
                        sys.exit(2)

            if options.key_limit is not None:
                if options.verbose:
                    sys.stdout.write("* Activation key '%s' - creating...\n" % (
                        base_info['label']))
                if not options.dry_run:
                    # create activation key
                    if options.key_limit == 'unlimited':
                        ak_args = (key, base_info['activationkey'],
                                   base_info['name'], base_info['label'],
                                   [], False)
                    else:
                        ak_args = (key, base_info['activationkey'],
                                   base_info['name'], base_info['label'],
                                   int(options.key_limit), [], False)
                    try:
                        client.activationkey.create(*ak_args)
                    except xmlrpclib.Fault as e:
                        if e.faultCode != 1091:  # ignore if ak exists
                            sys.stderr.write("ERROR: %s: %s\n" % (
                                base_info['label'], e.faultString))
        else:
            # check whether channel exists
            if channel_exists(client, base_channel_label):
                base_info = channel_get_details(client, base_channel_label)
                sys.stdout.write("Base channel '%s' - exists\n" % base_info['name'])
            else:
                sys.stderr.write("ERROR: %s could not be found at the server\n" % base_channel_label)

        if options.default_channels:
            try:
                client.distchannel.setMapForOrg(key,
                                                base_info['name'], base_info['dist_map_release'],
                                                base_info['arch'], base_info['label'])

            except xmlrpclib.Fault as e:
                sys.stderr.write("ERROR: %s: %s\n" % (
                    base_info['label'], e.faultString))

        for child_channel_label in sorted(child_channels[base_channel_label]):
            child_info = channels[child_channel_label]
            repo_type = child_info.get('repo_type', DEFAULT_REPO_TYPE) or DEFAULT_REPO_TYPE
            if options.verbose:
                sys.stdout.write("* Child channel '%s' - creating...\n"
                                 % child_info['name'])
            if options.verbose and options.verbose > 1:
                sys.stdout.write(
                    "** label=%s, summary=%s, arch=%s, parent=%s, repo_type=%s, checksum=%s\n"
                    % (child_info['label'], child_info['summary'],
                       child_info['arch'], base_channel_label, repo_type,
                       child_info['checksum']))

            if not options.dry_run:
                try:
                    # create child channels
                    client.channel.software.create(key,
                                                   child_info['label'], child_info['name'],
                                                   child_info['summary'],
                                                   CHANNEL_ARCH[child_info['arch']], base_channel_label,
                                                   child_info['checksum'],
                                                   {'url': child_info['gpgkey_url'],
                                                       'id': child_info['gpgkey_id'],
                                                       'fingerprint': child_info['gpgkey_fingerprint']})
                    if child_info['repo_url'] in existing_repo_urls:
                        # use existing repo
                        client.channel.software.associateRepo(key,
                                                              child_info['label'],
                                                              existing_repo_urls[child_info['repo_url']])
                    else:
                        client.channel.software.createRepo(key,
                                                           child_info['yum_repo_label'], repo_type,
                                                           child_info['repo_url'])
                        client.channel.software.associateRepo(key,
                                                              child_info['label'], child_info['yum_repo_label'])
                except xmlrpclib.Fault as e:
                    if e.faultCode == 1200:  # ignore if channel exists
                        sys.stderr.write("WARNING: %s: %s\n" % (
                            child_info['label'], e.faultString))
                    else:
                        sys.stderr.write("ERROR: %s: %s\n" % (
                            child_info['label'], e.faultString))
                        sys.exit(2)

            if options.key_limit is not None:
                if ('base_channel_activationkey' in child_info
                        and child_info['base_channel_activationkey']):
                    activationkey = "%s-%s" % (
                        org_id, child_info['base_channel_activationkey'])
                    if options.verbose:
                        sys.stdout.write(
                            "** Activation key '%s' - adding child channel...\n" % (
                                activationkey))
                    if not options.dry_run:
                        try:
                            client.activationkey.addChildChannels(key,
                                                                  activationkey, [child_info['label']])
                        except xmlrpclib.Fault as e:
                            sys.stderr.write("ERROR: %s: %s\n" % (
                                child_info['label'], e.faultString))

        if options.verbose:
            # an empty line after channel group
            sys.stdout.write("\n")
        existing_repo_urls = get_existing_repos(client)
    if client is not None:
        # logout
        xmlrpc_logout(client, key)
