# -*- coding: utf-8 -*-

# Authors:
#   Thomas Woerner <twoerner@redhat.com>
#
# Copyright (C) 2019-2022 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <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: ipagroup
short_description: Manage FreeIPA groups
description: Manage FreeIPA groups
extends_documentation_fragment:
  - ipamodule_base_docs
options:
  name:
    description: The group name
    type: list
    elements: str
    required: false
    aliases: ["cn"]
  groups:
    description: The list of group dicts (internally gid).
    type: list
    elements: dict
    suboptions:
      name:
        description: The group (internally gid).
        type: str
        required: true
        aliases: ["cn"]
      description:
        description: The group description
        type: str
        required: false
      gid:
        description: The GID
        type: int
        required: false
        aliases: ["gidnumber"]
      nonposix:
        description: Create as a non-POSIX group
        required: false
        type: bool
      external:
        description: Allow adding external non-IPA members from trusted domains
        required: false
        type: bool
      posix:
        description:
          Create a non-POSIX group or change a non-POSIX to a posix group.
        required: false
        type: bool
      nomembers:
        description: Suppress processing of membership attributes
        required: false
        type: bool
      user:
        description: List of user names assigned to this group.
        required: false
        type: list
        elements: str
      group:
        description: List of group names assigned to this group.
        required: false
        type: list
        elements: str
      service:
        description:
        - List of service names assigned to this group.
        - Only usable with IPA versions 4.7 and up.
        required: false
        type: list
        elements: str
      membermanager_user:
        description:
        - List of member manager users assigned to this group.
        - Only usable with IPA versions 4.8.4 and up.
        required: false
        type: list
        elements: str
      membermanager_group:
        description:
        - List of member manager groups assigned to this group.
        - Only usable with IPA versions 4.8.4 and up.
        required: false
        type: list
        elements: str
      externalmember:
        description:
        - List of members of a trusted domain in DOM\\name or name@domain form.
          Requires "server" context.
        required: false
        type: list
        elements: str
        aliases: ["ipaexternalmember", "external_member"]
      idoverrideuser:
        description:
        - User ID overrides to add. Requires "server" context.
        required: false
        type: list
        elements: str
      rename:
        description: Rename the group object
        required: false
        type: str
        aliases: ["new_name"]
  description:
    description: The group description
    type: str
    required: false
  gid:
    description: The GID
    type: int
    required: false
    aliases: ["gidnumber"]
  nonposix:
    description: Create as a non-POSIX group
    required: false
    type: bool
  external:
    description: Allow adding external non-IPA members from trusted domains
    required: false
    type: bool
  posix:
    description:
      Create a non-POSIX group or change a non-POSIX to a posix group.
    required: false
    type: bool
  nomembers:
    description: Suppress processing of membership attributes
    required: false
    type: bool
  user:
    description: List of user names assigned to this group.
    required: false
    type: list
    elements: str
  group:
    description: List of group names assigned to this group.
    required: false
    type: list
    elements: str
  service:
    description:
    - List of service names assigned to this group.
    - Only usable with IPA versions 4.7 and up.
    required: false
    type: list
    elements: str
  membermanager_user:
    description:
    - List of member manager users assigned to this group.
    - Only usable with IPA versions 4.8.4 and up.
    required: false
    type: list
    elements: str
  membermanager_group:
    description:
    - List of member manager groups assigned to this group.
    - Only usable with IPA versions 4.8.4 and up.
    required: false
    type: list
    elements: str
  externalmember:
    description:
    - List of members of a trusted domain in DOM\\name or name@domain form.
      Requires "server" context.
    required: false
    type: list
    elements: str
    aliases: ["ipaexternalmember", "external_member"]
  idoverrideuser:
    description:
    - User ID overrides to add. Requires "server" context.
    required: false
    type: list
    elements: str
  action:
    description: Work on group or member level
    type: str
    default: group
    choices: ["member", "group"]
  rename:
    description: Rename the group object
    required: false
    type: str
    aliases: ["new_name"]
  state:
    description: State to ensure
    type: str
    default: present
    choices: ["present", "absent", "renamed"]
author:
  - Thomas Woerner (@t-woerner)
"""

EXAMPLES = """
# Create group ops with gid 1234
- ipagroup:
    ipaadmin_password: SomeADMINpassword
    name: ops
    gidnumber: 1234

# Create group sysops
- ipagroup:
    ipaadmin_password: SomeADMINpassword
    name: sysops

# Create group appops
- ipagroup:
    ipaadmin_password: SomeADMINpassword
    name: appops

# Create multiple groups ops, sysops
- ipagroup:
    ipaadmin_password: SomeADMINpassword
    groups:
    - name: ops
      gidnumber: 1234
    - name: sysops

# Add user member pinky to group sysops
- ipagroup:
    ipaadmin_password: SomeADMINpassword
    name: sysops
    action: member
    user:
    - pinky

# Add user member brain to group sysops
- ipagroup:
    ipaadmin_password: SomeADMINpassword
    name: sysops
    action: member
    user:
    - brain

# Add group members sysops and appops to group ops
- ipagroup:
    ipaadmin_password: SomeADMINpassword
    name: ops
    group:
    - sysops
    - appops

# Add user and group members to groups sysops and appops
- ipagroup:
    ipaadmin_password: SomeADMINpassword
    groups:
    - name: sysops
      user:
        - user1
    - name: appops
      group:
        - group2

# Rename a group
- ipagroup:
    ipaadmin_password: SomeADMINpassword
    name: oldname
    rename: newestname
    state: renamed

# Create a non-POSIX group
- ipagroup:
    ipaadmin_password: SomeADMINpassword
    name: nongroup
    nonposix: yes

# Turn a non-POSIX group into a POSIX group.
- ipagroup:
    ipaadmin_password: SomeADMINpassword
    name: nonposix
    posix: yes

# Create an external group and add members from a trust to it.
# Module will fail if running under 'client' context.
- ipagroup:
    ipaadmin_password: SomeADMINpassword
    name: extgroup
    external: yes
    externalmember:
    - WINIPA\\Web Users
    - WINIPA\\Developers

# Create multiple non-POSIX and external groups
- ipagroup:
    ipaadmin_password: SomeADMINpassword
    groups:
    - name: nongroup
      nonposix: true
    - name: extgroup
      external: true

# Remove groups sysops, appops, ops and nongroup
- ipagroup:
    ipaadmin_password: SomeADMINpassword
    name: sysops,appops,ops, nongroup
    state: absent
"""

RETURN = """
"""

from ansible.module_utils._text import to_text
from ansible.module_utils.ansible_freeipa_module import \
    IPAAnsibleModule, compare_args_ipa, gen_add_del_lists, \
    gen_add_list, gen_intersection_list, api_check_param, \
    convert_to_sid
from ansible.module_utils import six
if six.PY3:
    unicode = str
# Ensuring (adding) several groups with mixed types external, nonposix
# and posix require to have a fix in IPA:
# FreeIPA issue: https://pagure.io/freeipa/issue/9349
# FreeIPA fix: https://github.com/freeipa/freeipa/pull/6741
try:
    from ipaserver.plugins import baseldap
except ImportError:
    FIX_6741_DEEPCOPY_OBJECTCLASSES = False
else:
    FIX_6741_DEEPCOPY_OBJECTCLASSES = \
        "deepcopy" in baseldap.LDAPObject.__json__.__code__.co_names


def find_group(module, name):
    _args = {
        "all": True,
        "cn": name,
    }

    _result = module.ipa_command("group_find", name, _args)

    if len(_result["result"]) > 1:
        module.fail_json(
            msg="There is more than one group '%s'" % (name))
    elif len(_result["result"]) == 1:
        _res = _result["result"][0]
        # The returned services are of type ipapython.kerberos.Principal,
        # also services are not case sensitive. Therefore services are
        # converted to lowercase strings to be able to do the comparison.
        if "member_service" in _res:
            _res["member_service"] = \
                [to_text(svc).lower() for svc in _res["member_service"]]
        return _res

    return None


def gen_args(description, gid, nomembers):
    _args = {}
    if description is not None:
        _args["description"] = description
    if gid is not None:
        _args["gidnumber"] = gid
    if nomembers is not None:
        _args["nomembers"] = nomembers

    return _args


def gen_member_args(user, group, service, externalmember, idoverrideuser):
    _args = {}
    if user is not None:
        _args["member_user"] = user
    if group is not None:
        _args["member_group"] = group
    if service is not None:
        _args["member_service"] = service
    if externalmember is not None:
        _args["member_external"] = externalmember
    if idoverrideuser is not None:
        _args["member_idoverrideuser"] = idoverrideuser

    return _args


def check_parameters(module, state, action):
    invalid = ["description", "gid", "posix", "nonposix", "external",
               "nomembers"]
    if action == "group":
        if state == "present":
            invalid = []
        elif state == "absent":
            invalid.extend(["user", "group", "service", "externalmember"])
    if state == "renamed":
        if action == "member":
            module.fail_json(
                msg="Action member can not be used with state: renamed.")
        invalid.extend(["user", "group", "service", "externalmember"])
    else:
        invalid.append("rename")
    module.params_fail_used_invalid(invalid, state, action)


def is_external_group(res_find):
    """Verify if the result group is an external group."""
    return res_find and 'ipaexternalgroup' in res_find['objectclass']


def is_posix_group(res_find):
    """Verify if the result group is an posix group."""
    return res_find and 'posixgroup' in res_find['objectclass']


def check_objectclass_args(module, res_find, posix, external):
    # Only a nonposix group can be changed to posix or external

    # A posix group can not be changed to nonposix or external
    if is_posix_group(res_find):
        if external is not None and external or posix is False:
            module.fail_json(
                msg="Cannot change `posix` group to `non-posix` or "
                "`external`.")
    # An external group can not be changed to nonposix or posix or nonexternal
    if is_external_group(res_find):
        if external is False or posix is not None:
            module.fail_json(
                msg="Cannot change `external` group to `posix` or "
                "`non-posix`.")


def main():
    group_spec = dict(
        # present
        description=dict(type="str", default=None),
        gid=dict(type="int", aliases=["gidnumber"], default=None),
        nonposix=dict(required=False, type='bool', default=None),
        external=dict(required=False, type='bool', default=None),
        posix=dict(required=False, type='bool', default=None),
        nomembers=dict(required=False, type='bool', default=None),
        user=dict(required=False, type='list', elements="str",
                  default=None),
        group=dict(required=False, type='list', elements="str",
                   default=None),
        service=dict(required=False, type='list', elements="str",
                     default=None),
        idoverrideuser=dict(required=False, type='list', elements="str",
                            default=None),
        membermanager_user=dict(required=False, type='list',
                                elements="str", default=None),
        membermanager_group=dict(required=False, type='list',
                                 elements="str", default=None),
        externalmember=dict(required=False, type='list', elements="str",
                            default=None,
                            aliases=[
                                "ipaexternalmember",
                                "external_member"
                            ]),
        rename=dict(type="str", required=False, default=None,
                    aliases=["new_name"]),
    )
    ansible_module = IPAAnsibleModule(
        argument_spec=dict(
            # general
            name=dict(type="list", elements="str", aliases=["cn"],
                      default=None, required=False),
            groups=dict(type="list",
                        default=None,
                        options=dict(
                            # Here name is a simple string
                            name=dict(type="str", required=True,
                                      aliases=["cn"]),
                            # Add group specific parameters
                            **group_spec
                        ),
                        elements='dict',
                        required=False),
            # general
            action=dict(type="str", default="group",
                        choices=["member", "group"]),
            state=dict(type="str", default="present",
                       choices=["present", "absent", "renamed"]),

            # Add group specific parameters for simple use case
            **group_spec
        ),
        # It does not make sense to set posix, nonposix or external at the
        # same time
        mutually_exclusive=[['posix', 'nonposix', 'external'],
                            ["name", "groups"]],
        required_one_of=[["name", "groups"]],
        supports_check_mode=True,
    )

    ansible_module._ansible_debug = True

    # Get parameters

    # general
    names = ansible_module.params_get("name")
    groups = ansible_module.params_get("groups")

    # present
    description = ansible_module.params_get("description")
    gid = ansible_module.params_get("gid")
    nonposix = ansible_module.params_get("nonposix")
    external = ansible_module.params_get("external")
    idoverrideuser = ansible_module.params_get("idoverrideuser")
    posix = ansible_module.params_get("posix")
    nomembers = ansible_module.params_get("nomembers")
    user = ansible_module.params_get_lowercase("user")
    group = ansible_module.params_get_lowercase("group")
    # Services are not case sensitive
    service = ansible_module.params_get_lowercase("service")
    membermanager_user = (
        ansible_module.params_get_lowercase("membermanager_user"))
    membermanager_group = (
        ansible_module.params_get_lowercase("membermanager_group"))
    externalmember = ansible_module.params_get("externalmember")
    # rename
    rename = ansible_module.params_get("rename")
    # state and action
    action = ansible_module.params_get("action")
    state = ansible_module.params_get("state")

    # Check parameters

    if (names is None or len(names) < 1) and \
       (groups is None or len(groups) < 1):
        ansible_module.fail_json(msg="At least one name or groups is required")

    if state in ["present", "renamed"]:
        if names is not None and len(names) != 1:
            what = "renamed" if state == "renamed" else "added"
            ansible_module.fail_json(
                msg="Only one group can be %s at a time using 'name'." % what)

    check_parameters(ansible_module, state, action)

    if external is False:
        ansible_module.fail_json(
            msg="group can not be non-external")

    # Ensuring (adding) several groups with mixed types external, nonposix
    # and posix require to have a fix in IPA:
    #
    # FreeIPA issue: https://pagure.io/freeipa/issue/9349
    # FreeIPA fix: https://github.com/freeipa/freeipa/pull/6741
    #
    # The simple solution is to switch to client context for ensuring
    # several groups simply if the user was not explicitly asking for
    # the server context no matter if mixed types are used.
    context = ansible_module.params_get("ipaapi_context")
    if state == "present" and groups is not None and len(groups) > 1 \
       and not FIX_6741_DEEPCOPY_OBJECTCLASSES:
        if context is None:
            context = "client"
            ansible_module.debug(
                "Switching to client context due to an unfixed issue in "
                "your IPA version: https://pagure.io/freeipa/issue/9349")
        elif context == "server":
            ansible_module.fail_json(
                msg="Ensuring several groups with server context is not "
                "supported by your IPA version: "
                "https://pagure.io/freeipa/issue/9349")

    if (
        externalmember is not None
        or idoverrideuser is not None
        and context == "client"
    ):
        ansible_module.fail_json(
            msg="Cannot use externalmember in client context."
        )

    # Use groups if names is None
    if groups is not None:
        names = groups

    # Init

    changed = False
    exit_args = {}

    # If nonposix is used, set posix as not nonposix
    if nonposix is not None:
        posix = not nonposix

    # Connect to IPA API
    with ansible_module.ipa_connect(context=context):

        has_add_member_service = ansible_module.ipa_command_param_exists(
            "group_add_member", "service")
        if service is not None and not has_add_member_service:
            ansible_module.fail_json(
                msg="Managing a service as part of a group is not supported "
                "by your IPA version")

        has_add_membermanager = ansible_module.ipa_command_exists(
            "group_add_member_manager")
        if ((membermanager_user is not None or
             membermanager_group is not None) and not has_add_membermanager):
            ansible_module.fail_json(
                msg="Managing a membermanager user or group is not supported "
                "by your IPA version"
            )

        has_idoverrideuser = api_check_param(
            "group_add_member", "idoverrideuser")
        if idoverrideuser is not None and not has_idoverrideuser:
            ansible_module.fail_json(
                msg="Managing a idoverrideuser as part of a group is not "
                "supported by your IPA version")

        commands = []
        group_set = set()

        for group_name in names:
            if isinstance(group_name, dict):
                name = group_name.get("name")
                if name in group_set:
                    ansible_module.fail_json(
                        msg="group '%s' is used more than once" % name)
                group_set.add(name)
                # present
                description = group_name.get("description")
                gid = group_name.get("gid")
                nonposix = group_name.get("nonposix")
                external = group_name.get("external")
                idoverrideuser = group_name.get("idoverrideuser")
                posix = group_name.get("posix")
                # Check mutually exclusive condition for multiple groups
                # creation. It's not possible to check it with
                # `mutually_exclusive` argument in `IPAAnsibleModule` class
                # because it accepts only (list[str] or list[list[str]]). Here
                # we need to loop over all groups and fail on mutually
                # exclusive ones.
                if all((posix, nonposix)) or\
                   all((posix, external)) or\
                   all((nonposix, external)):
                    ansible_module.fail_json(
                        msg="parameters are mutually exclusive for group "
                            "`{0}`: posix|nonposix|external".format(name))
                # Duplicating the condition for multiple group creation
                if external is False:
                    ansible_module.fail_json(
                        msg="group can not be non-external")
                # If nonposix is used, set posix as not nonposix
                if nonposix is not None:
                    posix = not nonposix
                user = group_name.get("user")
                group = group_name.get("group")
                service = group_name.get("service")
                membermanager_user = group_name.get("membermanager_user")
                membermanager_group = group_name.get("membermanager_group")
                externalmember = group_name.get("externalmember")
                nomembers = group_name.get("nomembers")
                rename = group_name.get("rename")

                check_parameters(ansible_module, state, action)

            elif (
                isinstance(
                    group_name, (str, unicode)  # pylint: disable=W0012,E0606
                )
            ):
                name = group_name
            else:
                ansible_module.fail_json(msg="Group '%s' is not valid" %
                                         repr(group_name))

            # Make sure group exists
            res_find = find_group(ansible_module, name)

            # external members must de handled as SID
            externalmember = convert_to_sid(externalmember)

            # idoverrides need to be compared through SID
            idoverrideuser_sid = convert_to_sid(idoverrideuser)
            res_idoverrideuser_sid = convert_to_sid(
                (res_find or {}).get("member_idoverrideuser", []))
            idoverride_set = dict(
                list(zip(idoverrideuser_sid or [], idoverrideuser or [])) +
                list(
                    zip(
                        res_idoverrideuser_sid or [],
                        (res_find or {}).get("member_idoverrideuser", [])
                    )
                )
            )

            user_add, user_del = [], []
            group_add, group_del = [], []
            service_add, service_del = [], []
            externalmember_add, externalmember_del = [], []
            idoverrides_add, idoverrides_del = [], []
            membermanager_user_add, membermanager_user_del = [], []
            membermanager_group_add, membermanager_group_del = [], []

            # Create command
            if state == "present":
                # Can't change an existing posix group
                check_objectclass_args(ansible_module, res_find, posix,
                                       external)

                # Generate args
                args = gen_args(description, gid, nomembers)

                if action == "group":
                    # Found the group
                    if res_find is not None:
                        # For all settings in args, check if there are
                        # different settings in the find result.
                        # If yes: modify
                        # Also if it is a modification from nonposix to posix
                        # or nonposix to external.
                        if not compare_args_ipa(
                            ansible_module, args, res_find
                        ) or (
                            not is_posix_group(res_find) and
                            not is_external_group(res_find) and
                            (posix or external)
                        ):
                            if posix:
                                args['posix'] = True
                            if external:
                                args['external'] = True
                            commands.append([name, "group_mod", args])
                    else:
                        if posix is not None and not posix:
                            args['nonposix'] = True
                        if external:
                            args['external'] = True
                        commands.append([name, "group_add", args])
                        # Set res_find dict for next step
                        res_find = {}

                    # if we just created/modified the group, update res_find
                    classes = list(res_find.setdefault("objectclass", []))
                    if external and not is_external_group(res_find):
                        classes.append("ipaexternalgroup")
                    if posix and not is_posix_group(res_find):
                        classes.append("posixgroup")
                    res_find["objectclass"] = classes

                    member_args = gen_member_args(
                        user, group, service, externalmember, idoverrideuser
                    )
                    if not compare_args_ipa(ansible_module, member_args,
                                            res_find):
                        # Generate addition and removal lists
                        user_add, user_del = gen_add_del_lists(
                            user, res_find.get("member_user"))

                        group_add, group_del = gen_add_del_lists(
                            group, res_find.get("member_group"))

                        service_add, service_del = gen_add_del_lists(
                            service, res_find.get("member_service"))

                        (externalmember_add,
                         externalmember_del) = gen_add_del_lists(
                            externalmember, (
                                list(res_find.get("member_external", []))
                                + list(res_find.get("ipaexternalmember", []))
                            )
                        )

                        # There are multiple ways to name an AD User, and any
                        # can be used in idoverrides, so we create the add/del
                        # lists based on SID, and then use the given user name
                        # to the idoverride.
                        (idoverrides_add,
                         idoverrides_del) = gen_add_del_lists(
                            idoverrideuser_sid, res_idoverrideuser_sid)
                        idoverrides_add = [
                            idoverride_set[sid] for sid in set(idoverrides_add)
                        ]
                        idoverrides_del = [
                            idoverride_set[sid] for sid in set(idoverrides_del)
                        ]

                    membermanager_user_add, membermanager_user_del = \
                        gen_add_del_lists(
                            membermanager_user,
                            res_find.get("membermanager_user")
                        )

                    membermanager_group_add, membermanager_group_del = \
                        gen_add_del_lists(
                            membermanager_group,
                            res_find.get("membermanager_group")
                        )

                elif action == "member":
                    if res_find is None:
                        ansible_module.fail_json(msg="No group '%s'" % name)

                    # Reduce add lists for member_user, member_group,
                    # member_service and member_external to new entries
                    # only that are not in res_find.
                    user_add = gen_add_list(
                        user, res_find.get("member_user"))
                    group_add = gen_add_list(
                        group, res_find.get("member_group"))
                    service_add = gen_add_list(
                        service, res_find.get("member_service"))
                    externalmember_add = gen_add_list(
                        externalmember, (
                            list(res_find.get("member_external", []))
                            + list(res_find.get("ipaexternalmember", []))
                        )
                    )
                    idoverrides_add = gen_add_list(
                        idoverrideuser_sid, res_idoverrideuser_sid)
                    idoverrides_add = [
                        idoverride_set[sid] for sid in set(idoverrides_add)
                    ]

                    membermanager_user_add = gen_add_list(
                        membermanager_user,
                        res_find.get("membermanager_user")
                    )
                    membermanager_group_add = gen_add_list(
                        membermanager_group,
                        res_find.get("membermanager_group")
                    )

            elif state == "absent":
                if action == "group":
                    if res_find is not None:
                        commands.append([name, "group_del", {}])

                elif action == "member":
                    if res_find is None:
                        ansible_module.fail_json(msg="No group '%s'" % name)

                    if not is_external_group(res_find) and externalmember:
                        ansible_module.fail_json(
                            msg="Cannot add external members to a "
                                "non-external group."
                        )

                    user_del = gen_intersection_list(
                        user, res_find.get("member_user"))
                    group_del = gen_intersection_list(
                        group, res_find.get("member_group"))
                    service_del = gen_intersection_list(
                        service, res_find.get("member_service"))
                    externalmember_del = gen_intersection_list(
                        externalmember, (
                            list(res_find.get("member_external", []))
                            + list(res_find.get("ipaexternalmember", []))
                        )
                    )
                    idoverrides_del = gen_intersection_list(
                        idoverrideuser_sid, res_idoverrideuser_sid)
                    idoverrides_del = [
                        idoverride_set[sid] for sid in set(idoverrides_del)
                    ]

                    membermanager_user_del = gen_intersection_list(
                        membermanager_user, res_find.get("membermanager_user"))
                    membermanager_group_del = gen_intersection_list(
                        membermanager_group,
                        res_find.get("membermanager_group")
                    )
            elif state == "renamed":
                if res_find is None:
                    ansible_module.fail_json(msg="No group '%s'" % name)
                elif rename != name:
                    commands.append([name, 'group_mod', {"rename": rename}])
            else:
                ansible_module.fail_json(msg="Unkown state '%s'" % state)

            # manage members
            # setup member args for add/remove members.
            add_member_args = {
                "user": user_add,
                "group": group_add,
            }

            del_member_args = {
                "user": user_del,
                "group": group_del,
            }

            if has_idoverrideuser:
                add_member_args["idoverrideuser"] = idoverrides_add
                del_member_args["idoverrideuser"] = idoverrides_del

            if has_add_member_service:
                add_member_args["service"] = service_add
                del_member_args["service"] = service_del

            if is_external_group(res_find):
                if len(externalmember_add) > 0:
                    add_member_args["ipaexternalmember"] = \
                        externalmember_add
                if len(externalmember_del) > 0:
                    del_member_args["ipaexternalmember"] = \
                        externalmember_del
            elif externalmember:
                ansible_module.fail_json(
                    msg="Cannot add external members to a "
                        "non-external group."
                )

            # Add members
            add_members = any([user_add, group_add, idoverrides_add,
                               service_add, externalmember_add])
            if add_members:
                commands.append(
                    [name, "group_add_member", add_member_args]
                )
            # Remove members
            remove_members = any([user_del, group_del, idoverrides_del,
                                  service_del, externalmember_del])
            if remove_members:
                commands.append(
                    [name, "group_remove_member", del_member_args]
                )

            # manage membermanager members
            if has_add_membermanager:
                # Add membermanager users and groups
                if any([membermanager_user_add, membermanager_group_add]):
                    commands.append(
                        [name, "group_add_member_manager",
                         {
                             "user": membermanager_user_add,
                             "group": membermanager_group_add,
                         }]
                    )
                # Remove member manager
                if any([membermanager_user_del, membermanager_group_del]):
                    commands.append(
                        [name, "group_remove_member_manager",
                         {
                             "user": membermanager_user_del,
                             "group": membermanager_group_del,
                         }]
                    )

        # Execute commands
        changed = ansible_module.execute_ipa_commands(
            commands, batch=True, keeponly=[], fail_on_member_errors=True)

    # Done

    ansible_module.exit_json(changed=changed, **exit_args)


if __name__ == "__main__":
    main()