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 存储库中找到它。

plugins/lookup/var_as_file.py

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()

如果您尝试一下,我很乐意收到任何反馈。