From 8960a57d533ad99d2c56a3cbde931c6a22ce1049 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 10 Aug 2025 13:32:35 +0200 Subject: [PATCH] Add binary_file lookup (#10616) * Add binary_file lookup. * Remove sentence on deprecation. --- .github/BOTMETA.yml | 2 + plugins/lookup/binary_file.py | 114 ++++++++++++++++++ .../targets/lookup_binary_file/aliases | 5 + .../targets/lookup_binary_file/files/file_1 | 1 + .../lookup_binary_file/files/file_1.license | 3 + .../targets/lookup_binary_file/files/file_2 | 1 + .../lookup_binary_file/files/file_2.license | 3 + .../targets/lookup_binary_file/tasks/main.yml | 57 +++++++++ tests/sanity/ignore-2.16.txt | 1 + 9 files changed, 187 insertions(+) create mode 100644 plugins/lookup/binary_file.py create mode 100644 tests/integration/targets/lookup_binary_file/aliases create mode 100644 tests/integration/targets/lookup_binary_file/files/file_1 create mode 100644 tests/integration/targets/lookup_binary_file/files/file_1.license create mode 100644 tests/integration/targets/lookup_binary_file/files/file_2 create mode 100644 tests/integration/targets/lookup_binary_file/files/file_2.license create mode 100644 tests/integration/targets/lookup_binary_file/tasks/main.yml diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 8a2ce082f2..cf18dac431 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -254,6 +254,8 @@ files: maintainers: ddelnano shinuza $lookups/: labels: lookups + $lookups/binary_file.py: + maintainers: felixfontein $lookups/bitwarden_secrets_manager.py: maintainers: jantari $lookups/bitwarden.py: diff --git a/plugins/lookup/binary_file.py b/plugins/lookup/binary_file.py new file mode 100644 index 0000000000..35f74516ea --- /dev/null +++ b/plugins/lookup/binary_file.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2025, Felix Fontein +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +DOCUMENTATION = r""" +name: binary_file +author: Felix Fontein (@felixfontein) +short_description: Read binary file and return it Base64 encoded +version_added: 11.2.0 +description: + - This lookup returns the contents from a file on the Ansible controller's file system. + - The file is read as a binary file and its contents are returned Base64 encoded. + This is similar to using P(ansible.builtin.file#lookup) combined with P(ansible.builtin.b64encode#filter), + except that P(ansible.builtin.file#lookup) does not support binary files as it interprets the contents as UTF-8, + which can cause the wrong content being Base64 encoded. +options: + _terms: + description: + - Paths of the files to read. + - Relative paths will be searched for in different places. See R(Ansible task paths, playbook_task_paths) for more details. + required: true + type: list + elements: str + not_exist: + description: + - Determine how to react if the specified file cannot be found. + type: str + choices: + error: Raise an error. + empty: Return an empty string for the file. + empty_str: + - Return the string C(empty) for the file. + - This cannot be confused with Base64 encoding due to the missing padding. + default: error +notes: + - This lookup does not understand 'globbing' - use the P(ansible.builtin.fileglob#lookup) lookup instead. +seealso: + - plugin: ansible.builtin.b64decode + plugin_type: filter + description: >- + The b64decode filter can be used to decode Base64 encoded data. + Note that Ansible cannot handle binary data, the data will be interpreted as UTF-8 text! + - plugin: ansible.builtin.file + plugin_type: lookup + description: You can use this lookup plugin to read text files from the Ansible controller. + - module: ansible.builtin.slurp + description: >- + Also allows to read binary files Base64 encoded, but from remote targets. + With C(delegate_to: localhost) can be redirected to run on the controller, but you have to know the path to the file to read. + Both this plugin and P(ansible.builtin.file#lookup) use some search path logic to for example also find files in the C(files) + directory of a role. + - ref: playbook_task_paths + description: Search paths used for relative files. +""" + +EXAMPLES = r""" +--- +- name: Output Base64 contents of binary files on screen + ansible.builtin.debug: + msg: "Content: {{ lookup('community.general.binary_file', item) }}" + loop: + - some-binary-file.bin +""" + +RETURN = r""" +_raw: + description: + - Base64 encoded content of requested files, or an empty string resp. the string C(empty), depending on the O(not_exist) option. + - This list contains one string per element of O(_terms) in the same order as O(_terms). + type: list + elements: str + returned: success +""" + +import base64 + +from ansible.errors import AnsibleLookupError +from ansible.plugins.lookup import LookupBase + +from ansible.utils.display import Display + +display = Display() + + +class LookupModule(LookupBase): + + def run(self, terms, variables=None, **kwargs): + self.set_options(var_options=variables, direct=kwargs) + not_exist = self.get_option("not_exist") + + result = [] + for term in terms: + display.debug(f"Searching for binary file: {term!r}") + path = self.find_file_in_search_path(variables, "files", term, ignore_missing=(not_exist != "error")) + display.vvvv(f"community.general.binary_file lookup using {path} as file") + + if not path: + if not_exist == "empty": + result.append("") + continue + if not_exist == "empty_str": + result.append("empty") + continue + raise AnsibleLookupError(f"Could not locate file in community.general.binary_file lookup: {term}") + + try: + with open(path, "rb") as f: + result.append(base64.b64encode(f.read()).decode("utf-8")) + except Exception as exc: + raise AnsibleLookupError(f"Error while reading {path}: {exc}") + + return result diff --git a/tests/integration/targets/lookup_binary_file/aliases b/tests/integration/targets/lookup_binary_file/aliases new file mode 100644 index 0000000000..12d1d6617e --- /dev/null +++ b/tests/integration/targets/lookup_binary_file/aliases @@ -0,0 +1,5 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/posix/2 diff --git a/tests/integration/targets/lookup_binary_file/files/file_1 b/tests/integration/targets/lookup_binary_file/files/file_1 new file mode 100644 index 0000000000..6c8db5df2b --- /dev/null +++ b/tests/integration/targets/lookup_binary_file/files/file_1 @@ -0,0 +1 @@ +file 1 \ No newline at end of file diff --git a/tests/integration/targets/lookup_binary_file/files/file_1.license b/tests/integration/targets/lookup_binary_file/files/file_1.license new file mode 100644 index 0000000000..9536c0b49d --- /dev/null +++ b/tests/integration/targets/lookup_binary_file/files/file_1.license @@ -0,0 +1,3 @@ +Copyright (c) 2025, Felix Fontein +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/tests/integration/targets/lookup_binary_file/files/file_2 b/tests/integration/targets/lookup_binary_file/files/file_2 new file mode 100644 index 0000000000..dd4128ed9e --- /dev/null +++ b/tests/integration/targets/lookup_binary_file/files/file_2 @@ -0,0 +1 @@ +file 2 \ No newline at end of file diff --git a/tests/integration/targets/lookup_binary_file/files/file_2.license b/tests/integration/targets/lookup_binary_file/files/file_2.license new file mode 100644 index 0000000000..9536c0b49d --- /dev/null +++ b/tests/integration/targets/lookup_binary_file/files/file_2.license @@ -0,0 +1,3 @@ +Copyright (c) 2025, Felix Fontein +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/tests/integration/targets/lookup_binary_file/tasks/main.yml b/tests/integration/targets/lookup_binary_file/tasks/main.yml new file mode 100644 index 0000000000..50ef92f0c0 --- /dev/null +++ b/tests/integration/targets/lookup_binary_file/tasks/main.yml @@ -0,0 +1,57 @@ +--- +# Copyright (c) 2025, Felix Fontein +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Read some files + ansible.builtin.set_fact: + file_1: >- + {{ query('community.general.binary_file', 'file_1') }} + empty: >- + {{ query('community.general.binary_file', 'does-not-exist', not_exist='empty') }} + empty_str: >- + {{ query('community.general.binary_file', 'does-not-exist', not_exist='empty_str') }} + multiple: >- + {{ query('community.general.binary_file', 'file_1', 'file_2') }} + multiple_empty: >- + {{ query('community.general.binary_file', 'file_1', 'does-not-exist', 'file_2', not_exist='empty') }} + multiple_empty_str: >- + {{ query('community.general.binary_file', 'file_1', 'does-not-exist', 'file_2', not_exist='empty_str') }} + +- name: Check results + ansible.builtin.assert: + that: + - file_1 == ["file 1" | ansible.builtin.b64encode] + - empty == [""] + - empty_str == ["empty"] + - multiple == ["file 1" | ansible.builtin.b64encode, "file 2" | ansible.builtin.b64encode] + - multiple_empty == ["file 1" | ansible.builtin.b64encode, "", "file 2" | ansible.builtin.b64encode] + - multiple_empty_str == ["file 1" | ansible.builtin.b64encode, "empty", "file 2" | ansible.builtin.b64encode] + +- name: Fail on non-existing file + ansible.builtin.debug: + msg: >- + {{ query('community.general.binary_file', 'does-not-exist') }} + ignore_errors: true + register: result + +- name: Check results + ansible.builtin.assert: + that: + - result is failed + - >- + "Could not locate file in community.general.binary_file lookup: does-not-exist" in result.msg + +- name: Fail on non-existing file after some existing ones + ansible.builtin.debug: + msg: >- + {{ query('community.general.binary_file', 'file_1', 'does-not-exist', 'file_2') }} + ignore_errors: true + register: result + +- name: Check results + ansible.builtin.assert: + that: + - result is failed + - >- + "Could not locate file in community.general.binary_file lookup: does-not-exist" in result.msg diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt index 1a4c8f89b1..446211c396 100644 --- a/tests/sanity/ignore-2.16.txt +++ b/tests/sanity/ignore-2.16.txt @@ -7,6 +7,7 @@ plugins/inventory/lxd.py yamllint:unparsable-with-libyaml plugins/inventory/nmap.py yamllint:unparsable-with-libyaml plugins/inventory/scaleway.py yamllint:unparsable-with-libyaml plugins/inventory/virtualbox.py yamllint:unparsable-with-libyaml +plugins/lookup/binary_file.py validate-modules:invalid-documentation plugins/lookup/dependent.py validate-modules:unidiomatic-typecheck plugins/modules/consul_session.py validate-modules:parameter-state-invalid-choice plugins/modules/homectl.py import-3.11 # Uses deprecated stdlib library 'crypt'