Skip to content

Commit 2599d7f

Browse files
rexeminddimatos
andauthored
[1.5.0-beta.1] [zos_copy] Update/forward port from 1.4.0/Issues #553, #554, #560 (#626)
* Ported enhancement for 553 * Ported enhancement for 554 * Ported bugfix for #560 * Ported bugfix related to #554 This bug came about after a customer from L2 support tried the bugfix for #554. * Fixed import error in Python 2.7 * Add missing var init resulting from merge conflict Signed-off-by: ddimatos <[email protected]> --------- Signed-off-by: ddimatos <[email protected]> Co-authored-by: Demetri <[email protected]>
1 parent 30f6519 commit 2599d7f

File tree

3 files changed

+127
-14
lines changed

3 files changed

+127
-14
lines changed

docs/source/modules/zos_copy.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ dest
6868

6969
``dest`` can be a USS file, directory or MVS data set name.
7070

71-
If ``src`` and ``dest`` are files and if the parent directory of ``dest`` does not exist, then the task will fail
71+
If ``dest`` has missing parent directories, they will be created.
7272

7373
If ``dest`` is a nonexistent USS file, it will be created.
7474

plugins/modules/zos_copy.py

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,7 @@
7373
description:
7474
- The remote absolute path or data set where the content should be copied to.
7575
- C(dest) can be a USS file, directory or MVS data set name.
76-
- If C(src) and C(dest) are files and if the parent directory of C(dest)
77-
does not exist, then the task will fail
76+
- If C(dest) has missing parent directories, they will be created.
7877
- If C(dest) is a nonexistent USS file, it will be created.
7978
- If C(dest) is a nonexistent data set, it will be created following the
8079
process outlined here and in the C(volume) option.
@@ -664,7 +663,7 @@
664663
from ansible_collections.ibm.ibm_zos_core.plugins.module_utils.ansible_module import (
665664
AnsibleModuleHelper,
666665
)
667-
from ansible.module_utils._text import to_bytes
666+
from ansible.module_utils._text import to_bytes, to_native
668667
from ansible.module_utils.basic import AnsibleModule
669668
from ansible.module_utils.six import PY3
670669
from re import IGNORECASE
@@ -678,6 +677,7 @@
678677

679678
if PY3:
680679
from re import fullmatch
680+
import pathlib
681681
else:
682682
from re import match as fullmatch
683683

@@ -975,6 +975,25 @@ def copy_to_uss(
975975
src, dest, src_ds_type, src_member, member_name=member_name
976976
)
977977
else:
978+
norm_dest = os.path.normpath(dest)
979+
dest_parent_dir, tail = os.path.split(norm_dest)
980+
if PY3:
981+
path_helper = pathlib.Path(dest_parent_dir)
982+
if dest_parent_dir != "/" and not path_helper.exists():
983+
path_helper.mkdir(parents=True, exist_ok=True)
984+
else:
985+
# When using Python 2, instead of using pathlib, we'll use os.makedirs.
986+
# pathlib is not available in Python 2.
987+
try:
988+
if dest_parent_dir != "/" and not os.path.exists(dest_parent_dir):
989+
os.makedirs(dest_parent_dir)
990+
except os.error as err:
991+
# os.makedirs throws an error whether the directories were already
992+
# present or their creation failed. There's no exist_ok to tell it
993+
# to ignore the first case, so we ignore it manually.
994+
if "File exists" not in err:
995+
raise CopyOperationError(msg=to_native(err))
996+
978997
if os.path.isfile(temp_path or conv_path or src):
979998
dest = self._copy_to_file(src, dest, conv_path, temp_path)
980999
changed_files = None
@@ -1084,7 +1103,7 @@ def _copy_to_dir(
10841103
# Restoring permissions for preexisting files and subdirectories.
10851104
for filepath, permissions in original_permissions:
10861105
mode = "0{0:o}".format(stat.S_IMODE(permissions))
1087-
self.module.set_mode_if_different(os.path.join(dest_dir, filepath), mode, False)
1106+
self.module.set_mode_if_different(os.path.join(dest, filepath), mode, False)
10881107
except Exception as err:
10891108
raise CopyOperationError(
10901109
msg="Error while copying data to destination directory {0}".format(dest_dir),
@@ -1110,21 +1129,23 @@ def _get_changed_files(self, src, dest, copy_directory):
11101129
for the files and directories already present on the
11111130
destination.
11121131
"""
1113-
original_files = self._walk_uss_tree(dest) if os.path.exists(dest) else []
11141132
copied_files = self._walk_uss_tree(src)
11151133

11161134
# It's not needed to normalize the path because it was already normalized
11171135
# on _copy_to_dir.
11181136
parent_dir = os.path.basename(src) if copy_directory else ''
11191137

1120-
changed_files = [
1121-
relative_path for relative_path in copied_files
1122-
if os.path.join(parent_dir, relative_path) not in original_files
1123-
]
1138+
changed_files = []
1139+
original_files = []
1140+
for relative_path in copied_files:
1141+
if os.path.exists(os.path.join(dest, parent_dir, relative_path)):
1142+
original_files.append(relative_path)
1143+
else:
1144+
changed_files.append(relative_path)
11241145

11251146
# Creating tuples with (filename, permissions).
11261147
original_permissions = [
1127-
(filepath, os.stat(os.path.join(dest, filepath)).st_mode)
1148+
(filepath, os.stat(os.path.join(dest, parent_dir, filepath)).st_mode)
11281149
for filepath in original_files
11291150
]
11301151

@@ -2030,6 +2051,9 @@ def run_module(module, arg_def):
20302051
# If temp_path, the plugin has copied a file from the controller to USS.
20312052
if temp_path or "/" in src:
20322053
src_ds_type = "USS"
2054+
2055+
if remote_src and os.path.isdir(src):
2056+
is_src_dir = True
20332057
else:
20342058
if data_set.DataSet.data_set_exists(src_name):
20352059
if src_member and not data_set.DataSet.data_set_member_exists(src):
@@ -2055,7 +2079,17 @@ def run_module(module, arg_def):
20552079

20562080
if is_uss:
20572081
dest_ds_type = "USS"
2058-
dest_exists = os.path.exists(dest)
2082+
if src_ds_type == "USS" and not is_src_dir and (dest.endswith("/") or os.path.isdir(dest)):
2083+
src_basename = os.path.basename(src) if src else "inline_copy"
2084+
dest = os.path.normpath("{0}/{1}".format(dest, src_basename))
2085+
2086+
if dest.startswith("//"):
2087+
dest = dest.replace("//", "/")
2088+
2089+
if is_src_dir and not src.endswith("/"):
2090+
dest_exists = os.path.exists(os.path.normpath("{0}/{1}".format(dest, os.path.basename(src))))
2091+
else:
2092+
dest_exists = os.path.exists(dest)
20592093

20602094
if dest_exists and not os.access(dest, os.W_OK):
20612095
module.fail_json(msg="Destination {0} is not writable".format(dest))
@@ -2173,14 +2207,23 @@ def run_module(module, arg_def):
21732207

21742208
# Creating an emergency backup or an empty data set to use as a model to
21752209
# be able to restore the destination in case the copy fails.
2210+
emergency_backup = ""
21762211
if dest_exists and not force:
21772212
if is_uss or not data_set.DataSet.is_empty(dest_name):
21782213
use_backup = True
21792214
if is_uss:
2215+
# When copying a directory without a trailing slash,
2216+
# appending the source's base name to the backup path to
2217+
# avoid backing up the whole parent directory that won't
2218+
# be modified.
2219+
src_basename = os.path.basename(src) if src else ''
2220+
backup_dest = "{0}/{1}".format(dest, src_basename) if is_src_dir and not src.endswith("/") else dest
2221+
backup_dest = os.path.normpath(backup_dest)
21802222
emergency_backup = tempfile.mkdtemp()
2181-
emergency_backup = backup_data(dest, dest_ds_type, emergency_backup, tmphlq)
2223+
emergency_backup = backup_data(backup_dest, dest_ds_type, emergency_backup, tmphlq)
21822224
else:
2183-
emergency_backup = backup_data(dest, dest_ds_type, None, tmphlq)
2225+
if not (dest_ds_type in data_set.DataSet.MVS_PARTITIONED and src_member and not dest_member_exists):
2226+
emergency_backup = backup_data(dest, dest_ds_type, None, tmphlq)
21842227
# If dest is an empty data set, instead create a data set to
21852228
# use as a model when restoring.
21862229
else:

tests/functional/modules/test_zos_copy_func.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,29 @@ def test_copy_file_to_uss_dir(ansible_zos_module, src):
249249
hosts.all.file(path=dest_path, state="absent")
250250

251251

252+
@pytest.mark.uss
253+
def test_copy_file_to_uss_dir_missing_parents(ansible_zos_module):
254+
hosts = ansible_zos_module
255+
src = "/etc/profile"
256+
dest_dir = "/tmp/parent_dir"
257+
dest = "{0}/subdir/profile".format(dest_dir)
258+
259+
try:
260+
hosts.all.file(path=dest_dir, state="absent")
261+
copy_res = hosts.all.zos_copy(src=src, dest=dest)
262+
stat_res = hosts.all.stat(path=dest)
263+
264+
for result in copy_res.contacted.values():
265+
assert result.get("msg") is None
266+
assert result.get("changed") is True
267+
assert result.get("dest") == dest
268+
assert result.get("state") == "file"
269+
for st in stat_res.contacted.values():
270+
assert st.get("stat").get("exists") is True
271+
finally:
272+
hosts.all.file(path=dest_dir, state="absent")
273+
274+
252275
@pytest.mark.uss
253276
def test_copy_local_symlink_to_uss_file(ansible_zos_module):
254277
hosts = ansible_zos_module
@@ -1654,6 +1677,53 @@ def test_copy_multiple_data_set_members(ansible_zos_module):
16541677
hosts.all.zos_data_set(name=dest, state="absent")
16551678

16561679

1680+
@pytest.mark.pdse
1681+
def test_copy_multiple_data_set_members_in_loop(ansible_zos_module):
1682+
"""
1683+
This test case was included in case the module is called inside a loop,
1684+
issue was discovered in https://github.com/ansible-collections/ibm_zos_core/issues/560.
1685+
"""
1686+
hosts = ansible_zos_module
1687+
src = "USER.FUNCTEST.SRC.PDS"
1688+
1689+
dest = "USER.FUNCTEST.DEST.PDS"
1690+
member_list = ["MEMBER1", "ABCXYZ", "ABCASD"]
1691+
src_ds_list = ["{0}({1})".format(src, member) for member in member_list]
1692+
dest_ds_list = ["{0}({1})".format(dest, member) for member in member_list]
1693+
1694+
try:
1695+
hosts.all.zos_data_set(name=src, type="pds")
1696+
hosts.all.zos_data_set(name=dest, type="pds")
1697+
1698+
for src_member in src_ds_list:
1699+
hosts.all.shell(
1700+
cmd="decho '{0}' '{1}'".format(DUMMY_DATA, src_member),
1701+
executable=SHELL_EXECUTABLE
1702+
)
1703+
1704+
for src_member, dest_member in zip(src_ds_list, dest_ds_list):
1705+
copy_res = hosts.all.zos_copy(src=src_member, dest=dest_member, remote_src=True)
1706+
for result in copy_res.contacted.values():
1707+
assert result.get("msg") is None
1708+
assert result.get("changed") is True
1709+
assert result.get("dest") == dest_member
1710+
1711+
verify_copy = hosts.all.shell(
1712+
cmd="mls {0}".format(dest),
1713+
executable=SHELL_EXECUTABLE
1714+
)
1715+
1716+
for v_cp in verify_copy.contacted.values():
1717+
assert v_cp.get("rc") == 0
1718+
stdout = v_cp.get("stdout")
1719+
assert stdout is not None
1720+
assert len(stdout.splitlines()) == 3
1721+
1722+
finally:
1723+
hosts.all.zos_data_set(name=src, state="absent")
1724+
hosts.all.zos_data_set(name=dest, state="absent")
1725+
1726+
16571727
@pytest.mark.uss
16581728
@pytest.mark.pdse
16591729
@pytest.mark.parametrize("ds_type", ["pds", "pdse"])

0 commit comments

Comments
 (0)