Skip to content

Commit e8d0d98

Browse files
committed
fix various bugs in list_present
fixes #31427 fixes #39875 (previous solution was incomplete) Trying to convert the list and sub-objects into a set caused various issues when checking for existance. Use a proper loop and `in` checks to provide a more correct behavior.
1 parent e717ef7 commit e8d0d98

File tree

4 files changed

+145
-65
lines changed

4 files changed

+145
-65
lines changed

changelog/31427.fixed.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fixed an issue with how existing entries are tracked in grains.list\_present. Previous entries were only considered if
2+
the grain previously exited. If not then the state would not "see" the duplicates. Removed the dubious tracking via
3+
"context" and focused on using checking for existance in the live grains.

changelog/39875.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed issue with complex objects in grains.list\_present. Original fix #52710 did not fully address the problem.

salt/states/grains.py

Lines changed: 37 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -38,30 +38,6 @@ def exists(name, delimiter=DEFAULT_TARGET_DELIM):
3838
return ret
3939

4040

41-
def make_hashable(list_grain, result=None):
42-
"""
43-
Ensure that a list grain is hashable.
44-
45-
list_grain
46-
The list grain that should be hashable
47-
48-
result
49-
This function is recursive, so it must be possible to use a
50-
sublist as parameter to the function. Should not be used by a caller
51-
outside of the function.
52-
53-
Make it possible to compare two list grains to each other if the list
54-
contains complex objects.
55-
"""
56-
result = result or set()
57-
for sublist in list_grain:
58-
if type(sublist) == list:
59-
make_hashable(sublist, result)
60-
else:
61-
result.add(frozenset(sublist))
62-
return result
63-
64-
6541
def present(name, value, delimiter=DEFAULT_TARGET_DELIM, force=False):
6642
"""
6743
Ensure that a grain is set
@@ -185,30 +161,32 @@ def list_present(name, value, delimiter=DEFAULT_TARGET_DELIM):
185161
ret["comment"] = f"Grain {name} is not a valid list"
186162
return ret
187163
if isinstance(value, list):
188-
if make_hashable(value).issubset(
189-
make_hashable(__salt__["grains.get"](name))
190-
):
164+
# An older implementation tried to convert everything to a set. Information was lost
165+
# during that conversion. It even converted str to set! Trying to add "racer" if
166+
# "racecar" was already present would fail. Additionaly, dictionary values would also
167+
# be lost. Trying to add {"foo": "bar"} and {"foo": "baz"} would also fail.
168+
# While potentially slower, the only valid option is to actually check for existance
169+
# within the existing grain.
170+
# Hopefully the performance penality is reasonable in the effort for correctness.
171+
# Very very very large grain lists will be probematic..
172+
intersection = []
173+
difference = []
174+
for new_value in value:
175+
if new_value in grain:
176+
intersection.append(new_value)
177+
else:
178+
difference.append(new_value)
179+
if difference:
180+
value = difference
181+
else:
191182
ret["comment"] = f"Value {value} is already in grain {name}"
192183
return ret
193-
elif name in __context__.get("pending_grains", {}):
194-
# elements common to both
195-
intersection = set(value).intersection(
196-
__context__.get("pending_grains", {})[name]
197-
)
198-
if intersection:
199-
value = list(
200-
set(value).difference(__context__["pending_grains"][name])
201-
)
202-
ret["comment"] = (
203-
'Removed value {} from update due to context found in "{}".\n'.format(
204-
value, name
205-
)
184+
if intersection:
185+
ret["comment"] = (
186+
'Removed value {} from update due to value found in "{}".\n'.format(
187+
intersection, name
206188
)
207-
if "pending_grains" not in __context__:
208-
__context__["pending_grains"] = {}
209-
if name not in __context__["pending_grains"]:
210-
__context__["pending_grains"][name] = set()
211-
__context__["pending_grains"][name].update(value)
189+
)
212190
else:
213191
if value in grain:
214192
ret["comment"] = f"Value {value} is already in grain {name}"
@@ -227,17 +205,21 @@ def list_present(name, value, delimiter=DEFAULT_TARGET_DELIM):
227205
ret["changes"] = {"new": grain}
228206
return ret
229207
new_grains = __salt__["grains.append"](name, value)
208+
# TODO: Rather than fetching the grains again can we just rely on the status of grains.append?
209+
actual_grains = __salt__["grains.get"](name, [])
210+
# Previous implementation tried to use a set for comparison. See above notes on why that is
211+
# not good.
230212
if isinstance(value, list):
231-
if not set(value).issubset(set(__salt__["grains.get"](name))):
232-
ret["result"] = False
233-
ret["comment"] = f"Failed append value {value} to grain {name}"
234-
return ret
235-
else:
236-
if value not in __salt__["grains.get"](name, delimiter=DEFAULT_TARGET_DELIM):
237-
ret["result"] = False
238-
ret["comment"] = f"Failed append value {value} to grain {name}"
239-
return ret
240-
ret["comment"] = f"Append value {value} to grain {name}"
213+
for new_value in value:
214+
if new_value not in actual_grains:
215+
ret["result"] = False
216+
ret["comment"] = f"Failed append value {value} to grain {name}"
217+
return ret
218+
elif value not in actual_grains:
219+
ret["result"] = False
220+
ret["comment"] = f"Failed append value {value} to grain {name}"
221+
return ret
222+
ret["comment"] = "{0}Append value {1} to grain {2}".format(ret.get("comment", ""), value, name)
241223
ret["changes"] = {"new": new_grains}
242224
return ret
243225

tests/pytests/unit/states/test_grains.py

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,6 @@ def test_exists_found():
7373
assert ret["comment"] == "Grain exists"
7474
assert ret["changes"] == {}
7575

76-
# 'make_hashable' function tests: 1
77-
78-
79-
def test_make_hashable():
80-
with set_grains({"cmplx_lst_grain": [{"a": "aval"}, {"foo": "bar"}]}):
81-
hashable_list = {"cmplx_lst_grain": [{"a": "aval"}, {"foo": "bar"}]}
82-
assert grains.make_hashable(grains.__grains__).issubset(
83-
grains.make_hashable(hashable_list)
84-
)
8576

8677
# 'present' function tests: 12
8778

@@ -643,7 +634,7 @@ def test_append_convert_to_list_empty():
643634
assert_grain_file_content("foo:\n- baz\n")
644635

645636

646-
# 'list_present' function tests: 7
637+
# 'list_present' function tests: 14
647638

648639

649640
def test_list_present():
@@ -721,6 +712,109 @@ def test_list_present_already():
721712
assert_grain_file_content("a: aval\nfoo:\n- bar\n")
722713

723714

715+
def test_list_present_complex_present():
716+
with set_grains({"a": [{"foo": "bar"}]}):
717+
ret = grains.list_present(name="a", value=[{"foo": "bar"}, {"foo": "baz"}])
718+
assert grains.__grains__ == {"a": [{"foo": "bar"}, {"foo": "baz"}]}
719+
assert ret["result"] is True
720+
assert ret["comment"] == (
721+
"Removed value [{'foo': 'bar'}] from update due to value found in \"a\".\n"
722+
"Append value [{'foo': 'baz'}] to grain a"
723+
)
724+
assert ret["changes"] == {"new": {"a": [{"foo": "bar"}, {"foo": "baz"}]}}
725+
assert_grain_file_content("a:\n- foo: bar\n- foo: baz\n")
726+
727+
728+
def test_list_present_racecar_present():
729+
with set_grains({"foo": ["racecar"]}):
730+
ret = grains.list_present(name="foo", value=["racer"])
731+
assert grains.__grains__ == {"foo": ["racecar", "racer"]}
732+
assert ret["result"] is True
733+
assert ret["comment"] == "Append value ['racer'] to grain foo"
734+
assert ret["changes"] == {"new": {"foo": ["racecar", "racer"]}}
735+
assert_grain_file_content("foo:\n- racecar\n- racer\n")
736+
737+
738+
def test_list_present_multiple_calls_empty_as_str():
739+
with set_grains({}):
740+
# First Entry
741+
ret = grains.list_present(name="foo", value="racecar")
742+
assert grains.__grains__ == {"foo": ["racecar"]}
743+
assert ret["result"] is True
744+
assert ret["comment"] == "Append value racecar to grain foo"
745+
assert ret["changes"] == {"new": {"foo": ["racecar"]}}
746+
747+
# Second Entry
748+
ret = grains.list_present(name="foo", value=["racecar", "taxi"])
749+
assert grains.__grains__ == {"foo": ["racecar", "taxi"]}
750+
assert ret["result"] is True
751+
assert ret["comment"] == (
752+
"Removed value ['racecar'] from update due to value found in \"foo\".\n"
753+
"Append value ['taxi'] to grain foo"
754+
)
755+
assert ret["changes"] == {"new": {"foo": ["racecar", "taxi"]}}
756+
757+
758+
def test_list_present_multiple_calls_empty_as_list():
759+
with set_grains({}):
760+
# First Entry
761+
ret = grains.list_present(name="foo", value=["racecar"])
762+
assert grains.__grains__ == {"foo": ["racecar"]}
763+
assert ret["result"] is True
764+
assert ret["comment"] == "Append value ['racecar'] to grain foo"
765+
assert ret["changes"] == {"new": {"foo": ["racecar"]}}
766+
767+
# Second Entry
768+
ret = grains.list_present(name="foo", value=["racecar", "taxi"])
769+
assert grains.__grains__ == {"foo": ["racecar", "taxi"]}
770+
assert ret["result"] is True
771+
assert ret["comment"] == (
772+
"Removed value ['racecar'] from update due to value found in \"foo\".\n"
773+
"Append value ['taxi'] to grain foo"
774+
)
775+
assert ret["changes"] == {"new": {"foo": ["racecar", "taxi"]}}
776+
777+
778+
def test_list_present_multiple_calls_present_as_str():
779+
with set_grains({"foo": ["nascar"]}):
780+
# First Entry
781+
ret = grains.list_present(name="foo", value="racecar")
782+
assert grains.__grains__ == {"foo": ["nascar", "racecar"]}
783+
assert ret["result"] is True
784+
assert ret["comment"] == "Append value racecar to grain foo"
785+
assert ret["changes"] == {"new": {"foo": ["nascar", "racecar"]}}
786+
787+
# Second Entry
788+
ret = grains.list_present(name="foo", value=["racecar", "taxi"])
789+
assert grains.__grains__ == {"foo": ["nascar", "racecar", "taxi"]}
790+
assert ret["result"] is True
791+
assert ret["comment"] == (
792+
"Removed value ['racecar'] from update due to value found in \"foo\".\n"
793+
"Append value ['taxi'] to grain foo"
794+
)
795+
assert ret["changes"] == {"new": {"foo": ["nascar", "racecar", "taxi"]}}
796+
797+
798+
def test_list_present_multiple_calls_present_as_list():
799+
with set_grains({"foo": ["nascar"]}):
800+
# First Entry
801+
ret = grains.list_present(name="foo", value=["racecar"])
802+
assert grains.__grains__ == {"foo": ["nascar", "racecar"]}
803+
assert ret["result"] is True
804+
assert ret["comment"] == "Append value ['racecar'] to grain foo"
805+
assert ret["changes"] == {"new": {"foo": ["nascar", "racecar"]}}
806+
807+
# Second Entry
808+
ret = grains.list_present(name="foo", value=["racecar", "taxi"])
809+
assert grains.__grains__ == {"foo": ["nascar", "racecar", "taxi"]}
810+
assert ret["result"] is True
811+
assert ret["comment"] == (
812+
"Removed value ['racecar'] from update due to value found in \"foo\".\n"
813+
"Append value ['taxi'] to grain foo"
814+
)
815+
assert ret["changes"] == {"new": {"foo": ["nascar", "racecar", "taxi"]}}
816+
817+
724818
def test_list_present_unknown_failure():
725819
with set_grains({"a": "aval", "foo": ["bar"]}):
726820
# Unknown reason failure

0 commit comments

Comments
 (0)