ipasudorule: Add support for batch mode and multiple sudorules

Currently, ipasudorule must add or modify a single sudorule at a time,
incurring in more load in the server if there are many rules to be
processed.

This patch adds suport for adding multiple sudorules in one playbook
task by using the parameter 'sudorules' and defining a list of sudorules
configurations to be ensured.

As multiple sudorules will be processed, the patch also enables batch
mode processing of sudorules, trying to reduce the load on the server.

Test 'tests/sudorule/test_sudorule_client_context.yml' was modified to
include tasks with 'sudorules' to be executed both on the server or on
the client context.

New tests were added to the sudorule test suite:

    tests/sudorule/test_sudorules.yml
    tests/sudorule/test_sudorules_member_case_insensitive.yml
This commit is contained in:
Rafael Guterres Jeffman
2024-09-04 21:43:35 -03:00
parent da775a21b2
commit 4fa0621156
5 changed files with 1209 additions and 273 deletions

View File

@@ -129,6 +129,49 @@ Example playbook to make sure Sudo Rule is absent:
state: absent
```
Example playbook to ensure multiple Sudo Rule are present using batch mode:
```yaml
---
- name: Playbook to handle sudorules
hosts: ipaserver
become: true
- name: Ensure multiple Sudo Rules are present using batch mode.
ipasudorule:
ipaadmin_password: SomeADMINpassword
sudorules:
- name: testrule1
hostmask:
- 192.168.122.1/24
- name: testrule2
hostcategory: all
```
Example playbook to ensure multiple Sudo Rule members are present using batch mode:
```yaml
---
- name: Playbook to handle sudorules
hosts: ipaserver
become: true
- name: Ensure multiple Sudo Rules are present using batch mode.
ipasudorule:
ipaadmin_password: SomeADMINpassword
action: member
sudorules:
- name: testrule1
user:
- user01
- user02
group:
- group01
- name: testrule2
hostgroup:
- hostgroup01
- hostgroup02
```
Variables
=========
@@ -139,7 +182,9 @@ Variable | Description | Required
`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no
`ipaapi_context` | The context in which the module will execute. Executing in a server context is preferred. If not provided context will be determined by the execution environment. Valid values are `server` and `client`. | no
`ipaapi_ldap_cache` | Use LDAP cache for IPA connection. The bool setting defaults to yes. (bool) | no
`name` \| `cn` | The list of sudorule name strings. | yes
`name` \| `cn` | The list of sudorule name strings. | no
`sudorules` | The list of sudorule dicts. Each `sudorule` dict entry can contain sudorule variables.<br>There is one required option in the `sudorule` dict:| no
&nbsp; | `name` - The sudorule name string of the entry. | yes
`description` | The sudorule description string. | no
`usercategory` \| `usercat` | User category the rule applies to. Choices: ["all", ""] | no
`hostcategory` \| `hostcat` | Host category the rule applies to. Choices: ["all", ""] | no

View File

@@ -38,6 +38,17 @@ description: Manage FreeIPA sudo rules
extends_documentation_fragment:
- ipamodule_base_docs
options:
name:
description: The sudorule name
type: list
elements: str
required: false
aliases: ["cn"]
sudorules:
description: The list of sudorule dicts.
type: list
elements: dict
suboptions:
name:
description: The sudorule name
type: list
@@ -153,6 +164,115 @@ options:
required: false
type: list
elements: str
description:
description: The sudorule description
type: str
required: false
user:
description: List of users assigned to the sudo rule.
type: list
elements: str
required: false
usercategory:
description: User category the sudo rule applies to
type: str
required: false
choices: ["all", ""]
aliases: ["usercat"]
group:
description: List of user groups assigned to the sudo rule.
type: list
elements: str
required: false
runasgroupcategory:
description: RunAs Group category applied to the sudo rule.
type: str
required: false
choices: ["all", ""]
aliases: ["runasgroupcat"]
runasusercategory:
description: RunAs User category applied to the sudorule.
type: str
required: false
choices: ["all", ""]
aliases: ["runasusercat"]
nomembers:
description: Suppress processing of membership attributes
required: false
type: bool
host:
description: List of host names assigned to this sudorule.
required: false
type: list
elements: str
hostgroup:
description: List of host groups assigned to this sudorule.
required: false
type: list
elements: str
hostcategory:
description: Host category the sudo rule applies to.
type: str
required: false
choices: ["all", ""]
aliases: ["hostcat"]
allow_sudocmd:
description: List of allowed sudocmds assigned to this sudorule.
required: false
type: list
elements: str
allow_sudocmdgroup:
description: List of allowed sudocmd groups assigned to this sudorule.
required: false
type: list
elements: str
deny_sudocmd:
description: List of denied sudocmds assigned to this sudorule.
required: false
type: list
elements: str
deny_sudocmdgroup:
description: List of denied sudocmd groups assigned to this sudorule.
required: false
type: list
elements: str
cmdcategory:
description: Command category the sudo rule applies to
type: str
required: false
choices: ["all", ""]
aliases: ["cmdcat"]
order:
description: Order to apply this rule.
required: false
type: int
aliases: ["sudoorder"]
sudooption:
description: List of sudo options.
required: false
type: list
elements: str
aliases: ["options"]
runasuser:
description: List of users for Sudo to execute as.
required: false
type: list
elements: str
runasuser_group:
description: List of groups for Sudo to execute as.
required: false
type: list
elements: str
runasgroup:
description: List of groups for Sudo to execute as.
required: false
type: list
elements: str
hostmask:
description: Host masks of allowed hosts.
required: false
type: list
elements: str
action:
description: Work on sudorule or member level
type: str
@@ -232,6 +352,16 @@ EXAMPLES = """
ipaadmin_password: SomeADMINpassword
name: testrule1
state: absent
# Ensure multiple Sudo Rules are present using batch mode.
- ipasudorule:
ipaadmin_password: SomeADMINpassword
sudorules:
- name: testrule1
hostmask:
- 192.168.122.1/24
- name: testrule2
hostcategory: all
"""
RETURN = """
@@ -239,57 +369,49 @@ RETURN = """
from ansible.module_utils.ansible_freeipa_module import \
IPAAnsibleModule, compare_args_ipa, gen_add_del_lists, gen_add_list, \
gen_intersection_list, api_get_domain, ensure_fqdn, netaddr, to_text
gen_intersection_list, api_get_domain, ensure_fqdn, netaddr, to_text, \
ipalib_errors, convert_param_value_to_lowercase, EntryFactory
def find_sudorule(module, name):
_args = {
"all": True,
"cn": name,
}
_result = module.ipa_command("sudorule_find", name, _args)
if len(_result["result"]) > 1:
module.fail_json(
msg="There is more than one sudorule '%s'" % (name))
elif len(_result["result"]) == 1:
return _result["result"][0]
try:
_result = module.ipa_command("sudorule_show", name, _args)
except ipalib_errors.NotFound:
return None
return _result["result"]
def gen_args(description, usercat, hostcat, cmdcat, runasusercat,
runasgroupcat, order, nomembers):
def gen_args(entry):
"""Generate args for sudorule."""
_args = {}
if description is not None:
_args['description'] = description
if usercat is not None:
_args['usercategory'] = usercat
if hostcat is not None:
_args['hostcategory'] = hostcat
if cmdcat is not None:
_args['cmdcategory'] = cmdcat
if runasusercat is not None:
_args['ipasudorunasusercategory'] = runasusercat
if runasgroupcat is not None:
_args['ipasudorunasgroupcategory'] = runasgroupcat
if order is not None:
_args['sudoorder'] = order
if nomembers is not None:
_args['nomembers'] = nomembers
if entry.description is not None:
_args['description'] = entry.description
if entry.usercategory is not None:
_args['usercategory'] = entry.usercategory
if entry.hostcategory is not None:
_args['hostcategory'] = entry.hostcategory
if entry.cmdcategory is not None:
_args['cmdcategory'] = entry.cmdcategory
if entry.runasusercategory is not None:
_args['ipasudorunasusercategory'] = entry.runasusercategory
if entry.runasgroupcategory is not None:
_args['ipasudorunasgroupcategory'] = entry.runasgroupcategory
if entry.order is not None:
_args['sudoorder'] = entry.order
if entry.nomembers is not None:
_args['nomembers'] = entry.nomembers
return _args
def main():
ansible_module = IPAAnsibleModule(
argument_spec=dict(
# general
name=dict(type="list", elements="str", aliases=["cn"],
required=True),
# present
def init_ansible_module():
"""Initialize IPAAnsibleModule object for sudorule."""
sudorule_spec = dict(
description=dict(required=False, type="str", default=None),
usercategory=dict(required=False, type="str", default=None,
choices=["all", ""], aliases=['usercat']),
@@ -331,88 +453,112 @@ def main():
order=dict(type="int", required=False, aliases=['sudoorder']),
sudooption=dict(required=False, type='list', elements="str",
default=None, aliases=["options"]),
)
ansible_module = IPAAnsibleModule(
argument_spec=dict(
# general
name=dict(type="list", elements="str", aliases=["cn"],
required=False),
sudorules=dict(
type="list",
defalut=None,
options=dict(
# name of the sudorule
name=dict(type="str", required=True, aliases=["cn"]),
# sudorule specific parameters
**sudorule_spec
),
elements='dict',
required=False,
),
# action
action=dict(type="str", default="sudorule",
choices=["member", "sudorule"]),
# state
state=dict(type="str", default="present",
choices=["present", "absent",
"enabled", "disabled"]),
# Specific parameters for simple use case
**sudorule_spec
),
mutually_exclusive=[["name", "sudorules"]],
required_one_of=[["name", "sudorules"]],
supports_check_mode=True,
)
ansible_module._ansible_debug = True
return ansible_module
def convert_list_of_hostmask(hostmasks):
"""Ensure all hostmasks is hostmask_list is a CIDR value."""
return [
to_text(netaddr.IPNetwork(mask).cidr)
for mask in (
hostmasks if isinstance(hostmasks, (list, tuple))
else [hostmasks]
)
]
def convert_list_of_hostnames(hostnames):
"""Ensure all hostnames in hostnames are lowercase FQDN."""
return list(
set(
ensure_fqdn(value.lower(), api_get_domain())
for value in (
hostnames if isinstance(hostnames, (list, tuple))
else [hostnames]
)
)
)
def validate_entry(module, entry, state, action):
"""Ensure entry object is valid."""
if state == "present" and action == "sudorule":
# Ensure the entry is valid for state:present, action:sudorule.
if entry.hostcategory == 'all' and any([entry.host, entry.hostgroup]):
module.fail_json(
msg="Hosts cannot be added when host category='all'"
)
if entry.usercategory == 'all' and any([entry.user, entry.group]):
module.fail_json(
msg="Users cannot be added when user category='all'"
)
if entry.cmdcategory == 'all' \
and any([entry.allow_sudocmd, entry.allow_sudocmdgroup]):
module.fail_json(
msg="Commands cannot be added when command category='all'"
)
return entry
def main():
ansible_module = init_ansible_module()
# Get parameters
# general
names = ansible_module.params_get("name")
# present
# The 'noqa' variables are not used here, but required for vars().
# The use of 'noqa' ensures flake8 does not complain about them.
description = ansible_module.params_get("description") # noqa
cmdcategory = ansible_module.params_get('cmdcategory') # noqa
usercategory = ansible_module.params_get("usercategory") # noqa
hostcategory = ansible_module.params_get("hostcategory") # noqa
runasusercategory = ansible_module.params_get( # noqa
"runasusercategory")
runasgroupcategory = ansible_module.params_get( # noqa
"runasgroupcategory")
hostcategory = ansible_module.params_get("hostcategory") # noqa
nomembers = ansible_module.params_get("nomembers") # noqa
host = ansible_module.params_get("host")
hostgroup = ansible_module.params_get_lowercase("hostgroup")
hostmask = ansible_module.params_get("hostmask")
user = ansible_module.params_get_lowercase("user")
group = ansible_module.params_get_lowercase("group")
allow_sudocmd = ansible_module.params_get('allow_sudocmd')
allow_sudocmdgroup = \
ansible_module.params_get_lowercase('allow_sudocmdgroup')
deny_sudocmd = ansible_module.params_get('deny_sudocmd')
deny_sudocmdgroup = \
ansible_module.params_get_lowercase('deny_sudocmdgroup')
sudooption = ansible_module.params_get("sudooption")
order = ansible_module.params_get("order")
runasuser = ansible_module.params_get_lowercase("runasuser")
runasuser_group = ansible_module.params_get_lowercase("runasuser_group")
runasgroup = ansible_module.params_get_lowercase("runasgroup")
# sudorules = ansible_module.params_get("sudorules")
# action
action = ansible_module.params_get("action")
# state
state = ansible_module.params_get("state")
# ensure hostmasks are network cidr
if hostmask is not None:
hostmask = [to_text(netaddr.IPNetwork(x).cidr) for x in hostmask]
# Check parameters
invalid = []
if state == "present":
if len(names) != 1:
if names is not None and len(names) != 1:
ansible_module.fail_json(
msg="Only one sudorule can be added at a time.")
msg="Only one sudorule can be added at a time using 'name'.")
if action == "member":
invalid = ["description", "usercategory", "hostcategory",
"cmdcategory", "runasusercategory",
"runasgroupcategory", "order", "nomembers"]
else:
if hostcategory == 'all' and any([host, hostgroup]):
ansible_module.fail_json(
msg="Hosts cannot be added when host category='all'")
if usercategory == 'all' and any([user, group]):
ansible_module.fail_json(
msg="Users cannot be added when user category='all'")
if cmdcategory == 'all' \
and any([allow_sudocmd, allow_sudocmdgroup]):
ansible_module.fail_json(
msg="Commands cannot be added when command category='all'")
elif state == "absent":
if len(names) < 1:
ansible_module.fail_json(msg="No name given.")
invalid = ["description", "usercategory", "hostcategory",
"cmdcategory", "runasusercategory",
"runasgroupcategory", "nomembers", "order"]
@@ -424,8 +570,6 @@ def main():
"runasuser_group"])
elif state in ["enabled", "disabled"]:
if len(names) < 1:
ansible_module.fail_json(msg="No name given.")
if action == "member":
ansible_module.fail_json(
msg="Action member can not be used with states enabled and "
@@ -439,24 +583,53 @@ def main():
else:
ansible_module.fail_json(msg="Invalid state '%s'" % state)
ansible_module.params_fail_used_invalid(invalid, state, action)
# Init
changed = False
exit_args = {}
# Factory parameters
params = {
"name": {},
"description": {},
"cmdcategory": {},
"usercategory": {},
"hostcategory": {},
"runasusercategory": {},
"runasgroupcategory": {},
"host": {"convert": [convert_list_of_hostnames]},
"hostgroup": {"convert": [convert_param_value_to_lowercase]},
"hostmask": {"convert": [convert_list_of_hostmask]},
"user": {"convert": [convert_param_value_to_lowercase]},
"group": {"convert": [convert_param_value_to_lowercase]},
"allow_sudocmd": {},
"allow_sudocmdgroup": {"convert": [convert_param_value_to_lowercase]},
"deny_sudocmd": {},
"deny_sudocmdgroup": {"convert": [convert_param_value_to_lowercase]},
"sudooption": {},
"order": {},
"runasuser": {"convert": [convert_param_value_to_lowercase]},
"runasuser_group": {"convert": [convert_param_value_to_lowercase]},
"runasgroup": {"convert": [convert_param_value_to_lowercase]},
"nomembers": {},
}
# Connect to IPA API
with ansible_module.ipa_connect():
default_domain = api_get_domain()
commands = []
# Ensure host is not short hostname.
if host:
host = list(
{ensure_fqdn(value.lower(), default_domain) for value in host}
# Creating factory after connect as host conversion
# requires 'api_get_domain()' to be available
entry_factory = EntryFactory(
ansible_module,
invalid,
"sudorules",
params,
validate_entry=validate_entry,
state=state,
action=action,
)
commands = []
for entry in entry_factory:
host_add, host_del = [], []
user_add, user_del = [], []
group_add, group_del = [], []
@@ -471,16 +644,20 @@ def main():
runasuser_group_add, runasuser_group_del = [], []
runasgroup_add, runasgroup_del = [], []
for name in names:
# Make sure sudorule exists
res_find = find_sudorule(ansible_module, name)
# Try to retrieve sudorule
res_find = find_sudorule(ansible_module, entry.name)
# Fail if sudorule must exist but is not found
if (
(state in ["enabled", "disabled"] or action == "member")
and res_find is None
):
ansible_module.fail_json(msg="No sudorule '%s'" % entry.name)
# Create command
if state == "present":
# Generate args
args = gen_args(description, usercategory, hostcategory,
cmdcategory, runasusercategory,
runasgroupcategory, order, nomembers)
args = gen_args(entry)
if action == "sudorule":
# Found the sudorule
if res_find is not None:
@@ -489,25 +666,35 @@ def main():
# from args if "" and if the category is not in the
# sudorule. The empty string is used to reset the
# category.
if "usercategory" in args \
and args["usercategory"] == "" \
and "usercategory" not in res_find:
if (
"usercategory" in args
and args["usercategory"] == ""
and "usercategory" not in res_find
):
del args["usercategory"]
if "hostcategory" in args \
and args["hostcategory"] == "" \
and "hostcategory" not in res_find:
if (
"hostcategory" in args
and args["hostcategory"] == ""
and "hostcategory" not in res_find
):
del args["hostcategory"]
if "cmdcategory" in args \
and args["cmdcategory"] == "" \
and "cmdcategory" not in res_find:
if (
"cmdcategory" in args
and args["cmdcategory"] == ""
and "cmdcategory" not in res_find
):
del args["cmdcategory"]
if "ipasudorunasusercategory" in args \
and args["ipasudorunasusercategory"] == "" \
and "ipasudorunasusercategory" not in res_find:
if (
"ipasudorunasusercategory" in args
and args["ipasudorunasusercategory"] == ""
and "ipasudorunasusercategory" not in res_find
):
del args["ipasudorunasusercategory"]
if "ipasudorunasgroupcategory" in args \
and args["ipasudorunasgroupcategory"] == "" \
and "ipasudorunasgroupcategory" not in res_find:
if (
"ipasudorunasgroupcategory" in args
and args["ipasudorunasgroupcategory"] == ""
and "ipasudorunasgroupcategory" not in res_find
):
del args["ipasudorunasgroupcategory"]
# For all settings is args, check if there are
@@ -515,46 +702,48 @@ def main():
# If yes: modify
if not compare_args_ipa(ansible_module, args,
res_find):
commands.append([name, "sudorule_mod", args])
commands.append([entry.name, "sudorule_mod", args])
else:
commands.append([name, "sudorule_add", args])
commands.append([entry.name, "sudorule_add", args])
# Set res_find to empty dict for next step
res_find = {}
# Generate addition and removal lists
host_add, host_del = gen_add_del_lists(
host, res_find.get('memberhost_host', []))
entry.host, res_find.get('memberhost_host', []))
hostgroup_add, hostgroup_del = gen_add_del_lists(
hostgroup, res_find.get('memberhost_hostgroup', []))
entry.hostgroup,
res_find.get('memberhost_hostgroup', [])
)
hostmask_add, hostmask_del = gen_add_del_lists(
hostmask, res_find.get('hostmask', []))
entry.hostmask, res_find.get('hostmask', []))
user_add, user_del = gen_add_del_lists(
user, res_find.get('memberuser_user', []))
entry.user, res_find.get('memberuser_user', []))
group_add, group_del = gen_add_del_lists(
group, res_find.get('memberuser_group', []))
entry.group, res_find.get('memberuser_group', []))
allow_cmd_add, allow_cmd_del = gen_add_del_lists(
allow_sudocmd,
entry.allow_sudocmd,
res_find.get('memberallowcmd_sudocmd', []))
allow_cmdgroup_add, allow_cmdgroup_del = gen_add_del_lists(
allow_sudocmdgroup,
entry.allow_sudocmdgroup,
res_find.get('memberallowcmd_sudocmdgroup', []))
deny_cmd_add, deny_cmd_del = gen_add_del_lists(
deny_sudocmd,
entry.deny_sudocmd,
res_find.get('memberdenycmd_sudocmd', []))
deny_cmdgroup_add, deny_cmdgroup_del = gen_add_del_lists(
deny_sudocmdgroup,
entry.deny_sudocmdgroup,
res_find.get('memberdenycmd_sudocmdgroup', []))
sudooption_add, sudooption_del = gen_add_del_lists(
sudooption, res_find.get('ipasudoopt', []))
entry.sudooption, res_find.get('ipasudoopt', []))
# runasuser attribute can be used with both IPA and
# non-IPA (external) users. IPA will handle the correct
@@ -562,15 +751,15 @@ def main():
# the provided list against both users and external
# users list.
runasuser_add, runasuser_del = gen_add_del_lists(
runasuser,
entry.runasuser,
(
res_find.get('ipasudorunas_user', [])
+ res_find.get('ipasudorunasextuser', [])
list(res_find.get('ipasudorunas_user', []))
+ list(res_find.get('ipasudorunasextuser', []))
)
)
runasuser_group_add, runasuser_group_del = (
gen_add_del_lists(
runasuser_group,
entry.runasuser_group,
res_find.get('ipasudorunas_group', [])
)
)
@@ -581,82 +770,81 @@ def main():
# the provided list against both groups and external
# groups list.
runasgroup_add, runasgroup_del = gen_add_del_lists(
runasgroup,
entry.runasgroup,
(
res_find.get('ipasudorunasgroup_group', [])
+ res_find.get('ipasudorunasextgroup', [])
list(res_find.get('ipasudorunasgroup_group', []))
+ list(res_find.get('ipasudorunasextgroup', []))
)
)
elif action == "member":
if res_find is None:
ansible_module.fail_json(msg="No sudorule '%s'" % name)
# Generate add lists for host, hostgroup, user, group,
# allow_sudocmd, allow_sudocmdgroup, deny_sudocmd,
# deny_sudocmdgroup, sudooption, runasuser, runasgroup
# and res_find to only try to add the items that not in
# the sudorule already
if host is not None:
if entry.host is not None:
host_add = gen_add_list(
host, res_find.get("memberhost_host"))
if hostgroup is not None:
entry.host, res_find.get("memberhost_host"))
if entry.hostgroup is not None:
hostgroup_add = gen_add_list(
hostgroup, res_find.get("memberhost_hostgroup"))
if hostmask is not None:
entry.hostgroup,
res_find.get("memberhost_hostgroup")
)
if entry.hostmask is not None:
hostmask_add = gen_add_list(
hostmask, res_find.get("hostmask"))
if user is not None:
entry.hostmask, res_find.get("hostmask"))
if entry.user is not None:
user_add = gen_add_list(
user, res_find.get("memberuser_user"))
if group is not None:
entry.user, res_find.get("memberuser_user"))
if entry.group is not None:
group_add = gen_add_list(
group, res_find.get("memberuser_group"))
if allow_sudocmd is not None:
entry.group, res_find.get("memberuser_group"))
if entry.allow_sudocmd is not None:
allow_cmd_add = gen_add_list(
allow_sudocmd,
entry.allow_sudocmd,
res_find.get("memberallowcmd_sudocmd")
)
if allow_sudocmdgroup is not None:
if entry.allow_sudocmdgroup is not None:
allow_cmdgroup_add = gen_add_list(
allow_sudocmdgroup,
entry.allow_sudocmdgroup,
res_find.get("memberallowcmd_sudocmdgroup")
)
if deny_sudocmd is not None:
if entry.deny_sudocmd is not None:
deny_cmd_add = gen_add_list(
deny_sudocmd,
entry.deny_sudocmd,
res_find.get("memberdenycmd_sudocmd")
)
if deny_sudocmdgroup is not None:
if entry.deny_sudocmdgroup is not None:
deny_cmdgroup_add = gen_add_list(
deny_sudocmdgroup,
entry.deny_sudocmdgroup,
res_find.get("memberdenycmd_sudocmdgroup")
)
if sudooption is not None:
if entry.sudooption is not None:
sudooption_add = gen_add_list(
sudooption, res_find.get("ipasudoopt"))
entry.sudooption, res_find.get("ipasudoopt"))
# runasuser attribute can be used with both IPA and
# non-IPA (external) users, so we need to compare
# the provided list against both users and external
# users list.
if runasuser is not None:
if entry.runasuser is not None:
runasuser_add = gen_add_list(
runasuser,
entry.runasuser,
(list(res_find.get('ipasudorunas_user', []))
+ list(res_find.get('ipasudorunasextuser', [])))
)
if runasuser_group is not None:
if entry.runasuser_group is not None:
runasuser_group_add = gen_add_list(
runasuser_group,
entry.runasuser_group,
res_find.get('ipasudorunas_group', [])
)
# runasgroup attribute can be used with both IPA and
# non-IPA (external) groups, so we need to compare
# the provided list against both users and external
# groups list.
if runasgroup is not None:
if entry.runasgroup is not None:
runasgroup_add = gen_add_list(
runasgroup,
entry.runasgroup,
(list(res_find.get("ipasudorunasgroup_group", []))
+ list(res_find.get("ipasudorunasextgroup", [])))
)
@@ -664,84 +852,83 @@ def main():
elif state == "absent":
if action == "sudorule":
if res_find is not None:
commands.append([name, "sudorule_del", {}])
commands.append([entry.name, "sudorule_del", {}])
elif action == "member":
if res_find is None:
ansible_module.fail_json(msg="No sudorule '%s'" % name)
# Generate intersection lists for host, hostgroup, user,
# group, allow_sudocmd, allow_sudocmdgroup, deny_sudocmd
# deny_sudocmdgroup, sudooption, runasuser, runasgroup
# and res_find to only try to remove the items that are
# in sudorule
if host is not None:
if entry.host is not None:
host_del = gen_intersection_list(
host, res_find.get("memberhost_host"))
entry.host, res_find.get("memberhost_host"))
if hostgroup is not None:
if entry.hostgroup is not None:
hostgroup_del = gen_intersection_list(
hostgroup, res_find.get("memberhost_hostgroup"))
entry.hostgroup,
res_find.get("memberhost_hostgroup")
)
if hostmask is not None:
if entry.hostmask is not None:
hostmask_del = gen_intersection_list(
hostmask, res_find.get("hostmask"))
entry.hostmask, res_find.get("hostmask"))
if user is not None:
if entry.user is not None:
user_del = gen_intersection_list(
user, res_find.get("memberuser_user"))
entry.user, res_find.get("memberuser_user"))
if group is not None:
if entry.group is not None:
group_del = gen_intersection_list(
group, res_find.get("memberuser_group"))
entry.group, res_find.get("memberuser_group"))
if allow_sudocmd is not None:
if entry.allow_sudocmd is not None:
allow_cmd_del = gen_intersection_list(
allow_sudocmd,
entry.allow_sudocmd,
res_find.get("memberallowcmd_sudocmd")
)
if allow_sudocmdgroup is not None:
if entry.allow_sudocmdgroup is not None:
allow_cmdgroup_del = gen_intersection_list(
allow_sudocmdgroup,
entry.allow_sudocmdgroup,
res_find.get("memberallowcmd_sudocmdgroup")
)
if deny_sudocmd is not None:
if entry.deny_sudocmd is not None:
deny_cmd_del = gen_intersection_list(
deny_sudocmd,
entry.deny_sudocmd,
res_find.get("memberdenycmd_sudocmd")
)
if deny_sudocmdgroup is not None:
if entry.deny_sudocmdgroup is not None:
deny_cmdgroup_del = gen_intersection_list(
deny_sudocmdgroup,
entry.deny_sudocmdgroup,
res_find.get("memberdenycmd_sudocmdgroup")
)
if sudooption is not None:
if entry.sudooption is not None:
sudooption_del = gen_intersection_list(
sudooption, res_find.get("ipasudoopt"))
entry.sudooption, res_find.get("ipasudoopt"))
# runasuser attribute can be used with both IPA and
# non-IPA (external) users, so we need to compare
# the provided list against both users and external
# users list.
if runasuser is not None:
if entry.runasuser is not None:
runasuser_del = gen_intersection_list(
runasuser,
entry.runasuser,
(
list(res_find.get('ipasudorunas_user', []))
+ list(res_find.get('ipasudorunasextuser', []))
)
)
if runasuser_group is not None:
if entry.runasuser_group is not None:
runasuser_group_del = gen_intersection_list(
runasuser_group,
entry.runasuser_group,
res_find.get('ipasudorunas_group', [])
)
# runasgroup attribute can be used with both IPA and
# non-IPA (external) groups, so we need to compare
# the provided list against both groups and external
# groups list.
if runasgroup is not None:
if entry.runasgroup is not None:
runasgroup_del = gen_intersection_list(
runasgroup,
entry.runasgroup,
(
list(res_find.get(
"ipasudorunasgroup_group", []))
@@ -751,8 +938,6 @@ def main():
)
elif state == "enabled":
if res_find is None:
ansible_module.fail_json(msg="No sudorule '%s'" % name)
# sudorule_enable is not failing on an enabled sudorule
# Therefore it is needed to have a look at the ipaenabledflag
# in res_find.
@@ -762,11 +947,9 @@ def main():
# See: https://github.com/freeipa/freeipa/pull/6294
enabled_flag = str(res_find.get("ipaenabledflag", [False])[0])
if enabled_flag.upper() != "TRUE":
commands.append([name, "sudorule_enable", {}])
commands.append([entry.name, "sudorule_enable", {}])
elif state == "disabled":
if res_find is None:
ansible_module.fail_json(msg="No sudorule '%s'" % name)
# sudorule_disable is not failing on an disabled sudorule
# Therefore it is needed to have a look at the ipaenabledflag
# in res_find.
@@ -776,7 +959,7 @@ def main():
# See: https://github.com/freeipa/freeipa/pull/6294
enabled_flag = str(res_find.get("ipaenabledflag", [False])[0])
if enabled_flag.upper() != "FALSE":
commands.append([name, "sudorule_disable", {}])
commands.append([entry.name, "sudorule_disable", {}])
else:
ansible_module.fail_json(msg="Unkown state '%s'" % state)
@@ -788,31 +971,31 @@ def main():
# An empty Hostmask cannot be used, or IPA API will fail.
if hostmask_add:
params["hostmask"] = hostmask_add
commands.append([name, "sudorule_add_host", params])
commands.append([entry.name, "sudorule_add_host", params])
if any([host_del, hostgroup_del, hostmask_del]):
params = {"host": host_del, "hostgroup": hostgroup_del}
# An empty Hostmask cannot be used, or IPA API will fail.
if hostmask_del:
params["hostmask"] = hostmask_del
commands.append([name, "sudorule_remove_host", params])
commands.append([entry.name, "sudorule_remove_host", params])
# Manage users and groups
if user_add or group_add:
commands.append([
name, "sudorule_add_user",
entry.name, "sudorule_add_user",
{"user": user_add, "group": group_add}
])
if user_del or group_del:
commands.append([
name, "sudorule_remove_user",
entry.name, "sudorule_remove_user",
{"user": user_del, "group": group_del}
])
# Manage commands allowed
if allow_cmd_add or allow_cmdgroup_add:
commands.append([
name, "sudorule_add_allow_command",
entry.name, "sudorule_add_allow_command",
{
"sudocmd": allow_cmd_add,
"sudocmdgroup": allow_cmdgroup_add,
@@ -820,7 +1003,7 @@ def main():
])
if allow_cmd_del or allow_cmdgroup_del:
commands.append([
name, "sudorule_remove_allow_command",
entry.name, "sudorule_remove_allow_command",
{
"sudocmd": allow_cmd_del,
"sudocmdgroup": allow_cmdgroup_del
@@ -829,7 +1012,7 @@ def main():
# Manage commands denied
if deny_cmd_add or deny_cmdgroup_add:
commands.append([
name, "sudorule_add_deny_command",
entry.name, "sudorule_add_deny_command",
{
"sudocmd": deny_cmd_add,
"sudocmdgroup": deny_cmdgroup_add,
@@ -837,7 +1020,7 @@ def main():
])
if deny_cmd_del or deny_cmdgroup_del:
commands.append([
name, "sudorule_remove_deny_command",
entry.name, "sudorule_remove_deny_command",
{
"sudocmd": deny_cmd_del,
"sudocmdgroup": deny_cmdgroup_del
@@ -851,10 +1034,10 @@ def main():
_args["user"] = runasuser_add
if runasuser_group_add:
_args["group"] = runasuser_group_add
commands.append([name, "sudorule_add_runasuser", _args])
commands.append([entry.name, "sudorule_add_runasuser", _args])
if runasuser_del or runasuser_group_del:
commands.append([
name,
entry.name,
"sudorule_remove_runasuser",
{"user": runasuser_del, "group": runasuser_group_del}
])
@@ -862,29 +1045,32 @@ def main():
# Manage RunAS Groups
if runasgroup_add:
commands.append([
name, "sudorule_add_runasgroup", {"group": runasgroup_add}
entry.name, "sudorule_add_runasgroup",
{"group": runasgroup_add}
])
if runasgroup_del:
commands.append([
name, "sudorule_remove_runasgroup",
entry.name, "sudorule_remove_runasgroup",
{"group": runasgroup_del}
])
# Manage sudo options
if sudooption_add:
for option in sudooption_add:
commands.append([
name, "sudorule_add_option", {"ipasudoopt": option}
entry.name, "sudorule_add_option",
{"ipasudoopt": option}
])
if sudooption_del:
for option in sudooption_del:
commands.append([
name, "sudorule_remove_option", {"ipasudoopt": option}
entry.name, "sudorule_remove_option",
{"ipasudoopt": option}
])
# Execute commands
changed = ansible_module.execute_ipa_commands(
commands, fail_on_member_errors=True)
commands, batch=True, fail_on_member_errors=True)
# Done

View File

@@ -37,3 +37,15 @@
when: groups['ipaclients'] is not defined or not groups['ipaclients']
vars:
ipa_context: client
- name: Test sudorule using client context, in client host.
ansible.builtin.import_playbook: test_sudorules.yml
when: groups['ipaclients']
vars:
ipa_test_host: ipaclients
- name: Test sudorule using client context, in server host.
ansible.builtin.import_playbook: test_sudorules.yml
when: groups['ipaclients'] is not defined or not groups['ipaclients']
vars:
ipa_context: client

View File

@@ -0,0 +1,382 @@
---
- name: Test sudorule
hosts: "{{ ipa_test_host | default('ipaserver') }}"
become: false
gather_facts: true # required for ansible_facts['fqdn']
module_defaults:
ipauser:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipagroup:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipahostgroup:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipasudocmdgroup:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipasudocmd:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipasudorule:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
tasks:
# setup
- name: Ensure ansible facts for DNS are available
ansible.builtin.setup:
gather_subset: dns
- name: Ensure test users are absent
ipauser:
name:
- user01
- user02
state: absent
- name: Ensure test groups are absent
ipagroup:
name:
- group01
- group02
state: absent
- name: Ensure test hostgroup is absent
ipahostgroup:
name: cluster
state: absent
- name: Ensure test users are present
ipauser:
users:
- name: user01
first: user
last: zeroone
- name: user02
first: user
last: zerotwo
- name: Ensure groups are present
ipagroup:
groups:
- name: group01
user: user01
- name: group02
- name: Ensure sudocmdgroup is absent
ipasudocmdgroup:
name: test_sudorule_cmdgroup
state: absent
- name: Ensure hostgroup is present, with a host.
ipahostgroup:
name: cluster
host: "{{ ansible_facts['fqdn'] }}"
- name: Ensure some sudocmds are available
ipasudocmd:
name:
- /sbin/ifconfig
- /usr/bin/vim
- /usr/bin/emacs
state: present
- name: Ensure sudocmdgroup is available
ipasudocmdgroup:
name: test_sudorule_cmdgroup
sudocmd: /usr/bin/vim
state: present
- name: Ensure another sudocmdgroup is available
ipasudocmdgroup:
name: test_sudorule_cmdgroup_2
sudocmd: /usr/bin/emacs
state: present
- name: Ensure sudorules are absent
ipasudorule:
name:
- testrule1
- testrule2
- allusers
- allhosts
- allcommands
state: absent
# tests
- name: Run sudorules tests.
block:
- name: Ensure sudorules are present
ipasudorule:
sudorules:
- name: testrule1
- name: testrule2
- name: allhosts
- name: allcommands
register: result
failed_when: not result.changed or result.failed
- name: Ensure sudorules are present, again
ipasudorule:
sudorules:
- name: testrule1
- name: testrule2
- name: allhosts
- name: allcommands
register: result
failed_when: result.changed or result.failed
- name: Ensure testrule1 and testrule2 are absent
ipasudorule:
sudorules:
- name: testrule1
- name: testrule2
state: absent
register: result
failed_when: not result.changed or result.failed
- name: Ensure testrule1 and testrule2 are absent, again
ipasudorule:
sudorules:
- name: testrule1
- name: testrule2
state: absent
register: result
failed_when: result.changed or result.failed
- name: Ensure allhosts and allcommands sudorules are still present
ipasudorule:
sudorules:
- name: allhosts
- name: allcomands
state: absent
check_mode: true
register: result
failed_when: not result.changed or result.failed
- name: Ensure sudorules with parameters are present
ipasudorule:
sudorules:
- name: testrule1
runasuser:
- user01
- name: testrule2
runasuser_group:
- group01
state: present
register: result
failed_when: not result.changed or result.failed
- name: Ensure sudorules with parameters are present, again
ipasudorule:
sudorules:
- name: testrule1
runasuser:
- user01
- name: testrule2
runasuser_group:
- group01
state: present
register: result
failed_when: result.changed or result.failed
- name: Ensure sudorules with parameters are modified
ipasudorule:
sudorules:
- name: testrule1
runasuser:
- user02
- name: testrule2
runasuser_group:
- group02
state: present
register: result
failed_when: not result.changed or result.failed
- name: Ensure sudorules with parameters are modified again
ipasudorule:
sudorules:
- name: testrule1
runasuser:
- user02
- name: testrule2
runasuser_group:
- group02
state: present
register: result
failed_when: result.changed or result.failed
- name: Ensure sudorules members can be modified
ipasudorule:
sudorules:
- name: testrule1
runasuser:
- user01
- name: testrule2
runasuser_group:
- group01
action: member
state: present
register: result
failed_when: not result.changed or result.failed
- name: Ensure sudorules members can modified, again
ipasudorule:
sudorules:
- name: testrule1
runasuser:
- user01
- user02
- name: testrule2
runasuser_group:
- group01
- group02
action: member
state: present
register: result
failed_when: result.changed or result.failed
- name: Ensure sudorules members are absent
ipasudorule:
sudorules:
- name: testrule1
runasuser:
- user01
- name: testrule2
runasuser_group:
- group02
action: member
state: absent
register: result
failed_when: not result.changed or result.failed
- name: Ensure sudorules members are absent, again
ipasudorule:
sudorules:
- name: testrule1
runasuser:
- user01
- name: testrule2
runasuser_group:
- group02
action: member
state: absent
register: result
failed_when: result.changed or result.failed
- name: Ensure testrule1 and testrule2 are present, with proper attributes
ipasudorule:
sudorules:
- name: testrule1
runasuser:
- user02
- name: testrule2
runasuser_group:
- group01
state: present
register: result
failed_when: result.changed or result.failed
- name: Ensure testrule1 and testrule2 are disabled
ipasudorule:
sudorules:
- name: testrule1
- name: testrule2
state: disabled
register: result
failed_when: not result.changed or result.failed
- name: Ensure testrule1 and testrule2 are disabled, again
ipasudorule:
sudorules:
- name: testrule1
- name: testrule2
state: disabled
register: result
failed_when: result.changed or result.failed
- name: Ensure testrule1 and testrule2 are enabled
ipasudorule:
sudorules:
- name: testrule1
- name: testrule2
state: enabled
register: result
failed_when: not result.changed or result.failed
- name: Ensure testrule1 and testrule2 are enabled, again
ipasudorule:
sudorules:
- name: testrule1
- name: testrule2
state: enabled
register: result
failed_when: result.changed or result.failed
- name: Ensure multiple sudorules cannot be enabled with invalid parameters
ipasudorule:
sudorules:
- name: testrule1
runasuser: user01
- name: testrule2
runasuser: user01
state: enabled
register: result
failed_when: not result.failed and "Argument 'runasuser' can not be used with action 'sudorule' and state 'enabled'" not in result.msg
- name: Ensure multiple sudorules cannot be disabled with invalid parameters
ipasudorule:
sudorules:
- name: testrule1
runasuser: user01
- name: testrule2
runasuser: user01
state: disabled
register: result
failed_when: not result.failed and "Argument 'runasuser' can not be used with action 'sudorule' and state 'disabled'" not in result.msg
# cleanup
always:
- name: Cleanup sudorules
ipasudorule:
name:
- testrule1
- testrule2
- allusers
- allhosts
- allcommands
state: absent
- name: Ensure sudocmdgroup is absent
ipasudocmdgroup:
name:
- test_sudorule_cmdgroup
- test_sudorule_cmdgroup_2
state: absent
- name: Ensure sudocmds are absent
ipasudocmd:
name:
- /sbin/ifconfig
- /usr/bin/vim
- /usr/bin/emacs
state: absent
- name: Ensure hostgroup is absent.
ipahostgroup:
name: cluster
state: absent
- name: Ensure groups are absent
ipagroup:
name: group01,group02
state: absent
- name: Ensure user is absent
ipauser:
name: user01,user02
state: absent

View File

@@ -0,0 +1,311 @@
---
- name: Test sudorules members should be case insensitive.
hosts: "{{ ipa_test_host | default('ipaserver') }}"
become: false
gather_facts: false
module_defaults:
ipauser:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipagroup:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipahost:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipahostgroup:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipasudocmdgroup:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipasudocmd:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
ipasudorule:
ipaadmin_password: SomeADMINpassword
ipaapi_context: "{{ ipa_context | default(omit) }}"
vars:
groups_present:
- eleMENT1
- Element2
- eLeMenT3
- ElemENT4
tasks:
- name: Test sudorule member case insensitive
block:
# SETUP
- name: Ensure domain name
ansible.builtin.set_fact:
ipa_domain: ipa.test
when: ipa_domain is not defined
- name: Ensure test groups are absent.
ipagroup:
name: "{{ groups_present }}"
state: absent
- name: Ensure test hostgroups are absent.
ipahostgroup:
name: "{{ groups_present }}"
state: absent
- name: Ensure test users are absent.
ipauser:
name: "{{ groups_present }}"
state: absent
- name: Ensure test groups exist.
ipagroup:
name: "{{ item }}"
loop: "{{ groups_present }}"
- name: Ensure test hostgroups exist.
ipahostgroup:
name: "{{ item }}"
loop: "{{ groups_present }}"
- name: Ensure test hosts exist.
ipahost:
name: "{{ item }}.{{ ipa_domain }}"
force: yes
loop: "{{ groups_present }}"
- name: Ensure test users exist.
ipauser:
name: "user{{ item }}"
first: "{{ item }}"
last: "{{ item }}"
loop: "{{ groups_present }}"
- name: Ensure sudorule do not exist
ipasudorule:
sudorules:
- name: "{{ item }}"
state: absent
loop: "{{ groups_present }}"
# TESTS
- name: Ensure sudorule exist with runasusers members
ipasudorule:
sudorules:
- name: "{{ item }}"
cmdcategory: all
runasuser: "user{{ item }}"
loop: "{{ groups_present }}"
register: result
failed_when: result.failed or not result.changed
- name: Ensure sudorule exist with lowercase runasusers members
ipasudorule:
sudorules:
- name: "{{ item }}"
cmdcategory: all
runasuser: "user{{ item | lower }}"
loop: "{{ groups_present }}"
register: result
failed_when: result.failed or result.changed
- name: Ensure sudorule exist with uppercase runasusers members
ipasudorule:
sudorules:
- name: "{{ item }}"
cmdcategory: all
runasuser: "user{{ item | upper }}"
loop: "{{ groups_present }}"
register: result
failed_when: result.failed or result.changed
- name: Ensure sudorule exist with runasgroup members
ipasudorule:
sudorules:
- name: "{{ item }}"
cmdcategory: all
runasgroup: "{{ item }}"
loop: "{{ groups_present }}"
register: result
failed_when: result.failed or not result.changed
- name: Ensure sudorule exist with lowercase runasgroup members
ipasudorule:
sudorules:
- name: "{{ item }}"
cmdcategory: all
runasgroup: "{{ item | lower }}"
loop: "{{ groups_present }}"
register: result
failed_when: result.failed or result.changed
- name: Ensure sudorule exist with uppercase runasgroup members
ipasudorule:
sudorules:
- name: "{{ item }}"
cmdcategory: all
runasgroup: "{{ item | upper }}"
loop: "{{ groups_present }}"
register: result
failed_when: result.failed or result.changed
- name: Ensure sudorule do not exist
ipasudorule:
sudorules:
- name: "{{ item }}"
state: absent
loop: "{{ groups_present }}"
#####
- name: Ensure sudorule exist with members
ipasudorule:
sudorules:
- name: "{{ item }}"
cmdcategory: all
hostgroup: "{{ item }}"
host: "{{ item }}.{{ ipa_domain }}"
group: "{{ item }}"
user: "user{{ item }}"
loop: "{{ groups_present }}"
register: result
failed_when: result.failed or not result.changed
- name: Ensure sudorule exist with members, lowercase
ipasudorule:
sudorules:
- name: "{{ item }}"
cmdcategory: all
hostgroup: "{{ item | lower }}"
host: "{{ item | lower }}.{{ ipa_domain }}"
group: "{{ item | lower }}"
user: "user{{ item | lower }}"
loop: "{{ groups_present }}"
register: result
failed_when: result.failed or result.changed
- name: Ensure sudorule exist with members, uppercase
ipasudorule:
sudorules:
- name: "{{ item }}"
cmdcategory: all
hostgroup: "{{ item | upper }}"
host: "{{ item | upper }}.{{ ipa_domain }}"
group: "{{ item | upper }}"
user: "user{{ item | upper }}"
loop: "{{ groups_present }}"
register: result
failed_when: result.failed or result.changed
- name: Ensure sudorule member is absent
ipasudorule:
sudorules:
- name: "{{ item }}"
hostgroup: "{{ item }}"
host: "{{ item }}.{{ ipa_domain }}"
group: "{{ item }}"
user: "user{{ item }}"
action: member
state: absent
loop: "{{ groups_present }}"
register: result
failed_when: result.failed or not result.changed
- name: Ensure sudorule member is absent, lowercase
ipasudorule:
sudorules:
- name: "{{ item }}"
hostgroup: "{{ item | lower }}"
host: "{{ item | lower }}.{{ ipa_domain }}"
group: "{{ item | lower }}"
user: "user{{ item | lower }}"
action: member
state: absent
loop: "{{ groups_present }}"
register: result
failed_when: result.failed or result.changed
- name: Ensure sudorule member is absent, upercase
ipasudorule:
sudorules:
- name: "{{ item }}"
hostgroup: "{{ item | upper }}"
host: "{{ item | upper }}.{{ ipa_domain }}"
group: "{{ item | upper }}"
user: "user{{ item | upper }}"
action: member
state: absent
loop: "{{ groups_present }}"
register: result
failed_when: result.failed or result.changed
- name: Ensure sudorule member is present, upercase
ipasudorule:
sudorules:
- name: "{{ item }}"
hostgroup: "{{ item | upper }}"
host: "{{ item | upper }}.{{ ipa_domain }}"
group: "{{ item | upper }}"
user: "user{{ item | upper }}"
action: member
loop: "{{ groups_present }}"
register: result
failed_when: result.failed or not result.changed
- name: Ensure sudorule member is present, lowercase
ipasudorule:
sudorules:
- name: "{{ item }}"
hostgroup: "{{ item | lower }}"
host: "{{ item | lower }}.{{ ipa_domain }}"
group: "{{ item | lower }}"
user: "user{{ item | lower }}"
action: member
loop: "{{ groups_present }}"
register: result
failed_when: result.failed or result.changed
- name: Ensure sudorule member is present, mixed case
ipasudorule:
sudorules:
- name: "{{ item }}"
hostgroup: "{{ item }}"
host: "{{ item }}.{{ ipa_domain }}"
group: "{{ item }}"
user: "user{{ item }}"
action: member
loop: "{{ groups_present }}"
register: result
failed_when: result.failed or result.changed
# cleanup
always:
- name: Ensure sudorule do not exist
ipasudorule:
name: "{{ item }}"
state: absent
loop: "{{ groups_present }}"
- name: Ensure test groups do not exist.
ipagroup:
name: "{{ item }}"
state: absent
loop: "{{ groups_present }}"
- name: Ensure test hostgroups do not exist.
ipahostgroup:
name: "{{ item }}"
state: absent
loop: "{{ groups_present }}"
- name: Ensure test hosts do not exist.
ipahost:
name: "{{ item }}.{{ ipa_domain }}"
state: absent
loop: "{{ groups_present }}"
- name: Ensure test users do not exist.
ipauser:
name: "user{{ item }}"
state: absent
loop: "{{ groups_present }}"