Ansible 与 Python 3
ansible-core 代码运行 Python 3(有关具体版本,请查看 控制节点要求)。ansible-core 和 Ansible Collections 的贡献者应了解本文档中的建议,以便编写出能够在与 Ansible 其余部分相同的 Python 版本上运行的代码。
根据 Ansible 代码类型的不同,我们确实有一些注意事项:
控制节点上的代码 - 在调用 /usr/bin/ansible 的机器上运行的代码,只需要支持控制节点的 Python 版本。
模块 - Ansible 传输到受管机器并在其上调用的代码。模块需要支持“受管节点”的 Python 版本(有少数例外)。
共享的
module_utils代码 - 模块用于执行任务的通用代码,有时也供控制节点上的代码使用。共享的module_utils代码需要支持与模块相同的 Python 版本范围。
然而,这三种类型的代码并不使用相同的字符串策略。如果您正在开发模块或某些 module_utils 代码,请务必仔细阅读有关字符串策略的章节。
Python 3.x 和 Python 2.x 的最低版本
有关支持的具体版本,请参阅 控制节点要求 和 受管节点要求。
您的自定义模块可以支持您想要的任何 Python 版本(或其他语言),但上述版本是贡献给 Ansible 项目的代码的要求。
开发支持 Python 2 和 Python 3 的 Ansible 代码
学习编写同时支持 Python 2 和 Python 3 的代码的最佳起点是 Lennart Regebro 的书:移植到 Python 3 (Porting to Python 3)。该书介绍了多种移植到 Python 3 的策略。我们所采用的策略是从单一代码库同时支持 Python 2 和 Python 3。
理解 Python 2 和 Python 3 中的字符串
Python 2 和 Python 3 处理字符串的方式不同,因此当您编写支持 Python 3 的代码时,必须决定使用哪种字符串模型。字符串可以是字节数组(类似于 C 语言),也可以是文本数组。文本是我们所认为的字母、数字、其他可打印符号以及少量不可打印的“符号”(控制代码)。
在 Python 2 中,这两种类型(str 用于字节,unicode 用于文本)通常可以互换使用。当只处理 ASCII 字符时,字符串可以自动合并、比较和相互转换。当引入非 ASCII 字符时,由于 Python 2 不知道非 ASCII 字符应该采用什么编码,它开始抛出异常。
Python 3 通过使字节(bytes)和文本(str)之间的区分更加严格来改变了这种行为。Python 3 在尝试合并和比较这两种类型时会抛出异常。程序员必须明确地将一种类型转换为另一种类型才能混合使用它们的值。
在 Python 3 中,当代码以不当方式混合使用字节和文本类型时,程序员会立即发现;而在 Python 2 中,混合使用这些类型的代码可能会一直运行,直到用户输入非 ASCII 字符引发异常。Python 3 迫使程序员主动为其程序中的字符串处理定义策略,从而避免无意中混合使用文本和字节字符串。
Ansible 在控制节点代码、模块 (modules) <module_string_strategy> 以及 module_utils 代码中使用不同的字符串处理策略。
控制节点字符串策略:Unicode 三明治
直到最近,ansible-core 还支持 Python 2.x 并遵循这种称为“Unicode 三明治”(以 Python 2 的 unicode 文本类型命名)的策略。对于 Unicode 三明治,我们知道在我们的代码与外部世界(例如文件和网络 IO、环境变量以及某些库调用)的边界处,我们将接收到字节。我们需要将这些字节转换为文本,并在代码内部始终使用文本。当我们必须将这些字符串发送回外部世界时,我们会首先将文本转回字节。为了直观地理解这一点,可以想象一个“三明治”,它由顶部和底部的字节层、中间的转换层以及所有位于中间的文本类型组成。
出于兼容性原因,您会看到我们开发的一堆自定义函数(to_text/to_bytes/to_native),尽管 Python 2 已不再是考虑因素,但我们仍将继续使用它们,因为它们适用于其他使得 Unicode 处理变得棘手的场景。
虽然我们不再使用其中的大部分内容,但下方的文档对于那些仍需同时支持 Python 2 和 3 的模块开发者仍然有用。
Unicode 三明治的常见边界:在控制节点代码中转换字节到文本的地方
这是一个部分列表,列出了在使用 Unicode 三明治字符串策略时必须进行字节与文本相互转换的地方。它并不详尽,但可以让你了解在哪里需要注意潜在的问题。
文件的读取和写入
在 Python 2 中,从文件读取会产生字节。在 Python 3 中,它可以产生文本。为了编写对两者都兼容的代码,我们不利用 Python 3 产生文本的能力,而是自己显式地进行转换。例如
from ansible.module_utils.common.text.converters import to_text
with open('filename-with-utf8-data.txt', 'rb') as my_file:
b_data = my_file.read()
try:
data = to_text(b_data, errors='surrogate_or_strict')
except UnicodeError:
# Handle the exception gracefully -- usually by displaying a good
# user-centric error message that can be traced back to this piece
# of code.
pass
注意
Ansible 的大部分内容假设所有编码后的文本都是 UTF-8。如果未来有对其他编码的需求,我们可能会更改,但目前假设字节是 UTF-8 是安全的。
写入文件是相反的过程
from ansible.module_utils.common.text.converters import to_bytes
with open('filename.txt', 'wb') as my_file:
my_file.write(to_bytes(some_text_string))
请注意,我们在这里不需要捕获 UnicodeError,因为我们正在转换为 UTF-8,而 Python 中的所有文本字符串都可以转换回 UTF-8。
文件系统交互
处理文件名通常涉及回退到字节,因为在类 UNIX 系统上,文件名即字节。在 Python 2 中,如果我们向这些函数传递文本字符串,文本字符串会在函数内部被转换为字节字符串,如果存在非 ASCII 字符,则会发生回溯(traceback)。在 Python 3 中,只有当文本字符串无法在当前区域设置(locale)中解码时才会发生回溯,但显式处理并在两个版本上都能工作的代码仍然是好的做法。
import os.path
from ansible.module_utils.common.text.converters import to_bytes
filename = u'/var/tmp/くらとみ.txt'
f = open(to_bytes(filename), 'wb')
mtime = os.path.getmtime(to_bytes(filename))
b_filename = os.path.expandvars(to_bytes(filename))
if os.path.exists(to_bytes(filename)):
pass
当您仅将文件名作为字符串进行操作而不与文件系统(或与文件系统对话的 C 库)交互时,通常无需转换为字节。
import os.path
os.path.join(u'/var/tmp/café', u'くらとみ')
os.path.split(u'/var/tmp/café/くらとみ')
另一方面,如果代码需要操作文件名并与文件系统对话,那么直接将其转换为字节并以字节进行操作会更方便。
警告
确保传递给函数的变量类型相同。如果您正在使用类似 os.path.join() 这样接受多个字符串并组合使用它们的函数,则需要确保所有类型相同(要么全部是字节,要么全部是文本)。混合使用字节和文本会导致回溯。
与其他程序交互
与其他程序的交互通过操作系统和 C 库进行,并在 UNIX 内核定义的事务上运行。这些接口都是面向字节的,因此 Python 接口也必须面向字节。在 Python 2 和 Python 3 中,都应将字节字符串提供给 Python 的 subprocess 库,并预期从中返回字节字符串。
Ansible 控制节点代码中与其他程序交互的主要位置之一是连接插件的 exec_command 方法。这些方法将它们在命令(以及命令参数)中接收到的任何文本字符串转换为字节来执行,并将 stdout 和 stderr 作为字节字符串返回。更高级别的函数(例如动作插件的 _low_level_execute_command)会将输出转换为文本字符串。
模块字符串策略:原生字符串 (Native String)
在模块中,我们使用一种称为“原生字符串”的策略。这使得维护 Ansible 大量模块的社区成员的工作变得更容易,不需要强制要求所有模块内部的字符串都必须是文本,也不需要在边界处进行文本和字节的强制转换,从而避免破坏向后兼容性。
原生字符串指的是您在指定裸字符串字面量时 Python 所使用的类型。
"This is a native string"
在 Python 2 中,这些是字节字符串。在 Python 3 中,这些是文本字符串。模块的编码应符合预期:在 Python 2 上接收字节,在 Python 3 上接收文本。
Module_utils 字符串策略:混合型
在 module_utils 代码中,我们使用混合字符串策略。尽管 Ansible 的 module_utils 代码在很大程度上类似于模块代码,但它的一些部分也被控制节点使用。因此,它需要与模块以及控制节点的假设兼容,特别是字符串策略。module_utils 代码尝试接受原生字符串作为其函数的输入,并发出原生字符串作为输出。
在 module_utils 代码中:
函数必须接受文本字符串或字节字符串作为字符串参数。
函数可以返回与其接收到的类型相同的字符串,也可以返回其运行所处 Python 版本对应的原生字符串类型。
返回字符串的函数必须记录它们返回的是与传入类型相同的字符串,还是原生字符串。
因此,module-utils 函数通常具有高度的防御性。它们在函数开头将字符串参数转换为文本(使用 ansible.module_utils.common.text.converters.to_text),完成工作后,将返回值转换为原生字符串类型(使用 ansible.module_utils.common.text.converters.to_native),或转换回其参数所接收的字符串类型。
Python 2/Python 3 兼容性的提示、技巧和惯用法
使用前向兼容的样板代码
在所有 Python 文件的顶部使用以下样板代码,以确保某些构造在 Python 2 和 Python 3 上的行为相同:
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
__metaclass__ = type 使得文件中定义的所有类都成为新式类,而无需显式继承自 object。
__future__ 导入执行以下操作:
字节字符串前缀使用 b_
由于混合文本和字节类型会导致回溯,我们希望清楚哪些变量保存文本,哪些变量保存字节。我们通过为任何保存字节的变量添加 b_ 前缀来实现这一点。例如
filename = u'/var/tmp/café.txt'
b_filename = to_bytes(filename)
with open(b_filename) as f:
data = f.read()
我们不为文本字符串添加前缀,因为我们只在边界操作字节字符串,所以需要字节的变量比文本变量少。
导入 Ansible 捆绑的 Python six 库
第三方 Python six 库旨在帮助项目创建在 Python 2 和 Python 3 上都能运行的代码。Ansible 在 module_utils 中包含了该库的一个版本,以便其他模块可以在无需在远程系统上安装它的情况下使用它。要使用它,请这样导入:
from ansible.module_utils import six
注意
Ansible 也可以使用系统安装的 six 副本
如果系统安装的 six 版本比 Ansible 捆绑的版本更新,Ansible 将使用系统副本。
使用 as 处理异常
为了让代码在 Python 2.6+ 和 Python 3 上正常工作,请使用使用 as 关键字的新异常捕获语法:
try:
a = 2/0
except ValueError as e:
module.fail_json(msg="Tried to divide by zero: %s" % e)
不要使用以下语法,因为它在所有 Python 3 版本上都会失败:
try:
a = 2/0
except ValueError, e:
module.fail_json(msg="Tried to divide by zero: %s" % e)
更新八进制数字
在 Python 2.x 中,八进制字面量可以指定为 0755。在 Python 3 中,八进制必须指定为 0o755。
控制节点代码的字符串格式化
使用 str.format() 以实现 Python 2.6 兼容性
从 Python 2.6 开始,字符串获得了一个名为 format() 的方法来组合字符串。然而,format() 的一个常用功能直到 Python 2.7 才添加,因此您需要记住不要在 Ansible 代码中使用它:
# Does not work in Python 2.6!
new_string = "Dear {}, Welcome to {}".format(username, location)
# Use this instead
new_string = "Dear {0}, Welcome to {1}".format(username, location)
上述两个格式字符串都将 format() 方法的位置参数映射到字符串中。但是,第一个版本在 Python 2.6 中不起作用。请务必记住在占位符中放入数字,以便代码与 Python 2.6 兼容。
对字节字符串使用百分号格式化
在 Python 3.5 及更高版本中,字节字符串没有 format() 方法。但是,它确实支持较旧的百分号格式化。
b_command_line = b'ansible-playbook --become-user %s -K %s' % (user, playbook_file)