diff --git a/changelogs/fragments/358-zos-data-set-support-disposition-shr.yml b/changelogs/fragments/358-zos-data-set-support-disposition-shr.yml new file mode 100644 index 000000000..4102bab0d --- /dev/null +++ b/changelogs/fragments/358-zos-data-set-support-disposition-shr.yml @@ -0,0 +1,2 @@ +minor_changes: + - zos_data_set - add force parameter to enable member delete while pdse is in use (https://github.com/ansible-collections/ibm_zos_core/pull/718). \ No newline at end of file diff --git a/plugins/module_utils/data_set.py b/plugins/module_utils/data_set.py index 2549c345c..8295a6541 100644 --- a/plugins/module_utils/data_set.py +++ b/plugins/module_utils/data_set.py @@ -1,4 +1,4 @@ -# Copyright (c) IBM Corporation 2020 +# Copyright (c) IBM Corporation 2020, 2023 # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -116,6 +116,7 @@ def ensure_present( sms_management_class=None, volumes=None, tmp_hlq=None, + force=None, ): """Creates data set if it does not already exist. @@ -171,6 +172,8 @@ def ensure_present( has GUARANTEED_SPACE=YES specified. Otherwise, the allocation will fail. Defaults to None. tmp_hlq (str, optional): High level qualifier for temporary datasets. + force (bool, optional): Used to determine behavior when performing member operations on a pdse. + Defaults to None. Returns: bool -- Indicates if changes were made. @@ -247,11 +250,11 @@ def ensure_member_present(name, replace=False): return True @staticmethod - def ensure_member_absent(name): + def ensure_member_absent(name, force=False): """Deletes provided data set member if it exists. Returns a boolean indicating if changes were made.""" if DataSet.data_set_member_exists(name): - DataSet.delete_member(name) + DataSet.delete_member(name, force) return True return False @@ -772,6 +775,7 @@ def replace( sms_management_class=None, volumes=None, tmp_hlq=None, + force=None, ): """Attempts to replace an existing data set. @@ -826,6 +830,8 @@ def replace( has GUARANTEED_SPACE=YES specified. Otherwise, the allocation will fail. Defaults to None. tmp_hlq (str, optional): High level qualifier for temporary datasets. + force (bool, optional): Used to determine behavior when performing member operations on a pdse. + Defaults to None. """ arguments = locals() DataSet.delete(name) @@ -884,6 +890,7 @@ def create( sms_management_class=None, volumes=None, tmp_hlq=None, + force=None, ): """A wrapper around zoautil_py Dataset.create() to raise exceptions on failure. @@ -940,6 +947,8 @@ def create( has GUARANTEED_SPACE=YES specified. Otherwise, the allocation will fail. Defaults to None. tmp_hlq (str, optional): High level qualifier for temporary datasets. + force (bool, optional): Used to determine behavior when performing member operations on a pdse. + Defaults to None. Raises: DatasetCreateError: When data set creation fails. """ @@ -992,7 +1001,7 @@ def create_member(name): raise DatasetMemberCreateError(name, rc) @staticmethod - def delete_member(name): + def delete_member(name, force=False): """A wrapper around zoautil_py Dataset.delete_members() to raise exceptions on failure. @@ -1002,7 +1011,7 @@ def delete_member(name): Raises: DatasetMemberDeleteError: When data set member deletion fails. """ - rc = datasets.delete_members(name) + rc = datasets.delete_members(name, force=force) if rc > 0: raise DatasetMemberDeleteError(name, rc) diff --git a/plugins/modules/zos_data_set.py b/plugins/modules/zos_data_set.py index c3a6936d7..3e7ee1700 100644 --- a/plugins/modules/zos_data_set.py +++ b/plugins/modules/zos_data_set.py @@ -43,6 +43,10 @@ - > If I(state=absent) and the data set does exist on the managed node, remove the data set, module completes successfully with I(changed=True). + - > + If I(state=absent) and I(type=MEMBER) and I(force=True), the data set + will be opened with I(DISP=SHR) such that the entire data set can be + accessed by other processes while the specified member is deleted. - > If I(state=absent) and I(volumes) is provided, and the data set is not found in the catalog, the module attempts to perform catalog using supplied @@ -247,6 +251,20 @@ that is not available, then the value C(TMPHLQ) is used. required: false type: str + force: + description: + - Specifies that the data set can be shared with others during a member + delete operation which results in the data set you are updating to be + simultaneously updated by others. + - This is helpful when a data set is being used in a long running process + such as a started task and you are wanting to delete a member. + - The I(force=True) option enables sharing of data sets through the + disposition I(DISP=SHR). + - The I(force=True) only applies to data set members when I(state=absent) + and I(type=MEMBER). + type: bool + required: false + default: false batch: description: - Batch can be used to perform operations on multiple data sets in a single module call. @@ -271,6 +289,11 @@ - > If I(state=absent) and the data set does exist on the managed node, remove the data set, module completes successfully with I(changed=True). + - > + If I(state=absent) and I(type=MEMBER) and I(force=True), the data + set will be opened with I(DISP=SHR) such that the entire data set + can be accessed by other processes while the specified member is + deleted. - > If I(state=absent) and I(volumes) is provided, and the data set is not found in the catalog, the module attempts to perform catalog using supplied @@ -467,6 +490,21 @@ type: bool required: false default: false + force: + description: + - Specifies that the data set can be shared with others during a member + delete operation which results in the data set you are updating to + be simultaneously updated by others. + - This is helpful when a data set is being used in a long running + process such as a started task and you are wanting to delete a + member. + - The I(force=True) option enables sharing of data sets through the + disposition I(DISP=SHR). + - The I(force=True) only applies to data set members when + I(state=absent) and I(type=MEMBER). + type: bool + required: false + default: false """ EXAMPLES = r""" @@ -552,6 +590,13 @@ state: absent type: MEMBER +- name: Remove a member from an existing PDS/E by opening with disposition DISP=SHR + zos_data_set: + name: someds.name.here(mydata) + state: absent + type: MEMBER + force: yes + - name: Create multiple partitioned data sets and add one or more members to each zos_data_set: batch: @@ -894,6 +939,9 @@ def perform_data_set_operations(name, state, **extra_args): """Calls functions to perform desired operations on one or more data sets. Returns boolean indicating if changes were made.""" changed = False + # passing in **extra_args forced me to modify the acceptable parameters + # for multiple functions in data_set.py including ensure_present, replace + # and create where the force parameter has no bearing. if state == "present" and extra_args.get("type") != "MEMBER": changed = DataSet.ensure_present(name, **extra_args) elif state == "present" and extra_args.get("type") == "MEMBER": @@ -901,7 +949,7 @@ def perform_data_set_operations(name, state, **extra_args): elif state == "absent" and extra_args.get("type") != "MEMBER": changed = DataSet.ensure_absent(name, extra_args.get("volumes")) elif state == "absent" and extra_args.get("type") == "MEMBER": - changed = DataSet.ensure_member_absent(name) + changed = DataSet.ensure_member_absent(name, extra_args.get("force")) elif state == "cataloged": changed = DataSet.ensure_cataloged(name, extra_args.get("volumes")) elif state == "uncataloged": @@ -1017,6 +1065,11 @@ def parse_and_validate_args(params): aliases=["volume"], dependencies=["state"], ), + force=dict( + type="bool", + required=False, + default=False, + ), ), ), # For individual data set args @@ -1086,6 +1139,11 @@ def parse_and_validate_args(params): required=False, default=None ), + force=dict( + type="bool", + required=False, + default=False, + ), mutually_exclusive=[ ["batch", "name"], # ["batch", "state"], @@ -1102,6 +1160,7 @@ def parse_and_validate_args(params): ["batch", "key_length"], # ["batch", "replace"], ["batch", "volumes"], + # ["batch", "force"], ], ) parser = BetterArgParser(arg_defs) @@ -1162,6 +1221,11 @@ def run_module(): default=False, ), volumes=dict(type="raw", required=False, aliases=["volume"]), + force=dict( + type="bool", + required=False, + default=False, + ), ), ), # For individual data set args @@ -1213,6 +1277,11 @@ def run_module(): required=False, default=None ), + force=dict( + type="bool", + required=False, + default=False + ), ) result = dict(changed=False, message="", names=[]) diff --git a/tests/functional/modules/test_zos_data_set_func.py b/tests/functional/modules/test_zos_data_set_func.py index 991ce07ca..37bdcb682 100644 --- a/tests/functional/modules/test_zos_data_set_func.py +++ b/tests/functional/modules/test_zos_data_set_func.py @@ -16,9 +16,12 @@ __metaclass__ = type import pytest +import time +import subprocess from pipes import quote from pprint import pprint + # TODO: determine if data set names need to be more generic for testcases # TODO: add additional tests to check additional data set creation parameter combinations @@ -460,6 +463,143 @@ def test_batch_data_set_and_member_creation(ansible_zos_module): hosts.all.zos_data_set(name=DEFAULT_DATA_SET_NAME, state="absent") +c_pgm="""#include +#include +#include +int main(int argc, char** argv) +{ + char dsname[ strlen(argv[1]) + 4]; + sprintf(dsname, "//'%s'", argv[1]); + FILE* member; + member = fopen(dsname, "rb,type=record"); + sleep(300); + fclose(member); + return 0; +} +""" + +call_c_jcl="""//PDSELOCK JOB MSGCLASS=A,MSGLEVEL=(1,1),NOTIFY=&SYSUID,REGION=0M +//LOCKMEM EXEC PGM=BPXBATCH +//STDPARM DD * +SH /tmp/disp_shr/pdse-lock '{0}({1})' +//STDIN DD DUMMY +//STDOUT DD SYSOUT=* +//STDERR DD SYSOUT=* +//""" + +def test_data_member_force_delete(ansible_zos_module): + MEMBER_1, MEMBER_2, MEMBER_3, MEMBER_4 = "MEM1", "MEM2", "MEM3", "MEM4" + try: + hosts = ansible_zos_module + + # set up: + # create pdse + results = hosts.all.zos_data_set(name=DEFAULT_DATA_SET_NAME, state="present", type="pdse", replace=True) + for result in results.contacted.values(): + assert result.get("changed") is True + + # add members + results = hosts.all.zos_data_set( + batch=[ + { + "name": DEFAULT_DATA_SET_NAME + "({0})".format(MEMBER_1), + "type": "member", + "state": "present", + "replace": True, + }, + { + "name": DEFAULT_DATA_SET_NAME + "({0})".format(MEMBER_2), + "type": "member", + "state": "present", + "replace": True, + }, + { + "name": DEFAULT_DATA_SET_NAME + "({0})".format(MEMBER_3), + "type": "member", + "state": "present", + "replace": True, + }, + { + "name": DEFAULT_DATA_SET_NAME + "({0})".format(MEMBER_4), + "type": "member", + "state": "present", + "replace": True, + }, + ] + ) + # ensure data set/members create successful + for result in results.contacted.values(): + assert result.get("changed") is True + + # copy/compile c program and copy jcl to hold data set lock for n seconds in background(&) + hosts.all.zos_copy(content=c_pgm, dest='/tmp/disp_shr/pdse-lock.c', force=True) + hosts.all.zos_copy( + content=call_c_jcl.format(DEFAULT_DATA_SET_NAME, MEMBER_1), + dest='/tmp/disp_shr/call_c_pgm.jcl', + force=True + ) + hosts.all.shell(cmd="xlc -o pdse-lock pdse-lock.c", chdir="/tmp/disp_shr/") + + # submit jcl + hosts.all.shell(cmd="submit call_c_pgm.jcl", chdir="/tmp/disp_shr/") + + # pause to ensure c code acquires lock + time.sleep(5) + + # non-force attempt to delete MEMBER_2 - should fail since pdse in in use. + results = hosts.all.zos_data_set( + name="{0}({1})".format(DEFAULT_DATA_SET_NAME, MEMBER_2), + state="absent", + type="MEMBER" + ) + for result in results.contacted.values(): + assert result.get("failed") is True + assert "DatasetMemberDeleteError" in result.get("msg") + + # attempt to delete MEMBER_3 with force option. + results = hosts.all.zos_data_set( + name="{0}({1})".format(DEFAULT_DATA_SET_NAME, MEMBER_3), state="absent", type="MEMBER", force=True + ) + for result in results.contacted.values(): + assert result.get("changed") is True + assert result.get("module_stderr") is None + + # attempt to delete MEMBER_4 with force option in batch mode. + results = hosts.all.zos_data_set( + batch=[ + { + "name": "{0}({1})".format(DEFAULT_DATA_SET_NAME, MEMBER_4), + "state": "absent", + "type": "MEMBER", + "force": True + } + ] + ) + for result in results.contacted.values(): + assert result.get("changed") is True + assert result.get("module_stderr") is None + + # confirm member deleted with mls -- mem1 and mem2 should be present but no mem3 and no mem4 + results = hosts.all.command(cmd="mls {0}".format(DEFAULT_DATA_SET_NAME)) + for result in results.contacted.values(): + assert MEMBER_1 in result.get("stdout") + assert MEMBER_2 in result.get("stdout") + assert MEMBER_3 not in result.get("stdout") + assert MEMBER_4 not in result.get("stdout") + + finally: + # extract pid + ps_list_res = hosts.all.shell(cmd="ps -e | grep -i 'pdse-lock'") + + # kill process - release lock - this also seems to end the job + pid = list(ps_list_res.contacted.values())[0].get('stdout').strip().split(' ')[0] + hosts.all.shell(cmd="kill 9 {0}".format(pid.strip())) + # clean up c code/object/executable files, jcl + hosts.all.shell(cmd='rm -r /tmp/disp_shr') + # remove pdse + hosts.all.zos_data_set(name=DEFAULT_DATA_SET_NAME, state="absent") + + def test_repeated_operations(ansible_zos_module): try: hosts = ansible_zos_module