Skip to content

Commit c642ba7

Browse files
committed
Added basic support for hash_behaviour=merge in roles
Dict vars passed to roles are now properly merged instead of simply overriding dict vars that are coming from vars_files.
1 parent fc2d25e commit c642ba7

File tree

6 files changed

+73
-52
lines changed

6 files changed

+73
-52
lines changed

lib/ansible/playbook/play.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,11 @@ def __init__(self, playbook, ds, basedir):
7777
# tasks/handlers as they may have inventory scope overrides
7878
_tasks = ds.pop('tasks', [])
7979
_handlers = ds.pop('handlers', [])
80-
ds = template(basedir, ds, self.vars)
80+
ds = template(basedir, ds, self.vars)
8181
ds['tasks'] = _tasks
8282
ds['handlers'] = _handlers
8383

84-
self._ds = ds
84+
self._ds = ds
8585

8686
hosts = ds.get('hosts')
8787
if hosts is None:
@@ -114,7 +114,7 @@ def __init__(self, playbook, ds, basedir):
114114

115115
if self.sudo_user != 'root':
116116
self.sudo = True
117-
117+
118118

119119
# *************************************************
120120

@@ -146,7 +146,7 @@ def _load_roles(self, roles, ds):
146146
# flush handlers after pre_tasks
147147
new_tasks.append(dict(meta='flush_handlers'))
148148

149-
# variables if the role was parameterized (i.e. given as a hash)
149+
# variables if the role was parameterized (i.e. given as a hash)
150150
has_dict = {}
151151

152152
for orig_path in roles:
@@ -178,14 +178,14 @@ def _load_roles(self, roles, ds):
178178
raise errors.AnsibleError("found role at %s, but cannot find %s or %s or %s or %s" % (path, task, handler, vars_file, library))
179179
if os.path.isfile(task):
180180
nt = dict(include=task, vars=has_dict)
181-
if when:
181+
if when:
182182
nt['when'] = when
183183
if with_items:
184184
nt['with_items'] = with_items
185185
new_tasks.append(nt)
186186
if os.path.isfile(handler):
187187
nt = dict(include=handler, vars=has_dict)
188-
if when:
188+
if when:
189189
nt['when'] = when
190190
if with_items:
191191
nt['with_items'] = with_items
@@ -242,7 +242,7 @@ def _load_tasks(self, tasks, vars={}, additional_conditions=[], original_file=No
242242
if x['meta'] == 'flush_handlers':
243243
results.append(Task(self,x))
244244
continue
245-
245+
246246
task_vars = self.vars.copy()
247247
task_vars.update(vars)
248248
if original_file:
@@ -269,7 +269,7 @@ def _load_tasks(self, tasks, vars={}, additional_conditions=[], original_file=No
269269
raise errors.AnsibleError("parse error: task includes cannot be used with other directives: %s" % k)
270270

271271
if 'vars' in x:
272-
task_vars.update(x['vars'])
272+
task_vars = utils.combine_vars(task_vars, x['vars'])
273273
if 'only_if' in x:
274274
included_additional_conditions.append(x['only_if'])
275275

@@ -281,7 +281,7 @@ def _load_tasks(self, tasks, vars={}, additional_conditions=[], original_file=No
281281
mv[k] = template(self.basedir, v, mv)
282282
dirname = self.basedir
283283
if original_file:
284-
dirname = os.path.dirname(original_file)
284+
dirname = os.path.dirname(original_file)
285285
include_file = template(dirname, tokens[0], mv)
286286
include_filename = utils.path_dwim(dirname, include_file)
287287
data = utils.parse_yaml_from_file(include_filename)

lib/ansible/utils/__init__.py

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -335,24 +335,23 @@ def parse_kv(args):
335335
return options
336336

337337
def merge_hash(a, b):
338-
''' merges hash b into a
339-
this means that if b has key k, the resulting has will have a key k
340-
which value comes from b
341-
said differently, all key/value combination from b will override a's '''
338+
''' recursively merges hash b into a
339+
keys from b take precedende over keys from a '''
342340

343-
# and iterate over b keys
341+
result = copy.deepcopy(a)
342+
343+
# next, iterate over b keys and values
344344
for k, v in b.iteritems():
345-
if k in a and isinstance(a[k], dict):
346-
# if this key is a hash and exists in a
347-
# we recursively call ourselves with
348-
# the key value of b
349-
a[k] = merge_hash(a[k], v)
345+
# if there's already such key in a
346+
# and that key contains dict
347+
if k in result and isinstance(result[k], dict):
348+
# merge those dicts recursively
349+
result[k] = merge_hash(a[k], v)
350350
else:
351-
# k is not in a, no need to merge b, we just deecopy
352-
# or k is not a dictionnary, no need to merge b either, we just deecopy it
353-
a[k] = v
354-
# finally, return the resulting hash when we're done iterating keys
355-
return a
351+
# otherwise, just copy a value from b to a
352+
result[k] = v
353+
354+
return result
356355

357356
def md5s(data):
358357
''' Return MD5 hex digest of data. '''
@@ -604,7 +603,7 @@ def compile_when_to_only_if(expression):
604603
# when: int $x in $alist
605604
# when: float $x > 2 and $y <= $z
606605
# when: str $x != $y
607-
# when: jinja2_compare asdf # implies {{ asdf }}
606+
# when: jinja2_compare asdf # implies {{ asdf }}
608607

609608
if type(expression) not in [ str, unicode ]:
610609
raise errors.AnsibleError("invalid usage of when_ operator: %s" % expression)
@@ -723,21 +722,21 @@ def get_diff(diff):
723722
return ">> the files are different, but the diff library cannot compare unicode strings"
724723

725724
def is_list_of_strings(items):
726-
for x in items:
725+
for x in items:
727726
if not isinstance(x, basestring):
728727
return False
729728
return True
730729

731730
def safe_eval(str):
732-
'''
731+
'''
733732
this is intended for allowing things like:
734733
with_items: {{ a_list_variable }}
735734
where Jinja2 would return a string
736735
but we do not want to allow it to call functions (outside of Jinja2, where
737736
the env is constrained)
738737
'''
739738
# FIXME: is there a more native way to do this?
740-
739+
741740
def is_set(var):
742741
return not var.startswith("$") and not '{{' in var
743742

@@ -776,7 +775,7 @@ def listify_lookup_plugin_terms(terms, basedir, inject):
776775
if isinstance(new_terms, basestring) and new_terms.find("{{") != -1:
777776
pass
778777
else:
779-
terms = new_terms
778+
terms = new_terms
780779
except:
781780
pass
782781

test/TestPlayBook.py

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ def setUp(self):
9696
os.unlink('/tmp/ansible_test_data_template.out')
9797
if os.path.exists('/tmp/ansible_test_messages.out'):
9898
os.unlink('/tmp/ansible_test_messages.out')
99+
if os.path.exists('/tmp/ansible_test_role_messages.out'):
100+
os.unlink('/tmp/ansible_test_role_messages.out')
99101

100102
def _prepare_stage_dir(self):
101103
stage_path = os.path.join(self.test_dir, 'test_data')
@@ -304,20 +306,17 @@ def test_playbook_hash_replace(self):
304306
)
305307
playbook.run()
306308

307-
with open('/tmp/ansible_test_messages.out') as f:
308-
actual = [l.strip() for l in f.readlines()]
309-
310-
print "**ACTUAL**"
311-
print actual
312-
313-
expected = [
309+
filename = '/tmp/ansible_test_messages.out'
310+
expected_lines = [
314311
"goodbye: Goodbye World!"
315312
]
313+
self._compare_file_output(filename, expected_lines)
316314

317-
print "**EXPECTED**"
318-
print expected
319-
320-
assert actual == expected
315+
filename = '/tmp/ansible_test_role_messages.out'
316+
expected_lines = [
317+
"inside_a_role: Indeed!"
318+
]
319+
self._compare_file_output(filename, expected_lines)
321320

322321
# restore default hash behavior
323322
C.DEFAULT_HASH_BEHAVIOUR = saved_hash_behavior
@@ -337,21 +336,34 @@ def test_playbook_hash_merge(self):
337336
)
338337
playbook.run()
339338

340-
with open('/tmp/ansible_test_messages.out') as f:
341-
actual = [l.strip() for l in f.readlines()]
342-
343-
print "**ACTUAL**"
344-
print actual
339+
filename = '/tmp/ansible_test_messages.out'
340+
expected_lines = [
341+
"goodbye: Goodbye World!",
342+
"hello: Hello World!"
343+
]
344+
self._compare_file_output(filename, expected_lines)
345345

346-
expected = [
346+
filename = '/tmp/ansible_test_role_messages.out'
347+
expected_lines = [
348+
"goodbye: Goodbye World!",
347349
"hello: Hello World!",
348-
"goodbye: Goodbye World!"
350+
"inside_a_role: Indeed!"
349351
]
350-
351-
print "**EXPECTED**"
352-
print expected
353-
354-
assert actual == expected
352+
self._compare_file_output(filename, expected_lines)
355353

356354
# restore default hash behavior
357355
C.DEFAULT_HASH_BEHAVIOUR = saved_hash_behavior
356+
357+
def _compare_file_output(self, filename, expected_lines):
358+
actual_lines = []
359+
with open(filename) as f:
360+
actual_lines = [l.strip() for l in f.readlines()]
361+
actual_lines = sorted(actual_lines)
362+
363+
print "**ACTUAL**"
364+
print actual_lines
365+
366+
print "**EXPECTED**"
367+
print expected_lines
368+
369+
assert actual_lines == expected_lines

test/test_hash_behavior/playbook.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@
99
tasks:
1010
- name: generate messages
1111
action: template src=message.j2 dest=/tmp/ansible_test_messages.out
12+
13+
roles:
14+
- role: hash_behavior_test_role
15+
messages:
16+
inside_a_role: "Indeed!"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- name: generate role messages
2+
action: template src=role_message.j2 dest=/tmp/ansible_test_role_messages.out
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{% for k, v in messages.iteritems() %}
2+
{{ k }}: {{ v }}
3+
{% endfor %}

0 commit comments

Comments
 (0)