make hash_params more robust in the face of many corner cases (#18701)

* make hash_params more robust in the face of many corner cases

Fixes #18680
Alternative fix to #18681

* add test case for role.hash_params

* Add role.hash_params test for more types

A set, a generator/iterable, and a Container that
is not Iterable.
This commit is contained in:
Toshio Kuratomi 2016-12-05 04:01:45 -08:00 committed by GitHub
commit 5f5ea06ca4
2 changed files with 139 additions and 21 deletions

View file

@ -19,10 +19,10 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.compat.six import iteritems
import collections
import os
from ansible.compat.six import iteritems, binary_type, text_type
from ansible.errors import AnsibleError, AnsibleParserError
from ansible.playbook.attribute import FieldAttribute
from ansible.playbook.base import Base
@ -41,25 +41,54 @@ __all__ = ['Role', 'hash_params']
# the role due to the fact that it would require the use of self
# in a static method. This is also used in the base class for
# strategies (ansible/plugins/strategy/__init__.py)
def hash_params(params):
if not isinstance(params, dict):
if isinstance(params, list):
return frozenset(params)
"""
Construct a data structure of parameters that is hashable.
This requires changing any mutable data structures into immutable ones.
We chose a frozenset because role parameters have to be unique.
.. warning:: this does not handle unhashable scalars. Two things
mitigate that limitation:
1) There shouldn't be any unhashable scalars specified in the yaml
2) Our only choice would be to return an error anyway.
"""
# Any container is unhashable if it contains unhashable items (for
# instance, tuple() is a Hashable subclass but if it contains a dict, it
# cannot be hashed)
if isinstance(params, collections.Container) and not isinstance(params, (text_type, binary_type)):
if isinstance(params, collections.Mapping):
try:
# Optimistically hope the contents are all hashable
new_params = frozenset(params.items())
except TypeError:
new_params = set()
for k, v in params.items():
# Hash each entry individually
new_params.update((k, hash_params(v)))
new_params = frozenset(new_params)
elif isinstance(params, (collections.Set, collections.Sequence)):
try:
# Optimistically hope the contents are all hashable
new_params = frozenset(params)
except TypeError:
new_params = set()
for v in params:
# Hash each entry individually
new_params.update(hash_params(v))
new_params = frozenset(new_params)
else:
return params
else:
s = set()
for k,v in iteritems(params):
if isinstance(v, dict):
s.update((k, hash_params(v)))
elif isinstance(v, list):
things = []
for item in v:
things.append(hash_params(item))
s.update((k, tuple(things)))
else:
s.update((k, v))
return frozenset(s)
# This is just a guess.
new_params = frozenset(params)
return new_params
# Note: We do not handle unhashable scalars but our only choice would be
# to raise an error there anyway.
return frozenset((params,))
class Role(Base, Become, Conditional, Taggable):