From baf1ed9100803b0962554d5bbf30e0e531af9b34 Mon Sep 17 00:00:00 2001 From: jctanner Date: Wed, 2 Aug 2017 10:04:09 -0400 Subject: [PATCH] [WIP] Create preserved_copy function in basic.py to perserve file ownership. (#27344) Create preserved_copy function in basic.py to perserve file ownership. * Add a test for template preserved backup * Use a script to get the random names * bytes to strings * Remove dump of hostvars * Stop being fancy and create a testuser instead * Fix pep8 * set file attributes * Pass the correct data to set_attributes_if_different * Use -j instead -b and pass the attributes as a string instead of a list * remove debugging message * Use shell to softly set the attr Fixes #24408 --- lib/ansible/module_utils/basic.py | 38 +++++++++++- .../targets/template/tasks/backup_test.yml | 60 +++++++++++++++++++ .../targets/template/tasks/main.yml | 3 + 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 test/integration/targets/template/tasks/backup_test.yml diff --git a/lib/ansible/module_utils/basic.py b/lib/ansible/module_utils/basic.py index 94aec93974d..e4cbc6c8c3e 100644 --- a/lib/ansible/module_utils/basic.py +++ b/lib/ansible/module_utils/basic.py @@ -2355,7 +2355,7 @@ class AnsibleModule(object): backupdest = '%s.%s.%s' % (fn, os.getpid(), ext) try: - shutil.copy2(fn, backupdest) + self.preserved_copy(fn, backupdest) except (shutil.Error, IOError): e = get_exception() self.fail_json(msg='Could not make backup of %s to %s: %s' % (fn, backupdest, e)) @@ -2370,6 +2370,42 @@ class AnsibleModule(object): e = get_exception() sys.stderr.write("could not cleanup %s: %s" % (tmpfile, e)) + def preserved_copy(self, src, dest): + """Copy a file with preserved ownership, permissions and context""" + + # shutil.copy2(src, dst) + # Similar to shutil.copy(), but metadata is copied as well - in fact, + # this is just shutil.copy() followed by copystat(). This is similar + # to the Unix command cp -p. + # + # shutil.copystat(src, dst) + # Copy the permission bits, last access time, last modification time, + # and flags from src to dst. The file contents, owner, and group are + # unaffected. src and dst are path names given as strings. + + shutil.copy2(src, dest) + + # Set the context + if self.selinux_enabled(): + context = self.selinux_context(src) + self.set_context_if_different(dest, context, False) + + # chown it + try: + dest_stat = os.stat(src) + tmp_stat = os.stat(dest) + if dest_stat and (tmp_stat.st_uid != dest_stat.st_uid or tmp_stat.st_gid != dest_stat.st_gid): + os.chown(dest, dest_stat.st_uid, dest_stat.st_gid) + except OSError as e: + if e.errno != errno.EPERM: + raise + + # Set the attributes + current_attribs = self.get_file_attributes(src) + current_attribs = current_attribs.get('attr_flags', []) + current_attribs = ''.join(current_attribs) + self.set_attributes_if_different(dest, current_attribs, True) + def atomic_move(self, src, dest, unsafe_writes=False): '''atomically move src to dest, copying attributes from dest, returns true on success it uses os.rename to ensure this as it is an atomic operation, rest of the function is diff --git a/test/integration/targets/template/tasks/backup_test.yml b/test/integration/targets/template/tasks/backup_test.yml new file mode 100644 index 00000000000..eb4eff1700c --- /dev/null +++ b/test/integration/targets/template/tasks/backup_test.yml @@ -0,0 +1,60 @@ +# https://github.com/ansible/ansible/issues/24408 + +- set_fact: + t_username: templateuser1 + t_groupname: templateuser1 + +- name: create the test group + group: + name: "{{ t_groupname }}" + +- name: create the test user + user: + name: "{{ t_username }}" + group: "{{ t_groupname }}" + createhome: no + +- name: set the dest file + set_fact: + t_dest: "{{ output_dir + '/tfile_dest.txt' }}" + +- name: create the old file + file: + path: "{{ t_dest }}" + state: touch + mode: 0777 + owner: "{{ t_username }}" + group: "{{ t_groupname }}" + +- name: failsafe attr change incase underlying system does not support it + shell: chattr =j "{{ t_dest }}" + ignore_errors: True + +- name: run the template + template: + src: foo.j2 + dest: "{{ t_dest }}" + backup: True + register: t_backup_res + +- name: check the data for the backup + stat: + path: "{{ t_backup_res.backup_file }}" + register: t_backup_stats + +- name: validate result of preserved backup + assert: + that: + - 't_backup_stats.stat.mode == "0777"' + - 't_backup_stats.stat.pw_name == t_username' + - 't_backup_stats.stat.gr_name == t_groupname' + +- name: cleanup the user + user: + name: "{{ t_username }}" + state: absent + +- name: cleanup the group + user: + name: "{{ t_groupname }}" + state: absent diff --git a/test/integration/targets/template/tasks/main.yml b/test/integration/targets/template/tasks/main.yml index 74847eae1f1..2e3064306d3 100644 --- a/test/integration/targets/template/tasks/main.yml +++ b/test/integration/targets/template/tasks/main.yml @@ -369,3 +369,6 @@ that: - 'diff_result.stdout == ""' - "diff_result.rc == 0" + +# aliases file requires root for template tests so this should be safe +- include: backup_test.yml