ini.py

作者:煮酒品茶

说明

ini.py是对配置文件进行解析的模块,主要处理了组,主机,组的变量,子组等关系。一个inventory中饮含 groups hosts 这两个非常重要的属性,其中有两个死的组all ungrouped组。

看看源码再理解他的配置就会很好理解,有点意思的地方是作者竟然用索引号去取值,而不是传统的for,有点搞。

继续分析

这里的逻辑是

  1. 打开配置文件,但配置文件是写死的
  2. 做基础的解析,得到all ungroupd组,并处理基本的主机主机变量组
  3. 处理子组,因为就是个对应关系
  4. 把深度为0的组加入到all组中
  5. 解析组的变量

测试,看一下下面的原型再看这个

cat /etc/ansible/hosts
[web]
10.1.1.2
10.1.1.3 ansible_ssh_user=zwhset ansible_ssh_port=22

[web:vars]
group=web
name=zwhset
age=18

[web:children]
nginx
tomcat
apache

[nginx]
10.1.1.4

[tomcat]
10.1.1.5

[apache]
10.1.1.6

测试,这里需要结合groupg与host来看

In [27]: from ansible.inventory.ini import InventoryParser

In [28]: inventory = InventoryParser()

In [29]: inventory.filename
Out[29]: '/etc/ansible/hosts'

In [30]: inventory.groups # 查看所有的组
Out[30]: 
{'all': ,
 'apache': ,
 'nginx': ,
 'tomcat': ,
 'ungrouped': ,
 'web': }

In [31]: inventory.hosts # 查看所有的主机
Out[31]: 
{'10.1.1.2': ,
 '10.1.1.3': ,
 '10.1.1.4': ,
 '10.1.1.5': ,
 '10.1.1.6': }

In [32]: # 查看一下web的子组

In [33]: web = inventory.groups["web"]

# 查看web的子组
In [36]: for g in web.child_groups:
    ...:     print g.name
    ...:     
nginx
tomcat
apache

# 查看web组子组的父组

In [38]: for g in web.child_groups:
    ...:     for kg in g.parent_groups: # 查看子组的父组
    ...:         print kg.name
    ...:         
    ...:     
web
web
web

# 查看web子组的主机
In [39]: for g in web.child_groups:
    ...:     print g.hosts
    ...:     
[]
[]
[]

# 查看web子组的主机变量,前面没设
In [41]: for g in web.child_groups:
    ...:     for h in  g.hosts:
    ...:         print h.vars
    ...:         
{}
{}
{}

# 唯一的一个组变量,在这里被成功解析
In [42]: for h in web.hosts:
    ...:     print h.vars
    ...:     
{}
{'ansible_ssh_port': 22, 'ansible_ssh_user': 'zwhset'}

模块原型

import ansible.constants as C
from ansible.inventory.host import Host
from ansible.inventory.group import Group
from ansible.inventory.expand_hosts import detect_range
from ansible.inventory.expand_hosts import expand_hostname_range
from ansible import errors
from ansible import utils
import shlex
import re
import ast

class InventoryParser(object):
    """
    Host inventory for ansible.
    """
    # 解析配置文件
    def __init__(self, filename=C.DEFAULT_HOST_LIST):

        # 获取一个文件对象
        with open(filename) as fh:
            self.filename = filename
            # 所有行的记录,一个列表,将要对这个列表进行解析,也就是配置文件的每一行
            self.lines = fh.readlines() 
            self.groups = {}
            self.hosts = {}
            # 实例化的时候会角化_parse方法
            self._parse()

    # 执行一堆函数,然后返回groups        
    def _parse(self):   
        # 对配置文件进行一个解析,最后得到实例化的所有东西 一个all一个ungroupd
        # 这是处理基础的主机以及变量,并没有对组关系进行处理
        self._parse_base_groups()

        # 1234 再来一次,处理子组
        self._parse_group_children()
        # 把深度为0并且不是all组的添加进all组
        self._add_allgroup_children()
        # 解析组的变量
        self._parse_group_variables()
        return self.groups

    @staticmethod
    def _parse_value(v):
        # 变量的value不包含#
        if "#" not in v:
            try:
                # 安全值的检查,
                ret = ast.literal_eval(v)
                # 符点转换
                if not isinstance(ret, float):
                    # Do not trim floats. Eg: "1.20" to 1.2
                    return ret
            # Using explicit exceptions.
            # Likely a string that literal_eval does not like. We wil then just set it.
            except ValueError:
                # For some reason this was thought to be malformed.
                pass
            except SyntaxError:
                # Is this a hash with an equals at the end?
                pass
        return v

    # [webservers]
    # alpha
    # beta:2345
    # gamma sudo=True user=root
    # delta asdf=jkl favcolor=red

    def _add_allgroup_children(self):
        # 获取groups的所有的值
        for group in self.groups.values():
            # 如果深度为0 并且组名不等于all的,添加到all组
            # 那么深度不为0的呢,不知道深度的可以看一下group的方法
            if group.depth == 0 and group.name != 'all':
                self.groups['all'].add_child_group(group)

    def _parse_base_groups(self):
        # FIXME: refactor

        # 定义ungrouped all组名, 并在all里添加一个ungrouped组
        ungrouped = Group(name='ungrouped')
        all = Group(name='all')
        all.add_child_group(ungrouped)

        self.groups = dict(all=all, ungrouped=ungrouped)
        active_group_name = 'ungrouped' # 活动的组,没啥好说的

        # 这里没用啥黑科技,使用range + len获取的其实就是文件的索引号
        for lineno in range(len(self.lines)):
            # 取 #号之前的字符串,然后消除两边的空白
            line = utils.before_comment(self.lines[lineno]).strip()
            # 如果字符串开始是[*]这种形式表明就是一种组
            if line.startswith("[") and line.endswith("]"):
                # 把[]去除掉,拿中间的*
                active_group_name = line.replace("[","").replace("]","")
                # 如果是变量或或是子组的方式
                if ":vars" in line or ":children" in line:
                    # 组名取:vars 左侧分割的,即 [webs:vars] [web:children]取web
                    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)
                    active_group_name = None
                elif active_group_name not in self.groups:
                    new_group = self.groups[active_group_name] = Group(name=active_group_name)

            # 如果是空行或者;开头的就当成注释,注意这里有前面是#号分割的,拿#号之前的
            # 所以就不需要就判断#号了
            elif line.startswith(";") or line == '':
                pass
            # 这肯定是真,因为前面定义了,而且走的是elif, 这里针对的是不以[]就不是组的
            elif active_group_name:
                # 一个处理类shell的解析方式
                tokens = shlex.split(line)
                # 空则跳到下一循环
                if len(tokens) == 0:
                    continue
                # 拿到主机名,并默认定义一个端口
                hostname = tokens[0]
                port = C.DEFAULT_REMOTE_PORT
                # Three cases to check:
                # 0. A hostname that contains a range pesudo-code and a port
                # 1. A hostname that contains just a port
                # 如果主机名中包含:大于1,即为IPV6的地址, XXX:XXX::XXX.port
                if hostname.count(":") > 1:
                    # Possible an IPv6 address, or maybe a host line with multiple ranges
                    # IPv6 with Port  XXX:XXX::XXX.port
                    # FQDN            foo.example.com
                    if hostname.count(".") == 1:
                        (hostname, port) = hostname.rsplit(".", 1)
                # 取主机名和端口
                elif ("[" in hostname and
                    "]" in hostname and
                    ":" in hostname and
                    (hostname.rindex("]") < hostname.rindex(":")) or
                    ("]" not in hostname and ":" in hostname)):
                        (hostname, port) = hostname.rsplit(":", 1)

                # 定义一个字的主机组
                hostnames = []
                # 这里是处理这种[a-z] [1-3]这种主机名,会返回匹配关系的主机名的
                if detect_range(hostname):
                    hostnames = expand_hostname_range(hostname)
                else:
                    hostnames = [hostname]

                # 遍历一下
                for hn in hostnames:
                    host = None
                    # 判断是否已经存在, 存在更新变量host,这里是以已经存在的为准
                    if hn in self.hosts:
                        host = self.hosts[hn]
                    else:
                        # 一个实例化Host,更新一下实例的hosts列表
                        host = Host(name=hn, port=port)
                        self.hosts[hn] = host

                    # 环境变量,即配置文件里面定义什么密码帐号之类的玩意儿
                    if len(tokens) > 1:
                        for t in tokens[1:]:
                            # 带#号就跳出
                            if t.startswith('#'):
                                break
                            try:
                                # kv型式,在后面设置主机的变量
                                (k,v) = t.split("=", 1)
                            except ValueError, e:
                                raise errors.AnsibleError("%s:%s: Invalid ini entry: %s - %s" % (self.filename, lineno + 1, t, str(e)))
                            host.set_variable(k, self._parse_value(v))
                    # 添加主机在 ungrouped里面,由于指针的关系,所以all里面就会有 
                    self.groups[active_group_name].add_host(host)

    # [southeast:children]
    # atlanta
    # raleigh

    def _parse_group_children(self):
        group = None
        #再来一次,注意用的是索引号,有更好的方式
        for lineno in range(len(self.lines)):
            # 利用索引取到值,这写的就尴尬,并去除两边的空格
            line = self.lines[lineno].strip()
            # 空行跳到下一次
            if line is None or line == '':
                continue
            # 注意这里,是专门处理[:children]子组的往下看
            if line.startswith("[") and ":children]" in line:
                # 同样清空 [ :children] 留下的就是组名
                line = line.replace("[","").replace(":children]","")
                #判断一下组名是否在组里,没有就加呗
                group = self.groups.get(line, None)
                if group is None:
                    group = self.groups[line] = Group(name=line)
            # 同理
            elif line.startswith("#") or line.startswith(";"):
                pass
            elif line.startswith("["):
                group = None
            # 如果匹配第一个if是子组,第二次循环group就为真了
            elif group:
                # 就是添加子组
                kid_group = self.groups.get(line, None)
                if kid_group is None:
                    raise errors.AnsibleError("%s:%d: child group is not defined: (%s)" % (self.filename, lineno + 1, line))
                else:
                    group.add_child_group(kid_group)

    # [webservers:vars]
    # http_port=1234
    # maxRequestsPerChild=200

    def _parse_group_variables(self):
        group = None
        for lineno in range(len(self.lines)):
            line = self.lines[lineno].strip()
            # [web:vars]这种形式,因为前面添加过组,所组应该是存在的,如果不存在就报错呗
            if line.startswith("[") and ":vars]" in line:
                line = line.replace("[","").replace(":vars]","")
                group = self.groups.get(line, None)
                if group is None:
                    raise errors.AnsibleError("%s:%d: can't add vars to undefined group: %s" % (self.filename, lineno + 1, line))
            # 跳过
            elif line.startswith("#") or line.startswith(";"):
                pass
            # [开头的前面处理过了这里不处理,只处理组的变量
            elif line.startswith("["):
                group = None
            elif line == '':
                pass
            # 如果匹配到了即代表这是组的变量设置
            elif group:
                # 必须是用=号来进行赋值的
                if "=" not in line:
                    raise errors.AnsibleError("%s:%d: variables assigned to group must be in key=value form" % (self.filename, lineno + 1))
                else:
                    # 走K value 
                    (k, v) = [e.strip() for e in line.split("=", 1)]
                    # 这里用到组的方法设置一个字典其实就是一个字典
                    # 在这里会检查一下值是不是一些不安全的东西
                    group.set_variable(k, self._parse_value(v))

    def get_host_variables(self, host):
        return {}