From 932ba3616067007fd5e449611a34e7e3837fc8ae Mon Sep 17 00:00:00 2001 From: Brian Coca <bcoca@users.noreply.github.com> Date: Mon, 21 Dec 2020 11:20:52 -0500 Subject: [PATCH] ensure unsafe writes fallback (#70722) * Ensure we actually fallback to unsafe_writes when set to true add integration test add fix for get_url not passing the parameter from args --- lib/ansible/module_utils/basic.py | 19 ++++--- lib/ansible/modules/get_url.py | 2 +- .../integration/targets/unsafe_writes/aliases | 6 +++ .../targets/unsafe_writes/basic.yml | 53 +++++++++++++++++++ .../targets/unsafe_writes/runme.sh | 5 ++ .../module_utils/basic/test_atomic_move.py | 1 + 6 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 test/integration/targets/unsafe_writes/aliases create mode 100644 test/integration/targets/unsafe_writes/basic.yml create mode 100755 test/integration/targets/unsafe_writes/runme.sh diff --git a/lib/ansible/module_utils/basic.py b/lib/ansible/module_utils/basic.py index 6081574853b..f1cef2ff1d5 100644 --- a/lib/ansible/module_utils/basic.py +++ b/lib/ansible/module_utils/basic.py @@ -2369,8 +2369,7 @@ class AnsibleModule(object): if e.errno not in [errno.EPERM, errno.EXDEV, errno.EACCES, errno.ETXTBSY, errno.EBUSY]: # only try workarounds for errno 18 (cross device), 1 (not permitted), 13 (permission denied) # and 26 (text file busy) which happens on vagrant synced folders and other 'exotic' non posix file systems - self.fail_json(msg='Could not replace file: %s to %s: %s' % (src, dest, to_native(e)), - exception=traceback.format_exc()) + self.fail_json(msg='Could not replace file: %s to %s: %s' % (src, dest, to_native(e)), exception=traceback.format_exc()) else: # Use bytes here. In the shippable CI, this fails with # a UnicodeError with surrogateescape'd strings for an unknown @@ -2380,14 +2379,13 @@ class AnsibleModule(object): error_msg = None tmp_dest_name = None try: - tmp_dest_fd, tmp_dest_name = tempfile.mkstemp(prefix=b'.ansible_tmp', - dir=b_dest_dir, suffix=b_suffix) + tmp_dest_fd, tmp_dest_name = tempfile.mkstemp(prefix=b'.ansible_tmp', dir=b_dest_dir, suffix=b_suffix) except (OSError, IOError) as e: error_msg = 'The destination directory (%s) is not writable by the current user. Error was: %s' % (os.path.dirname(dest), to_native(e)) except TypeError: # We expect that this is happening because python3.4.x and - # below can't handle byte strings in mkstemp(). Traceback - # would end in something like: + # below can't handle byte strings in mkstemp(). + # Traceback would end in something like: # file = _os.path.join(dir, pre + name + suf) # TypeError: can't concat bytes to str error_msg = ('Failed creating tmp file for atomic move. This usually happens when using Python3 less than Python3.5. ' @@ -2431,11 +2429,12 @@ class AnsibleModule(object): self._unsafe_writes(b_tmp_dest_name, b_dest) else: self.fail_json(msg='Unable to make %s into to %s, failed final rename from %s: %s' % - (src, dest, b_tmp_dest_name, to_native(e)), - exception=traceback.format_exc()) + (src, dest, b_tmp_dest_name, to_native(e)), exception=traceback.format_exc()) except (shutil.Error, OSError, IOError) as e: - self.fail_json(msg='Failed to replace file: %s to %s: %s' % (src, dest, to_native(e)), - exception=traceback.format_exc()) + if unsafe_writes: + self._unsafe_writes(b_src, b_dest) + else: + self.fail_json(msg='Failed to replace file: %s to %s: %s' % (src, dest, to_native(e)), exception=traceback.format_exc()) finally: self.cleanup(b_tmp_dest_name) diff --git a/lib/ansible/modules/get_url.py b/lib/ansible/modules/get_url.py index 6ac370c2ef1..c29d69e5c94 100644 --- a/lib/ansible/modules/get_url.py +++ b/lib/ansible/modules/get_url.py @@ -630,7 +630,7 @@ def main(): if backup: if os.path.exists(dest): backup_file = module.backup_local(dest) - module.atomic_move(tmpsrc, dest) + module.atomic_move(tmpsrc, dest, unsafe_writes=module.params['unsafe_writes']) except Exception as e: if os.path.exists(tmpsrc): os.remove(tmpsrc) diff --git a/test/integration/targets/unsafe_writes/aliases b/test/integration/targets/unsafe_writes/aliases new file mode 100644 index 00000000000..4fb7a116401 --- /dev/null +++ b/test/integration/targets/unsafe_writes/aliases @@ -0,0 +1,6 @@ +needs/root +skip/freebsd +skip/osx +skip/macos +skip/aix +shippable/posix/group3 diff --git a/test/integration/targets/unsafe_writes/basic.yml b/test/integration/targets/unsafe_writes/basic.yml new file mode 100644 index 00000000000..b173c7f8727 --- /dev/null +++ b/test/integration/targets/unsafe_writes/basic.yml @@ -0,0 +1,53 @@ +- hosts: testhost + gather_facts: false + vars: + testudir: '{{output_dir}}/unsafe_writes_test' + testufile: '{{testudir}}/unreplacablefile.txt' + tasks: + - name: test unsafe_writes on immutable dir (file cannot be atomically replaced) + block: + - name: create target dir + file: path={{testudir}} state=directory + - name: setup test file + copy: content=ORIGINAL dest={{testufile}} + - name: make target dir immutable (cannot write to file w/o unsafe_writes) + file: path={{testudir}} state=directory attributes="+i" + become: yes + ignore_errors: true + register: madeimmutable + + - name: only run if immutable dir command worked, some of our test systems don't allow for it + when: madeimmutable is success + block: + - name: test this is actually immmutable working as we expect + file: path={{testufile}} state=absent + register: breakimmutable + ignore_errors: True + + - name: only run if reallyh immutable dir + when: breakimmutable is failed + block: + - name: test overwriting file w/o unsafe + copy: content=NEW dest={{testufile}} unsafe_writes=False + ignore_errors: true + register: copy_without + + - name: ensure we properly failed + assert: + that: + - copy_without is failed + + - name: test overwriting file with unsafe + copy: content=NEW dest={{testufile}} unsafe_writes=True + register: copy_with + + - name: ensure we properly changed + assert: + that: + - copy_with is changed + + always: + - name: remove immutable flag from dir to prevent issues with cleanup + file: path={{testudir}} state=directory attributes="-i" + ignore_errors: true + become: yes diff --git a/test/integration/targets/unsafe_writes/runme.sh b/test/integration/targets/unsafe_writes/runme.sh new file mode 100755 index 00000000000..5c37f727eea --- /dev/null +++ b/test/integration/targets/unsafe_writes/runme.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -eux + +ansible-playbook basic.yml -i ../../inventory -e "output_dir=${OUTPUT_DIR}" "$@" diff --git a/test/units/module_utils/basic/test_atomic_move.py b/test/units/module_utils/basic/test_atomic_move.py index 7bd9496eddf..bbdb0519664 100644 --- a/test/units/module_utils/basic/test_atomic_move.py +++ b/test/units/module_utils/basic/test_atomic_move.py @@ -23,6 +23,7 @@ def atomic_am(am, mocker): am.selinux_context = mocker.MagicMock() am.selinux_default_context = mocker.MagicMock() am.set_context_if_different = mocker.MagicMock() + am._unsafe_writes = mocker.MagicMock() yield am