From 56b1368441fce0f711ec12254aa0945450f122d6 Mon Sep 17 00:00:00 2001 From: chrisp <chris@chrisprocter.co.uk> Date: Thu, 19 Dec 2019 20:57:24 +0000 Subject: [PATCH] There is a new config management module placed in the plugins folder: plugins/modules/ipaconfig.py The config module allows the user change global config settings. The config module is as compatible as possible to the Ansible upstream ipa_config module, but adds many extra variables. Here is the documentation for the module: README-config.md --- README-config.md | 145 +++++++++++ plugins/modules/ipaconfig.py | 460 +++++++++++++++++++++++++++++++++++ tests/config/test_config.yml | 143 +++++++++++ 3 files changed, 748 insertions(+) create mode 100644 README-config.md create mode 100644 plugins/modules/ipaconfig.py create mode 100644 tests/config/test_config.yml diff --git a/README-config.md b/README-config.md new file mode 100644 index 00000000..608df548 --- /dev/null +++ b/README-config.md @@ -0,0 +1,145 @@ +Config module +=========== + +Description +----------- + +The config module allows the setting of global config parameters within IPA. If no parameters are specified it returns the list of all current parameters. + +The config module is as compatible as possible to the Ansible upstream `ipa_config` module, but adds many additional parameters + + +Features +-------- +* IPA server configuration management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipaconfig module. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +Example playbook to read config options: + +```yaml +--- +- name: Playbook to handle global config options + hosts: ipaserver + become: true + tasks: + - name: return current values of the global configuration options + ipaconfig: + ipaadmin_password: password + register: result + - name: display default login shell + debug: + msg: '{{result.config.defaultlogin }}' + + - name: ensure defaultloginshell and maxusernamelength are set as required + ipaconfig: + ipaadmin_password: password + defaultlogin: /bin/bash + maxusername: 64 +``` + +```yaml +--- +- name: Playbook to ensure some config options are set + hosts: ipaserver + become: true + tasks: + - name: set defaultlogin and maxusername + ipaconfig: + ipaadmin_password: password + defaultlogin: /bin/bash + maxusername: 64 +``` + + +Variables +========= + +ipauser +------- + +**General Variables:** + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`maxusername` \| `ipamaxusernamelength` | Set the maximum username length (1 to 255) | false +`homedirectory` \| `ipahomesrootdir` | Set the default location of home directories | false +`defaultshell` \| `ipadefaultloginshell` | Set the default shell for new users | false +`defaultgroup` \| `ipadefaultprimarygroup` | Set the default group for new users | false +`emaildomain`\| `ipadefaultemaildomain` | Set the default e-mail domain | false +`searchtimelimit` \| `ipasearchtimelimit` | Set maximum amount of time (seconds) for a search -1 to 2147483647 (-1 or 0 is unlimited) | false +`searchrecordslimit` \| `ipasearchrecordslimit` | Set maximum number of records to search -1 to 2147483647 (-1 or 0 is unlimited) | false +`usersearch` \| `ipausersearchfields` | Set list of fields to search when searching for users | false +`groupsearch` \| `ipagroupsearchfields` | Set list of fields to search in when searching for groups | false +`enable_migration` \| `ipamigrationenabled` | Enable migration mode (choices: True, False ) | false +`groupobjectclasses` \| `ipagroupobjectclasses` | Set default group objectclasses (list) | false +`userobjectclasses` \| `ipauserobjectclasses` | Set default user objectclasses (list) | false +`pwdexpnotify` \| `ipapwdexpadvnotify` | Set number of days's notice of impending password expiration (0 to 2147483647) | false +`configstring` \| `ipaconfigstring` | Set extra hashes to generate in password plug-in (choices:`AllowNThash`, `KDC:Disable Last Success`, `KDC:Disable Lockout`, `KDC:Disable Default Preauth for SPNs`) | false +`selinuxusermaporder` \| `ipaselinuxusermaporder`| Set ordered list in increasing priority of SELinux users | false +`selinuxusermapdefault`\| `ipaselinuxusermapdefault` | Set default SELinux user when no match is found in SELinux map rule | false +`pac_type` \| `ipakrbauthzdata` | set default types of PAC supported for services (choices: `MS-PAC`, `PAD`, `nfs:NONE`) +`user_auth_type` \| `ipauserauthtype` | set default types of supported user authentication (choices: `password`, `radius`, `otp`, `disabled`) | false +`domain_resolution_order` \| `ipadomainresolutionorder` | Set list of domains used for short name qualification | false + + +Return Values +============= + +Variable | Description | Returned When +-------- | ----------- | ------------- +`config` | config dict <br />Fields: | No values to configure are specified + | `homedirectory` | + | `defaultshell` | + | `defaultgroup` | + | `emaildomain` | + | `searchtimelimit` | + | `searchrecordslimit` | + | `usersearch` | + | `groupsearch` | + | `enable_migration` | + | `groupobjectclasses` | + | `userobjectclasses` | + | `pwdexpnotify` | + | `configstring` | + | `selinuxusermaporder` | + | `selinuxusermapdefault` | + | `pac_type` | + | `user_auth_type` | + | `domain_resolution_order` | + + +All returned fields take the same form as their namesake input parameters + +Authors +======= + +Chris Procter diff --git a/plugins/modules/ipaconfig.py b/plugins/modules/ipaconfig.py new file mode 100644 index 00000000..d4383adc --- /dev/null +++ b/plugins/modules/ipaconfig.py @@ -0,0 +1,460 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Chris Procter <cprocter@redhat.com> +# +# Copyright (C) 2020 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + + +DOCUMENTATION = ''' +--- +module: ipa_config +author: chris procter +short_description: Modify IPA global config options +description: +- Modify IPA global config options +options: + ipaadmin_principal: + description: The admin principal + default: admin + ipaadmin_password: + description: The admin password + required: false + maxusername: + description: Set the maximum username length between 1-255 + required: false + aliases: ['ipamaxusernamelength'] + homedirectory: + description: Set the default location of home directories + required: false + aliases: ['ipahomesrootdir'] + defaultshell: + description: Set the default shell for new users + required: false + aliases: ['ipadefaultloginshell', 'loginshell'] + defaultgroup: + description: Set the default group for new users + required: false + aliases: ['ipadefaultprimarygroup'] + emaildomain: + description: Set the default e-mail domain + required: false + aliases: ['ipadefaultemaildomain'] + searchtimelimit: + description: + - Set maximum amount of time (seconds) for a search + - values -1 to 2147483647 (-1 or 0 is unlimited) + required: false + aliases: ['ipasearchtimelimit'] + searchrecordslimit: + description: + - Set maximum number of records to search + - values -1 to 2147483647 (-1 or 0 is unlimited) + required: false + aliases: ['ipasearchrecordslimit'] + usersearch: + description: + - Set comma-separated list of fields to search for user search + required: false + aliases: ['ipausersearchfields'] + groupsearch: + description: + - Set comma-separated list of fields to search for group search + required: false + aliases: ['ipagroupsearchfields'] + enable_migration: + description: Enable migration mode + type: bool + required: false + aliases: ['enable-migration','ipamigrationenabled'] + groupobjectclasses: + description: Set default group objectclasses (comma-separated list) + required: false + type: list + aliases: ['ipagroupobjectclasses'] + userobjectclasses: + description: Set default user objectclasses (comma-separated list) + required: false + type: list + aliases: ['ipauserobjectclasses'] + pwdexpnotify: + description: + - Set number of days's notice of impending password expiration + - values 0 to 2147483647 + required: false + aliases: ['ipapwdexpadvnotify'] + configstring: + description: Set extra hashes to generate in password plug-in + required: false + type: list + choices: + - "AllowNThash" + - "KDC:Disable Last Success" + - "KDC:Disable Lockout" + - "KDC:Disable Default Preauth for SPNs" + aliases: ['ipaconfigstring'] + selinuxusermaporder: + description: Set order in increasing priority of SELinux users + required: false + type: list + aliases: ['ipaselinuxusermaporder'] + selinuxusermapdefault: + description: Set default SELinux user when no match found in map rule + required: false + aliases: ['ipaselinuxusermapdefault'] + pac_type: + description: set default types of PAC supported for services + required: false + type: list + choices: ["MS-PAC", "PAD", "nfs:NONE"] + aliases: ["pac-type","ipakrbauthzdata"] + user_auth_type: + description: set default types of supported user authentication + required: false + type: list + choices: ["password", "radius", "otp", "disabled"] + aliases: ["user-auth_type","user-auth-type","ipauserauthtype"] + domain_resolution_order: + description: set list of domains used for short name qualification + required: false + type: list + aliases: ["domain-resolution_order", + "domain-resolution-order", + "ipadomainresolutionorder"] +''' + +EXAMPLES = ''' +--- +- name: Playbook to handle global configuration options + hosts: ipaserver + become: true + tasks: + - name: return current values of the global configuration options + ipaconfig: + ipaadmin_password: password + register: result + - name: display default login shell + debug: + msg: '{{result.config.defaultshell[0] }}' + + - name: set defaultshell and maxusername + ipaconfig: + ipaadmin_password: password + defaultshell: /bin/bash + maxusername: 64 +''' + +RETURN = ''' +config: + description: Dict of all global config options + returned: When no options are set + type: dict + options: + maxusername: + description: maximum username length + returned: always + homedirectory: + description: default location of home directories + returned: always + defaultshell: + description: default shell for new users + returned: always + defaultgroup: + description: default group for new users + returned: always + emaildomain: + description: default e-mail domain + returned: always + searchtimelimit: + description: maximum amount of time (seconds) for a search + returned: always + searchrecordslimit: + description: maximum number of records to search + returned: always + usersearch: + description: comma-separated list of fields to search in user search + type: list + returned: always + groupsearch: + description: comma-separated list of fields to search in group search + type: list + returned: always + enable_migration: + description: Enable migration mode + type: bool + returned: always + groupobjectclasses: + description: default group objectclasses (comma-separated list) + type: list + returned: always + userobjectclasses: + description: default user objectclasses (comma-separated list) + type: list + returned: always + pwdexpnotify: + description: number of days's notice of impending password expiration + returned: always + configstring: + description: extra hashes to generate in password plug-in + type: list + returned: always + selinuxusermaporder: + description: order in increasing priority of SELinux users + returned: always + selinuxusermapdefault: + description: default SELinux user when no match is found in map rule + returned: always + pac_type: + description: default types of PAC supported for services + type: list + returned: always + user_auth_type: + description: default types of supported user authentication + returned: always + domain_resolution_order: + description: list of domains used for short name qualification + returned: always +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_freeipa_module import temp_kinit, \ + temp_kdestroy, valid_creds, api_connect, api_command_no_name, \ + compare_args_ipa, module_params_get + + +def config_show(module): + _result = api_command_no_name(module, "config_show", {}) + + return _result["result"] + + +def gen_args(params): + _args = {} + for k, v in params.items(): + if v is not None: + _args[k] = v + + return _args + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # general + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", required=False, no_log=True), + maxusername=dict(type="int", required=False, + aliases=['ipamaxusernamelength']), + homedirectory=dict(type="str", required=False, + aliases=['ipahomesrootdir']), + defaultshell=dict(type="str", required=False, + aliases=['ipadefaultloginshell', + 'loginshell']), + defaultgroup=dict(type="str", required=False, + aliases=['ipadefaultprimarygroup']), + emaildomain=dict(type="str", required=False, + aliases=['ipadefaultemaildomain']), + searchtimelimit=dict(type="int", required=False, + aliases=['ipasearchtimelimit']), + searchrecordslimit=dict(type="int", required=False, + aliases=['ipasearchrecordslimit']), + usersearch=dict(type="list", required=False, + aliases=['ipausersearchfields']), + groupsearch=dict(type="list", required=False, + aliases=['ipagroupsearchfields']), + enable_migration=dict(type="bool", required=False, + aliases=['ipamigrationenabled', + 'enable-migration']), + groupobjectclasses=dict(type="list", required=False, + aliases=['ipagroupobjectclasses']), + userobjectclasses=dict(type="list", required=False, + aliases=['ipauserobjectclasses']), + pwdexpnotify=dict(type="int", required=False, + aliases=['ipapwdexpadvnotify']), + configstring=dict(type="list", required=False, + aliases=['ipaconfigstring'], + choices=["AllowNThash", + "KDC:Disable Last Success", + "KDC:Disable Lockout", + "KDC:Disable Default Preauth for SPNs"]), # noqa E128 + selinuxusermaporder=dict(type="list", required=False, + aliases=['ipaselinuxusermaporder']), + selinuxusermapdefault=dict(type="str", required=False, + aliases=['ipaselinuxusermapdefault']), + pac_type=dict(type="list", required=False, + aliases=["ipakrbauthzdata", "pac-type"], + choices=["MS-PAC", "PAD", "nfs:NONE"]), + user_auth_type=dict(type="list", required=False, + aliases=["ipauserauthtype", + "user-auth_type", + "user-auth-type"]), + domain_resolution_order=dict(type="list", required=False, + aliases=["ipadomainresolutionorder", + "domain-resolution_order", + "domain-resolution-order"]) + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + # general + ipaadmin_principal = module_params_get(ansible_module, + "ipaadmin_principal") + ipaadmin_password = module_params_get(ansible_module, + "ipaadmin_password") + + field_map = { + "maxusername": "ipamaxusernamelength", + "homedirectory": "ipahomesrootdir", + "defaultshell": "ipadefaultloginshell", + "defaultgroup": "ipadefaultprimarygroup", + "emaildomain": "ipadefaultemaildomain", + "searchtimelimit": "ipasearchtimelimit", + "searchrecordslimit": "ipasearchrecordslimit", + "usersearch": "ipausersearchfields", + "groupsearch": "ipagroupsearchfields", + "enable_migration": "ipamigrationenabled", + "groupobjectclasses": "ipagroupobjectclasses", + "userobjectclasses": "ipauserobjectclasses", + "pwdexpnotify": "ipapwdexpadvnotify", + "configstring": "ipaconfigstring", + "selinuxusermaporder": "ipaselinuxusermaporder", + "selinuxusermapdefault": "ipaselinuxusermapdefault", + "pac_type": "ipakrbauthzdata", + "user_auth_type": "ipauserauthtype", + "domain_resolution_order": "ipadomainresolutionorder" + } + reverse_field_map = {v: k for k, v in field_map.items()} + + params = {} + for x in field_map.keys(): + val = module_params_get(ansible_module, x) + + if val is not None: + params[field_map.get(x, x)] = val + + if params.get("ipamigrationenabled") is not None: + params["ipamigrationenabled"] = \ + str(params["ipamigrationenabled"]).upper() + + if params.get("ipaselinuxusermaporder", None): + params["ipaselinuxusermaporder"] = \ + "$".join(params["ipaselinuxusermaporder"]) + + if params.get("ipadomainresolutionorder", None): + params["ipadomainresolutionorder"] = \ + ":".join(params["ipadomainresolutionorder"]) + + if params.get("ipausersearchfields", None): + params["ipausersearchfields"] = \ + ",".join(params["ipausersearchfields"]) + + if params.get("ipagroupsearchfields", None): + params["ipagroupsearchfields"] = \ + ",".join(params["ipagroupsearchfields"]) + + if params.get("ipamaxusernamelength", 0) > 255 \ + or params.get("ipamaxusernamelength", 2) < 1: + ansible_module.fail_json( + msg="Argument 'maxusername' mustn range 1 to 255") + + for x in ["ipasearchtimelimit", + "ipasearchrecordslimit", + "ipapwdexpadvnotify"]: + if params.get(x, 0) > 2147483647: + ansible_module.fail_json( + msg="Argument '%s' has a maximum value of 2147483647" % (x)) + + for x in ["ipasearchtimelimit", "ipasearchrecordslimit"]: + if params.get(x, 0) < -2147483648: + ansible_module.fail_json( + msg="Argument '%s' has minimum value of -2147483648" % (x)) + + changed = False + exit_args = {} + ccache_dir = None + ccache_name = None + res_show = None + try: + if not valid_creds(ansible_module, ipaadmin_principal): + ccache_dir, ccache_name = temp_kinit(ipaadmin_principal, + ipaadmin_password) + api_connect() + + if params.keys(): + res_show = config_show(ansible_module) + if not compare_args_ipa(ansible_module, params, res_show): + changed = True + api_command_no_name(ansible_module, "config_mod", params) + + else: + rawresult = api_command_no_name(ansible_module, "config_show", {}) + result = rawresult['result'] + del result['dn'] + for key, v in result.items(): + k = reverse_field_map.get(key, key) + if ansible_module.argument_spec.get(k): + if k == 'ipaselinuxusermaporder': + exit_args['ipaselinuxusermaporder'] = \ + result.get(key)[0].split('$') + elif k == 'domain_resolution_order': + exit_args['domain_resolution_order'] = \ + result.get(key)[0].split('$') + elif k == 'usersearch': + exit_args['usersearch'] = \ + result.get(key)[0].split(',') + elif k == 'groupsearch': + exit_args['groupsearch'] = \ + result.get(key)[0].split(',') + elif isinstance(v, str) and \ + ansible_module.argument_spec[k]['type'] == "list": + exit_args[k] = [v] + elif isinstance(v, list) and \ + ansible_module.argument_spec[k]['type'] == "str": + exit_args[k] = ",".join(v) + elif isinstance(v, list) and \ + ansible_module.argument_spec[k]['type'] == "int": + exit_args[k] = ",".join(v) + elif isinstance(v, list) and \ + ansible_module.argument_spec[k]['type'] == "bool": + exit_args[k] = (v[0] == "TRUE") + else: + exit_args[k] = v + + except Exception as e: + ansible_module.fail_json(msg="%s %s" % (params, str(e))) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + # Done + ansible_module.exit_json(changed=changed, config=exit_args) + + +if __name__ == "__main__": + main() diff --git a/tests/config/test_config.yml b/tests/config/test_config.yml new file mode 100644 index 00000000..81f2f4e0 --- /dev/null +++ b/tests/config/test_config.yml @@ -0,0 +1,143 @@ +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: return current values of the global configuration options + ipaconfig: + ipaadmin_password: SomeADMINpassword + register: previousconfig + + - debug: + msg: "{{previousconfig}}" + + - name: set default shell to default value + ipaconfig: + ipaadmin_password: SomeADMINpassword + defaultshell: /bin/sh + register: result + failed_when: result.changed + + - name: set default shell to new value + ipaconfig: + ipaadmin_password: SomeADMINpassword + defaultshell: /bin/bash + register: result + failed_when: not result.changed + + - name: check default shell is changed + ipaconfig: + ipaadmin_password: SomeADMINpassword + defaultshell: /bin/bash + register: result + failed_when: result.changed + + - name: reset default shell to old value + ipaconfig: + ipaadmin_password: SomeADMINpassword + defaultshell: '{{previousconfig.config.defaultshell }}' + register: result + failed_when: not result.changed + + - name: check default shell is reset + ipaconfig: + ipaadmin_password: SomeADMINpassword + defaultshell: '{{previousconfig.config.defaultshell }}' + register: result + failed_when: result.changed + + - name: Ensure the default e-mail domain is ansible.com. + ipaconfig: + ipaadmin_password: SomeADMINpassword + emaildomain: ansible.com + register: result + failed_when: not result.changed + + - name: Ensure the default e-mail domain is set + ipaconfig: + ipaadmin_password: SomeADMINpassword + emaildomain: ansible.com + register: result + failed_when: result.changed + + - name: reset default e-mail domain + ipaconfig: + ipaadmin_password: SomeADMINpassword + emaildomain: '{{previousconfig.config.emaildomain }}' + register: result + failed_when: not result.changed + + - name: set pac-type + ipaconfig: + ipaadmin_password: SomeADMINpassword + pac_type: + - nfs:NONE + register: result + failed_when: not result.changed + + - name: reset pac-type + ipaconfig: + ipaadmin_password: SomeADMINpassword + pac_type: '{{previousconfig.config.pac_type}}' + register: result + failed_when: not result.changed + + - name: set usersearch + ipaconfig: + ipaadmin_password: SomeADMINpassword + usersearch: + - uid + register: result + failed_when: not result.changed + + - name: check usersearch + ipaconfig: + ipaadmin_password: SomeADMINpassword + usersearch: + - uid + register: result + failed_when: result.changed + + - name: reset changed fields + ipaconfig: + ipaadmin_password: 'SomeADMINpassword' + configstring: '{{previousconfig.config.configstring}}' + emaildomain: '{{previousconfig.config.emaildomain}}' + defaultshell: '{{previousconfig.config.defaultshell}}' + defaultgroup: '{{previousconfig.config.defaultgroup}}' + groupsearch: '{{previousconfig.config.groupsearch}}' + homedirectory: '{{previousconfig.config.homedirectory}}' + pac_type: '{{previousconfig.config.pac_type}}' + maxusername: '{{previousconfig.config.maxusername}}' + enable_migration: '{{previousconfig.config.enable_migration}}' + pwdexpnotify: '{{previousconfig.config.pwdexpnotify}}' + searchrecordslimit: '{{previousconfig.config.searchrecordslimit}}' + searchtimelimit: '{{previousconfig.config.searchtimelimit}}' + selinuxusermapdefault: '{{previousconfig.config.selinuxusermapdefault}}' + selinuxusermaporder: '{{previousconfig.config.selinuxusermaporder}}' + usersearch: '{{previousconfig.config.usersearch}}' + register: result + failed_when: not result.changed + + - name: check reset fields + ipaconfig: + ipaadmin_password: 'SomeADMINpassword' + configstring: '{{previousconfig.config.configstring}}' + emaildomain: '{{previousconfig.config.emaildomain}}' + defaultshell: '{{previousconfig.config.defaultshell}}' + defaultgroup: '{{previousconfig.config.defaultgroup}}' + groupsearch: '{{previousconfig.config.groupsearch}}' + homedirectory: '{{previousconfig.config.homedirectory}}' + pac_type: '{{previousconfig.config.pac_type}}' + maxusername: '{{previousconfig.config.maxusername}}' + enable_migration: '{{previousconfig.config.enable_migration}}' + pwdexpnotify: '{{previousconfig.config.pwdexpnotify}}' + searchrecordslimit: '{{previousconfig.config.searchrecordslimit}}' + searchtimelimit: '{{previousconfig.config.searchtimelimit}}' + selinuxusermapdefault: '{{previousconfig.config.selinuxusermapdefault}}' + selinuxusermaporder: '{{previousconfig.config.selinuxusermaporder}}' + usersearch: '{{previousconfig.config.usersearch}}' + register: result + failed_when: result.changed -- GitLab