Skip to content

Commit 0f87f3b

Browse files
[Enhancement] [zos_script] Implement async support for zos_script (#1934)
* adding async support to zos_script * Update test_zos_script_func.py * Update test_zos_script_func.py * Update test_zos_script_func.py * Update test_zos_script_func.py * Update test_zos_script_func.py * Update test_zos_script_func.py * Update test_zos_script_func.py * Update test_zos_script_func.py * Update test_zos_script_func.py * Update test_zos_script_func.py * Update test_zos_script_func.py * Updating tmp file delete code * Moving tmp file removal logic to module from action * Update test_zos_script_func.py * added change log * Updating removing tmp file in previous test case * updating indentation * Update zos_script.py * Update changelogs/fragments/1934-zos_script-implemented-async-support Co-authored-by: Alex Moreno <[email protected]> --------- Co-authored-by: Alex Moreno <[email protected]>
1 parent 98312ba commit 0f87f3b

File tree

4 files changed

+206
-82
lines changed

4 files changed

+206
-82
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_script - Adds support for running local and remote scripts in asynchronous mode inside playbooks.
3+
(https://github.com/ansible-collections/ibm_zos_core/pull/1934).

plugins/action/zos_script.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
class ActionModule(ActionBase):
2626
def run(self, tmp=None, task_vars=None):
27+
self._supports_async = True
2728
if task_vars is None:
2829
task_vars = dict()
2930

@@ -99,6 +100,8 @@ def run(self, tmp=None, task_vars=None):
99100
)
100101
copy_task = self._task.copy()
101102
copy_task.args = copy_module_args
103+
# Making the zos_copy task run synchronously every time.
104+
copy_task.async_val = 0
102105
copy_action = self._shared_loader_obj.action_loader.get(
103106
'ibm.ibm_zos_core.zos_copy',
104107
task=copy_task,
@@ -133,7 +136,8 @@ def run(self, tmp=None, task_vars=None):
133136
module_result = self._execute_module(
134137
module_name='ibm.ibm_zos_core.zos_script',
135138
module_args=module_args,
136-
task_vars=task_vars
139+
task_vars=task_vars,
140+
wrap_async=self._task.async_val
137141
)
138142

139143
result = module_result
@@ -143,16 +147,8 @@ def run(self, tmp=None, task_vars=None):
143147
# restore it to what the user supplied.
144148
result['cmd'] = user_cmd
145149

146-
if not remote_src:
147-
self._remote_cleanup(tempfile_path)
148-
149150
return result
150151

151-
def _remote_cleanup(self, tempfile_path):
152-
"""Removes the temporary file in a managed node created for a local
153-
script."""
154-
self._connection.exec_command("rm -f {0}".format(tempfile_path))
155-
156152
def _process_boolean(self, arg, default=False):
157153
try:
158154
return boolean(arg)

plugins/modules/zos_script.py

Lines changed: 87 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@
227227
import os
228228
import stat
229229
import shlex
230+
import os
231+
from ansible.module_utils._text import to_text
230232

231233
from ansible.module_utils.basic import AnsibleModule
232234
from ansible_collections.ibm.ibm_zos_core.plugins.module_utils import (
@@ -340,84 +342,101 @@ def run_module():
340342
removes = module.params.get('removes')
341343
remote_src = module.params.get('remote_src')
342344
script_permissions = None
345+
temp_file = script_path if not remote_src else None
343346

344-
if creates and os.path.exists(creates):
345-
result = dict(
346-
changed=False,
347-
skipped=True,
348-
msg='File {0} already exists on the system, skipping script'.format(creates)
349-
)
350-
module.exit_json(**result)
347+
result = dict()
351348

352-
if removes and not os.path.exists(removes):
353-
result = dict(
354-
changed=False,
355-
skipped=True,
356-
msg='File {0} is already missing on the system, skipping script'.format(removes)
357-
)
358-
module.exit_json(**result)
349+
try:
350+
if creates and os.path.exists(creates):
351+
result = dict(
352+
changed=False,
353+
skipped=True,
354+
msg='File {0} already exists on the system, skipping script'.format(creates)
355+
)
356+
module.exit_json(**result)
359357

360-
if chdir and not os.path.exists(chdir):
361-
module.fail_json(
362-
msg='The given chdir {0} does not exist on the system.'.format(chdir)
363-
)
358+
if removes and not os.path.exists(removes):
359+
result = dict(
360+
changed=False,
361+
skipped=True,
362+
msg='File {0} is already missing on the system, skipping script'.format(removes)
363+
)
364+
module.exit_json(**result)
364365

365-
if remote_src and not os.path.exists(script_path):
366-
result = dict(
367-
changed=False,
368-
skipped=True,
369-
msg='File {0} does not exist on the system, skipping script'.format(script_path)
370-
)
371-
module.fail_json(**result)
372-
373-
# Checking if current user has permission to execute the script.
374-
# If not, we'll try to set execution permissions if possible.
375-
if not os.access(script_path, os.X_OK):
376-
# Adding owner execute permissions to the script.
377-
# The module will fail if the Ansible user is not the owner!
378-
try:
379-
script_permissions = os.lstat(script_path).st_mode
380-
os.chmod(
381-
script_path,
382-
script_permissions | stat.S_IXUSR
366+
if chdir and not os.path.exists(chdir):
367+
msg = 'The given chdir {0} does not exist on the system.'.format(chdir)
368+
raise Exception(msg)
369+
370+
if remote_src and not os.path.exists(script_path):
371+
result = dict(
372+
changed=False,
373+
skipped=True,
374+
msg='File {0} does not exist on the system, skipping script'.format(script_path)
383375
)
384-
except PermissionError:
385-
module.fail_json(
386-
msg='User running Ansible does not have permission to run script {0}.'.format(
387-
script_path
376+
module.fail_json(**result)
377+
378+
# Checking if current user has permission to execute the script.
379+
# If not, we'll try to set execution permissions if possible.
380+
if not os.access(script_path, os.X_OK):
381+
# Adding owner execute permissions to the script.
382+
# The module will fail if the Ansible user is not the owner!
383+
try:
384+
script_permissions = os.lstat(script_path).st_mode
385+
os.chmod(
386+
script_path,
387+
script_permissions | stat.S_IXUSR
388388
)
389-
)
390-
391-
if executable:
392-
cmd_str = "{0} {1}".format(executable, cmd_str)
389+
except PermissionError:
390+
msg = 'User running Ansible does not have permission to run script {0}.'.format(script_path)
391+
raise PermissionError(msg)
392+
393+
if executable:
394+
cmd_str = "{0} {1}".format(executable, cmd_str)
395+
396+
cmd_str = cmd_str.strip()
397+
script_rc, stdout, stderr = module.run_command(
398+
cmd_str,
399+
cwd=chdir,
400+
errors='replace'
401+
)
393402

394-
cmd_str = cmd_str.strip()
395-
script_rc, stdout, stderr = module.run_command(
396-
cmd_str,
397-
cwd=chdir,
398-
errors='replace'
399-
)
403+
result = dict(
404+
changed=True,
405+
cmd=module.params.get('cmd'),
406+
remote_cmd=cmd_str,
407+
rc=script_rc,
408+
stdout=stdout,
409+
stderr=stderr,
410+
stdout_lines=stdout.split('\n'),
411+
stderr_lines=stderr.split('\n'),
412+
)
400413

401-
result = dict(
402-
changed=True,
403-
cmd=module.params.get('cmd'),
404-
remote_cmd=cmd_str,
405-
rc=script_rc,
406-
stdout=stdout,
407-
stderr=stderr,
408-
stdout_lines=stdout.split('\n'),
409-
stderr_lines=stderr.split('\n'),
410-
)
414+
# Reverting script's permissions when needed.
415+
if script_permissions:
416+
os.chmod(script_path, script_permissions)
411417

412-
# Reverting script's permissions when needed.
413-
if script_permissions:
414-
os.chmod(script_path, script_permissions)
418+
if script_rc != 0 or stderr:
419+
result['msg'] = 'The script terminated with an error'
420+
if temp_file is not None:
421+
os.remove(temp_file)
422+
module.fail_json(
423+
**result
424+
)
425+
except PermissionError as err:
426+
result["failed"] = True
427+
result["changed"] = False
428+
result["msg"] = ("The script terminated with an error: {0}".format(to_text(err)))
429+
module.exit_json(**result)
430+
except Exception as err:
431+
# if not result["changed"]:
432+
result["changed"] = False
433+
result["failed"] = True
434+
result["msg"] = ("The script terminated with an error: {0}".format(to_text(err)))
435+
module.exit_json(**result)
415436

416-
if script_rc != 0 or stderr:
417-
result['msg'] = 'The script terminated with an error'
418-
module.fail_json(
419-
**result
420-
)
437+
finally:
438+
if temp_file is not None:
439+
os.remove(temp_file)
421440

422441
module.exit_json(**result)
423442

tests/functional/modules/test_zos_script_func.py

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
import os
1919
import tempfile
2020
import pytest
21+
import yaml
22+
from shellescape import quote
23+
import subprocess
2124
__metaclass__ = type
2225

2326
from ibm_zos_core.tests.helpers.users import ManagedUserType, ManagedUser
@@ -55,6 +58,49 @@
5558
"""
5659

5760

61+
PLAYBOOK_ASYNC_TEST = """- hosts: zvm
62+
collections:
63+
- ibm.ibm_zos_core
64+
gather_facts: False
65+
environment:
66+
_BPXK_AUTOCVT: "ON"
67+
ZOAU_HOME: "{0}"
68+
PYTHONPATH: "{0}/lib/{2}"
69+
LIBPATH: "{0}/lib:{1}/lib:/lib:/usr/lib:."
70+
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:"
71+
_CEE_RUNOPTS: "FILETAG(AUTOCVT,AUTOTAG) POSIX(ON)"
72+
_TAG_REDIR_ERR: "txt"
73+
_TAG_REDIR_IN: "txt"
74+
_TAG_REDIR_OUT: "txt"
75+
LANG: "C"
76+
77+
tasks:
78+
- name: Execute script in async mode.
79+
ibm.ibm_zos_core.zos_script:
80+
cmd: "{3} FIRST=a SECOND=b"
81+
async: 45
82+
poll: 0
83+
register: job_task
84+
85+
- name: Query async task.
86+
async_status:
87+
jid: "{{{{ job_task.ansible_job_id }}}}"
88+
register: job_result
89+
until: job_result.finished
90+
retries: 20
91+
delay: 5
92+
"""
93+
94+
INVENTORY_ASYNC_TEST = """all:
95+
hosts:
96+
zvm:
97+
ansible_host: {0}
98+
ansible_ssh_private_key_file: {1}
99+
ansible_user: {2}
100+
ansible_python_interpreter: {3}"""
101+
102+
103+
58104
def create_script_content(msg, script_type):
59105
"""Returns a string containing either a valid REXX script or a valid
60106
Python script. The script will print the given message."""
@@ -424,21 +470,18 @@ def managed_user_run_script(ansible_zos_module):
424470
hosts = ansible_zos_module
425471
script_path = '/tmp/zos_script_test_script'
426472
msg = "Success"
427-
428473
zos_script_result = hosts.all.zos_script(
429474
cmd=script_path,
430475
remote_src=True
431476
)
432477

433478
for result in zos_script_result.contacted.values():
434-
print(result)
435479
assert result.get('changed') is True
436480
assert result.get('failed', False) is False
437481
assert result.get('rc') == 0
438482
assert result.get('stdout', '').strip() == msg
439483
assert result.get('stderr', '') == ''
440484

441-
442485
# Related to issue #1542 in our repository.
443486
def test_user_run_script_from_another_user(ansible_zos_module, z_python_interpreter):
444487
hosts = ansible_zos_module
@@ -556,7 +599,70 @@ def test_rexx_script_with_args_remote_src(ansible_zos_module):
556599
assert args in result.get('remote_cmd')
557600
assert result.get('stderr', '') == ''
558601
finally:
559-
if os.path.exists(script_path):
560-
os.remove(script_path)
561602
if os.path.exists(local_script):
562603
os.remove(local_script)
604+
hosts.all.file(path=script_path, state="absent")
605+
606+
607+
def test_job_script_async(get_config):
608+
# Creating temp REXX file used by the playbook.
609+
try:
610+
rexx_script = REXX_SCRIPT_ARGS
611+
script_path = create_local_file(rexx_script, 'rexx')
612+
613+
# Getting all the info required to run the playbook.
614+
path = get_config
615+
with open(path, 'r') as file:
616+
enviroment = yaml.safe_load(file)
617+
618+
ssh_key = enviroment["ssh_key"]
619+
hosts = enviroment["host"].upper()
620+
user = enviroment["user"].upper()
621+
python_path = enviroment["python_path"]
622+
cut_python_path = python_path[:python_path.find('/bin')].strip()
623+
zoau = enviroment["environment"]["ZOAU_ROOT"]
624+
python_version = cut_python_path.split('/')[2]
625+
626+
playbook = tempfile.NamedTemporaryFile(delete=True)
627+
inventory = tempfile.NamedTemporaryFile(delete=True)
628+
629+
os.system("echo {0} > {1}".format(
630+
quote(PLAYBOOK_ASYNC_TEST.format(
631+
zoau,
632+
cut_python_path,
633+
python_version,
634+
script_path
635+
)),
636+
playbook.name
637+
))
638+
639+
os.system("echo {0} > {1}".format(
640+
quote(INVENTORY_ASYNC_TEST.format(
641+
hosts,
642+
ssh_key,
643+
user,
644+
python_path
645+
)),
646+
inventory.name
647+
))
648+
649+
command = "ansible-playbook -i {0} {1}".format(
650+
inventory.name,
651+
playbook.name
652+
)
653+
654+
result = subprocess.run(
655+
command,
656+
capture_output=True,
657+
shell=True,
658+
timeout=120,
659+
encoding='utf-8'
660+
)
661+
assert result.returncode == 0
662+
assert "ok=2" in result.stdout
663+
assert "changed=2" in result.stdout
664+
assert result.stderr == ""
665+
finally:
666+
if os.path.exists(script_path):
667+
os.remove(script_path)
668+

0 commit comments

Comments
 (0)