Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DRAFT]: Add support for state:query to management modules #782

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions plugins/module_utils/ansible_freeipa_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -1356,3 +1356,84 @@ def exception_handler(module, ex, exit_args, one_name):
self.fail_json(msg=", ".join(_errors))

return changed

def execute_query(self, names, prefix, name_ipa_param,
query_param, find_command, query_param_settings):
"""
Execute query state.

Parameters
----------
names: The main items to return
It names is not None and not an empty list then all items
found with "item_find" are returned, else the items in names.
prefix: The prefix for use with several main items
The prefix is "users" for the "user" module. It is used
if only the list of main items (example: users) is returned.
name_ipa_param: The IPA param name of the name parameter
This is for example "uid" that is used for the user name in
the user module.
query_param: The parameters to return
The parameters that should be returned. If query_param is
["ALL"], all parameters in ipa_pram_names will be returned.
query_param_settings: IPA base parameters, all and mapping
The dict provides all parameters the "ALL" list and the
mapping of the default module paramter name to IPA option name
if it is not the same.
Example: "uid" for user name of the user commands.
find_command: The find function
This is a module function that returns the structure(s) from
the show or find command.

"""

def store_params(exit_args, name, prefix, name_ipa_param, result,
params):
if params is None:
exit_args.setdefault(prefix, []).append(
result[name_ipa_param])
return
for field in params:
if field not in query_param_settings["ALL"]:
self.fail_json(
msg="query_param '%s' is not supported" % field)
if "mapping" in query_param_settings and \
field in query_param_settings["mapping"]:
ipa_field = query_param_settings["mapping"][field]
else:
ipa_field = field

if ipa_field in result:
value = result[ipa_field]
if name is None:
exit_args[field] = value
else:
exit_args.setdefault(name, {})[field] = value

# Create exit_args
exit_args = {}

if query_param == ["BASE"]:
query_param = query_param_settings["BASE"]
elif query_param == ["ALL"]:
query_param = query_param_settings["ALL"]

if names and isinstance(names, list):
with_name = len(names) > 1
for name in names:
result = find_command(self, name,
pkey_only=query_param is None)
if result:
store_params(exit_args, name if with_name else None,
prefix, name_ipa_param, result,
query_param)
else:
results = find_command(self, None,
pkey_only=query_param is None)
if results is not None:
for result in results:
name = result[name_ipa_param]
store_params(exit_args, name, prefix, name_ipa_param,
result, query_param)

return exit_args
138 changes: 116 additions & 22 deletions plugins/modules/ipagroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@
required: false
type: list
elements: str
query_param:
description: The fields to query with state=query
required: false
action:
description: Work on group or member level
type: str
Expand All @@ -202,7 +205,8 @@
description: State to ensure
type: str
default: present
choices: ["present", "absent"]
choices: ["present", "absent",
"query"]
author:
- Thomas Woerner (@t-woerner)
"""
Expand Down Expand Up @@ -310,7 +314,7 @@
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
gen_add_list, gen_intersection_list, api_check_param, ipalib_errors
from ansible.module_utils import six
if six.PY3:
unicode = str
Expand All @@ -327,28 +331,67 @@
"deepcopy" in baseldap.LDAPObject.__json__.__code__.co_names


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

_result = module.ipa_command("group_find", name, _args)
try:
_result = module.ipa_command("group_show", name, _args).get("result")
except ipalib_errors.NotFound:
return None

# 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 _result:
_result["member_service"] = \
[to_text(svc).lower() for svc in _result["member_service"]]
return _result


def convert_result(res):
_res = {}
for key in res:
if isinstance(res[key], list):
# All single value parameters should not be lists
# This does not apply to manager, krbprincipalname,
# usercertificate and ipacertmapdata
if len(res[key]) == 1:
_res[key] = to_text(res[key][0])
else:
_res[key] = [to_text(item) for item in res[key]]
elif key in ["gidNumber"]:
_res[key] = int(res[key])
else:
_res[key] = to_text(res[key])
return _res


def group_find(module, name, pkey_only=False, sizelimit=None, timelimit=None):
_args = {"all": True}

if name:
_args["cn"] = name
if pkey_only:
_args["pkey_only"] = True
if sizelimit is not None:
_args["sizelimit"] = sizelimit
if timelimit is not None:
_args["timelimit"] = timelimit

try:
_result = module.ipa_command_no_name("group_find", _args).get("result")
if _result:
if name:
_result = convert_result(_result[0])
else:
_result = [convert_result(res) for res in _result]

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
except ipalib_errors.NotFound:
return None

return None
return _result


def gen_args(description, gid, nomembers):
Expand Down Expand Up @@ -392,6 +435,11 @@ def check_parameters(module, state, action):
if action == "group":
invalid.extend(["user", "group", "service", "externalmember"])

if state == "query":
invalid.append("groups")
else:
invalid.append("query_param")

module.params_fail_used_invalid(invalid, state, action)


Expand Down Expand Up @@ -422,6 +470,31 @@ def check_objectclass_args(module, res_find, posix, external):
"`non-posix`.")


query_param_settings = {
"ALL": [
"dn", "objectclass", "ipauniqueid", "ipantsecurityidentifier",
"name",
"description",
"gid",
"nomembers",
"user", "group", "service", "external",
"idoverrideuser"
],
"BASE": [
"name", "description", "gid"
],
"mapping": {
"name": "cn",
"gid": "gidnumber",
"user": "member_user",
"group": "member_group",
"service": "member_service",
"externalmember": "member_external",
"idoverrideuser": "member_idoverrideuser",
}
}


def main():
group_spec = dict(
# present
Expand Down Expand Up @@ -466,11 +539,16 @@ def main():
),
elements='dict',
required=False),
# query
query_param=dict(type="list", default=None,
choices=["ALL", "BASE"].extend(
query_param_settings["ALL"]),
required=False),
# general
action=dict(type="str", default="group",
choices=["member", "group"]),
state=dict(type="str", default="present",
choices=["present", "absent"]),
choices=["present", "absent", "query"]),

# Add group specific parameters for simple use case
**group_spec
Expand All @@ -479,7 +557,7 @@ def main():
# same time
mutually_exclusive=[['posix', 'nonposix', 'external'],
["name", "groups"]],
required_one_of=[["name", "groups"]],
# required_one_of=[["name", "groups"]] is handled below
supports_check_mode=True,
)

Expand All @@ -506,13 +584,17 @@ def main():
membermanager_user = ansible_module.params_get("membermanager_user")
membermanager_group = ansible_module.params_get("membermanager_group")
externalmember = ansible_module.params_get("externalmember")
# query
query_param = ansible_module.params_get("query_param")
# action
action = ansible_module.params_get("action")
# state
state = ansible_module.params_get("state")

# Check parameters

if (names is None or len(names) < 1) and \
if state != "query" and \
(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")

Expand All @@ -521,6 +603,11 @@ def main():
ansible_module.fail_json(
msg="Only one group can be added at a time using 'name'.")

if state == "query":
if action == "member":
ansible_module.fail_json(
msg="Query is not possible with action=query")

check_parameters(ansible_module, state, action)

if external is False:
Expand Down Expand Up @@ -567,6 +654,13 @@ def main():
# Connect to IPA API
with ansible_module.ipa_connect(context=context):

if state == "query":
exit_args = ansible_module.execute_query(
names, "groups", "cn", query_param, group_find,
query_param_settings)

ansible_module.exit_json(changed=False, group=exit_args)

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:
Expand Down Expand Up @@ -643,7 +737,7 @@ def main():
repr(group_name))

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

user_add, user_del = [], []
group_add, group_del = [], []
Expand Down
Loading