Repository: ansible-collections/ibm_zos_core
Pull request #2073: [Enhancement] [zos_find] find migrated datasets and find different types of datasets at the same time
Author: login: surendrababuravella
Created: 2025-05-02T08:01:34Z | Merged: 2025-05-28T19:06:44Z

##### SUMMARY
Enhancements - Optionally find migrated datasets #1136 and find different types of datasets at the same time #2067


##### ISSUE TYPE
- Feature Pull Request

##### COMPONENT NAME
zos_find

##### ADDITIONAL INFORMATION
- Migrated datasets will be retrieved by comparing results of dls -m and dls -l commands.
- C(resource_type) changed from str to list to support different types of datasets at the same time.
- Handled excludes, examined params.
- Added new option migrated_type
- Added testcases and updated documentation




Diff:
diff --git a/changelogs/fragments/2073-zos_find-finding-migrated-datasets.yml b/changelogs/fragments/2073-zos_find-finding-migrated-datasets.yml
new file mode 100644
index 0000000000..b96d9ba79a
--- /dev/null
+++ b/changelogs/fragments/2073-zos_find-finding-migrated-datasets.yml
@@ -0,0 +1,6 @@
+minor_changes:
+  - zos_find
+      - Adds functionality to find migrated data sets.
+      - Adds functionality to find different types of data sets at the same time.
+    (https://github.com/ansible-collections/ibm_zos_core/pull/2073).
+    
diff --git a/plugins/modules/zos_find.py b/plugins/modules/zos_find.py
index 96ba9381c1..68319b9171 100644
--- a/plugins/modules/zos_find.py
+++ b/plugins/modules/zos_find.py
@@ -105,20 +105,41 @@
     required: false
   resource_type:
     description:
-      - The type of resource to search.
+      - The types of resources to search.
       - C(nonvsam) refers to one of SEQ, LIBRARY (PDSE), PDS, LARGE, BASIC, EXTREQ, or EXTPREF.
       - C(cluster) refers to a VSAM cluster. The C(data) and C(index) are the data and index
         components of a VSAM cluster.
       - C(gdg) refers to Generation Data Groups. The module searches based on the GDG base name.
+      - C(migrated) refers to listing migrated datasets. Only C(excludes) and C(migrated_type) options can be used along
+        with this option. The module only searches based on dataset patterns.
+    aliases:
+      - resource_types
     choices:
       - nonvsam
       - cluster
       - data
       - index
       - gdg
-    type: str
+      - migrated
+    type: list
+    elements: str
     required: false
     default: "nonvsam"
+  migrated_type:
+    description:
+      - A migrated data set related attribute, only valid when C(resource_type=migrated).
+      - If provided, will search for only those types of migrated datasets.
+    aliases:
+      - migrated_types
+    choices:
+      - nonvsam
+      - cluster
+      - data
+      - index
+    type: list
+    elements: str
+    required: false
+    default: ["cluster", "data", "index", "nonvsam"]
   volume:
     description:
       - If provided, only the data sets allocated in the specified list of
@@ -189,6 +210,8 @@
   - The time taken to execute the module is proportional to the number of data
     sets present on the system and how large the data sets are.
   - When searching for content within data sets, only non-binary content is considered.
+  - As a migrated data set's information can't be retrieved without recalling it first, other options
+    besides C(excludes) and C(migrated_type) are not supported.
 seealso:
 - module: zos_data_set
 """
@@ -213,7 +236,7 @@
   zos_find:
     patterns: 'IMS.LIB.*'
     contains: 'hello'
-    excludes: '*.TEST'
+    excludes: '.*TEST'
 
 - name: Find all members starting with characters 'TE' in a given list of PDS patterns
   zos_find:
@@ -235,16 +258,27 @@
   zos_find:
     patterns:
       - USER.*
-    resource_type: cluster
+    resource_type:
+      - 'cluster'
 
 - name: Find all Generation Data Groups starting with the word 'USER' and specific GDG attributes.
   zos_find:
     patterns:
       - USER.*
-    resource_type: gdg
+    resource_type:
+      - 'gdg'
     limit: 30
     scratch: true
     purge: true
+
+- name: Find all migrated and nonvsam data sets starting with the word 'USER'
+  zos_find:
+    patterns:
+      - USER.*
+    resource_type:
+      - 'migrated'
+    migrated_type:
+      - 'nonvsam'
 """
 
 
@@ -490,7 +524,7 @@ def pds_filter(module, pds_dict, member_patterns, excludes=None):
     return filtered_pds
 
 
-def vsam_filter(module, patterns, resource_type, age=None):
+def vsam_filter(module, patterns, vsam_types, age=None, excludes=None):
     """ Return all VSAM data sets that match any of the patterns
     in the given list of patterns.
 
@@ -511,8 +545,10 @@ def vsam_filter(module, patterns, resource_type, age=None):
     fail_json
         Non-zero return code received while executing ZOAU shell command 'vls'.
     """
-    filtered_data_sets = set()
+
+    filtered_data_sets = list()
     now = time.time()
+    examined = 0
     for pattern in patterns:
         request_details = age is not None
         rc, out, err = _vls_wrapper(pattern, details=request_details)
@@ -525,14 +561,94 @@ def vsam_filter(module, patterns, resource_type, age=None):
             if entry:
                 vsam_props = entry.split()
                 vsam_name = vsam_props[0]
-                vsam_type = vsam_name.split('.')[-1]
-                if _match_resource_type(resource_type, vsam_type):
-                    if age:
-                        if _age_filter(vsam_props[1], now, age):
-                            filtered_data_sets.add(vsam_name)
-                    else:
-                        filtered_data_sets.add(vsam_name)
-    return filtered_data_sets
+                vsam_ignore = False
+                if excludes is not None:
+                    for ex_pat in excludes:
+                        if _match_regex(module, ex_pat, vsam_name):
+                            vsam_ignore = True
+                            break
+                if not vsam_ignore:
+                    vsam_type = vsam_name.split('.')[-1]
+                    if vsam_type not in {"DATA", "INDEX"}:
+                        examined = examined + 1
+                    for type in vsam_types:
+                        if _match_resource_type(type, vsam_type):
+                            if age:
+                                if _age_filter(vsam_props[1], now, age):
+                                    filtered_data_sets.append({"name": vsam_name, "type": type})
+                            else:
+                                filtered_data_sets.append({"name": vsam_name, "type": type})
+                            break
+    return filtered_data_sets, examined
+
+
+def migrated_vsam_filter(module, patterns, vsam_types, excludes):
+    """ Return all migrated VSAM data sets that match any of the patterns
+    in the given list of patterns.
+
+    Parameters
+    ----------
+    module : AnsibleModule
+        The Ansible module object being used.
+    patterns : list[str]
+        A list of data set patterns.
+
+    Returns
+    -------
+    set[str]
+        Matched VSAM data sets.
+
+    Raises
+    ------
+    fail_json
+        Non-zero return code received while executing ZOAU shell command 'vls'.
+    """
+    filtered_data_sets = list()
+    examined = 0
+    for pattern in patterns:
+        # Fetch non-migrtated datasets
+        nonmigrated_data_sets = set()
+        nmrc, nmout, nmerr = _vls_wrapper(pattern)
+        if nmrc > 4:
+            module.fail_json(
+                msg="Non-zero return code received while executing ZOAU shell command 'vls'",
+                rc=nmrc, stdout=nmout, stderr=nmerr
+            )
+        for entry in nmout.splitlines():
+            if entry:
+                vsam_props = entry.split()
+                vsam_name = vsam_props[0]
+                for type in vsam_types:
+                    vsam_type = vsam_name.split('.')[-1]
+                    if _match_resource_type(type, vsam_type):
+                        nonmigrated_data_sets.add(vsam_name)
+        # Fetch migrated datasets
+        rc, out, err = _vls_wrapper(pattern, migrated=True)
+        if rc > 4:
+            module.fail_json(
+                msg="Non-zero return code received while executing ZOAU shell command 'vls'",
+                rc=rc, stdout=out, stderr=err
+            )
+        # Compare migrated and non-migrated datasets and create list of only migrated datasets
+        for entry in out.splitlines():
+            if entry:
+                vsam_props = entry.split()
+                vsam_name = vsam_props[0]
+                vsam_ignore = False
+                if excludes is not None:
+                    for ex_pat in excludes:
+                        if _match_regex(module, ex_pat, vsam_name):
+                            vsam_ignore = True
+                            break
+                if not vsam_ignore:
+                    if vsam_name not in nonmigrated_data_sets:
+                        vsam_type = vsam_name.split('.')[-1]
+                        if vsam_type not in {"DATA", "INDEX"}:
+                            examined = examined + 1
+                        for type in vsam_types:
+                            if _match_resource_type(type, vsam_type):
+                                filtered_data_sets.append({"name": vsam_name, "type": "MIGRATED", "migrated_resource_type": type})
+    return filtered_data_sets, examined
 
 
 def data_set_attribute_filter(
@@ -561,13 +677,16 @@ def data_set_attribute_filter(
     fail_json
         Non-zero return code received while executing ZOAU shell command 'dls'.
     """
-    filtered_data_sets = set()
+    filtered_data_sets = list()
     now = time.time()
     for ds in data_sets:
         rc, out, err = _dls_wrapper(
             ds, u_time=age is not None, size=size is not None
         )
         if rc != 0:
+            # Continue when no matching datasets are found
+            if "BGYSC1103E" in err:
+                continue
             module.fail_json(
                 msg="Non-zero return code received while executing ZOAU shell command 'dls'",
                 rc=rc, stdout=out, stderr=err
@@ -588,11 +707,11 @@ def data_set_attribute_filter(
                 size and not age and _size_filter(int(out[6]), size)
             )
         ):
-            filtered_data_sets.add(ds)
+            filtered_data_sets.append(ds)
     return filtered_data_sets
 
 
-def gdg_filter(module, data_sets, limit, empty, fifo, purge, scratch, extended):
+def gdg_filter(module, data_sets, limit, empty, fifo, purge, scratch, extended, excludes):
     """ Filter Generation Data Groups based on their attributes.
 
     Parameters
@@ -624,11 +743,14 @@ def gdg_filter(module, data_sets, limit, empty, fifo, purge, scratch, extended):
     fail_json
         Non-zero return code received while executing ZOAU shell command 'dls'.
     """
-    filtered_data_sets = set()
+    filtered_data_sets = list()
     for ds in data_sets:
         rc, out, err = _dls_wrapper(ds, data_set_type='gdg', list_details=True, json=True)
 
         if rc != 0:
+            # Continue when no matching datasets are found
+            if "BGYSC1103E" in err:
+                continue
             module.fail_json(
                 msg="Non-zero return code received while executing ZOAU shell command 'dls'",
                 rc=rc, stdout=out, stderr=err
@@ -645,13 +767,79 @@ def gdg_filter(module, data_sets, limit, empty, fifo, purge, scratch, extended):
                     gdg['scratch'] == (gdg['scratch'] if scratch is None else scratch) and
                     gdg['extended'] == (gdg['extended'] if extended is None else extended)
                 ):
-                    filtered_data_sets.add(gdg['base'])
+                    if excludes is not None:
+                        for ex_pat in excludes:
+                            if not _match_regex(module, ex_pat, gdg['base']):
+                                filtered_data_sets.append({"name": gdg['base'], "type": "GDG"})
+                    else:
+                        filtered_data_sets.append({"name": gdg['base'], "type": "GDG"})
         except Exception as e:
             module.fail_json(repr(e))
 
         return filtered_data_sets
 
 
+# TODO:
+# Use dls -m command output only when ZOAU adds volser/volume information also for datasets
+def migrated_nonvsam_filter(module, data_sets, excludes):
+    """ Filter Migrated Datasets by comparing results of dls -m and dls -l commands.
+
+    Parameters
+    ----------
+    module : AnsibleModule
+        The Ansible module object being used.
+    data_sets : set[str]
+        A set of data set names.
+
+    Returns
+    -------
+    set[str]
+        Matched Migrated datasets base names.
+
+    Raises
+    ------
+    fail_json
+        Non-zero return code received while executing ZOAU shell command 'dls'.
+    """
+    filtered_data_sets = list()
+    for ds in data_sets:
+        # Fetch active and migrated datasets
+        rc, out, err = _dls_wrapper(ds, migrated=True)
+        if rc != 0:
+            # Continue when no matching datasets are found
+            if "BGYSC1103E" in err:
+                continue
+            module.fail_json(
+                msg="Non-zero return code received while executing ZOAU shell command 'dls'",
+                rc=rc, stdout=out, stderr=err
+            )
+        try:
+            # Fetch only active datasets
+            init_filtered_data_sets = data_set_filter(
+                module,
+                None,
+                [ds]
+            )
+            active_datasets = \
+                init_filtered_data_sets.get("ps").union(set(init_filtered_data_sets['pds'].keys()))
+            # Create a result list of datasets which are existing in migrated datasets list and
+            # not existed in active datasets list
+            for line in out.splitlines():
+                ds = line.strip()
+                if ds not in active_datasets:
+                    if excludes is not None:
+                        for ex_pat in excludes:
+                            if not _match_regex(module, ex_pat, ds):
+                                filtered_data_sets.append({"name": ds, "type": "MIGRATED", "migrated_resource_type": "NONVSAM"})
+                                break
+                    else:
+                        filtered_data_sets.append({"name": ds, "type": "MIGRATED", "migrated_resource_type": "NONVSAM"})
+        except Exception as e:
+            module.fail_json(repr(e))
+
+    return filtered_data_sets
+
+
 # TODO:
 # Implement volume_filter() using "vtocls" shell command from ZOAU
 # when it becomes available.
@@ -678,13 +866,13 @@ def volume_filter(module, data_sets, volumes):
     fail_json
         Unable to retrieve VTOC information.
     """
-    filtered_data_sets = set()
+    filtered_data_sets = list()
     for volume in volumes:
         vtoc_entry = vtoc.get_volume_entry(volume)
         if vtoc_entry:
             for ds in vtoc_entry:
                 if ds.get('data_set_name') in data_sets:
-                    filtered_data_sets.add(ds.get('data_set_name'))
+                    filtered_data_sets.append(ds.get('data_set_name'))
         else:
             module.fail_json(
                 msg="Unable to retrieve VTOC information for volume {0}".format(volume)
@@ -710,7 +898,7 @@ def exclude_data_sets(module, data_set_list, excludes):
     set[str]
         The remaining data sets that have not been excluded.
     """
-    for ds in set(data_set_list):
+    for ds in list(data_set_list):
         for ex_pat in excludes:
             if _match_regex(module, ex_pat, ds):
                 data_set_list.remove(ds)
@@ -947,7 +1135,7 @@ def _dls_wrapper(
     return AnsibleModuleHelper(argument_spec={}).run_command(dls_cmd, errors='replace')
 
 
-def _vls_wrapper(pattern, details=False, verbose=False):
+def _vls_wrapper(pattern, details=False, migrated=False, verbose=False):
     """A wrapper for ZOAU 'vls' shell command.
 
     Parameters
@@ -965,8 +1153,11 @@ def _vls_wrapper(pattern, details=False, verbose=False):
         Return code, standard output and standard error.
     """
     vls_cmd = "vls"
-    if details:
-        vls_cmd += " -l"
+    if migrated:
+        vls_cmd += " -m"
+    else:
+        if details:
+            vls_cmd += " -l"
     if verbose:
         vls_cmd += " -v"
 
@@ -1051,19 +1242,38 @@ def run_module(module):
         or module.params.get('pds_patterns')
         or module.params.get('pds_pattern')
     )
-    resource_type = module.params.get('resource_type').upper()
+    resource_type = module.params.get('resource_type') or module.params.get('resource_types')
+    resource_type = [type.upper() for type in resource_type]
     volume = module.params.get('volume') or module.params.get('volumes')
     limit = module.params.get('limit')
     empty = module.params.get('empty')
     scratch = module.params.get('scratch')
     purge = module.params.get('purge')
     extended = module.params.get('extended')
+    migrated_type = module.params.get('migrated_type') or module.params.get('migrated_types')
     fifo = module.params.get('fifo')
-
+    vsam_types = {"CLUSTER", "DATA", "INDEX"}
     res_args = dict(data_sets=[])
-    filtered_data_sets = set()
-    init_filtered_data_sets = filtered_pds = dict()
-
+    examined_ds = 0
+    filtered_resource_types = set()
+    vsam_resource_types = set()
+    filtered_migrated_types = set()
+    vsam_migrated_types = set()
+
+    for type in resource_type:
+        if type in vsam_types:
+            filtered_resource_types.add("VSAM")
+            vsam_resource_types.add(type)
+        else:
+            filtered_resource_types.add(type)
+    if "MIGRATED" in resource_type:
+        migrated_type = [type.upper() for type in migrated_type]
+        for type in migrated_type:
+            if type in vsam_types:
+                filtered_migrated_types.add("VSAM")
+                vsam_migrated_types.add(type)
+            else:
+                filtered_migrated_types.add(type)
     if age:
         # convert age to days:
         m = re.match(r"^(-?\d+)(d|w|m|y)?$", age.lower())
@@ -1081,63 +1291,71 @@ def run_module(module):
             size = int(m.group(1)) * bytes_per_unit.get(m.group(2), 1)
         else:
             module.fail_json(size=size, msg="failed to process size")
-    if resource_type == "NONVSAM":
-        if contains:
-            init_filtered_data_sets = content_filter(
-                module,
-                pds_paths if pds_paths else patterns,
-                contains
-            )
-        else:
-            init_filtered_data_sets = data_set_filter(
-                module,
-                pds_paths,
-                patterns
-            )
-        if pds_paths:
-            filtered_pds = pds_filter(
-                module, init_filtered_data_sets.get("pds"), patterns, excludes=excludes
-            )
-            filtered_data_sets = set(filtered_pds.keys())
-        else:
-            filtered_data_sets = \
-                init_filtered_data_sets.get("ps").union(set(init_filtered_data_sets['pds'].keys()))
-
-        # Filter data sets by age or size
-        if size or age:
-            filtered_data_sets = data_set_attribute_filter(
-                module, filtered_data_sets, size=size, age=age, age_stamp=age_stamp
-            )
-
-        # Filter data sets by volume
-        if volume:
-            filtered_data_sets = volume_filter(module, filtered_data_sets, volume)
-
-        res_args['examined'] = init_filtered_data_sets.get("searched")
-
-    elif resource_type in ["CLUSTER", "DATA", "INDEX"]:
-        filtered_data_sets = vsam_filter(module, patterns, resource_type, age=age)
-        res_args['examined'] = len(filtered_data_sets)
-    elif resource_type == "GDG":
-        filtered_data_sets = gdg_filter(module, patterns, limit, empty, fifo, purge, scratch, extended)
-
-    # Filter out data sets that match one of the patterns in 'excludes'
-    if excludes and not pds_paths:
-        filtered_data_sets = exclude_data_sets(module, filtered_data_sets, excludes)
-
-    for ds in filtered_data_sets:
-        if resource_type == "NONVSAM":
-            members = filtered_pds.get(ds) or init_filtered_data_sets['pds'].get(ds)
-            if members:
-                res_args['data_sets'].append(
-                    dict(name=ds, members=members, type=resource_type)
+    for res_type in filtered_resource_types:
+        examined = 0
+        filtered_data_sets = list()
+        init_filtered_data_sets = filtered_pds = dict()
+        if res_type == "MIGRATED":
+            migrated_data_sets = list()
+            for mtype in filtered_migrated_types:
+                if mtype == "NONVSAM":
+                    migrated_data_sets = migrated_nonvsam_filter(module, patterns, excludes)
+                elif mtype == "VSAM":
+                    migrated_data_sets, examined = migrated_vsam_filter(module, patterns, vsam_migrated_types, excludes)
+                filtered_data_sets = filtered_data_sets + migrated_data_sets
+        if res_type == "NONVSAM":
+            if contains:
+                init_filtered_data_sets = content_filter(
+                    module,
+                    pds_paths if pds_paths else patterns,
+                    contains
                 )
             else:
-                res_args['data_sets'].append(dict(name=ds, type=resource_type))
-        else:
-            res_args['data_sets'].append(dict(name=ds, type=resource_type))
-
-    res_args['matched'] = len(filtered_data_sets)
+                init_filtered_data_sets = data_set_filter(
+                    module,
+                    pds_paths,
+                    patterns
+                )
+            if pds_paths:
+                filtered_pds = pds_filter(
+                    module, init_filtered_data_sets.get("pds"), patterns, excludes=excludes
+                )
+                filtered_data_sets = list(filtered_pds.keys())
+            else:
+                filtered_data_sets = \
+                    list(init_filtered_data_sets.get("ps").union(set(init_filtered_data_sets['pds'].keys())))
+            if excludes:
+                filtered_data_sets = exclude_data_sets(module, filtered_data_sets, excludes)
+            # Filter data sets by age or size
+            if size or age:
+                filtered_data_sets = data_set_attribute_filter(
+                    module, filtered_data_sets, size=size, age=age, age_stamp=age_stamp
+                )
+            # Filter data sets by volume
+            if volume:
+                filtered_data_sets = volume_filter(module, filtered_data_sets, volume)
+            examined = init_filtered_data_sets.get("searched")
+        elif res_type == "VSAM":
+            filtered_data_sets, examined = vsam_filter(module, patterns, vsam_resource_types, age=age, excludes=excludes)
+        elif res_type == "GDG":
+            filtered_data_sets = gdg_filter(module, patterns, limit, empty, fifo, purge, scratch, extended, excludes)
+
+        if filtered_data_sets:
+            for ds in filtered_data_sets:
+                if ds:
+                    if res_type == "NONVSAM":
+                        members = filtered_pds.get(ds) or init_filtered_data_sets['pds'].get(ds)
+                        if members:
+                            res_args['data_sets'].append(
+                                dict(name=ds, members=members, type=res_type)
+                            )
+                        else:
+                            res_args['data_sets'].append(dict(name=ds, type=res_type))
+                    else:
+                        res_args['data_sets'].append(ds)
+        examined_ds = examined_ds + examined
+    res_args['examined'] = examined_ds
+    res_args['matched'] = len(res_args['data_sets'])
     return res_args
 
 
@@ -1178,8 +1396,20 @@ def main():
                 aliases=["pds_pattern", "pds_paths"]
             ),
             resource_type=dict(
-                type="str", required=False, default="nonvsam",
-                choices=["cluster", "data", "index", "nonvsam", "gdg"]
+                type="list",
+                elements="str",
+                required=False,
+                default=["nonvsam"],
+                choices=["cluster", "data", "index", "nonvsam", "gdg", "migrated"],
+                aliases=["resource_types"]
+            ),
+            migrated_type=dict(
+                type="list",
+                required=False,
+                elements="str",
+                default=["cluster", "data", "index", "nonvsam"],
+                choices=["cluster", "data", "index", "nonvsam"],
+                aliases=["migrated_types"]
             ),
             volume=dict(
                 type="list",
@@ -1214,10 +1444,16 @@ def main():
             aliases=["pds_pattern", "pds_paths"]
         ),
         resource_type=dict(
-            arg_type="str",
+            arg_type="list",
+            required=False,
+            default=["nonvsam"],
+            aliases=["resource_types"]
+        ),
+        migrated_type=dict(
+            arg_type="list",
             required=False,
-            default="nonvsam",
-            choices=["cluster", "data", "index", "nonvsam", "gdg"]
+            default=["cluster", "data", "index", "nonvsam"],
+            aliases=["migrated_types"]
         ),
         volume=dict(arg_type="list", required=False, aliases=["volumes"]),
         limit=dict(type="int", required=False),
diff --git a/tests/functional/modules/test_zos_find_func.py b/tests/functional/modules/test_zos_find_func.py
index 66a812f946..c773223ced 100644
--- a/tests/functional/modules/test_zos_find_func.py
+++ b/tests/functional/modules/test_zos_find_func.py
@@ -18,7 +18,7 @@
 
 from ibm_zos_core.tests.helpers.dataset import get_tmp_ds_name
 
-
+import re
 import pytest
 
 # hlq used across the test suite.
@@ -40,6 +40,8 @@
     f"{TEST_SUITE_HLQ}.FIND.VSAM.FUNCTEST.FIRST"
 ]
 
+MIGRATED_DATASETS_PATTERNS = ['IMSBLD.I15STSMM.*','IMSBLD.DCC71QPP.*']
+
 DATASET_TYPES = ['seq', 'pds', 'pdse']
 
 LOCK_VSAM_JCL = """//SLEEP    JOB (T043JM,JM00,1,0,0,0),'SLEEP - JRM',CLASS=R,
@@ -91,7 +93,7 @@ def test_find_gdg_data_sets(ansible_zos_module):
 
         find_res = hosts.all.zos_find(
             patterns=[f'{TEST_SUITE_HLQ}.*.*'],
-            resource_type="gdg",
+            resource_type=["gdg"],
             limit=3,
         )
 
@@ -103,7 +105,7 @@ def test_find_gdg_data_sets(ansible_zos_module):
 
         find_res = hosts.all.zos_find(
             patterns=[f'{TEST_SUITE_HLQ}.*.*'],
-            resource_type="gdg",
+            resource_type=["gdg"],
             extended=True,
         )
 
@@ -115,7 +117,7 @@ def test_find_gdg_data_sets(ansible_zos_module):
 
         find_res = hosts.all.zos_find(
             patterns=[f'{TEST_SUITE_HLQ}.*.*'],
-            resource_type="gdg",
+            resource_type=["gdg"],
             purge=True,
             scratch=True,
         )
@@ -411,7 +413,7 @@ def test_find_vsam_pattern(ansible_zos_module, volumes_on_systems):
         # This test should find all three
         find_res = hosts.all.zos_find(
             patterns=[f'{TEST_SUITE_HLQ}.FIND.VSAM.FUNCTEST.*'],
-            resource_type='cluster'
+            resource_type=['cluster']
         )
         for val in find_res.contacted.values():
             assert len(val.get('data_sets')) == 1
@@ -420,7 +422,7 @@ def test_find_vsam_pattern(ansible_zos_module, volumes_on_systems):
 
         find_res = hosts.all.zos_find(
             patterns=[f'{TEST_SUITE_HLQ}.FIND.VSAM.FUNCTEST.*'],
-            resource_type='data'
+            resource_type=['data']
         )
         for val in find_res.contacted.values():
             assert len(val.get('data_sets')) == 1
@@ -429,12 +431,29 @@ def test_find_vsam_pattern(ansible_zos_module, volumes_on_systems):
 
         find_res = hosts.all.zos_find(
             patterns=[f'{TEST_SUITE_HLQ}.FIND.VSAM.FUNCTEST.*'],
-            resource_type='index'
+            resource_type=['data', 'cluster']
+        )
+        for val in find_res.contacted.values():
+            assert len(val.get('data_sets')) == 2
+            assert val.get('matched') == len(val.get('data_sets'))
+
+        find_res = hosts.all.zos_find(
+            patterns=[f'{TEST_SUITE_HLQ}.FIND.VSAM.FUNCTEST.*'],
+            resource_type=['index']
         )
         for val in find_res.contacted.values():
             assert len(val.get('data_sets')) == 1
             assert val.get('matched') == len(val.get('data_sets'))
             assert val.get('data_sets')[0].get("name", None) == f"{VSAM_NAMES[0]}.INDEX"
+
+        find_res = hosts.all.zos_find(
+            patterns=[f'{TEST_SUITE_HLQ}.FIND.VSAM.FUNCTEST.*'],
+            resource_type=['cluster', 'data', 'index']
+        )
+        for val in find_res.contacted.values():
+            assert len(val.get('data_sets')) == 3
+            assert val.get('matched') == len(val.get('data_sets'))
+            assert val.get('examined') == 1
     finally:
         hosts.all.zos_data_set(
             batch=[
@@ -463,12 +482,13 @@ def test_find_vsam_pattern_disp_old(ansible_zos_module, volumes_on_systems):
         hosts.all.shell(cmd=f"decho \"{LOCK_VSAM_JCL.format(VSAM_NAMES[0])}\" '{jcl_ds}'; jsub '{jcl_ds}'")
         find_res = hosts.all.zos_find(
             patterns=[f'{TEST_SUITE_HLQ}.FIND.VSAM.FUNCTEST.*'],
-            resource_type='cluster'
+            resource_type=['cluster']
         )
         for val in find_res.contacted.values():
             assert len(val.get('data_sets')) == 1
             assert val.get('matched') == len(val.get('data_sets'))
     finally:
+        hosts.all.shell(cmd=f"drm '{jcl_ds}'")
         hosts.all.zos_data_set(
             batch=[
                 {
@@ -491,7 +511,7 @@ def test_find_vsam_in_volume(ansible_zos_module, volumes_on_systems):
         find_res = hosts.all.zos_find(
             patterns=[f'{TEST_SUITE_HLQ}.FIND.VSAM.*.*'],
             volumes=[volume_1],
-            resource_type='cluster'
+            resource_type=['cluster']
         )
         for val in find_res.contacted.values():
             assert len(val.get('data_sets')) == 1
@@ -593,17 +613,14 @@ def test_find_sequential_special_data_sets_containing_single_string(ansible_zos_
     hosts = ansible_zos_module
     search_string = "hello"
     try:
-
         special_chars = ["$", "-", "@", "#"]
         special_names = [ "".join([get_tmp_ds_name(mlq_size=7, llq_size=6, symbols=True), special_chars[i]]) for i in range(4)]
         # Creates a command like  dtouch dsname &&  dtouch dsname && dtouch dsname to avoid multiple ssh calls and improve test performance
         dtouch_command = " && ".join([f"dtouch -tseq '{item}'" for item in special_names])
         hosts.all.shell(cmd=dtouch_command)
-
         # Creates a command like decho dsname && decho dsname && decho dsname to avoid multiple ssh calls and improve test performance
         decho_command = " && ".join([f"decho '{search_string}' '{item}'" for item in special_names])
         hosts.all.shell(cmd=decho_command)
-
         find_res = hosts.all.zos_find(
             patterns=[f'{TEST_SUITE_HLQ}.*.*'],
             contains=search_string
@@ -617,3 +634,184 @@ def test_find_sequential_special_data_sets_containing_single_string(ansible_zos_
     finally:
         for ds in special_names:
             hosts.all.shell(cmd=f"drm '{ds}'")
+
+
+def test_find_vsam_and_gdg_data_sets(ansible_zos_module, volumes_on_systems):
+    hosts = ansible_zos_module
+    try:
+        # Create GDG
+        gdg_a = get_tmp_ds_name()
+        hosts.all.shell(cmd=f"dtouch -tgdg -L3 {gdg_a}")
+        # Create VSAM Dataset
+        volumes = Volume_Handler(volumes_on_systems)
+        for vsam in VSAM_NAMES:
+            volume = volumes.get_available_vol()
+            create_vsam_ksds(vsam, hosts, volume)
+        # This test should cluster and gdg datasets
+        find_res = hosts.all.zos_find(
+            patterns=[f'{TEST_SUITE_HLQ}.*'],
+            resource_type=['cluster', 'gdg']
+        )
+        for val in find_res.contacted.values():
+            assert len(val.get('data_sets')) == 2
+            assert val.get('matched') == len(val.get('data_sets'))
+            assert {"name":VSAM_NAMES[0], "type": "CLUSTER"} in val.get('data_sets')
+            assert {"name":gdg_a, "type": "GDG"} in val.get('data_sets')
+    finally:
+        hosts.all.zos_data_set(
+            batch=[
+                {
+                    "name":i,
+                    "state":'absent'
+                } for i in VSAM_NAMES
+            ]
+        )
+        hosts.all.shell(cmd=f"drm {gdg_a}")
+
+
+def test_find_gdg_and_nonvsam_data_sets(ansible_zos_module):
+    hosts = ansible_zos_module
+    try:
+        gdg_b = get_tmp_ds_name()
+        # one with EXTENDED flag -X
+        hosts.all.shell(cmd=f"dtouch -tgdg -L1 -X {gdg_b}")
+        # Create 3 sequential datasets
+        hosts.all.zos_data_set(
+            batch=[
+                {
+                    "name":i,
+                    "type":'seq',
+                    "state":'present'
+                } for i in SEQ_NAMES
+            ]
+        )
+        find_res = hosts.all.zos_find(
+            patterns=[f'{TEST_SUITE_HLQ}.*.*'],
+            resource_type=["gdg", "nonvsam"],
+        )
+        for val in find_res.contacted.values():
+            assert val.get('msg') is None
+            assert len(val.get('data_sets')) == 4
+            assert {"name":gdg_b, "type": "GDG"} in val.get('data_sets')
+            assert val.get('matched') == len(val.get('data_sets'))
+    finally:
+        # Remove GDG.
+        hosts.all.shell(cmd=f"drm {gdg_b}")
+        # Remove SEQ Datasets
+        hosts.all.zos_data_set(
+            batch=[
+                {
+                    "name":i,
+                    "state":'absent'
+                } for i in SEQ_NAMES
+            ]
+        )
+
+
+def test_find_vsam_and_nonvsam_data_sets(ansible_zos_module, volumes_on_systems):
+    hosts = ansible_zos_module
+    try:
+        # Create VSAM Dataset
+        volumes = Volume_Handler(volumes_on_systems)
+        for vsam in VSAM_NAMES:
+            volume = volumes.get_available_vol()
+            create_vsam_ksds(vsam, hosts, volume)
+        # Create 3 sequential datasets
+        hosts.all.zos_data_set(
+            batch=[
+                {
+                    "name":i,
+                    "type":'seq',
+                    "state":'present'
+                } for i in SEQ_NAMES
+            ]
+        )
+        find_res = hosts.all.zos_find(
+            patterns=[f'{TEST_SUITE_HLQ}.*.*'],
+            resource_type=["data", "nonvsam"],
+        )
+        for val in find_res.contacted.values():
+            assert val.get('msg') is None
+            assert len(val.get('data_sets')) == 4
+            assert {"name":f'{VSAM_NAMES[0]}.DATA', "type": "DATA"} in val.get('data_sets')
+            assert val.get('matched') == len(val.get('data_sets'))
+    finally:
+        # Remove VSAM.
+        hosts.all.zos_data_set(
+            batch=[
+                {
+                    "name":i,
+                    "state":'absent'
+                } for i in VSAM_NAMES
+            ]
+        )
+        # Remove SEQ Datasets
+        hosts.all.zos_data_set(
+            batch=[
+                {
+                    "name":i,
+                    "state":'absent'
+                } for i in SEQ_NAMES
+            ]
+        )
+
+
+def test_find_migrated_data_sets(ansible_zos_module):
+    hosts = ansible_zos_module
+    find_res = hosts.all.zos_find(
+        patterns = MIGRATED_DATASETS_PATTERNS,
+        resource_type = ['migrated']
+    )
+    for val in find_res.contacted.values():
+        assert len(val.get('data_sets')) != 0
+        for ds in val.get('data_sets'):
+            assert ds.get("type") == "MIGRATED"
+
+
+def test_find_migrated_data_sets_with_excludes(ansible_zos_module):
+    hosts = ansible_zos_module
+    find_res = hosts.all.zos_find(
+        patterns = MIGRATED_DATASETS_PATTERNS,
+        resource_type = ['migrated'],
+        excludes = '.*F4'
+    )
+    for val in find_res.contacted.values():
+        assert len(val.get('data_sets')) != 0
+        for ds in val.get('data_sets'):
+            assert not re.fullmatch(r".*F4", ds.get("name"))
+
+
+def test_find_migrated_data_sets_with_migrated_type(ansible_zos_module):
+    hosts = ansible_zos_module
+    find_res = hosts.all.zos_find(
+        patterns = MIGRATED_DATASETS_PATTERNS,
+        resource_type = ['migrated'],
+        migrated_type = ['nonvsam']
+    )
+    for val in find_res.contacted.values():
+        assert len(val.get('data_sets')) != 0
+        for ds in val.get('data_sets'):
+            assert ds.get("type") == "MIGRATED"
+            assert ds.get("migrated_resource_type") == "NONVSAM"
+
+
+def test_find_migrated_and_gdg_data_sets(ansible_zos_module):
+    hosts = ansible_zos_module
+    try:
+        gdg_a = get_tmp_ds_name()
+        # Create GDG with limit 3
+        hosts.all.shell(cmd=f"dtouch -tgdg -L3 {gdg_a}")
+        MIGRATED_DATASETS_PATTERNS.append(gdg_a)
+        find_res = hosts.all.zos_find(
+            patterns = MIGRATED_DATASETS_PATTERNS,
+            resource_type = ['migrated', 'gdg'],
+            migrated_type = ['nonvsam']
+        )
+        for val in find_res.contacted.values():
+            assert len(val.get('data_sets')) != 0
+            assert {"name":gdg_a, "type": "GDG"} in val.get('data_sets')
+            for ds in val.get('data_sets'):
+                assert ds.get("type") in ["MIGRATED", "GDG"]
+    finally:
+        # Remove GDG.
+        hosts.all.shell(cmd=f"drm {gdg_a}")


Reviews:
user: login: fernandofloresg
body: requested some additions to the tests
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T17:19:22Z
user: login: richp405
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T14:19:45Z
user: login: rexemin
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T16:14:32Z
user: login: richp405
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T14:53:48Z
user: login: fernandofloresg
body: LGTM!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T15:21:01Z
user: login: rexemin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T21:07:09Z
user: login: rexemin
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T18:02:14Z

Review comments:
diff_hunk: @@ -110,12 +110,15 @@
       - C(cluster) refers to a VSAM cluster. The C(data) and C(index) are the data and index
         components of a VSAM cluster.
       - C(gdg) refers to Generation Data Groups. The module searches based on the GDG base name.
+      - C(migrated) referes to listing migrated datasets. Only excludes option can be used along
path: plugins/modules/zos_find.py
user: login: fernandofloresg
body: ```suggestion
      - C(migrated) refers to listing migrated datasets. Only excludes option can be used along
```
created_at: 2025-05-02T16:14:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,12 +110,15 @@
       - C(cluster) refers to a VSAM cluster. The C(data) and C(index) are the data and index
         components of a VSAM cluster.
       - C(gdg) refers to Generation Data Groups. The module searches based on the GDG base name.
+      - C(migrated) referes to listing migrated datasets. Only excludes option can be used along
+        with this option. The module searches based on only dataset patterns.
path: plugins/modules/zos_find.py
user: login: fernandofloresg
body: ```suggestion
        with this option. The module searches based only on dataset patterns.
```
created_at: 2025-05-02T16:15:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -617,3 +617,23 @@ def test_find_sequential_special_data_sets_containing_single_string(ansible_zos_
     finally:
         for ds in special_names:
             hosts.all.shell(cmd=f"drm '{ds}'")
+
+
+def test_find_migrated_data_sets(ansible_zos_module):
+    hosts = ansible_zos_module
+    find_res = hosts.all.zos_find(
+        patterns=['IMSBLD.I15STSMM.*','IMSBLD.DCC71QPP.*'],
+        resource_type='migrated'
+    )
+    for val in find_res.contacted.values():
+        assert len(val.get('data_sets')) != 0
+
+def test_find_migrated_data_sets_with_excludes(ansible_zos_module):
+    hosts = ansible_zos_module
+    find_res = hosts.all.zos_find(
+        patterns=['IMSBLD.I15STSMM.*','IMSBLD.DCC71QPP.*'],
+        resource_type='migrated',
+        excludes='.*F4'
+    )
+    for val in find_res.contacted.values():
+        assert len(val.get('data_sets')) != 0
path: tests/functional/modules/test_zos_find_func.py
user: login: fernandofloresg
body: Please add an assertion that makes sure there are no data sets that you are trying to exclude like :
`IMSBLD.DCC71QPP.JMK7702.F4`, this can be done in many ways, like using an iterator and an anonymous function to make sure there are no matches to that regex.

created_at: 2025-05-02T17:18:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -617,3 +617,23 @@ def test_find_sequential_special_data_sets_containing_single_string(ansible_zos_
     finally:
         for ds in special_names:
             hosts.all.shell(cmd=f"drm '{ds}'")
+
+
+def test_find_migrated_data_sets(ansible_zos_module):
+    hosts = ansible_zos_module
+    find_res = hosts.all.zos_find(
+        patterns=['IMSBLD.I15STSMM.*','IMSBLD.DCC71QPP.*'],
+        resource_type='migrated'
+    )
+    for val in find_res.contacted.values():
+        assert len(val.get('data_sets')) != 0
path: tests/functional/modules/test_zos_find_func.py
user: login: fernandofloresg
body: will just request an assertion to make sure all returned data sets are of type 'MIGRATED'.
created_at: 2025-05-02T17:19:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,12 +110,15 @@
       - C(cluster) refers to a VSAM cluster. The C(data) and C(index) are the data and index
         components of a VSAM cluster.
       - C(gdg) refers to Generation Data Groups. The module searches based on the GDG base name.
+      - C(migrated) refers to listing migrated datasets. Only excludes option can be used along
path: plugins/modules/zos_find.py
user: login: rexemin
body: ```suggestion
      - C(migrated) refers to listing migrated datasets. Only the C(excludes) option can be used along
```
created_at: 2025-05-07T15:09:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,12 +110,15 @@
       - C(cluster) refers to a VSAM cluster. The C(data) and C(index) are the data and index
         components of a VSAM cluster.
       - C(gdg) refers to Generation Data Groups. The module searches based on the GDG base name.
+      - C(migrated) refers to listing migrated datasets. Only excludes option can be used along
+        with this option. The module searches based only on dataset patterns.
path: plugins/modules/zos_find.py
user: login: rexemin
body: ```suggestion
        with this option. The module only searches based on dataset patterns.
```
created_at: 2025-05-07T15:10:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -189,6 +192,9 @@
   - The time taken to execute the module is proportional to the number of data
     sets present on the system and how large the data sets are.
   - When searching for content within data sets, only non-binary content is considered.
+  - When searching for migrated datasets, output retreived by comparing results of dls -m and dls -l commands.
path: plugins/modules/zos_find.py
user: login: rexemin
body: We can probably skip this part, it's not completely necessary for users to know how we go about getting the migrated data sets
created_at: 2025-05-07T15:11:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -189,6 +192,9 @@
   - The time taken to execute the module is proportional to the number of data
     sets present on the system and how large the data sets are.
   - When searching for content within data sets, only non-binary content is considered.
+  - When searching for migrated datasets, output retreived by comparing results of dls -m and dls -l commands.
+    As migrated dataset information can't be retrived without recalling, other options like age, contains ..etc are not
path: plugins/modules/zos_find.py
user: login: rexemin
body: ```suggestion
    As a migrated dataset's information can't be retrieved without recalling it first, other options besides C(excludes) are not supported.
```
created_at: 2025-05-07T15:13:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -245,6 +251,12 @@
     limit: 30
     scratch: true
     purge: true
+
+- name: Find all Migrated Datasets starting with the word 'USER'
path: plugins/modules/zos_find.py
user: login: rexemin
body: ```suggestion
- name: Find all migrated data sets starting with the word 'USER'
```
created_at: 2025-05-07T15:14:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -652,6 +664,64 @@ def gdg_filter(module, data_sets, limit, empty, fifo, purge, scratch, extended):
         return filtered_data_sets
 
 
+# TODO:
+# Use dls -m command output only when ZOAU adds volser/volume information also for datasets
+def migrated_filter(module, data_sets, excludes):
path: plugins/modules/zos_find.py
user: login: rexemin
body: I see you're using `_dls_wrapper` to get the list of data sets, but you could also use [`datasets.list_dataset_names`](https://www.ibm.com/docs/en/zoau/1.3.0?topic=apis-datasets#zoautil_pydatasetslist_dataset_namesempattern-migratedfalse-kwargsem) which returns a list of strings and then you could skip parsing the raw output from a call to `dls`.
created_at: 2025-05-07T16:14:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,4 @@
+minor_changes:
+  - zos_find - Adds features to find migrated datasets and also allows to find different types of data sets at the same time.
path: changelogs/fragments/2073-zos_find-finding-migrated-datasets.yml
user: login: rexemin
body: I would suggest splitting this point into two:
- Adds functionality to find migrated data sets
- Adds functionality to find different types of data sets at the same time
created_at: 2025-05-13T21:37:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,20 +105,41 @@
     required: false
   resource_type:
     description:
-      - The type of resource to search.
+      - The types of resources to search.
       - C(nonvsam) refers to one of SEQ, LIBRARY (PDSE), PDS, LARGE, BASIC, EXTREQ, or EXTPREF.
       - C(cluster) refers to a VSAM cluster. The C(data) and C(index) are the data and index
         components of a VSAM cluster.
       - C(gdg) refers to Generation Data Groups. The module searches based on the GDG base name.
+      - C(migrated) refers to listing migrated datasets. Only C(excludes) and C(migrated_type) options can be used along
+        with this option. The module only searches based on dataset patterns.
+    aliases:
+      - resource_types
     choices:
       - nonvsam
       - cluster
       - data
       - index
       - gdg
-    type: str
+      - migrated
+    type: list
+    elements: str
     required: false
     default: "nonvsam"
+  migrated_type:
+    description:
+      - A Migrated dataset related attribute, only valid when C(resource_type=migrated).
path: plugins/modules/zos_find.py
user: login: rexemin
body: ```suggestion
      - A migrated data set related attribute, only valid when C(resource_type=migrated).
```
created_at: 2025-05-13T21:38:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -189,6 +210,8 @@
   - The time taken to execute the module is proportional to the number of data
     sets present on the system and how large the data sets are.
   - When searching for content within data sets, only non-binary content is considered.
+  - As a migrated dataset's information can't be retrieved without recalling it first, other options
path: plugins/modules/zos_find.py
user: login: rexemin
body: ```suggestion
  - As a migrated data set's information can't be retrieved without recalling it first, other options
```
created_at: 2025-05-13T21:38:35Z
author_association: CONTRIBUTOR

PR comments:
user: login: surendrababuravella
created_at: 2025-05-02T08:57:47Z
body: Jenkin job:

<img width="1401" alt="Screenshot 2025-05-13 at 12 33 23 PM" src="https://github.com/user-attachments/assets/aeaff9a3-d4d0-4d86-a9c7-c353d0e9675f" />
<img width="1505" alt="Screenshot 2025-05-13 at 12 33 52 PM" src="https://github.com/user-attachments/assets/115df58b-ac6d-4291-aa46-758205fcf22c" />


author_association: COLLABORATOR
user: login: surendrababuravella
created_at: 2025-05-02T09:00:47Z
body: Results:
<img width="590" alt="Screenshot 2025-05-13 at 12 41 39 PM" src="https://github.com/user-attachments/assets/2ad2b28b-3833-4a29-8b68-4bc770e6523e" />


author_association: COLLABORATOR