diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py index 2f90b3e10f4aee997e15445d2d05ed054c19f67d..3386cb8c10e64cd95a00d3bbe83530380481d083 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,232 @@ class IPAAnsibleModule(AnsibleModule): 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 its configuration as a + dict. The 'convert' configuration is a list of functions to be + applied, in order, to the provided value for the paarameter. Any + other configuration field is ignored in the current implementation. + 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 or modification to the entry values. If the entry is + not valid, 'fail_json' should be called. The function must return + the entry, modified or not. The funcion signature is + 'def fn(module:IPAAnsibleModule, entry: Entry) -> Entry:' + **user_vars: any other keyword argument is passed to the + validate_entry callback as user data. + + Example + ------- + def validate_entry(module, entry, mydata): + if (something_is_wrong(entry)): + module.fail_json(msg=f"Something wrong with {entry.name}") + entry.some_field = mydata + return entry + + def main(): + # ... + # Create param mapping, all moudle parameters must be + # present as keys of this dictionary + params = { + "name": {}, + "description": {} + "user": { + "convert": [convert_param_value_to_lowercase] + }, + "group": {"convert": [convert_param_value_to_lowercase]} + } + entries = EntryFactory( + module, invalid_params, "entries", params, + validate_entry=validate_entry, + mydata=1234 + ) + #... + with module.ipa_connect(context=context): + # ... + for entry in entries: + # process entry and create commands + # ... + + """ + + def __init__( + self, + ansible_module, + invalid_params, + multiname, + params, + validate_entry=None, + **user_vars + ): + """Initialize the Entry Factory.""" + self.ansible_module = ansible_module + self.invalid_params = set(invalid_params) + self.multiname = multiname + self.params = params + self.convert = { + param: (config or {}).get("convert", []) + for param, config in params.items() + } + self.validate_entry = validate_entry + self.user_vars = user_vars + self.__entries = self._get_entries() + + def __iter__(self): + """Initialize factory iterator.""" + return iter(self.__entries) + + def __next__(self): + """Retrieve next entry.""" + return next(self.__entries) + + def check_invalid_parameter_usage(self, entry_dict, fail_on_check=True): + """ + Check if entry_dict parameters are valid for the current state/action. + + Parameters + ---------- + entry_dict: A dictionary representing the module parameters. + fail_on_check: If set to True wil make the module execution fail + if invalid parameters are used. + + Return + ------ + If fail_on_check is not True, returns True if the entry parameters + are valid and execution should proceed, False otherwise. + + """ + state = self.ansible_module.params_get("state") + action = self.ansible_module.params_get("action") + + if action is None: + msg = "Arguments '{0}' can not be used with state '{1}'" + else: + msg = "Arguments '{0}' can not be used with action " \ + "'{2}' and state '{1}'" + + entry_params = set(k for k, v in entry_dict.items() if v is not None) + match_invalid = self.invalid_params & entry_params + if match_invalid: + if fail_on_check: + self.ansible_module.fail_json( + msg=msg.format(", ".join(match_invalid), state, action)) + return False + + if not entry_dict.get("name"): + if fail_on_check: + self.ansible_module.fail_json(msg="Entry 'name' is not set.") + return False + + return True + + 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 copy(self): + """Make a copy of the entry.""" + return EntryFactory.Entry(self.values.copy()) + + def __setitem__(self, item, value): + """Allow entries to be treated as dictionaries.""" + self.values[item] = value + setattr(self, item, value) + + def __getitem__(self, item): + """Allow entries to be treated as dictionaries.""" + return self.values[item] + + def __setattr__(self, attrname, value): + if attrname != "values" and attrname in self.values: + self.values[attrname] = value + super().__setattr__(attrname, value) + + 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.""" + def copy_entry_and_set_name(entry, name): + _result = entry.copy() + _result.name = name + return _result + + names = self.ansible_module.params_get("name") + if names is not None: + if not isinstance(names, list): + names = [names] + # Get entrie(s) defined by the 'name' parameter. + # For most states and modules, 'name' will represent a single + # entry, but for some states, like 'absent', it could be a + # list of names. + _entry = self._extract_entry( + self.ansible_module, + IPAAnsibleModule.params_get + ) + # copy attribute values if 'name' returns a list + _entries = [ + copy_entry_and_set_name(_entry, _name) + for _name in names + ] + else: + _entries = [ + self._extract_entry(data, dict.get) + for data in self.ansible_module.params_get(self.multiname) + ] + + return _entries + + def _extract_entry(self, data, param_get): + """Extract an entry from the given data, using the given method.""" + def get_entry_param_value(parameter, conversions): + _value = param_get(data, parameter) + if _value and conversions: + for fn in conversions: + _value = fn(_value) + return _value + + # Build 'parameter: value' mapping for all module parameters + _entry = { + parameter: get_entry_param_value(parameter, conversions) + for parameter, conversions in self.convert.items() + } + + # Check if any invalid parameter is used. + self.check_invalid_parameter_usage(_entry) + + # Create Entry object + _result = EntryFactory.Entry(_entry) + # Call entry validation callback, if provided. + if self.validate_entry: + _result = self.validate_entry( + self.ansible_module, _result, **self.user_vars) + + return _result diff --git a/setup.cfg b/setup.cfg index c44ac599c7c615a75879ddd84e0423eadfc37ca8..c1cd4c64d1d5abebec65c1087b6c9a5ee553db5d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -82,6 +82,9 @@ ignored-modules = os, SSSDConfig +[pylint.DESIGN] +max-attributes=12 + [pylint.REFACTORING] max-nested-blocks = 9