From 32fef233f23b3dcdecf3147a7d55cf7d2aa827aa Mon Sep 17 00:00:00 2001 From: Thomas Krahn Date: Wed, 9 Nov 2016 21:16:00 +0100 Subject: [PATCH] Add FreeIPA modules (#3247) * Add FreeIPA modules * Update version_added from 2.2 to 2.3 * ipa_*: Use Python 2.4 syntax to concatenate strings * ipa_*: Replace 'except Exception as e' with 'e = get_exception()' * ipa_*: import simplejson if json can't be imported * ipa_hbacrule: Fix: 'SyntaxError' on Python 2.4 * ipa_sudorule: Fix: 'SyntaxError' on Python 2.4 * ipa_*: Fix 'SyntaxError' on Python 2.4 * ipa_*: Import get_exception from ansible.module_utils.pycompat24 * Add FreeIPA modules * Update version_added from 2.2 to 2.3 * ipa_*: Fix 'SyntaxError' on Python 2.4 * ipa_*: Replace Python requests by ansible.module_utils.url * ipa_*: Replace Python requests by ansible.module_utils.url * ipa_*: Add option validate_certs * ipa_*: Remove requests from Ansible module documentation requirements * ipa_sudorule: Remove unnecessary empty line * ipa_sudorule: Remove markdown code from example * ipa_group: Add choices of state option * ipa_host: Rename options nshostlocation to ns_host_location, nshardwareplatform to ns_hardware_platform, nsosversion to ns_os_version, macaddress to mac_address and usercertificate to user_certificate and add aliases to be backward compatible --- .../modules/extras/identity/ipa/__init__.py | 0 .../modules/extras/identity/ipa/ipa_group.py | 384 ++++++++++++++ .../extras/identity/ipa/ipa_hbacrule.py | 479 +++++++++++++++++ .../modules/extras/identity/ipa/ipa_host.py | 378 ++++++++++++++ .../extras/identity/ipa/ipa_hostgroup.py | 343 ++++++++++++ .../modules/extras/identity/ipa/ipa_role.py | 411 +++++++++++++++ .../extras/identity/ipa/ipa_sudocmd.py | 275 ++++++++++ .../extras/identity/ipa/ipa_sudocmdgroup.py | 317 +++++++++++ .../extras/identity/ipa/ipa_sudorule.py | 491 ++++++++++++++++++ .../modules/extras/identity/ipa/ipa_user.py | 401 ++++++++++++++ 10 files changed, 3479 insertions(+) create mode 100644 lib/ansible/modules/extras/identity/ipa/__init__.py create mode 100644 lib/ansible/modules/extras/identity/ipa/ipa_group.py create mode 100644 lib/ansible/modules/extras/identity/ipa/ipa_hbacrule.py create mode 100644 lib/ansible/modules/extras/identity/ipa/ipa_host.py create mode 100644 lib/ansible/modules/extras/identity/ipa/ipa_hostgroup.py create mode 100644 lib/ansible/modules/extras/identity/ipa/ipa_role.py create mode 100644 lib/ansible/modules/extras/identity/ipa/ipa_sudocmd.py create mode 100644 lib/ansible/modules/extras/identity/ipa/ipa_sudocmdgroup.py create mode 100644 lib/ansible/modules/extras/identity/ipa/ipa_sudorule.py create mode 100644 lib/ansible/modules/extras/identity/ipa/ipa_user.py diff --git a/lib/ansible/modules/extras/identity/ipa/__init__.py b/lib/ansible/modules/extras/identity/ipa/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/extras/identity/ipa/ipa_group.py b/lib/ansible/modules/extras/identity/ipa/ipa_group.py new file mode 100644 index 00000000000..39ce80639d1 --- /dev/null +++ b/lib/ansible/modules/extras/identity/ipa/ipa_group.py @@ -0,0 +1,384 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_group +author: Thomas Krahn (@Nosmoht) +short_description: Manage FreeIPA group +description: +- Add, modify and delete group within IPA server +options: + cn: + description: + - Canonical name. + - Can not be changed as it is the unique identifier. + required: true + aliases: ['name'] + external: + description: + - Allow adding external non-IPA members from trusted domains. + required: false + gidnumber: + description: + - GID (use this option to set it manually). + required: false + group: + description: + - List of group names assigned to this group. + - If an empty list is passed all groups will be removed from this group. + - If option is omitted assigned groups will not be checked or changed. + - Groups that are already assigned but not passed will be removed. + nonposix: + description: + - Create as a non-POSIX group. + required: false + user: + description: + - List of user names assigned to this group. + - If an empty list is passed all users will be removed from this group. + - If option is omitted assigned users will not be checked or changed. + - Users that are already assigned but not passed will be removed. + state: + description: + - State to ensure + required: false + default: "present" + choices: ["present", "absent"] + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: "ipa.example.com" + ipa_user: + description: Administrative account used on IPA server + required: false + default: "admin" + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: "https" + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- json +''' + +EXAMPLES = ''' +# Ensure group is present +- ipa_group: + name: oinstall + gidnumber: 54321 + state: present + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure that groups sysops and appops are assigned to ops but no other group +- ipa_group: + name: ops + group: + - sysops + - appops + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure that users linus and larry are assign to the group, but no other user +- ipa_group: + name: sysops + user: + - linus + - larry + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure group is absent +- ipa_group: + name: sysops + state: absent + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +group: + description: Group as returned by IPA API + returned: always + type: dict +''' + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def group_find(self, name): + return self._post_json(method='group_find', name=None, item={'all': True, 'cn': name}) + + def group_add(self, name, item): + return self._post_json(method='group_add', name=name, item=item) + + def group_mod(self, name, item): + return self._post_json(method='group_mod', name=name, item=item) + + def group_del(self, name): + return self._post_json(method='group_del', name=name) + + def group_add_member(self, name, item): + return self._post_json(method='group_add_member', name=name, item=item) + + def group_add_member_group(self, name, item): + return self.group_add_member(name=name, item={'group': item}) + + def group_add_member_user(self, name, item): + return self.group_add_member(name=name, item={'user': item}) + + def group_remove_member(self, name, item): + return self._post_json(method='group_remove_member', name=name, item=item) + + def group_remove_member_group(self, name, item): + return self.group_remove_member(name=name, item={'group': item}) + + def group_remove_member_user(self, name, item): + return self.group_remove_member(name=name, item={'user': item}) + + +def get_group_dict(description=None, external=None, gid=None, nonposix=None): + group = {} + if description is not None: + group['description'] = description + if external is not None: + group['external'] = external + if gid is not None: + group['gidnumber'] = gid + if nonposix is not None: + group['nonposix'] = nonposix + return group + + +def get_group_diff(ipa_group, module_group): + data = [] + # With group_add attribute nonposix is passed, whereas with group_mod only posix can be passed. + if 'nonposix' in module_group: + # Only non-posix groups can be changed to posix + if not module_group['nonposix'] and ipa_group.get('nonposix'): + module_group['posix'] = True + del module_group['nonposix'] + + for key in module_group.keys(): + module_value = module_group.get(key, None) + ipa_value = ipa_group.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.append(key) + return data + + +def modify_if_diff(module, name, ipa_list, module_list, add_method, remove_method): + changed = False + diff = list(set(ipa_list) - set(module_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + remove_method(name=name, item=diff) + + diff = list(set(module_list) - set(ipa_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + add_method(name=name, item=diff) + + return changed + + +def ensure(module, client): + state = module.params['state'] + name = module.params['name'] + group = module.params['group'] + user = module.params['user'] + + module_group = get_group_dict(description=module.params['description'], external=module.params['external'], + gid=module.params['gidnumber'], nonposix=module.params['nonposix']) + ipa_group = client.group_find(name=name) + + changed = False + if state == 'present': + if not ipa_group: + changed = True + if not module.check_mode: + ipa_group = client.group_add(name, item=module_group) + else: + diff = get_group_diff(ipa_group, module_group) + if len(diff) > 0: + changed = True + if not module.check_mode: + data = {} + for key in diff: + data[key] = module_group.get(key) + client.group_mod(name=name, item=data) + + if group is not None: + changed = modify_if_diff(module, name, ipa_group.get('member_group', []), group, + client.group_add_member_group, + client.group_remove_member_group) or changed + + if user is not None: + changed = modify_if_diff(module, name, ipa_group.get('member_user', []), user, + client.group_add_member_user, + client.group_remove_member_user) or changed + + else: + if ipa_group: + changed = True + if not module.check_mode: + client.group_del(name) + + return changed, client.group_find(name=name) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + cn=dict(type='str', required=True, aliases=['name']), + description=dict(type='str', required=False), + external=dict(type='bool', required=False), + gidnumber=dict(type='str', required=False, aliases=['gid']), + group=dict(type='list', required=False), + nonposix=dict(type='bool', required=False), + state=dict(type='str', required=False, default='present', choices=['present', 'absent']), + user=dict(type='list', required=False), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, group = ensure(module, client) + module.exit_json(changed=changed, group=group) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/extras/identity/ipa/ipa_hbacrule.py b/lib/ansible/modules/extras/identity/ipa/ipa_hbacrule.py new file mode 100644 index 00000000000..5657ffb8efe --- /dev/null +++ b/lib/ansible/modules/extras/identity/ipa/ipa_hbacrule.py @@ -0,0 +1,479 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_hbacrule +author: Thomas Krahn (@Nosmoht) +short_description: Manage FreeIPA HBAC rule +description: +- Add, modify or delete an IPA HBAC rule using IPA API. +options: + cn: + description: + - Canonical name. + - Can not be changed as it is the unique identifier. + required: true + aliases: ["name"] + description: + description: Description + required: false + host: + description: + - List of host names to assign. + - If an empty list is passed all hosts will be removed from the rule. + - If option is omitted hosts will not be checked or changed. + required: false + hostcategory: + description: Host category + required: false + choices: ['all'] + hostgroup: + description: + - List of hostgroup names to assign. + - If an empty list is passed all hostgroups will be removed. from the rule + - If option is omitted hostgroups will not be checked or changed. + service: + description: + - List of service names to assign. + - If an empty list is passed all services will be removed from the rule. + - If option is omitted services will not be checked or changed. + servicecategory: + description: Service category + required: false + choices: ['all'] + servicegroup: + description: + - List of service group names to assign. + - If an empty list is passed all assigned service groups will be removed from the rule. + - If option is omitted service groups will not be checked or changed. + sourcehost: + description: + - List of source host names to assign. + - If an empty list if passed all assigned source hosts will be removed from the rule. + - If option is omitted source hosts will not be checked or changed. + sourcehostcategory: + description: Source host category + required: false + choices: ['all'] + sourcehostgroup: + description: + - List of source host group names to assign. + - If an empty list if passed all assigned source host groups will be removed from the rule. + - If option is omitted source host groups will not be checked or changed. + state: + description: State to ensure + required: false + default: "present" + choices: ["present", "absent", "enabled", "disabled"] + user: + description: + - List of user names to assign. + - If an empty list if passed all assigned users will be removed from the rule. + - If option is omitted users will not be checked or changed. + usercategory: + description: User category + required: false + choices: ['all'] + usergroup: + description: + - List of user group names to assign. + - If an empty list if passed all assigned user groups will be removed from the rule. + - If option is omitted user groups will not be checked or changed. + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: "ipa.example.com" + ipa_user: + description: Administrative account used on IPA server + required: false + default: "admin" + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: "https" + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- json +''' + +EXAMPLES = ''' +# Ensure rule to allow all users to access any host from any host +- ipa_hbacrule: + name: allow_all + description: Allow all users to access any host from any host + hostcategory: all + servicecategory: all + usercategory: all + state: present + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure rule with certain limitations +- ipa_hbacrule: + name: allow_all_developers_access_to_db + description: Allow all developers to access any database from any host + hostgroup: + - db-server + usergroup: + - developers + state: present + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure rule is absent +- ipa_hbacrule: + name: rule_to_be_deleted + state: absent + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +hbacrule: + description: HBAC rule as returned by IPA API. + returned: always + type: dict +''' + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def hbacrule_find(self, name): + return self._post_json(method='hbacrule_find', name=None, item={'all': True, 'cn': name}) + + def hbacrule_add(self, name, item): + return self._post_json(method='hbacrule_add', name=name, item=item) + + def hbacrule_mod(self, name, item): + return self._post_json(method='hbacrule_mod', name=name, item=item) + + def hbacrule_del(self, name): + return self._post_json(method='hbacrule_del', name=name) + + def hbacrule_add_host(self, name, item): + return self._post_json(method='hbacrule_add_host', name=name, item=item) + + def hbacrule_remove_host(self, name, item): + return self._post_json(method='hbacrule_remove_host', name=name, item=item) + + def hbacrule_add_service(self, name, item): + return self._post_json(method='hbacrule_add_service', name=name, item=item) + + def hbacrule_remove_service(self, name, item): + return self._post_json(method='hbacrule_remove_service', name=name, item=item) + + def hbacrule_add_user(self, name, item): + return self._post_json(method='hbacrule_add_user', name=name, item=item) + + def hbacrule_remove_user(self, name, item): + return self._post_json(method='hbacrule_remove_user', name=name, item=item) + + def hbacrule_add_sourcehost(self, name, item): + return self._post_json(method='hbacrule_add_sourcehost', name=name, item=item) + + def hbacrule_remove_sourcehost(self, name, item): + return self._post_json(method='hbacrule_remove_sourcehost', name=name, item=item) + + +def get_hbacrule_dict(description=None, hostcategory=None, ipaenabledflag=None, servicecategory=None, + sourcehostcategory=None, + usercategory=None): + data = {} + if description is not None: + data['description'] = description + if hostcategory is not None: + data['hostcategory'] = hostcategory + if ipaenabledflag is not None: + data['ipaenabledflag'] = ipaenabledflag + if servicecategory is not None: + data['servicecategory'] = servicecategory + if sourcehostcategory is not None: + data['sourcehostcategory'] = sourcehostcategory + if usercategory is not None: + data['usercategory'] = usercategory + return data + + +def get_hbcarule_diff(ipa_hbcarule, module_hbcarule): + data = [] + for key in module_hbcarule.keys(): + module_value = module_hbcarule.get(key, None) + ipa_value = ipa_hbcarule.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.append(key) + return data + + +def modify_if_diff(module, name, ipa_list, module_list, add_method, remove_method, item): + changed = False + diff = list(set(ipa_list) - set(module_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + remove_method(name=name, item={item: diff}) + + diff = list(set(module_list) - set(ipa_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + add_method(name=name, item={item: diff}) + + return changed + + +def ensure(module, client): + name = module.params['name'] + state = module.params['state'] + + if state in ['present', 'enabled']: + ipaenabledflag = 'TRUE' + else: + ipaenabledflag = 'NO' + + host = module.params['host'] + hostcategory = module.params['hostcategory'] + hostgroup = module.params['hostgroup'] + service = module.params['service'] + servicecategory = module.params['servicecategory'] + servicegroup = module.params['servicegroup'] + sourcehost = module.params['sourcehost'] + sourcehostcategory = module.params['sourcehostcategory'] + sourcehostgroup = module.params['sourcehostgroup'] + user = module.params['user'] + usercategory = module.params['usercategory'] + usergroup = module.params['usergroup'] + + module_hbacrule = get_hbacrule_dict(description=module.params['description'], + hostcategory=hostcategory, + ipaenabledflag=ipaenabledflag, + servicecategory=servicecategory, + sourcehostcategory=sourcehostcategory, + usercategory=usercategory) + ipa_hbacrule = client.hbacrule_find(name=name) + + changed = False + if state in ['present', 'enabled', 'disabled']: + if not ipa_hbacrule: + changed = True + if not module.check_mode: + ipa_hbacrule = client.hbacrule_add(name=name, item=module_hbacrule) + else: + diff = get_hbcarule_diff(ipa_hbacrule, module_hbacrule) + if len(diff) > 0: + changed = True + if not module.check_mode: + data = {} + for key in diff: + data[key] = module_hbacrule.get(key) + client.hbacrule_mod(name=name, item=data) + + if host is not None: + changed = modify_if_diff(module, name, ipa_hbacrule.get('memberhost_host', []), host, + client.hbacrule_add_host, + client.hbacrule_remove_host, 'host') or changed + + if hostgroup is not None: + changed = modify_if_diff(module, name, ipa_hbacrule.get('memberhost_hostgroup', []), hostgroup, + client.hbacrule_add_host, + client.hbacrule_remove_host, 'hostgroup') or changed + + if service is not None: + changed = modify_if_diff(module, name, ipa_hbacrule.get('memberservice_hbacsvc', []), service, + client.hbacrule_add_service, + client.hbacrule_remove_service, 'hbacsvc') or changed + + if servicegroup is not None: + changed = modify_if_diff(module, name, ipa_hbacrule.get('memberservice_hbacsvcgroup', []), + servicegroup, + client.hbacrule_add_service, + client.hbacrule_remove_service, 'hbacsvcgroup') or changed + + if sourcehost is not None: + changed = modify_if_diff(module, name, ipa_hbacrule.get('sourcehost_host', []), sourcehost, + client.hbacrule_add_sourcehost, + client.hbacrule_remove_sourcehost, 'host') or changed + + if sourcehostgroup is not None: + changed = modify_if_diff(module, name, ipa_hbacrule.get('sourcehost_group', []), sourcehostgroup, + client.hbacrule_add_sourcehost, + client.hbacrule_remove_sourcehost, 'hostgroup') or changed + + if user is not None: + changed = modify_if_diff(module, name, ipa_hbacrule.get('memberuser_user', []), user, + client.hbacrule_add_user, + client.hbacrule_remove_user, 'user') or changed + + if usergroup is not None: + changed = modify_if_diff(module, name, ipa_hbacrule.get('memberuser_group', []), usergroup, + client.hbacrule_add_user, + client.hbacrule_remove_user, 'group') or changed + else: + if ipa_hbacrule: + changed = True + if not module.check_mode: + client.hbacrule_del(name=name) + + return changed, client.hbacrule_find(name=name) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + cn=dict(type='str', required=True, aliases=['name']), + description=dict(type='str', required=False), + host=dict(type='list', required=False), + hostcategory=dict(type='str', required=False, choices=['all']), + hostgroup=dict(type='list', required=False), + service=dict(type='list', required=False), + servicecategory=dict(type='str', required=False, choices=['all']), + servicegroup=dict(type='list', required=False), + sourcehost=dict(type='list', required=False), + sourcehostcategory=dict(type='str', required=False, choices=['all']), + sourcehostgroup=dict(type='list', required=False), + state=dict(type='str', required=False, default='present', + choices=['present', 'absent', 'enabled', 'disabled']), + user=dict(type='list', required=False), + usercategory=dict(type='str', required=False, choices=['all']), + usergroup=dict(type='list', required=False), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, hbacrule = ensure(module, client) + module.exit_json(changed=changed, hbacrule=hbacrule) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/extras/identity/ipa/ipa_host.py b/lib/ansible/modules/extras/identity/ipa/ipa_host.py new file mode 100644 index 00000000000..1ca4113b9d6 --- /dev/null +++ b/lib/ansible/modules/extras/identity/ipa/ipa_host.py @@ -0,0 +1,378 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_host +short_description: Manage FreeIPA host +description: +- Add, modify and delete an IPA host using IPA API +options: + fqdn: + description: + - Full qualified domain name. + - Can not be changed as it is the unique identifier. + required: true + aliases: ["name"] + description: + description: + - A description of this host. + required: false + force: + description: + - Force host name even if not in DNS. + required: false + ip_address: + description: + - Add the host to DNS with this IP address. + required: false + mac_address: + description: + - List of Hardware MAC address(es) off this host. + - If option is omitted MAC addresses will not be checked or changed. + - If an empty list is passed all assigned MAC addresses will be removed. + - MAC addresses that are already assigned but not passed will be removed. + required: false + aliases: ["macaddress"] + ns_host_location: + description: + - Host location (e.g. "Lab 2") + required: false + aliases: ["nshostlocation"] + ns_hardware_platform: + description: + - Host hardware platform (e.g. "Lenovo T61") + required: false + aliases: ["nshardwareplatform"] + ns_os_version: + description: + - Host operating system and version (e.g. "Fedora 9") + required: false + aliases: ["nsosversion"] + user_certificate: + description: + - List of Base-64 encoded server certificates. + - If option is ommitted certificates will not be checked or changed. + - If an emtpy list is passed all assigned certificates will be removed. + - Certificates already assigned but not passed will be removed. + required: false + aliases: ["usercertificate"] + state: + description: State to ensure + required: false + default: present + choices: ["present", "absent", "disabled"] + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: ipa.example.com + ipa_user: + description: Administrative account used on IPA server + required: false + default: admin + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: https + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- json +''' + +EXAMPLES = ''' +# Ensure host is present +- ipa_host: + name: host01.example.com + description: Example host + ip_address: 192.168.0.123 + ns_host_location: Lab + ns_os_version: CentOS 7 + ns_hardware_platform: Lenovo T61 + mac_address: + - "08:00:27:E3:B1:2D" + - "52:54:00:BD:97:1E" + state: present + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure host is disabled +- ipa_host: + name: host01.example.com + state: disabled + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure that all user certificates are removed +- ipa_host: + name: host01.example.com + user_certificate: [] + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure host is absent +- ipa_host: + name: host01.example.com + state: absent + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +host: + description: Host as returned by IPA API. + returned: always + type: dict +host_diff: + description: List of options that differ and would be changed + returned: if check mode and a difference is found + type: list +''' + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def host_find(self, name): + return self._post_json(method='host_find', name=None, item={'all': True, 'fqdn': name}) + + def host_add(self, name, host): + return self._post_json(method='host_add', name=name, item=host) + + def host_mod(self, name, host): + return self._post_json(method='host_mod', name=name, item=host) + + def host_del(self, name): + return self._post_json(method='host_del', name=name) + + def host_disable(self, name): + return self._post_json(method='host_disable', name=name) + + +def get_host_dict(description=None, force=None, ip_address=None, ns_host_location=None, ns_hardware_platform=None, + ns_os_version=None, user_certificate=None, mac_address=None): + data = {} + if description is not None: + data['description'] = description + if force is not None: + data['force'] = force + if ip_address is not None: + data['ip_address'] = ip_address + if ns_host_location is not None: + data['nshostlocation'] = ns_host_location + if ns_hardware_platform is not None: + data['nshardwareplatform'] = ns_hardware_platform + if ns_os_version is not None: + data['nsosversion'] = ns_os_version + if user_certificate is not None: + data['usercertificate'] = [{"__base64__": item} for item in user_certificate] + if mac_address is not None: + data['macaddress'] = mac_address + return data + + +def get_host_diff(ipa_host, module_host): + non_updateable_keys = ['force', '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.append(key) + return data + + +def ensure(module, client): + name = module.params['name'] + state = module.params['state'] + + ipa_host = client.host_find(name=name) + module_host = get_host_dict(description=module.params['description'], + force=module.params['force'], ip_address=module.params['ip_address'], + ns_host_location=module.params['ns_host_location'], + ns_hardware_platform=module.params['ns_hardware_platform'], + ns_os_version=module.params['ns_os_version'], + user_certificate=module.params['user_certificate'], + mac_address=module.params['mac_address']) + changed = False + if state in ['present', 'enabled', 'disabled']: + if not ipa_host: + changed = True + if not module.check_mode: + client.host_add(name=name, host=module_host) + else: + diff = get_host_diff(ipa_host, module_host) + if len(diff) > 0: + changed = True + if not module.check_mode: + data = {} + for key in diff: + data[key] = module_host.get(key) + client.host_mod(name=name, host=data) + + else: + if ipa_host: + changed = True + if not module.check_mode: + client.host_del(name=name) + + return changed, client.host_find(name=name) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + description=dict(type='str', required=False), + fqdn=dict(type='str', required=True, aliases=['name']), + force=dict(type='bool', required=False), + ip_address=dict(type='str', required=False), + ns_host_location=dict(type='str', required=False, aliases=['nshostlocation']), + ns_hardware_platform=dict(type='str', required=False, aliases=['nshardwareplatform']), + ns_os_version=dict(type='str', required=False, aliases=['nsosversion']), + user_certificate=dict(type='list', required=False, aliases=['usercertificate']), + mac_address=dict(type='list', required=False, aliases=['macaddress']), + state=dict(type='str', required=False, default='present', + choices=['present', 'absent', 'enabled', 'disabled']), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, host = ensure(module, client) + module.exit_json(changed=changed, host=host) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/extras/identity/ipa/ipa_hostgroup.py b/lib/ansible/modules/extras/identity/ipa/ipa_hostgroup.py new file mode 100644 index 00000000000..50e66428805 --- /dev/null +++ b/lib/ansible/modules/extras/identity/ipa/ipa_hostgroup.py @@ -0,0 +1,343 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_hostgroup +short_description: Manage FreeIPA host-group +description: +- Add, modify and delete an IPA host-group using IPA API +options: + cn: + description: + - Name of host-group. + - Can not be changed as it is the unique identifier. + required: true + aliases: ["name"] + description: + description: + - Description + required: false + host: + description: + - List of hosts that belong to the host-group. + - If an empty list is passed all hosts will be removed from the group. + - If option is omitted hosts will not be checked or changed. + - If option is passed all assigned hosts that are not passed will be unassigned from the group. + required: false + hostgroup: + description: + - List of host-groups than belong to that host-group. + - If an empty list is passed all host-groups will be removed from the group. + - If option is omitted host-groups will not be checked or changed. + - If option is passed all assigned hostgroups that are not passed will be unassigned from the group. + required: false + state: + description: + - State to ensure. + required: false + default: "present" + choices: ["present", "absent"] + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: "ipa.example.com" + ipa_user: + description: Administrative account used on IPA server + required: false + default: "admin" + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: "https" + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- json +''' + +EXAMPLES = ''' +# Ensure host-group databases is present +- ipa_hostgroup: + name: databases + state: present + host: + - db.example.com + hostgroup: + - mysql-server + - oracle-server + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure host-group databases is absent +- ipa_hostgroup: + name: databases + state: absent + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +hostgroup: + description: Hostgroup as returned by IPA API. + returned: always + type: dict +''' + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def hostgroup_find(self, name): + return self._post_json(method='hostgroup_find', name=None, item={'all': True, 'cn': name}) + + def hostgroup_add(self, name, item): + return self._post_json(method='hostgroup_add', name=name, item=item) + + def hostgroup_mod(self, name, item): + return self._post_json(method='hostgroup_mod', name=name, item=item) + + def hostgroup_del(self, name): + return self._post_json(method='hostgroup_del', name=name) + + def hostgroup_add_member(self, name, item): + return self._post_json(method='hostgroup_add_member', name=name, item=item) + + def hostgroup_add_host(self, name, item): + return self.hostgroup_add_member(name=name, item={'host': item}) + + def hostgroup_add_hostgroup(self, name, item): + return self.hostgroup_add_member(name=name, item={'hostgroup': item}) + + def hostgroup_remove_member(self, name, item): + return self._post_json(method='hostgroup_remove_member', name=name, item=item) + + def hostgroup_remove_host(self, name, item): + return self.hostgroup_remove_member(name=name, item={'host': item}) + + def hostgroup_remove_hostgroup(self, name, item): + return self.hostgroup_remove_member(name=name, item={'hostgroup': item}) + + +def get_hostgroup_dict(description=None): + data = {} + if description is not None: + data['description'] = description + return data + + +def get_hostgroup_diff(ipa_hostgroup, module_hostgroup): + data = [] + for key in module_hostgroup.keys(): + ipa_value = ipa_hostgroup.get(key, None) + module_value = module_hostgroup.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.append(key) + return data + + +def modify_if_diff(module, name, ipa_list, module_list, add_method, remove_method): + changed = False + diff = list(set(ipa_list) - set(module_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + remove_method(name=name, item=diff) + + diff = list(set(module_list) - set(ipa_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + add_method(name=name, item=diff) + return changed + + +def ensure(module, client): + name = module.params['name'] + state = module.params['state'] + host = module.params['host'] + hostgroup = module.params['hostgroup'] + + ipa_hostgroup = client.hostgroup_find(name=name) + module_hostgroup = get_hostgroup_dict(description=module.params['description']) + + changed = False + if state == 'present': + if not ipa_hostgroup: + changed = True + if not module.check_mode: + ipa_hostgroup = client.hostgroup_add(name=name, item=module_hostgroup) + else: + diff = get_hostgroup_diff(ipa_hostgroup, module_hostgroup) + if len(diff) > 0: + changed = True + if not module.check_mode: + data = {} + for key in diff: + data[key] = module_hostgroup.get(key) + client.hostgroup_mod(name=name, item=data) + + if host is not None: + changed = modify_if_diff(module, name, ipa_hostgroup.get('member_host', []), host, + client.hostgroup_add_host, client.hostgroup_remove_host) or changed + + if hostgroup is not None: + changed = modify_if_diff(module, name, ipa_hostgroup.get('member_hostgroup', []), hostgroup, + client.hostgroup_add_hostgroup, client.hostgroup_remove_hostgroup) or changed + + else: + if ipa_hostgroup: + changed = True + if not module.check_mode: + client.hostgroup_del(name=name) + + return changed, client.hostgroup_find(name=name) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + cn=dict(type='str', required=True, aliases=['name']), + description=dict(type='str', required=False), + host=dict(type='list', required=False), + hostgroup=dict(type='list', required=False), + state=dict(type='str', required=False, default='present', + choices=['present', 'absent', 'enabled', 'disabled']), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, hostgroup = ensure(module, client) + module.exit_json(changed=changed, hostgroup=hostgroup) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/extras/identity/ipa/ipa_role.py b/lib/ansible/modules/extras/identity/ipa/ipa_role.py new file mode 100644 index 00000000000..48508a256ae --- /dev/null +++ b/lib/ansible/modules/extras/identity/ipa/ipa_role.py @@ -0,0 +1,411 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_role +short_description: Manage FreeIPA role +description: +- Add, modify and delete a role within FreeIPA server using FreeIPA API +options: + cn: + description: + - Role name. + - Can not be changed as it is the unique identifier. + required: true + aliases: ['name'] + description: + description: + - A description of this role-group. + required: false + group: + description: + - List of group names assign to this role. + - If an empty list is passed all assigned groups will be unassigned from the role. + - If option is omitted groups will not be checked or changed. + - If option is passed all assigned groups that are not passed will be unassigned from the role. + host: + description: + - List of host names to assign. + - If an empty list is passed all assigned hosts will be unassigned from the role. + - If option is omitted hosts will not be checked or changed. + - If option is passed all assigned hosts that are not passed will be unassigned from the role. + required: false + hostgroup: + description: + - List of host group names to assign. + - If an empty list is passed all assigned host groups will be removed from the role. + - If option is omitted host groups will not be checked or changed. + - If option is passed all assigned hostgroups that are not passed will be unassigned from the role. + required: false + service: + description: + - List of service names to assign. + - If an empty list is passed all assigned services will be removed from the role. + - If option is omitted services will not be checked or changed. + - If option is passed all assigned services that are not passed will be removed from the role. + required: false + state: + description: State to ensure + required: false + default: "present" + choices: ["present", "absent"] + user: + description: + - List of user names to assign. + - If an empty list is passed all assigned users will be removed from the role. + - If option is omitted users will not be checked or changed. + required: false + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: "ipa.example.com" + ipa_user: + description: Administrative account used on IPA server + required: false + default: "admin" + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: "https" + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- json +''' + +EXAMPLES = ''' +# Ensure role is present +- ipa_role: + name: dba + description: Database Administrators + state: present + user: + - pinky + - brain + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure role with certain details +- ipa_role: + name: another-role + description: Just another role + group: + - editors + host: + - host01.example.com + hostgroup: + - hostgroup01 + service: + - service01 + +# Ensure role is absent +- ipa_role: + name: dba + state: absent + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +role: + description: Role as returned by IPA API. + returned: always + type: dict +''' + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def role_find(self, name): + return self._post_json(method='role_find', name=None, item={'all': True, 'cn': name}) + + def role_add(self, name, item): + return self._post_json(method='role_add', name=name, item=item) + + def role_mod(self, name, item): + return self._post_json(method='role_mod', name=name, item=item) + + def role_del(self, name): + return self._post_json(method='role_del', name=name) + + def role_add_member(self, name, item): + return self._post_json(method='role_add_member', name=name, item=item) + + def role_add_group(self, name, item): + return self.role_add_member(name=name, item={'group': item}) + + def role_add_host(self, name, item): + return self.role_add_member(name=name, item={'host': item}) + + def role_add_hostgroup(self, name, item): + return self.role_add_member(name=name, item={'hostgroup': item}) + + def role_add_service(self, name, item): + return self.role_add_member(name=name, item={'service': item}) + + def role_add_user(self, name, item): + return self.role_add_member(name=name, item={'user': item}) + + def role_remove_member(self, name, item): + return self._post_json(method='role_remove_member', name=name, item=item) + + def role_remove_group(self, name, item): + return self.role_remove_member(name=name, item={'group': item}) + + def role_remove_host(self, name, item): + return self.role_remove_member(name=name, item={'host': item}) + + def role_remove_hostgroup(self, name, item): + return self.role_remove_member(name=name, item={'hostgroup': item}) + + def role_remove_service(self, name, item): + return self.role_remove_member(name=name, item={'service': item}) + + def role_remove_user(self, name, item): + return self.role_remove_member(name=name, item={'user': item}) + + +def get_role_dict(description=None): + data = {} + if description is not None: + data['description'] = description + return data + + +def get_role_diff(ipa_role, module_role): + data = [] + for key in module_role.keys(): + module_value = module_role.get(key, None) + ipa_value = ipa_role.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.append(key) + return data + + +def modify_if_diff(module, name, ipa_list, module_list, add_method, remove_method): + changed = False + diff = list(set(ipa_list) - set(module_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + remove_method(name=name, item=diff) + + diff = list(set(module_list) - set(ipa_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + add_method(name=name, item=diff) + return changed + + +def ensure(module, client): + state = module.params['state'] + name = module.params['name'] + group = module.params['group'] + host = module.params['host'] + hostgroup = module.params['hostgroup'] + service = module.params['service'] + user = module.params['user'] + + module_role = get_role_dict(description=module.params['description']) + ipa_role = client.role_find(name=name) + + changed = False + if state == 'present': + if not ipa_role: + changed = True + if not module.check_mode: + ipa_role = client.role_add(name=name, item=module_role) + else: + diff = get_role_diff(ipa_role=ipa_role, module_role=module_role) + if len(diff) > 0: + changed = True + if not module.check_mode: + data = {} + for key in diff: + data[key] = module_role.get(key) + client.role_mod(name=name, item=data) + + if group is not None: + changed = modify_if_diff(module, name, ipa_role.get('member_group', []), group, + client.role_add_group, + client.role_remove_group) or changed + + if host is not None: + changed = modify_if_diff(module, name, ipa_role.get('member_host', []), host, + client.role_add_host, + client.role_remove_host) or changed + + if hostgroup is not None: + changed = modify_if_diff(module, name, ipa_role.get('member_hostgroup', []), hostgroup, + client.role_add_hostgroup, + client.role_remove_hostgroup) or changed + + if service is not None: + changed = modify_if_diff(module, name, ipa_role.get('member_service', []), service, + client.role_add_service, + client.role_remove_service) or changed + if user is not None: + changed = modify_if_diff(module, name, ipa_role.get('member_user', []), user, + client.role_add_user, + client.role_remove_user) or changed + else: + if ipa_role: + changed = True + if not module.check_mode: + client.role_del(name) + + return changed, client.role_find(name=name) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + cn=dict(type='str', required=True, aliases=['name']), + description=dict(type='str', required=False), + group=dict(type='list', required=False), + host=dict(type='list', required=False), + hostgroup=dict(type='list', required=False), + service=dict(type='list', required=False), + state=dict(type='str', required=False, default='present', choices=['present', 'absent']), + user=dict(type='list', required=False), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, role = ensure(module, client) + module.exit_json(changed=changed, role=role) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/extras/identity/ipa/ipa_sudocmd.py b/lib/ansible/modules/extras/identity/ipa/ipa_sudocmd.py new file mode 100644 index 00000000000..5b9dbec5fde --- /dev/null +++ b/lib/ansible/modules/extras/identity/ipa/ipa_sudocmd.py @@ -0,0 +1,275 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_sudocmd +author: Thomas Krahn (@Nosmoht) +short_description: Manage FreeIPA sudo command +description: +- Add, modify or delete sudo command within FreeIPA server using FreeIPA API. +options: + sudocmd: + description: + - Sudo Command. + aliases: ['name'] + required: true + description: + description: + - A description of this command. + required: false + state: + description: State to ensure + required: false + default: present + choices: ['present', 'absent'] + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: "ipa.example.com" + ipa_user: + description: Administrative account used on IPA server + required: false + default: "admin" + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: "https" + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- json +''' + +EXAMPLES = ''' +# Ensure sudo command exists +- ipa_sudocmd: + name: su + description: Allow to run su via sudo + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure sudo command does not exist +- ipa_sudocmd: + name: su + state: absent + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +sudocmd: + description: Sudo command as return from IPA API + returned: always + type: dict +''' + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def sudocmd_find(self, name): + return self._post_json(method='sudocmd_find', name=None, item={'all': True, 'sudocmd': name}) + + def sudocmd_add(self, name, item): + return self._post_json(method='sudocmd_add', name=name, item=item) + + def sudocmd_mod(self, name, item): + return self._post_json(method='sudocmd_mod', name=name, item=item) + + def sudocmd_del(self, name): + return self._post_json(method='sudocmd_del', name=name) + + +def get_sudocmd_dict(description=None): + data = {} + if description is not None: + data['description'] = description + return data + + +def get_sudocmd_diff(ipa_sudocmd, module_sudocmd): + data = [] + for key in module_sudocmd.keys(): + module_value = module_sudocmd.get(key, None) + ipa_value = ipa_sudocmd.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.append(key) + return data + + +def ensure(module, client): + name = module.params['sudocmd'] + state = module.params['state'] + + module_sudocmd = get_sudocmd_dict(description=module.params['description']) + ipa_sudocmd = client.sudocmd_find(name=name) + + changed = False + if state == 'present': + if not ipa_sudocmd: + changed = True + if not module.check_mode: + client.sudocmd_add(name=name, item=module_sudocmd) + else: + diff = get_sudocmd_diff(ipa_sudocmd, module_sudocmd) + if len(diff) > 0: + changed = True + if not module.check_mode: + data = {} + for key in diff: + data[key] = module_sudocmd.get(key) + client.sudocmd_mod(name=name, item=data) + else: + if ipa_sudocmd: + changed = True + if not module.check_mode: + client.sudocmd_del(name=name) + + return changed, client.sudocmd_find(name=name) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + description=dict(type='str', required=False), + state=dict(type='str', required=False, default='present', + choices=['present', 'absent', 'enabled', 'disabled']), + sudocmd=dict(type='str', required=True, aliases=['name']), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, sudocmd = ensure(module, client) + module.exit_json(changed=changed, sudocmd=sudocmd) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/extras/identity/ipa/ipa_sudocmdgroup.py b/lib/ansible/modules/extras/identity/ipa/ipa_sudocmdgroup.py new file mode 100644 index 00000000000..182bf82806a --- /dev/null +++ b/lib/ansible/modules/extras/identity/ipa/ipa_sudocmdgroup.py @@ -0,0 +1,317 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_sudocmdgroup +author: Thomas Krahn (@Nosmoht) +short_description: Manage FreeIPA sudo command group +description: +- Add, modify or delete sudo command group within IPA server using IPA API. +options: + cn: + description: + - Sudo Command Group. + aliases: ['name'] + required: true + description: + description: + - Group description. + state: + description: State to ensure + required: false + default: present + choices: ['present', 'absent'] + sudocmd: + description: + - List of sudo commands to assign to the group. + - If an empty list is passed all assigned commands will be removed from the group. + - If option is omitted sudo commands will not be checked or changed. + required: false + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: "ipa.example.com" + ipa_user: + description: Administrative account used on IPA server + required: false + default: "admin" + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: "https" + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- json +''' + +EXAMPLES = ''' +- name: Ensure sudo command group exists + ipa_sudocmdgroup: + name: group01 + description: Group of important commands + sudocmd: + - su + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +- name: Ensure sudo command group does not exists + ipa_sudocmdgroup: + name: group01 + state: absent + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +sudocmdgroup: + description: Sudo command group as returned by IPA API + returned: always + type: dict +''' + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def sudocmdgroup_find(self, name): + return self._post_json(method='sudocmdgroup_find', name=None, item={'all': True, 'cn': name}) + + def sudocmdgroup_add(self, name, item): + return self._post_json(method='sudocmdgroup_add', name=name, item=item) + + def sudocmdgroup_mod(self, name, item): + return self._post_json(method='sudocmdgroup_mod', name=name, item=item) + + def sudocmdgroup_del(self, name): + return self._post_json(method='sudocmdgroup_del', name=name) + + def sudocmdgroup_add_member(self, name, item): + return self._post_json(method='sudocmdgroup_add_member', name=name, item=item) + + def sudocmdgroup_add_member_sudocmd(self, name, item): + return self.sudocmdgroup_add_member(name=name, item={'sudocmd': item}) + + def sudocmdgroup_remove_member(self, name, item): + return self._post_json(method='sudocmdgroup_remove_member', name=name, item=item) + + def sudocmdgroup_remove_member_sudocmd(self, name, item): + return self.sudocmdgroup_remove_member(name=name, item={'sudocmd': item}) + + +def get_sudocmdgroup_dict(description=None): + data = {} + if description is not None: + data['description'] = description + return data + + +def modify_if_diff(module, name, ipa_list, module_list, add_method, remove_method): + changed = False + diff = list(set(ipa_list) - set(module_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + remove_method(name=name, item=diff) + + diff = list(set(module_list) - set(ipa_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + add_method(name=name, item=diff) + return changed + + +def get_sudocmdgroup_diff(ipa_sudocmdgroup, module_sudocmdgroup): + data = [] + for key in module_sudocmdgroup.keys(): + module_value = module_sudocmdgroup.get(key, None) + ipa_value = ipa_sudocmdgroup.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.append(key) + return data + + +def ensure(module, client): + name = module.params['name'] + state = module.params['state'] + sudocmd = module.params['sudocmd'] + + module_sudocmdgroup = get_sudocmdgroup_dict(description=module.params['description']) + ipa_sudocmdgroup = client.sudocmdgroup_find(name=name) + + changed = False + if state == 'present': + if not ipa_sudocmdgroup: + changed = True + if not module.check_mode: + ipa_sudocmdgroup = client.sudocmdgroup_add(name=name, item=module_sudocmdgroup) + else: + diff = get_sudocmdgroup_diff(ipa_sudocmdgroup, module_sudocmdgroup) + if len(diff) > 0: + changed = True + if not module.check_mode: + data = {} + for key in diff: + data[key] = module_sudocmdgroup.get(key) + client.sudocmdgroup_mod(name=name, item=data) + + if sudocmd is not None: + changed = modify_if_diff(module, name, ipa_sudocmdgroup.get('member_sudocmd', []), sudocmd, + client.sudocmdgroup_add_member_sudocmd, + client.sudocmdgroup_remove_member_sudocmd) + else: + if ipa_sudocmdgroup: + changed = True + if not module.check_mode: + client.sudocmdgroup_del(name=name) + + return changed, client.sudocmdgroup_find(name=name) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + cn=dict(type='str', required=True, aliases=['name']), + description=dict(type='str', required=False), + state=dict(type='str', required=False, default='present', + choices=['present', 'absent', 'enabled', 'disabled']), + sudocmd=dict(type='list', required=False), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, sudocmdgroup = ensure(module, client) + module.exit_json(changed=changed, sudorule=sudocmdgroup) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/extras/identity/ipa/ipa_sudorule.py b/lib/ansible/modules/extras/identity/ipa/ipa_sudorule.py new file mode 100644 index 00000000000..162baad5d8c --- /dev/null +++ b/lib/ansible/modules/extras/identity/ipa/ipa_sudorule.py @@ -0,0 +1,491 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_sudorule +author: Thomas Krahn (@Nosmoht) +short_description: Manage FreeIPA sudo rule +description: +- Add, modify or delete sudo rule within IPA server using IPA API. +options: + cn: + description: + - Canonical name. + - Can not be changed as it is the unique identifier. + required: true + aliases: ['name'] + cmdcategory: + description: + - Command category the rule applies to. + choices: ['all'] + required: false + cmd: + description: + - List of commands assigned to the rule. + - If an empty list is passed all commands will be removed from the rule. + - If option is omitted commands will not be checked or changed. + required: false + host: + description: + - List of hosts assigned to the rule. + - If an empty list is passed all hosts will be removed from the rule. + - If option is omitted hosts will not be checked or changed. + - Option C(hostcategory) must be omitted to assign hosts. + required: false + hostcategory: + description: + - Host category the rule applies to. + - If 'all' is passed one must omit C(host) and C(hostgroup). + - Option C(host) and C(hostgroup) must be omitted to assign 'all'. + choices: ['all'] + required: false + hostgroup: + description: + - List of host groups assigned to the rule. + - If an empty list is passed all host groups will be removed from the rule. + - If option is omitted host groups will not be checked or changed. + - Option C(hostcategory) must be omitted to assign host groups. + required: false + user: + description: + - List of users assigned to the rule. + - If an empty list is passed all users will be removed from the rule. + - If option is omitted users will not be checked or changed. + required: false + usercategory: + description: + - User category the rule applies to. + choices: ['all'] + required: false + usergroup: + description: + - List of user groups assigned to the rule. + - If an empty list is passed all user groups will be removed from the rule. + - If option is omitted user groups will not be checked or changed. + required: false + state: + description: State to ensure + required: false + default: present + choices: ['present', 'absent', 'enabled', 'disabled'] + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: "ipa.example.com" + ipa_user: + description: Administrative account used on IPA server + required: false + default: "admin" + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: "https" + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- json +''' + +EXAMPLES = ''' +# Ensure sudo rule is present thats allows all every body to execute any command on any host without beeing asked for a password. +- ipa_sudorule: + name: sudo_all_nopasswd + cmdcategory: all + description: Allow to run every command with sudo without password + hostcategory: all + sudoopt: + - '!authenticate' + usercategory: all + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +# Ensure user group developers can run every command on host group db-server as well as on host db01.example.com. +- ipa_sudorule: + name: sudo_dev_dbserver + description: Allow developers to run every command with sudo on all database server + cmdcategory: all + host: + - db01.example.com + hostgroup: + - db-server + sudoopt: + - '!authenticate' + usergroup: + - developers + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +sudorule: + description: Sudorule as returned by IPA + returned: always + type: dict +''' + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def sudorule_find(self, name): + return self._post_json(method='sudorule_find', name=None, item={'all': True, 'cn': name}) + + def sudorule_add(self, name, item): + return self._post_json(method='sudorule_add', name=name, item=item) + + def sudorule_mod(self, name, item): + return self._post_json(method='sudorule_mod', name=name, item=item) + + def sudorule_del(self, name): + return self._post_json(method='sudorule_del', name=name) + + def sudorule_add_option(self, name, item): + return self._post_json(method='sudorule_add_option', name=name, item=item) + + def sudorule_add_option_ipasudoopt(self, name, item): + return self.sudorule_add_option(name=name, item={'ipasudoopt': item}) + + def sudorule_remove_option(self, name, item): + return self._post_json(method='sudorule_remove_option', name=name, item=item) + + def sudorule_remove_option_ipasudoopt(self, name, item): + return self.sudorule_remove_option(name=name, item={'ipasudoopt': item}) + + def sudorule_add_host(self, name, item): + return self._post_json(method='sudorule_add_host', name=name, item=item) + + def sudorule_add_host_host(self, name, item): + return self.sudorule_add_host(name=name, item={'host': item}) + + def sudorule_add_host_hostgroup(self, name, item): + return self.sudorule_add_host(name=name, item={'hostgroup': item}) + + def sudorule_remove_host(self, name, item): + return self._post_json(method='sudorule_remove_host', name=name, item=item) + + def sudorule_remove_host_host(self, name, item): + return self.sudorule_remove_host(name=name, item={'host': item}) + + def sudorule_remove_host_hostgroup(self, name, item): + return self.sudorule_remove_host(name=name, item={'hostgroup': item}) + + def sudorule_add_allow_command(self, name, item): + return self._post_json(method='sudorule_add_allow_command', name=name, item=item) + + def sudorule_remove_allow_command(self, name, item): + return self._post_json(method='sudorule_remove_allow_command', name=name, item=item) + + def sudorule_add_user(self, name, item): + return self._post_json(method='sudorule_add_user', name=name, item=item) + + def sudorule_add_user_user(self, name, item): + return self.sudorule_add_user(name=name, item={'user': item}) + + def sudorule_add_user_group(self, name, item): + return self.sudorule_add_user(name=name, item={'group': item}) + + def sudorule_remove_user(self, name, item): + return self._post_json(method='sudorule_remove_user', name=name, item=item) + + def sudorule_remove_user_user(self, name, item): + return self.sudorule_remove_user(name=name, item={'user': item}) + + def sudorule_remove_user_group(self, name, item): + return self.sudorule_remove_user(name=name, item={'group': item}) + + +def get_sudorule_dict(cmdcategory=None, description=None, hostcategory=None, ipaenabledflag=None, usercategory=None): + data = {} + if cmdcategory is not None: + data['cmdcategory'] = cmdcategory + if description is not None: + data['description'] = description + if hostcategory is not None: + data['hostcategory'] = hostcategory + if ipaenabledflag is not None: + data['ipaenabledflag'] = ipaenabledflag + if usercategory is not None: + data['usercategory'] = usercategory + return data + + +def get_sudorule_diff(ipa_sudorule, module_sudorule): + data = [] + for key in module_sudorule.keys(): + module_value = module_sudorule.get(key, None) + ipa_value = ipa_sudorule.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.append(key) + return data + + +def modify_if_diff(module, name, ipa_list, module_list, add_method, remove_method): + changed = False + diff = list(set(ipa_list) - set(module_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + for item in diff: + remove_method(name=name, item=item) + + diff = list(set(module_list) - set(ipa_list)) + if len(diff) > 0: + changed = True + if not module.check_mode: + for item in diff: + add_method(name=name, item=item) + + return changed + + +def category_changed(module, client, category_name, ipa_sudorule): + if ipa_sudorule.get(category_name, None) == ['all']: + if not module.check_mode: + client.sudorule_mod(name=ipa_sudorule.get('cn'), item={category_name: None}) + return True + return False + + +def ensure(module, client): + state = module.params['state'] + name = module.params['name'] + cmd = module.params['cmd'] + cmdcategory = module.params['cmdcategory'] + host = module.params['host'] + hostcategory = module.params['hostcategory'] + hostgroup = module.params['hostgroup'] + + if state in ['present', 'enabled']: + ipaenabledflag = 'TRUE' + else: + ipaenabledflag = 'NO' + + sudoopt = module.params['sudoopt'] + user = module.params['user'] + usercategory = module.params['usercategory'] + usergroup = module.params['usergroup'] + + module_sudorule = get_sudorule_dict(cmdcategory=cmdcategory, + description=module.params['description'], + hostcategory=hostcategory, + ipaenabledflag=ipaenabledflag, + usercategory=usercategory) + ipa_sudorule = client.sudorule_find(name=name) + + changed = False + if state in ['present', 'disabled', 'enabled']: + if not ipa_sudorule: + changed = True + if not module.check_mode: + ipa_sudorule = client.sudorule_add(name=name, item=module_sudorule) + else: + diff = get_sudorule_diff(ipa_sudorule, module_sudorule) + if len(diff) > 0: + changed = True + if not module.check_mode: + if 'hostcategory' in diff: + if ipa_sudorule.get('memberhost_host', None) is not None: + client.sudorule_remove_host_host(name=name, item=ipa_sudorule.get('memberhost_host')) + if ipa_sudorule.get('memberhost_hostgroup', None) is not None: + client.sudorule_remove_host_hostgroup(name=name, + item=ipa_sudorule.get('memberhost_hostgroup')) + + client.sudorule_mod(name=name, item=module_sudorule) + + if cmd is not None: + changed = category_changed(module, client, 'cmdcategory', ipa_sudorule) or changed + if not module.check_mode: + client.sudorule_add_allow_command(name=name, item=cmd) + + if host is not None: + changed = category_changed(module, client, 'hostcategory', ipa_sudorule) or changed + changed = modify_if_diff(module, name, ipa_sudorule.get('memberhost_host', []), host, + client.sudorule_add_host_host, + client.sudorule_remove_host_host) or changed + + if hostgroup is not None: + changed = category_changed(module, client, 'hostcategory', ipa_sudorule) or changed + changed = modify_if_diff(module, name, ipa_sudorule.get('memberhost_hostgroup', []), hostgroup, + client.sudorule_add_host_hostgroup, + client.sudorule_remove_host_hostgroup) or changed + if sudoopt is not None: + changed = modify_if_diff(module, name, ipa_sudorule.get('ipasudoopt', []), sudoopt, + client.sudorule_add_option_ipasudoopt, + client.sudorule_remove_option_ipasudoopt) or changed + if user is not None: + changed = category_changed(module, client, 'usercategory', ipa_sudorule) or changed + changed = modify_if_diff(module, name, ipa_sudorule.get('memberuser_user', []), user, + client.sudorule_add_user_user, + client.sudorule_remove_user_user) or changed + if usergroup is not None: + changed = category_changed(module, client, 'usercategory', ipa_sudorule) or changed + changed = modify_if_diff(module, name, ipa_sudorule.get('memberuser_group', []), usergroup, + client.sudorule_add_user_group, + client.sudorule_remove_user_group) or changed + else: + if ipa_sudorule: + changed = True + if not module.check_mode: + client.sudorule_del(name) + + return changed, client.sudorule_find(name) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + cmd=dict(type='list', required=False), + cmdcategory=dict(type='str', required=False, choices=['all']), + cn=dict(type='str', required=True, aliases=['name']), + description=dict(type='str', required=False), + host=dict(type='list', required=False), + hostcategory=dict(type='str', required=False, choices=['all']), + hostgroup=dict(type='list', required=False), + sudoopt=dict(type='list', required=False), + state=dict(type='str', required=False, default='present', + choices=['present', 'absent', 'enabled', 'disabled']), + user=dict(type='list', required=False), + usercategory=dict(type='str', required=False, choices=['all']), + usergroup=dict(type='list', required=False), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + mutually_exclusive=[['cmdcategory', 'cmd'], + ['hostcategory', 'host'], + ['hostcategory', 'hostgroup'], + ['usercategory', 'user'], + ['usercategory', 'usergroup']], + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, sudorule = ensure(module, client) + module.exit_json(changed=changed, sudorule=sudorule) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/extras/identity/ipa/ipa_user.py b/lib/ansible/modules/extras/identity/ipa/ipa_user.py new file mode 100644 index 00000000000..8a5c4b09168 --- /dev/null +++ b/lib/ansible/modules/extras/identity/ipa/ipa_user.py @@ -0,0 +1,401 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: ipa_user +short_description: Manage FreeIPA users +description: +- Add, modify and delete user within IPA server +options: + displayname: + description: Display name + required: false + givenname: + description: First name + required: false + loginshell: + description: Login shell + required: false + mail: + description: + - List of mail addresses assigned to the user. + - If an empty list is passed all assigned email addresses will be deleted. + - If None is passed email addresses will not be checked or changed. + required: false + password: + description: + - Password + required: false + sn: + description: Surname + required: false + sshpubkey: + description: + - List of public SSH key. + - If an empty list is passed all assigned public keys will be deleted. + - If None is passed SSH public keys will not be checked or changed. + required: false + state: + description: State to ensure + required: false + default: "present" + choices: ["present", "absent", "enabled", "disabled"] + telephonenumber: + description: + - List of telephone numbers assigned to the user. + - If an empty list is passed all assigned telephone numbers will be deleted. + - If None is passed telephone numbers will not be checked or changed. + required: false + title: + description: Title + required: false + uid: + description: uid of the user + required: true + aliases: ["name"] + ipa_port: + description: Port of IPA server + required: false + default: 443 + ipa_host: + description: IP or hostname of IPA server + required: false + default: "ipa.example.com" + ipa_user: + description: Administrative account used on IPA server + required: false + default: "admin" + ipa_pass: + description: Password of administrative user + required: true + ipa_prot: + description: Protocol used by IPA server + required: false + default: "https" + choices: ["http", "https"] + validate_certs: + description: + - This only applies if C(ipa_prot) is I(https). + - If set to C(no), the SSL certificates will not be validated. + - This should only set to C(no) used on personally controlled sites using self-signed certificates. + required: false + default: true +version_added: "2.3" +requirements: +- base64 +- hashlib +- json +''' + +EXAMPLES = ''' +# Ensure pinky is present +- ipa_user: + name: pinky + state: present + givenname: Pinky + sn: Acme + mail: + - pinky@acme.com + telephonenumber: + - '+555123456' + sshpubkeyfp: + - ssh-rsa .... + - ssh-dsa .... + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret + +# Ensure brain is absent +- ipa_user: + name: brain + state: absent + ipa_host: ipa.example.com + ipa_user: admin + ipa_pass: topsecret +''' + +RETURN = ''' +user: + description: User as returned by IPA API + returned: always + type: dict +''' + +import base64 +import hashlib + +try: + import json +except ImportError: + import simplejson as json + + +class IPAClient: + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (username, password) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=data, headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['body']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().getheader('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=json.dumps(data), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['body']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + resp = json.loads(resp.read()) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + return result + return None + + def user_find(self, name): + return self._post_json(method='user_find', name=None, item={'all': True, 'uid': name}) + + def user_add(self, name, item): + return self._post_json(method='user_add', name=name, item=item) + + def user_mod(self, name, item): + return self._post_json(method='user_mod', name=name, item=item) + + def user_del(self, name): + return self._post_json(method='user_del', name=name) + + def user_disable(self, name): + return self._post_json(method='user_disable', name=name) + + def user_enable(self, name): + return self._post_json(method='user_enable', name=name) + + +def get_user_dict(givenname=None, loginshell=None, mail=None, nsaccountlock=False, sn=None, sshpubkey=None, + telephonenumber=None, + title=None): + user = {} + if givenname is not None: + user['givenname'] = givenname + if loginshell is not None: + user['loginshell'] = loginshell + if mail is not None: + user['mail'] = mail + user['nsaccountlock'] = nsaccountlock + if sn is not None: + user['sn'] = sn + if sshpubkey is not None: + user['ipasshpubkey'] = sshpubkey + if telephonenumber is not None: + user['telephonenumber'] = telephonenumber + if title is not None: + user['title'] = title + + return user + + +def get_user_diff(ipa_user, module_user): + """ + Return the keys of each dict whereas values are different. Unfortunately the IPA + API returns everything as a list even if only a single value is possible. + Therefore some more complexity is needed. + The method will check if the value type of module_user.attr is not a list and + create a list with that element if the same attribute in ipa_user is list. In this way i hope that the method + must not be changed if the returned API dict is changed. + :param ipa_user: + :param module_user: + :return: + """ + # return [item for item in module_user.keys() if module_user.get(item, None) != ipa_user.get(item, None)] + result = [] + # sshpubkeyfp is the list of ssh key fingerprints. IPA doesn't return the keys itself but instead the fingerprints. + # These are used for comparison. + sshpubkey = None + if 'ipasshpubkey' in module_user: + module_user['sshpubkeyfp'] = [get_ssh_key_fingerprint(pubkey) for pubkey in module_user['ipasshpubkey']] + # Remove the ipasshpubkey element as it is not returned from IPA but save it's value to be used later on + sshpubkey = module_user['ipasshpubkey'] + del module_user['ipasshpubkey'] + for key in module_user.keys(): + mod_value = module_user.get(key, None) + ipa_value = ipa_user.get(key, None) + if isinstance(ipa_value, list) and not isinstance(mod_value, list): + mod_value = [mod_value] + if isinstance(ipa_value, list) and isinstance(mod_value, list): + mod_value = sorted(mod_value) + ipa_value = sorted(ipa_value) + if mod_value != ipa_value: + result.append(key) + # If there are public keys, remove the fingerprints and add them back to the dict + if sshpubkey is not None: + del module_user['sshpubkeyfp'] + module_user['ipasshpubkey'] = sshpubkey + return result + + +def get_ssh_key_fingerprint(ssh_key): + """ + Return the public key fingerprint of a given public SSH key + in format "FB:0C:AC:0A:07:94:5B:CE:75:6E:63:32:13:AD:AD:D7 (ssh-rsa)" + :param ssh_key: + :return: + """ + parts = ssh_key.strip().split() + if len(parts) == 0: + return None + key_type = parts[0] + key = base64.b64decode(parts[1].encode('ascii')) + + fp_plain = hashlib.md5(key).hexdigest() + return ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2])).upper() + ' (%s)' % key_type + + +def ensure(module, client): + state = module.params['state'] + name = module.params['name'] + nsaccountlock = state == 'disabled' + + module_user = get_user_dict(givenname=module.params.get('givenname'), loginshell=module.params['loginshell'], + mail=module.params['mail'], sn=module.params['sn'], + sshpubkey=module.params['sshpubkey'], nsaccountlock=nsaccountlock, + telephonenumber=module.params['telephonenumber'], title=module.params['title']) + + ipa_user = client.user_find(name=name) + + changed = False + if state in ['present', 'enabled', 'disabled']: + if not ipa_user: + changed = True + if not module.check_mode: + ipa_user = client.user_add(name=name, item=module_user) + else: + diff = get_user_diff(ipa_user, module_user) + if len(diff) > 0: + changed = True + if not module.check_mode: + ipa_user = client.user_mod(name=name, item=module_user) + else: + if ipa_user: + changed = True + if not module.check_mode: + client.user_del(name) + + return changed, ipa_user + + +def main(): + module = AnsibleModule( + argument_spec=dict( + displayname=dict(type='str', required=False), + givenname=dict(type='str', required=False), + loginshell=dict(type='str', required=False), + mail=dict(type='list', required=False), + sn=dict(type='str', required=False), + uid=dict(type='str', required=True, aliases=['name']), + password=dict(type='str', required=False, no_log=True), + sshpubkey=dict(type='list', required=False), + state=dict(type='str', required=False, default='present', + choices=['present', 'absent', 'enabled', 'disabled']), + telephonenumber=dict(type='list', required=False), + title=dict(type='str', required=False), + ipa_prot=dict(type='str', required=False, default='https', choices=['http', 'https']), + ipa_host=dict(type='str', required=False, default='ipa.example.com'), + ipa_port=dict(type='int', required=False, default=443), + ipa_user=dict(type='str', required=False, default='admin'), + ipa_pass=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', required=False, default=True), + ), + supports_check_mode=True, + ) + + client = IPAClient(module=module, + host=module.params['ipa_host'], + port=module.params['ipa_port'], + protocol=module.params['ipa_prot']) + + # If sshpubkey is defined as None than module.params['sshpubkey'] is [None]. IPA itself returns None (not a list). + # Therefore a small check here to replace list(None) by None. Otherwise get_user_diff() would return sshpubkey + # as different which should be avoided. + if module.params['sshpubkey'] is not None: + if len(module.params['sshpubkey']) == 1 and module.params['sshpubkey'][0] is "": + module.params['sshpubkey'] = None + + try: + client.login(username=module.params['ipa_user'], + password=module.params['ipa_pass']) + changed, user = ensure(module, client) + module.exit_json(changed=changed, user=user) + except Exception: + e = get_exception() + module.fail_json(msg=str(e)) + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + +if __name__ == '__main__': + main()