# -*- coding: utf-8 -*- # Authors: # Mark Hahl <mhahl@redhat.com> # Jake Reynolds <jakealexis@gmail.com> # # Copyright (C) 2021 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/>. from __future__ import (absolute_import, division, print_function) __metaclass__ = type ANSIBLE_METADATA = { "metadata_version": "1.0", "supported_by": "community", "status": ["preview"], } DOCUMENTATION = """ --- module: ipaautomember short description: Add and delete FreeIPA Auto Membership Rules. description: Add, modify and delete an IPA Auto Membership Rules. extends_documentation_fragment: - ipamodule_base_docs options: name: description: The automember rule required: true aliases: ["cn"] description: description: A description of this auto member rule required: false automember_type: description: Grouping to which the rule applies required: true type: str choices: ["group", "hostgroup"] exclusive: description: List of dictionaries containing the attribute and expression. type: list elements: dict aliases: ["automemberexclusiveregex"] options: key: description: The attribute of the regex type: str required: true expression: description: The expression of the regex type: str required: true inclusive: description: List of dictionaries containing the attribute and expression. type: list elements: dict aliases: ["automemberinclusiveregex"] options: key: description: The attribute of the regex type: str required: true expression: description: The expression of the regex type: str required: true users: description: Users to rebuild membership for. type: list required: false hosts: description: Hosts to rebuild membership for. type: list required: false no_wait: description: Don't wait for rebuilding membership. type: bool default_group: description: Default (fallback) group for all unmatched entries. type: str action: description: Work on automember or member level default: automember choices: ["member", "automember"] state: description: State to ensure default: present choices: ["present", "absent", "rebuilt", "orphans_removed"] author: - Mark Hahl - Jake Reynolds - Thomas Woerner """ EXAMPLES = """ # Ensure an automember rule exists - ipaautomember: ipaadmin_password: SomeADMINpassword name: admins description: "example description" automember_type: group state: present inclusive: - key: "mail" expression: "example.com" # Delete an automember rule - ipaautomember: ipaadmin_password: SomeADMINpassword name: admins description: "my automember rule" automember_type: group state: absent # Add an inclusive condition to an existing rule - ipaautomember: ipaadmin_password: SomeADMINpassword name: "My domain hosts" automember_type: hostgroup action: member inclusive: - key: fqdn expression: ".*.mydomain.com" # Ensure group membership for all users has been rebuilt - ipaautomember: ipaadmin_password: SomeADMINpassword automember_type: group state: rebuilt # Ensure group membership for given users has been rebuilt - ipaautomember: ipaadmin_password: SomeADMINpassword users: - user1 - user2 state: rebuilt # Ensure hostgroup membership for all hosts has been rebuilt - ipaautomember: ipaadmin_password: SomeADMINpassword automember_type: hostgroup state: rebuilt # Ensure hostgroup membership for given hosts has been rebuilt - ipaautomember: ipaadmin_password: SomeADMINpassword hosts: - host1.mydomain.com - host2.mydomain.com state: rebuilt # Ensure default group fallback_group for all unmatched group entries is set - ipaautomember: ipaadmin_password: SomeADMINpassword automember_type: group default_group: fallback_group # Ensure default group for all unmatched group entries is not set - ipaautomember: ipaadmin_password: SomeADMINpassword default_group: "" automember_type: group state: absent # Ensure default hostgroup fallback_hostgroup for all unmatched group entries # is set - ipaautomember: ipaadmin_password: SomeADMINpassword automember_type: hostgroup default_group: fallback_hostgroup # Ensure default hostgroup for all unmatched group entries is not set - ipaautomember: ipaadmin_password: SomeADMINpassword automember_type: hostgroup default_group: "" state: absent # Example playbook to ensure all orphan automember group rules are removed: - ipaautomember: ipaadmin_password: SomeADMINpassword automember_type: group state: orphans_removed # Example playbook to ensure all orphan automember hostgroup rules are removed: - ipaautomember: ipaadmin_password: SomeADMINpassword automember_type: hostgroup state: orphans_removed """ RETURN = """ """ from ansible.module_utils.ansible_freeipa_module import ( IPAAnsibleModule, compare_args_ipa, gen_add_del_lists, ipalib_errors, DN ) def find_automember(module, name, automember_type): _args = { "all": True, "type": automember_type } try: _result = module.ipa_command("automember_show", name, _args) except ipalib_errors.NotFound: return None return _result["result"] def find_automember_orphans(module, automember_type): _args = { "all": True, "type": automember_type } try: _result = module.ipa_command_no_name("automember_find_orphans", _args) except ipalib_errors.NotFound: return None return _result def find_automember_default_group(module, automember_type): _args = { "all": True, "type": automember_type } try: _result = module.ipa_command_no_name("automember_default_group_show", _args) except ipalib_errors.NotFound: return None return _result["result"] def gen_condition_args(automember_type, key, inclusiveregex=None, exclusiveregex=None): _args = {} if automember_type is not None: _args['type'] = automember_type if key is not None: _args['key'] = key if inclusiveregex is not None: _args['automemberinclusiveregex'] = inclusiveregex if exclusiveregex is not None: _args['automemberexclusiveregex'] = exclusiveregex return _args def gen_rebuild_args(automember_type, rebuild_users, rebuild_hosts, no_wait): _args = {"no_wait": no_wait} if automember_type is not None: _args['type'] = automember_type if rebuild_users is not None: _args["users"] = rebuild_users if rebuild_hosts is not None: _args["hosts"] = rebuild_hosts return _args def gen_args(description, automember_type): _args = {} if description is not None: _args["description"] = description if automember_type is not None: _args['type'] = automember_type return _args def transform_conditions(conditions): """Transform a list of dicts into a list with the format of key=value.""" transformed = ['%s=%s' % (condition['key'], condition['expression']) for condition in conditions] return transformed def check_condition_keys(ansible_module, conditions, aciattrs): if conditions is None: return for condition in conditions: if condition["key"] not in aciattrs: ansible_module.fail_json( msg="Invalid automember condition key '%s'" % condition["key"]) def main(): ansible_module = IPAAnsibleModule( argument_spec=dict( # general inclusive=dict(type="list", aliases=["automemberinclusiveregex"], default=None, options=dict( key=dict(type="str", required=True), expression=dict(type="str", required=True) ), elements="dict", required=False), exclusive=dict(type="list", aliases=["automemberexclusiveregex"], default=None, options=dict( key=dict(type="str", required=True), expression=dict(type="str", required=True) ), elements="dict", required=False), name=dict(type="list", aliases=["cn"], default=None, required=False), description=dict(type="str", default=None), automember_type=dict(type='str', required=False, choices=['group', 'hostgroup']), no_wait=dict(type="bool", default=None), default_group=dict(type="str", default=None), action=dict(type="str", default="automember", choices=["member", "automember"]), state=dict(type="str", default="present", choices=["present", "absent", "rebuilt", "orphans_removed"]), users=dict(type="list", default=None), hosts=dict(type="list", default=None), ), supports_check_mode=True, ) ansible_module._ansible_debug = True # Get parameters # general names = ansible_module.params_get("name") if names is None: names = [] # present description = ansible_module.params_get("description") # conditions inclusive = ansible_module.params_get("inclusive") exclusive = ansible_module.params_get("exclusive") # no_wait for rebuilt no_wait = ansible_module.params_get("no_wait") # default_group default_group = ansible_module.params_get("default_group") # action action = ansible_module.params_get("action") # state state = ansible_module.params_get("state") # grouping/type automember_type = ansible_module.params_get("automember_type") rebuild_users = ansible_module.params_get("users") rebuild_hosts = ansible_module.params_get("hosts") # Check parameters invalid = [] if state in ["rebuilt", "orphans_removed"]: invalid = ["name", "description", "exclusive", "inclusive", "default_group"] if action == "member": ansible_module.fail_json( msg="'action=member' is not usable with state '%s'" % state) if state == "rebuilt": if automember_type == "group" and rebuild_hosts is not None: ansible_module.fail_json( msg="state %s: hosts can not be set when type is '%s'" % (state, automember_type)) if automember_type == "hostgroup" and rebuild_users is not None: ansible_module.fail_json( msg="state %s: users can not be set when type is '%s'" % (state, automember_type)) elif state == "orphans_removed": invalid.extend(["users", "hosts"]) if not automember_type: ansible_module.fail_json( msg="'automember_type' is required unless state: rebuilt") else: if default_group is not None: for param in ["name", "exclusive", "inclusive", "users", "hosts" "no_wait"]: if ansible_module.params.get(param) is not None: msg = "Cannot use {0} together with default_group" ansible_module.fail_json(msg=msg.format(param)) if action == "member": ansible_module.fail_json( msg="Cannot use default_group with action:member") if state == "absent": ansible_module.fail_json( msg="Cannot use default_group with state:absent") else: invalid = ["users", "hosts", "no_wait"] if not automember_type: ansible_module.fail_json( msg="'automember_type' is required.") ansible_module.params_fail_used_invalid(invalid, state, action) # Init changed = False exit_args = {} res_find = None with ansible_module.ipa_connect(): commands = [] for name in names: # Make sure automember rule exists res_find = find_automember(ansible_module, name, automember_type) # Check inclusive and exclusive conditions if inclusive is not None or exclusive is not None: # automember_type is either "group" or "hostgorup" if automember_type == "group": _type = u"user" elif automember_type == "hostgroup": _type = u"host" else: ansible_module.fail_json( msg="Bad automember type '%s'" % automember_type) try: aciattrs = ansible_module.ipa_command( "json_metadata", _type, {} )['objects'][_type]['aciattrs'] except Exception as ex: ansible_module.fail_json( msg="%s: %s: %s" % ("json_metadata", _type, str(ex))) check_condition_keys(ansible_module, inclusive, aciattrs) check_condition_keys(ansible_module, exclusive, aciattrs) # Create command if state == 'present': args = gen_args(description, automember_type) if action == "automember": if res_find is not None: if not compare_args_ipa(ansible_module, args, res_find, ignore=['type']): commands.append([name, 'automember_mod', args]) else: commands.append([name, 'automember_add', args]) res_find = {} if inclusive is not None: inclusive_add, inclusive_del = gen_add_del_lists( transform_conditions(inclusive), res_find.get("automemberinclusiveregex", []) ) else: inclusive_add, inclusive_del = [], [] if exclusive is not None: exclusive_add, exclusive_del = gen_add_del_lists( transform_conditions(exclusive), res_find.get("automemberexclusiveregex", []) ) else: exclusive_add, exclusive_del = [], [] elif action == "member": if res_find is None: ansible_module.fail_json( msg="No automember '%s'" % name) inclusive_add = transform_conditions(inclusive or []) inclusive_del = [] exclusive_add = transform_conditions(exclusive or []) exclusive_del = [] for _inclusive in inclusive_add: key, regex = _inclusive.split("=", 1) condition_args = gen_condition_args( automember_type, key, inclusiveregex=regex) commands.append([name, 'automember_add_condition', condition_args]) for _inclusive in inclusive_del: key, regex = _inclusive.split("=", 1) condition_args = gen_condition_args( automember_type, key, inclusiveregex=regex) commands.append([name, 'automember_remove_condition', condition_args]) for _exclusive in exclusive_add: key, regex = _exclusive.split("=", 1) condition_args = gen_condition_args( automember_type, key, exclusiveregex=regex) commands.append([name, 'automember_add_condition', condition_args]) for _exclusive in exclusive_del: key, regex = _exclusive.split("=", 1) condition_args = gen_condition_args( automember_type, key, exclusiveregex=regex) commands.append([name, 'automember_remove_condition', condition_args]) elif state == 'absent': if action == "automember": if res_find is not None: commands.append([name, 'automember_del', {'type': automember_type}]) elif action == "member": if res_find is None: ansible_module.fail_json( msg="No automember '%s'" % name) if inclusive is not None: for _inclusive in transform_conditions(inclusive): key, regex = _inclusive.split("=", 1) condition_args = gen_condition_args( automember_type, key, inclusiveregex=regex) commands.append( [name, 'automember_remove_condition', condition_args]) if exclusive is not None: for _exclusive in transform_conditions(exclusive): key, regex = _exclusive.split("=", 1) condition_args = gen_condition_args( automember_type, key, exclusiveregex=regex) commands.append([name, 'automember_remove_condition', condition_args]) if len(names) == 0: if state == "rebuilt": args = gen_rebuild_args(automember_type, rebuild_users, rebuild_hosts, no_wait) commands.append([None, 'automember_rebuild', args]) elif state == "orphans_removed": res_find = find_automember_orphans(ansible_module, automember_type) if res_find["count"] > 0: commands.append([None, 'automember_find_orphans', {'type': automember_type, 'remove': True}]) elif default_group is not None and state == "present": res_find = find_automember_default_group(ansible_module, automember_type) if default_group == "": if isinstance(res_find["automemberdefaultgroup"], list): commands.append([None, 'automember_default_group_remove', {'type': automember_type}]) else: dn_default_group = [DN(('cn', default_group), ('cn', '%ss' % automember_type), ('cn', 'accounts'), ansible_module.ipa_get_basedn())] if repr(res_find["automemberdefaultgroup"]) != \ repr(dn_default_group): commands.append( [None, 'automember_default_group_set', {'type': automember_type, 'automemberdefaultgroup': default_group}]) else: ansible_module.fail_json(msg="Invalid operation") # Execute commands changed = ansible_module.execute_ipa_commands(commands) # result["failed"] is used only for INCLUDE_RE, EXCLUDE_RE # if entries could not be added that are already there and # if entries could not be removed that are not there. # All other issues like invalid attributes etc. are handled # as exceptions. Therefore the error section is not here as # in other modules. # Done ansible_module.exit_json(changed=changed, **exit_args) if __name__ == "__main__": main()