Ansible-core 2.19 移植指南
本节讨论 ansible-core 2.18 和 ansible-core 2.19 之间的行为变更。
旨在帮助您更新剧本 (playbooks)、插件以及 Ansible 基础架构的其他部分,以便它们能与此版本的 Ansible 协同工作。
请查看本页面以及 ansible-core 2.19 更新日志 以了解必要的变更。
本文档是移植系列指南的一部分。完整的移植指南列表可以在 移植指南 中找到。
简介
此版本包括对模版系统的全面改革以及名为“数据标记 (Data Tagging)”的新功能。这些更改使得报告大量在先前版本中未被发现的问题行为成为可能,并在安全性、性能和用户体验方面产生了深远的积极影响。
在切实可行的情况下保留了向后兼容性,但某些破坏性更改是必要的。本指南描述了一些常见的问题场景,并提供了示例内容、错误消息和建议的解决方案。
我们建议您在预生产环境中通过此版本测试您的剧本和角色,以确定可能需要进行更改的地方。
Playbook
损坏的条件判断
当输入表达式或模版不是字符串,或者结果不是布尔值时,会发生条件判断中断。Python 和 Jinja 会对条件表达式中大多数非空、非布尔值进行隐式的“真值 (truthy)”评估。虽然有时为了简洁而需要这种行为,但“真值”条件评估往往会掩盖剧本中严重的逻辑错误,而旧版本的 ansible-core 无法可靠地检测到这些错误。
此版本对模版渲染的更改会在表达式评估期间检测非布尔值的条件判断,并默认报告错误。可以通过 ALLOW_BROKEN_CONDITIONALS 配置项将错误暂时降级为警告。
以下示例源自实际剧本中掩盖了逻辑错误的中断条件判断。
示例 - 隐式布尔转换
此表达式依赖于 inventory_hostname 的隐式真值评估。应改用具有布尔结果的显式谓词,例如 | length > 0 或 is truthy。
- assert:
that: inventory_hostname
报告的错误是
Conditional result was 'localhost' of type 'str', which evaluates to True. Conditionals must have a boolean result.
通过使用显式布尔转换可以解决此问题
- assert:
that: inventory_hostname | length > 0
示例 - 无意的真值条件判断
此条件判断的第二部分被错误地添加了引号。带引号的部分变成了表达式的结果(评估为真值),因此该表达式永远不可能是 False。
- assert:
that: inventory_hostname is defined and 'inventory_hostname | length > 0'
报告的错误是
Conditional result was 'inventory_hostname | length > 0' of type 'str', which evaluates to True. Conditionals must have a boolean result.
移除错误的引号即可解决此问题
- assert:
that: inventory_hostname is defined and inventory_hostname | length > 0
示例 - 表达式语法错误
以前的 Ansible 版本可能会将某些表达式语法错误掩盖为真值结果。
- assert:
that: 1 == 2,
# ^ invalid comma
报告的错误是
Syntax error in expression: chunk after expression
删除表达式后无效的逗号即可解决此问题。
示例 - Jinja 运算顺序
此表达式使用了 ~ 连接运算符,该运算符在 contains 测试之后进行评估。结果总是一个非空字符串,即为真值。
- assert:
that: inventory_hostname is contains "local" ~ "host"
报告的错误是
Conditional result was 'Truehost' of type 'str', which evaluates to True. Conditionals must have a boolean result.
插入括号以在 contains 测试之前处理连接运算,即可解决此问题
- assert:
that: inventory_hostname is contains("local" ~ "host")
示例 - 字典作为条件
此条件判断本应加引号。在 YAML 列表元素中,冒号后带有空格的非引号字符串会被 YAML 解析器解释为映射。非空映射总是真值。
- assert:
that:
- result.msg == "some_key: some_value"
# ^^ colon+space == problem
报告的错误是
Conditional expressions must be strings.
为整个断言表达式加引号即可解决此问题
- assert:
that:
- 'result.msg == "some_key: some_value"'
多轮模板化
此前,在其他模版或表达式中嵌入模版可能导致执行不受信任的模版。此版本中经过重构的模版引擎不再支持这种不安全的行为。
示例 - 表达式中不必要的模板
此条件判断使用模版引用变量,而不是直接在表达式中使用变量。
- assert:
that: 1 + {{ value }} == 2
vars:
value: 1
报告的错误是
Syntax error in expression. Template delimiters are not supported in expressions: expected token ':', got '}'
直接引用变量而不使用模版即可解决此问题
- assert:
that: 1 + value == 2
vars:
value: 1
示例 - 动态表达式构建
此条件判断是使用模版动态创建的,本应作为表达式进行评估。之前,该模版由任务参数模版渲染,产生一个纯字符串,随后由 assert 动作进行评估。
- assert:
that: inventory_hostname {{ comparison }} 'localhost'
vars:
comparison: ==
报告的错误是
Syntax error in expression. Template delimiters are not supported in expressions: chunk after expression
从剧本中动态构建表达式是不安全且不受支持的。
排查不受信任的模板
默认情况下,不受信任的模版会被静默忽略。启用对不受信任模版的警告或错误有助于排查模版信任问题。环境变量 _ANSIBLE_TEMPLAR_UNTRUSTED_TEMPLATE_BEHAVIOR 可用于控制此行为。
有效选项为:
warning- 遇到不受信任的模版时发出警告。error- 遇到不受信任的模版时引发错误。ignore- 静默忽略不受信任的模版并按原样使用。这是默认行为。
注意
此可选的警告和失败行为是实验性的,未来版本可能会有变动。
循环不再泄露 omit 占位符
Omit 占位符不再会在循环项模版渲染和任务模版渲染之间泄漏。
此前,omit 占位符在模版渲染后可能仍嵌入在循环项中,并被用作任务模版渲染的 omit。现在,在渲染循环项时,解析为 omit 的值会被立即丢弃。
要将缺失值转换为任务模版渲染的 omit,请使用 | default(omit)。此解决方案与以前版本的 ansible-core 向后兼容。
示例 - 缺少 default(omit)
以下任务尝试从循环向任务传递 omit,但由于该值已被省略,因此它是未定义的
- debug:
msg: "{{ item.msg }}" # 'msg' is undefined
loop:
- msg: "{{ omit }}" # 'msg' will be omitted from the loop item
此更新后的任务在缺失值上使用 default(omit),以确保它能被任务省略
- debug:
msg: "{{ item.msg | default(omit) }}" # 'msg' is undefined, use 'default(omit)' to turn it into an omit
loop:
- msg: "{{ omit }}" # passed through in earlier versions, this value is now omitted from the loop item
权限提升超时
等待权限提升 (become) 超时现在属于不可达错误 (unreachable error),而不是任务错误。对于需要忽略 become 超时的任务,现有的剧本应将 ignore_errors 替换为 ignore_unreachable。
引擎
模板化
模板信任模型倒置
此前,ansible-core 默认信任所有字符串值作为 Jinja 模版进行渲染,但对从不受信任源(例如模块结果)获取的字符串应用了一个“不安全 (unsafe)”包装对象。由于许多模版操作可以在控制主机上以运行 ansible-core 的用户身份执行任意代码,模版引擎会静默忽略这些被标记为不安全的字符串。这要求所有处理字符串的代码必须正确传播该包装对象,从而导致了大量潜在的 CVE 级远程代码执行 (RCE) 漏洞。
此版本颠倒了先前的信任模型。只有标记为从受信任源加载的字符串才有资格作为模版进行渲染。不受信任的值(与以前一样)可以被模版引用,但模版表达式本身必须始终是受信任的。虽然这一更改仍然要求在操作字符串时考虑信任标记的传播,但如果操作失败,现在只会导致模版渲染能力丧失,而不再是高严重性的安全问题。
尝试渲染出现在不受信任字符串中的模版(与以前一样)将返回未经修改的原始字符串。默认情况下,尝试渲染不受信任的模版会静默失败,但可以通过配置将此类失败升级为警告或错误。
由模版操作产生的新字符串结果永远不会自动应用信任,尽管模版如果原封不动地返回现有的受信任字符串值,也不会剥离其信任。插件也可以显式应用信任。
向后兼容的模版信任行为在大多数情况下会自动应用;例如,出现在剧本、角色、变量文件和大多数内置清单插件中的模版将产生受信任的模版字符串。获取模版字符串的自定义插件将需要使用新的公共 API 在适当的情况下应用信任。
要求原生 Jinja 模式
以前的版本支持两种不同的模版模式:
Jinja 原生的字符串模版模式将每个模版操作的结果转换为字符串。
Jinja 原生模式通常在模版结果中保留变量类型。
在两种模式下,ansible-core 都会将最终的模版字符串结果作为 Python 字面量进行评估,如果评估导致错误,则回退到原始字符串。模版模式的选择由配置控制,默认使用 Jinja 原生的字符串模版。
现在 exclusively 使用 Jinja 的原生模版模式。设置模版模式的配置选项已被弃用,且不再起任何作用。
模版渲染中对原生类型的保留得到了改进,修复了先前实现中的缺陷,完全消除了最终的字面量评估过程(这是困惑、错误和性能问题的常见来源)。在少数剧本依赖于字符串到对象隐式转换的情况下,将需要进行显式转换。
一些现有的模版可能会无意中将非字符串转换为字符串。在以前的版本中,这种转换可能被字符串作为 Python 字面量的评估所掩盖。
示例 - 无意的字符串转换
此表达式错误地将一个列表传递给仅对字符串操作的 replace 过滤器。该过滤器会静默地将列表输入转换为字符串。由于某些字符串结果以前会被解析为列表,因此这种错误在早期版本中经常未被发现。
- debug:
msg: "{{ ['test1', 'test2'] | replace('test', 'prod') }}"
此模版的结果变成了字符串
ok: [localhost] => {
"msg": "['prod1', 'prod2']"
}
通过使用 map 过滤器将 replace 过滤器应用于每个列表元素,可以解决此问题
- debug:
msg: "{{ ['test1', 'test2'] | map('replace', 'test', 'prod') }}"
修正后的模版结果将保持为列表
ok: [localhost] => {
"msg": [
"prod1",
"prod2"
]
}
示例 - 无意的 None 结果
如果模版评估结果为 None,在旧版本的 ansible-core 中它会被隐式转换为一个空字符串。现在,这可能导致模版评估结果为值 None。
以下示例显示了这种情况发生的情形
- set_fact:
# If 'foo' is not defined, the else branch basically evaluates to None.
# So value_none will not be an empty string, but None:
value_none: |-
{% if foo is defined %}foo is defined{% endif %}
此示例可以按如下方式修复
- set_fact:
# Explicitly return an empty string in the 'else' branch.
# The value is always a string: either "foo is defined" or "".
value_none: |-
{% if foo is defined %}foo is defined{% else %}{{ "" }}{% endif %}
此调整与较旧的 ansible-core 版本向后兼容。
注意
自 ansible-core 2.19.1 起,字符串类型的模块选项接受 None 并将其转换为一个空字符串。在 ansible-core 2.18 之前,将 None 传递给此类选项会导致错误。这意味着在大多数情况下,角色和剧本中的表达式不需要因为无意的 None 结果而进行调整。
延迟模板化
Ansible 与 Jinja 模版引擎的接口经过了深度优化,为许多复杂的模版操作带来了显著的性能提升。以前,深度嵌套、递归或自引用的模版操作在每次访问时总是被解析到其全部深度和广度,包括在单个模版操作中对相同数据的重复访问。即使是那些从未被直接访问的嵌套数据结构深处的模版,也会在单个逻辑模版操作中被昂贵且重复地评估。新的模版引擎会惰性地推迟几乎所有的递归和模版渲染,直到值被访问或确定要退出模版引擎为止;且在模版操作期间,中间嵌套或间接渲染的结果会被缓存,减少了重复渲染。这些更改已显示在许多实际的复杂模版场景中具有指数级的性能提升。
一致的 range 处理
使用 Jinja 全局函数 range() 的结果在很大程度上取决于其使用的上下文以及是否启用了 Jinja 的原生模式。为了保留在过滤器链中使用超大范围的能力,结果现在总是 range 对象,这意味着除非将其转换为可返回的类型,否则无法从模版中返回它。
示例 - 有意的列表转换
- debug:
loop: "{{ range(0, 2) }}"
未嵌入容器的 range 通常会在模版最终化期间转换为列表。现在它们将导致此错误
Error rendering template: Type 'range' is unsupported for variable storage.
通过显式转换可以解决此问题
- debug:
loop: "{{ range(0, 2) | list }}"
示例 - 无意的字符串转换
- debug:
msg: "{{ [range(0,2), range(7,10)] }}"
嵌入容器中的 range 通常会被转换为该 range 对象的字符串表示形式。
ok: [localhost] => {
"msg": "[range(0, 2), range(7, 10)]"
}
现在尝试这样做将导致错误;如果您确实想对其进行操作,可以显式地将容器转换为字符串,或者将 range 转换为列表。
- debug:
msg: "{{ [range(0,2), range(7,10)] | string }}"
- debug:
msg: "{{ [range(0,2), range(7,10)] | map('list') }}"
错误处理
上下文相关的警告和错误
ansible-core 内部错误处理的更改在许多导致警告或错误的情况下都将可见。在大多数情况下,操作上下文(产生错误或警告时正在发生什么)和涉及的数据元素被捕获并包含在面向用户的消息中。在任务执行期间发生的错误和警告会更一致地包含在任务结果中,回调可以访问完整详情,(如果是错误)结果的 msg 字段中也会包含最低限度的错误消息。由于这种错误处理的标准化性质,某些错误消息中可能会出现看起来多余的元素。随着其他错误处理改进的进行,这些情况将会改善,但目前这是确保在所有错误情况下都能获得正确上下文所必需的。错误消息内容不被视为稳定,因此应尽可能避免自动化依赖于它们。
变量来源追踪
新的数据标记功能将变量的来源追踪扩展到了几乎每个源。这允许更具描述性的错误消息,因为可以查阅整个执行链以包含关于错误发生时正在发生什么的上下文信息。在大多数情况下,这包括文件路径、源行和列标记。非文件变量源(如 CLI 参数、清单插件和环境变量)也受到支持。
值访问时的弃用警告
新功能允许大多数 ansible-core 变量和值被标记为已弃用。插件和模块可以应用这些标签,以使用描述和建议替代方案的帮助文本来增强其返回值的弃用部分,当标记的值被例如剧本或模版访问时,这些文本将显示在运行时警告中。这使得模块和事实结果以及过时的核心行为的演变和移除变得更容易。
例如,访问已弃用的 play_hosts 魔术变量将触发弃用警告,建议改用 ansible_play_batch 变量。
改进的 Ansible 模块错误处理
现在,以 Python 实现的 Ansible 模块通过 AnsiballZ 包装器提供异常处理。在之前版本的 ansible-core 中,Ansible 模块中未处理的异常只会打印回溯信息并退出,而不提供标准的模块响应,这导致任务结果包含通用的 MODULE FAILURE 消息以及模块产生的任何原始输出文本。
为了解决这个问题,模块通常会在大部分代码周围实现不必要的 try/except 块,且无法进行特定的错误处理,只能调用带有通用失败消息的 AnsibleModule.fail_json。这种模式不再必要,因为所有 Ansible Python 模块中未处理的异常现在都被 AnsiballZ 包装器捕获,并作为结构化的模块结果返回,当控制器启用时会自动包含回溯信息。
改进对未定义变量的处理
未定义变量的处理得到了改进,避免了 Jinja 插件静默忽略未定义值的情况。
这种情况通常发生在 Jinja 插件(如过滤器或测试)在不考虑可能存在未定义值的情况下检查变量类型时。
示例 - 缺少属性
此任务在条件判断中错误地引用了 stat 结果中一个未定义的 exists 属性。在以前的版本中,未定义的值未被检测到,因为它被传递给了 false Jinja 测试插件,该插件静默忽略了未定义值。因此,在早期版本的 ansible-core 中,此条件判断永远不可能是 True,并且没有迹象表明 failed_when 表达式无效。
- stat:
path: /does-not-exist
register: result
failed_when: result.exists is false
# ^ missing reference to stat
在当前版本中,会检测到错误的表达式并导致错误。
通过向条件判断添加缺失的 stat 属性可以纠正此问题
- stat:
path: /does-not-exist
register: result
failed_when: result.stat.exists is false
显示回溯信息
在以前的 ansible-core 版本中,通过 -vvv 选项增加详细程度可以获取某些控制器端错误的回溯信息,但其可用性和行为不一致。此功能也仅限于错误。
现在,整个 ansible-core 代码库中对错误、警告和弃用情况的处理已标准化。使用 ANSIBLE_DISPLAY_TRACEBACK 环境变量,可以针对所有异常,以及错误、警告或弃用的调用点(即使在模块代码中)选择性地收集和显示回溯信息。
有效选项为:
always- 始终显示回溯信息。此选项优先于下方的其他选项。never- 从不显示回溯信息。此选项优先于下方的其他选项。error- 显示错误的回溯信息。warning- 显示除弃用警告之外的警告回溯信息。deprecated- 显示弃用警告的回溯信息。
多个选项可以通过逗号分隔进行组合。
vars_files 中存在未定义变量时显示警告
在 ansible-core 的先前版本中,在 vars_files 中指定文件路径时使用的未定义变量会被静默忽略,且不会触发警告。现在情况有所改变,当在 vars_files 中指定文件路径时遇到未定义变量,将会显示一条警告。
- hosts: all
vars_files:
- "{{ inventory_dir }}/vars_files/bar.yml"
PLAYBOOK: foo.yml ************************************************************************
1 plays in foo.yml
[WARNING]: skipping vars_file item due to an undefined variable
Origin: /examples/foo.yml:6:7
4
5 vars_files:
6 - "{{ inventory_dir }}/vars_files/bar.yml"
^ column 7
在上述示例中,之所以显示警告,是因为 inventory_dir 是在任务级别评估的主机范围变量,而不是在处理 vars_files 的 Play 级别进行评估。虽然 inventory_dir 不能在 vars_files 中使用,但它可以在任务级变量中使用,此时来自 vars_files 的变量已经可用。
插件 API
废弃值
插件和 Python 模块可以使用 ansible.module_utils.datatag 中的新函数 deprecate_value 将返回的值标记为已弃用。可以将已弃用功能的描述、可选的帮助文本和移除时间框架附加到该值上,如果已弃用的值在表达式中被引用,则会在运行时警告中显示。警告消息将包含有关应用弃用标签的模块/插件的信息以及访问它的表达式的位置。
from ansible.module_utils.datatag import deprecate_value
...
module.exit_json(
color_name=deprecate_value(
value="blue",
msg="The `color_name` return value is deprecated.",
help_text="Use `color_code` instead.",
),
color_code="#0000ff",
)
当从模块结果访问 color_name 时,将显示以下警告
[DEPRECATION WARNING]: The `color_name` return value is deprecated. This feature will be removed from the 'ns.collection.paint' module in a future release.
Origin: /examples/use_deprecated.yml:8:14
6
7 - debug:
8 var: result.color_name
^ column 14
Use `color_code` instead.
将模板信任应用于单个值
字符串值默认不再受信任作为模版进行渲染。从剧本、变量文件和其他内置受信任源加载的字符串通常默认标记为受信任。创建具有嵌入模版的新字符串实例的插件,必须使用 ansible.template 中的新函数 trust_as_template 来标记这些值源自受信任源,以允许模版被渲染。
警告
本节和相关的公共 API 目前尚不完整。
在清单和变量插件中应用模板信任
清单插件可以设置组和主机变量。在大多数情况下,这些变量是来自外部源的静态值,不需要信任。包含模版的变量值将需要通过 trust_as_template 进行显式信任才能允许渲染,但绝对不应将信任应用于来自可能被恶意篡改以包含模版的外部源的变量值。
警告
本节和相关的公共 API 目前尚不完整。
抛出异常
在异常处理程序中引发异常时,请务必根据需要使用 raise ... from。这取代了使用 AnsibleError 参数 orig_exc 来表示原因的做法。为了向后兼容,仍允许将 orig_exc 指定为原因。
在设置了 orig_exc 时不使用 raise ... from 将导致警告。此外,如果两个导致异常的原因不匹配,也会发出警告。
Jinja 插件中过于宽泛的异常处理
具有过于宽泛的异常处理(如 except Exception)的 Jinja 插件,在访问作为容器(dict、list)的变量内容时,其行为可能不正确。这可能发生在变量中的模版化值是未定义的、不可解密的加密值或触发惰性报告故障状况的其他值时。
Jinja 插件应尽可能捕获更具体的异常类型,并在合理的最少代码部分进行处理。尤其要小心避免在访问容器变量内容的代码周围进行宽泛的异常处理。
Ansible 自定义数据类型
ansible-core 中的许多变量对象由自定义类型表示。在以前的版本中,它们可以被视为如下类型:
AnsibleUnicode(str的子类)AnsibleSequence(list的子类)AnsibleMapping(dict的子类)
这些类型以及更多类型现在具有源自其原生 Python 类型的新子类。在大多数情况下,这些类型的行为与它们扩展的类型没有区别,现有的代码应该正常运行。然而,一些 Python 库不能正确处理内置对象的子类。与此类库交互的自定义插件可能需要进行更改,以转换并传递原生类型。
警告
本节和相关的公共 API 目前尚不完整。
AnsibleVaultEncryptedUnicode 被 EncryptedString 替换
AnsibleVaultEncryptedUnicode 类型已被 EncryptedString 取代。
创建 AnsibleVaultEncryptedUnicode 的插件现在将接收 EncryptedString 实例。此功能确保了与之前版本 ansible-core 的向后兼容性。
寻找 AnsibleVaultEncryptedUnicode 并执行 isinstance 检查的插件将不再遇到这些类型。以前由该类型表示的值现在将显示为带标签的 str。插件中不再需要特殊处理来访问这些值的内容。
非字符串字典键不再隐式转换
在以前的版本中,ansible-core 依赖 Python 的 json.dumps 在各种场景中(包括返回模块结果时)将 int、float、bool 和 None 字典键隐式转换为字符串。例如,模块被允许包含以下代码:
oid = 123
d = {oid: "value"}
module.exit_json(return_value=d)
从本版本开始,模块在将字典传递给 ansible-core 的 AnsibleModule.exit_json() 方法之前,必须显式地将任何非字符串键转换为字符串(例如,使用 Python 的 str() 函数)。上述代码必须按如下方式更改:
oid = 123
d = {str(oid): "value"}
module.exit_json(return_value=d)
如果您遇到 "[ERROR]: Task failed: Module failed: Key of type '<NON-STRING>' is not JSON serializable by the 'module_legacy_m2c' profile.,则表明任务中使用的模块未执行所需的键转换。
命令行
无显著变更
已废弃
无显著变更
模块
随着模版系统更改,不再可能将
async_status模块的started和finished整数属性用作条件判断中的值,因为需要布尔值。建议改用started和finished测试插件,例如:
- async_status:
jid: '{{ registered_task_result.ansible_job_id }}'
register: job_result
until: job_result is finished
retries: 5
delay: 10
已移除的模块
以下模块不再存在
无显著变更
弃用通知
无显著变更
值得注意的模块变更
无显著变更
插件
值得注意的插件变更
ssh连接插件现在支持使用SSH_ASKPASS提供密码进行身份验证,作为sshpass程序的替代方案。默认使用SSH_ASKPASS而不是sshpass。这由ssh连接插件的password_mechanism配置控制。要切回使用sshpass,请进行以下更改之一:对您的
ansible.cfg文件进行更改[ssh_connection] password_mechanism = sshpass
通过导出环境变量
export ANSIBLE_SSH_PASSWORD_MECHANISM=sshpass
通过设置以下变量:
ansible_ssh_password_mechanism: sshpass
bool过滤器中强制转换无法识别的输入值已被弃用。bool过滤器现在仅返回True或False,具体取决于输入:True- 对True、1以及字符串“yes”、“on”、“true”、“1”(不区分大小写)的匹配返回。False- 对False、0以及字符串“no”、“off”、“false”、“0”(不区分大小写)的匹配返回。
任何其他输入都将导致弃用警告。此警告将在
ansible-core2.23 中变为错误。当发出弃用警告时,返回值是
False,除非输入等于1(当输入是float值的1.0时可能发生这种情况)。当输入为
None时,此过滤器现在返回False而不是None。在这种情况下也会发出上述弃用警告。将带有可能解析为
Undefined的嵌入模版的嵌套非标量传递给 Jinja2 过滤器插件(如default和mandatory)以及测试插件(包括defined和undefined)将不再像以前的版本那样进行评估,因为带有嵌入模版的嵌套非标量仅在使用时进行模版渲染。在 2.19 中,此断言通过:- assert: that: # Unlike earlier versions, complex_var is defined even though complex_var.nested is not. - complex_var is defined # Unlike earlier versions, the default value is not applied because complex_var is defined. - (complex_var | default(unused)).nested is undefined # Like earlier versions, directly accessing complex_var.nested evaluates as undefined. - complex_var.nested is undefined vars: complex_var: # Before 2.19, complex_var.nested is evaluated immediately when complex_var is accessed. # In 2.19, complex_var.nested is evaluated only when it is accessed. nested: "{{ undefined_variable }}" unused: # This variable is used only if complex_var is undefined. # This only happens in ansible-core before 2.19. nested: default
移植自定义脚本
无显著变更
网络
无显著变更