Skip to content

Commit 371e52b

Browse files
[Enhancement][1875]zos_copy_implemented_async_support (#1953)
* Add first iteration * Keep working * Fix path * Updated modules * First iteration of solution * Add fragment and change yml * Add test case * Change removal * Remove removal * Add debug * Ensure properly clean up * Fix finally * Fix sanity and test * Fix import * Fix last import * Update changelogs/fragments/1953-zos_copy_implemented_async_support.yml Co-authored-by: Fernando Flores <[email protected]> * fix space lint * Fix positional * Fix assertion --------- Co-authored-by: Fernando Flores <[email protected]>
1 parent 7230f37 commit 371e52b

File tree

5 files changed

+205
-87
lines changed

5 files changed

+205
-87
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
minor_changes:
2+
- zos_copy - Adds support for copying in asynchronous mode inside playbooks.
3+
(https://github.com/ansible-collections/ibm_zos_core/pull/1953).

plugins/action/zos_copy.py

Lines changed: 9 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@
2828
from ansible.utils.display import Display
2929
from ansible import cli
3030

31-
from ansible_collections.ibm.ibm_zos_core.plugins.module_utils.data_set import (
32-
is_member
33-
)
34-
3531
from ansible_collections.ibm.ibm_zos_core.plugins.module_utils import encode
3632

3733
from ansible_collections.ibm.ibm_zos_core.plugins.module_utils import template
@@ -42,6 +38,8 @@
4238
class ActionModule(ActionBase):
4339
def run(self, tmp=None, task_vars=None):
4440
""" handler for file transfer operations """
41+
self._supports_async = True
42+
4543
if task_vars is None:
4644
task_vars = dict()
4745

@@ -272,20 +270,14 @@ def run(self, tmp=None, task_vars=None):
272270
encoding=encoding,
273271
)
274272
)
273+
275274
copy_res = self._execute_module(
276275
module_name="ibm.ibm_zos_core.zos_copy",
277276
module_args=task_args,
278277
task_vars=task_vars,
278+
wrap_async=self._task.async_val
279279
)
280280

281-
# Erasing all rendered Jinja2 templates from the controller.
282-
if template_dir:
283-
shutil.rmtree(template_dir, ignore_errors=True)
284-
# Remove temporary directory from remote
285-
if self.tmp_dir is not None:
286-
path = os.path.normpath(f"{self.tmp_dir}/ansible-zos-copy")
287-
self._connection.exec_command(f"rm -rf {path}*")
288-
289281
if copy_res.get("note") and not force:
290282
result["note"] = copy_res.get("note")
291283
return result
@@ -304,10 +296,13 @@ def run(self, tmp=None, task_vars=None):
304296
)
305297
if backup or backup_name:
306298
result["backup_name"] = copy_res.get("backup_name")
307-
self._remote_cleanup(dest, copy_res.get("dest_exists"), task_vars)
308299
return result
309300

310-
return _update_result(is_binary, copy_res, self._task.args, original_src)
301+
# Erasing all rendered Jinja2 templates from the controller.
302+
if template_dir:
303+
shutil.rmtree(template_dir, ignore_errors=True)
304+
305+
return copy_res
311306

312307
def _copy_to_remote(self, src, is_dir=False, ignore_stderr=False):
313308
"""Copy a file or directory to the remote z/OS system """
@@ -412,26 +407,6 @@ def _copy_to_remote(self, src, is_dir=False, ignore_stderr=False):
412407

413408
return dict(temp_path=full_temp_path)
414409

415-
def _remote_cleanup(self, dest, dest_exists, task_vars):
416-
"""Remove all files or data sets pointed to by 'dest' on the remote
417-
z/OS system. The idea behind this cleanup step is that if, for some
418-
reason, the module fails after copying the data, we want to return the
419-
remote system to its original state. Which means deleting any newly
420-
created files or data sets.
421-
"""
422-
if dest_exists is False:
423-
if "/" in dest:
424-
self._connection.exec_command("rm -rf {0}".format(dest))
425-
else:
426-
module_args = dict(name=dest, state="absent")
427-
if is_member(dest):
428-
module_args["type"] = "member"
429-
self._execute_module(
430-
module_name="ibm.ibm_zos_core.zos_data_set",
431-
module_args=module_args,
432-
task_vars=task_vars,
433-
)
434-
435410
def _exit_action(self, result, msg, failed=False):
436411
"""Exit action plugin with a message"""
437412
result.update(
@@ -448,59 +423,6 @@ def _exit_action(self, result, msg, failed=False):
448423
return result
449424

450425

451-
def _update_result(is_binary, copy_res, original_args, original_src):
452-
""" Helper function to update output result with the provided values """
453-
ds_type = copy_res.get("ds_type")
454-
src = copy_res.get("src")
455-
note = copy_res.get("note")
456-
backup_name = copy_res.get("backup_name")
457-
dest_data_set_attrs = copy_res.get("dest_data_set_attrs")
458-
updated_result = dict(
459-
dest=copy_res.get("dest"),
460-
is_binary=is_binary,
461-
changed=copy_res.get("changed"),
462-
invocation=dict(module_args=original_args),
463-
)
464-
if src:
465-
updated_result["src"] = original_src
466-
if note:
467-
updated_result["note"] = note
468-
if backup_name:
469-
updated_result["backup_name"] = backup_name
470-
if ds_type == "USS":
471-
updated_result.update(
472-
dict(
473-
gid=copy_res.get("gid"),
474-
uid=copy_res.get("uid"),
475-
group=copy_res.get("group"),
476-
owner=copy_res.get("owner"),
477-
mode=copy_res.get("mode"),
478-
state=copy_res.get("state"),
479-
size=copy_res.get("size"),
480-
)
481-
)
482-
checksum = copy_res.get("checksum")
483-
if checksum:
484-
updated_result["checksum"] = checksum
485-
if dest_data_set_attrs is not None:
486-
if len(dest_data_set_attrs) > 0:
487-
dest_data_set_attrs.pop("name")
488-
updated_result["dest_created"] = True
489-
updated_result["destination_attributes"] = dest_data_set_attrs
490-
491-
# Setting attributes to lower case to conform to docs.
492-
# Part of the change to lowercase choices in the collection involves having
493-
# a consistent interface that also returns the same values in lowercase.
494-
if "record_format" in updated_result["destination_attributes"]:
495-
updated_result["destination_attributes"]["record_format"] = updated_result["destination_attributes"]["record_format"].lower()
496-
if "space_type" in updated_result["destination_attributes"]:
497-
updated_result["destination_attributes"]["space_type"] = updated_result["destination_attributes"]["space_type"].lower()
498-
if "type" in updated_result["destination_attributes"]:
499-
updated_result["destination_attributes"]["type"] = updated_result["destination_attributes"]["type"].lower()
500-
501-
return updated_result
502-
503-
504426
def _process_boolean(arg, default=False):
505427
try:
506428
return boolean(arg)

plugins/modules/zos_copy.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3199,6 +3199,75 @@ def normalize_line_endings(src, encoding=None):
31993199
return src
32003200

32013201

3202+
def remote_cleanup(module):
3203+
"""Remove all files or data sets pointed to by 'dest' on the remote
3204+
z/OS system. The idea behind this cleanup step is that if, for some
3205+
reason, the module fails after copying the data, we want to return the
3206+
remote system to its original state. Which means deleting any newly
3207+
created files or data sets.
3208+
"""
3209+
dest = module.params.get('dest')
3210+
if "/" in dest:
3211+
if os.path.isfile(dest):
3212+
os.remove(dest)
3213+
else:
3214+
shutil.rmtree(dest)
3215+
else:
3216+
dest = data_set.extract_dsname(dest)
3217+
data_set.DataSet.ensure_absent(name=dest)
3218+
3219+
3220+
def update_result(res_args, original_args):
3221+
ds_type = res_args.get("ds_type")
3222+
src = res_args.get("src")
3223+
note = res_args.get("note")
3224+
backup_name = res_args.get("backup_name")
3225+
dest_data_set_attrs = res_args.get("dest_data_set_attrs")
3226+
updated_result = dict(
3227+
dest=res_args.get("dest"),
3228+
is_binary=original_args.get("is_binary"),
3229+
changed=res_args.get("changed"),
3230+
invocation=dict(module_args=original_args),
3231+
)
3232+
if src:
3233+
updated_result["src"] = original_args.get("src")
3234+
if note:
3235+
updated_result["note"] = note
3236+
if backup_name:
3237+
updated_result["backup_name"] = backup_name
3238+
if ds_type == "USS":
3239+
updated_result.update(
3240+
dict(
3241+
gid=res_args.get("gid"),
3242+
uid=res_args.get("uid"),
3243+
group=res_args.get("group"),
3244+
owner=res_args.get("owner"),
3245+
mode=res_args.get("mode"),
3246+
state=res_args.get("state"),
3247+
size=res_args.get("size"),
3248+
)
3249+
)
3250+
checksum = res_args.get("checksum")
3251+
if checksum:
3252+
updated_result["checksum"] = checksum
3253+
if dest_data_set_attrs is not None:
3254+
if len(dest_data_set_attrs) > 0:
3255+
dest_data_set_attrs.pop("name")
3256+
updated_result["dest_created"] = True
3257+
updated_result["destination_attributes"] = dest_data_set_attrs
3258+
3259+
# Setting attributes to lower case to conform to docs.
3260+
# Part of the change to lowercase choices in the collection involves having
3261+
# a consistent interface that also returns the same values in lowercase.
3262+
if "record_format" in updated_result["destination_attributes"]:
3263+
updated_result["destination_attributes"]["record_format"] = updated_result["destination_attributes"]["record_format"].lower()
3264+
if "space_type" in updated_result["destination_attributes"]:
3265+
updated_result["destination_attributes"]["space_type"] = updated_result["destination_attributes"]["space_type"].lower()
3266+
if "type" in updated_result["destination_attributes"]:
3267+
updated_result["destination_attributes"]["type"] = updated_result["destination_attributes"]["type"].lower()
3268+
return updated_result
3269+
3270+
32023271
def run_module(module, arg_def):
32033272
"""Initialize module
32043273
@@ -4047,9 +4116,24 @@ def main():
40474116
res_args = conv_path = None
40484117
try:
40494118
res_args, conv_path = run_module(module, arg_def)
4119+
4120+
# Verification of default tmpdir use by the collection to remove
4121+
path = str(module.tmpdir)
4122+
position = path[:-1].rfind('/')
4123+
tmp_dir = path[:position]
4124+
4125+
default_path = os.path.normpath(f"{tmp_dir}/ansible-zos-copy")
4126+
4127+
if os.path.exists(path):
4128+
shutil.rmtree(path)
4129+
elif os.path.exists(default_path):
4130+
shutil.rmtree(default_path)
4131+
4132+
res_args = update_result(res_args=res_args, original_args=module.params)
40504133
module.exit_json(**res_args)
40514134
except CopyOperationError as err:
40524135
cleanup([])
4136+
remote_cleanup(module=module)
40534137
module.fail_json(**(err.json_args))
40544138
finally:
40554139
cleanup([conv_path])

tests/functional/modules/test_zos_copy_func.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
import shutil
1919
import re
2020
import time
21+
import yaml
2122
import tempfile
2223
import subprocess
24+
from shellescape import quote
2325

2426
from ibm_zos_core.tests.helpers.volumes import Volume_Handler
2527
from ibm_zos_core.tests.helpers.dataset import get_tmp_ds_name
@@ -222,6 +224,54 @@
222224
//STDERR DD SYSOUT=*
223225
//"""
224226

227+
PLAYBOOK_ASYNC_TEST = """- hosts: zvm
228+
collections:
229+
- ibm.ibm_zos_core
230+
gather_facts: False
231+
environment:
232+
_BPXK_AUTOCVT: "ON"
233+
ZOAU_HOME: "{0}"
234+
PYTHONPATH: "{0}/lib/{2}"
235+
LIBPATH: "{0}/lib:{1}/lib:/lib:/usr/lib:."
236+
PATH: "{0}/bin:/bin:/usr/lpp/rsusr/ported/bin:/var/bin:/usr/lpp/rsusr/ported/bin:/usr/lpp/java/java180/J8.0_64/bin:{1}/bin:"
237+
_CEE_RUNOPTS: "FILETAG(AUTOCVT,AUTOTAG) POSIX(ON)"
238+
_TAG_REDIR_ERR: "txt"
239+
_TAG_REDIR_IN: "txt"
240+
_TAG_REDIR_OUT: "txt"
241+
LANG: "C"
242+
243+
tasks:
244+
- name: Create a copy
245+
zos_copy:
246+
src: /etc/profile
247+
remote_src: True
248+
force: True
249+
dest: {3}
250+
async: 50
251+
poll: 0
252+
register: copy_output
253+
254+
- name: Query async task.
255+
async_status:
256+
jid: "{{{{ copy_output.ansible_job_id }}}}"
257+
register: job_result
258+
until: job_result.finished
259+
retries: 10
260+
delay: 30
261+
262+
- name: Echo copy_output.
263+
debug:
264+
msg: "{{ job_result }}"
265+
"""
266+
267+
INVENTORY_ASYNC_TEST = """all:
268+
hosts:
269+
zvm:
270+
ansible_host: {0}
271+
ansible_ssh_private_key_file: {1}
272+
ansible_user: {2}
273+
ansible_python_interpreter: {3}"""
274+
225275
def populate_dir(dir_path):
226276
for i in range(5):
227277
with open(dir_path + "/" + "file" + str(i + 1), "w") as infile:
@@ -637,6 +687,7 @@ def test_copy_dir_to_existing_uss_dir_not_forced(ansible_zos_module):
637687
)
638688

639689
for result in copy_result.contacted.values():
690+
print(result)
640691
assert result.get("msg") is not None
641692
assert result.get("changed") is False
642693
assert "Error" in result.get("msg")
@@ -5514,3 +5565,61 @@ def test_copy_to_dataset_with_special_symbols(ansible_zos_module):
55145565
finally:
55155566
hosts.all.zos_data_set(name=src_data_set, state="absent")
55165567
hosts.all.zos_data_set(name=dest_data_set, state="absent")
5568+
5569+
5570+
def test_job_script_async(ansible_zos_module, get_config):
5571+
try:
5572+
ds_name = get_tmp_ds_name()
5573+
path = get_config
5574+
with open(path, 'r') as file:
5575+
enviroment = yaml.safe_load(file)
5576+
5577+
ssh_key = enviroment["ssh_key"]
5578+
hosts = enviroment["host"].upper()
5579+
user = enviroment["user"].upper()
5580+
python_path = enviroment["python_path"]
5581+
cut_python_path = python_path[:python_path.find('/bin')].strip()
5582+
zoau = enviroment["environment"]["ZOAU_ROOT"]
5583+
python_version = cut_python_path.split('/')[2]
5584+
5585+
playbook = tempfile.NamedTemporaryFile(delete=True)
5586+
inventory = tempfile.NamedTemporaryFile(delete=True)
5587+
5588+
os.system("echo {0} > {1}".format(
5589+
quote(PLAYBOOK_ASYNC_TEST.format(
5590+
zoau,
5591+
cut_python_path,
5592+
python_version,
5593+
ds_name
5594+
)),
5595+
playbook.name
5596+
))
5597+
5598+
os.system("echo {0} > {1}".format(
5599+
quote(INVENTORY_ASYNC_TEST.format(
5600+
hosts,
5601+
ssh_key,
5602+
user,
5603+
python_path
5604+
)),
5605+
inventory.name
5606+
))
5607+
5608+
command = "ansible-playbook -i {0} {1}".format(
5609+
inventory.name,
5610+
playbook.name
5611+
)
5612+
5613+
result = subprocess.run(
5614+
command,
5615+
capture_output=True,
5616+
shell=True,
5617+
timeout=120,
5618+
encoding='utf-8'
5619+
)
5620+
assert result.returncode == 0
5621+
assert "ok=3" in result.stdout
5622+
assert "changed=2" in result.stdout
5623+
assert result.stderr == ""
5624+
finally:
5625+
ansible_zos_module.all.zos_data_set(name=ds_name, state="absent")

0 commit comments

Comments
 (0)