开发动态资产

Ansible 可以使用所提供的 资产插件 从动态来源(包括云来源)中提取资产信息。关于如何提取资产信息的详细信息,请参阅 使用动态资产。如果现有的插件不支持您想要的来源,您可以像开发其他类型的插件一样,创建自己的资产插件。

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

资产来源

资产来源是资产插件处理的输入字符串。资产来源可以是文件或脚本的路径,也可以是插件可以解析的原始数据。

下表显示了一些资产插件及其示例,您可以在命令行中使用 -i 将这些源类型传递给它们。

Plugin

主机列表

以逗号分隔的主机列表

yaml

YAML 格式数据文件的路径

constructed

YAML 配置文件路径

ini

INI 格式数据文件的路径

virtualbox

YAML 配置文件路径

script 插件

输出 JSON 的可执行文件路径

资产插件

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

开发插件 中的大部分文档也适用于此处。您应该先阅读该文档以获得总体了解,然后再回到本文档了解资产插件的细节。

通常,资产插件在运行开始时,且在加载 Playbook、Play 或角色之前执行。但是,您可以使用 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'

对于插件中的大部分工作,我们主要需要处理 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

inventory 对象

传递给 parseinventory 对象具有用于填充资产的有用方法。

add_group:如果组不存在,则将组添加到资产中。它仅接受组名作为位置参数。

add_child:将资产中已存在的组或主机添加到资产中的父组。它接受两个位置参数,即父组名称和子组或主机名称。

add_host:如果主机不存在,则将其添加到资产中,可选地添加到特定组。它接受主机名作为第一个参数,并接受两个可选的关键字参数 groupportgroup 是资产中组的名称,port 是整数。

set_variable:向资产中的组或主机添加变量。它接受三个位置参数:组或主机名、变量名和变量值。

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

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

inventory 缓存

要缓存资产,请使用 inventory_cache 文档片段扩展资产插件文档,并使用 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)

注意

get_cache_key 是一个辅助方法,用于检索插件名称和 path 参数的唯一键。生成多个唯一缓存条目的资产插件不应使用此辅助方法,因为它旨在为每个资产来源获取一个键。通常我们建议插件开发人员重构以避免为资产来源生成多个缓存条目。

现在您已经启用了缓存、加载了正确的插件并检索了唯一缓存键,您可以使用 parse 方法的 cache 参数来设置缓存和资产之间的数据流。该值来自资产管理器,指示资产是否正在刷新(例如通过 --flush-cache 或 meta 任务 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:在 parse 方法完成之前,强制使用 self._cache 的内容设置缓存插件

  • update_cache_if_changed:仅在 self._cache 被修改且 parse 方法完成之前,设置缓存插件

  • clear_cache:清除缓存,最终通过调用缓存插件的 flush() 方法实现,其具体实现取决于所使用的特定缓存插件。请注意,如果用户对 facts 和资产使用相同的缓存后端,两者都会被刷新。为避免这种情况,用户可以在其资产插件配置中指定不同的缓存后端。

constructed 特性

资产插件可以通过使用 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 方法实现的。

应为添加到资产的每个主机调用每个方法。需要三个位置参数:constructed 选项、变量字典和主机名。先调用 _set_composite_vars 方法将允许 keyed_groupsgroups 使用组合后的变量。

默认情况下,未定义的变量会被忽略。这对于 compose 是默认允许的,因此您可以使变量定义依赖于稍后在 Play 中从其他来源填充的变量。对于组,它允许使用并不总是存在的变量,而无需使用 default 过滤器。要支持将未定义变量配置为错误,请将 constructed 选项 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 设置为新函数来更改用于 constructed 特性的清理规则。核心引擎也会进行清理,因此如果自定义函数限制较少,则应将其与配置设置 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 调用时,脚本必须向 stdout 输出一个包含所有待管理组的 JSON 对象。每个组的值应该是包含每个主机列表、任何子组和潜在组变量的对象,或者仅仅是主机列表

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

}

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

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

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

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

调优外部资产脚本

1.3 版本新增功能。

上述库存资产脚本系统适用于所有版本的 Ansible,但为每个主机调用 --host 可能效率相当低,特别是如果它涉及对远程子系统的 API 调用。

为了避免这种低效,如果资产脚本返回一个名为“_meta”的顶级元素,则可以在单个脚本执行中返回所有主机变量。当此 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

Playbook 和 Ad Hoc 任务执行的 Python API

开发模块

开始开发模块

开发插件

如何开发插件

AWX

Ansible 的 REST API 端点和 GUI,与动态资产同步

交流方式

有疑问?需要帮助?想分享你的想法?请访问 Ansible 通信指南