From d483d646eb1af1e4702b6797525fb439ac9e95ee Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 30 Jul 2018 19:42:49 -0700 Subject: [PATCH] Normalize config from environment as text strings On Python3, these would be text strings already. On Python2, we need to convert them from bytes. Use a new helper function py3compat to do this. Fixes #43207 --- .../fix-config-from-environment.yaml | 5 ++ lib/ansible/config/manager.py | 6 +- lib/ansible/utils/py3compat.py | 64 +++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 changelogs/fragments/fix-config-from-environment.yaml create mode 100644 lib/ansible/utils/py3compat.py diff --git a/changelogs/fragments/fix-config-from-environment.yaml b/changelogs/fragments/fix-config-from-environment.yaml new file mode 100644 index 0000000000..b86272149d --- /dev/null +++ b/changelogs/fragments/fix-config-from-environment.yaml @@ -0,0 +1,5 @@ +--- +bugfixes: +- On Python2, loading config values from environment variables could lead to + a traceback if there were nonascii characters present. Converted them to + text strings so that no traceback will occur (https://github.com/ansible/ansible/pull/43468) diff --git a/lib/ansible/config/manager.py b/lib/ansible/config/manager.py index c308c3810d..59a3de5745 100644 --- a/lib/ansible/config/manager.py +++ b/lib/ansible/config/manager.py @@ -28,6 +28,8 @@ from ansible.module_utils.parsing.convert_bool import boolean from ansible.parsing.quoting import unquote from ansible.utils.path import unfrackpath from ansible.utils.path import makedirs_safe +from ansible.utils import py3compat + Plugin = namedtuple('Plugin', 'name type') Setting = namedtuple('Setting', 'name value origin type') @@ -315,7 +317,7 @@ class ConfigManager(object): def get_config_value_and_origin(self, config, cfile=None, plugin_type=None, plugin_name=None, keys=None, variables=None, direct=None): ''' Given a config key figure out the actual value and report on the origin of the settings ''' - + 1/0 if cfile is None: # use default config cfile = self._config_file @@ -351,7 +353,7 @@ class ConfigManager(object): # env vars are next precedence if value is None and defs[config].get('env'): - value, origin = self._loop_entries(os.environ, defs[config]['env']) + value, origin = self._loop_entries(py3compat.environ, defs[config]['env']) origin = 'env: %s' % origin # try config file entries next, if we have one diff --git a/lib/ansible/utils/py3compat.py b/lib/ansible/utils/py3compat.py new file mode 100644 index 0000000000..ad6a890904 --- /dev/null +++ b/lib/ansible/utils/py3compat.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# +# (c) 2018, Toshio Kuratomi +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Note that the original author of this, Toshio Kuratomi, is trying to submit this to six. If +# successful, the code in six will be available under six's more liberal license: +# https://mail.python.org/pipermail/python-porting/2018-July/000539.html + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import sys +from collections import MutableMapping + +from ansible.module_utils.six import PY3 +from ansible.module_utils._text import to_bytes, to_text + +__all__ = ('environ',) + + +class _TextEnviron(MutableMapping): + """ + Utility class to return text strings from the environment instead of byte strings + + Mimics the behaviour of os.environ on Python3 + """ + def __init__(self, env=None): + if env is None: + env = os.environ + self._raw_environ = env + self._value_cache = {} + # Since we're trying to mimic Python3's os.environ, use sys.getfilesystemencoding() + # instead of utf-8 + self.encoding = sys.getfilesystemencoding() + + def __delitem__(self, key): + del self._raw_environ[key] + + def __getitem__(self, key): + value = self._raw_environ[key] + if PY3: + return value + # Cache keys off of the undecoded values to handle any environment variables which change + # during a run + if value not in self._value_cache: + self._value_cache[value] = to_text(value, encoding=self.encoding, + nonstring='passthru', errors='surrogate_or_strict') + return self._value_cache[value] + + def __setitem__(self, key, value): + self._raw_environ[key] = to_bytes(value, encoding=self.encoding, nonstring='strict', + errors='surrogate_or_strict') + + def __iter__(self): + return self._raw_environ.__iter__() + + def __len__(self): + return len(self._raw_environ) + + +environ = _TextEnviron()