diff --git a/docs/Makefile b/docs/Makefile index cbc1f2b419..eaf87fad04 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -22,7 +22,7 @@ help: @echo "$(line_header)" @echo " make clean;make role-doc;make html;make view-html;" @echo " make clean;make module-doc;make html;make view-html;" - @echo " make clean;make module-doc;make role-doc;make html;make view-html;" + @echo " make clean;make filter-doc;make module-doc;make role-doc;make html;make view-html;" @echo " make clean;" @echo $(line_header) @@ -81,6 +81,11 @@ clean: rm -rf source/roles; \ fi + @if test -d source/filters; then \ + echo "Deleting filters '$(ROOT_DIR)/source/filters'."; \ + rm -rf source/filters; \ + fi + @if test -d ../plugins/modules/rexx_module_doc; then \ echo "Deleting directory '../plugins/modules/rexx_module_doc'."; \ rm -rf ../plugins/modules/rexx_module_doc; \ @@ -91,7 +96,40 @@ clean: mv -f ../plugins/modules/__init__.py.skip ../plugins/modules/__init__.py; \ fi - @echo "Completed cleanup, run 'make module-doc' or 'make role-doc'." + @echo "Completed cleanup, run 'make module-doc', 'male filter-doc' or 'make role-doc'." + +filter-doc: + @echo $(line_header) + @echo "Running Target filter-doc" + @echo $(line_header) + + @if ! test -d build; then \ + mkdir build; \ + echo "Make $(ROOT_DIR)/build directory for Sphinx generated HTML."; \ + fi + + @if ! test -d source/filters; then \ + mkdir -p source/filters; \ + echo "Make $(ROOT_DIR)/source/filters directory for Sphinx generated HTML."; \ + fi + + @if test -e ../plugins/filter/__init__.py; then \ + echo "Rename file '../plugins/filter/__init__.py' to ../plugins/filter/__init__.py.skip to avoid reading empty python file.'"; \ + mv ../plugins/filter/__init__.py ../plugins/filter/__init__.py.skip; \ + fi + + @echo "Generating ReStructuredText for all ansible modules found at '../plugins/filter/' to 'source/filters'." + @ansible-doc-extractor --template templates/module.rst.j2 source/filters ../plugins/filter/*.py + + + @if test -e ../plugins/filter/__init__.py.skip; then \ + echo "Rename file '../plugins/filter/__init__.py.skip' back to ../plugins/filter/__init__.py.'"; \ + mv -f ../plugins/filter/__init__.py.skip ../plugins/filter/__init__.py; \ + fi + + @echo $(line_header) + @echo "Completed ReStructuredText generation for filters; next run 'make html'" + @echo $(line_header) role-doc: @echo $(line_header) diff --git a/docs/source/filters.rst b/docs/source/filters.rst index bbf24c6d41..a78680b044 100644 --- a/docs/source/filters.rst +++ b/docs/source/filters.rst @@ -27,7 +27,15 @@ the `filter`_ directory included in the collection. https://github.com/ansible-collections/ibm_zos_core/tree/main/plugins/filter/ +The **IBM z/OS core** collection provides many filters. +Reference material for each role contains documentation on how to use certain +filters in your playbook. +.. toctree:: + :maxdepth: 1 + :glob: + + filters/* diff --git a/docs/source/filters/filter_wtor_messages.rst b/docs/source/filters/filter_wtor_messages.rst new file mode 100644 index 0000000000..cd35fff071 --- /dev/null +++ b/docs/source/filters/filter_wtor_messages.rst @@ -0,0 +1,95 @@ + +:github_url: https://github.com/ansible-collections/ibm_zos_core/blob/dev/plugins/modules/filter_wtor_messages.py + +.. _filter_wtor_messages_module: + + +filter_wtor_messages -- Filter a list of WTOR messages +====================================================== + + + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Filter a list of WTOR (write to operator with reply) messages found by module zos_operator_action_query. +- Filter using a string or regular expression. + + + + + +Parameters +---------- + + +wtor_response + A list containing response property `message_text`, provided the module zos_operator_action_query. + + The list can be the outstanding messages found in the modules response under the `actions` property or the entire module response. + + | **required**: True + | **type**: list + + +text + String of text to match or a regular expression to use as filter criteria. + + | **required**: True + | **type**: str + + +ingore_case + Should the filter enable case sensitivity when performing a match. + + | **required**: False + | **type**: bool + | **default**: False + + + + + + +Examples +-------- + +.. code-block:: yaml+jinja + + + - name: Filter actionable messages that match 'IEE094D SPECIFY OPERAND' and if so, set is_specify_operand = true. + set_fact: + is_specify_operand: "{{ result | ibm.ibm_zos_core.filter_wtor_messages('IEE094D SPECIFY OPERAND') }}" + when: result is defined and not result.failed + + - name: Evaluate if there are any existing dump messages matching 'IEE094D SPECIFY OPERAND' + assert: + that: + - is_specify_operand is defined + - bool_zos_operator_action_continue + success_msg: "Found 'IEE094D SPECIFY OPERAND' message." + fail_msg: "Did not find 'IEE094D SPECIFY OPERAND' message." + + + + + + + + + + +Return Values +------------- + + +_value + A list containing dictionaries matching the WTOR. + + | **type**: list + | **elements**: dict + diff --git a/docs/source/filters/generate_data_set_name.rst b/docs/source/filters/generate_data_set_name.rst new file mode 100644 index 0000000000..b04361e4a1 --- /dev/null +++ b/docs/source/filters/generate_data_set_name.rst @@ -0,0 +1,113 @@ + +:github_url: https://github.com/ansible-collections/ibm_zos_core/blob/dev/plugins/modules/generate_data_set_name.py + +.. _generate_data_set_name_module: + + +generate_data_set_name -- Filter HLQs to generate a new random valid data set name. +=================================================================================== + + + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Provide a valid temporary data set name. + + + + + +Parameters +---------- + + +value + High level qualifier to be used in the data set names. + + | **required**: True + | **type**: str + + +middle_level_qualifier + Middle level qualifier to be used in the data set names. + + | **required**: False + | **type**: str + + +last_level_qualifier + Low level qualifier to be used in the data set names. + + | **required**: False + | **type**: str + + +num_names + Number of data set names to be generated. + + | **required**: False + | **type**: int + + + + + + +Examples +-------- + +.. code-block:: yaml+jinja + + + - name: Filter to get one data set name + set_fact: + data_set_name: "{{ hlq | ibm.ibm_zos_core.generate_data_set_name }}" + + - name: Filter to get a data set name with a specific middle level qualifier + set_fact: + data_set_name: "{{ hlq | ibm.ibm_zos_core.generate_data_set_name(middle_level_qualifier='MLQADM') }}" + + - name: Filter to generate a data set name with a specific last level qualifier + set_fact: + data_set_name: "{{ hlq | ibm.ibm_zos_core.generate_data_set_name(last_level_qualifier='LLQADM') }}" + + - name: Filter to generate a data set name with a specific middle and last level qualifier + set_fact: + data_set_name: "{{ hlq | ibm.ibm_zos_core.generate_data_set_name(middle_level_qualifier='MLQADM', last_level_qualifier='LLQADM') }}" + + - name: Filter to generate 10 data set names + set_fact: + data_set_names: "{{ hlq | ibm.ibm_zos_core.generate_data_set_name(num_names=10) }}" + + - name: Filter to generate 3 data set names with a specific last level qualifier + set_fact: + data_set_names: "{{ hlq | ibm.ibm_zos_core.generate_data_set_name(last_level_qualifier='LLQADM', num_names=3) }}" + + - name: Filter to generate 5 data set names with a specific middle level qualifier + set_fact: + data_set_names: "{{ hlq | ibm.ibm_zos_core.generate_data_set_name(middle_level_qualifier='MLQADM', num_names=5) }}" + + + + + + + + + + +Return Values +------------- + + +_value + Name or names generated by the filter. + + | **type**: list + | **elements**: str + diff --git a/docs/source/roles.rst b/docs/source/roles.rst index b61b941232..b0cef42962 100644 --- a/docs/source/roles.rst +++ b/docs/source/roles.rst @@ -17,7 +17,7 @@ recommend migration actions between version 1 and version 2, collect diagnostic facts for support and debugging, and easily determine whether a job is currently running. -The **IBM z/OS core** provides many roles. +The **IBM z/OS core** collection provides many roles. Reference material for each role contains documentation on how to use certain roles in your playbook. diff --git a/plugins/filter/generate_data_set_name.py b/plugins/filter/generate_data_set_name.py new file mode 100644 index 0000000000..35398ef8d6 --- /dev/null +++ b/plugins/filter/generate_data_set_name.py @@ -0,0 +1,201 @@ +# Copyright (c) IBM Corporation 2025 +# 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 +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r''' +name: generate_data_set_name +author: Marcel Gutierrez (@AndreMarcel99) +version_added: "2.0.0" +short_description: Filter HLQs to generate a new random valid data set name. +description: + - Provide a valid temporary data set name. +options: + value: + description: + - High level qualifier to be used in the data set names. + type: str + required: true + samples: USER + middle_level_qualifier: + description: + - Middle level qualifier to be used in the data set names. + type: str + required: false + low_level_qualifier: + description: + - Low level qualifier to be used in the data set names. + type: str + required: false + num_names: + description: + - Number of data set names to be generated. + type: int + required: false +''' + +EXAMPLES = r''' +- name: Filter to get one data set name + set_fact: + data_set_name: "{{ hlq | ibm.ibm_zos_core.generate_data_set_name }}" + +- name: Filter to get a data set name with a specific middle level qualifier + set_fact: + data_set_name: "{{ hlq | ibm.ibm_zos_core.generate_data_set_name(middle_level_qualifier='MLQADM') }}" + +- name: Filter to generate a data set name with a specific low level qualifier + set_fact: + data_set_name: "{{ hlq | ibm.ibm_zos_core.generate_data_set_name(low_level_qualifier='LLQADM') }}" + +- name: Filter to generate a data set name with a specific middle and low level qualifier + set_fact: + data_set_name: "{{ hlq | ibm.ibm_zos_core.generate_data_set_name(middle_level_qualifier='MLQADM', low_level_qualifier='LLQADM') }}" + +- name: Filter to generate 10 data set names + set_fact: + data_set_names: "{{ hlq | ibm.ibm_zos_core.generate_data_set_name(num_names=10) }}" + +- name: Filter to generate 3 data set names with a specific low level qualifier + set_fact: + data_set_names: "{{ hlq | ibm.ibm_zos_core.generate_data_set_name(low_level_qualifier='LLQADM', num_names=3) }}" + +- name: Filter to generate 5 data set names with a specific middle level qualifier + set_fact: + data_set_names: "{{ hlq | ibm.ibm_zos_core.generate_data_set_name(middle_level_qualifier='MLQADM', num_names=5) }}" +''' + +RETURN = r''' + _value: + description: Name or names generated by the filter. + type: list + elements: str +''' + + +import secrets +import string +import re +from ansible.errors import AnsibleFilterError + + +def generate_data_set_name(value, middle_level_qualifier="", low_level_qualifier="", num_names=1): + """Filter to generate valid data set names + + Args: + value {str} -- value of high level qualifier to use on data set names + middle_level_qualifier {str,optional} -- str of a possible qualifier + low_level_qualifier {str, optional} -- str of a possible qualifier + num_names {int, optional} -- number of dataset names to generate. Defaults to 1. + + Returns: + list -- the total dataset names valid + """ + if value is None or value == "": + raise AnsibleFilterError("A High-Level Qualifier is required.") + + hlq = validate_qualifier(qualifier=value) + mlq = "" + llq = "" + + if bool(middle_level_qualifier): + mlq = validate_qualifier(qualifier=middle_level_qualifier) + + if bool(low_level_qualifier): + llq = validate_qualifier(qualifier=low_level_qualifier) + + if num_names > 1: + dataset_names = [] + for generation in range(num_names): + name = hlq + get_tmp_ds_name(middle_level_qualifier=mlq, low_level_qualifier=llq) + dataset_names.append(name) + else: + dataset_names = hlq + get_tmp_ds_name(middle_level_qualifier=mlq, low_level_qualifier=llq) + + return dataset_names + + +def get_tmp_ds_name(middle_level_qualifier="", low_level_qualifier=""): + """Unify the random qualifiers generated into one name. + + Args: + middle_level_qualifier {str,optional} -- valid str of a qualifier + low_level_qualifier {str, optional} -- valid str of a qualifier + + Returns: + str: valid data set name + """ + ds = "." + + if bool(middle_level_qualifier): + ds += middle_level_qualifier + "." + else: + ds += "P" + get_random_q() + "." + + ds += "C" + get_random_q() + "." + + if bool(low_level_qualifier): + ds += low_level_qualifier + else: + ds += "T" + get_random_q() + + return ds + + +def get_random_q(): + """Function or test to ensure random hlq of datasets""" + # Generate the first random hlq of size pass as parameter + letters = string.ascii_uppercase + string.digits + random_q = ''.join(secrets.choice(letters)for iteration in range(7)) + count = 0 + # Generate a random HLQ and verify if is valid, if not, repeat the process + while count < 5 and not re.fullmatch( + r"^(?:[A-Z$#@]{1}[A-Z0-9$#@-]{0,7})", + random_q, + re.IGNORECASE, + ): + random_q = ''.join(secrets.choice(letters)for iteration in range(7)) + count += 1 + return random_q + + +def validate_qualifier(qualifier): + """Function to validate a qualifier with naming rules. + + Args: + qualifier (str): Str to validate as a Qualifier. + + Raises: + AnsibleFilterError: Error of the valid len on the qualifier. + AnsibleFilterError: Error on naming convention on the qualifier. + + Returns: + str: Valid qualifier in upper case. + """ + qualifier = qualifier.upper() + + if len(qualifier) > 8: + raise AnsibleFilterError(f"The qualifier {qualifier} is too long for the data set name.") + + pattern = r'^[A-Z@#$][A-Z0-9@#$]{0,7}$' + if bool(re.fullmatch(pattern, qualifier)): + return qualifier + else: + raise AnsibleFilterError(f"The qualifier {qualifier} is not following the rules for naming conventions.") + + +class FilterModule(object): + """ Jinja2 filter for the returned list or string by the collection module. """ + def filters(self): + return { + 'generate_data_set_name': generate_data_set_name + } diff --git a/tests/dependencyfinder.py b/tests/dependencyfinder.py index dfc7a38e02..69f0de7ea9 100755 --- a/tests/dependencyfinder.py +++ b/tests/dependencyfinder.py @@ -574,6 +574,12 @@ def get_changed_plugins(path, branch="origin/dev"): path_corrected_line = line.split("|", 1)[0].strip() if "plugins/modules/" in line: path_corrected_line = line.split("|", 1)[0].strip() + if "plugins/filter/" in line: + path_corrected_line = line.split("|", 1)[0].strip() + if "functional/filters/" in line: + if re.match('..', line): + line = line.replace("..", "tests") + path_corrected_line = line.split("|", 1)[0].strip() if "roles/" in line: path_corrected_line = line.split("|", 1)[0].strip() if "functional/roles/" in line: diff --git a/tests/functional/filters/test_generate_data_set_name.py b/tests/functional/filters/test_generate_data_set_name.py new file mode 100644 index 0000000000..c8cba90caf --- /dev/null +++ b/tests/functional/filters/test_generate_data_set_name.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) IBM Corporation 2025 +# 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 +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest + +def test_generate_data_set_name_filter(ansible_zos_module): + hosts = ansible_zos_module + input_string = "OMVSADM" + hosts.all.set_fact(input_string=input_string) + results = hosts.all.debug(msg="{{ input_string | generate_data_set_name }}") + + for result in results.contacted.values(): + assert result.get('msg') is not None + assert input_string in result.get('msg') + +def test_generate_data_set_name_mlq_filter(ansible_zos_module): + hosts = ansible_zos_module + input_string = "OMVSADM" + mlq = "mlqadm" + hosts.all.set_fact(input_string=input_string) + jinja_expr = ( + f"{{{{ input_string | generate_data_set_name(" + f"middle_level_qualifier='{mlq}'" + f") }}}}" + ) + results = hosts.all.debug(msg=jinja_expr) + + for result in results.contacted.values(): + assert result.get('msg') is not None + assert input_string in result.get('msg') + assert mlq.upper() in result.get('msg') + +def test_generate_data_set_name_mlq_multiple_generations_filter(ansible_zos_module): + hosts = ansible_zos_module + input_string = "OMVSADM" + mlq = "mlqadm" + num_names = 5 + hosts.all.set_fact(input_string=input_string) + jinja_expr = ( + f"{{{{ input_string | generate_data_set_name(" + f"middle_level_qualifier='{mlq}', " + f"num_names={num_names}" + f") }}}}" + ) + results = hosts.all.debug(msg=jinja_expr) + + for result in results.contacted.values(): + assert result.get('msg') is not None + assert len(result.get('msg')) == num_names + assert input_string in result.get('msg')[0] + assert mlq.upper() in result.get('msg')[0] + +def test_generate_data_set_name_llq_filter(ansible_zos_module): + hosts = ansible_zos_module + input_string = "OMVSADM" + llq = "llqadm" + hosts.all.set_fact(input_string=input_string) + jinja_expr = ( + f"{{{{ input_string | generate_data_set_name(" + f"low_level_qualifier='{llq}'" + f") }}}}" + ) + results = hosts.all.debug(msg=jinja_expr) + + for result in results.contacted.values(): + assert result.get('msg') is not None + assert input_string in result.get('msg') + assert llq.upper() in result.get('msg') + +def test_generate_data_set_name_llq_multiple_generations_filter(ansible_zos_module): + hosts = ansible_zos_module + input_string = "OMVSADM" + llq = "llqadm" + num_names = 5 + hosts.all.set_fact(input_string=input_string) + jinja_expr = ( + f"{{{{ input_string | generate_data_set_name(" + f"low_level_qualifier='{llq}', " + f"num_names={num_names}" + f") }}}}" + ) + results = hosts.all.debug(msg=jinja_expr) + + for result in results.contacted.values(): + assert result.get('msg') is not None + assert len(result.get('msg')) == num_names + assert input_string in result.get('msg')[0] + assert llq.upper() in result.get('msg')[0] + +def test_generate_data_set_name_mlq_llq_filter(ansible_zos_module): + hosts = ansible_zos_module + input_string = "OMVSADM" + mlq = "mlqadm" + llq = "llqadm" + hosts.all.set_fact(input_string=input_string) + jinja_expr = ( + f"{{{{ input_string | generate_data_set_name(" + f"middle_level_qualifier='{mlq}', " + f"low_level_qualifier='{llq}') }}}}" + ) + results = hosts.all.debug(msg=jinja_expr) + + for result in results.contacted.values(): + assert result.get('msg') is not None + assert input_string in result.get('msg') + assert mlq.upper() in result.get('msg') + assert llq.upper() in result.get('msg') + +def test_generate_data_set_name_mlq_llq_multiple_generations_filter(ansible_zos_module): + hosts = ansible_zos_module + input_string = "OMVSADM" + mlq = "mlqadm" + llq = "llqadm" + num_names = 3 + hosts.all.set_fact(input_string=input_string) + jinja_expr = ( + f"{{{{ input_string | generate_data_set_name(" + f"middle_level_qualifier='{mlq}', " + f"low_level_qualifier='{llq}', " + f"num_names={num_names}" + f") }}}}" + ) + results = hosts.all.debug(msg=jinja_expr) + + for result in results.contacted.values(): + assert result.get('msg') is not None + assert len(result.get('msg')) == num_names + assert input_string in result.get('msg')[0] + assert mlq.upper() in result.get('msg')[0] + assert llq.upper() in result.get('msg')[0] + +def test_generate_data_set_name_filter_multiple_generations(ansible_zos_module): + hosts = ansible_zos_module + input_string = "OMVSADM" + num_names = 10 + hosts.all.set_fact(input_string=input_string) + jinja_expr = ( + f"{{{{ input_string | generate_data_set_name(" + f"num_names={num_names}" + f") }}}}" + ) + results = hosts.all.debug(msg=jinja_expr) + + for result in results.contacted.values(): + assert result.get('msg') is not None + assert input_string in result.get('msg')[0] + assert len(result.get('msg')) == 10 + +def test_generate_data_set_name_filter_bad_hlq(ansible_zos_module): + hosts = ansible_zos_module + input_string = "OMVSADMONE" + hosts.all.set_fact(input_string=input_string) + results = hosts.all.debug(msg="{{ input_string | generate_data_set_name }}") + + for result in results.contacted.values(): + assert result.get('failed') is True + assert result.get('msg') == f"The qualifier {input_string} is too long for the data set name." + +def test_generate_data_set_name_filter_bad_mlq(ansible_zos_module): + hosts = ansible_zos_module + input_string = "OMVSADM" + mlq = "1mlq" + hosts.all.set_fact(input_string=input_string) + jinja_expr = ( + f"{{{{ input_string | generate_data_set_name(" + f"middle_level_qualifier='{mlq}'" + f") }}}}" + ) + results = hosts.all.debug(msg=jinja_expr) + + for result in results.contacted.values(): + assert result.get('failed') is True + assert result.get('msg') == f"The qualifier {mlq.upper()} is not following the rules for naming conventions." + +def test_generate_data_set_name_mlq_bad_llq(ansible_zos_module): + hosts = ansible_zos_module + input_string = "OMVSADM" + mlq = "mlqadm" + llq = "llqadmhere" + hosts.all.set_fact(input_string=input_string) + jinja_expr = ( + f"{{{{ input_string | generate_data_set_name(" + f"middle_level_qualifier='{mlq}', " + f"low_level_qualifier='{llq}') }}}}" + ) + results = hosts.all.debug(msg=jinja_expr) + + for result in results.contacted.values(): + assert result.get('failed') is True + assert result.get('msg') == f"The qualifier {llq.upper()} is too long for the data set name." + +def test_generate_data_set_name_filter_no_hlq(ansible_zos_module): + hosts = ansible_zos_module + input_string = "OMVSADMONE" + results = hosts.all.debug(msg="{{ generate_data_set_name }}") + + for result in results.contacted.values(): + assert result.get('failed') is True + +def test_generate_data_set_name_filter_bad_hlq(ansible_zos_module): + hosts = ansible_zos_module + input_string = "" + hosts.all.set_fact(input_string=input_string) + results = hosts.all.debug(msg="{{ input_string | generate_data_set_name }}") + + for result in results.contacted.values(): + assert result.get('failed') is True + assert result.get('msg') == "A High-Level Qualifier is required." \ No newline at end of file