#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (c) James Laska # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = r''' --- module: rhn_register short_description: Manage Red Hat Network registration using the C(rhnreg_ks) command description: - Manage registration to the Red Hat Network. author: - James Laska (@jlaska) notes: - This is for older Red Hat products. You probably want the M(community.general.redhat_subscription) module instead. - In order to register a system, C(rhnreg_ks) requires either a username and password, or an activationkey. requirements: - rhnreg_ks - either libxml2 or lxml extends_documentation_fragment: - community.general.attributes attributes: check_mode: support: none diff_mode: support: none options: state: description: - Whether to register (C(present)), or unregister (C(absent)) a system. type: str choices: [ absent, present ] default: present username: description: - Red Hat Network username. type: str password: description: - Red Hat Network password. type: str server_url: description: - Specify an alternative Red Hat Network server URL. - The default is the current value of I(serverURL) from C(/etc/sysconfig/rhn/up2date). type: str activationkey: description: - Supply an activation key for use with registration. type: str profilename: description: - Supply an profilename for use with registration. type: str force: description: - Force registration, even if system is already registered. type: bool default: false version_added: 2.0.0 ca_cert: description: - Supply a custom ssl CA certificate file for use with registration. type: path aliases: [ sslcacert ] systemorgid: description: - Supply an organizational id for use with registration. type: str channels: description: - Optionally specify a list of channels to subscribe to upon successful registration. type: list elements: str default: [] enable_eus: description: - If C(false), extended update support will be requested. type: bool default: false nopackages: description: - If C(true), the registered node will not upload its installed packages information to Satellite server. type: bool default: false deprecated: removed_in: 10.0.0 why: | RHN hosted at redhat.com was discontinued years ago, and Spacewalk 5 (which uses RHN) is EOL since 2020, May 31st; while this module could work on Uyuni / SUSE Manager (fork of Spacewalk 5), we have not heard about anyone using it in those setups. alternative: | Contact the community.general maintainers to report the usage of this module, and potentially step up to maintain it. ''' EXAMPLES = r''' - name: Unregister system from RHN community.general.rhn_register: state: absent username: joe_user password: somepass - name: Register as user with password and auto-subscribe to available content community.general.rhn_register: state: present username: joe_user password: somepass - name: Register with activationkey and enable extended update support community.general.rhn_register: state: present activationkey: 1-222333444 enable_eus: true - name: Register with activationkey and set a profilename which may differ from the hostname community.general.rhn_register: state: present activationkey: 1-222333444 profilename: host.example.com.custom - name: Register as user with password against a satellite server community.general.rhn_register: state: present username: joe_user password: somepass server_url: https://xmlrpc.my.satellite/XMLRPC - name: Register as user with password and enable channels community.general.rhn_register: state: present username: joe_user password: somepass channels: rhel-x86_64-server-6-foo-1,rhel-x86_64-server-6-bar-1 - name: Force-register as user with password to ensure registration is current on server community.general.rhn_register: state: present username: joe_user password: somepass server_url: https://xmlrpc.my.satellite/XMLRPC force: true ''' RETURN = r''' # Default return values ''' import os import sys # Attempt to import rhn client tools sys.path.insert(0, '/usr/share/rhn') try: import up2date_client import up2date_client.config HAS_UP2DATE_CLIENT = True except ImportError: HAS_UP2DATE_CLIENT = False # INSERT REDHAT SNIPPETS from ansible_collections.community.general.plugins.module_utils import redhat from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.six.moves import urllib, xmlrpc_client class Rhn(redhat.RegistrationBase): def __init__(self, module=None, username=None, password=None): redhat.RegistrationBase.__init__(self, module, username, password) self.config = self.load_config() self.server = None self.session = None def logout(self): if self.session is not None: self.server.auth.logout(self.session) def load_config(self): ''' Read configuration from /etc/sysconfig/rhn/up2date ''' if not HAS_UP2DATE_CLIENT: return None config = up2date_client.config.initUp2dateConfig() return config @property def server_url(self): return self.config['serverURL'] @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 = urllib.parse.urlparse(self.server_url) 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: raise Exception('"libxml2" or "lxml" is required for this module.') # 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_server_url(self, server_url): ''' Configure server_url 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 ''' redhat.RegistrationBase.enable(self) self.update_plugin_conf('rhnplugin', True) self.update_plugin_conf('subscription-manager', False) def register(self, enable_eus=False, activationkey=None, profilename=None, sslcacert=None, systemorgid=None, nopackages=False): ''' Register system to RHN. If enable_eus=True, extended update support will be requested. ''' register_cmd = ['/usr/sbin/rhnreg_ks', '--force'] if self.username: register_cmd.extend(['--username', self.username, '--password', self.password]) if self.server_url: register_cmd.extend(['--serverUrl', self.server_url]) if enable_eus: register_cmd.append('--use-eus-channel') if nopackages: register_cmd.append('--nopackages') if activationkey is not None: register_cmd.extend(['--activationkey', activationkey]) if profilename is not None: register_cmd.extend(['--profilename', profilename]) if sslcacert is not None: register_cmd.extend(['--sslCACert', sslcacert]) if systemorgid is not None: register_cmd.extend(['--systemorgid', systemorgid]) rc, stdout, stderr = self.module.run_command(register_cmd, check_rc=True) def api(self, method, *args): ''' Convenience RPC wrapper ''' if self.server is None: if self.hostname != 'rhn.redhat.com': url = "https://%s/rpc/api" % self.hostname else: url = "https://xmlrpc.%s/rpc/api" % self.hostname self.server = xmlrpc_client.ServerProxy(url) 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 not channels: return if self._is_hosted(): 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, list(new_channels)) else: current_channels = self.api('channel.software.listSystemChannels', self.systemid) current_channels = [item['label'] for item in current_channels] new_base = None new_childs = [] for ch in channels: if ch in current_channels: continue if self.api('channel.software.getDetails', ch)['parent_channel_label'] == '': new_base = ch else: if ch not in new_childs: new_childs.append(ch) out_base = 0 out_childs = 0 if new_base: out_base = self.api('system.setBaseChannel', self.systemid, new_base) if new_childs: out_childs = self.api('system.setChildChannels', self.systemid, new_childs) return out_base and out_childs def _is_hosted(self): ''' Return True if we are running against Hosted (rhn.redhat.com) or False otherwise (when running against Satellite or Spacewalk) ''' return 'rhn.redhat.com' in self.hostname def main(): module = AnsibleModule( argument_spec=dict( state=dict(type='str', default='present', choices=['absent', 'present']), username=dict(type='str'), password=dict(type='str', no_log=True), server_url=dict(type='str'), activationkey=dict(type='str', no_log=True), profilename=dict(type='str'), ca_cert=dict(type='path', aliases=['sslcacert']), systemorgid=dict(type='str'), enable_eus=dict(type='bool', default=False), force=dict(type='bool', default=False), nopackages=dict(type='bool', default=False), channels=dict(type='list', elements='str', default=[]), ), # username/password is required for state=absent, or if channels is not empty # (basically anything that uses self.api requires username/password) but it doesn't # look like we can express that with required_if/required_together/mutually_exclusive # only username+password can be used for unregister required_if=[['state', 'absent', ['username', 'password']]], ) if not HAS_UP2DATE_CLIENT: module.fail_json(msg="Unable to import up2date_client. Is 'rhn-client-tools' installed?") server_url = module.params['server_url'] username = module.params['username'] password = module.params['password'] state = module.params['state'] force = module.params['force'] activationkey = module.params['activationkey'] profilename = module.params['profilename'] sslcacert = module.params['ca_cert'] systemorgid = module.params['systemorgid'] channels = module.params['channels'] enable_eus = module.params['enable_eus'] nopackages = module.params['nopackages'] rhn = Rhn(module=module, username=username, password=password) # use the provided server url and persist it to the rhn config. if server_url: rhn.configure_server_url(server_url) if not rhn.server_url: module.fail_json( msg="No serverURL was found (from either the 'server_url' module arg or the config file option 'serverURL' in /etc/sysconfig/rhn/up2date)" ) # 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 and not force: module.exit_json(changed=False, msg="System already registered.") try: rhn.enable() rhn.register(enable_eus, activationkey, profilename, sslcacert, systemorgid, nopackages) rhn.subscribe(channels) except Exception as exc: module.fail_json(msg="Failed to register with '%s': %s" % (rhn.hostname, exc)) finally: rhn.logout() 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.") if not (rhn.username and rhn.password): module.fail_json(msg="Missing arguments, the system is currently registered and unregistration requires a username and password") try: rhn.unregister() except Exception as exc: module.fail_json(msg="Failed to unregister: %s" % exc) finally: rhn.logout() module.exit_json(changed=True, msg="System successfully unregistered from %s." % rhn.hostname) if __name__ == '__main__': main()