ansible2.4 源码分析-自定义inventory解析插件实现

Posted 进击的大杂烩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ansible2.4 源码分析-自定义inventory解析插件实现相关的知识,希望对你有一定的参考价值。

通过,初步的分析了ansible2.4的一个运行流程,没有对 ansible 解析 source 的过程进行详细分析。本文通过分析 ansible 解析 source 并生成 inventory 对象的过程,从而进一步理解 inventory。

入口文件分析

解析 source 的入口函数为:parse_sources()。源文件位置:ansible/inventory/manager.py

    def parse_sources(self, cache=False):
       ''' iterate over inventory sources and parse each one to populate it'''
       #_setup_inventory_plugins() 函数的作用是根据配置从插件目录(C.DEFAULT_INVENTORY_PLUGIN_PATH)加载插件,并将插件放到 _inventory_plugins 中。
       #然后通过遍历 _sources 来处理 source,真正的处理函数是:parse_source()。
       self._setup_inventory_plugins()

       parsed = False
       # allow for multiple inventory parsing
       for source in self._sources:
           if source:
               if ',' not in source:
                   source = unfrackpath(source, follow=False)
               parse = self.parse_source(source, cache=cache)
               if parse and not parsed:
                   parsed = True

       if parsed:
           # reconcile_inventory() 函数实际上是 InventoryData 类中的函数,作用是再次确认所有组属于'all'组,
           # 并将'ungrouped'组中的host放到'all'组中。将所有的host也加入到'all'组。
           # 还将组变量作为 host 变量分别添加到每个 host 上。
           self._inventory.reconcile_inventory()
       else:
           display.warning("No inventory was parsed, only implicit localhost is available")

       self._inventory_plugins = []

   #parse_source()简化后如下:
   def parse_source(self, source, cache=False):
       parsed = False
       # 如果是目录的话就遍历目录然后递归调用parse_source()函数,来解析 source。
       if os.path.isdir(b_source):
           for i in sorted(os.listdir(b_source)):
               fullpath = os.path.join(b_source, i)
               parsed_this_one = self.parse_source(to_native(fullpath))
               if not parsed:
                   parsed = parsed_this_one
       # 如果不是目录就初始化self._inventory.current_source = source,其中 _inventory 是 InventoryData 的实例
       else:
           self._inventory.current_source = source
           failures = []
           # 然后通过遍历所有插件来尝试解析 source ,解析是通过plugin.parse() 函数来处理的。而 plugin 就是['host_list', 'script', 'yaml', 'ini']中的一种。
           # 我们通过分析其中最简单的一个插件: host_list 来分析解析过程。
           for plugin in self._inventory_plugins:
               # initialize
               if plugin.verify_file(source):
                   plugin.parse(self._inventory, self._loader, source, cache=cache)
                   parsed = True
               else:
                   display.debug('%s did not meet %s requirements' % (to_text(source), plugin_name))

host_list插件分析

host_list 源文件位置: ansible/plugins/inventory/host_list.py。如下:

class InventoryModule(BaseInventoryPlugin):

   NAME = 'host_list'

   def verify_file(self, host_list):
       valid = False
       b_path = to_bytes(host_list, errors='surrogate_or_strict')
       if not os.path.exists(b_path) and ',' in host_list:
           valid = True
       return valid

   def parse(self, inventory, loader, host_list, cache=True):
       super(InventoryModule, self).parse(inventory, loader, host_list)
       for h in host_list.split(','):
           h = h.strip()
           if h:
               try:
                   (host, port) = parse_address(h, allow_ranges=False)
               except AnsibleError as e:
                   host = h
                   port = None
               if host not in self.inventory.hosts:
                   self.inventory.add_host(host, group='ungrouped', port=port)

解析类 InventoryModule 继承自 BaseInventoryPlugin,BaseInventoryPlugin 的源文件位置:ansible/plugins/inventory/__init__.py

  • super(InventoryModule, self).parse(inventory, loader, host_list) 的作用是初始化了 host_list 插件中的几个属性:

self.loader = loader
self.inventory = inventory #此处的 inventory 就是 InventoryData 的实例
self.templar = Templar(loader=loader)
  • 然后通过逗号分隔遍历 host_list ,然后通过函数 parse_address 将 host 和 port 解析出来并通过self.inventory.add_host(host, group='ungrouped', port=port)将解析出来的 host 初始化到 inventory 实例中。每个 host 被放到 'ungrouped' 组中。整个解析过程完毕,下面主要分析 InventoryData 类。

InventoryData 类分析

InventoryData 类主要作用是用来保存 inventory 的数据(主机,组)。源文件位置:ansible/inventory/data.py
首先来看 InventoryData 的源码(部分):

class InventoryData(object):
   def __init__(self):

       # 初始化了 groups, hosts, 等变量,并初始化了2个特殊组 'all' 和 'ungrouped',并将 'ungrouped'组加入到了'all'组的子组

       # the inventory object holds a list of groups
       # groups 和 hosts 初始化为字典
       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

       # 创建并实例化 'all' 和 'ungrouped' 这两个组
       for group in ('all', 'ungrouped'):
           self.add_group(group)
       self.add_child('all', 'ungrouped')

       # prime cache
       self.cache = FactCache()

   def add_host(self, host, group=None, port=None):
       ''' adds a host to inventory and possibly a group if not there already '''

       g = None
       # 获取组,通过上面的分析 host_list 插件默认将所有 host 加入到 'ungrouped' 组中
       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:
           # 新的 host 初始化为 Host 的实例(Host 类比较简单这里不再叙述)
           h = Host(host, port)
           # 以 host 和 Host(host, port) 作为 self.hosts 的键值对
           self.hosts[host] = h
           # self.current_source 在实例化时为 None ,但是在函数 parse_source() 中将 current_source 初始化为 source
           # 以 host_list 为例,self.current_source 为:'1.1.1.1:22,2.2.2.2:22'
           if self.current_source:  # set to 'first source' in which host was encountered
               # set_variable 函数的作用是将值绑定到对应实例(grou或host)的相应属性上(实际上是将host或group的变量和对应的值),后面会详细分析。
               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'.
           # 定义localhost,其中 C.LOCALHOST 的默认配置为:['127.0.0.1', 'localhost', '::1']
           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:
           # 将 Host(host, port) 的实例加入到组 g 中
           g.add_host(h)
           self._groups_dict_cache = {}
           display.debug("Added host %s to group %s" % (host, group))    


   def set_variable(self, entity, varname, value):
       ''' sets a varible for an inventory object '''
       # 函数根据输入的 entity 来判断是 group 还是 host 并将相应的 varname 和 value 绑定到相应的 inventory 实例上。
       # 这个函数对于我们自定义解析插件来说基本上都是要用到的。
       # 举个例子:比如ssh 的port 就可以通过 set_variable('x.x.x.x', 'ansible_port', 22) 来设置 ssh port 的变量。

       if entity in self.groups:
           inv_object = self.groups[entity]
       elif entity in self.hosts:
           inv_object = self.hosts[entity]
       else:
           raise AnsibleError("Could not identify group or host named %s" % entity)
         # 通过调用 host 实例或 group 实例的 set_variable 函数来初始化相关ssh参数。
         inv_object.set_variable(varname, value)
       display.debug('set %s for %s' % (varname, entity))

通过对以上源码的分析,可以知道解析 source 的整个过程就是实例化存储 groups 对象和 hosts 对象的 InventoryData 。
当 ansible 提供的默认解析插件都不能满足我们的条件的时候,就需要我们自定义插件。
比如通过cmdb接口获取到的 inventory 的格式如下:
{"group1":{"hosts":[{"ip":"192.168.100.101","port":22,"ansible_ssh_pass":"soft123"},{"ip":"192.168.100.102","port":22,"ansible_ssh_pass":"soft123"}]}}
注意:在2.0中ssh的参数有所改变,具体参考:http://docs.ansible.com/ansible/latest/intro_inventory.html

自定义解析插件 host_dict.py

  1. 确认插件的目录(C.DEFAULTINVENTORYPLUGIN_PATH)
    默认为:~/.ansible/plugins/inventory:/usr/share/ansible/plugins/inventory
    将插件脚本放到目录:~/.ansible/plugins/inventory

  2. 确认插件的名字(C.INVENTORY_ENABLED)
    默认为:['host_list', 'script', 'yaml', 'ini']
    要在 ansible 配置文件中定义,如下:
    [inventory]
    在 base.yml 中插件的格式为 list,此处如果有多个解析插件可以写成 pl,pl2 即可
    enable_plugins = host_dict,host_list,script,yaml,ini

  3. 编写插件 host_dict.py

#coding: utf-8

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type


import os
import json

from ansible.errors import AnsibleParserError
from ansible.module_utils._text import to_bytes, to_native
from ansible.plugins.inventory import BaseInventoryPlugin


class InventoryModule(BaseInventoryPlugin):

   NAME = 'host_dict'

   def verify_file(self, host_dict):
       """判断host_dict格式是否正确"""
       valid = False
       try:
           host_dict = json.loads(to_bytes(host_dict, errors='surrogate_or_strict'))
           if isinstance(host_dict, dict):
               valid = True
       except Exception as e:
           pass
       return valid

   def parse(self, inventory, loader, host_dict, cache=True):
       ''' parses the inventory file '''

       super(InventoryModule, self).parse(inventory, loader, host_dict)
       try:
           host_dict = json.loads(host_dict)
           for group in host_dict:
               #初始化group
               self.inventory.add_group(group)
               for host in host_dict[group]['hosts']:
                   ansible_host = host.pop('ip')
                   ansible_port = host.pop('port')
                   self.inventory.add_host(ansible_host, group=group, port=ansible_port)
                   #给host添加其他参数,如: ansible_ssh_pass
                   for k, v in host.iteritems():
                       self.inventory.set_variable(ansible_host, k, v)
       except Exception as e:
           raise AnsibleParserError("Invalid data from string, could not parse: %s" % to_native(e))

测试

  • 使用中 ansible api 脚本来测试

if __name__ == '__main__':
   # 先测试没有密码的情况
   hosts = '192.168.100.103:22,192.168.100.102:22'
   tasks = (('shell', 'hostname'),)
   hadoc = AdHocRunnerAPI(hosts, seconds=0, poll_interval=1)
   result_code = hadoc.run(tasks)
   print json.dumps(hadoc.result_info, indent=4)

可以看到如下的错误输出,说明需要密码:

"msg": "Failed to connect to the host via ssh: Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password).\r\n", 
  • 将hosts改为如下形式,测试自定义解析插件脚本
    hosts = '{"group1":{"hosts":[{"ip":"192.168.100.103","port":22,"ansiblesshpass":"soft123"},{"ip":"192.168.100.102","port":22,"ansiblesshpass":"soft123"}]}}'

if __name__ == '__main__':
   # 先测试没有密码的情况
   hosts = '{"group1":{"hosts":[{"ip":"192.168.100.103","port":22,"ansible_ssh_pass":"soft123"},{"ip":"192.168.100.102","port":22,"ansible_ssh_pass":"soft123"}]}}'
   tasks = (('shell', 'hostname'),)
   hadoc = AdHocRunnerAPI(hosts, seconds=0, poll_interval=1)
   result_code = hadoc.run(tasks)
   print json.dumps(hadoc.result_info, indent=4)

再次运行脚本,可以正常执行并返回(结果太长这里就不贴了)。

参考:
官方文档:http://docs.ansible.com/ansible/latest/intro.html





以上是关于ansible2.4 源码分析-自定义inventory解析插件实现的主要内容,如果未能解决你的问题,请参考以下文章

HanLP用户自定义词典源码分析

自定义View系列教程05--示例分析

Spring 源码分析--自定义标签的使用

Spring 源码分析--自定义标签的解析

Kafka 自定义指定消息partition策略规则及DefaultPartitioner源码分析

自定义View系列教程04--Draw源码分析及其实践