From a2cbcec328ee73891d5f3efa324978bf93639964 Mon Sep 17 00:00:00 2001 From: James Laska Date: Fri, 7 Jun 2013 13:45:25 -0400 Subject: [PATCH] The rhnreg_ks module acts as a wrapper to the command 'rhnreg_ks'. The rhnreg_ks command allows users to manage registration to a Red Hat Network (RHN) (e.g. rhn.redhat.com) server. The moduel will also interact with the specified RHN system via XMLRPC as needed. Before proceeding with registration, the rhnreg_ks module will enable the system to receive updates from RHN. This involves enabling the appropriate RHN yum plugin, as well as disabling an active subscription-manager yum plugin. Once enabled, the module will support the following operations: * configure * register * subscribe to custom child channels (see `rhn-channel` command) * unregister --- library/packaging/rhnreg_ks | 371 ++++++++++++++++++++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 library/packaging/rhnreg_ks diff --git a/library/packaging/rhnreg_ks b/library/packaging/rhnreg_ks new file mode 100644 index 00000000000..3d40b7d77ad --- /dev/null +++ b/library/packaging/rhnreg_ks @@ -0,0 +1,371 @@ +#!/usr/bin/python + +DOCUMENTATION = ''' +--- +module: rhnreg_ks +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: + - code: rhnreg_ks state=absent username=joe_user password=somepass + description: Unregister system from RHN. + + - code: rhnreg_ks state=present username=joe_user password=somepass + description: Register as user I(joe_user) with password I(somepass) and auto-subscribe to available content. + + - code: rhnreg_ks state=present activationkey=1-222333444 enable_eus=true + description: Register with activationkey I(1-222333444) and enable extended update support. + + - code: rhnreg_ks state=present username=joe_user password=somepass server_url=https://xmlrpc.my.satellite/XMLRPC + description: Register as user I(joe_user) with password I(somepass) against a satellite server specified by I(server_url). + + - code: rhnreg_ks state=present username=joe_user password=somepass channels=rhel-x86_64-server-6-foo-1,rhel-x86_64-server-6-bar-1 + description: Register as user I(joe_user) with password I(somepass) and enable channels I(rhel-x86_64-server-6-foo-1) and I(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, filename=None): + RegistrationBase.__init__(self, username, password) + self.config = self._read_config(filename) + + def _read_config(self, filename=None): + ''' + Read configuration from /etc/sysconfig/rhn/up2date + ''' + self.config = up2date_client.config.initUp2dateConfig(filename) + + # 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=''): + 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.netloc.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 = "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(): + + # Load RHSM configuration from file + reg = 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=reg.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'] + reg.username = module.params['username'] + reg.password = module.params['password'] + reg.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 reg.username or reg.password): + module.fail_json(msg="Missing arguments, must supply an activationkey (%s) or username (%s) and password (%s)" % (activationkey, reg.username, reg.password)) + if not activationkey and not (reg.username and reg.password): + module.fail_json(msg="Missing arguments, If registering without an activationkey, must supply username or password") + + # Register system + if reg.is_registered: + module.exit_json(changed=False, msg="System already registered.") + else: + try: + reg.enable() + reg.register(module.params['enable_eus'] == True, activationkey) + reg.subscribe(channels) + except CommandException, e: + module.fail_json(msg="Failed to register with '%s': %s" % (reg.hostname, e)) + else: + module.exit_json(changed=True, msg="System successfully registered to '%s'." % reg.hostname) + + # Ensure system is *not* registered + if state == 'absent': + if not reg.is_registered: + module.exit_json(changed=False, msg="System already unregistered.") + else: + try: + reg.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." % reg.hostname) + + +# include magic from lib/ansible/module_common.py +#<> +main()