From d6b7d38958d20a3fb744d95411376995fc2946bd Mon Sep 17 00:00:00 2001 From: Rafael Guterres Jeffman Date: Mon, 4 Nov 2024 19:55:13 -0300 Subject: [PATCH] ansible_freeipa_module_utils: Add EntryFactory class This patch adds the class EntryFactory to the ansible-freeipa module utils. This class allows the handling of modules with multiple object entries as list of objects. When the multi-object parameter is not used, it creates a list of a single object, allowing for the same code idiom to be user. The entries created can be used both as objects, by acessing the values as properties, or as dictionaires, by accessing the elements as key-value pairs. The entry objects are read-only, and the values cannot be modified. --- .../module_utils/ansible_freeipa_module.py | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py index 2f90b3e10f..171c98cb51 100644 --- a/plugins/module_utils/ansible_freeipa_module.py +++ b/plugins/module_utils/ansible_freeipa_module.py @@ -526,6 +526,10 @@ def module_params_get(module, name, allow_empty_list_item=False): def module_params_get_lowercase(module, name, allow_empty_list_item=False): value = module_params_get(module, name, allow_empty_list_item) + return convert_param_value_to_lowercase(value) + + +def convert_param_value_to_lowercase(value): if isinstance(value, list): value = [v.lower() for v in value] if isinstance(value, (str, unicode)): @@ -1584,3 +1588,156 @@ def tm_warn(self, warning): ts = time.time() # pylint: disable=super-with-arguments super(IPAAnsibleModule, self).warn("%f %s" % (ts, warning)) + + +class EntryFactory: + """ + Implement an Entry Factory to extract objects from modules. + + When defining an ansible-freeipa module which allows the setting of + multiple objects in a single task, the object parameters can be set + as a set of parameters, or as a list of dictionaries with multiple + objects. + + The EntryFactory abstracts the extraction of the entry values so + that the entries set in a module can be treated as a list of objects + independent of the way the objects have been defined (as single object + defined by its parameters or as a list). + + Parameters + ---------- + ansible_module: The ansible module to be processed. + invalid_params: The list of invalid parameters for the current + state/action combination. + multiname: The name of the list of objects parameters. + params: a dict of the entry parameters with optional list of data + conversion functions. The functions are applied to the parameter + value in the order they appear in the list. + aliases: an optional dict of aliases where the keys are the alias + name and the value is the target parameter. + validate_entry: an optional function to validate the entry values. + This function is called after the parameters for the current + state/action are checked, and can be used to perform further + validation. If the entry is not valid, 'fail_json' should be + called by the validate_entry function. If the function ends, it + is assumed that the entry is valid. + + """ + + def __init__( + self, + ansible_module, + invalid_params, + multiname, + params, + aliases=None, + validate_entry=None + ): + """Initialize the Entry Factory.""" + self.module = ansible_module + self.invalid_params = invalid_params + self.multiname = multiname + self.params = params + self.aliases = aliases + self.validate_entry = validate_entry + self.entries = None + + class Entry: + """Provide an abstraction to handle module entries.""" + + def __init__(self, values): + """Initialize entry to be used as dict or object.""" + self.values = values + for key, value in values.items(): + setattr(self, key, value) + + def __getitem__(self, item): + """Allow entries to be treated as dictionaries.""" + return self.values[item] + + def __repr__(self): + """Provide a string representation of the stored values.""" + return repr(self.values) + + def _get_entries(self): + """Retrieve all entries from the module.""" + name = self.module.params_get("name") + if name is not None: + if isinstance(name, list): + self.entries = [ + EntryFactory.Entry({"name": _name}) for _name in name + ] + else: + self.entries = [ + self._extract_entry( + self.module, + IPAAnsibleModule.params_get + ) + ] + self.entries[0].name = name + else: + self.entries = [ + self._extract_entry(data, dict.get) + for data in self.module.params_get(self.multiname) + ] + + def _extract_entry(self, data, param_get): + """Extract an entry from the given data, using the given method.""" + def get_entry_param(entry_param): + _param, _conversions = entry_param + _value = param_get(data, _param) + for fn in _conversions: + _value = fn(_value) + result = (_param, _value) + return result + + # entry = {} + # for entry_param in self.params.items(): + # param, value = get_entry_param(entry_param) + # entry[param] = value + entry = dict( + get_entry_param(entry_param) + for entry_param in self.params.items() + ) + if self.aliases: + entry.update({ + alias: entry[param] + for alias, param in self.aliases.items() + }) + state = self.module.params_get("state") + action = self.module.params_get("action") + _result = EntryFactory.Entry(entry) + self._check_invalid_params( + _result, state, action, self.invalid_params + ) + # Call entry validation callback, if provided. + if self.validate_entry: + self.validate_entry(self.module, _result, state, action) + + return _result + + def _check_invalid_params(self, entry, state, action, invalid_params): + """Check if all parameters are valid for the given state/action.""" + if action is None: + msg = "Argument '{0}' can not be used with state '{1}'" + else: + msg = "Argument '{0}' can not be used with action " \ + "'{2}' and state '{1}'" + + for param in invalid_params: + if getattr(entry, param, None) is not None: + self.module.fail_json(msg=msg.format(param, state, action)) + + if not entry.name: + self.module.fail_json(msg="Entry 'name' is not set.") + + def __iter__(self): + """Initialize factory iterator.""" + self._get_entries() + return self + + def __next__(self): + """Retrieve next entry.""" + if not self.entries: + raise StopIteration() + return self.entries.pop()