#!/usr/bin/python

DOCUMENTATION = '''
---
module: rhn_register
short_description: Manage Red Hat Network registration using the C(rhnreg_ks) command
description:
    - Manage registration to the Red Hat Network.
version_added: "1.2"
author: James Laska
notes:
    - In order to register a system, rhnreg_ks requires either a username and password, or an activationkey.
requirements:
    - rhnreg_ks
options:
    state:
        description:
          - whether to register (C(present)), or unregister (C(absent)) a system
        required: false
        choices: [ "present", "absent" ]
        default: "present"
    username:
        description:
            - Red Hat Network username
        required: False
        default: null
    password:
        description:
            - Red Hat Network password
        required: False
        default: null
    server_url:
        description:
            - Specify an alternative Red Hat Network server URL
        required: False
        default: Current value of I(serverURL) from C(/etc/sysconfig/rhn/up2date) is the default
    activationkey:
        description:
            - supply an activation key for use with registration
        required: False
        default: null
    channels:
        description:
            - Optionally specify a list of comma-separated channels to subscribe to upon successful registration.
        required: false
        default: []
'''

EXAMPLES = '''
# Unregister system from RHN.
- rhn_register: state=absent username=joe_user password=somepass

# Register as user (joe_user) with password (somepass) and auto-subscribe to available content.
- rhn_register: state=present username=joe_user password=somepass

# Register with activationkey (1-222333444) and enable extended update support.
- rhn_register: state=present activationkey=1-222333444 enable_eus=true

# Register as user (joe_user) with password (somepass) against a satellite
# server specified by (server_url).
- rhn_register:
    state=present
    username=joe_user
    password=somepass
    server_url=https://xmlrpc.my.satellite/XMLRPC

# Register as user (joe_user) with password (somepass) and enable
# channels (rhel-x86_64-server-6-foo-1) and (rhel-x86_64-server-6-bar-1).
- rhn_register: state=present username=joe_user
                password=somepass
                channels=rhel-x86_64-server-6-foo-1,rhel-x86_64-server-6-bar-1
'''

import sys
import os
import re
import types
import subprocess
import ConfigParser
import shlex
import xmlrpclib
import urlparse

# Attempt to import rhn client tools
sys.path.insert(0, '/usr/share/rhn')
try:
    import up2date_client
    import up2date_client.config
except ImportError, e:
    module.fail_json(msg="Unable to import up2date_client.  Is 'rhn-client-tools' installed?\n%s" % e)


class CommandException(Exception):
    pass


def run_command(args):
    '''
        Convenience method to run a command, specified as a list of arguments.
        Returns:
            * tuple - (stdout, stder, retcode)
    '''

    # Coerce into a string
    if isinstance(args, str):
        args = shlex.split(args)

    # Run desired command
    proc = subprocess.Popen(args, stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT)
    (stdout, stderr) = proc.communicate()
    returncode = proc.poll()
    if returncode != 0:
        cmd = ' '.join(args)
        raise CommandException("Command failed (%s): %s\n%s" % (returncode, cmd, stdout))
    return (stdout, stderr, returncode)


class RegistrationBase (object):
    def __init__(self, username=None, password=None):
        self.username = username
        self.password = password

    def configure(self):
        raise NotImplementedError("Must be implemented by a sub-class")

    def enable(self):
        # Remove any existing redhat.repo
        redhat_repo = '/etc/yum.repos.d/redhat.repo'
        if os.path.isfile(redhat_repo):
            os.unlink(redhat_repo)

    def register(self):
        raise NotImplementedError("Must be implemented by a sub-class")

    def unregister(self):
        raise NotImplementedError("Must be implemented by a sub-class")

    def unsubscribe(self):
        raise NotImplementedError("Must be implemented by a sub-class")

    def update_plugin_conf(self, plugin, enabled=True):
        plugin_conf = '/etc/yum/pluginconf.d/%s.conf' % plugin
        if os.path.isfile(plugin_conf):
            cfg = ConfigParser.ConfigParser()
            cfg.read([plugin_conf])
            if enabled:
                cfg.set('main', 'enabled', 1)
            else:
                cfg.set('main', 'enabled', 0)
            fd = open(plugin_conf, 'rwa+')
            cfg.write(fd)
            fd.close()

    def subscribe(self, **kwargs):
        raise NotImplementedError("Must be implemented by a sub-class")


class Rhn(RegistrationBase):

    def __init__(self, username=None, password=None):
        RegistrationBase.__init__(self, username, password)
        self.config = self.load_config()

    def load_config(self):
        '''
            Read configuration from /etc/sysconfig/rhn/up2date
        '''
        self.config = up2date_client.config.initUp2dateConfig()

        # Add support for specifying a default value w/o having to standup some
        # configuration.  Yeah, I know this should be subclassed ... but, oh
        # well
        def get_option_default(self, key, default=''):
            # ignore pep8 W601 errors for this line
            # setting this to use 'in' does not work in the rhn library
            if self.has_key(key):
                return self[key]
            else:
                return default

        self.config.get_option = types.MethodType(get_option_default, self.config, up2date_client.config.Config)

        return self.config

    @property
    def hostname(self):
        '''
            Return the non-xmlrpc RHN hostname.  This is a convenience method
            used for displaying a more readable RHN hostname.

            Returns: str
        '''
        url = urlparse.urlparse(self.config['serverURL'])
        return url[1].replace('xmlrpc.','')

    @property
    def systemid(self):
        systemid = None
        xpath_str = "//member[name='system_id']/value/string"

        if os.path.isfile(self.config['systemIdPath']):
            fd = open(self.config['systemIdPath'], 'r')
            xml_data = fd.read()
            fd.close()

            # Ugh, xml parsing time ...
            # First, try parsing with libxml2 ...
            if systemid is None:
                try:
                    import libxml2
                    doc = libxml2.parseDoc(xml_data)
                    ctxt = doc.xpathNewContext()
                    systemid = ctxt.xpathEval(xpath_str)[0].content
                    doc.freeDoc()
                    ctxt.xpathFreeContext()
                except ImportError:
                    pass

            # m-kay, let's try with lxml now ...
            if systemid is None:
                try:
                    from lxml import etree
                    root = etree.fromstring(xml_data)
                    systemid = root.xpath(xpath_str)[0].text
                except ImportError:
                    pass

            # Strip the 'ID-' prefix
            if systemid is not None and systemid.startswith('ID-'):
                systemid = systemid[3:]

        return int(systemid)

    @property
    def is_registered(self):
        '''
            Determine whether the current system is registered.

            Returns: True|False
        '''
        return os.path.isfile(self.config['systemIdPath'])

    def configure(self, server_url):
        '''
            Configure system for registration
        '''

        self.config.set('serverURL', server_url)
        self.config.save()

    def enable(self):
        '''
            Prepare the system for RHN registration.  This includes ...
             * enabling the rhnplugin yum plugin
             * disabling the subscription-manager yum plugin
        '''
        RegistrationBase.enable(self)
        self.update_plugin_conf('rhnplugin', True)
        self.update_plugin_conf('subscription-manager', False)

    def register(self, enable_eus=False, activationkey=None):
        '''
            Register system to RHN.  If enable_eus=True, extended update
            support will be requested.
        '''
        register_cmd = "/usr/sbin/rhnreg_ks --username '%s' --password '%s' --force" % (self.username, self.password)
        if enable_eus:
            register_cmd += " --use-eus-channel"
        if activationkey is not None:
            register_cmd += " --activationkey '%s'" % activationkey
        # FIXME - support --profilename
        # FIXME - support --systemorgid
        run_command(register_cmd)

    def api(self, method, *args):
        '''
            Convenience RPC wrapper
        '''
        if not hasattr(self, 'server') or self.server is None:
            url = "https://xmlrpc.%s/rpc/api" % self.hostname
            self.server = xmlrpclib.Server(url, verbose=0)
            self.session = self.server.auth.login(self.username, self.password)

        func = getattr(self.server, method)
        return func(self.session, *args)

    def unregister(self):
        '''
            Unregister a previously registered system
        '''

        # Initiate RPC connection
        self.api('system.deleteSystems', [self.systemid])

        # Remove systemid file
        os.unlink(self.config['systemIdPath'])

    def subscribe(self, channels=[]):
        if len(channels) <= 0:
            return
        current_channels = self.api('channel.software.listSystemChannels', self.systemid)
        new_channels = [item['channel_label'] for item in current_channels]
        new_channels.extend(channels)
        return self.api('channel.software.setSystemChannels', self.systemid, new_channels)

    def _subscribe(self, channels=[]):
        '''
            Subscribe to requested yum repositories using 'rhn-channel' command
        '''
        rhn_channel_cmd = "rhn-channel --user='%s' --password='%s'" % (self.username, self.password)
        (stdout, stderr, rc) = run_command(rhn_channel_cmd + " --available-channels")

        # Enable requested repoid's
        for wanted_channel in channels:
            # Each inserted repo regexp will be matched. If no match, no success.
            for availaible_channel in stdout.rstrip().split('\n'): # .rstrip() because of \n at the end -> empty string at the end
                if re.search(wanted_repo, available_channel):
                    run_command(rhn_channel_cmd + " --add --channel=%s" % available_channel)

def main():

    # Read system RHN configuration
    rhn = Rhn()

    module = AnsibleModule(
                argument_spec = dict(
                    state = dict(default='present', choices=['present', 'absent']),
                    username = dict(default=None, required=False),
                    password = dict(default=None, required=False),
                    server_url = dict(default=rhn.config.get_option('serverURL'), required=False),
                    activationkey = dict(default=None, required=False),
                    enable_eus = dict(default=False, type='bool'),
                    channels = dict(default=[], type='list'),
                )
            )

    state = module.params['state']
    rhn.username = module.params['username']
    rhn.password = module.params['password']
    rhn.configure(module.params['server_url'])
    activationkey = module.params['activationkey']
    channels = module.params['channels']

    # Ensure system is registered
    if state == 'present':

        # Check for missing parameters ...
        if not (activationkey or rhn.username or rhn.password):
            module.fail_json(msg="Missing arguments, must supply an activationkey (%s) or username (%s) and password (%s)" % (activationkey, rhn.username, rhn.password))
        if not activationkey and not (rhn.username and rhn.password):
            module.fail_json(msg="Missing arguments, If registering without an activationkey, must supply username or password")

        # Register system
        if rhn.is_registered:
            module.exit_json(changed=False, msg="System already registered.")
        else:
            try:
                rhn.enable()
                rhn.register(module.params['enable_eus'] == True, activationkey)
                rhn.subscribe(channels)
            except CommandException, e:
                module.fail_json(msg="Failed to register with '%s': %s" % (rhn.hostname, e))
            else:
                module.exit_json(changed=True, msg="System successfully registered to '%s'." % rhn.hostname)

    # Ensure system is *not* registered
    if state == 'absent':
        if not rhn.is_registered:
            module.exit_json(changed=False, msg="System already unregistered.")
        else:
            try:
                rhn.unregister()
            except CommandException, e:
                module.fail_json(msg="Failed to unregister: %s" % e)
            else:
                module.exit_json(changed=True, msg="System successfully unregistered from %s." % rhn.hostname)


# import module snippets
from ansible.module_utils.basic import *
main()
