Ansible 中基于路径的参数的进程替换
Process Substitution in Ansible for Path-based Parameters
许多 Ansible 模块被设计为接受文件路径作为参数,但无法直接提供文件的内容。在输入数据实际上来自文件以外的其他东西的情况下,这迫使人们在磁盘上的某个地方创建一个临时文件,将预期的参数值写入其中,然后将这个临时文件的路径提供给 Ansible 模块。
为了便于说明,举一个现实生活中的例子:java_cert Ansible module 采用参数 pkcs12_path 作为指向包含要导入的密钥对的 PKCS12 密钥库的路径到给定的 Java 密钥库。现在说这个数据是通过 Vault 查找检索的,所以为了能够为模块提供它需要的路径,我们必须将 Vault 查找结果写入一个临时文件,使用文件的路径作为参数,然后处理临时文件的安全删除,因为数据可能是机密的。
当在 Shell/bash 脚本的上下文中出现这种情况时,即命令行工具的标志仅支持与文件交互,进程替换的魔力(例如 --file=<(echo $FILE_CONTENTS)
)允许通过透明地提供一个命名管道,将工具的输入和输出数据与其他命令链接起来,就像它是磁盘上的(大部分)普通文件一样。
在 Ansible 中,是否有任何类似的机制可以将基于文件的参数替换为更灵活的结构,从而允许使用来自变量或其他命令的数据?如果没有内置方法来实现这一点,是否有第 3 方解决方案允许它,或者像我描述的那样简化工作流程?例如,类似于自定义查找插件的东西,它随文件内容数据一起提供,然后在后台透明地处理文件管理(即创建、写入数据和最终删除)并提供临时路径作为其 return 值,用户不必知道它。
此类插件的示例用法可能是:
...
pkcs_path: "{{ lookup('as_file', '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY----- ') }}"
...
使用插件然后创建一个文件,例如/tmp/as_file.sg7N3bX 包含来自第二个参数的文本键和 returning 此文件路径作为查找结果。然而,我不确定在这种情况下如何实现文件的持续管理(尤其是及时删除敏感数据)。
免责声明:
- 我是(很明显!)以下合集的作者,该合集是作为对上述问题的回应而创建的
- 查找插件未经过全面测试,可能会因特定模块而失败。
由于这是个不错的主意,而且什么都没有,所以我决定试一试。这一切都在现在称为 thoteam.var_as_file
的集合中结束,可以 in a github repo. I won't paste all files in this answer as they are all available in the mentioned repo with a full README documentation 安装、测试和使用。
全局思路如下:
- 创建一个查找插件,负责推送具有给定内容的新临时文件并返回使用它们的路径。
- 清理剧本末尾创建的文件运行。对于这一步,我创建了一个回调插件,它启动了监听
v2_playbook_on_stats
事件的清理操作。
我仍然担心并发性(尚未清理的文件存储在磁盘上的静态 json 文件中)和可靠性(不确定 stats
阶段是否在所有情况下都会发生,特别是在崩溃时)。我也不完全确定为此使用回调是一个好的做法/最佳选择。
与此同时,编写代码非常有趣并且可以完成工作。我会看看这项工作是否被其他人使用,并可能在接下来的几周内很好地增强所有这些(如果你有 PRs 来修复已知的问题,我很乐意接受它们)。
安装并启用回调插件后(请参阅https://github.com/ansible-ThoTeam/thoteam.var_as_file#installing-the-collection),可以在任何地方使用查找来获取包含传递内容的文件路径。例如:
- name: Get a filename with the given content for later use
ansible.builtin.set_fact:
my_tmp_file: "{{ lookup('thoteam.var_as_file.var_as_file', some_variable) }}"
- name: Use in place in a module where a file is mandatory and you have the content in a var
community.general.java_cert:
pkcs12_path: "{{ lookup('thoteam.var_as_file.var_as_file', pkcs12_store_from_vault) }}"
cert_alias: default
keystore_path: /path/to/my/keystore.jks
keystore_pass: changeit
keystore_create: yes
state: present
这些是两个插件文件的相关部分。我删除了 ansible 文档变量(为简洁起见),如果您愿意,您可以直接在 git 存储库中找到它。
from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.thoteam.var_as_file.plugins.module_utils.var_as_file import VAR_AS_FILE_TRACK_FILE
from hashlib import sha256
import tempfile
import json
import os
def _hash_content(content):
"""
Returns the hex digest of the sha256 sum of content
"""
return sha256(content.encode()).hexdigest()
class LookupModule(LookupBase):
created_files = dict()
def _load_created(self):
if os.path.exists(VAR_AS_FILE_TRACK_FILE):
with open(VAR_AS_FILE_TRACK_FILE, 'r') as jfp:
self.created_files = json.load(jfp)
def _store_created(self):
"""
serialize the created files as json in tracking file
"""
with open(VAR_AS_FILE_TRACK_FILE, 'w') as jfp:
json.dump(self.created_files, jfp)
def run(self, terms, variables=None, **kwargs):
'''
terms contains the content to be written to the temporary file
'''
try:
self._load_created()
ret = []
for content in terms:
content_sig = _hash_content(content)
file_exists = False
# Check if file was already create for this content and check it.
if content_sig in self.created_files:
if os.path.exists(self.created_files[content_sig]):
with open(self.created_files[content_sig], 'r') as efh:
if content_sig == _hash_content(efh.read()):
file_exists = True
ret.append(self.created_files[content_sig])
else:
os.remove(self.created_files[content_sig])
# Create / Replace the file
if not file_exists:
temp_handle, temp_path = tempfile.mkstemp(text=True)
with os.fdopen(temp_handle, 'a') as temp_file:
temp_file.write(content)
self.created_files[content_sig] = temp_path
ret.append(temp_path)
self._store_created()
return ret
except Exception as e:
raise AnsibleError(to_native(repr(e)))
plugins/callback/clean_var_as_file.py
from ansible.plugins.callback import CallbackBase
from ansible_collections.thoteam.var_as_file.plugins.module_utils.var_as_file import VAR_AS_FILE_TRACK_FILE
from ansible.module_utils.common.text.converters import to_native
from ansible.errors import AnsibleError
import os
import json
def _make_clean():
"""Clean all files listed in VAR_AS_FILE_TRACK_FILE"""
try:
with open(VAR_AS_FILE_TRACK_FILE, 'r') as jfp:
files = json.load(jfp)
for f in files.values():
os.remove(f)
os.remove(VAR_AS_FILE_TRACK_FILE)
except Exception as e:
raise AnsibleError(to_native(repr(e)))
class CallbackModule(CallbackBase):
''' This Ansible callback plugin cleans-up files created by the thoteam.var_as_file.var_as_file lookup '''
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'utility'
CALLBACK_NAME = 'thoteam.var_as_file.clean_var_as_file'
CALLBACK_NEEDS_WHITELIST = False
# This one doesn't work for a collection plugin
# Needs to be enabled anyway in ansible.cfg callbacks_enabled option
CALLBACK_NEEDS_ENABLED = False
def v2_playbook_on_stats(self, stats):
_make_clean()
如果您尝试一下,我很乐意收到任何反馈。
许多 Ansible 模块被设计为接受文件路径作为参数,但无法直接提供文件的内容。在输入数据实际上来自文件以外的其他东西的情况下,这迫使人们在磁盘上的某个地方创建一个临时文件,将预期的参数值写入其中,然后将这个临时文件的路径提供给 Ansible 模块。
为了便于说明,举一个现实生活中的例子:java_cert Ansible module 采用参数 pkcs12_path 作为指向包含要导入的密钥对的 PKCS12 密钥库的路径到给定的 Java 密钥库。现在说这个数据是通过 Vault 查找检索的,所以为了能够为模块提供它需要的路径,我们必须将 Vault 查找结果写入一个临时文件,使用文件的路径作为参数,然后处理临时文件的安全删除,因为数据可能是机密的。
当在 Shell/bash 脚本的上下文中出现这种情况时,即命令行工具的标志仅支持与文件交互,进程替换的魔力(例如 --file=<(echo $FILE_CONTENTS)
)允许通过透明地提供一个命名管道,将工具的输入和输出数据与其他命令链接起来,就像它是磁盘上的(大部分)普通文件一样。
在 Ansible 中,是否有任何类似的机制可以将基于文件的参数替换为更灵活的结构,从而允许使用来自变量或其他命令的数据?如果没有内置方法来实现这一点,是否有第 3 方解决方案允许它,或者像我描述的那样简化工作流程?例如,类似于自定义查找插件的东西,它随文件内容数据一起提供,然后在后台透明地处理文件管理(即创建、写入数据和最终删除)并提供临时路径作为其 return 值,用户不必知道它。
此类插件的示例用法可能是:
...
pkcs_path: "{{ lookup('as_file', '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY----- ') }}"
...
使用插件然后创建一个文件,例如/tmp/as_file.sg7N3bX 包含来自第二个参数的文本键和 returning 此文件路径作为查找结果。然而,我不确定在这种情况下如何实现文件的持续管理(尤其是及时删除敏感数据)。
免责声明:
- 我是(很明显!)以下合集的作者,该合集是作为对上述问题的回应而创建的
- 查找插件未经过全面测试,可能会因特定模块而失败。
由于这是个不错的主意,而且什么都没有,所以我决定试一试。这一切都在现在称为 thoteam.var_as_file
的集合中结束,可以 in a github repo. I won't paste all files in this answer as they are all available in the mentioned repo with a full README documentation 安装、测试和使用。
全局思路如下:
- 创建一个查找插件,负责推送具有给定内容的新临时文件并返回使用它们的路径。
- 清理剧本末尾创建的文件运行。对于这一步,我创建了一个回调插件,它启动了监听
v2_playbook_on_stats
事件的清理操作。
我仍然担心并发性(尚未清理的文件存储在磁盘上的静态 json 文件中)和可靠性(不确定 stats
阶段是否在所有情况下都会发生,特别是在崩溃时)。我也不完全确定为此使用回调是一个好的做法/最佳选择。
与此同时,编写代码非常有趣并且可以完成工作。我会看看这项工作是否被其他人使用,并可能在接下来的几周内很好地增强所有这些(如果你有 PRs 来修复已知的问题,我很乐意接受它们)。
安装并启用回调插件后(请参阅https://github.com/ansible-ThoTeam/thoteam.var_as_file#installing-the-collection),可以在任何地方使用查找来获取包含传递内容的文件路径。例如:
- name: Get a filename with the given content for later use
ansible.builtin.set_fact:
my_tmp_file: "{{ lookup('thoteam.var_as_file.var_as_file', some_variable) }}"
- name: Use in place in a module where a file is mandatory and you have the content in a var
community.general.java_cert:
pkcs12_path: "{{ lookup('thoteam.var_as_file.var_as_file', pkcs12_store_from_vault) }}"
cert_alias: default
keystore_path: /path/to/my/keystore.jks
keystore_pass: changeit
keystore_create: yes
state: present
这些是两个插件文件的相关部分。我删除了 ansible 文档变量(为简洁起见),如果您愿意,您可以直接在 git 存储库中找到它。
from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.thoteam.var_as_file.plugins.module_utils.var_as_file import VAR_AS_FILE_TRACK_FILE
from hashlib import sha256
import tempfile
import json
import os
def _hash_content(content):
"""
Returns the hex digest of the sha256 sum of content
"""
return sha256(content.encode()).hexdigest()
class LookupModule(LookupBase):
created_files = dict()
def _load_created(self):
if os.path.exists(VAR_AS_FILE_TRACK_FILE):
with open(VAR_AS_FILE_TRACK_FILE, 'r') as jfp:
self.created_files = json.load(jfp)
def _store_created(self):
"""
serialize the created files as json in tracking file
"""
with open(VAR_AS_FILE_TRACK_FILE, 'w') as jfp:
json.dump(self.created_files, jfp)
def run(self, terms, variables=None, **kwargs):
'''
terms contains the content to be written to the temporary file
'''
try:
self._load_created()
ret = []
for content in terms:
content_sig = _hash_content(content)
file_exists = False
# Check if file was already create for this content and check it.
if content_sig in self.created_files:
if os.path.exists(self.created_files[content_sig]):
with open(self.created_files[content_sig], 'r') as efh:
if content_sig == _hash_content(efh.read()):
file_exists = True
ret.append(self.created_files[content_sig])
else:
os.remove(self.created_files[content_sig])
# Create / Replace the file
if not file_exists:
temp_handle, temp_path = tempfile.mkstemp(text=True)
with os.fdopen(temp_handle, 'a') as temp_file:
temp_file.write(content)
self.created_files[content_sig] = temp_path
ret.append(temp_path)
self._store_created()
return ret
except Exception as e:
raise AnsibleError(to_native(repr(e)))
plugins/callback/clean_var_as_file.py
from ansible.plugins.callback import CallbackBase
from ansible_collections.thoteam.var_as_file.plugins.module_utils.var_as_file import VAR_AS_FILE_TRACK_FILE
from ansible.module_utils.common.text.converters import to_native
from ansible.errors import AnsibleError
import os
import json
def _make_clean():
"""Clean all files listed in VAR_AS_FILE_TRACK_FILE"""
try:
with open(VAR_AS_FILE_TRACK_FILE, 'r') as jfp:
files = json.load(jfp)
for f in files.values():
os.remove(f)
os.remove(VAR_AS_FILE_TRACK_FILE)
except Exception as e:
raise AnsibleError(to_native(repr(e)))
class CallbackModule(CallbackBase):
''' This Ansible callback plugin cleans-up files created by the thoteam.var_as_file.var_as_file lookup '''
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'utility'
CALLBACK_NAME = 'thoteam.var_as_file.clean_var_as_file'
CALLBACK_NEEDS_WHITELIST = False
# This one doesn't work for a collection plugin
# Needs to be enabled anyway in ansible.cfg callbacks_enabled option
CALLBACK_NEEDS_ENABLED = False
def v2_playbook_on_stats(self, stats):
_make_clean()
如果您尝试一下,我很乐意收到任何反馈。