Skip to content

Commit fbf9109

Browse files
[zos_script][zos_copy] Support automatic removal of carriage return characters (CR) when copying files (#1954)
* Changes related to removing carriage return from file while copying to USS * Update zos_copy.py * Update zos_copy.py * Adding new method just to remove CR * Update zos_copy.py * Update test_zos_copy_func.py * Create 1954-zos_copy-cr-removal-while-copying-file-to-remote.yml * Updating comment * Resolving PR comments * Updating chunk size to 32 MB * Update zos_copy.py * adding logging for lock testcase * Update test_zos_copy_func.py * Update zos_copy.py * removing extra method * Update zos_copy.py * Fixed sanity issues * Updated script --------- Co-authored-by: Fernando Flores <[email protected]>
1 parent 10a5b0c commit fbf9109

File tree

3 files changed

+107
-8
lines changed

3 files changed

+107
-8
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
minor_changes:
2+
- zos_script - Support automatic removal of carriage return line breaks [CR, CRLF] when copying local files to USS.
3+
(https://github.com/ansible-collections/ibm_zos_core/pull/1954).
4+
bugfixes:
5+
- zos_copy - the carriage return characters were being removed from only first 1024 bytes of a file. Now fixed that issue to support
6+
removal of the carriage return characters from the complete file content if the file size is more than 1024 bytes.
7+
(https://github.com/ansible-collections/ibm_zos_core/pull/1954).

plugins/modules/zos_copy.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,8 +1437,8 @@ def file_has_crlf_endings(self, src):
14371437
content = src_file.read(1024)
14381438

14391439
while content:
1440-
# In EBCDIC, \r\n are bytes 0d and 15, respectively.
1441-
if b'\x0d\x15' in content:
1440+
# In EBCDIC, \r is bytes 0d
1441+
if b'\x0d' in content:
14421442
return True
14431443
content = src_file.read(1024)
14441444

@@ -1466,12 +1466,15 @@ def create_temp_with_lf_endings(self, src):
14661466
try:
14671467
fd, converted_src = tempfile.mkstemp(dir=os.environ['TMPDIR'])
14681468
os.close(fd)
1469-
1469+
# defining 32 MB chunk size for reading large files efficiently
1470+
chunk_size = 32 * 1024 * 1024
14701471
with open(converted_src, "wb") as converted_file:
14711472
with open(src, "rb") as src_file:
1472-
chunk = src_file.read(1024)
1473-
# In IBM-037, \r is the byte 0d.
1474-
converted_file.write(chunk.replace(b'\x0d', b''))
1473+
chunk = src_file.read(chunk_size)
1474+
while chunk:
1475+
# In IBM-037, \r is the byte 0d.
1476+
converted_file.write(chunk.replace(b'\x0d', b''))
1477+
chunk = src_file.read(chunk_size)
14751478

14761479
self._tag_file_encoding(converted_src, "IBM-037")
14771480

@@ -1482,6 +1485,45 @@ def create_temp_with_lf_endings(self, src):
14821485
stderr=to_native(err)
14831486
)
14841487

1488+
def remove_cr_endings(self, src):
1489+
"""Creates a temporary file with the same content as src but without
1490+
carriage returns.
1491+
1492+
Parameters
1493+
----------
1494+
src : str
1495+
Path to a USS source file.
1496+
1497+
Returns
1498+
-------
1499+
str
1500+
Path to the temporary file created.
1501+
1502+
Raises
1503+
------
1504+
CopyOperationError
1505+
If the conversion fails.
1506+
"""
1507+
try:
1508+
fd, converted_src = tempfile.mkstemp(dir=os.environ['TMPDIR'])
1509+
os.close(fd)
1510+
# defining 32 MB chunk size for reading large files efficiently
1511+
chunk_size = 32 * 1024 * 1024
1512+
with open(converted_src, "wb") as converted_file:
1513+
with open(src, "rb") as src_file:
1514+
chunk = src_file.read(chunk_size)
1515+
while chunk:
1516+
# In IBM-037, \r is the byte 0d.
1517+
converted_file.write(chunk.replace(b'\x0d', b''))
1518+
chunk = src_file.read(chunk_size)
1519+
1520+
return converted_src
1521+
except Exception as err:
1522+
raise CopyOperationError(
1523+
msg="Error while trying to convert EOL sequence for source.",
1524+
stderr=to_native(err)
1525+
)
1526+
14851527

14861528
class USSCopyHandler(CopyHandler):
14871529
def __init__(
@@ -3670,6 +3712,11 @@ def run_module(module, arg_def):
36703712
# Copy to USS file or directory
36713713
# ---------------------------------------------------------------------
36723714
if is_uss:
3715+
# Removing the carriage return characters
3716+
if src_ds_type == "USS" and not is_binary and not executable:
3717+
new_src = conv_path or src
3718+
if os.path.isfile(new_src):
3719+
conv_path = copy_handler.remove_cr_endings(new_src)
36733720
uss_copy_handler = USSCopyHandler(
36743721
module,
36753722
is_binary=is_binary,

tests/functional/modules/test_zos_script_func.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,20 @@ def create_local_file(content, suffix):
148148

149149
return file_path
150150

151+
def create_local_file_with_carriagereturn(content, suffix):
152+
"""Creates a tempfile that has the given content."""
153+
154+
fd, file_path = tempfile.mkstemp(
155+
prefix='zos_script',
156+
suffix=suffix
157+
)
158+
os.close(fd)
159+
160+
with open(file_path, 'w', encoding="utf-8", newline='\r\n') as f:
161+
f.write(content)
162+
163+
return file_path
164+
151165

152166
def test_rexx_script_without_args(ansible_zos_module):
153167
hosts = ansible_zos_module
@@ -604,6 +618,37 @@ def test_rexx_script_with_args_remote_src(ansible_zos_module):
604618
hosts.all.file(path=script_path, state="absent")
605619

606620

621+
def test_rexx_script_with_args_and_carriagereturn(ansible_zos_module):
622+
hosts = ansible_zos_module
623+
script_path = ''
624+
try:
625+
rexx_script = REXX_SCRIPT_ARGS
626+
script_path = create_local_file_with_carriagereturn(rexx_script, 'rexx')
627+
628+
first_arg = 'one'
629+
second_arg = 'two'
630+
args = f'FIRST={first_arg} SECOND={second_arg}'
631+
cmd = f"{script_path} '{args}'"
632+
633+
zos_script_result = hosts.all.zos_script(
634+
cmd=cmd
635+
)
636+
637+
for result in zos_script_result.contacted.values():
638+
assert result.get('changed') is True
639+
assert result.get('failed', False) is False
640+
assert result.get('rc') == 0
641+
assert first_arg in result.get('stdout', '')
642+
assert second_arg in result.get('stdout', '')
643+
# Making sure the action plugin passed every argument to the module.
644+
assert args in result.get('invocation').get('module_args').get('cmd')
645+
assert args in result.get('remote_cmd')
646+
assert result.get('stderr', '') == ''
647+
finally:
648+
if os.path.exists(script_path):
649+
os.remove(script_path)
650+
651+
607652
def test_job_script_async(get_config):
608653
# Creating temp REXX file used by the playbook.
609654
try:
@@ -632,7 +677,7 @@ def test_job_script_async(get_config):
632677
cut_python_path,
633678
python_version,
634679
script_path
635-
)),
680+
)),
636681
playbook.name
637682
))
638683

@@ -642,7 +687,7 @@ def test_job_script_async(get_config):
642687
ssh_key,
643688
user,
644689
python_path
645-
)),
690+
)),
646691
inventory.name
647692
))
648693

0 commit comments

Comments
 (0)