Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
A
ansible
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
OpenEdx
ansible
Commits
4e5eb755
Commit
4e5eb755
authored
Jul 10, 2014
by
James Cammarata
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'svg_and_inventory_refactor' into devel
parents
6d24f437
aa261bdd
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
598 additions
and
288 deletions
+598
-288
bin/ansible
+1
-1
bin/ansible-playbook
+5
-8
lib/ansible/inventory/__init__.py
+169
-41
lib/ansible/inventory/dir.py
+163
-23
lib/ansible/inventory/group.py
+19
-2
lib/ansible/inventory/ini.py
+8
-2
lib/ansible/inventory/script.py
+7
-3
lib/ansible/inventory/vars_plugins/group_vars.py
+0
-195
lib/ansible/inventory/vars_plugins/noop.py
+48
-0
lib/ansible/playbook/__init__.py
+4
-0
lib/ansible/utils/__init__.py
+121
-12
test/units/TestInventory.py
+53
-1
No files found.
bin/ansible
View file @
4e5eb755
...
...
@@ -136,7 +136,7 @@ class Cli(object):
if
not
options
.
ask_vault_pass
:
vault_pass
=
tmp_vault_pass
inventory_manager
=
inventory
.
Inventory
(
options
.
inventory
)
inventory_manager
=
inventory
.
Inventory
(
options
.
inventory
,
vault_password
=
vault_pass
)
if
options
.
subset
:
inventory_manager
.
subset
(
options
.
subset
)
hosts
=
inventory_manager
.
list_hosts
(
pattern
)
...
...
bin/ansible-playbook
View file @
4e5eb755
...
...
@@ -100,11 +100,6 @@ def main(args):
if
(
options
.
ask_vault_pass
and
options
.
vault_password_file
):
parser
.
error
(
"--ask-vault-pass and --vault-password-file are mutually exclusive"
)
inventory
=
ansible
.
inventory
.
Inventory
(
options
.
inventory
)
inventory
.
subset
(
options
.
subset
)
if
len
(
inventory
.
list_hosts
())
==
0
:
raise
errors
.
AnsibleError
(
"provided hosts list is empty"
)
sshpass
=
None
sudopass
=
None
su_pass
=
None
...
...
@@ -160,12 +155,14 @@ def main(args):
if
not
(
os
.
path
.
isfile
(
playbook
)
or
stat
.
S_ISFIFO
(
os
.
stat
(
playbook
)
.
st_mode
)):
raise
errors
.
AnsibleError
(
"the playbook:
%
s does not appear to be a file"
%
playbook
)
inventory
=
ansible
.
inventory
.
Inventory
(
options
.
inventory
,
vault_password
=
vault_pass
)
inventory
.
subset
(
options
.
subset
)
if
len
(
inventory
.
list_hosts
())
==
0
:
raise
errors
.
AnsibleError
(
"provided hosts list is empty"
)
# run all playbooks specified on the command line
for
playbook
in
args
:
# let inventory know which playbooks are using so it can know the basedirs
inventory
.
set_playbook_basedir
(
os
.
path
.
dirname
(
playbook
))
stats
=
callbacks
.
AggregateStats
()
playbook_cb
=
callbacks
.
PlaybookCallbacks
(
verbose
=
utils
.
VERBOSITY
)
if
options
.
step
:
...
...
lib/ansible/inventory/__init__.py
View file @
4e5eb755
...
...
@@ -16,7 +16,6 @@
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#############################################
import
fnmatch
import
os
import
sys
...
...
@@ -39,13 +38,14 @@ class Inventory(object):
__slots__
=
[
'host_list'
,
'groups'
,
'_restriction'
,
'_also_restriction'
,
'_subset'
,
'parser'
,
'_vars_per_host'
,
'_vars_per_group'
,
'_hosts_cache'
,
'_groups_list'
,
'_pattern_cache'
,
'_vars_plugins'
,
'_playbook_basedir'
]
'_pattern_cache'
,
'_va
ult_password'
,
'_va
rs_plugins'
,
'_playbook_basedir'
]
def
__init__
(
self
,
host_list
=
C
.
DEFAULT_HOST_LIST
):
def
__init__
(
self
,
host_list
=
C
.
DEFAULT_HOST_LIST
,
vault_password
=
None
):
# the host file file, or script path, or list of hosts
# if a list, inventory data will NOT be loaded
self
.
host_list
=
host_list
self
.
_vault_password
=
vault_password
# caching to avoid repeated calculations, particularly with
# external inventory scripts.
...
...
@@ -56,7 +56,7 @@ class Inventory(object):
self
.
_groups_list
=
{}
self
.
_pattern_cache
=
{}
# to be set by calling set_playbook_basedir by
ansible-playbook
# to be set by calling set_playbook_basedir by
playbook code
self
.
_playbook_basedir
=
None
# the inventory object holds a list of groups
...
...
@@ -140,6 +140,14 @@ class Inventory(object):
self
.
_vars_plugins
=
[
x
for
x
in
utils
.
plugins
.
vars_loader
.
all
(
self
)
]
# get group vars from group_vars/ files and vars plugins
for
group
in
self
.
groups
:
group
.
vars
=
utils
.
combine_vars
(
group
.
vars
,
self
.
get_group_variables
(
group
.
name
,
self
.
_vault_password
))
# get host vars from host_vars/ files and vars plugins
for
host
in
self
.
get_hosts
():
host
.
vars
=
utils
.
combine_vars
(
host
.
vars
,
self
.
get_variables
(
host
.
name
,
self
.
_vault_password
))
def
_match
(
self
,
str
,
pattern_str
):
if
pattern_str
.
startswith
(
'~'
):
...
...
@@ -147,6 +155,17 @@ class Inventory(object):
else
:
return
fnmatch
.
fnmatch
(
str
,
pattern_str
)
def
_match_list
(
self
,
items
,
item_attr
,
pattern_str
):
results
=
[]
if
not
pattern_str
.
startswith
(
'~'
):
pattern
=
re
.
compile
(
fnmatch
.
translate
(
pattern_str
))
else
:
pattern
=
re
.
compile
(
pattern_str
[
1
:])
for
item
in
items
:
if
pattern
.
search
(
getattr
(
item
,
item_attr
)):
results
.
append
(
item
)
return
results
def
get_hosts
(
self
,
pattern
=
"all"
):
"""
find all host names matching a pattern string, taking into account any inventory restrictions or
...
...
@@ -187,7 +206,7 @@ class Inventory(object):
pattern_exclude
.
append
(
p
)
elif
p
.
startswith
(
"&"
):
pattern_intersection
.
append
(
p
)
el
se
:
el
if
p
:
pattern_regular
.
append
(
p
)
# if no regular pattern was given, hence only exclude and/or intersection
...
...
@@ -202,15 +221,18 @@ class Inventory(object):
hosts
=
[]
for
p
in
patterns
:
that
=
self
.
__get_hosts
(
p
)
if
p
.
startswith
(
"!"
):
hosts
=
[
h
for
h
in
hosts
if
h
not
in
that
]
elif
p
.
startswith
(
"&"
):
hosts
=
[
h
for
h
in
hosts
if
h
in
that
]
# avoid resolving a pattern that is a plain host
if
p
in
self
.
_hosts_cache
:
hosts
.
append
(
self
.
get_host
(
p
))
else
:
to_append
=
[
h
for
h
in
that
if
h
.
name
not
in
[
y
.
name
for
y
in
hosts
]
]
hosts
.
extend
(
to_append
)
that
=
self
.
__get_hosts
(
p
)
if
p
.
startswith
(
"!"
):
hosts
=
[
h
for
h
in
hosts
if
h
not
in
that
]
elif
p
.
startswith
(
"&"
):
hosts
=
[
h
for
h
in
hosts
if
h
in
that
]
else
:
to_append
=
[
h
for
h
in
that
if
h
.
name
not
in
[
y
.
name
for
y
in
hosts
]
]
hosts
.
extend
(
to_append
)
return
hosts
def
__get_hosts
(
self
,
pattern
):
...
...
@@ -301,20 +323,31 @@ class Inventory(object):
def
_hosts_in_unenumerated_pattern
(
self
,
pattern
):
""" Get all host names matching the pattern """
results
=
[]
hosts
=
[]
hostnames
=
set
()
# ignore any negative checks here, this is handled elsewhere
pattern
=
pattern
.
replace
(
"!"
,
""
)
.
replace
(
"&"
,
""
)
results
=
[]
def
__append_host_to_results
(
host
):
if
host
not
in
results
and
host
.
name
not
in
hostnames
:
hostnames
.
add
(
host
.
name
)
results
.
append
(
host
)
groups
=
self
.
get_groups
()
for
group
in
groups
:
for
host
in
group
.
get_hosts
():
if
pattern
==
'all'
or
self
.
_match
(
group
.
name
,
pattern
)
or
self
.
_match
(
host
.
name
,
pattern
):
if
host
not
in
results
and
host
.
name
not
in
hostnames
:
results
.
append
(
host
)
hostnames
.
add
(
host
.
name
)
if
pattern
==
'all'
:
for
host
in
group
.
get_hosts
():
__append_host_to_results
(
host
)
else
:
if
self
.
_match
(
group
.
name
,
pattern
):
for
host
in
group
.
get_hosts
():
__append_host_to_results
(
host
)
else
:
matching_hosts
=
self
.
_match_list
(
group
.
get_hosts
(),
'name'
,
pattern
)
for
host
in
matching_hosts
:
__append_host_to_results
(
host
)
if
pattern
in
[
"localhost"
,
"127.0.0.1"
]
and
len
(
results
)
==
0
:
new_host
=
self
.
_create_implicit_localhost
(
pattern
)
...
...
@@ -326,14 +359,10 @@ class Inventory(object):
self
.
_pattern_cache
=
{}
def
groups_for_host
(
self
,
host
):
results
=
[]
groups
=
self
.
get_groups
()
for
group
in
groups
:
for
hostn
in
group
.
get_hosts
():
if
host
==
hostn
.
name
:
results
.
append
(
group
)
continue
return
results
if
host
in
self
.
_hosts_cache
:
return
self
.
_hosts_cache
[
host
]
.
get_groups
()
else
:
return
[]
def
groups_list
(
self
):
if
not
self
.
_groups_list
:
...
...
@@ -374,19 +403,35 @@ class Inventory(object):
return
group
return
None
def
get_group_variables
(
self
,
groupname
):
if
groupname
not
in
self
.
_vars_per_group
:
self
.
_vars_per_group
[
groupname
]
=
self
.
_get_group_variables
(
groupname
)
def
get_group_variables
(
self
,
groupname
,
update_cached
=
False
,
vault_password
=
None
):
if
groupname
not
in
self
.
_vars_per_group
or
update_cached
:
self
.
_vars_per_group
[
groupname
]
=
self
.
_get_group_variables
(
groupname
,
vault_password
=
vault_password
)
return
self
.
_vars_per_group
[
groupname
]
def
_get_group_variables
(
self
,
groupname
):
def
_get_group_variables
(
self
,
groupname
,
vault_password
=
None
):
group
=
self
.
get_group
(
groupname
)
if
group
is
None
:
raise
Exception
(
"group not found:
%
s"
%
groupname
)
return
group
.
get_variables
()
def
get_variables
(
self
,
hostname
,
vault_password
=
None
):
if
hostname
not
in
self
.
_vars_per_host
:
vars
=
{}
# plugin.get_group_vars retrieves just vars for specific group
vars_results
=
[
plugin
.
get_group_vars
(
group
,
vault_password
=
vault_password
)
for
plugin
in
self
.
_vars_plugins
if
hasattr
(
plugin
,
'get_group_vars'
)]
for
updated
in
vars_results
:
if
updated
is
not
None
:
vars
=
utils
.
combine_vars
(
vars
,
updated
)
# get group variables set by Inventory Parsers
vars
=
utils
.
combine_vars
(
vars
,
group
.
get_variables
())
# Read group_vars/ files
vars
=
utils
.
combine_vars
(
vars
,
self
.
get_group_vars
(
group
))
return
vars
def
get_variables
(
self
,
hostname
,
update_cached
=
False
,
vault_password
=
None
):
if
hostname
not
in
self
.
_vars_per_host
or
update_cached
:
self
.
_vars_per_host
[
hostname
]
=
self
.
_get_variables
(
hostname
,
vault_password
=
vault_password
)
return
self
.
_vars_per_host
[
hostname
]
...
...
@@ -397,19 +442,39 @@ class Inventory(object):
raise
errors
.
AnsibleError
(
"host not found:
%
s"
%
hostname
)
vars
=
{}
vars_results
=
[
plugin
.
run
(
host
,
vault_password
=
vault_password
)
for
plugin
in
self
.
_vars_plugins
]
# plugin.run retrieves all vars (also from groups) for host
vars_results
=
[
plugin
.
run
(
host
,
vault_password
=
vault_password
)
for
plugin
in
self
.
_vars_plugins
if
hasattr
(
plugin
,
'run'
)]
for
updated
in
vars_results
:
if
updated
is
not
None
:
vars
=
utils
.
combine_vars
(
vars
,
updated
)
# plugin.get_host_vars retrieves just vars for specific host
vars_results
=
[
plugin
.
get_host_vars
(
host
,
vault_password
=
vault_password
)
for
plugin
in
self
.
_vars_plugins
if
hasattr
(
plugin
,
'get_host_vars'
)]
for
updated
in
vars_results
:
if
updated
is
not
None
:
vars
=
utils
.
combine_vars
(
vars
,
updated
)
# get host variables set by Inventory Parsers
vars
=
utils
.
combine_vars
(
vars
,
host
.
get_variables
())
# still need to check InventoryParser per host vars
# which actually means InventoryScript per host,
# which is not performant
if
self
.
parser
is
not
None
:
vars
=
utils
.
combine_vars
(
vars
,
self
.
parser
.
get_host_variables
(
host
))
# Read host_vars/ files
vars
=
utils
.
combine_vars
(
vars
,
self
.
get_host_vars
(
host
))
return
vars
def
add_group
(
self
,
group
):
self
.
groups
.
append
(
group
)
self
.
_groups_list
=
None
# invalidate internal cache
if
group
.
name
not
in
self
.
groups_list
():
self
.
groups
.
append
(
group
)
self
.
_groups_list
=
None
# invalidate internal cache
else
:
raise
errors
.
AnsibleError
(
"group already in inventory:
%
s"
%
group
.
name
)
def
list_hosts
(
self
,
pattern
=
"all"
):
...
...
@@ -504,10 +569,73 @@ class Inventory(object):
return
self
.
_playbook_basedir
def
set_playbook_basedir
(
self
,
dir
):
"""
sets the base directory of the playbook so inventory plugins can use it to find
variable files and other things.
"""
self
.
_playbook_basedir
=
dir
sets the base directory of the playbook so inventory can use it as a
basedir for host_ and group_vars, and other things.
"""
# Only update things if dir is a different playbook basedir
if
dir
!=
self
.
_playbook_basedir
:
self
.
_playbook_basedir
=
dir
# get group vars from group_vars/ files
for
group
in
self
.
groups
:
group
.
vars
=
utils
.
combine_vars
(
group
.
vars
,
self
.
get_group_vars
(
group
,
new_pb_basedir
=
True
))
# get host vars from host_vars/ files
for
host
in
self
.
get_hosts
():
host
.
vars
=
utils
.
combine_vars
(
host
.
vars
,
self
.
get_host_vars
(
host
,
new_pb_basedir
=
True
))
def
get_host_vars
(
self
,
host
,
new_pb_basedir
=
False
):
""" Read host_vars/ files """
return
self
.
_get_hostgroup_vars
(
host
=
host
,
group
=
None
,
new_pb_basedir
=
False
)
def
get_group_vars
(
self
,
group
,
new_pb_basedir
=
False
):
""" Read group_vars/ files """
return
self
.
_get_hostgroup_vars
(
host
=
None
,
group
=
group
,
new_pb_basedir
=
False
)
def
_get_hostgroup_vars
(
self
,
host
=
None
,
group
=
None
,
new_pb_basedir
=
False
):
"""
Loads variables from group_vars/<groupname> and host_vars/<hostname> in directories parallel
to the inventory base directory or in the same directory as the playbook. Variables in the playbook
dir will win over the inventory dir if files are in both.
"""
results
=
{}
scan_pass
=
0
_basedir
=
self
.
basedir
()
# look in both the inventory base directory and the playbook base directory
# unless we do an update for a new playbook base dir
if
not
new_pb_basedir
:
basedirs
=
[
_basedir
,
self
.
_playbook_basedir
]
else
:
basedirs
=
[
self
.
_playbook_basedir
]
for
basedir
in
basedirs
:
# this can happen from particular API usages, particularly if not run
# from /usr/bin/ansible-playbook
if
basedir
is
None
:
continue
scan_pass
=
scan_pass
+
1
# it's not an eror if the directory does not exist, keep moving
if
not
os
.
path
.
exists
(
basedir
):
continue
# save work of second scan if the directories are the same
if
_basedir
==
self
.
_playbook_basedir
and
scan_pass
!=
1
:
continue
if
group
and
host
is
None
:
# load vars in dir/group_vars/name_of_group
base_path
=
os
.
path
.
join
(
basedir
,
"group_vars/
%
s"
%
group
.
name
)
results
=
utils
.
load_vars
(
base_path
,
results
,
vault_password
=
self
.
_vault_password
)
elif
host
and
group
is
None
:
# same for hostvars in dir/host_vars/name_of_host
base_path
=
os
.
path
.
join
(
basedir
,
"host_vars/
%
s"
%
host
.
name
)
results
=
utils
.
load_vars
(
base_path
,
results
,
vault_password
=
self
.
_vault_password
)
# all done, results is a dictionary of variables for this particular host.
return
results
lib/ansible/inventory/dir.py
View file @
4e5eb755
# (c) 2013, Daniel Hokka Zakrisson <daniel@hozac.com>
# (c) 2014, Serge van Ginderachter <serge@vanginderachter.be>
#
# This file is part of Ansible
#
...
...
@@ -56,29 +57,168 @@ class InventoryDirectory(object):
else
:
parser
=
InventoryParser
(
filename
=
fullpath
)
self
.
parsers
.
append
(
parser
)
# This takes a lot of code because we can't directly use any of the objects, as they have to blend
for
name
,
group
in
parser
.
groups
.
iteritems
():
if
name
not
in
self
.
groups
:
self
.
groups
[
name
]
=
group
else
:
# group is already there, copy variables
# note: depth numbers on duplicates may be bogus
for
k
,
v
in
group
.
get_variables
()
.
iteritems
():
self
.
groups
[
name
]
.
set_variable
(
k
,
v
)
for
host
in
group
.
get_hosts
():
if
host
.
name
not
in
self
.
hosts
:
self
.
hosts
[
host
.
name
]
=
host
else
:
# host is already there, copy variables
# note: depth numbers on duplicates may be bogus
for
k
,
v
in
host
.
vars
.
iteritems
():
self
.
hosts
[
host
.
name
]
.
set_variable
(
k
,
v
)
self
.
groups
[
name
]
.
add_host
(
self
.
hosts
[
host
.
name
])
# This needs to be a second loop to ensure all the parent groups exist
for
name
,
group
in
parser
.
groups
.
iteritems
():
for
ancestor
in
group
.
get_ancestors
():
self
.
groups
[
ancestor
.
name
]
.
add_child_group
(
self
.
groups
[
name
])
# retrieve all groups and hosts form the parser and add them to
# self, don't look at group lists yet, to avoid
# recursion trouble, but just make sure all objects exist in self
newgroups
=
parser
.
groups
.
values
()
for
group
in
newgroups
:
for
host
in
group
.
hosts
:
self
.
_add_host
(
host
)
for
group
in
newgroups
:
self
.
_add_group
(
group
)
# now check the objects lists so they contain only objects from
# self; membership data in groups is already fine (except all &
# ungrouped, see later), but might still reference objects not in self
for
group
in
self
.
groups
.
values
():
# iterate on a copy of the lists, as those lists get changed in
# the loop
# list with group's child group objects:
for
child
in
group
.
child_groups
[:]:
if
child
!=
self
.
groups
[
child
.
name
]:
group
.
child_groups
.
remove
(
child
)
group
.
child_groups
.
append
(
self
.
groups
[
child
.
name
])
# list with group's parent group objects:
for
parent
in
group
.
parent_groups
[:]:
if
parent
!=
self
.
groups
[
parent
.
name
]:
group
.
parent_groups
.
remove
(
parent
)
group
.
parent_groups
.
append
(
self
.
groups
[
parent
.
name
])
# list with group's host objects:
for
host
in
group
.
hosts
[:]:
if
host
!=
self
.
hosts
[
host
.
name
]:
group
.
hosts
.
remove
(
host
)
group
.
hosts
.
append
(
self
.
hosts
[
host
.
name
])
# also check here that the group that contains host, is
# also contained in the host's group list
if
group
not
in
self
.
hosts
[
host
.
name
]
.
groups
:
self
.
hosts
[
host
.
name
]
.
groups
.
append
(
group
)
# extra checks on special groups all and ungrouped
# remove hosts from 'ungrouped' if they became member of other groups
if
'ungrouped'
in
self
.
groups
:
ungrouped
=
self
.
groups
[
'ungrouped'
]
# loop on a copy of ungrouped hosts, as we want to change that list
for
host
in
ungrouped
.
hosts
[:]:
if
len
(
host
.
groups
)
>
1
:
host
.
groups
.
remove
(
ungrouped
)
ungrouped
.
hosts
.
remove
(
host
)
# remove hosts from 'all' if they became member of other groups
# all should only contain direct children, not grandchildren
# direct children should have dept == 1
if
'all'
in
self
.
groups
:
allgroup
=
self
.
groups
[
'all'
]
# loop on a copy of all's child groups, as we want to change that list
for
group
in
allgroup
.
child_groups
[:]:
# groups might once have beeen added to all, and later be added
# to another group: we need to remove the link wit all then
if
len
(
group
.
parent_groups
)
>
1
:
# real children of all have just 1 parent, all
# this one has more, so not a direct child of all anymore
group
.
parent_groups
.
remove
(
allgroup
)
allgroup
.
child_groups
.
remove
(
group
)
elif
allgroup
not
in
group
.
parent_groups
:
# this group was once added to all, but doesn't list it as
# a parent any more; the info in the group is the correct
# info
allgroup
.
child_groups
.
remove
(
group
)
def
_add_group
(
self
,
group
):
""" Merge an existing group or add a new one;
Track parent and child groups, and hosts of the new one """
if
group
.
name
not
in
self
.
groups
:
# it's brand new, add him!
self
.
groups
[
group
.
name
]
=
group
if
self
.
groups
[
group
.
name
]
!=
group
:
# different object, merge
self
.
_merge_groups
(
self
.
groups
[
group
.
name
],
group
)
def
_add_host
(
self
,
host
):
if
host
.
name
not
in
self
.
hosts
:
# Papa's got a brand new host
self
.
hosts
[
host
.
name
]
=
host
if
self
.
hosts
[
host
.
name
]
!=
host
:
# different object, merge
self
.
_merge_hosts
(
self
.
hosts
[
host
.
name
],
host
)
def
_merge_groups
(
self
,
group
,
newgroup
):
""" Merge all of instance newgroup into group,
update parent/child relationships
group lists may still contain group objects that exist in self with
same name, but was instanciated as a different object in some other
inventory parser; these are handled later """
# name
if
group
.
name
!=
newgroup
.
name
:
raise
errors
.
AnsibleError
(
"Cannot merge group
%
s with
%
s"
%
(
group
.
name
,
newgroup
.
name
))
# depth
group
.
depth
=
max
([
group
.
depth
,
newgroup
.
depth
])
# hosts list (host objects are by now already added to self.hosts)
for
host
in
newgroup
.
hosts
:
grouphosts
=
dict
([(
h
.
name
,
h
)
for
h
in
group
.
hosts
])
if
host
.
name
in
grouphosts
:
# same host name but different object, merge
self
.
_merge_hosts
(
grouphosts
[
host
.
name
],
host
)
else
:
# new membership, add host to group from self
# group from self will also be added again to host.groups, but
# as different object
group
.
add_host
(
self
.
hosts
[
host
.
name
])
# now remove this the old object for group in host.groups
for
hostgroup
in
[
g
for
g
in
host
.
groups
]:
if
hostgroup
.
name
==
group
.
name
and
hostgroup
!=
self
.
groups
[
group
.
name
]:
self
.
hosts
[
host
.
name
]
.
groups
.
remove
(
hostgroup
)
# group child membership relation
for
newchild
in
newgroup
.
child_groups
:
# dict with existing child groups:
childgroups
=
dict
([(
g
.
name
,
g
)
for
g
in
group
.
child_groups
])
# check if child of new group is already known as a child
if
newchild
.
name
not
in
childgroups
:
self
.
groups
[
group
.
name
]
.
add_child_group
(
newchild
)
# group parent membership relation
for
newparent
in
newgroup
.
parent_groups
:
# dict with existing parent groups:
parentgroups
=
dict
([(
g
.
name
,
g
)
for
g
in
group
.
parent_groups
])
# check if parent of new group is already known as a parent
if
newparent
.
name
not
in
parentgroups
:
if
newparent
.
name
not
in
self
.
groups
:
# group does not exist yet in self, import him
self
.
groups
[
newparent
.
name
]
=
newparent
# group now exists but not yet as a parent here
self
.
groups
[
newparent
.
name
]
.
add_child_group
(
group
)
# variables
group
.
vars
=
utils
.
combine_vars
(
group
.
vars
,
newgroup
.
vars
)
def
_merge_hosts
(
self
,
host
,
newhost
):
""" Merge all of instance newhost into host """
# name
if
host
.
name
!=
newhost
.
name
:
raise
errors
.
AnsibleError
(
"Cannot merge host
%
s with
%
s"
%
(
host
.
name
,
newhost
.
name
))
# group membership relation
for
newgroup
in
newhost
.
groups
:
# dict with existing groups:
hostgroups
=
dict
([(
g
.
name
,
g
)
for
g
in
host
.
groups
])
# check if new group is already known as a group
if
newgroup
.
name
not
in
hostgroups
:
if
newgroup
.
name
not
in
self
.
groups
:
# group does not exist yet in self, import him
self
.
groups
[
newgroup
.
name
]
=
newgroup
# group now exists but doesn't have host yet
self
.
groups
[
newgroup
.
name
]
.
add_host
(
host
)
# variables
host
.
vars
=
utils
.
combine_vars
(
host
.
vars
,
newhost
.
vars
)
def
get_host_variables
(
self
,
host
):
""" Gets additional host variables from all inventories """
...
...
lib/ansible/inventory/group.py
View file @
4e5eb755
...
...
@@ -28,7 +28,8 @@ class Group(object):
self
.
vars
=
{}
self
.
child_groups
=
[]
self
.
parent_groups
=
[]
self
.
clear_hosts_cache
()
self
.
_hosts_cache
=
None
#self.clear_hosts_cache()
if
self
.
name
is
None
:
raise
Exception
(
"group name is required"
)
...
...
@@ -40,10 +41,26 @@ class Group(object):
# don't add if it's already there
if
not
group
in
self
.
child_groups
:
self
.
child_groups
.
append
(
group
)
# update the depth of the child
group
.
depth
=
max
([
self
.
depth
+
1
,
group
.
depth
])
group
.
parent_groups
.
append
(
self
)
# update the depth of the grandchildren
group
.
_check_children_depth
()
# now add self to child's parent_groups list, but only if there
# isn't already a group with the same name
if
not
self
.
name
in
[
g
.
name
for
g
in
group
.
parent_groups
]:
group
.
parent_groups
.
append
(
self
)
self
.
clear_hosts_cache
()
def
_check_children_depth
(
self
):
for
group
in
self
.
child_groups
:
group
.
depth
=
max
([
self
.
depth
+
1
,
group
.
depth
])
group
.
_check_children_depth
()
def
add_host
(
self
,
host
):
self
.
hosts
.
append
(
host
)
...
...
lib/ansible/inventory/ini.py
View file @
4e5eb755
...
...
@@ -45,6 +45,7 @@ class InventoryParser(object):
self
.
_parse_base_groups
()
self
.
_parse_group_children
()
self
.
_add_allgroup_children
()
self
.
_parse_group_variables
()
return
self
.
groups
...
...
@@ -69,6 +70,13 @@ class InventoryParser(object):
# gamma sudo=True user=root
# delta asdf=jkl favcolor=red
def
_add_allgroup_children
(
self
):
for
group
in
self
.
groups
.
values
():
if
group
.
depth
==
0
and
group
.
name
!=
'all'
:
self
.
groups
[
'all'
]
.
add_child_group
(
group
)
def
_parse_base_groups
(
self
):
# FIXME: refactor
...
...
@@ -87,11 +95,9 @@ class InventoryParser(object):
active_group_name
=
active_group_name
.
rsplit
(
":"
,
1
)[
0
]
if
active_group_name
not
in
self
.
groups
:
new_group
=
self
.
groups
[
active_group_name
]
=
Group
(
name
=
active_group_name
)
all
.
add_child_group
(
new_group
)
active_group_name
=
None
elif
active_group_name
not
in
self
.
groups
:
new_group
=
self
.
groups
[
active_group_name
]
=
Group
(
name
=
active_group_name
)
all
.
add_child_group
(
new_group
)
elif
line
.
startswith
(
";"
)
or
line
==
''
:
pass
elif
active_group_name
:
...
...
lib/ansible/inventory/script.py
View file @
4e5eb755
...
...
@@ -46,6 +46,7 @@ class InventoryScript(object):
self
.
host_vars_from_top
=
None
self
.
groups
=
self
.
_parse
(
stderr
)
def
_parse
(
self
,
err
):
all_hosts
=
{}
...
...
@@ -63,7 +64,7 @@ class InventoryScript(object):
raise
errors
.
AnsibleError
(
"failed to parse executable inventory script results:
%
s"
%
self
.
raw
)
for
(
group_name
,
data
)
in
self
.
raw
.
items
():
# in Ansible 1.3 and later, a "_meta" subelement may contain
# a variable "hostvars" which contains a hash for each host
# if this "hostvars" exists at all then do not call --host for each
...
...
@@ -100,8 +101,6 @@ class InventoryScript(object):
all
.
set_variable
(
k
,
v
)
else
:
group
.
set_variable
(
k
,
v
)
if
group
.
name
!=
all
.
name
:
all
.
add_child_group
(
group
)
# Separate loop to ensure all groups are defined
for
(
group_name
,
data
)
in
self
.
raw
.
items
():
...
...
@@ -111,6 +110,11 @@ class InventoryScript(object):
for
child_name
in
data
[
'children'
]:
if
child_name
in
groups
:
groups
[
group_name
]
.
add_child_group
(
groups
[
child_name
])
for
group
in
groups
.
values
():
if
group
.
depth
==
0
and
group
.
name
!=
'all'
:
all
.
add_child_group
(
group
)
return
groups
def
get_host_variables
(
self
,
host
):
...
...
lib/ansible/inventory/vars_plugins/group_vars.py
deleted
100644 → 0
View file @
6d24f437
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
import
os
import
stat
import
errno
from
ansible
import
errors
from
ansible
import
utils
import
ansible.constants
as
C
def
_load_vars
(
basepath
,
results
,
vault_password
=
None
):
"""
Load variables from any potential yaml filename combinations of basepath,
returning result.
"""
paths_to_check
=
[
""
.
join
([
basepath
,
ext
])
for
ext
in
C
.
YAML_FILENAME_EXTENSIONS
]
found_paths
=
[]
for
path
in
paths_to_check
:
found
,
results
=
_load_vars_from_path
(
path
,
results
,
vault_password
=
vault_password
)
if
found
:
found_paths
.
append
(
path
)
# disallow the potentially confusing situation that there are multiple
# variable files for the same name. For example if both group_vars/all.yml
# and group_vars/all.yaml
if
len
(
found_paths
)
>
1
:
raise
errors
.
AnsibleError
(
"Multiple variable files found. "
"There should only be one.
%
s"
%
(
found_paths
,
))
return
results
def
_load_vars_from_path
(
path
,
results
,
vault_password
=
None
):
"""
Robustly access the file at path and load variables, carefully reporting
errors in a friendly/informative way.
Return the tuple (found, new_results, )
"""
try
:
# in the case of a symbolic link, we want the stat of the link itself,
# not its target
pathstat
=
os
.
lstat
(
path
)
except
os
.
error
,
err
:
# most common case is that nothing exists at that path.
if
err
.
errno
==
errno
.
ENOENT
:
return
False
,
results
# otherwise this is a condition we should report to the user
raise
errors
.
AnsibleError
(
"
%
s is not accessible:
%
s."
" Please check its permissions."
%
(
path
,
err
.
strerror
))
# symbolic link
if
stat
.
S_ISLNK
(
pathstat
.
st_mode
):
try
:
target
=
os
.
path
.
realpath
(
path
)
except
os
.
error
,
err2
:
raise
errors
.
AnsibleError
(
"The symbolic link at
%
s "
"is not readable:
%
s. Please check its permissions."
%
(
path
,
err2
.
strerror
,
))
# follow symbolic link chains by recursing, so we repeat the same
# permissions checks above and provide useful errors.
return
_load_vars_from_path
(
target
,
results
)
# directory
if
stat
.
S_ISDIR
(
pathstat
.
st_mode
):
# support organizing variables across multiple files in a directory
return
True
,
_load_vars_from_folder
(
path
,
results
,
vault_password
=
vault_password
)
# regular file
elif
stat
.
S_ISREG
(
pathstat
.
st_mode
):
data
=
utils
.
parse_yaml_from_file
(
path
,
vault_password
=
vault_password
)
if
data
and
type
(
data
)
!=
dict
:
raise
errors
.
AnsibleError
(
"
%
s must be stored as a dictionary/hash"
%
path
)
elif
data
is
None
:
data
=
{}
# combine vars overrides by default but can be configured to do a
# hash merge in settings
results
=
utils
.
combine_vars
(
results
,
data
)
return
True
,
results
# something else? could be a fifo, socket, device, etc.
else
:
raise
errors
.
AnsibleError
(
"Expected a variable file or directory "
"but found a non-file object at path
%
s"
%
(
path
,
))
def
_load_vars_from_folder
(
folder_path
,
results
,
vault_password
=
None
):
"""
Load all variables within a folder recursively.
"""
# this function and _load_vars_from_path are mutually recursive
try
:
names
=
os
.
listdir
(
folder_path
)
except
os
.
error
,
err
:
raise
errors
.
AnsibleError
(
"This folder cannot be listed:
%
s:
%
s."
%
(
folder_path
,
err
.
strerror
))
# evaluate files in a stable order rather than whatever order the
# filesystem lists them.
names
.
sort
()
# do not parse hidden files or dirs, e.g. .svn/
paths
=
[
os
.
path
.
join
(
folder_path
,
name
)
for
name
in
names
if
not
name
.
startswith
(
'.'
)]
for
path
in
paths
:
_found
,
results
=
_load_vars_from_path
(
path
,
results
,
vault_password
=
vault_password
)
return
results
class
VarsModule
(
object
):
"""
Loads variables from group_vars/<groupname> and host_vars/<hostname> in directories parallel
to the inventory base directory or in the same directory as the playbook. Variables in the playbook
dir will win over the inventory dir if files are in both.
"""
def
__init__
(
self
,
inventory
):
""" constructor """
self
.
inventory
=
inventory
def
run
(
self
,
host
,
vault_password
=
None
):
""" main body of the plugin, does actual loading """
inventory
=
self
.
inventory
basedir
=
inventory
.
playbook_basedir
()
if
basedir
is
not
None
:
basedir
=
os
.
path
.
abspath
(
basedir
)
self
.
pb_basedir
=
basedir
# sort groups by depth so deepest groups can override the less deep ones
groupz
=
sorted
(
inventory
.
groups_for_host
(
host
.
name
),
key
=
lambda
g
:
g
.
depth
)
groups
=
[
g
.
name
for
g
in
groupz
]
inventory_basedir
=
inventory
.
basedir
()
results
=
{}
scan_pass
=
0
# look in both the inventory base directory and the playbook base directory
for
basedir
in
[
inventory_basedir
,
self
.
pb_basedir
]:
# this can happen from particular API usages, particularly if not run
# from /usr/bin/ansible-playbook
if
basedir
is
None
:
continue
scan_pass
=
scan_pass
+
1
# it's not an eror if the directory does not exist, keep moving
if
not
os
.
path
.
exists
(
basedir
):
continue
# save work of second scan if the directories are the same
if
inventory_basedir
==
self
.
pb_basedir
and
scan_pass
!=
1
:
continue
# load vars in dir/group_vars/name_of_group
for
group
in
groups
:
base_path
=
os
.
path
.
join
(
basedir
,
"group_vars/
%
s"
%
group
)
results
=
_load_vars
(
base_path
,
results
,
vault_password
=
vault_password
)
# same for hostvars in dir/host_vars/name_of_host
base_path
=
os
.
path
.
join
(
basedir
,
"host_vars/
%
s"
%
host
.
name
)
results
=
_load_vars
(
base_path
,
results
,
vault_password
=
vault_password
)
# all done, results is a dictionary of variables for this particular host.
return
results
lib/ansible/inventory/vars_plugins/noop.py
0 → 100644
View file @
4e5eb755
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
# (c) 2014, Serge van Ginderachter <serge@vanginderachter.be>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
class
VarsModule
(
object
):
"""
Loads variables for groups and/or hosts
"""
def
__init__
(
self
,
inventory
):
""" constructor """
self
.
inventory
=
inventory
self
.
inventory_basedir
=
inventory
.
basedir
()
def
run
(
self
,
host
,
vault_password
=
None
):
""" For backwards compatibility, when only vars per host were retrieved
This method should return both host specific vars as well as vars
calculated from groups it is a member of """
return
{}
def
get_host_vars
(
self
,
host
,
vault_password
=
None
):
""" Get host specific variables. """
return
{}
def
get_group_vars
(
self
,
group
,
vault_password
=
None
):
""" Get group specific variables. """
return
{}
lib/ansible/playbook/__init__.py
View file @
4e5eb755
...
...
@@ -164,6 +164,10 @@ class PlayBook(object):
self
.
basedir
=
os
.
path
.
dirname
(
playbook
)
or
'.'
utils
.
plugins
.
push_basedir
(
self
.
basedir
)
# let inventory know the playbook basedir so it can load more vars
self
.
inventory
.
set_playbook_basedir
(
self
.
basedir
)
vars
=
extra_vars
.
copy
()
vars
[
'playbook_dir'
]
=
self
.
basedir
if
self
.
inventory
.
basedir
()
is
not
None
:
...
...
lib/ansible/utils/__init__.py
View file @
4e5eb755
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
# (c) 2012
-2014
, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
...
...
@@ -15,6 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
import
errno
import
sys
import
re
import
os
...
...
@@ -620,18 +621,19 @@ def merge_hash(a, b):
''' recursively merges hash b into a
keys from b take precedence over keys from a '''
result
=
copy
.
deepcopy
(
a
)
result
=
{}
# next, iterate over b keys and values
for
k
,
v
in
b
.
iteritems
():
# if there's already such key in a
# and that key contains dict
if
k
in
result
and
isinstance
(
result
[
k
],
dict
):
# merge those dicts recursively
result
[
k
]
=
merge_hash
(
a
[
k
],
v
)
else
:
# otherwise, just copy a value from b to a
result
[
k
]
=
v
for
dicts
in
a
,
b
:
# next, iterate over b keys and values
for
k
,
v
in
dicts
.
iteritems
():
# if there's already such key in a
# and that key contains dict
if
k
in
result
and
isinstance
(
result
[
k
],
dict
):
# merge those dicts recursively
result
[
k
]
=
merge_hash
(
a
[
k
],
v
)
else
:
# otherwise, just copy a value from b to a
result
[
k
]
=
v
return
result
...
...
@@ -1208,5 +1210,112 @@ def before_comment(msg):
msg
=
msg
.
replace
(
"**NOT_A_COMMENT**"
,
"#"
)
return
msg
def
load_vars
(
basepath
,
results
,
vault_password
=
None
):
"""
Load variables from any potential yaml filename combinations of basepath,
returning result.
"""
paths_to_check
=
[
""
.
join
([
basepath
,
ext
])
for
ext
in
C
.
YAML_FILENAME_EXTENSIONS
]
found_paths
=
[]
for
path
in
paths_to_check
:
found
,
results
=
_load_vars_from_path
(
path
,
results
,
vault_password
=
vault_password
)
if
found
:
found_paths
.
append
(
path
)
# disallow the potentially confusing situation that there are multiple
# variable files for the same name. For example if both group_vars/all.yml
# and group_vars/all.yaml
if
len
(
found_paths
)
>
1
:
raise
errors
.
AnsibleError
(
"Multiple variable files found. "
"There should only be one.
%
s"
%
(
found_paths
,
))
return
results
## load variables from yaml files/dirs
# e.g. host/group_vars
#
def
_load_vars_from_path
(
path
,
results
,
vault_password
=
None
):
"""
Robustly access the file at path and load variables, carefully reporting
errors in a friendly/informative way.
Return the tuple (found, new_results, )
"""
try
:
# in the case of a symbolic link, we want the stat of the link itself,
# not its target
pathstat
=
os
.
lstat
(
path
)
except
os
.
error
,
err
:
# most common case is that nothing exists at that path.
if
err
.
errno
==
errno
.
ENOENT
:
return
False
,
results
# otherwise this is a condition we should report to the user
raise
errors
.
AnsibleError
(
"
%
s is not accessible:
%
s."
" Please check its permissions."
%
(
path
,
err
.
strerror
))
# symbolic link
if
stat
.
S_ISLNK
(
pathstat
.
st_mode
):
try
:
target
=
os
.
path
.
realpath
(
path
)
except
os
.
error
,
err2
:
raise
errors
.
AnsibleError
(
"The symbolic link at
%
s "
"is not readable:
%
s. Please check its permissions."
%
(
path
,
err2
.
strerror
,
))
# follow symbolic link chains by recursing, so we repeat the same
# permissions checks above and provide useful errors.
return
_load_vars_from_path
(
target
,
results
)
# directory
if
stat
.
S_ISDIR
(
pathstat
.
st_mode
):
# support organizing variables across multiple files in a directory
return
True
,
_load_vars_from_folder
(
path
,
results
,
vault_password
=
vault_password
)
# regular file
elif
stat
.
S_ISREG
(
pathstat
.
st_mode
):
data
=
parse_yaml_from_file
(
path
,
vault_password
=
vault_password
)
if
type
(
data
)
!=
dict
:
raise
errors
.
AnsibleError
(
"
%
s must be stored as a dictionary/hash"
%
path
)
# combine vars overrides by default but can be configured to do a
# hash merge in settings
results
=
combine_vars
(
results
,
data
)
return
True
,
results
# something else? could be a fifo, socket, device, etc.
else
:
raise
errors
.
AnsibleError
(
"Expected a variable file or directory "
"but found a non-file object at path
%
s"
%
(
path
,
))
def
_load_vars_from_folder
(
folder_path
,
results
,
vault_password
=
None
):
"""
Load all variables within a folder recursively.
"""
# this function and _load_vars_from_path are mutually recursive
try
:
names
=
os
.
listdir
(
folder_path
)
except
os
.
error
,
err
:
raise
errors
.
AnsibleError
(
"This folder cannot be listed:
%
s:
%
s."
%
(
folder_path
,
err
.
strerror
))
# evaluate files in a stable order rather than whatever order the
# filesystem lists them.
names
.
sort
()
# do not parse hidden files or dirs, e.g. .svn/
paths
=
[
os
.
path
.
join
(
folder_path
,
name
)
for
name
in
names
if
not
name
.
startswith
(
'.'
)]
for
path
in
paths
:
_found
,
results
=
_load_vars_from_path
(
path
,
results
,
vault_password
=
vault_password
)
return
results
test/units/TestInventory.py
View file @
4e5eb755
...
...
@@ -433,7 +433,7 @@ class TestInventory(unittest.TestCase):
expected_vars
=
{
'inventory_hostname'
:
'zeus'
,
'inventory_hostname_short'
:
'zeus'
,
'group_names'
:
[
'greek'
,
'major-god'
,
'ungrouped'
],
'group_names'
:
[
'greek'
,
'major-god'
],
'var_a'
:
'3#4'
}
print
"HOST VARS=
%
s"
%
host_vars
...
...
@@ -451,3 +451,55 @@ class TestInventory(unittest.TestCase):
def
test_dir_inventory_skip_extension
(
self
):
inventory
=
self
.
dir_inventory
()
assert
'skipme'
not
in
[
h
.
name
for
h
in
inventory
.
get_hosts
()]
def
test_dir_inventory_group_hosts
(
self
):
inventory
=
self
.
dir_inventory
()
expected_groups
=
{
'all'
:
[
'morpheus'
,
'thor'
,
'zeus'
],
'major-god'
:
[
'thor'
,
'zeus'
],
'minor-god'
:
[
'morpheus'
],
'norse'
:
[
'thor'
],
'greek'
:
[
'morpheus'
,
'zeus'
],
'ungrouped'
:
[]}
actual_groups
=
{}
for
group
in
inventory
.
get_groups
():
actual_groups
[
group
.
name
]
=
sorted
([
h
.
name
for
h
in
group
.
get_hosts
()])
print
"INVENTORY groups[
%
s].hosts=
%
s"
%
(
group
.
name
,
actual_groups
[
group
.
name
])
print
"EXPECTED groups[
%
s].hosts=
%
s"
%
(
group
.
name
,
expected_groups
[
group
.
name
])
assert
actual_groups
==
expected_groups
def
test_dir_inventory_groups_for_host
(
self
):
inventory
=
self
.
dir_inventory
()
expected_groups_for_host
=
{
'morpheus'
:
[
'all'
,
'greek'
,
'minor-god'
],
'thor'
:
[
'all'
,
'major-god'
,
'norse'
],
'zeus'
:
[
'all'
,
'greek'
,
'major-god'
]}
actual_groups_for_host
=
{}
for
(
host
,
expected
)
in
expected_groups_for_host
.
iteritems
():
groups
=
inventory
.
groups_for_host
(
host
)
names
=
sorted
([
g
.
name
for
g
in
groups
])
actual_groups_for_host
[
host
]
=
names
print
"INVENTORY groups_for_host(
%
s)=
%
s"
%
(
host
,
names
)
print
"EXPECTED groups_for_host(
%
s)=
%
s"
%
(
host
,
expected
)
assert
actual_groups_for_host
==
expected_groups_for_host
def
test_dir_inventory_groups_list
(
self
):
inventory
=
self
.
dir_inventory
()
inventory_groups
=
inventory
.
groups_list
()
expected_groups
=
{
'all'
:
[
'morpheus'
,
'thor'
,
'zeus'
],
'major-god'
:
[
'thor'
,
'zeus'
],
'minor-god'
:
[
'morpheus'
],
'norse'
:
[
'thor'
],
'greek'
:
[
'morpheus'
,
'zeus'
],
'ungrouped'
:
[]}
for
(
name
,
expected_hosts
)
in
expected_groups
.
iteritems
():
inventory_groups
[
name
]
=
sorted
(
inventory_groups
.
get
(
name
,
[]))
print
"INVENTORY groups_list['
%
s']=
%
s"
%
(
name
,
inventory_groups
[
name
])
print
"EXPECTED groups_list['
%
s']=
%
s"
%
(
name
,
expected_hosts
)
assert
inventory_groups
==
expected_groups
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment