diff --git a/bin/ansible b/bin/ansible
index 1e2540fafb7..7e767b2f7db 100755
--- a/bin/ansible
+++ b/bin/ansible
@@ -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)
diff --git a/bin/ansible-playbook b/bin/ansible-playbook
index d7c9182e2f6..149a9f1c6ef 100755
--- a/bin/ansible-playbook
+++ b/bin/ansible-playbook
@@ -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:
diff --git a/lib/ansible/inventory/__init__.py b/lib/ansible/inventory/__init__.py
index 64bae5d815e..da601ceb2cb 100644
--- a/lib/ansible/inventory/__init__.py
+++ b/lib/ansible/inventory/__init__.py
@@ -16,7 +16,6 @@
# along with Ansible. If not, see .
#############################################
-
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', '_vault_password', '_vars_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)
- else:
+ elif 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/ and host_vars/ 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
diff --git a/lib/ansible/inventory/dir.py b/lib/ansible/inventory/dir.py
index e3eda226958..97a0fb9ea25 100644
--- a/lib/ansible/inventory/dir.py
+++ b/lib/ansible/inventory/dir.py
@@ -1,4 +1,5 @@
# (c) 2013, Daniel Hokka Zakrisson
+# (c) 2014, Serge van Ginderachter
#
# 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 """
diff --git a/lib/ansible/inventory/group.py b/lib/ansible/inventory/group.py
index c5270ad554c..262558e69c8 100644
--- a/lib/ansible/inventory/group.py
+++ b/lib/ansible/inventory/group.py
@@ -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)
diff --git a/lib/ansible/inventory/ini.py b/lib/ansible/inventory/ini.py
index 9863de17b8e..3848696006e 100644
--- a/lib/ansible/inventory/ini.py
+++ b/lib/ansible/inventory/ini.py
@@ -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:
diff --git a/lib/ansible/inventory/script.py b/lib/ansible/inventory/script.py
index 69e99e047de..8b2e4619a9c 100644
--- a/lib/ansible/inventory/script.py
+++ b/lib/ansible/inventory/script.py
@@ -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):
diff --git a/lib/ansible/inventory/vars_plugins/group_vars.py b/lib/ansible/inventory/vars_plugins/group_vars.py
deleted file mode 100644
index 96a24318bdd..00000000000
--- a/lib/ansible/inventory/vars_plugins/group_vars.py
+++ /dev/null
@@ -1,195 +0,0 @@
-# (c) 2012-2014, Michael DeHaan
-#
-# 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 .
-
-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/ and host_vars/ 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
-
diff --git a/lib/ansible/inventory/vars_plugins/noop.py b/lib/ansible/inventory/vars_plugins/noop.py
new file mode 100644
index 00000000000..5d4b4b6658c
--- /dev/null
+++ b/lib/ansible/inventory/vars_plugins/noop.py
@@ -0,0 +1,48 @@
+# (c) 2012-2014, Michael DeHaan
+# (c) 2014, Serge van Ginderachter
+#
+# 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 .
+
+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 {}
+
diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py
index 0232f5b86e8..4188d65d543 100644
--- a/lib/ansible/playbook/__init__.py
+++ b/lib/ansible/playbook/__init__.py
@@ -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:
diff --git a/lib/ansible/utils/__init__.py b/lib/ansible/utils/__init__.py
index 9d09254e3c2..fb391f9efd0 100644
--- a/lib/ansible/utils/__init__.py
+++ b/lib/ansible/utils/__init__.py
@@ -1,4 +1,4 @@
-# (c) 2012, Michael DeHaan
+# (c) 2012-2014, Michael DeHaan
#
# 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 .
+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
diff --git a/test/units/TestInventory.py b/test/units/TestInventory.py
index 65da05dba3c..dc3a0ce6d6e 100644
--- a/test/units/TestInventory.py
+++ b/test/units/TestInventory.py
@@ -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
+