# -*- coding: utf-8 -*- # Authors: # Florence Blanc-Renaud # # Copyright (C) 2017-2022 Red Hat # see file 'COPYING' for use and warranty information # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from __future__ import (absolute_import, division, print_function) __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.0', 'status': ['preview'], 'supported_by': 'community'} DOCUMENTATION = ''' --- module: ipaclient_get_otp short_description: Manage IPA hosts description: Manage hosts in a IPA domain. The operation needs to be authenticated with Kerberos either by providing a password or a keytab corresponding to a principal allowed to perform host operations. options: principal: description: User Principal allowed to promote replicas and join IPA realm type: str required: no default: admin ccache: description: The local ccache type: path required: no fqdn: description: The fully-qualified hostname of the host to add/modify/remove type: str required: yes certificates: description: A list of host certificates type: list elements: str required: no sshpubkey: description: The SSH public key for the host type: str required: no ipaddress: description: The IP address for the host type: str required: no random: description: Generate a random password to be used in bulk enrollment type: bool required: no default: no state: description: The desired host state type: str choices: ['present', 'absent'] default: present required: no author: - Florence Blanc-Renaud (@flo-renaud) ''' EXAMPLES = ''' # Example from Ansible Playbooks # Add a new host with a random OTP, authenticate using principal/password - ipaclient_get_otp: principal: admin password: MySecretPassword fqdn: ipaclient.ipa.domain.com ipaddress: 192.168.100.23 random: True register: result_ipaclient_get_otp ''' RETURN = ''' host: description: the host structure as returned from IPA API returned: always type: complex contains: dn: description: the DN of the host entry type: str returned: always fqdn: description: the fully qualified host name type: str returned: always has_keytab: description: whether the host entry contains a keytab type: bool returned: always has_password: description: whether the host entry contains a password type: bool returned: always managedby_host: description: the list of hosts managing the host type: list returned: always randompassword: description: the OneTimePassword generated for this host type: str returned: changed certificates: description: the list of host certificates type: list elements: str returned: when present sshpubkey: description: the SSH public key for the host type: str returned: when present ipaddress: description: the IP address for the host type: str returned: when present ''' import os from ansible.module_utils.basic import AnsibleModule from ansible.module_utils import six from ansible.module_utils.ansible_ipa_client import ( check_imports, api, errors, paths, run ) if six.PY3: unicode = str def get_host_diff(ipa_host, module_host): """ Build a dict with the differences from two host dicts. :param ipa_host: the host structure seen from IPA :param module_host: the target host structure seen from the module params :return: a dict representing the host attributes to apply """ non_updateable_keys = ['ip_address'] data = {} for key in non_updateable_keys: if key in module_host: del module_host[key] for key in module_host.keys(): ipa_value = ipa_host.get(key, None) module_value = module_host.get(key, None) if isinstance(ipa_value, list) and not isinstance(module_value, list): module_value = [module_value] if isinstance(ipa_value, list) and isinstance(module_value, list): ipa_value = sorted(ipa_value) module_value = sorted(module_value) if ipa_value != module_value: data[key] = unicode(module_value) return data def get_module_host(module): """ Create a structure representing the host information. Reads the module parameters and builds the host structure as expected from the module :param module: the ansible module :returns: a dict representing the host attributes """ data = {} certificates = module.params.get('certificates') if certificates: data['usercertificate'] = certificates sshpubkey = module.params.get('sshpubkey') if sshpubkey: data['ipasshpubkey'] = unicode(sshpubkey) ipaddress = module.params.get('ipaddress') if ipaddress: data['ip_address'] = unicode(ipaddress) random = module.params.get('random') if random: data['random'] = random return data def ensure_host_present(module, _api, ipahost): """ Ensure host exists in IPA and has the same attributes. :param module: the ansible module :param api: IPA api handle :param ipahost: the host information present in IPA, can be none if the host does not exist """ fqdn = unicode(module.params.get('fqdn')) if ipahost: # Host already present, need to compare the attributes module_host = get_module_host(module) diffs = get_host_diff(ipahost, module_host) if not diffs: # Same attributes, success module.exit_json(changed=False, host=ipahost) # Need to modify the host - only if not in check_mode if module.check_mode: module.exit_json(changed=True) # If we want to create a random password, and the host # already has Keytab: true, then we need first to run # ipa host-disable in order to remove OTP and keytab if module.params.get('random') and ipahost['has_keytab'] is True: _api.Command.host_disable(fqdn) result = _api.Command.host_mod(fqdn, **diffs) # Save random password as it is not displayed by host-show if module.params.get('random'): randompassword = result['result']['randompassword'] result = _api.Command.host_show(fqdn) if module.params.get('random'): result['result']['randompassword'] = randompassword module.exit_json(changed=True, host=result['result']) if not ipahost: # Need to add the user, only if not in check_mode if module.check_mode: module.exit_json(changed=True) # Must add the user module_host = get_module_host(module) # force creation of host even if there is no DNS record module_host["force"] = True result = _api.Command.host_add(fqdn, **module_host) # Save random password as it is not displayed by host-show if module.params.get('random'): randompassword = result['result']['randompassword'] result = _api.Command.host_show(fqdn) if module.params.get('random'): result['result']['randompassword'] = randompassword module.exit_json(changed=True, host=result['result']) def ensure_host_absent(module, _api, host): """ Ensure host does not exist in IPA. :param module: the ansible module :param api: the IPA API handle :param host: the host information present in IPA, can be none if the host does not exist """ if not host: # Nothing to do, host already removed module.exit_json(changed=False) # Need to remove the host - only if not in check_mode if module.check_mode: module.exit_json(changed=True, host=host) fqdn = unicode(module.params.get('fqdn')) try: _api.Command.host_del(fqdn) except Exception as e: module.fail_json(msg="Failed to remove host: %s" % e) module.exit_json(changed=True) def main(): module = AnsibleModule( argument_spec=dict( principal=dict(required=False, type='str', default='admin'), ccache=dict(required=False, type='path'), fqdn=dict(required=True, type='str'), certificates=dict(required=False, type='list', elements='str'), sshpubkey=dict(required=False, type='str'), ipaddress=dict(required=False, type='str'), random=dict(required=False, type='bool', default=False), state=dict(required=False, type='str', choices=['present', 'absent'], default='present'), ), supports_check_mode=True, ) check_imports(module) ccache = module.params.get('ccache') fqdn = unicode(module.params.get('fqdn')) state = module.params.get('state') try: os.environ['KRB5CCNAME'] = ccache cfg = dict( context='ansible_module', confdir=paths.ETC_IPA, in_server=False, debug=False, verbose=0, ) api.bootstrap(**cfg) api.finalize() api.Backend.rpcclient.connect() try: result = api.Command.host_show(fqdn, all=True) host = result['result'] except errors.NotFound: host = None if state in ['present', 'disabled']: ensure_host_present(module, api, host) elif state == 'absent': ensure_host_absent(module, api, host) except Exception as e: module.fail_json(msg="ipaclient_get_otp module failed : %s" % str(e)) finally: run([paths.KDESTROY], raiseonerr=False, env=os.environ) module.exit_json(changed=False, host=host) if __name__ == '__main__': main()