Skip to content

[Enabler][zos_operator] Update module interface #2230

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Aug 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelogs/fragments/2230_zos_operator_interface_update.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
breaking_changes:
- zos_operator - Option ``wait_time_s`` is being deprecated in favor of ``wait_time``. New option ``time_unit`` is being added to select
seconds or centiseconds. New return value ``time_unit`` is being added. Return value ``wait_time_s`` is being deprecated in favor of ``wait_time``.
(https://github.com/ansible-collections/ibm_zos_core/pull/2230).
69 changes: 51 additions & 18 deletions plugins/modules/zos_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,26 @@
type: bool
required: false
default: false
wait_time_s:
wait_time:
description:
- Set maximum time in seconds to wait for the commands to execute.
- When set to 0, the system default is used.
- This option is helpful on a busy system requiring more time to execute
commands.
- Setting I(wait) can instruct if execution should wait the
full I(wait_time_s).
full I(wait_time).
type: int
required: false
default: 1
time_unit:
description:
- Set the C(wait_time) unit of time, which can be C(s) (seconds) or C(cs) (centiseconds).
type: str
required: false
default: s
choices:
- s
- cs
case_sensitive:
description:
- If C(true), the command will not be converted to uppercase before
Expand Down Expand Up @@ -103,11 +112,17 @@
- name: Execute operator command to show jobs, always waiting 5 seconds for response
zos_operator:
cmd: 'd a,all'
wait_time_s: 5
wait_time: 5

- name: Display the system symbols and associated substitution texts.
zos_operator:
cmd: 'D SYMBOLS'

- name: Execute an operator command to show device status and allocation wait 10 centiseconds.
zos_operator:
cmd: 'd u'
wait_time : 10
time_unit : 'cs'
"""

RETURN = r"""
Expand All @@ -125,16 +140,22 @@
sample: d u,all
elapsed:
description:
The number of seconds that elapsed waiting for the command to complete.
The number of seconds or centiseconds that elapsed waiting for the command to complete.
returned: always
type: float
sample: 51.53
wait_time_s:
wait_time:
description:
The maximum time in seconds to wait for the commands to execute.
The maximum time in the time_unit set to wait for the commands to execute.
returned: always
type: int
sample: 5
time_unit:
description:
The time unit set for wait_time.
returned: always
type: str
sample: s
content:
description:
The resulting text from the command submitted.
Expand Down Expand Up @@ -200,14 +221,16 @@
opercmd = ZOAUImportError(traceback.format_exc())


def execute_command(operator_cmd, timeout_s=1, preserve=False, *args, **kwargs):
def execute_command(operator_cmd, time_unit, timeout=1, preserve=False, *args, **kwargs):
"""
Executes an operator command.

Parameters
----------
operator_cmd : str
Command to execute.
time_unit : str
Unit of time to wait of execution of the command.
timeout : int
Time until it stops whether it finished or not.
preserve : bool
Expand All @@ -223,15 +246,20 @@ def execute_command(operator_cmd, timeout_s=1, preserve=False, *args, **kwargs):
Return code, standard output, standard error and time elapsed from start to finish.
"""
# as of ZOAU v1.3.0, timeout is measured in centiseconds, therefore:
timeout_c = 100 * timeout_s
if time_unit == "s":
timeout = 100 * timeout

start = timer()
response = opercmd.execute(operator_cmd, timeout=timeout_c, preserve=preserve, *args, **kwargs)
response = opercmd.execute(operator_cmd, timeout=timeout, preserve=preserve, *args, **kwargs)
end = timer()
rc = response.rc
stdout = response.stdout_response
stderr = response.stderr_response
elapsed = round(end - start, 2)
if time_unit == "cs":
elapsed = round((end - start) * 100, 2)
else:
elapsed = round(end - start, 2)

return rc, stdout, stderr, elapsed


Expand All @@ -252,7 +280,8 @@ def run_module():
module_args = dict(
cmd=dict(type="str", required=True),
verbose=dict(type="bool", required=False, default=False),
wait_time_s=dict(type="int", required=False, default=1),
wait_time=dict(type="int", required=False, default=1),
time_unit=dict(type="str", required=False, choices=["s", "cs"], default="s"),
case_sensitive=dict(type="bool", required=False, default=False),
)

Expand Down Expand Up @@ -294,7 +323,8 @@ def run_module():
# call is returned from run_operator_command, specifying what was run.
# result["cmd"] = new_params.get("cmd")
result["cmd"] = rc_message.get("call")
result["wait_time_s"] = new_params.get("wait_time_s")
result["wait_time"] = new_params.get("wait_time")
result["time_unit"] = new_params.get("time_unit")
result["changed"] = False

# rc=0, something succeeded (the calling script ran),
Expand All @@ -309,7 +339,8 @@ def run_module():
module.fail_json(msg=("A non-zero return code was received : {0}. Review the response for more details.").format(result["rc"]),
cmd=result["cmd"],
elapsed_time=result["elapsed"],
wait_time_s=result["wait_time_s"],
wait_time=result["wait_time"],
time_unit=result["time_unit"],
stderr=str(error) if error is not None else result["content"],
stderr_lines=str(error).splitlines() if error is not None else result["content"],
changed=result["changed"],)
Expand Down Expand Up @@ -338,9 +369,10 @@ def parse_params(params):
"""
arg_defs = dict(
cmd=dict(arg_type="str", required=True),
verbose=dict(arg_type="bool", required=False),
wait_time_s=dict(arg_type="int", required=False),
case_sensitive=dict(arg_type="bool", required=False),
verbose=dict(arg_type="bool", required=False, default=False),
wait_time=dict(arg_type="int", required=False, default=1),
time_unit=dict(type="str", required=False, choices=["s", "cs"], default="s"),
case_sensitive=dict(arg_type="bool", required=False, default=False),
)
parser = BetterArgParser(arg_defs)
new_params = parser.parse_args(params)
Expand Down Expand Up @@ -369,7 +401,8 @@ def run_operator_command(params):
kwargs.update({"verbose": True})
kwargs.update({"debug": True})

wait_s = params.get("wait_time_s")
wait_time = params.get("wait_time")
time_unit = params.get("time_unit")
cmdtxt = params.get("cmd")
preserve = params.get("case_sensitive")

Expand All @@ -381,7 +414,7 @@ def run_operator_command(params):
kwargs.update({"wait": True})

args = []
rc, stdout, stderr, elapsed = execute_command(cmdtxt, timeout_s=wait_s, preserve=preserve, *args, **kwargs)
rc, stdout, stderr, elapsed = execute_command(cmdtxt, time_unit=time_unit, timeout=wait_time, preserve=preserve, *args, **kwargs)

if rc > 0:
message = "\nOut: {0}\nErr: {1}\nRan: {2}".format(stdout, stderr, cmdtxt)
Expand Down
103 changes: 85 additions & 18 deletions tests/functional/modules/test_zos_operator_func.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
- name: zos_operator
zos_operator:
cmd: 'd a,all'
wait_time_s: 3
wait_time: 3
verbose: true
register: output

Expand Down Expand Up @@ -77,22 +77,42 @@ def test_zos_operator_various_command(ansible_zos_module):
hosts = ansible_zos_module
results = hosts.all.zos_operator(cmd=command)
for result in results.contacted.values():
assert result["rc"] == expected_rc
print(result)
assert result.get("rc") == expected_rc
assert result.get("changed") is changed
assert result.get("msg", False) is False
assert result.get("cmd") == command
assert result.get("elapsed") is not None
assert result.get("wait_time") is not None
assert result.get("time_unit") == "s"
assert result.get("content") is not None


def test_zos_operator_invalid_command(ansible_zos_module):
hosts = ansible_zos_module
results = hosts.all.zos_operator(cmd="invalid,command", verbose=False)
for result in results.contacted.values():
print(result)
assert result.get("changed") is True
assert result.get("rc") == 0
assert result.get("cmd") is not None
assert result.get("elapsed") is not None
assert result.get("wait_time") is not None
assert result.get("time_unit") == "s"
assert result.get("content") is not None


def test_zos_operator_invalid_command_to_ensure_transparency(ansible_zos_module):
hosts = ansible_zos_module
results = hosts.all.zos_operator(cmd="DUMP COMM=('ERROR DUMP')", verbose=False)
for result in results.contacted.values():
assert result.get("changed") is True
assert result.get("rc") == 0
assert result.get("cmd") is not None
assert result.get("elapsed") is not None
assert result.get("wait_time") is not None
assert result.get("time_unit") == "s"
assert result.get("content") is not None
transparency = False
if any('DUMP COMMAND' in str for str in result.get("content")):
transparency = True
Expand All @@ -103,17 +123,27 @@ def test_zos_operator_positive_path(ansible_zos_module):
hosts = ansible_zos_module
results = hosts.all.zos_operator(cmd="d u,all", verbose=False)
for result in results.contacted.values():
assert result["rc"] == 0
assert result.get("rc") == 0
assert result.get("changed") is True
assert result.get("msg", False) is False
assert result.get("cmd") is not None
assert result.get("elapsed") is not None
assert result.get("wait_time") is not None
assert result.get("time_unit") == "s"
assert result.get("content") is not None


def test_zos_operator_positive_path_verbose(ansible_zos_module):
hosts = ansible_zos_module
results = hosts.all.zos_operator(cmd="d u,all", verbose=True)
for result in results.contacted.values():
assert result["rc"] == 0
assert result.get("rc") == 0
assert result.get("changed") is True
assert result.get("msg", False) is False
assert result.get("cmd") is not None
assert result.get("elapsed") is not None
assert result.get("wait_time") is not None
assert result.get("time_unit") == "s"
assert result.get("content") is not None
# Traverse the content list for a known verbose keyword and track state
is_verbose = False
Expand All @@ -127,45 +157,55 @@ def test_zos_operator_positive_verbose_with_full_delay(ansible_zos_module):
hosts = ansible_zos_module
wait_time = 10
results = hosts.all.zos_operator(
cmd="RO *ALL,LOG 'dummy syslog message'", verbose=True, wait_time_s=wait_time
cmd="RO *ALL,LOG 'dummy syslog message'", verbose=True, wait_time=wait_time
)

for result in results.contacted.values():
assert result["rc"] == 0
assert result.get("rc") == 0
assert result.get("changed") is True
assert result.get("content") is not None
assert result.get("msg", False) is False
assert result.get("cmd") is not None
assert result.get("elapsed") > wait_time
assert result.get("wait_time") is not None
assert result.get("time_unit") == "s"
assert result.get("content") is not None


def test_zos_operator_positive_verbose_with_quick_delay(ansible_zos_module):
hosts = ansible_zos_module
wait_time_s=10
wait_time=10
results = hosts.all.zos_operator(
cmd="d u,all", verbose=True, wait_time_s=wait_time_s
cmd="d u,all", verbose=True, wait_time=wait_time
)

for result in results.contacted.values():
assert result["rc"] == 0
assert result.get("rc") == 0
assert result.get("changed") is True
assert result.get("msg", False) is False
assert result.get("cmd") is not None
assert result.get("elapsed") <= (2 * wait_time)
assert result.get("wait_time") is not None
assert result.get("time_unit") == "s"
assert result.get("content") is not None
# Account for slower network
assert result.get('elapsed') <= (2 * wait_time_s)


def test_zos_operator_positive_verbose_blocking(ansible_zos_module):
hosts = ansible_zos_module
if is_zoau_version_higher_than(hosts,"1.2.4.5"):
wait_time_s=5
wait_time=5
results = hosts.all.zos_operator(
cmd="d u,all", verbose=True, wait_time_s=wait_time_s
cmd="d u,all", verbose=True, wait_time=wait_time
)

for result in results.contacted.values():
assert result["rc"] == 0
assert result.get("rc") == 0
assert result.get("changed") is True
assert result.get("msg", False) is False
assert result.get("cmd") is not None
assert result.get("elapsed") >= wait_time
assert result.get("wait_time") is not None
assert result.get("time_unit") == "s"
assert result.get("content") is not None
# Account for slower network
assert result.get('elapsed') >= wait_time_s


def test_zos_operator_positive_path_preserve_case(ansible_zos_module):
Expand All @@ -178,8 +218,13 @@ def test_zos_operator_positive_path_preserve_case(ansible_zos_module):
)

for result in results.contacted.values():
assert result["rc"] == 0
assert result.get("rc") == 0
assert result.get("changed") is True
assert result.get("msg", False) is False
assert result.get("cmd") is not None
assert result.get("wait_time") is not None
assert result.get("elapsed") is not None
assert result.get("time_unit") == "s"
assert result.get("content") is not None
# Making sure the output from opercmd logged the command
# exactly as it was written.
Expand All @@ -193,12 +238,34 @@ def test_response_come_back_complete(ansible_zos_module):
res = {}
res["stdout"] = []
for result in results.contacted.values():
assert result.get("rc") == 0
assert result.get("changed") is True
assert result.get("msg", False) is False
assert result.get("cmd") is not None
assert result.get("wait_time") is not None
assert result.get("elapsed") is not None
assert result.get("time_unit") == "s"
assert result.get("content") is not None
stdout = result.get('content')
# HASP646 Only appears in the last line that before did not appears
last_line = len(stdout)
assert "HASP646" in stdout[last_line - 1]


def test_operator_sentiseconds(ansible_zos_module):
hosts = ansible_zos_module
results = hosts.all.zos_operator(cmd="d a", time_unit="cs", wait_time=100)
for result in results.contacted.values():
assert result.get("rc") == 0
assert result.get("changed") is True
assert result.get("msg", False) is False
assert result.get("cmd") is not None
assert result.get("elapsed") is not None
assert result.get("wait_time") is not None
assert result.get("time_unit") == "cs"
assert result.get("content") is not None


def test_zos_operator_parallel_terminal(get_config):
path = get_config
with open(path, 'r') as file:
Expand Down