Pass environment variables privately to modules

add POSIX note as windows works very diff

Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>
pull/81320/head
Brian Coca 3 years ago
parent ccfb7b1364
commit 79b2b00ed3

@ -0,0 +1,2 @@
minor_changes:
- module_environment, new keyword that adds the ability to pass environment variables for POSIX targets directly into the module instead of wrapping all task execution.

@ -987,7 +987,8 @@ def _find_module_utils(
async_timeout: int,
become_plugin: BecomeBase | None,
environment: dict[str, str],
remote_is_local: bool = False
remote_is_local: bool = False,
module_env: dict[str, str] | None = None,
) -> _BuiltModule:
"""
Given the source of the module, convert it to a Jinja2 template to insert
@ -996,6 +997,8 @@ def _find_module_utils(
module_substyle: t.Literal['binary', 'jsonargs', 'non_native_want_json', 'old', 'powershell', 'python']
module_style: t.Literal['binary', 'new', 'non_native_want_json', 'old']
module_substyle = module_style = 'old'
if module_env is None:
module_env = {}
# module_style is something important to calling code (ActionBase). It
# determines how arguments are formatted (json vs k=v) and whether
@ -1010,7 +1013,8 @@ def _find_module_utils(
# we substitute "from ansible.module_utils basic" for REPLACER
module_style = 'new'
module_substyle = 'python'
b_module_data = b_module_data.replace(REPLACER, b'from ansible.module_utils.basic import *')
replacer_header = ['from ansible.module_utils.basic import *', '', 'import os, json', '', f'os.environ.update({json.dumps(module_env)}))']
b_module_data = b_module_data.replace(REPLACER, to_bytes('\n'.join(replacer_header)))
elif NEW_STYLE_PYTHON_MODULE_RE.search(b_module_data):
module_style = 'new'
module_substyle = 'python'
@ -1197,7 +1201,7 @@ if __name__ == "__main__":
module_data=b_module_data,
module_path=module_path,
module_args=module_args,
environment=environment,
environment=environment.update(module_env),
async_timeout=async_timeout,
become_plugin=become_plugin,
substyle=module_substyle,
@ -1280,6 +1284,7 @@ def modify_module(
become_plugin=None,
environment=None,
remote_is_local=False,
moduile_env=None,
) -> _BuiltModule:
"""
Used to insert chunks of code into modules before transfer rather than
@ -1321,6 +1326,7 @@ def modify_module(
become_plugin=become_plugin,
environment=environment,
remote_is_local=remote_is_local,
module_env=module_env,
)
b_module_data = module_bits.b_module_data

@ -39,7 +39,8 @@ ignore_unreachable: Boolean that allows you to ignore task failures due to an un
loop: "Takes a list for the task to iterate over, saving each list element into the ``item`` variable (configurable via loop_control)"
loop_control: Several keys here allow you to modify/set loop behavior in a task. See :ref:`loop_control`.
max_fail_percentage: can be used to abort the run after a given percentage of hosts in the current batch has failed. This only works on linear or linear-derived strategies.
module_defaults: Specifies default parameter values for modules.
module_environment: A dictionary of environment variables to be provided for the module(s) that are executed as a result of a task. Unlike `environment` this will not affect shell or privilege escalation and is only available to subprocesses of the module itself.
module_defaults: Specifies default parameter values for modules on POSIX targets.
name: "Identifier. Can be used for documentation, or in tasks/handlers."
no_log: Boolean that controls information disclosure.
notify: "List of handlers to notify when the task returns a 'changed=True' status."

@ -408,6 +408,7 @@ class AnsibleModule(object):
self._legal_inputs = []
self._options_context = list()
self._tmpdir = None
self._module_env = {}
if add_file_common_args:
for k, v in FILE_COMMON_ARGUMENTS.items():
@ -421,9 +422,14 @@ class AnsibleModule(object):
# a known valid (LANG=C) if it's an invalid/unavailable locale
self._check_locale()
# get input from controller
self._load_params()
self._set_internal_properties()
# setup env based on module env vars if we have any
if self._module_env:
os.environ.update(self._module_env)
self.validator = ModuleArgumentSpecValidator(self.argument_spec,
self.mutually_exclusive,
self.required_together,

@ -84,6 +84,7 @@ PASS_VARS: dict[str, t.Any] = {
'ignore_unknown_opts': ('_ignore_unknown_opts', False),
'module_name': ('_name', None),
'no_log': ('no_log', False),
'module_env': ('_module_env', {}),
'remote_tmp': ('_remote_tmp', None),
'target_log_info': ('_target_log_info', None),
'selinux_special_fs': ('_selinux_special_fs', ['fuse', 'nfs', 'vboxsf', 'ramfs', '9p', 'vfat']),

@ -83,6 +83,7 @@ namespace Ansible.Basic
{ "tmpdir", "tmpdir" },
{ "verbosity", "Verbosity" },
{ "version", "AnsibleVersion" },
{ "module_env", null },
};
private List<string> passBools = new List<string>() { "check_mode", "debug", "diff", "keep_remote_files", "ignore_unknown_opts", "no_log" };
private List<string> passInts = new List<string>() { "verbosity" };

@ -699,6 +699,7 @@ class Base(FieldAttributeBase):
# flags and misc. settings
environment = FieldAttribute(isa='list', extend=True, prepend=True)
module_environment = FieldAttribute(isa='dict')
no_log = FieldAttribute(isa='bool', default=C.DEFAULT_NO_LOG)
run_once = FieldAttribute(isa='bool')
ignore_errors = FieldAttribute(isa='bool')

@ -443,6 +443,8 @@ class Task(Base, Conditional, Taggable, CollectionSearch, Notifiable, Delegatabl
return env
_post_validate_module_env = _post_validate_environment
def _post_validate_changed_when(self, attr, value, templar):
"""
changed_when is evaluated after the execution of the task is complete,

@ -320,8 +320,8 @@ class ActionBase(ABC, _AnsiblePluginInfoMixin):
environment=final_environment,
remote_is_local=bool(getattr(self._connection, '_remote_is_local', False)),
become_plugin=self._connection.become,
module_env=self._task.module_environment,
)
break
except InterpreterDiscoveryRequiredError as idre:
self._discovered_interpreter = discover_interpreter(action=self, interpreter_name=idre.interpreter_name,
@ -724,7 +724,7 @@ class ActionBase(ABC, _AnsiblePluginInfoMixin):
# of the other logic below will get run. This is fairly hacky and a
# corner case, but probably one that shows up pretty often in
# Solaris-based environments (and possibly others).
pass
display.debug(f"Ignoring auth failure on chmod: {e!r}")
else:
if res['rc'] == 0:
return remote_paths
@ -1005,6 +1005,9 @@ class ActionBase(ABC, _AnsiblePluginInfoMixin):
module_args['_ansible_tracebacks_for'] = _traceback.traceback_for()
# pass through confidential environment variables
module_args['_ansible_module_env'] = getattr(self._task, 'module_environment', {})
def _execute_module(self, module_name=None, module_args=None, tmp=None, task_vars=None, persist_files=False, delete_remote_tmp=None, wrap_async=False,
ignore_unknown_opts: bool = False):
"""

@ -684,6 +684,7 @@ class TestActionBase(unittest.TestCase):
mock_task.diff = False
mock_task.check_mode = False
mock_task.no_log = False
mock_task.module_environment = {}
# create a mock connection, so we don't actually try and connect to things
def get_option(option):

Loading…
Cancel
Save