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

My take on state:fetched #3

Open
wants to merge 4 commits into
base: state_fetched
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
212 changes: 212 additions & 0 deletions plugins/module_utils/ansible_freeipa_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,218 @@ def exception_handler(module, ex, exit_args, one_name):

return changed

def fetch_objects(
self, object_type, object_key, ipa_param_mapping,
obj_filter=None, encoding_fn=None, sizelimit=None, timelimit=None
):
"""
Fetch objects using IPA API *_find calls.

Parameters
----------
object_type: string
The IPA object to search (e.g.: "user", "automember").
object_key: list
The IPA attribute name that uniquely identify the object.
ipa_param_mapping: dict
A mapping of <playbook param>:<ipa attribute>. It must also
include the 'object_key' keys mapping (e.g. 'name': 'uid' for
users).
obj_filter: function
An optional predicate function that evaluates to True if the
object is to be added to the result set.
encoding_fn: dict
An optional mapping <ipa attribute>: <encoding function>. The
encoding function will be applied to the 'ipa attribute', if a
function is not provided a 'best guess' one will be used
(either the buitin for the defined data type, or 'to_text').
sizelimit: int
Optionally set the size limit for the IPA API query.
timelimit: int
Optionally set the time limit for the IPA API query.

"""
self.warn("Fetching objects might be limited by 'sizelimit'.")
self.warn("Fetching objects might fail due to 'timelimit'.")

if not isinstance(object_key, (list, tuple)):
object_key = [object_key]
if not obj_filter:
obj_filter = bool
fetch_params = self.params_get("fetch_param") or []
param_ipa_map = {v: k for k, v in ipa_param_mapping.items()}
encoding_fn = encoding_fn or {}

# retrieve all objects.
try:
_args = {"all": True}
if sizelimit is not None:
_args["sizelimit"] = sizelimit
if timelimit is not None:
_args["timelimit"] = timelimit
_result = self.ipa_command_no_name(
"%s_find" % object_type, _args
)
except ipalib_errors.NotFound:
return []
else:
_result = _result.get("result")

# Filter objects to be returned.
_result = [res for res in _result if obj_filter(res)]

for res in _result:
# All single value parameters should not be lists
for param in res:
mod_arg = param_ipa_map.get(param)
arg_desc = self.argument_spec.get(mod_arg)
# Mark params that will not be returned.
if (
mod_arg not in object_key
and "all" not in fetch_params
and mod_arg not in fetch_params
and (arg_desc is None or arg_desc.get("no_log"))
):
res[param] = None
continue
# Format data to be returned.
# Apply encoding function to parameters
encode = encoding_fn.get(param)
# if encode function is not set, apply the builtin
# conversion for the specified data type.
if not encode:
param_type = (arg_desc or {}).get("type", "str")
arg_type = getattr(sys.modules["builtins"], param_type)
if (
param_type in ("list", "tuple", "str")
or not arg_type
):
encode = to_text
else:
encode = arg_type

if not isinstance(res[param], (list, tuple)):
res[param] = [res[param]]
# Apply data encoding
res[param] = [encode(x) for x in res[param]]
# Return only actual object if list has a single element.
if len(res[param]) == 1:
res[param] = res[param][0]

# Return object data, if requested.
if fetch_params:
return [
{
param_ipa_map[k]: v
for k, v in res.items()
if v is not None and k in param_ipa_map
}
for res in _result
]
# Otherwise, return the object "keys"
# If object key is compound, each object is returend as a dict.
return [
res[ipa_param_mapping[object_key[0]]]
if len(object_key) == 1
else {k: res[ipa_param_mapping[k]] for k in object_key}
for res in _result
]

def execute_fetched(self, exit_args, names, prefix, name_ipa_param,
fetch_params, ipa_param_mapping,
ipa_param_converter,
show_function, find_function):
"""
Execute fetched state.

Parameters
----------
exit_args: Exit args dict
This is the dict that will be resturned with
ansible_module.exit_json
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.
fetch_params: The parameters to return
The parameters that should be returned. If fetch_params is
["all"], all parameters in ipa_pram_names will be returned.
ipa_param_mapping: IPA param mapping
This is the mapping of the default module paramter name to
IPA option name.
Example: "uid" for user name of the user commands.
ipa_param_converter: Parameter converter
This is an extra parameter converter for parameters that
need to be converted into integers for example.
show_function: The function to show one entry
This is "user-show" for the user command.
find_function: The function to find several entries
This is "user-find" for the user command.

Example (ipauser module):

if state == "fetched":
changed = ansible_module.execute_fetched(
exit_args, "users", "uid", fetch_params, ipa_param_mapping,
names, user_show, user_find)
ansible_module.exit_json(changed=False, user=exit_args)

"""

def store_params(exit_args, name, prefix, name_ipa_param, result,
params, ipa_param_converter):
if params is None:
exit_args.setdefault(prefix, []).append(
result[name_ipa_param])
return
for field in params:
ipa_field = ipa_param_mapping[field]

if ipa_field in result:
value = result[ipa_field]
if ipa_param_converter and \
field in ipa_param_converter:
if isinstance(value, list):
value = [ipa_param_converter[field](val)
for val in value]
else:
value = ipa_param_converter[field](value)
else:
if isinstance(value, list):
value = [to_text(val) for val in value]
else:
value = to_text(value)
if name is None:
exit_args[field] = value
else:
exit_args.setdefault(name, {})[field] = value

if fetch_params == ["all"]:
fetch_params = ipa_param_mapping.keys()

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

return False

class FreeIPABaseModule(IPAAnsibleModule):
"""
Base class for FreeIPA Ansible modules.
Expand Down
64 changes: 60 additions & 4 deletions plugins/modules/ipaservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,17 @@
required: false
default: True
type: bool
fetch_param:
description: The fields to fetch with state=fetched
required: false
action:
description: Work on service or member level
default: service
choices: ["member", "service"]
state:
description: State to ensure
default: present
choices: ["present", "absent", "disabled"]
choices: ["present", "absent", "disabled", "fetched"]
author:
- Rafael Jeffman
"""
Expand Down Expand Up @@ -303,7 +306,12 @@ def check_parameters(module, state, action, names):
'allow_retrieve_keytab_group', 'allow_retrieve_keytab_host',
'allow_retrieve_keytab_hostgroup']

if state == 'present':
if state == 'fetched':
if action != 'service':
module.fail_json(msg="Can only fetch if action is 'service'.")
invalid = ['delete_continue']

elif state == 'present':
if len(names) != 1:
module.fail_json(msg="Only one service can be added at a time.")

Expand Down Expand Up @@ -347,7 +355,7 @@ def init_ansible_module():
argument_spec=dict(
# general
name=dict(type="list", aliases=["service"], default=None,
required=True),
required=False),
# service attributesstr
certificate=dict(type="list", aliases=['usercertificate'],
default=None, required=False),
Expand Down Expand Up @@ -392,14 +400,19 @@ def init_ansible_module():
allow_retrieve_keytab_hostgroup=dict(
type="list", required=False,
aliases=['ipaallowedtoperform_read_keys_hostgroup']),
# fetched
fetch_param=dict(type="list", default=None,
choices=["all"].extend(ipa_param_mapping.keys()),
required=False),
# absent
delete_continue=dict(type="bool", required=False,
aliases=['continue']),
# action
action=dict(type="str", default="service",
choices=["member", "service"]),
# state
state=dict(type="str", default="present",
choices=["present", "absent", "disabled"]),
choices=["present", "absent", "disabled", "fetched"]),
),
supports_check_mode=True,
)
Expand All @@ -409,6 +422,20 @@ def init_ansible_module():
return ansible_module


ipa_param_mapping = {
"principal": "krbprincipalname",
"certificate": "usercertificate",
"pac_type": "ipakrbauthzdata",
"auth_ind": "krbprincipalauthind",
"requires_pre_auth": "ipakrbrequirespreauth",
"ok_as_delegate": "ipakrbokasdelegate",
"ok_to_auth_as_delegate": "ipakrboktoauthasdelegate",
"netbiosname": "ipantflatname",
"host": "managedby_host",
"service": "krbcanonicalname",
}


def main():
ansible_module = init_ansible_module()

Expand Down Expand Up @@ -461,6 +488,35 @@ def main():
commands = []
keytab_members = ["user", "group", "host", "hostgroup"]

if state == "fetched":
encoding_fn = {
"usercertificate": encode_certificate
}
# set filter based on "name"
if names:
names = [
p.lower() if "@" in p
else "%s@%s".lower() % (p.lower(), api_get_realm().lower())
for p in names
]

def object_filter(res):
return any(
(
to_text(svc).lower().startswith(n)
for svc in res["krbcanonicalname"] for n in names
)
)

# fetch objects
fetched = ansible_module.fetch_objects(
"service", ["service"], ipa_param_mapping,
object_filter, encoding_fn=encoding_fn
)
exit_args["services"] = fetched
ansible_module.exit_json(changed=False, service=exit_args)
names = []

for name in names:
res_find = find_service(ansible_module, name)
res_principals = []
Expand Down
Loading