Skip to content

Commit e7819b1

Browse files
committed
ipahost: Fix idempotence issues with update_dns and multiple objects
When using ipahost with the 'hosts' parameters several parameter conversion routines were not being performed causing the module to try to execute tasks it was not suposed to. This issues were fixed by adapting the module to use EntryFactory. Another issue was happening when 'update_dns: true' is used with some parameters, like 'description', 'location' or 'mac_address', where, even if no change was to be performed the plugin would evaluate that the arguments were different, and tried to modify the object. The fix for this issue is to ignore 'updatedns' when comparing the parameters provided with the existing IPA object. This modification also fix the case where duplicate entries used with the 'name' parameter in case 'state: absent' would fail without indicating the use of duplicate entries. Fixes: #1296 Fixes: https://issues.redhat.com/browse/RHEL-111063 Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
1 parent 961eb46 commit e7819b1

5 files changed

Lines changed: 416 additions & 358 deletions

File tree

plugins/module_utils/ansible_freeipa_module.py

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,9 @@ def _afm_convert(value):
497497
return value
498498

499499

500-
def module_params_get(module, name, allow_empty_list_item=False):
500+
def module_params_get(
501+
module, name, allow_empty_list_item=False, ignore_list_check=False
502+
):
501503
value = _afm_convert(module.params.get(name))
502504

503505
# Fail on empty strings in the list or if allow_empty_list_item is True
@@ -506,7 +508,7 @@ def module_params_get(module, name, allow_empty_list_item=False):
506508
# "" for lists with choices, even if the empty list is not part of
507509
# the choices.
508510
# Ansible issue https://github.com/ansible/ansible/issues/77108
509-
if isinstance(value, list):
511+
if not ignore_list_check and isinstance(value, list):
510512
for val in value:
511513
if (
512514
isinstance(val, (str, unicode)) # pylint: disable=W0012,E0606
@@ -537,6 +539,14 @@ def convert_param_value_to_lowercase(value):
537539
return value
538540

539541

542+
def convert_param_value_to_uppercase(value):
543+
if isinstance(value, list):
544+
value = [v.upper() for v in value]
545+
if isinstance(value, (str, unicode)):
546+
value = value.upper()
547+
return value
548+
549+
540550
def module_params_get_with_type_cast(
541551
module, name, datatype, allow_empty=False
542552
):
@@ -1189,7 +1199,9 @@ def ipa_connect(self, context=None):
11891199
finally:
11901200
temp_kdestroy(ccache_dir, ccache_name)
11911201

1192-
def params_get(self, name, allow_empty_list_item=False):
1202+
def params_get(
1203+
self, name, allow_empty_list_item=False, ignore_list_check=False
1204+
):
11931205
"""
11941206
Retrieve value set for module parameter.
11951207
@@ -1201,7 +1213,8 @@ def params_get(self, name, allow_empty_list_item=False):
12011213
The parameter allowes to have empty strings in a list
12021214
12031215
"""
1204-
return module_params_get(self, name, allow_empty_list_item)
1216+
return module_params_get(
1217+
self, name, allow_empty_list_item, ignore_list_check)
12051218

12061219
def params_get_lowercase(self, name, allow_empty_list_item=False):
12071220
"""
@@ -1656,6 +1669,10 @@ class EntryFactory:
16561669
not valid, 'fail_json' should be called. The function must return
16571670
the entry, modified or not. The funcion signature is
16581671
'def fn(module:IPAAnsibleModule, entry: Entry) -> Entry:'
1672+
unique_key: A string or list of strings representing parameter names
1673+
that uniquely identify an object. When set, the objects are checked
1674+
for duplicates and a EntryFactory.DuplicateKeyError is raised, when
1675+
instantiating the EntryFactory object if any duplicate is found.
16591676
**user_vars: any other keyword argument is passed to the
16601677
validate_entry callback as user data.
16611678
@@ -1693,13 +1710,21 @@ def main():
16931710
16941711
"""
16951712

1713+
class DuplicateKeyError(Exception):
1714+
"""An exeption for duplicate keys in EntryFactory."""
1715+
1716+
def __init__(self, dups):
1717+
dups = [k if len(k) > 1 else k[0] for k in dups]
1718+
super().__init__(", ".join(repr(k) for k in dups))
1719+
16961720
def __init__(
16971721
self,
16981722
ansible_module,
16991723
invalid_params,
17001724
multiname,
17011725
params,
17021726
validate_entry=None,
1727+
unique_key=None,
17031728
**user_vars
17041729
):
17051730
"""Initialize the Entry Factory."""
@@ -1711,9 +1736,37 @@ def __init__(
17111736
param: (config or {}).get("convert", [])
17121737
for param, config in params.items()
17131738
}
1739+
self.attrs = {
1740+
param: {
1741+
_attr: _value
1742+
for _attr, _value in config.items()
1743+
if _attr not in {"convert"}
1744+
}
1745+
for param, config in params.items()
1746+
}
17141747
self.validate_entry = validate_entry
17151748
self.user_vars = user_vars
17161749
self.__entries = self._get_entries()
1750+
_dups = self.get_duplicate_keys(unique_key)
1751+
if _dups:
1752+
raise EntryFactory.DuplicateKeyError(_dups)
1753+
1754+
def get_duplicate_keys(self, unique_key):
1755+
"""Get a set of duplicate keys based on the given parameter name(s)."""
1756+
_dups = set()
1757+
if unique_key is not None:
1758+
if isinstance(unique_key, (str, unicode)): # pylint: disable=all
1759+
unique_key = [unique_key]
1760+
_keys = set()
1761+
for _entry in self.__entries:
1762+
_key_set = tuple(set(_entry[k] for k in unique_key))
1763+
if _key_set in _keys:
1764+
_dups.add(_key_set)
1765+
_keys.add(_key_set)
1766+
return _dups
1767+
1768+
def __len__(self):
1769+
return len(self.__entries)
17171770

17181771
def __iter__(self):
17191772
"""Initialize factory iterator."""
@@ -1811,7 +1864,9 @@ def copy_entry_and_set_name(entry, name):
18111864
# list of names.
18121865
_entry = self._extract_entry(
18131866
self.ansible_module,
1814-
IPAAnsibleModule.params_get
1867+
lambda s, p: (
1868+
IPAAnsibleModule.params_get(s, p, ignore_list_check=True)
1869+
)
18151870
)
18161871
# copy attribute values if 'name' returns a list
18171872
_entries = [
@@ -1828,16 +1883,44 @@ def copy_entry_and_set_name(entry, name):
18281883

18291884
def _extract_entry(self, data, param_get):
18301885
"""Extract an entry from the given data, using the given method."""
1831-
def get_entry_param_value(parameter, conversions):
1832-
_value = param_get(data, parameter)
1886+
def check_empty_list_item(parameter, value, attrs):
1887+
allow_empty_list_item = (attrs if attrs is not None else {}).get(
1888+
"allow_empty_list_item",
1889+
False
1890+
)
1891+
if isinstance(value, list):
1892+
empty = [v for v in value if v == ""]
1893+
if empty:
1894+
if not allow_empty_list_item:
1895+
self.ansible_module.fail_json(
1896+
msg="Parameter '%s' contains an empty string" &
1897+
parameter
1898+
)
1899+
elif len(value) > 1:
1900+
self.ansible_module.fail_json(
1901+
msg="Parameter '%s' may not contain another "
1902+
"entry together with an empty string" % parameter
1903+
)
1904+
return value
1905+
1906+
def get_entry_param_value(parameter, conversions, attrs=None):
1907+
_value = check_empty_list_item(
1908+
parameter,
1909+
param_get(data, parameter),
1910+
attrs
1911+
)
18331912
if _value and conversions:
18341913
for fn in conversions:
18351914
_value = fn(_value)
18361915
return _value
18371916

18381917
# Build 'parameter: value' mapping for all module parameters
18391918
_entry = {
1840-
parameter: get_entry_param_value(parameter, conversions)
1919+
parameter: get_entry_param_value(
1920+
parameter,
1921+
conversions,
1922+
self.attrs.get(parameter)
1923+
)
18411924
for parameter, conversions in self.convert.items()
18421925
}
18431926

0 commit comments

Comments
 (0)