(本文基于Ansible 2.7)
在Ansible API: 动态清单(Inventory)的使用一文中,我们讨论了纯动态清单的使用,其中提到,简单的添加Host到Inventory中,这些Host并不能通过在playsource中指定hosts=‘all’取到
#假设我们有一个IP地址的列表(这个列表可以通过合适的其他服务获得,或者从数据库直接查询)
host_list = ['192.168.1.1','192.168.1.2']
#建立一个空的InventoryManager(sources默认就是None,此处就是明确一下)
inventory = InventoryManager(loader=loader, sources=None)
#下面开始向Inventory中添加Host
for host in host_list:
inventory.add_host(host)
#也可以分组,但此分组必须先显式通过add_group方法添加
#inventory.add_host(host,group='groupName')
这是为什么呢?
首先,按照ansible的设计,all是一个内置的分组(group),同样的内置分组还有‘ungrouped’。它们的定义在lib/ansible/inventory/data.py:
def __init__(self):
# the inventory object holds a list of groups
self.groups = {}
self.hosts = {}
# provides 'groups' magic var, host object has group_names
self._groups_dict_cache = {}
# current localhost, implicit or explicit
self.localhost = None
self.current_source = None
# Always create the 'all' and 'ungrouped' groups,
for group in ('all', 'ungrouped'):
self.add_group(group)
self.add_child('all', 'ungrouped')
可以看到,在InventoryData的__init__()中,创建了’all’, 'ungrouped’两个内置分组,并将ungrouped设为all的子分组。那么使用‘all’无法取到所有host的原因,肯定是因为这些host并没有添加这些host到‘all’这个分组中,因为Group.get_hosts中并没有什么猫腻(我没有贴代码,文末有简要说明)。那么,为什么指定ini或yaml文件格式的静态清单(命令行通过-i选项),却可以通过all取到所有host呢?
lib/ansible/inventory/data.py中还有一个方法reconcile_inventory,代码如下:
def reconcile_inventory(self):
''' Ensure inventory basic rules, run after updates '''
display.debug('Reconcile groups and hosts in inventory.')
self.current_source = None
group_names = set()
# set group vars from group_vars/ files and vars plugins
for g in self.groups:
group = self.groups[g]
group_names.add(group.name)
# ensure all groups inherit from 'all'
if group.name != 'all' and not group.get_ancestors():
self.add_child('all', group.name)
host_names = set()
# get host vars from host_vars/ files and vars plugins
for host in self.hosts.values():
host_names.add(host.name)
mygroups = host.get_groups()
if self.groups['ungrouped'] in mygroups:
# clear ungrouped of any incorrectly stored by parser
if set(mygroups).difference(set([self.groups['all'], self.groups['ungrouped']])):
self.groups['ungrouped'].remove_host(host)
elif not host.implicit:
# add ungrouped hosts to ungrouped, except implicit
length = len(mygroups)
if length == 0 or (length == 1 and self.groups['all'] in mygroups):
self.add_child('ungrouped', host.name)
# special case for implicit hosts
if host.implicit:
host.vars = combine_vars(self.groups['all'].get_vars(), host.vars)
# warn if overloading identifier as both group and host
for conflict in group_names.intersection(host_names):
display.warning("Found both group and host with same name: %s" % conflict)
self._groups_dict_cache = {}
这里我们需要关注的是第二个for循环的循环体中的elif分支,我们通过InventoryManager.add_host()方法添加的host,没有添加任何分组(包括ungrouped分组)(lib/ansible/inventory/data.py):
def add_host(self, host, group=None, port=None):
''' adds a host to inventory and possibly a group if not there already '''
if host:
g = None
if group:
if group in self.groups:
g = self.groups[group]
else:
raise AnsibleError("Could not find group %s in inventory" % group)
if host not in self.hosts:
h = Host(host, port)
self.hosts[host] = h
if self.current_source: # set to 'first source' in which host was encountered
self.set_variable(host, 'inventory_file', self.current_source)
self.set_variable(host, 'inventory_dir', basedir(self.current_source))
else:
self.set_variable(host, 'inventory_file', None)
self.set_variable(host, 'inventory_dir', None)
display.debug("Added host %s to inventory" % (host))
# set default localhost from inventory to avoid creating an implicit one. Last localhost defined 'wins'.
if host in C.LOCALHOST:
if self.localhost is None:
self.localhost = self.hosts[host]
display.vvvv("Set default localhost to %s" % h)
else:
display.warning("A duplicate localhost-like entry was found (%s). First found localhost was %s" % (h, self.localhost.name))
else:
h = self.hosts[host]
if g:
g.add_host(h)
self._groups_dict_cache = {}
display.debug("Added host %s to group %s" % (host, group))
else:
raise AnsibleError("Invalid empty host name provided: %s" % host)
默认的implicit是False(lib/ansible/inventory/host.py,Host类的__init__方法):
def __init__(self, name=None, port=None, gen_uuid=True):
self.vars = {}
self.groups = []
self._uuid = None
self.name = name
self.address = name
if port:
self.set_variable('ansible_port', int(port))
if gen_uuid:
self._uuid = get_unique_id()
self.implicit = False
也就是说,在reconcile_inventory方法中,没有指定host的所有host,必然会走到上述的elif分支——将host添加到内置的ungrouped分组中。而ungrouped分组,是all分组的子分组(见本文引用的第二段代码,InventoryData.__init__的最后一行),ungrouped中的host可以通过all取到。reconcile_inventory方法在InventoryManager.parse_sources中有调用,InventoryManager.parse_sources在InventoryManager.init()中有调用。也就是说,只要指定了inventory source,那么在InventoryManager构造时就会重新调整分组结构,不管使用何种Inventory source,在Inventory source中如何定义分组,都可以通过all分组取到所有的host。
如果有想深究的朋友,可以关注lib/ansible/inventory/group.py中的get_hosts、_get_hosts、get_descendants、_walk_relationship四个方法,解释了如何取子分组中的hosts(descendants是后代的意思,基本上就是通过指定有向图的一个节点(group)做BFS生成树遍历取所有的子分组、孙子分组、重孙子分组等等,然后取所有取得的分组中的host再去重)。代码就不贴了,与本文的关系不太大。
结论:
- 如果在InventoryManager.add_host时未指定分组,则可以通过调用InventoryManager.reconcile_inventory()使所有的host能够通过all分组取到(InventoryManager.reconcile_inventory()调用的是self._inventory.reconcile_inventory(),self._inventory即为一个InventoryData对象)
- 通过调用InventoryManager.reconcile_inventory()确实可以使所有的host能够通过all分组取到,但这从代码的简洁度和运行性能上来说显然不是个好选择,虽然不是多复杂的操作,但完全是个多余的步骤,不如在add_host的时候指定分组(all或ungrouped)。
- 我暂时也没想清楚ansible为什么不在add_host的时候默认指定host属于ungrouped分组,这样似乎可以省去一些步骤,使代码更精练,使用起来也方便一些。不知道是不是跟inventory source的解析有关,有空的时候再看看吧。