Ansible 源码解析:动态清单(Inventory)中添加Host不能被all分组识别的问题分析

(本文基于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再去重)。代码就不贴了,与本文的关系不太大。

结论:

  1. 如果在InventoryManager.add_host时未指定分组,则可以通过调用InventoryManager.reconcile_inventory()使所有的host能够通过all分组取到(InventoryManager.reconcile_inventory()调用的是self._inventory.reconcile_inventory(),self._inventory即为一个InventoryData对象)
  2. 通过调用InventoryManager.reconcile_inventory()确实可以使所有的host能够通过all分组取到,但这从代码的简洁度和运行性能上来说显然不是个好选择,虽然不是多复杂的操作,但完全是个多余的步骤,不如在add_host的时候指定分组(all或ungrouped)。
  3. 我暂时也没想清楚ansible为什么不在add_host的时候默认指定host属于ungrouped分组,这样似乎可以省去一些步骤,使代码更精练,使用起来也方便一些。不知道是不是跟inventory source的解析有关,有空的时候再看看吧。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值