开发动态清单

Ansible 可以通过使用提供的 清单插件 从动态来源(包括云来源)提取清单信息。有关如何提取清单信息的详细信息,请参阅 使用动态清单。如果您所需的来源当前不受现有插件覆盖,您可以像创建任何其他插件类型一样创建自己的清单插件。

在以前的版本中,您必须创建一个脚本或程序,该脚本或程序在使用正确的参数调用时可以输出正确格式的 JSON。您仍然可以使用和编写清单脚本,因为我们通过 脚本清单插件 确保了向后兼容性,并且对使用的编程语言没有限制。但是,如果您选择编写脚本,则需要自己实现一些功能,例如缓存、配置管理、动态变量和组组成等等。如果您改用 清单插件,则可以使用 Ansible 代码库并自动添加这些常见功能。

清单源

清单源是清单插件使用的输入字符串。清单源可以是文件或脚本的路径,也可以是插件可以解释的原始数据。

下表显示了一些清单插件示例以及您可以使用命令行上的 -i 传递给它们的源类型。

插件

主机列表

主机列表(以逗号分隔)

yaml

YAML 格式数据文件的路径

构造的

YAML 配置文件的路径

ini

INI 格式数据文件的路径

virtualbox

YAML 配置文件的路径

脚本插件

输出 JSON 的可执行文件的路径

清单插件

与大多数插件类型(模块除外)一样,清单插件必须使用 Python 开发。它们在控制节点上执行,因此应遵守 控制节点要求

开发插件 中的大多数文档也适用于此处。您应该首先阅读该文档以了解一般情况,然后返回本文档以了解有关清单插件的详细信息。

通常,清单插件在运行开始时以及在加载 playbook、任务或角色之前执行。但是,您可以使用 meta: refresh_inventory 任务清除当前清单并再次执行清单插件,此任务将生成一个新的清单。

如果您使用持久缓存,清单插件还可以使用配置的缓存插件来存储和检索数据。缓存清单避免了重复且代价高昂的外部调用。

开发清单插件

您首先要做的就是使用基类

from ansible.plugins.inventory import BaseInventoryPlugin

class InventoryModule(BaseInventoryPlugin):

    NAME = 'myplugin'  # used internally by Ansible, it should match the file name but not required

如果清单插件位于集合中,则 NAME 应采用“namespace.collection_name.myplugin”格式。基类有一些每个插件都应该实现的方法,以及一些用于解析清单源和更新清单的帮助程序。

在您使基本插件正常工作后,可以通过添加更多基类来合并其他功能

from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable

class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):

    NAME = 'myplugin'

对于插件中的大部分工作,我们主要希望处理 2 个方法 verify_fileparse

verify_file 方法

Ansible 使用此方法快速确定清单源是否可被插件使用。此确定不需要 100% 准确,因为插件可以处理的内容可能存在重叠,并且默认情况下,Ansible 将根据其顺序尝试启用的插件。

def verify_file(self, path):
    ''' return true/false if this is possibly a valid file for this plugin to consume '''
    valid = False
    if super(InventoryModule, self).verify_file(path):
        # base class verifies that file exists and is readable by current user
        if path.endswith(('virtualbox.yaml', 'virtualbox.yml', 'vbox.yaml', 'vbox.yml')):
            valid = True
    return valid

在上面的示例中,来自 virtualbox 清单插件,我们筛选特定的文件名模式以避免尝试使用任何有效的 YAML 文件。您可以在此处添加任何类型的条件,但最常见的是“扩展名匹配”。如果您为 YAML 配置文件实现了扩展名匹配,则应接受路径后缀 <plugin_name>.<yml|yaml>。所有有效扩展名都应在插件描述中记录。

以下是从 主机列表 插件中不使用“文件”而是使用清单源字符串本身的另一个示例

def verify_file(self, path):
    ''' don't call base class as we don't expect a path, but a host list '''
    host_list = path
    valid = False
    b_path = to_bytes(host_list, errors='surrogate_or_strict')
    if not os.path.exists(b_path) and ',' in host_list:
        # the path does NOT exist and there is a comma to indicate this is a 'host list'
        valid = True
    return valid

此方法只是为了加快清单过程并避免对在导致解析错误之前易于过滤掉的源进行不必要的解析。

parse 方法

此方法执行插件中的大部分工作。它采用以下参数

  • inventory:包含现有数据以及将主机/组/变量添加到清单的方法的清单对象。

  • loader:Ansible 的 DataLoader。DataLoader 可以读取文件、自动加载 JSON/YAML 和解密保管库数据,以及缓存读取的文件。

  • path:包含清单源的字符串(这通常是路径,但不是必需的)

  • cache:指示插件是否应使用或避免缓存(缓存插件和/或加载程序)

基类为在其他方法中重用执行了一些最小的分配。

def parse(self, inventory, loader, path, cache=True):

     self.loader = loader
     self.inventory = inventory
     self.templar = Templar(loader=loader)

现在插件需要解析提供的清单源并将其转换为 Ansible 清单。为了促进这一点,下面的示例使用了一些辅助函数

NAME = 'myplugin'

def parse(self, inventory, loader, path, cache=True):

     # call base method to ensure properties are available for use with other helper methods
     super(InventoryModule, self).parse(inventory, loader, path, cache)

     # this method will parse 'common format' inventory sources and
     # update any options declared in DOCUMENTATION as needed
     config = self._read_config_data(path)

     # if NOT using _read_config_data you should call set_options directly,
     # to process any defined configuration for this plugin,
     # if you don't define any options you can skip
     #self.set_options()

     # example consuming options from inventory source
     mysession = apilib.session(user=self.get_option('api_user'),
                                password=self.get_option('api_pass'),
                                server=self.get_option('api_server')
     )


     # make requests to get data to feed into inventory
     mydata = mysession.getitall()

     #parse data and create inventory objects:
     for colo in mydata:
         for server in mydata[colo]['servers']:
             self.inventory.add_host(server['name'])
             self.inventory.set_variable(server['name'], 'ansible_host', server['external_ip'])

具体细节将根据返回的 API 和结构而有所不同。请记住,如果您遇到清单源错误或任何其他问题,则应 raise AnsibleParserError 以让 Ansible 知道源无效或过程失败。

有关如何实现清单插件的示例,请参阅此处的源代码:lib/ansible/plugins/inventory

清单对象

传递给 parseinventory 对象有一些用于填充清单的有用方法。

add_group 如果组尚不存在,则将组添加到清单中。它将组名作为唯一的定位参数。

add_child 将清单中存在的组或主机添加到清单中的父组中。它采用两个定位参数,父组的名称和子组或主机的名称。

add_host 用于将主机添加到清单中(如果该主机尚不存在),可以选择将其添加到特定的组中。它以主机名作为第一个参数,并接受两个可选的关键字参数:groupportgroup 是清单中组的名称,port 是一个整数。

set_variable 用于向清单中的组或主机添加变量。它接受三个位置参数:组或主机的名称、变量的名称以及变量的值。

要使用 Jinja2 表达式创建组和变量,请参阅下面关于实现 constructed 功能的部分。

要查看其他清单对象方法,请参阅此处的源代码:lib/ansible/inventory/data.py

清单缓存

要缓存清单,请使用清单缓存文档片段扩展清单插件文档,并使用 Cacheable 基类。

extends_documentation_fragment:
  - inventory_cache
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):

    NAME = 'myplugin'

接下来,加载用户指定的缓存插件以读取和更新缓存。如果您的清单插件使用基于 YAML 的配置文件以及 _read_config_data 方法,则会在该方法中加载缓存插件。如果您的清单插件不使用 _read_config_data,则必须使用 load_cache_plugin 显式加载缓存。

NAME = 'myplugin'

def parse(self, inventory, loader, path, cache=True):
    super(InventoryModule, self).parse(inventory, loader, path)

    self.load_cache_plugin()

在使用缓存插件之前,必须使用 get_cache_key 方法检索唯一的缓存键。所有使用缓存的清单模块都需要执行此任务,以便您不会使用/覆盖缓存的其他部分。

def parse(self, inventory, loader, path, cache=True):
    super(InventoryModule, self).parse(inventory, loader, path)

    self.load_cache_plugin()
    cache_key = self.get_cache_key(path)

现在您已经启用了缓存、加载了正确的插件并检索了唯一的缓存键,您可以使用 parse 方法的 cache 参数设置缓存和清单之间的数据流。此值来自清单管理器,并指示清单是否正在刷新(例如,通过 --flush-cache 或元任务 refresh_inventory)。虽然在刷新时不应使用缓存来填充清单,但如果用户启用了缓存,则应使用新清单更新缓存。您可以像使用字典一样使用 self._cache。以下模式允许刷新清单与缓存一起工作。

def parse(self, inventory, loader, path, cache=True):
    super(InventoryModule, self).parse(inventory, loader, path)

    self.load_cache_plugin()
    cache_key = self.get_cache_key(path)

    # cache may be True or False at this point to indicate if the inventory is being refreshed
    # get the user's cache option too to see if we should save the cache if it is changing
    user_cache_setting = self.get_option('cache')

    # read if the user has caching enabled and the cache isn't being refreshed
    attempt_to_read_cache = user_cache_setting and cache
    # update if the user has caching enabled and the cache is being refreshed; update this value to True if the cache has expired below
    cache_needs_update = user_cache_setting and not cache

    # attempt to read the cache if inventory isn't being refreshed and the user has caching enabled
    if attempt_to_read_cache:
        try:
            results = self._cache[cache_key]
        except KeyError:
            # This occurs if the cache_key is not in the cache or if the cache_key expired, so the cache needs to be updated
            cache_needs_update = True
    if not attempt_to_read_cache or cache_needs_update:
        # parse the provided inventory source
        results = self.get_inventory()
    if cache_needs_update:
        self._cache[cache_key] = results

    # submit the parsed data to the inventory object (add_host, set_variable, etc)
    self.populate(results)

parse 方法完成后,将使用 self._cache 的内容设置缓存插件(如果缓存的内容已更改)。

您还有其他三个缓存方法可用
  • set_cache_plugin 强制使用 self._cache 的内容设置缓存插件,在 parse 方法完成之前。

  • update_cache_if_changed 仅当 self._cache 已修改时才设置缓存插件,在 parse 方法完成之前。

  • clear_cache 刷新缓存,最终通过调用缓存插件的 flush() 方法来实现,该方法的实现取决于正在使用的特定缓存插件。请注意,如果用户对事实和清单使用相同的缓存后端,则两者都将被刷新。为避免这种情况,用户可以在其清单插件配置中指定不同的缓存后端。

构造的功能

清单插件可以通过使用 constructed 清单插件的功能,从 Jinja2 表达式和变量创建主机变量和组。为此,请使用 Constructable 基类,并使用 constructed 文档片段扩展清单插件的文档。

extends_documentation_fragment:
  - constructed
class InventoryModule(BaseInventoryPlugin, Constructable):

    NAME = 'ns.coll.myplugin'

constructed 文档片段中有三个主要选项

compose 使用 Jinja2 表达式创建变量。这是通过调用 _set_composite_vars 方法实现的。keyed_groups 基于变量值创建主机组。这是通过调用 _add_host_to_keyed_groups 方法实现的。groups 基于 Jinja2 条件创建组。这是通过调用 _add_host_to_composed_groups 方法实现的。

每个方法都应为添加到清单中的每个主机调用。需要三个位置参数:构造选项、变量字典和主机名。首先调用 _set_composite_vars 方法将允许 keyed_groupsgroups 使用组合变量。

默认情况下,未定义的变量将被忽略。默认情况下,这在 compose 中是被允许的,因此您可以使变量定义依赖于稍后在剧本中从其他来源填充的变量。对于组,它允许使用并非始终存在的变量,而无需使用 default 过滤器。为了支持将未定义的变量配置为错误,请将构造选项 strict 作为关键字参数传递给每个方法。

keyed_groupsgroups 使用已与主机关联的任何变量(例如,来自较早的清单源)。_add_host_to_keyed_groupsadd_host_to_composed_groups 可以通过传递关键字参数 fetch_hostvars 来关闭此功能。

这是一个使用所有三种方法的示例

def add_host(self, hostname, host_vars):
    self.inventory.add_host(hostname, group='all')

    for var_name, var_value in host_vars.items():
        self.inventory.set_variable(hostname, var_name, var_value)

    strict = self.get_option('strict')

    # Add variables created by the user's Jinja2 expressions to the host
    self._set_composite_vars(self.get_option('compose'), host_vars, hostname, strict=True)

    # Create user-defined groups using variables and Jinja2 conditionals
    self._add_host_to_composed_groups(self.get_option('groups'), host_vars, hostname, strict=strict)
    self._add_host_to_keyed_groups(self.get_option('keyed_groups'), host_vars, hostname, strict=strict)

默认情况下,使用 _add_host_to_composed_groups()_add_host_to_keyed_groups() 创建的组名是有效的 Python 标识符。无效字符将替换为下划线 _。插件可以通过将 self._sanitize_group_name 设置为新函数来更改用于构造功能的清理方式。核心引擎也会进行清理,因此,如果自定义函数不太严格,则应将其与配置设置 TRANSFORM_INVALID_GROUP_CHARS 结合使用。

from ansible.inventory.group import to_safe_group_name

class InventoryModule(BaseInventoryPlugin, Constructable):

    NAME = 'ns.coll.myplugin'

    @staticmethod
    def custom_sanitizer(name):
        return to_safe_group_name(name, replacer='')

    def parse(self, inventory, loader, path, cache=True):
        super(InventoryModule, self).parse(inventory, loader, path)

        self._sanitize_group_name = custom_sanitizer

清单源的通用格式

为了简化开发,大多数插件使用标准的基于 YAML 的配置文件作为清单源。该文件只有一个必需字段 plugin,它应该包含预期使用该文件的插件的名称。根据使用的其他常见功能,您可能需要其他字段,并且可以根据需要在每个插件中添加自定义选项。例如,如果您使用集成的缓存、cache_plugincache_timeout 和其他与缓存相关的字段,则可能会存在这些字段。

‘auto’ 插件

从 Ansible 2.5 开始,我们包含了 auto 清单插件 并默认启用它。如果标准配置文件中的 plugin 字段与清单插件的名称匹配,则 auto 清单插件将加载您的插件。‘auto’ 插件使您可以更轻松地使用您的插件,而无需更新配置。

清单脚本

即使我们现在有了清单插件,我们仍然支持清单脚本,不仅是为了向后兼容,还为了允许用户使用其他编程语言。

清单脚本约定

清单脚本必须接受 --list--host <hostname> 参数。虽然允许其他参数,但 Ansible 不会使用它们。这些参数对于直接执行脚本仍然可能有用。

当脚本使用单个参数 --list 调用时,脚本必须向标准输出输出一个 JSON 对象,其中包含所有要管理的组。每个组的值应该是一个对象,其中包含每个主机的列表、任何子组和潜在的组变量,或者只是一个主机列表。

{
    "group001": {
        "hosts": ["host001", "host002"],
        "vars": {
            "var1": true
        },
        "children": ["group002"]
    },
    "group002": {
        "hosts": ["host003","host004"],
        "vars": {
            "var2": 500
        },
        "children":[]
    }

}

如果组的任何元素为空,则可以从输出中省略它们。

当使用参数 --host <hostname> 调用时(其中 <hostname> 是上述主机),脚本必须打印一个 JSON 对象,该对象可以为空或包含变量,以便将其提供给模板和剧本。例如

{
    "VAR001": "VALUE",
    "VAR002": "VALUE"
}

打印变量是可选的。如果脚本不打印变量,则应打印一个空 JSON 对象。

调整外部清单脚本

版本 1.3 中的新增功能。

上面提到的库存脚本系统适用于所有版本的 Ansible,但对每个主机调用 --host 可能效率低下,尤其是在涉及对远程子系统的 API 调用时。

为了避免这种低效率,如果清单脚本返回名为“_meta”的顶级元素,则可以在一次脚本执行中返回所有主机变量。当此元元素包含“hostvars”的值时,将不会使用 --host 为每个主机调用清单脚本。此行为会导致大量主机性能显着提高。

要添加到顶级 JSON 对象中的数据如下所示

{

    # results of inventory script as above go here
    # ...

    "_meta": {
        "hostvars": {
            "host001": {
                "var001" : "value"
            },
            "host002": {
                "var002": "value"
            }
        }
    }
}

为了满足使用_meta的要求,并防止Ansible使用--host调用您的清单,您必须至少使用一个空的hostvars对象填充_meta。例如

{

    # results of inventory script as above go here
    # ...

    "_meta": {
        "hostvars": {}
    }
}

如果您打算用清单脚本替换现有的静态清单文件,则该脚本必须返回一个JSON对象,其中包含一个'all'组,该组包含清单中的每个主机作为成员,以及清单中的每个组作为子组。它还应该包含一个'ungrouped'组,其中包含所有不属于任何其他组的主机。此JSON对象的骨架示例如下所示

{
    "_meta": {
      "hostvars": {}
    },
    "all": {
      "children": [
        "ungrouped"
      ]
    },
    "ungrouped": {
      "children": [
      ]
    }
}

一个简单的方法来查看它应该是什么样子,是使用ansible-inventory,它也支持像清单脚本一样的--list--host参数。

另请参阅

Python API

剧本和Ad Hoc任务执行的Python API

开发模块

开始开发模块

开发插件

如何开发插件

AWX

Ansible的REST API端点和GUI,与动态清单同步

沟通

有问题?需要帮助?想分享您的想法?请访问Ansible沟通指南