开发动态清单
Ansible 可以通过使用提供的 清单插件 从动态源(包括云源)获取清单信息。有关如何获取清单信息的详细信息,请参阅 使用动态清单。如果你想要的源目前不受现有插件的覆盖,你可以像创建任何其他插件类型一样创建自己的清单插件。
在以前的版本中,你必须创建一个脚本或程序,当使用适当的参数调用时,该脚本或程序可以输出正确格式的 JSON。你仍然可以使用和编写清单脚本,因为我们通过 脚本清单插件 确保了向后兼容性,并且对使用的编程语言没有限制。但是,如果你选择编写脚本,则需要自己实现一些功能,例如缓存、配置管理、动态变量和组组成等等。如果你使用 清单插件,则可以使用 Ansible 代码库并自动添加这些通用功能。
清单源
清单源是清单插件使用的输入字符串。清单源可以是文件或脚本的路径,也可以是插件可以解释的原始数据。
下表显示了一些清单插件示例以及你可以使用命令行上的 -i
传递给它们的源类型。
插件 |
源 |
用逗号分隔的主机列表 |
|
YAML 格式数据文件的路径 |
|
YAML 配置文件的路径 |
|
INI 格式数据文件的路径 |
|
YAML 配置文件的路径 |
|
输出 JSON 的可执行文件的路径 |
清单插件
与大多数插件类型(模块除外)一样,清单插件必须用 Python 开发。它们在控制节点上执行,因此应遵守 控制节点要求。
开发插件 中的大多数文档也适用于此。你应该先阅读该文档以了解一般情况,然后回到本文档以了解清单插件的具体信息。
通常,清单插件在运行开始时以及在加载剧本、任务或角色之前执行。但是,你可以使用 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_file
和 parse
。
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。
清单对象
传递给 parse
的 inventory
对象具有用于填充清单的实用方法。
add_group
如果组不存在,则将组添加到清单。它将组名作为唯一的 positional 参数。
add_child
将清单中存在的组或主机添加到清单中的父组。它接受两个 positional 参数,即父组的名称和子组或主机的名称。
add_host
将主机添加到清单中(如果它尚不存在),可以选择添加到特定组。它以主机名作为第一个参数,并接受两个可选关键字参数,group
和 port
。 group
是清单中组的名称,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_groups
和 groups
使用组合的变量。
默认情况下,未定义的变量将被忽略。默认情况下,这是允许 compose
进行的,因此您可以使变量定义依赖于稍后在剧本中从其他来源填充的变量。对于组,它允许使用不总是存在的变量,而不必使用 default
过滤器。要支持将未定义的变量配置为错误,请将构建的选项 strict
作为关键字参数传递给每个方法。
keyed_groups
和 groups
使用与主机关联的任何变量(例如,来自早期清单源)。 _add_host_to_keyed_groups
和 add_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_plugin
、cache_timeout
和其他与缓存相关的字段可能存在。
‘auto’ 插件
从 Ansible 2.5 开始,我们包含了 自动清单插件 并在默认情况下启用它。如果标准配置文件中的 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
Playbook 和 Ad Hoc 任务执行的 Python API
- 开发模块
开始开发模块
- 开发插件
如何开发插件
- AWX
Ansible 的 REST API 端点和 GUI,与动态库存同步
- 沟通
有问题?需要帮助?想要分享你的想法?请访问 Ansible 沟通指南