1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) James Laska
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10ANSIBLE_METADATA = {'metadata_version': '1.1',
11                    'status': ['preview'],
12                    'supported_by': 'community'}
13
14DOCUMENTATION = r'''
15---
16module: rhn_register
17short_description: Manage Red Hat Network registration using the C(rhnreg_ks) command
18description:
19    - Manage registration to the Red Hat Network.
20version_added: "1.2"
21author:
22- James Laska (@jlaska)
23notes:
24    - This is for older Red Hat products. You probably want the M(redhat_subscription) module instead.
25    - In order to register a system, C(rhnreg_ks) requires either a username and password, or an activationkey.
26requirements:
27    - rhnreg_ks
28    - either libxml2 or lxml
29options:
30    state:
31        description:
32          - Whether to register (C(present)), or unregister (C(absent)) a system.
33        type: str
34        choices: [ absent, present ]
35        default: present
36    username:
37        description:
38            - Red Hat Network username.
39        type: str
40    password:
41        description:
42            - Red Hat Network password.
43        type: str
44    server_url:
45        description:
46            - Specify an alternative Red Hat Network server URL.
47            - The default is the current value of I(serverURL) from C(/etc/sysconfig/rhn/up2date).
48        type: str
49    activationkey:
50        description:
51            - Supply an activation key for use with registration.
52        type: str
53    profilename:
54        description:
55            - Supply an profilename for use with registration.
56        type: str
57        version_added: "2.0"
58    ca_cert:
59        description:
60            - Supply a custom ssl CA certificate file for use with registration.
61        type: path
62        version_added: "2.1"
63        aliases: [ sslcacert ]
64    systemorgid:
65        description:
66            - Supply an organizational id for use with registration.
67        type: str
68        version_added: "2.1"
69    channels:
70        description:
71            - Optionally specify a list of channels to subscribe to upon successful registration.
72        type: list
73        default: []
74    enable_eus:
75        description:
76            - If C(no), extended update support will be requested.
77        type: bool
78        default: no
79    nopackages:
80        description:
81            - If C(yes), the registered node will not upload its installed packages information to Satellite server.
82        type: bool
83        default: no
84        version_added: "2.5"
85'''
86
87EXAMPLES = r'''
88- name: Unregister system from RHN
89  rhn_register:
90    state: absent
91    username: joe_user
92    password: somepass
93
94- name: Register as user with password and auto-subscribe to available content
95  rhn_register:
96    state: present
97    username: joe_user
98    password: somepass
99
100- name: Register with activationkey and enable extended update support
101  rhn_register:
102    state: present
103    activationkey: 1-222333444
104    enable_eus: yes
105
106- name: Register with activationkey and set a profilename which may differ from the hostname
107  rhn_register:
108    state: present
109    activationkey: 1-222333444
110    profilename: host.example.com.custom
111
112- name: Register as user with password against a satellite server
113  rhn_register:
114    state: present
115    username: joe_user
116    password: somepass
117    server_url: https://xmlrpc.my.satellite/XMLRPC
118
119- name: Register as user with password and enable channels
120  rhn_register:
121    state: present
122    username: joe_user
123    password: somepass
124    channels: rhel-x86_64-server-6-foo-1,rhel-x86_64-server-6-bar-1
125'''
126
127RETURN = r'''
128# Default return values
129'''
130
131import os
132import sys
133
134# Attempt to import rhn client tools
135sys.path.insert(0, '/usr/share/rhn')
136try:
137    import up2date_client
138    import up2date_client.config
139    HAS_UP2DATE_CLIENT = True
140except ImportError:
141    HAS_UP2DATE_CLIENT = False
142
143# INSERT REDHAT SNIPPETS
144from ansible.module_utils import redhat
145from ansible.module_utils.basic import AnsibleModule
146from ansible.module_utils.six.moves import urllib, xmlrpc_client
147
148
149class Rhn(redhat.RegistrationBase):
150
151    def __init__(self, module=None, username=None, password=None):
152        redhat.RegistrationBase.__init__(self, module, username, password)
153        self.config = self.load_config()
154        self.server = None
155        self.session = None
156
157    def logout(self):
158        if self.session is not None:
159            self.server.auth.logout(self.session)
160
161    def load_config(self):
162        '''
163            Read configuration from /etc/sysconfig/rhn/up2date
164        '''
165        if not HAS_UP2DATE_CLIENT:
166            return None
167
168        config = up2date_client.config.initUp2dateConfig()
169
170        return config
171
172    @property
173    def server_url(self):
174        return self.config['serverURL']
175
176    @property
177    def hostname(self):
178        '''
179            Return the non-xmlrpc RHN hostname.  This is a convenience method
180            used for displaying a more readable RHN hostname.
181
182            Returns: str
183        '''
184        url = urllib.parse.urlparse(self.server_url)
185        return url[1].replace('xmlrpc.', '')
186
187    @property
188    def systemid(self):
189        systemid = None
190        xpath_str = "//member[name='system_id']/value/string"
191
192        if os.path.isfile(self.config['systemIdPath']):
193            fd = open(self.config['systemIdPath'], 'r')
194            xml_data = fd.read()
195            fd.close()
196
197            # Ugh, xml parsing time ...
198            # First, try parsing with libxml2 ...
199            if systemid is None:
200                try:
201                    import libxml2
202                    doc = libxml2.parseDoc(xml_data)
203                    ctxt = doc.xpathNewContext()
204                    systemid = ctxt.xpathEval(xpath_str)[0].content
205                    doc.freeDoc()
206                    ctxt.xpathFreeContext()
207                except ImportError:
208                    pass
209
210            # m-kay, let's try with lxml now ...
211            if systemid is None:
212                try:
213                    from lxml import etree
214                    root = etree.fromstring(xml_data)
215                    systemid = root.xpath(xpath_str)[0].text
216                except ImportError:
217                    raise Exception('"libxml2" or "lxml" is required for this module.')
218
219            # Strip the 'ID-' prefix
220            if systemid is not None and systemid.startswith('ID-'):
221                systemid = systemid[3:]
222
223        return int(systemid)
224
225    @property
226    def is_registered(self):
227        '''
228            Determine whether the current system is registered.
229
230            Returns: True|False
231        '''
232        return os.path.isfile(self.config['systemIdPath'])
233
234    def configure_server_url(self, server_url):
235        '''
236            Configure server_url for registration
237        '''
238
239        self.config.set('serverURL', server_url)
240        self.config.save()
241
242    def enable(self):
243        '''
244            Prepare the system for RHN registration.  This includes ...
245             * enabling the rhnplugin yum plugin
246             * disabling the subscription-manager yum plugin
247        '''
248        redhat.RegistrationBase.enable(self)
249        self.update_plugin_conf('rhnplugin', True)
250        self.update_plugin_conf('subscription-manager', False)
251
252    def register(self, enable_eus=False, activationkey=None, profilename=None, sslcacert=None, systemorgid=None, nopackages=False):
253        '''
254            Register system to RHN.  If enable_eus=True, extended update
255            support will be requested.
256        '''
257        register_cmd = ['/usr/sbin/rhnreg_ks', '--force']
258        if self.username:
259            register_cmd.extend(['--username', self.username, '--password', self.password])
260        if self.server_url:
261            register_cmd.extend(['--serverUrl', self.server_url])
262        if enable_eus:
263            register_cmd.append('--use-eus-channel')
264        if nopackages:
265            register_cmd.append('--nopackages')
266        if activationkey is not None:
267            register_cmd.extend(['--activationkey', activationkey])
268        if profilename is not None:
269            register_cmd.extend(['--profilename', profilename])
270        if sslcacert is not None:
271            register_cmd.extend(['--sslCACert', sslcacert])
272        if systemorgid is not None:
273            register_cmd.extend(['--systemorgid', systemorgid])
274        rc, stdout, stderr = self.module.run_command(register_cmd, check_rc=True)
275
276    def api(self, method, *args):
277        '''
278            Convenience RPC wrapper
279        '''
280        if self.server is None:
281            if self.hostname != 'rhn.redhat.com':
282                url = "https://%s/rpc/api" % self.hostname
283            else:
284                url = "https://xmlrpc.%s/rpc/api" % self.hostname
285            self.server = xmlrpc_client.ServerProxy(url)
286            self.session = self.server.auth.login(self.username, self.password)
287
288        func = getattr(self.server, method)
289        return func(self.session, *args)
290
291    def unregister(self):
292        '''
293            Unregister a previously registered system
294        '''
295
296        # Initiate RPC connection
297        self.api('system.deleteSystems', [self.systemid])
298
299        # Remove systemid file
300        os.unlink(self.config['systemIdPath'])
301
302    def subscribe(self, channels):
303        if not channels:
304            return
305
306        if self._is_hosted():
307            current_channels = self.api('channel.software.listSystemChannels', self.systemid)
308            new_channels = [item['channel_label'] for item in current_channels]
309            new_channels.extend(channels)
310            return self.api('channel.software.setSystemChannels', self.systemid, list(new_channels))
311
312        else:
313            current_channels = self.api('channel.software.listSystemChannels', self.systemid)
314            current_channels = [item['label'] for item in current_channels]
315            new_base = None
316            new_childs = []
317            for ch in channels:
318                if ch in current_channels:
319                    continue
320                if self.api('channel.software.getDetails', ch)['parent_channel_label'] == '':
321                    new_base = ch
322                else:
323                    if ch not in new_childs:
324                        new_childs.append(ch)
325            out_base = 0
326            out_childs = 0
327
328            if new_base:
329                out_base = self.api('system.setBaseChannel', self.systemid, new_base)
330
331            if new_childs:
332                out_childs = self.api('system.setChildChannels', self.systemid, new_childs)
333
334            return out_base and out_childs
335
336    def _is_hosted(self):
337        '''
338            Return True if we are running against Hosted (rhn.redhat.com) or
339            False otherwise (when running against Satellite or Spacewalk)
340        '''
341        return 'rhn.redhat.com' in self.hostname
342
343
344def main():
345
346    module = AnsibleModule(
347        argument_spec=dict(
348            state=dict(type='str', default='present', choices=['absent', 'present']),
349            username=dict(type='str'),
350            password=dict(type='str', no_log=True),
351            server_url=dict(type='str'),
352            activationkey=dict(type='str', no_log=True),
353            profilename=dict(type='str'),
354            ca_cert=dict(type='path', aliases=['sslcacert']),
355            systemorgid=dict(type='str'),
356            enable_eus=dict(type='bool', default=False),
357            nopackages=dict(type='bool', default=False),
358            channels=dict(type='list', default=[]),
359        ),
360        # username/password is required for state=absent, or if channels is not empty
361        # (basically anything that uses self.api requires username/password) but it doesn't
362        # look like we can express that with required_if/required_together/mutually_exclusive
363
364        # only username+password can be used for unregister
365        required_if=[['state', 'absent', ['username', 'password']]],
366    )
367
368    if not HAS_UP2DATE_CLIENT:
369        module.fail_json(msg="Unable to import up2date_client.  Is 'rhn-client-tools' installed?")
370
371    server_url = module.params['server_url']
372    username = module.params['username']
373    password = module.params['password']
374
375    state = module.params['state']
376    activationkey = module.params['activationkey']
377    profilename = module.params['profilename']
378    sslcacert = module.params['ca_cert']
379    systemorgid = module.params['systemorgid']
380    channels = module.params['channels']
381    enable_eus = module.params['enable_eus']
382    nopackages = module.params['nopackages']
383
384    rhn = Rhn(module=module, username=username, password=password)
385
386    # use the provided server url and persist it to the rhn config.
387    if server_url:
388        rhn.configure_server_url(server_url)
389
390    if not rhn.server_url:
391        module.fail_json(
392            msg="No serverURL was found (from either the 'server_url' module arg or the config file option 'serverURL' in /etc/sysconfig/rhn/up2date)"
393        )
394
395    # Ensure system is registered
396    if state == 'present':
397
398        # Check for missing parameters ...
399        if not (activationkey or rhn.username or rhn.password):
400            module.fail_json(msg="Missing arguments, must supply an activationkey (%s) or username (%s) and password (%s)" % (activationkey, rhn.username,
401                                                                                                                              rhn.password))
402        if not activationkey and not (rhn.username and rhn.password):
403            module.fail_json(msg="Missing arguments, If registering without an activationkey, must supply username or password")
404
405        # Register system
406        if rhn.is_registered:
407            module.exit_json(changed=False, msg="System already registered.")
408
409        try:
410            rhn.enable()
411            rhn.register(enable_eus, activationkey, profilename, sslcacert, systemorgid, nopackages)
412            rhn.subscribe(channels)
413        except Exception as exc:
414            module.fail_json(msg="Failed to register with '%s': %s" % (rhn.hostname, exc))
415        finally:
416            rhn.logout()
417
418        module.exit_json(changed=True, msg="System successfully registered to '%s'." % rhn.hostname)
419
420    # Ensure system is *not* registered
421    if state == 'absent':
422        if not rhn.is_registered:
423            module.exit_json(changed=False, msg="System already unregistered.")
424
425        if not (rhn.username and rhn.password):
426            module.fail_json(msg="Missing arguments, the system is currently registered and unregistration requires a username and password")
427
428        try:
429            rhn.unregister()
430        except Exception as exc:
431            module.fail_json(msg="Failed to unregister: %s" % exc)
432        finally:
433            rhn.logout()
434
435        module.exit_json(changed=True, msg="System successfully unregistered from %s." % rhn.hostname)
436
437
438if __name__ == '__main__':
439    main()
440