diff --git a/.github/workflows/dev-release.yaml.old b/.github/workflows/dev-release.yaml.old deleted file mode 100644 index 512f5846..00000000 --- a/.github/workflows/dev-release.yaml.old +++ /dev/null @@ -1,69 +0,0 @@ -name: Zowe SDK Release - -on: - pull_request_target: - types: - - closed - branches: - - main - -jobs: - release: - if: ${{ github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release-dev') }} - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - with: - ref: main - token: ${{ secrets.ZOWE_ROBOT_TOKEN }} - - - name: Set up Python 3.7 - uses: actions/setup-python@v4 - with: - python-version: 3.7 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - - name: Increment dev version - id: update-version - shell: python - run: | - import sys - sys.path.append("src") - from _version import __version__ - prerelease_tag = "dev" - tag_end_index = __version__.index(prerelease_tag) + len(prerelease_tag) - new_version = __version__[:tag_end_index] + str(int(__version__[tag_end_index:]) + 1) - with open("src/_version.py", 'w', encoding="utf-8") as f: - f.write("__version__ = \"" + new_version + "\"\n") - print("::set-output name=version::" + new_version) - - - name: Build dist wheels - run: bash build.sh - - - name: Create zip bundle - working-directory: dist - run: | - pip download -f . --no-binary=pyyaml zowe-${{ steps.update-version.outputs.version }}-py3-none-any.whl - zip zowe-python-sdk-${{ steps.update-version.outputs.version }}.zip PyYAML*.tar.gz *.whl - - - name: Commit version update - uses: stefanzweifel/git-auto-commit-action@v4 - with: - branch: main - commit_message: 'Bump version to ${{ steps.update-version.outputs.version }} [ci skip]' - commit_options: '--signoff' - commit_user_name: ${{ secrets.ZOWE_ROBOT_USER }} - commit_user_email: ${{ secrets.ZOWE_ROBOT_EMAIL }} - file_pattern: 'src/_version.py' - tagging_message: v${{ steps.update-version.outputs.version }} - - - name: Create GitHub release - uses: softprops/action-gh-release@v1 - with: - files: 'dist/zowe-python-sdk-${{ steps.update-version.outputs.version }}.zip' - tag_name: v${{ steps.update-version.outputs.version }} diff --git a/.github/workflows/sdk-build.yml b/.github/workflows/sdk-build.yml index 60a3694b..b90cf85f 100644 --- a/.github/workflows/sdk-build.yml +++ b/.github/workflows/sdk-build.yml @@ -13,36 +13,41 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 ./src --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 ./src --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest - run: | - coverage run -m pytest ./tests/unit - - name: Generate a coverage xml file - run: | - coverage xml - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - directory: ./ - fail_ci_if_error: true - files: ./coverage.xml - flags: unittests - name: codecov-umbrella - verbose: true + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 ./src --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 ./src --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + coverage run -m pytest ./tests/unit + - name: Generate a coverage xml file + run: | + coverage xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + if: ${{ matrix.python-version == '3.12' }} + with: + directory: ./ + env_vars: OS,PYTHON + fail_ci_if_error: true + files: ./coverage.xml + flags: unittests + name: codecov-umbrella + verbose: true diff --git a/.github/workflows/sdk-release.yaml b/.github/workflows/sdk-release.yaml new file mode 100644 index 00000000..ff5430f9 --- /dev/null +++ b/.github/workflows/sdk-release.yaml @@ -0,0 +1,79 @@ +name: Zowe SDK Release + +on: + workflow_dispatch: + inputs: + version: + description: Update project version before publish + required: false + type: string + dry-run: + description: Dry run mode + required: false + type: boolean + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-tags: true + persist-credentials: false + ref: ${{ github.ref }} + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip twine + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Update version + id: update-version + shell: python + run: | + import os, sys + sys.path.append("src") + from _version import __version__ + new_version = "${{ inputs.version }}" + if new_version: + with open("src/_version.py", 'w') as f: + f.write("__version__ = \"" + new_version + "\"\n") + else: + new_version = __version__ + with open(os.environ["GITHUB_OUTPUT"], 'a') as f: + print("version=" + ("-".join(new_version.rsplit(".", 1)) if new_version.count(".") > 2 else new_version), file=f) + + - name: Update version (cargo) + run: cargo install cargo-edit && cargo set-version ${{ steps.update-version.outputs.version }} + working-directory: src/secrets + + - name: Update version (git) + run: git add src/_version.py src/secrets/Cargo.* + + - name: Build wheels + run: bash build.sh + + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: dist/ + + - uses: zowe-actions/octorelease@v1 + env: + GIT_COMMITTER_NAME: ${{ secrets.ZOWE_ROBOT_USER }} + GIT_COMMITTER_EMAIL: ${{ secrets.ZOWE_ROBOT_EMAIL }} + GIT_CREDENTIALS: x-access-token:${{ secrets.ZOWE_ROBOT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_ROBOT_TOKEN }} + with: + dry-run: ${{ inputs.dry-run }} + new-version: ${{ steps.update-version.outputs.version }} diff --git a/.github/workflows/secrets-sdk.yml b/.github/workflows/secrets-sdk.yml new file mode 100644 index 00000000..6e454834 --- /dev/null +++ b/.github/workflows/secrets-sdk.yml @@ -0,0 +1,146 @@ +# This file is autogenerated by maturin v1.3.1 +# To update, run +# +# maturin generate-ci github +# +name: Secrets SDK CI + +on: + push: + branches: ["**"] + paths: + - "src/secrets/**" + - ".github/workflows/secrets-sdk.yml" + tags: ["**"] + pull_request: + paths: + - "src/secrets/**" + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + linux: + runs-on: ubuntu-latest + strategy: + matrix: + target: [x86_64, x86, aarch64, armv7, s390x] + fail-fast: false + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Set environment variables + run: src/secrets/scripts/configure-cross.sh ${{ matrix.target }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist + sccache: "true" + manylinux: auto + docker-options: -e PKG_CONFIG_SYSROOT_DIR -e PKG_CONFIG_PATH + working-directory: src/secrets + before-script-linux: | + if command -v yum &> /dev/null; then + yum update -y && yum install -y libsecret-devel.${{ env.CROSS_DEB_ARCH }} pkgconfig + else + dpkg --add-architecture ${{ env.CROSS_DEB_ARCH }} + sed -i "s/deb /deb [arch=amd64] /g" /etc/apt/sources.list + echo "deb [arch=${{ env.CROSS_DEB_ARCH }}] http://ports.ubuntu.com/ubuntu-ports/ jammy main universe" >> /etc/apt/sources.list + echo "deb [arch=${{ env.CROSS_DEB_ARCH }}] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main universe" >> /etc/apt/sources.list + apt-get update && apt-get install -y libsecret-1-dev:${{ env.CROSS_DEB_ARCH }} pkg-config + fi + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: src/secrets/dist + + windows: + runs-on: windows-latest + strategy: + matrix: + target: [x64, x86, aarch64] + fail-fast: false + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + if: ${{ matrix.target != 'aarch64' }} + with: + python-version: "3.10" + architecture: ${{ matrix.target }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist + sccache: "true" + working-directory: src/secrets + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: src/secrets/dist + + macos: + runs-on: macos-latest + strategy: + matrix: + target: [x86_64, aarch64] + fail-fast: false + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist + sccache: "true" + working-directory: src/secrets + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: src/secrets/dist + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + working-directory: src/secrets + - name: Upload sdist + uses: actions/upload-artifact@v3 + with: + name: wheels + path: src/secrets/dist + + release: + name: Release + runs-on: ubuntu-latest + if: ${{ startsWith(github.ref, 'refs/tags/') }} + needs: [linux, windows, macos, sdist] + steps: + - uses: actions/download-artifact@v3 + with: + name: wheels + - name: Publish to PyPI + uses: PyO3/maturin-action@v1 + env: + MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_ROBOT_TOKEN }} + with: + command: upload + args: --non-interactive --skip-existing * diff --git a/.readthedocs.yml b/.readthedocs.yml index cc03fd20..3bd5116b 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -5,16 +5,20 @@ # Required version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3" + # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/source/conf.py # Optionally build your docs in additional formats such as PDF -formats: +formats: - pdf # Optionally set the version of Python and requirements required to build your docs python: - version: 3.7 install: - requirements: docs/requirements.txt diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..1233617b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "editor.formatOnSave": true, + "python.formatting.provider": "black", + "rust-analyzer.linkedProjects": ["./src/secrets/Cargo.toml"], + "[python]": { + "editor.codeActionsOnSave": { + "source.organizeImports": true + } + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 168c5874..953034a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,4 +4,24 @@ All notable changes to the Zowe Client Python SDK will be documented in this fil ## Recent Changes -- Feature: Added method to load profile properties from environment variables \ No newline at end of file +### Bug Fixes + +- Fixed Secrets SDK requiring LD_LIBRARY_PATH to be defined when installed from wheel on Linux [#229](https://github.com/zowe/zowe-client-python-sdk/issues/229) +- Fixed 'issue_command' Console API function to provide custom console name [#231](https://github.com/zowe/zowe-client-python-sdk/issues/231) + +## `1.0.0-dev11` + +### Enhancements + +- Added method to save secure profile properties to vault [#72](https://github.com/zowe/zowe-client-python-sdk/issues/72) +- Added method to save profile properties to zowe.config.json file [#73](https://github.com/zowe/zowe-client-python-sdk/issues/73) +- Added CredentialManager class to securely retrieve values from credentials and manage multiple credential entries on Windows [#134](https://github.com/zowe/zowe-client-python-sdk/issues/134) +- Added method to load profile properties from environment variables [#136](https://github.com/zowe/zowe-client-python-sdk/issues/136) +- Added validation of zowe.config.json file matching the schema [#192](https://github.com/zowe/zowe-client-python-sdk/issues/192) +- Added Secrets SDK for storing client secrets in OS keyring [#208](https://github.com/zowe/zowe-client-python-sdk/issues/208) + +### Bug Fixes + +- Fixed profile merge order to match Node.js SDK [#190](https://github.com/zowe/zowe-client-python-sdk/issues/190) +- Fixed issue for datasets and jobs with special characters in URL [#211](https://github.com/zowe/zowe-client-python-sdk/issues/211) +- Fixed exception handling in session.py [#213](https://github.com/zowe/zowe-client-python-sdk/issues/213) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index edeb1307..6ac66f87 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,4 +67,4 @@ This project follows the [PEP 8](https://www.python.org/dev/peps/pep-0008/) styl This project also uses `flake8` for code linting. Make sure to run `flake8` on your code before submitting a pull request. -We recommend using `black` as a code formatter. Please format your code using `black` before creating a pull request. +We recommend using `black` and `isort` as code formatters. Please format your code using these tools before creating a pull request. diff --git a/README.md b/README.md index 93723df9..5a46b26e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![](https://img.shields.io/badge/license-EPL--2.0-blue) [![Latest Release](https://img.shields.io/github/v/release/zowe/zowe-client-python-sdk)](https://github.com/zowe/zowe-client-python-sdk/releases/latest) [![Documentation Status](https://readthedocs.org/projects/zowe-client-python-sdk/badge/?version=latest)](https://zowe-client-python-sdk.readthedocs.io/en/latest/index.html) -The Zowe Client Python SDK, is a set of Python packages designed to allow programmatic +The Zowe Client Python SDK, is a set of Python packages designed to allow programmatic interactions with z/OS REST API interfaces with minimal effort. Python developers can leverage the Zowe SDK in order to create powerful scripts/applications @@ -18,11 +18,10 @@ When installing the Zowe Client Python SDK, you have two options: - Install a single sub-package The choice depends on your intentions. If you choose to install all Zowe SDK packages, -this means that you will install everything under the `zowe` namespace in PyPi. +this means that you will install everything under the `zowe` namespace in PyPi. Alternatively, you can choose to install only a single subpackage for a smaller installation. - To install all Zowe SDK packages using pip: ``` @@ -36,27 +35,40 @@ pip install zowe._for_zowe_sdk ``` For more information on the available sub-packages click [HERE](https://zowe-client-python-sdk.readthedocs.io/en/latest/packages/packages.html) - ## Requirements -The Zowe core package has dependencies on the below packages: +The Zowe core package has dependencies on the packages listed below: ``` -requests>=2.22 -keyring +commentjson +deepmerge +jsonschema pyyaml +requests>=2.22 urllib3 ``` +It also has an optional dependency on the Zowe Secrets SDK for storing client secrets which can be installed with the `secrets` extra: + +``` +pip install zowe.core-for-zowe-sdk[secrets] +``` + ### Developer setup -**Prerequisite:** The minimum supported Python version is 3.7. +Ensure the following prerequisites are installed and on your PATH: + +- Python >= 3.7 and `pip` +- Cargo >= 1.72 (to build Rust bindings for Secrets SDK) +- Visual Studio Build Tools >= 2015 (Windows only) Clone the repository using `git`: @@ -71,9 +83,9 @@ cd zowe-client-python-sdk/ git checkout main ``` -We recommend that developers make a virtual environment to install all required dependencies. +We recommend that developers make a virtual environment to install all required dependencies. -Create a virtual environment in the root of the repository folder using the `venv` module. +Create a virtual environment in the root of the repository folder using the `venv` module. The command below assumes that `python` is a version of Python3: ``` @@ -82,7 +94,7 @@ python -m venv venv _(If this isn't the case for your environment, use the appropriate command alias for Python3)_ -Activate your virtual environment so that Python uses it to manage dependencies. +Activate your virtual environment so that Python uses it to manage dependencies. Assuming that you are using Bash shell, reference the command below: ``` @@ -97,7 +109,7 @@ Install the dependencies listed in `requirements.txt` using `pip`: pip install -r requirements.txt ``` -You can now develop the Python SDK with the installed dependencies. +You can now develop the Python SDK with the installed dependencies. When you are finished with your development session, deactivate your virtual environment: ``` @@ -106,11 +118,9 @@ deactivate ## Quickstart - -After you install the package in your project, import the class for the required sub-package (i.e `Console` class for z/OS Console commands). +After you install the package in your project, import the class for the required sub-package (i.e `Console` class for z/OS Console commands). Create a dictionary to handle communication with the plug-in: - ```python from zowe.zos_console_for_zowe_sdk import Console profile = { @@ -135,60 +145,67 @@ Alternatively, you can use an existing Zowe CLI profile instead: **Important**: If your z/OSMF profile uses a credentials manager, this approach may not work depending on your operating system. Support for loading secure profiles has only been tested on Windows and Ubuntu so far. - # Available options Currently, the Zowe Python SDK supports the following interfaces: -* Console commands -* z/OSMF Information retrieval -* Submit job from a dataset -* Submit job from local file -* Submit job as plain text JCL -* Retrieve job status -* Retrieve job list from JES spool -* Start/End TSO address space -* Ping TSO address space -* Issue TSO command - -**Important**: Notice that the below examples assume that you have already created +- Console commands +- z/OSMF Information retrieval +- Submit job from a dataset +- Submit job from local file +- Submit job as plain text JCL +- Retrieve job status +- Retrieve job list from JES spool +- Start/End TSO address space +- Ping TSO address space +- Issue TSO command + +**Important**: Notice that the below examples assume that you have already created an object for the sub-package of your preference just like in the quickstart example. ## Console Usage of the console api: + ```python result = my_console.issue_command("") ``` + The result will be a JSON object containing the result from the console command. ## Job To retrieve the status of a job on JES + ```python result = my_jobs.get_job_status("", "") ``` To retrieve list of jobs in JES spool + ```python result = my_jobs.list_jobs(owner="", prefix="") ``` + Additional parameters available are: -* max_jobs -* user_correlator +- max_jobs +- user_correlator To submit a job from a dataset: + ```python result = my_jobs.submit_from_mainframe("") ``` To submit a job from a local file: + ```python result = my_jobs.submit_from_local_file("") ``` To submit from plain text: + ```python jcl = ''' //IEFBR14Q JOB (AUTOMATION),CLASS=A,MSGCLASS=0, @@ -203,6 +220,7 @@ result = my_jobs.submit_from_plaintext(jcl) ## TSO Starting a TSO address space + ```python session_parameters = { @@ -217,27 +235,32 @@ session_parameters = { session_key = my_tso.start_tso_session(**session_parameters) ``` + If you don't provide any session parameter ZoweSDK will attempt to start a session with default parameters. To end a TSO address space + ```python my_tso.end_tso_session("") ``` In order to issue a TSO command + ```python tso_output = my_tso.issue_command("") ``` ## z/OSMF + Usage of the z/OSMF api + ```python result = my_zosmf.get_info() ``` -The result will be a JSON object containing z/OSMF information +The result will be a JSON object containing z/OSMF information # Acknowledgments -* Make sure to check out the [Zowe project](https://github.com/zowe)! -* For further information on z/OSMF REST API, click [HERE](https://www.ibm.com/support/knowledgecenter/SSLTBW_2.1.0/com.ibm.zos.v2r1.izua700/IZUHPINFO_RESTServices.htm) +- Make sure to check out the [Zowe project](https://github.com/zowe)! +- For further information on z/OSMF REST API, click [HERE](https://www.ibm.com/support/knowledgecenter/SSLTBW_2.1.0/com.ibm.zos.v2r1.izua700/IZUHPINFO_RESTServices.htm) diff --git a/docs/requirements.txt b/docs/requirements.txt index 96f30abe..5601d12c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ sphinx_rtd_theme>=0.5.1 -sphinxcontrib-spelling==5.4.0 +sphinxcontrib-spelling==8.0.0 -e ./src/core -e ./src/zos_console -e ./src/zos_files diff --git a/docs/source/_ext/zowe_autodoc.py b/docs/source/_ext/zowe_autodoc.py index 5f9e9d9a..8472d91a 100644 --- a/docs/source/_ext/zowe_autodoc.py +++ b/docs/source/_ext/zowe_autodoc.py @@ -52,16 +52,15 @@ def main(): py_name = os.path.basename(py_file) if py_name == "__init__.py": continue - with open(py_file, 'r', encoding="utf-8") as f: + with open(py_file, "r", encoding="utf-8") as f: py_contents = f.read() class_names = re.findall(r"^class (\w+)\b", py_contents, re.MULTILINE) if len(class_names) == 1: rst_name = f"{py_name[:-3]}.rst" - rst_contents = render_template(CLASS_TEMPLATE, { - "fullname": f"{sdk_name}.{pkg_name}.{class_names[0]}", - "header": class_names[0] - }) - with open(f"docs/source/classes/{sdk_name}/{rst_name}", 'w', encoding="utf-8") as f: + rst_contents = render_template( + CLASS_TEMPLATE, {"fullname": f"{sdk_name}.{pkg_name}.{class_names[0]}", "header": class_names[0]} + ) + with open(f"docs/source/classes/{sdk_name}/{rst_name}", "w", encoding="utf-8") as f: f.write(rst_contents) rst_names.append(rst_name) elif len(class_names) > 1: @@ -70,39 +69,44 @@ def main(): child_rst_names = [] for class_name in sorted(class_names): rst_name = f"{class_name.lower()}.rst" - rst_contents = render_template(CLASS_TEMPLATE, { - "fullname": f"{sdk_name}.{pkg_name}.{module_name}.{class_name}", - "header": class_name - }) - with open(f"docs/source/classes/{sdk_name}/{module_name}/{rst_name}", 'w', encoding="utf-8") as f: + rst_contents = render_template( + CLASS_TEMPLATE, + {"fullname": f"{sdk_name}.{pkg_name}.{module_name}.{class_name}", "header": class_name}, + ) + with open(f"docs/source/classes/{sdk_name}/{module_name}/{rst_name}", "w", encoding="utf-8") as f: f.write(rst_contents) child_rst_names.append(rst_name) rst_name = f"{module_name}/index.rst" - rst_contents = render_template(INDEX_TEMPLATE, { - "filelist": "\n ".join(name[:-4] for name in child_rst_names), - "header": f"{module_name.replace('_', ' ').title()} classes", - "maxdepth": 2 - }) - with open(f"docs/source/classes/{sdk_name}/{rst_name}", 'w', encoding="utf-8") as f: + rst_contents = render_template( + INDEX_TEMPLATE, + { + "filelist": "\n ".join(name[:-4] for name in child_rst_names), + "header": f"{module_name.replace('_', ' ').title()} classes", + "maxdepth": 2, + }, + ) + with open(f"docs/source/classes/{sdk_name}/{rst_name}", "w", encoding="utf-8") as f: f.write(rst_contents) parent_rst_names.append(rst_name) - rst_contents = render_template(INDEX_TEMPLATE, { - "filelist": "\n ".join(name[:-4] for name in rst_names + parent_rst_names), - "header": pkg_name, - "maxdepth": 2 - }) - with open(f"docs/source/classes/{sdk_name}/index.rst", 'w', encoding="utf-8") as f: + rst_contents = render_template( + INDEX_TEMPLATE, + { + "filelist": "\n ".join(name[:-4] for name in rst_names + parent_rst_names), + "header": pkg_name, + "maxdepth": 2, + }, + ) + with open(f"docs/source/classes/{sdk_name}/index.rst", "w", encoding="utf-8") as f: f.write(rst_contents) sdk_names.append(sdk_name) print("done") - rst_contents = render_template(INDEX_TEMPLATE, { - "filelist": "\n ".join(f"{name}/index" for name in sdk_names), - "header": "Classes", - "maxdepth": 3 - }) - with open(f"docs/source/classes/index.rst", 'w', encoding="utf-8") as f: + rst_contents = render_template( + INDEX_TEMPLATE, + {"filelist": "\n ".join(f"{name}/index" for name in sdk_names), "header": "Classes", "maxdepth": 3}, + ) + with open(f"docs/source/classes/index.rst", "w", encoding="utf-8") as f: f.write(rst_contents) diff --git a/docs/source/conf.py b/docs/source/conf.py index 2781f3ef..58d5df40 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -13,16 +13,16 @@ import os import sys from datetime import date -sys.path.insert(0, os.path.abspath('../../src')) -sys.path.append(os.path.abspath('./_ext')) -from _version import __version__ +sys.path.insert(0, os.path.abspath("../../src")) +sys.path.append(os.path.abspath("./_ext")) +from _version import __version__ # -- Project information ----------------------------------------------------- -project = 'Zowe Client Python SDK' -copyright = f'{date.today().year}, Contributors to the Zowe Project' -author = 'Contributors to the Zowe Project' +project = "Zowe Client Python SDK" +copyright = f"{date.today().year}, Contributors to the Zowe Project" +author = "Contributors to the Zowe Project" # The full version, including alpha/beta/rc tags release = __version__ @@ -33,12 +33,14 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.coverage', - 'sphinx.ext.napoleon', - 'sphinx_rtd_theme', - 'sphinxcontrib.spelling', - 'zowe_autodoc'] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.coverage", + "sphinx.ext.napoleon", + "sphinx_rtd_theme", + "sphinxcontrib.spelling", + "zowe_autodoc", +] # Napoleon options napoleon_google_docstring = False @@ -56,7 +58,7 @@ # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -69,11 +71,11 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] -html_logo = '_static/zowe-white.png' +html_logo = "_static/zowe-white.png" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..85c3b073 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[tool.black] +line-length = 120 + +[tool.isort] +profile = "black" diff --git a/release.config.js b/release.config.js new file mode 100644 index 00000000..33027773 --- /dev/null +++ b/release.config.js @@ -0,0 +1,22 @@ +module.exports = { + branches: [ + { + name: "main", + level: "minor" + }, + { + name: "zowe-v?-lts", + level: "patch" + } + // { + // name: "next", + // prerelease: true + // } + ], + plugins: [ + "@octorelease/changelog", + "@octorelease/pypi", + "@octorelease/github", + "@octorelease/git" + ] +}; diff --git a/requirements.txt b/requirements.txt index dde23817..a5a442e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,20 +1,23 @@ +black certifi==2023.7.22 chardet==4.0.0 colorama==0.4.4 commentjson==0.9.0 coverage==5.4 -flake8==3.8.4 +deepmerge==1.1.0 +flake8==5.0.0 idna==2.10 -importlib-metadata==3.6.0 -jsonschema==4.14.0 -keyring +importlib-metadata==3.6.0;python_version<"3.8" +isort +jsonschema==4.17.3 lxml==4.9.3 -mccabe==0.6.1 +maturin +mccabe==0.7.0 nose2==0.10.0 -pycodestyle==2.6.0 +pycodestyle==2.9.0 pydocstyle==5.1.1 pyfakefs -pyflakes==2.2.0 +pyflakes==2.5.0 pylama==7.7.1 pytest==7.1.2 python-decouple==3.4 @@ -22,14 +25,15 @@ PyYAML==6.0.1 requests==2.31.0 six==1.15.0 snowballstemmer==2.1.0 -typing-extensions==3.7.4.3 +typing-extensions==4.0.1 Unidecode==1.2.0 -urllib3==1.26.5 +urllib3==1.26.18 wheel zipp==3.4.0 --e ./src/core +-e ./src/core[secrets] +-e ./src/secrets -e ./src/zos_console -e ./src/zos_files -e ./src/zos_jobs -e ./src/zos_tso --e ./src/zosmf \ No newline at end of file +-e ./src/zosmf diff --git a/samples/SampleConsole.py b/samples/SampleConsole.py index cecf9279..55581152 100644 --- a/samples/SampleConsole.py +++ b/samples/SampleConsole.py @@ -2,16 +2,11 @@ # Change below to the name of your zosmf profile -connection = { - "plugin_profile": "xxxx" -} +connection = {"plugin_profile": "xxxx"} my_console = Console(connection) -command = 'D IPLINFO' +command = "D IPLINFO" command_result = my_console.issue_command(command) -command_output = command_result['cmd-response'].replace('\r','\n') +command_output = command_result["cmd-response"].replace("\r", "\n") print(f"Command: {command} \n Output: \n\n{command_output}") - - - \ No newline at end of file diff --git a/samples/SampleFiles.py b/samples/SampleFiles.py index d76d5583..7da6912c 100644 --- a/samples/SampleFiles.py +++ b/samples/SampleFiles.py @@ -2,24 +2,22 @@ # Change below to the name of your zosmf profile -connection = { - "plugin_profile": "xxxx" -} +connection = {"plugin_profile": "xxxx"} # ----------------------------------------------------- # print list of zos datasets -# ----------------------------------------------------- +# ----------------------------------------------------- print("...SYS1 datasets\n") my_files = Files(connection) my_dsn_list = my_files.list_dsn("SYS1.**.*") -datasets = my_dsn_list['items'] +datasets = my_dsn_list["items"] for ds in datasets: - print(ds['dsname']) + print(ds["dsname"]) # ----------------------------------------------------- # Now try the uss side... Not in the SDK in GitHub yet -# ----------------------------------------------------- +# ----------------------------------------------------- print("...files in /etc\n") my_file_list = my_files.list_files("/etc") files = my_file_list["items"] @@ -28,8 +26,7 @@ # ----------------------------------------------------- # Get the content of one of the files. -# ----------------------------------------------------- +# ----------------------------------------------------- print("...content of a file\n") my_file_content = my_files.get_file_content("/z/tm891807/file.txt") print(my_file_content["response"]) - diff --git a/samples/SampleJobs.py b/samples/SampleJobs.py index c2d2a2b5..95960fa9 100644 --- a/samples/SampleJobs.py +++ b/samples/SampleJobs.py @@ -1,48 +1,46 @@ -from zowe.zos_jobs_for_zowe_sdk import Jobs -import time import os +import time +from zowe.zos_jobs_for_zowe_sdk import Jobs # ----------------------------------------------------- # Test drive the jobs SDK with jcl from a file -# ----------------------------------------------------- +# ----------------------------------------------------- # Change below to the name of your zosmf profile -connection = { - "plugin_profile": "xxxx" -} +connection = {"plugin_profile": "xxxx"} print("...Submit a sleeper job\n") my_jobs = Jobs(connection) job = my_jobs.submit_from_local_file("jcl\sleep.jcl") -job_name = job['jobname'] -job_id = job['jobid'] +job_name = job["jobname"] +job_id = job["jobid"] print(f"Job {job_name} ID {job_id} submitted") # ----------------------------------------------------- # Wait until the job completes -# ----------------------------------------------------- +# ----------------------------------------------------- boolJobNotDone = True while boolJobNotDone: - status = my_jobs.get_job_status(job_name,job_id) - job_status = status['status'] - if job_status != 'OUTPUT': + status = my_jobs.get_job_status(job_name, job_id) + job_status = status["status"] + if job_status != "OUTPUT": print(f"Status {job_status}") time.sleep(5) else: boolJobNotDone = False # ----------------------------------------------------- -# Get the return code -# ----------------------------------------------------- -job_retcode = status['retcode'] -job_correlator = status['job-correlator'] +# Get the return code +# ----------------------------------------------------- +job_retcode = status["retcode"] +job_correlator = status["job-correlator"] print(f"Job {job_name} ID {job_id} ended with {job_retcode}") # ----------------------------------------------------- # Get all the spool files and dump them in -# ----------------------------------------------------- -output_dir = './output' -my_jobs.get_job_output_as_files(status,output_dir) +# ----------------------------------------------------- +output_dir = "./output" +my_jobs.get_job_output_as_files(status, output_dir) diff --git a/src/_version.py b/src/_version.py index a327e8c8..e5affdee 100644 --- a/src/_version.py +++ b/src/_version.py @@ -1 +1 @@ -__version__ = "1.0.0.dev10" +__version__ = "1.0.0.dev11" diff --git a/src/core/setup.py b/src/core/setup.py index 81a0a0d9..1b46e7ea 100644 --- a/src/core/setup.py +++ b/src/core/setup.py @@ -1,12 +1,17 @@ import sys -from setuptools import setup, find_namespace_packages -sys.path.append("..") + +from setuptools import find_namespace_packages, setup + +sys.path.insert(0, "..") from _version import __version__ +from setup import resolve_sdk_dep setup( name="zowe_core_for_zowe_sdk", version=__version__, description="Zowe Python SDK - Core package", + long_description=open("README.md", 'r').read(), + long_description_content_type="text/markdown", url="https://github.com/zowe/zowe-client-python-sdk", author="Guilherme Cartier", author_email="gcartier94@gmail.com", @@ -16,6 +21,16 @@ "Programming Language :: Python :: 3.7", "License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)", ], - install_requires=["requests", "urllib3", "pyyaml", "commentjson"], + install_requires=[ + "commentjson~=0.9.0", + "deepmerge~=1.1.0", + "jsonschema~=4.17.3", + "pyyaml~=6.0.1", + "requests~=2.31.0", + "urllib3~=1.26.18", + ], + extras_require={ + "secrets": [resolve_sdk_dep("secrets", "~=" + __version__)] + }, packages=find_namespace_packages(include=["zowe.*"]), ) diff --git a/src/core/zowe/core_for_zowe_sdk/__init__.py b/src/core/zowe/core_for_zowe_sdk/__init__.py index ecfd1d22..615b966f 100644 --- a/src/core/zowe/core_for_zowe_sdk/__init__.py +++ b/src/core/zowe/core_for_zowe_sdk/__init__.py @@ -2,13 +2,14 @@ Zowe Python SDK - Core package """ +from .config_file import ConfigFile from .connection import ApiConnection from .constants import constants +from .credential_manager import CredentialManager from .exceptions import * from .profile_manager import ProfileManager from .request_handler import RequestHandler from .sdk_api import SdkApi -from .session_constants import * from .session import Session +from .session_constants import * from .zosmf_profile import ZosmfProfile -from .config_file import ConfigFile diff --git a/src/core/zowe/core_for_zowe_sdk/config_file.py b/src/core/zowe/core_for_zowe_sdk/config_file.py index ef1b912d..2f97458c 100644 --- a/src/core/zowe/core_for_zowe_sdk/config_file.py +++ b/src/core/zowe/core_for_zowe_sdk/config_file.py @@ -10,46 +10,32 @@ Copyright Contributors to the Zowe Project. """ -import base64 +import json import os.path import re -import json -import requests -import sys import warnings +from copy import deepcopy from dataclasses import dataclass, field -from typing import Optional, NamedTuple +from typing import NamedTuple, Optional import commentjson +import requests -from .constants import constants -from .custom_warnings import ( - ProfileNotFoundWarning, - ProfileParsingWarning, - SecurePropsNotFoundWarning, -) -from .exceptions import ProfileNotFound, SecureProfileLoadFailed -from .profile_constants import ( - GLOBAL_CONFIG_NAME, - TEAM_CONFIG, - USER_CONFIG, -) - -HAS_KEYRING = True -try: - import keyring -except ImportError: - HAS_KEYRING = False +from .credential_manager import CredentialManager +from .custom_warnings import ProfileNotFoundWarning, ProfileParsingWarning +from .exceptions import ProfileNotFound +from .profile_constants import GLOBAL_CONFIG_NAME, TEAM_CONFIG, USER_CONFIG +from .validators import validate_config_json HOME = os.path.expanduser("~") -GLOBAl_CONFIG_LOCATION = os.path.join(HOME, ".zowe") -GLOBAL_CONFIG_PATH = os.path.join( - GLOBAl_CONFIG_LOCATION, f"{GLOBAL_CONFIG_NAME}.config.json" -) +GLOBAL_CONFIG_LOCATION = os.path.join(HOME, ".zowe") +GLOBAL_CONFIG_PATH = os.path.join(GLOBAL_CONFIG_LOCATION, f"{GLOBAL_CONFIG_NAME}.config.json") CURRENT_DIR = os.getcwd() # Profile datatype is used by ConfigFile to return Profile Data along with # metadata such as profile_name and secure_props_not_found + + class Profile(NamedTuple): data: dict = {} name: str = "" @@ -80,8 +66,9 @@ class ConfigFile: _location: Optional[str] = None profiles: Optional[dict] = None defaults: Optional[dict] = None - secure_props: Optional[dict] = None schema_property: Optional[dict] = None + secure_props: Optional[dict] = None + jsonc: Optional[dict] = None _missing_secure_props: list = field(default_factory=list) @property @@ -107,7 +94,7 @@ def location(self) -> Optional[str]: @property def schema_path(self) -> Optional[str]: - self.schema_property + return self.schema_property @location.setter def location(self, dirname: str) -> None: @@ -116,25 +103,56 @@ def location(self, dirname: str) -> None: else: raise FileNotFoundError(f"given path {dirname} is not valid") - def init_from_file(self) -> None: + def init_from_file( + self, + validate_schema: Optional[bool] = True, + ) -> None: """ Initializes the class variable after setting filepath (or if not set, autodiscover the file) """ if self.filepath is None: - self.autodiscover_config_dir() + try: + self.autodiscover_config_dir() + except FileNotFoundError: + pass + + if self.filepath is None or not os.path.isfile(self.filepath): + warnings.warn(f"Config file does not exist at {self.filepath}") + return with open(self.filepath, encoding="UTF-8", mode="r") as fileobj: profile_jsonc = commentjson.load(fileobj) self.profiles = profile_jsonc.get("profiles", {}) - self.defaults = profile_jsonc.get("defaults", {}) self.schema_property = profile_jsonc.get("$schema", None) + self.defaults = profile_jsonc.get("defaults", {}) + self.jsonc = profile_jsonc + + if self.schema_property and validate_schema: + self.validate_schema() - # loading secure props is done in load_profile_properties - # since we want to try loading secure properties only when - # we know that the profile has saved properties - # self.load_secure_props() + CredentialManager.load_secure_props() + self.__load_secure_properties() + + def validate_schema(self) -> None: + """ + Get the $schema_property from the config and load the schema + + Returns + ------- + file_path to the $schema property + """ + + path_schema_json = None + + path_schema_json = self.schema_path + if path_schema_json is None: # check if the $schema property is not defined + warnings.warn(f"$schema property could not found") + + # validate the $schema property + if path_schema_json: + validate_config_json(self.jsonc, path_schema_json, cwd=self.location) def schema_list( self, @@ -142,11 +160,11 @@ def schema_list( """ Loads the schema properties in a sorted order according to the priority - + Returns ------- Dictionary - + Returns the profile properties from schema (prop: value) """ @@ -157,23 +175,23 @@ def schema_list( if schema.startswith("https://") or schema.startswith("http://"): schema_json = requests.get(schema).json() + elif os.path.isfile(schema) or schema.startswith("file://"): + with open(schema.replace("file://", "")) as f: + schema_json = json.load(f) + elif not os.path.isabs(schema): schema = os.path.join(self.location, schema) with open(schema) as f: schema_json = json.load(f) - - elif os.path.isfile(schema): - with open(schema) as f: - schema_json = json.load(f) else: return [] - profile_props:dict = {} + profile_props: dict = {} schema_json = dict(schema_json) - - for props in schema_json['properties']['profiles']['patternProperties']["^\\S*$"]["allOf"]: + + for props in schema_json["properties"]["profiles"]["patternProperties"]["^\\S*$"]["allOf"]: props = props["then"] - + while "properties" in props: props = props.pop("properties") profile_props = props @@ -184,6 +202,7 @@ def get_profile( self, profile_name: Optional[str] = None, profile_type: Optional[str] = None, + validate_schema: Optional[bool] = True, ) -> Profile: """ Load given profile including secure properties and excluding values from base profile @@ -193,7 +212,7 @@ def get_profile( Returns a namedtuple called Profile """ if self.profiles is None: - self.init_from_file() + self.init_from_file(validate_schema) if profile_name is None and profile_type is None: raise ProfileNotFound( @@ -202,10 +221,7 @@ def get_profile( ) if profile_name is None: - profile_name = self.get_profilename_from_profiletype( - profile_type=profile_type - ) - + profile_name = self.get_profilename_from_profiletype(profile_type=profile_type) props: dict = self.load_profile_properties(profile_name=profile_name) return Profile(props, profile_name, self._missing_secure_props) @@ -262,7 +278,7 @@ def get_profilename_from_profiletype(self, profile_type: str) -> str: return profilename # iterate through the profiles and check if profile is found - for (key, value) in self.profiles.items(): + for key, value in self.profiles.items(): try: temp_profile_type = value["type"] if profile_type == temp_profile_type: @@ -278,7 +294,7 @@ def get_profilename_from_profiletype(self, profile_type: str) -> str: profile_name=profile_type, error_msg=f"No profile with matching profile_type '{profile_type}' found", ) - + def find_profile(self, path: str, profiles: dict): """ Find a profile at a specified location from within a set of nested profiles @@ -309,7 +325,6 @@ def load_profile_properties(self, profile_name: str) -> dict: Load exact profile properties (without prepopulated fields from base profile) from the profile dict and populate fields from the secure credentials storage """ - props = {} lst = profile_name.split(".") secure_fields: list = [] @@ -318,78 +333,166 @@ def load_profile_properties(self, profile_name: str) -> dict: profile_name = ".".join(lst) profile = self.find_profile(profile_name, self.profiles) if profile is not None: - props = { **profile.get("properties", {}), **props } + props = {**profile.get("properties", {}), **props} secure_fields.extend(profile.get("secure", [])) else: - warnings.warn( - f"Profile {profile_name} not found", - ProfileNotFoundWarning - ) + warnings.warn(f"Profile {profile_name} not found", ProfileNotFoundWarning) lst.pop() + return props - # load secure props only if there are secure fields - if secure_fields: - self.load_secure_props() + def __load_secure_properties(self): + """ + Inject secure properties that have been loaded from the vault into the profiles object. + """ + secure_props = CredentialManager.secure_props.get(self.filepath, {}) + for key, value in secure_props.items(): + segments = [name for i, name in enumerate(key.split(".")) if i % 2 == 1] + profiles_obj = self.profiles + property_name = segments.pop() + for i, profile_name in enumerate(segments): + if profile_name in profiles_obj: + profiles_obj = profiles_obj[profile_name] + if i == len(segments) - 1: + profiles_obj.setdefault("properties", {}) + profiles_obj["properties"][property_name] = value + else: + break + + def __extract_secure_properties(self, profiles_obj, json_path="profiles"): + """ + Extract secure properties from the profiles object so they can be saved to the vault. + """ + secure_props = {} + for key, value in profiles_obj.items(): + for property_name in value.get("secure", []): + if property_name in value.get("properties", {}): + secure_props[f"{json_path}.{key}.properties.{property_name}"] = value["properties"].pop( + property_name + ) + if value.get("profiles"): + secure_props.update(self.__extract_secure_properties(value["profiles"], f"{json_path}.{key}.profiles")) + return secure_props + + def __set_or_create_nested_profile(self, profile_name, profile_data): + """ + Set or create a nested profile. + """ + path = self.get_profile_path_from_name(profile_name) + keys = path.split(".")[1:] + nested_profiles = self.profiles + for key in keys: + nested_profiles = nested_profiles.setdefault(key, {}) + nested_profiles.update(profile_data) + + def __is_secure(self, json_path: str, property_name: str) -> bool: + """ + Check whether the given JSON path corresponds to a secure property. - # load properties with key as profile.{profile_name}.properties.{*} - for (key, value) in self.secure_props.items(): - if re.match( - "profiles\\." + profile_name + "\\.properties\\.[a-z]+", key - ): - property_name = key.split(".")[3] - if property_name in secure_fields: - props[property_name] = value - secure_fields.remove(property_name) + Parameters: + json_path (str): The JSON path of the property to check. + property_name (str): The name of the property to check. - # if len(secure_fields) > 0: - # self._missing_secure_props.extend(secure_fields) + Returns: + bool: True if the property should be stored securely, False otherwise. + """ - return props + profile = self.find_profile(json_path, self.profiles) + if profile and profile.get("secure"): + return property_name in profile["secure"] + return False - def load_secure_props(self) -> None: + def set_property(self, json_path, value, secure=None) -> None: """ - load secure_props stored for the given config file - Returns - ------- - None + Set a property in the profile, storing it securely if necessary. - if keyring is not initialized, set empty value + Parameters: + json_path (str): The JSON path of the property to set. + value (str): The value to be set for the property. + profile_name (str): The name of the profile to set the property in. + secure (bool): If True, the property will be stored securely. Default is None. """ - if not HAS_KEYRING: - self.secure_props = {} - return - - try: - service_name = constants["ZoweServiceName"] - - if sys.platform == "win32": - service_name += "/" + constants["ZoweAccountName"] + if self.profiles is None: + self.init_from_file() - secret_value = keyring.get_password( - service_name, constants["ZoweAccountName"] - ) + # Checking whether the property should be stored securely or in plain text + property_name = json_path.split(".")[-1] + profile_name = self.get_profile_name_from_path(json_path) + # check if the property is already secure + is_property_secure = self.__is_secure(profile_name, property_name) + is_secure = secure if secure is not None else is_property_secure + + current_profile = self.find_profile(profile_name, self.profiles) or {} + current_properties = current_profile.setdefault("properties", {}) + current_secure = current_profile.setdefault("secure", []) + current_properties[property_name] = value + if is_secure and not is_property_secure: + current_secure.append(property_name) + elif not is_secure and is_property_secure: + current_secure.remove(property_name) + + current_profile["properties"] = current_properties + current_profile["secure"] = current_secure + self.__set_or_create_nested_profile(profile_name, current_profile) + + def set_profile(self, profile_path: str, profile_data: dict) -> None: + """ + Set a profile in the config file. - except Exception as exc: - raise SecureProfileLoadFailed( - constants["ZoweServiceName"], error_msg=str(exc) - ) from exc + Parameters: + profile_path (str): The path of the profile to be set. eg: profiles.zosmf + profile_data (dict): The data to be set for the profile. + """ + if self.profiles is None: + self.init_from_file() + profile_name = self.get_profile_name_from_path(profile_path) + if "secure" in profile_data: + # Checking if the profile has a 'secure' field with values + secure_fields = profile_data["secure"] + current_profile = self.find_profile(profile_name, self.profiles) or {} + existing_secure_fields = current_profile.get("secure", []) + new_secure_fields = [field for field in secure_fields if field not in existing_secure_fields] + + # Updating the 'secure' field of the profile with the combined list of secure fields + profile_data["secure"] = existing_secure_fields + new_secure_fields + # If a field is provided in the 'secure' list and its value exists in 'profile_data', remove it + profile_data["properties"] = { + **current_profile.get("properties", {}), + **profile_data.get("properties", {}), + } + self.__set_or_create_nested_profile(profile_name, profile_data) + + def save(self, update_secure_props=True): + """ + Save the config file to disk. and secure props to vault + parameters: + secure_props (bool): If True, the secure properties will be stored in the vault. Default is True. + Returns: + None + """ + # Updating the config file with any changes + if not any(self.profiles.values()): + return - secure_config: str - if sys.platform == "win32": - secure_config = secret_value.encode("utf-16") - else: - secure_config = secret_value + profiles_temp = deepcopy(self.profiles) + secure_props = self.__extract_secure_properties(profiles_temp) + CredentialManager.secure_props[self.filepath] = secure_props + with open(self.filepath, "w") as file: + self.jsonc["profiles"] = profiles_temp + commentjson.dump(self.jsonc, file, indent=4) + if update_secure_props: + CredentialManager.save_secure_props() - secure_config_json = commentjson.loads(base64.b64decode(secure_config).decode()) + def get_profile_name_from_path(self, path: str) -> str: + """ + Get the name of the profile from the given path. + """ + segments = path.split(".") + profile_name = ".".join(segments[i] for i in range(1, len(segments), 2) if segments[i - 1] != "properties") + return profile_name - # look for credentials stored for currently loaded config - try: - self.secure_props = secure_config_json.get(self.filepath, {}) - except KeyError as exc: - error_msg = str(exc) - warnings.warn( - f"No credentials found for loaded config file '{self.filepath}'" - f" with error '{error_msg}'", - SecurePropsNotFoundWarning, - ) + def get_profile_path_from_name(self, short_path: str) -> str: + """ + Get the path of the profile from the given name. + """ + return re.sub(r"(^|\.)", r"\1profiles.", short_path) diff --git a/src/core/zowe/core_for_zowe_sdk/connection.py b/src/core/zowe/core_for_zowe_sdk/connection.py index 756d9138..fa97609c 100644 --- a/src/core/zowe/core_for_zowe_sdk/connection.py +++ b/src/core/zowe/core_for_zowe_sdk/connection.py @@ -27,11 +27,7 @@ class ApiConnection: ssl_verification: bool """ - def __init__(self, - host_url, - user, - password, - ssl_verification=True): + def __init__(self, host_url, user, password, ssl_verification=True): """Construct an ApiConnection object.""" if not host_url or not user or not password: raise MissingConnectionArgs() diff --git a/src/core/zowe/core_for_zowe_sdk/constants.py b/src/core/zowe/core_for_zowe_sdk/constants.py index fa045b66..89767a59 100644 --- a/src/core/zowe/core_for_zowe_sdk/constants.py +++ b/src/core/zowe/core_for_zowe_sdk/constants.py @@ -16,4 +16,5 @@ "ZoweCredentialKey": "Zowe-Plugin", "ZoweServiceName": "Zowe", "ZoweAccountName": "secure_config_props", + "WIN32_CRED_MAX_STRING_LENGTH": 2560, } diff --git a/src/core/zowe/core_for_zowe_sdk/credential_manager.py b/src/core/zowe/core_for_zowe_sdk/credential_manager.py new file mode 100644 index 00000000..3459619c --- /dev/null +++ b/src/core/zowe/core_for_zowe_sdk/credential_manager.py @@ -0,0 +1,154 @@ +"""Zowe Python Client SDK. + +This program and the accompanying materials are made available under the terms of the +Eclipse Public License v2.0 which accompanies this distribution, and is available at + +https://www.eclipse.org/legal/epl-v20.html + +SPDX-License-Identifier: EPL-2.0 + +Copyright Contributors to the Zowe Project. +""" +import base64 +import sys +from typing import Optional + +import commentjson + +from .constants import constants +from .exceptions import SecureProfileLoadFailed + +HAS_KEYRING = True +try: + from zowe.secrets_for_zowe_sdk import keyring +except ImportError: + HAS_KEYRING = False + + +class CredentialManager: + secure_props = {} + + @staticmethod + def load_secure_props() -> None: + """ + load secure_props stored for the given config file + Returns + ------- + None + + if keyring is not initialized, set empty value + """ + if not HAS_KEYRING: + CredentialManager.secure_props = {} + return + + try: + secret_value = CredentialManager._get_credential(constants["ZoweServiceName"], constants["ZoweAccountName"]) + # Handle the case when secret_value is None + if secret_value is None: + return + + except Exception as exc: + raise SecureProfileLoadFailed(constants["ZoweServiceName"], error_msg=str(exc)) from exc + + secure_config: str + secure_config = secret_value.encode() + secure_config_json = commentjson.loads(base64.b64decode(secure_config).decode()) + # update the secure props + CredentialManager.secure_props = secure_config_json + + @staticmethod + def save_secure_props() -> None: + """ + Set secure_props for the given config file + Returns + ------- + None + """ + if not HAS_KEYRING: + return + + credential = CredentialManager.secure_props + # Check if credential is a non-empty string + if credential: + encoded_credential = base64.b64encode(commentjson.dumps(credential).encode()).decode() + if sys.platform == "win32": + # Delete the existing credential + CredentialManager._delete_credential(constants["ZoweServiceName"], constants["ZoweAccountName"]) + CredentialManager._set_credential(constants["ZoweServiceName"], constants["ZoweAccountName"], encoded_credential) + + @staticmethod + def _get_credential(service_name: str, account_name: str) -> Optional[str]: + """ + Retrieve the credential from the keyring or storage. + If the credential exceeds the maximum length, retrieve it in parts. + Parameters + ---------- + service_name: str + The service name for the credential retrieval + Returns + ------- + str + The retrieved encoded credential + """ + encoded_credential = keyring.get_password(service_name, account_name) + if encoded_credential is None and sys.platform == "win32": + # Retrieve the secure value with an index + index = 1 + temp_value = keyring.get_password(service_name, f"{account_name}-{index}") + while temp_value is not None: + if encoded_credential is None: + encoded_credential = temp_value + else: + encoded_credential += temp_value + index += 1 + temp_value = keyring.get_password(service_name, f"{account_name}-{index}") + + if encoded_credential is not None and encoded_credential.endswith("\0"): + encoded_credential = encoded_credential[:-1] + + return encoded_credential + + @staticmethod + def _set_credential(service_name: str, account_name: str, encoded_credential: str) -> None: + # Check if the encoded credential exceeds the maximum length for win32 + if sys.platform == "win32" and len(encoded_credential) > constants["WIN32_CRED_MAX_STRING_LENGTH"]: + # Split the encoded credential string into chunks of maximum length + chunk_size = constants["WIN32_CRED_MAX_STRING_LENGTH"] + encoded_credential += "\0" + chunks = [encoded_credential[i : i + chunk_size] for i in range(0, len(encoded_credential), chunk_size)] + # Set the individual chunks as separate keyring entries + for index, chunk in enumerate(chunks, start=1): + field_name = f"{account_name}-{index}" + keyring.set_password(service_name, field_name, chunk) + + else: + # Credential length is within the maximum limit or not on win32, set it as a single keyring entry + keyring.set_password(service_name, account_name, encoded_credential) + + @staticmethod + def _delete_credential(service_name: str, account_name: str) -> None: + """ + Delete the credential from the keyring or storage. + If the keyring.delete_password function is not available, iterate through and delete credentials. + Parameters + ---------- + service_name: str + The service name for the credential deletion + account_name: str + The account name for the credential deletion + Returns + ------- + None + """ + + keyring.delete_password(service_name, account_name) + + # Handling multiple credentials stored when the operating system is Windows + if sys.platform == "win32": + index = 1 + while True: + field_name = f"{account_name}-{index}" + if not keyring.delete_password(service_name, field_name): + break + index += 1 diff --git a/src/core/zowe/core_for_zowe_sdk/exceptions.py b/src/core/zowe/core_for_zowe_sdk/exceptions.py index e3d4b849..1c8ed618 100644 --- a/src/core/zowe/core_for_zowe_sdk/exceptions.py +++ b/src/core/zowe/core_for_zowe_sdk/exceptions.py @@ -39,7 +39,7 @@ def __init__(self, expected, received, request_output): The output from the request """ super().__init__( - "The status code from z/OSMF was {} it was expected {}. \n {}".format( + "The status code from z/OSMF was: {}\nExpected: {}\nRequest output: {}".format( received, expected, request_output ) ) @@ -57,11 +57,7 @@ def __init__(self, status_code, request_output): request_output The output from the request """ - super().__init__( - "HTTP Request has failed with status code {}. \n {}".format( - status_code, request_output - ) - ) + super().__init__("HTTP Request has failed with status code {}. \n {}".format(status_code, request_output)) class FileNotFound(Exception): @@ -100,11 +96,7 @@ def __init__(self, profile_name: str = "unknown", error_msg: str = "error"): error_msg The error message received while trying to load the profile """ - super().__init__( - "Failed to load secure profile '{}' because '{}'".format( - profile_name, error_msg - ) - ) + super().__init__("Failed to load secure profile '{}' because '{}'".format(profile_name, error_msg)) class ProfileNotFound(Exception): @@ -120,9 +112,7 @@ def __init__(self, profile_name: str = "unknown", error_msg: str = "error"): The error message received while trying to load the profile """ - super().__init__( - "Failed to load profile '{}' because '{}'".format(profile_name, error_msg) - ) + super().__init__("Failed to load profile '{}' because '{}'".format(profile_name, error_msg)) class SecureValuesNotFound(Exception): diff --git a/src/core/zowe/core_for_zowe_sdk/profile_manager.py b/src/core/zowe/core_for_zowe_sdk/profile_manager.py index 70eda0cf..20a6b38c 100644 --- a/src/core/zowe/core_for_zowe_sdk/profile_manager.py +++ b/src/core/zowe/core_for_zowe_sdk/profile_manager.py @@ -10,12 +10,17 @@ Copyright Contributors to the Zowe Project. """ -import os.path import os +import os.path import warnings +from copy import deepcopy from typing import Optional +import jsonschema +from deepmerge import always_merger + from .config_file import ConfigFile, Profile +from .credential_manager import CredentialManager from .custom_warnings import ( ConfigNotFoundWarning, ProfileNotFoundWarning, @@ -33,10 +38,8 @@ HOME = os.path.expanduser("~") -GLOBAl_CONFIG_LOCATION = os.path.join(HOME, ".zowe") -GLOBAL_CONFIG_PATH = os.path.join( - GLOBAl_CONFIG_LOCATION, f"{GLOBAL_CONFIG_NAME}.config.json" -) +GLOBAL_CONFIG_LOCATION = os.path.join(HOME, ".zowe") +GLOBAL_CONFIG_PATH = os.path.join(GLOBAL_CONFIG_LOCATION, f"{GLOBAL_CONFIG_NAME}.config.json") CURRENT_DIR = os.getcwd() @@ -56,7 +59,7 @@ def __init__(self, appname: str = "zowe", show_warnings: bool = True): self.global_config = ConfigFile(type=TEAM_CONFIG, name=GLOBAL_CONFIG_NAME) try: - self.global_config.location = GLOBAl_CONFIG_LOCATION + self.global_config.location = GLOBAL_CONFIG_LOCATION except Exception: warnings.warn( "Could not find Global Config Directory, please provide one.", @@ -65,7 +68,7 @@ def __init__(self, appname: str = "zowe", show_warnings: bool = True): self.global_user_config = ConfigFile(type=USER_CONFIG, name=GLOBAL_CONFIG_NAME) try: - self.global_user_config.location = GLOBAl_CONFIG_LOCATION + self.global_user_config.location = GLOBAL_CONFIG_LOCATION except Exception: warnings.warn( "Could not find Global User Config Directory, please provide one.", @@ -116,50 +119,50 @@ def get_env( ) -> dict: """ Maps the env variables to the profile properties - + Returns ------- Dictionary Containing profile properties from env variables (prop: value) """ - + props = cfg.schema_list() if props == []: return {} - + env, env_var = {}, {} - + for var in list(os.environ.keys()): if var.startswith("ZOWE_OPT"): - env[var[len("ZOWE_OPT_"):].lower()] = os.environ.get(var) - + env[var[len("ZOWE_OPT_") :].lower()] = os.environ.get(var) + for k, v in env.items(): word = k.split("_") if len(word) > 1: - k = word[0]+word[1].capitalize() + k = word[0] + word[1].capitalize() else: k = word[0] if k in list(props.keys()): - if props[k]['type'] == "number": + if props[k]["type"] == "number": env_var[k] = int(v) - elif props[k]['type'] == "string": + elif props[k]["type"] == "string": env_var[k] = str(v) - elif props[k]['type'] == "boolean": + elif props[k]["type"] == "boolean": env_var[k] = bool(v) return env_var - + @staticmethod def get_profile( cfg: ConfigFile, profile_name: Optional[str], profile_type: Optional[str], - config_type: str, + validate_schema: Optional[bool] = True, ) -> Profile: """ Get just the profile from the config file (overriden with base props in the config file) @@ -174,8 +177,22 @@ def get_profile( cfg_profile = Profile() try: cfg_profile = cfg.get_profile( - profile_name=profile_name, profile_type=profile_type + profile_name=profile_name, profile_type=profile_type, validate_schema=validate_schema + ) + except jsonschema.exceptions.ValidationError as exc: + raise jsonschema.exceptions.ValidationError( + f"Instance was invalid under the provided $schema property, {exc}" ) + except jsonschema.exceptions.SchemaError as exc: + raise jsonschema.exceptions.SchemaError(f"The provided schema is invalid, {exc}") + except jsonschema.exceptions.UndefinedTypeCheck as exc: + raise jsonschema.exceptions.UndefinedTypeCheck( + f"A type checker was asked to check a type it did not have registered, {exc}" + ) + except jsonschema.exceptions.UnknownType as exc: + raise jsonschema.exceptions.UnknownType(f"Unknown type is found in schema_json, exc") + except jsonschema.exceptions.FormatError as exc: + raise jsonschema.exceptions.FormatError(f"Validating a format config_json failed for schema_json, {exc}") except ProfileNotFound: if profile_name: warnings.warn( @@ -188,38 +205,20 @@ def get_profile( f" instead.", ProfileNotFoundWarning, ) - except SecureProfileLoadFailed: - warnings.warn( - f"Config '{cfg.filename}' has no saved secure properties.", - SecurePropsNotFoundWarning, - ) - except SecurePropsNotFoundWarning: - if profile_name: - warnings.warn( - f"Secure properties of profile '{profile_name}' from file '{cfg.filename}' were not found " - f"hence profile not loaded.", - SecurePropsNotFoundWarning, - ) - else: - warnings.warn( - f"Secure properties of profile type '{profile_type}' from file '{cfg.filename}' were not found " - f"hence profile not loaded.", - SecurePropsNotFoundWarning, - ) except Exception as exc: warnings.warn( - f"Could not load {config_type} '{cfg.filename}' at '{cfg.filepath}'" - f"because {type(exc).__name__}'{exc}'.", + f"Could not load '{cfg.filename}' at '{cfg.filepath}'" f"because {type(exc).__name__}'{exc}'.", ConfigNotFoundWarning, ) - finally: - return cfg_profile + + return cfg_profile def load( self, profile_name: Optional[str] = None, profile_type: Optional[str] = None, check_missing_props: bool = True, + validate_schema: Optional[bool] = True, override_with_env: Optional[bool] = False, ) -> dict: """Load connection details from a team config profile. @@ -248,36 +247,59 @@ def load( if not self._show_warnings: warnings.simplefilter("ignore") - config_layers = { - "Project User Config": self.project_user_config, - "Project Config": self.project_config, - "Global User Config": self.global_user_config, - "Global Config": self.global_config, - } profile_props: dict = {} env_var: dict = {} - missing_secure_props = [] # track which secure props were not loaded - for i, (config_type, cfg) in enumerate(config_layers.items()): - profile_loaded = self.get_profile( - cfg, profile_name, profile_type, config_type - ) - # TODO Why don't user and password show up here for Project User Config? - # Probably need to update load_profile_properties method in config_file.py - if profile_loaded.name and not profile_name: - profile_name = ( - profile_loaded.name - ) # Define profile name that will be merged from other layers - profile_props = {**profile_loaded.data, **profile_props} - + defaults_merged: dict = {} + profiles_merged: dict = {} + cfg_name = None + cfg_schema = None + + for cfg_layer in (self.project_user_config, self.project_config, self.global_user_config, self.global_config): + if cfg_layer.profiles is None: + try: + cfg_layer.init_from_file(validate_schema) + except SecureProfileLoadFailed: + warnings.warn( + f"Could not load secure properties for {cfg_layer.filepath}", + SecurePropsNotFoundWarning, + ) + if cfg_layer.defaults: + for name, value in cfg_layer.defaults.items(): + defaults_merged[name] = defaults_merged.get(name, value) + if not cfg_name and cfg_layer.name: + cfg_name = cfg_layer.name + if not cfg_schema and cfg_layer.schema_property: + cfg_schema = cfg_layer.schema_property + + usrProject = self.project_user_config.profiles or {} + project = self.project_config.profiles or {} + project_temp = always_merger.merge(deepcopy(project), usrProject) + + usrGlobal = self.global_user_config.profiles or {} + global_ = self.global_config.profiles or {} + global_temp = always_merger.merge(deepcopy(global_), usrGlobal) + + profiles_merged = project_temp + for name, value in global_temp.items(): + if name not in profiles_merged: + profiles_merged[name] = value + + cfg = ConfigFile( + type="Merged Config", + name=cfg_name, + profiles=profiles_merged, + defaults=defaults_merged, + schema_property=cfg_schema, + ) + profile_loaded = self.get_profile(cfg, profile_name, profile_type, validate_schema) + if profile_loaded: + profile_props = profile_loaded.data missing_secure_props.extend(profile_loaded.missing_secure_props) - if override_with_env: - env_var = {**self.get_env(cfg)} - - if i == 1 and profile_props: - break # Skip loading from global config if profile was found in project config + if override_with_env: + env_var = {**self.get_env(cfg)} if profile_type != BASE_PROFILE: profile_props = { @@ -296,8 +318,93 @@ def load( warnings.resetwarnings() - for k, v in profile_props.items(): + for k in profile_props: if k in env_var: profile_props[k] = env_var[k] return profile_props + + def get_highest_priority_layer(self, json_path: str) -> Optional[ConfigFile]: + """ + Get the highest priority layer (configuration file) based on the given profile name + + Parameters: + profile_name (str): The name of the profile to look for in the layers. + + Returns: + Optional[ConfigFile]: The highest priority layer (configuration file) that contains the specified profile, + or None if the profile is not found in any layer. + """ + highest_layer = None + longest_match = "" + layers = [self.project_user_config, self.project_config, self.global_user_config, self.global_config] + + original_name = layers[0].get_profile_name_from_path(json_path) + + for layer in layers: + try: + layer.init_from_file() + except FileNotFoundError: + continue + parts = original_name.split(".") + current_name = "" + + while parts: + current_name = ".".join(parts) + profile = layer.find_profile(current_name, layer.profiles) + + if profile is not None and len(current_name) > len(longest_match): + highest_layer = layer + longest_match = current_name + + else: + parts.pop() + if original_name == longest_match: + break + + if highest_layer is None: + highest_layer = layer + + if highest_layer is None: + raise FileNotFoundError(f"Could not find a valid layer for {json_path}") + + return highest_layer + + def set_property(self, json_path, value, secure=None) -> None: + """ + Set a property in the profile, storing it securely if necessary. + + Parameters: + json_path (str): The JSON path of the property to set. + value (str): The value to be set for the property. + secure (bool): If True, the property will be stored securely. Default is None. + """ + + # highest priority layer for the given profile name + highest_priority_layer = self.get_highest_priority_layer(json_path) + + # Set the property in the highest priority layer + + highest_priority_layer.set_property(json_path, value, secure=secure) + + def set_profile(self, profile_path: str, profile_data: dict) -> None: + """ + Set a profile in the highest priority layer (configuration file) based on the given profile name + + Parameters: + profile_path (str): TThe path of the profile to be set. eg: profiles.zosmf + profile_data (dict): The data of the profile to set. + """ + highest_priority_layer = self.get_highest_priority_layer(profile_path) + + highest_priority_layer.set_profile(profile_path, profile_data) + + def save(self) -> None: + """ + Save the layers (configuration files) to disk. + """ + layers = [self.project_user_config, self.project_config, self.global_user_config, self.global_config] + + for layer in layers: + layer.save(False) + CredentialManager.save_secure_props() diff --git a/src/core/zowe/core_for_zowe_sdk/request_handler.py b/src/core/zowe/core_for_zowe_sdk/request_handler.py index 29c05ab2..adb271e0 100644 --- a/src/core/zowe/core_for_zowe_sdk/request_handler.py +++ b/src/core/zowe/core_for_zowe_sdk/request_handler.py @@ -10,12 +10,11 @@ Copyright Contributors to the Zowe Project. """ -from .exceptions import UnexpectedStatus -from .exceptions import RequestFailed -from .exceptions import InvalidRequestMethod import requests import urllib3 +from .exceptions import InvalidRequestMethod, RequestFailed, UnexpectedStatus + class RequestHandler: """ @@ -44,7 +43,7 @@ def __init__(self, session_arguments): def __handle_ssl_warnings(self): """Turn off warnings if the SSL verification argument if off.""" - if not self.session_arguments['verify']: + if not self.session_arguments["verify"]: urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def perform_request(self, method, request_arguments, expected_code=[200]): @@ -142,8 +141,8 @@ def __normalize_response(self): ------- A bytes object if the response content type is application/octet-stream, a normalized JSON for the request response otherwise - """ - if self.response.headers.get('Content-Type') == 'application/octet-stream': + """ + if self.response.headers.get("Content-Type") == "application/octet-stream": return self.response.content else: try: diff --git a/src/core/zowe/core_for_zowe_sdk/sdk_api.py b/src/core/zowe/core_for_zowe_sdk/sdk_api.py index 7234f3fb..146f29aa 100644 --- a/src/core/zowe/core_for_zowe_sdk/sdk_api.py +++ b/src/core/zowe/core_for_zowe_sdk/sdk_api.py @@ -10,10 +10,12 @@ Copyright Contributors to the Zowe Project. """ +import urllib + +from . import session_constants from .exceptions import UnsupportedAuthType from .request_handler import RequestHandler -from .session import Session, ISession -from . import session_constants +from .session import ISession, Session class SdkApi: @@ -21,7 +23,7 @@ class SdkApi: Abstract class used to represent the base SDK API. """ - def __init__(self, profile, default_url): + def __init__(self, profile, default_url): self.profile = profile session = Session(profile) self.session: ISession = session.load() @@ -60,3 +62,14 @@ def _create_custom_request_arguments(self): dictionary creation """ return self.request_arguments.copy() + + def _encode_uri_component(self, str_to_adjust): + """Adjust string to be correct in a URL + + Returns + ------- + adjusted_str + A string with special characters, acceptable for a URL + """ + + return urllib.parse.quote(str_to_adjust, safe="!~*'()") if str_to_adjust is not None else None diff --git a/src/core/zowe/core_for_zowe_sdk/session.py b/src/core/zowe/core_for_zowe_sdk/session.py index 5d5cc6cb..54647f38 100644 --- a/src/core/zowe/core_for_zowe_sdk/session.py +++ b/src/core/zowe/core_for_zowe_sdk/session.py @@ -45,7 +45,7 @@ def __init__(self, props: dict) -> None: if props.get("host") is not None: self.session: ISession = ISession(host=props.get("host")) else: - raise "Host must be supplied" + raise Exception("Host must be supplied") # determine authentication type if props.get("user") is not None and props.get("password") is not None: @@ -61,15 +61,13 @@ def __init__(self, props: dict) -> None: self.session.tokenValue = props.get("tokenValue") self.session.type = session_constants.AUTH_TYPE_BEARER else: - raise "An authentication method must be supplied" + raise Exception("An authentication method must be supplied") # set additional parameters self.session.basePath = props.get("basePath") self.session.port = props.get("port", self.session.port) self.session.protocol = props.get("protocol", self.session.protocol) - self.session.rejectUnauthorized = props.get( - "rejectUnauthorized", self.session.rejectUnauthorized - ) + self.session.rejectUnauthorized = props.get("rejectUnauthorized", self.session.rejectUnauthorized) def load(self) -> ISession: return self.session diff --git a/src/core/zowe/core_for_zowe_sdk/validators.py b/src/core/zowe/core_for_zowe_sdk/validators.py index c1a11719..cd6b2332 100644 --- a/src/core/zowe/core_for_zowe_sdk/validators.py +++ b/src/core/zowe/core_for_zowe_sdk/validators.py @@ -10,11 +10,15 @@ Copyright Contributors to the Zowe Project. """ +import os +from typing import Optional, Union + import commentjson +import requests from jsonschema import validate -def validate_config_json(path_config_json: str, path_schema_json: str): +def validate_config_json(path_config_json: Union[str, dict], path_schema_json: str, cwd: str): """ Function validating that zowe.config.json file matches zowe.schema.json. @@ -31,10 +35,29 @@ def validate_config_json(path_config_json: str, path_schema_json: str): Provides details if config.json doesn't match schema.json, otherwise it returns None. """ - with open(path_config_json) as file: - config_json = commentjson.load(file) - - with open(path_schema_json) as file: - schema_json = commentjson.load(file) + # checks if the path_schema_json point to an internet URI and download the schema using the URI + if path_schema_json.startswith("https://") or path_schema_json.startswith("http://"): + schema_json = requests.get(path_schema_json).json() + + # checks if the path_schema_json is a file + elif os.path.isfile(path_schema_json) or path_schema_json.startswith("file://"): + with open(path_schema_json.replace("file://", "")) as file: + schema_json = commentjson.load(file) + + # checks if the path_schema_json is absolute + elif not os.path.isabs(path_schema_json): + path_schema_json = os.path.join(cwd, path_schema_json) + with open(path_schema_json) as file: + schema_json = commentjson.load(file) + + # if there is no path_schema_json it will return None + else: + return None + + if isinstance(path_config_json, str): + with open(path_config_json) as file: + config_json = commentjson.load(file) + else: + config_json = path_config_json return validate(instance=config_json, schema=schema_json) diff --git a/src/core/zowe/core_for_zowe_sdk/zosmf_profile.py b/src/core/zowe/core_for_zowe_sdk/zosmf_profile.py index 0594ca10..5ab3ea41 100644 --- a/src/core/zowe/core_for_zowe_sdk/zosmf_profile.py +++ b/src/core/zowe/core_for_zowe_sdk/zosmf_profile.py @@ -13,15 +13,16 @@ import base64 import os.path import sys + import yaml +from .connection import ApiConnection from .constants import constants from .exceptions import SecureProfileLoadFailed -from .connection import ApiConnection HAS_KEYRING = True try: - import keyring + from zowe.secrets_for_zowe_sdk import keyring except ImportError: HAS_KEYRING = False @@ -32,8 +33,8 @@ class ZosmfProfile: Description ----------- - This class is only used when there is already a Zowe z/OSMF profile created - and the user opted to use the profile instead of passing the credentials directly + This class is only used when there is already a Zowe z/OSMF profile created + and the user opted to use the profile instead of passing the credentials directly in the object constructor. Attributes @@ -67,9 +68,7 @@ def load(self): zosmf_connection z/OSMF connection object """ - profile_file = os.path.join( - self.profiles_dir, "{}.yaml".format(self.profile_name) - ) + profile_file = os.path.join(self.profiles_dir, "{}.yaml".format(self.profile_name)) with open(profile_file, "r") as fileobj: profile_yaml = yaml.safe_load(fileobj) @@ -80,30 +79,26 @@ def load(self): zosmf_user = profile_yaml["user"] zosmf_password = profile_yaml["password"] - if zosmf_user.startswith( + if zosmf_user.startswith(constants["SecureValuePrefix"]) and zosmf_password.startswith( constants["SecureValuePrefix"] - ) and zosmf_password.startswith(constants["SecureValuePrefix"]): + ): zosmf_user, zosmf_password = self.__load_secure_credentials() zosmf_ssl_verification = True if "rejectUnauthorized" in profile_yaml: zosmf_ssl_verification = profile_yaml["rejectUnauthorized"] - return ApiConnection( - zosmf_host, zosmf_user, zosmf_password, zosmf_ssl_verification - ) + return ApiConnection(zosmf_host, zosmf_user, zosmf_password, zosmf_ssl_verification) def __get_secure_value(self, name): service_name = constants["ZoweCredentialKey"] account_name = "zosmf_{}_{}".format(self.profile_name, name) - if sys.platform == "win32": - service_name += "/" + account_name - secret_value = keyring.get_password(service_name, account_name) - if sys.platform == "win32": - secret_value = secret_value.encode("utf-16") + # Handle the case when secret_value is None + if secret_value is None: + secret_value = "" secret_value = base64.b64decode(secret_value).decode().strip('"') @@ -112,9 +107,7 @@ def __get_secure_value(self, name): def __load_secure_credentials(self): """Load secure credentials for a z/OSMF profile.""" if not HAS_KEYRING: - raise SecureProfileLoadFailed( - self.profile_name, "Keyring module not installed" - ) + raise SecureProfileLoadFailed(self.profile_name, "Keyring module not installed") try: zosmf_user = self.__get_secure_value("user") @@ -123,37 +116,3 @@ def __load_secure_credentials(self): raise SecureProfileLoadFailed(self.profile_name, e) else: return (zosmf_user, zosmf_password) - - -if HAS_KEYRING and sys.platform.startswith("linux"): - from contextlib import closing - from keyring.backends import SecretService - - class KeyringBackend(SecretService.Keyring): - """ - Class used to handle secured profiles. - - Methods - ------- - get_password(service, username) - Get the decoded password - """ - - def __get_password(self, service, username, collection): - items = collection.search_items({"account": username, "service": service}) - for item in items: - if hasattr(item, "unlock"): - if item.is_locked() and item.unlock()[0]: - raise keyring.errors.InitError("failed to unlock item") - return item.get_secret().decode("utf-8") - - def get_password(self, service, username): - """Get password of the username for the service.""" - collection = self.get_preferred_collection() - if hasattr(collection, "connection"): - with closing(collection.connection): - return self.__get_password(service, username, collection) - else: - return self.__get_password(service, username, collection) - - keyring.set_keyring(KeyringBackend()) diff --git a/src/secrets/.gitignore b/src/secrets/.gitignore new file mode 100644 index 00000000..c8f04429 --- /dev/null +++ b/src/secrets/.gitignore @@ -0,0 +1,72 @@ +/target + +# Byte-compiled / optimized / DLL files +__pycache__/ +.pytest_cache/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +.venv/ +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +include/ +man/ +venv/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +pip-selfcheck.json + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +.DS_Store + +# Sphinx documentation +docs/_build/ + +# PyCharm +.idea/ + +# VSCode +.vscode/ + +# Pyenv +.python-version diff --git a/src/secrets/Cargo.lock b/src/secrets/Cargo.lock new file mode 100644 index 00000000..06a47635 --- /dev/null +++ b/src/secrets/Cargo.lock @@ -0,0 +1,767 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "cfg-expr" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "futures-channel" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" + +[[package]] +name = "futures-macro" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gio" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57052f84e8e5999b258e8adf8f5f2af0ac69033864936b8b6838321db2f759b1" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c316afb01ce8067c5eaab1fc4f2cd47dc21ce7b6296358605e2ffab23ccbd19" +dependencies = [ + "bitflags 2.4.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8da903822b136d42360518653fcf154455defc437d3e7a81475bf9a95ff1e47" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "indexmap" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + +[[package]] +name = "keyring" +version = "1.0.0-dev12" +dependencies = [ + "pyo3", + "secrets_core", +] + +[[package]] +name = "libc" +version = "0.2.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" + +[[package]] +name = "libsecret" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6fae6ebe590e06ef9d01b125e46b7d4c05ccbd5961f12b4aefe2ecd010220f" +dependencies = [ + "gio", + "glib", + "libc", + "libsecret-sys", +] + +[[package]] +name = "libsecret-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b716fc5e1c82eb0d28665882628382ab0e0a156a6d73580e33f0ac6ac8d2540" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8453b658fe480c3e70c8ed4e3d3ec33eb74988bd186561b0cc66b85c3bc4b" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96fe70b176a89cff78f2fa7b3c930081e163d5379b4dcdf993e3ae29ca662e5" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214929900fd25e6604661ed9cf349727c8920d47deff196c4e28165a6ef2a96b" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac53072f717aa1bfa4db832b39de8c875b7c7af4f4a6fe93cdbf9264cf8383b" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7774b5a8282bd4f25f803b1f0d945120be959a36c72e08e7cd031c792fdfd424" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrets_core" +version = "0.1.0" +source = "git+https://github.com/zowe/zowe-cli.git?branch=master#1b4d492d35ab44145e823821245a7e08de7d9192" +dependencies = [ + "cfg-if", + "core-foundation", + "core-foundation-sys", + "gio", + "glib", + "glib-sys", + "libsecret", + "libsecret-sys", + "thiserror", + "windows-sys", +] + +[[package]] +name = "serde" +version = "1.0.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94af52f9402f94aac4948a2518b43359be8d9ce6cd9efc1c4de3b2f7b7e897d6" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "toml" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.7", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c" +dependencies = [ + "memchr", +] diff --git a/src/secrets/Cargo.toml b/src/secrets/Cargo.toml new file mode 100644 index 00000000..6262e3e0 --- /dev/null +++ b/src/secrets/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "keyring" +readme = "README.md" +repository = "https://github.com/zowe/zowe-client-python-sdk" +version = "1.0.0-dev12" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "keyring" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { version = "0.20.0", features = ["abi3-py37"] } +secrets_core = { git = "https://github.com/zowe/zowe-cli.git", branch = "master" } diff --git a/src/secrets/README.md b/src/secrets/README.md new file mode 100644 index 00000000..98a9a7ba --- /dev/null +++ b/src/secrets/README.md @@ -0,0 +1,22 @@ +Secrets Package +================== + +Contains APIs to store, retrieve, and delete credentials in the end user's operating system (OS) keyring. + +This Python package requires the OS keyring to be unlocked before credentials can stored or retrieved. Please follow the [installation guidelines for Zowe CLI](https://docs.zowe.org/stable/user-guide/cli-installcli#installation-guidelines) to ensure that the Secure Credential Store is accessible. + +If you are using a headless Linux environment, please consult the following article on Zowe Docs: [Configuring Secure Credential Store on headless Linux operating systems](https://docs.zowe.org/stable/user-guide/cli-configure-scs-on-headless-linux-os). + +Example +------------ +```py +from zowe.secrets_for_zowe_sdk import keyring +# Store a short password using the keyring module: +password = "Zowe ❕" +keyring.set_password("Test", "ShortPassword", password) +# Retrieving a password under a given service and account: +assert keyring.get_password("Test", "ShortPassword") == password +# Deleting a password: +assert keyring.delete_password("Test", "ShortPassword") +assert keyring.get_password("Test", "ShortPassword") is None +``` \ No newline at end of file diff --git a/src/secrets/pyproject.toml b/src/secrets/pyproject.toml new file mode 100644 index 00000000..ba4c13b5 --- /dev/null +++ b/src/secrets/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = ["maturin>=1.3,<2.0"] +build-backend = "maturin" + +[project] +name = "zowe_secrets_for_zowe_sdk" +description = "Zowe Python SDK - Client Secrets package" +license = "EPL-2.0" +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", +] +dynamic = ["version"] + +[tool.maturin] +compatibility = "manylinux2014" +features = ["pyo3/extension-module"] +module-name = "zowe.secrets_for_zowe_sdk.keyring" +skip-auditwheel = true +strip = true diff --git a/src/secrets/scripts/configure-cross.sh b/src/secrets/scripts/configure-cross.sh new file mode 100755 index 00000000..78caf762 --- /dev/null +++ b/src/secrets/scripts/configure-cross.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Set environment variables needed for cross-compilation in GITHUB_ENV +set_env() { + echo "CROSS_DEB_ARCH=$1" >> $GITHUB_ENV + echo "PKG_CONFIG_SYSROOT_DIR=/" >> $GITHUB_ENV + echo "RUSTFLAGS=-L $2 $RUSTFLAGS" >> $GITHUB_ENV + echo "PKG_CONFIG_PATH=$2/pkgconfig" >> $GITHUB_ENV +} + +case "$1" in + "aarch64") + set_env arm64 "/usr/lib/aarch64-linux-gnu" + ;; + "armv7") + set_env armhf "/usr/lib/arm-linux-gnueabihf" + ;; + "ppc64le") + set_env ppc64el "/usr/lib/powerpc64le-linux-gnu" + ;; + "s390x") + set_env s390x "/usr/lib/s390x-linux-gnu" + ;; + "x86") + set_env i686 "/usr/lib/i386-linux-gnu" + ;; + "x86_64") + set_env x86_64 "/usr/lib/x86_64-linux-gnu" + ;; + *) + ;; +esac diff --git a/src/secrets/src/lib.rs b/src/secrets/src/lib.rs new file mode 100644 index 00000000..aa012451 --- /dev/null +++ b/src/secrets/src/lib.rs @@ -0,0 +1,57 @@ +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; + +extern crate secrets_core; +use secrets_core::*; + +#[pyfunction] +fn set_password(service: String, account: String, password: String) -> PyResult<()> { + match os::set_password(&service, &account, &password) { + Ok(_) => Ok(()), + Err(e) => Err(PyValueError::new_err(format!("{:#?}", e))), + } +} + +#[pyfunction] +fn get_password(service: String, account: String) -> PyResult> { + match os::get_password(&service, &account) { + Ok(pw) => Ok(pw), + Err(e) => Err(PyValueError::new_err(format!("{:#?}", e))), + } +} + +#[pyfunction] +fn delete_password(service: String, account: String) -> PyResult { + match os::delete_password(&service, &account) { + Ok(res) => Ok(res), + Err(e) => Err(PyValueError::new_err(format!("{:#?}", e))), + } +} + +#[pyfunction] +fn find_password(service: String) -> PyResult> { + match os::find_password(&service) { + Ok(res) => Ok(res), + Err(e) => Err(PyValueError::new_err(format!("{:#?}", e))), + } +} + +#[pyfunction] +fn find_credentials(service: String) -> PyResult> { + let mut creds: Vec<(String, String)> = vec![]; + match os::find_credentials(&service, &mut creds) { + Ok(res) => Ok(creds), + Err(e) => Err(PyValueError::new_err(format!("{:#?}", e))), + } +} + +/// A Python module implemented in Rust. +#[pymodule] +fn keyring(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(get_password, m)?)?; + m.add_function(wrap_pyfunction!(set_password, m)?)?; + m.add_function(wrap_pyfunction!(delete_password, m)?)?; + m.add_function(wrap_pyfunction!(find_password, m)?)?; + m.add_function(wrap_pyfunction!(find_credentials, m)?)?; + Ok(()) +} diff --git a/src/secrets/zowe/secrets_for_zowe_sdk/__init__.py b/src/secrets/zowe/secrets_for_zowe_sdk/__init__.py new file mode 100644 index 00000000..f80fb08b --- /dev/null +++ b/src/secrets/zowe/secrets_for_zowe_sdk/__init__.py @@ -0,0 +1,5 @@ +""" +Zowe Python SDK - Client Secrets package +""" + +from . import keyring diff --git a/src/setup.py b/src/setup.py index b69b595b..5b65a608 100644 --- a/src/setup.py +++ b/src/setup.py @@ -1,11 +1,14 @@ import os.path import uuid + from setuptools import setup + from _version import __version__ src_dir = os.path.realpath(os.path.dirname(__file__)) uuid4 = uuid.uuid4() + def resolve_sdk_dep(sdk_name, version_spec): # if os.path.exists(os.path.join(src_dir, sdk_name, "zowe")): # # Handle building from a Git checkout @@ -15,11 +18,14 @@ def resolve_sdk_dep(sdk_name, version_spec): # else: return f"zowe.{sdk_name}_for_zowe_sdk{version_spec}" + if __name__ == "__main__": setup( - name='zowe', + name="zowe", version=__version__, - description='Zowe Python SDK', + description="Zowe Python SDK", + long_description=open("../README.md", 'r').read(), + long_description_content_type="text/markdown", url="https://github.com/zowe/zowe-client-python-sdk", author="Guilherme Cartier", author_email="gcartier94@gmail.com", @@ -27,11 +33,14 @@ def resolve_sdk_dep(sdk_name, version_spec): classifiers=[ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", - "License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)"], - install_requires=[resolve_sdk_dep('zos_console', '==' + __version__), - resolve_sdk_dep('zos_files', '==' + __version__), - resolve_sdk_dep('zos_tso', '==' + __version__), - resolve_sdk_dep('zos_jobs', '==' + __version__), - resolve_sdk_dep('zosmf', '==' + __version__)], - py_modules=[] + "License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)", + ], + install_requires=[ + resolve_sdk_dep("zos_console", "==" + __version__), + resolve_sdk_dep("zos_files", "==" + __version__), + resolve_sdk_dep("zos_tso", "==" + __version__), + resolve_sdk_dep("zos_jobs", "==" + __version__), + resolve_sdk_dep("zosmf", "==" + __version__), + ], + py_modules=[], ) diff --git a/src/zos_console/setup.py b/src/zos_console/setup.py index 224cbe36..864f8e4c 100644 --- a/src/zos_console/setup.py +++ b/src/zos_console/setup.py @@ -1,13 +1,17 @@ import sys -from setuptools import setup, find_namespace_packages + +from setuptools import find_namespace_packages, setup + sys.path.insert(0, "..") from _version import __version__ from setup import resolve_sdk_dep setup( - name='zowe_zos_console_for_zowe_sdk', + name="zowe_zos_console_for_zowe_sdk", version=__version__, - description='Zowe Python SDK - z/OS Console package', + description="Zowe Python SDK - z/OS Console package", + long_description=open("README.md", 'r').read(), + long_description_content_type="text/markdown", url="https://github.com/zowe/zowe-client-python-sdk", author="Guilherme Cartier", author_email="gcartier94@gmail.com", @@ -15,7 +19,8 @@ classifiers=[ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", - "License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)"], - install_requires=[resolve_sdk_dep('core', '~=' + __version__)], - packages=find_namespace_packages(include=['zowe.*']) + "License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)", + ], + install_requires=[resolve_sdk_dep("core", "~=" + __version__)], + packages=find_namespace_packages(include=["zowe.*"]), ) diff --git a/src/zos_console/zowe/zos_console_for_zowe_sdk/console.py b/src/zos_console/zowe/zos_console_for_zowe_sdk/console.py index 39723694..59817c86 100644 --- a/src/zos_console/zowe/zos_console_for_zowe_sdk/console.py +++ b/src/zos_console/zowe/zos_console_for_zowe_sdk/console.py @@ -1,5 +1,6 @@ from zowe.core_for_zowe_sdk import SdkApi + class Console(SdkApi): """ Class used to represent the base z/OSMF Console API. @@ -39,6 +40,7 @@ def issue_command(self, command, console=None): A JSON containing the response from the console command """ custom_args = self._create_custom_request_arguments() + custom_args["url"] = self.request_endpoint.replace("defcn", console or "defcn") request_body = {"cmd": command} custom_args["json"] = request_body response_json = self.request_handler.perform_request("PUT", custom_args) @@ -47,7 +49,7 @@ def issue_command(self, command, console=None): def get_response(self, response_key, console=None): """ Collect outstanding synchronous z/OS Console response messages. - + Parameters ---------- response_key diff --git a/src/zos_files/setup.py b/src/zos_files/setup.py index 3cc4d750..3bf890a2 100644 --- a/src/zos_files/setup.py +++ b/src/zos_files/setup.py @@ -1,13 +1,17 @@ import sys -from setuptools import setup, find_namespace_packages + +from setuptools import find_namespace_packages, setup + sys.path.insert(0, "..") from _version import __version__ from setup import resolve_sdk_dep setup( - name='zowe_zos_files_for_zowe_sdk', + name="zowe_zos_files_for_zowe_sdk", version=__version__, - description='Zowe Python SDK - z/OS Files package', + description="Zowe Python SDK - z/OS Files package", + long_description=open("README.md", 'r').read(), + long_description_content_type="text/markdown", url="https://github.com/zowe/zowe-client-python-sdk", author="Guilherme Cartier", author_email="gcartier94@gmail.com", @@ -15,7 +19,8 @@ classifiers=[ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", - "License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)"], - install_requires=[resolve_sdk_dep('core', '~=' + __version__)], - packages=find_namespace_packages(include=['zowe.*']) + "License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)", + ], + install_requires=[resolve_sdk_dep("core", "~=" + __version__)], + packages=find_namespace_packages(include=["zowe.*"]), ) diff --git a/src/zos_files/zowe/zos_files_for_zowe_sdk/__init__.py b/src/zos_files/zowe/zos_files_for_zowe_sdk/__init__.py index 7537a03d..a772ba07 100644 --- a/src/zos_files/zowe/zos_files_for_zowe_sdk/__init__.py +++ b/src/zos_files/zowe/zos_files_for_zowe_sdk/__init__.py @@ -2,5 +2,5 @@ Zowe Python SDK - z/OS Files package """ +from . import constants, exceptions from .files import Files -from . import exceptions, constants diff --git a/src/zos_files/zowe/zos_files_for_zowe_sdk/constants.py b/src/zos_files/zowe/zos_files_for_zowe_sdk/constants.py index 6335c406..4e7e7942 100644 --- a/src/zos_files/zowe/zos_files_for_zowe_sdk/constants.py +++ b/src/zos_files/zowe/zos_files_for_zowe_sdk/constants.py @@ -14,8 +14,9 @@ "MaxAllocationQuantity": 16777215, } from enum import Enum + + class FileType(Enum): BINARY = "binary" EXECUTABLE = "executable" TEXT = "text" - \ No newline at end of file diff --git a/src/zos_files/zowe/zos_files_for_zowe_sdk/exceptions.py b/src/zos_files/zowe/zos_files_for_zowe_sdk/exceptions.py index 06809f74..f079c0f5 100644 --- a/src/zos_files/zowe/zos_files_for_zowe_sdk/exceptions.py +++ b/src/zos_files/zowe/zos_files_for_zowe_sdk/exceptions.py @@ -30,4 +30,6 @@ class MaxAllocationQuantityExceeded(Exception): """Class used to represent an invalid allocation quantity.""" def __init__(self): - super().__init__("Maximum allocation quantity of {} exceeded".format(zos_file_constants['MaxAllocationQuantity'])) + super().__init__( + "Maximum allocation quantity of {} exceeded".format(zos_file_constants["MaxAllocationQuantity"]) + ) diff --git a/src/zos_files/zowe/zos_files_for_zowe_sdk/files.py b/src/zos_files/zowe/zos_files_for_zowe_sdk/files.py index 4ca01ebe..f338854a 100644 --- a/src/zos_files/zowe/zos_files_for_zowe_sdk/files.py +++ b/src/zos_files/zowe/zos_files_for_zowe_sdk/files.py @@ -11,14 +11,16 @@ """ -from zowe.core_for_zowe_sdk import SdkApi -from zowe.core_for_zowe_sdk.exceptions import FileNotFound -from zowe.zos_files_for_zowe_sdk import exceptions, constants import os import shutil -from zowe.zos_files_for_zowe_sdk.constants import zos_file_constants, FileType -_ZOWE_FILES_DEFAULT_ENCODING='utf-8' +from zowe.core_for_zowe_sdk import SdkApi +from zowe.core_for_zowe_sdk.exceptions import FileNotFound +from zowe.zos_files_for_zowe_sdk import constants, exceptions +from zowe.zos_files_for_zowe_sdk.constants import FileType, zos_file_constants + +_ZOWE_FILES_DEFAULT_ENCODING = "utf-8" + class Files(SdkApi): """ @@ -46,7 +48,6 @@ def __init__(self, connection): super().__init__(connection, "/zosmf/restfiles/") self.default_headers["Accept-Encoding"] = "gzip" - def list_files(self, path): """Retrieve a list of USS files based on a given pattern. @@ -70,11 +71,11 @@ def get_file_content(self, filepath_name): A JSON with the contents of the specified USS file """ custom_args = self._create_custom_request_arguments() - #custom_args["params"] = {"filepath-name": filepath_name} - custom_args["url"] = "{}fs{}".format(self.request_endpoint,filepath_name) + # custom_args["params"] = {"filepath-name": filepath_name} + custom_args["url"] = "{}fs{}".format(self.request_endpoint, filepath_name) response_json = self.request_handler.perform_request("GET", custom_args) return response_json - + def delete_uss(self, filepath_name, recursive=False): """ Delete a file or directory @@ -99,8 +100,7 @@ def delete_uss(self, filepath_name, recursive=False): response_json = self.request_handler.perform_request("DELETE", custom_args, expected_code=[204]) return response_json - - def list_dsn(self, name_pattern, return_attributes= False): + def list_dsn(self, name_pattern, return_attributes=False): """Retrieve a list of datasets based on a given pattern. Parameters @@ -113,24 +113,20 @@ def list_dsn(self, name_pattern, return_attributes= False): Returns ------- list of dict - + A JSON with a list of dataset names (and attributes if specified) matching the given pattern. """ custom_args = self._create_custom_request_arguments() - custom_args["params"] = {"dslevel": name_pattern} + custom_args["params"] = {"dslevel": self._encode_uri_component(name_pattern)} custom_args["url"] = "{}ds".format(self.request_endpoint) - - + if return_attributes: - custom_args["headers"]["X-IBM-Attributes"] = "base" - + custom_args["headers"]["X-IBM-Attributes"] = "base" + response_json = self.request_handler.perform_request("GET", custom_args) return response_json - - - def list_dsn_members(self, dataset_name, member_pattern=None, - member_start=None, limit=1000, attributes='member'): + def list_dsn_members(self, dataset_name, member_pattern=None, member_start=None, limit=1000, attributes="member"): """Retrieve the list of members on a given PDS/PDSE. Returns @@ -141,21 +137,23 @@ def list_dsn_members(self, dataset_name, member_pattern=None, custom_args = self._create_custom_request_arguments() additional_parms = {} if member_start is not None: - additional_parms['start'] = member_start + additional_parms["start"] = member_start if member_pattern is not None: - additional_parms['pattern'] = member_pattern + additional_parms["pattern"] = member_pattern url = "{}ds/{}/member".format(self.request_endpoint, dataset_name) - separator = '?' - for k,v in additional_parms.items(): - url = "{}{}{}={}".format(url,separator,k,v) - separator = '&' - custom_args['url'] = url - custom_args["headers"]["X-IBM-Max-Items"] = "{}".format(limit) + separator = "?" + for k, v in additional_parms.items(): + url = "{}{}{}={}".format(url, separator, k, v) + separator = "&" + custom_args["url"] = self._encode_uri_component(url) + custom_args["headers"]["X-IBM-Max-Items"] = "{}".format(limit) custom_args["headers"]["X-IBM-Attributes"] = attributes response_json = self.request_handler.perform_request("GET", custom_args) - return response_json['items'] # type: ignore - - def copy_uss_to_dataset(self, from_filename, to_dataset_name, to_member_name=None, type=FileType.TEXT,replace=False): + return response_json["items"] # type: ignore + + def copy_uss_to_dataset( + self, from_filename, to_dataset_name, to_member_name=None, type=FileType.TEXT, replace=False + ): """ Copy a USS file to dataset. @@ -177,25 +175,31 @@ def copy_uss_to_dataset(self, from_filename, to_dataset_name, to_member_name=Non json A JSON containing the result of the operation. """ - - data={ - "request":"copy", - "from-file":{ - "filename":from_filename.strip(), - "type":type.value - }, - "replace":replace + + data = { + "request": "copy", + "from-file": {"filename": from_filename.strip(), "type": type.value}, + "replace": replace, } - + path_to_member = f"{to_dataset_name}({to_member_name})" if to_member_name else to_dataset_name custom_args = self._create_custom_request_arguments() - custom_args['json'] = data - custom_args["url"] = "{}ds/{}".format(self.request_endpoint, path_to_member) + custom_args["json"] = data + custom_args["url"] = "{}ds/{}".format(self.request_endpoint, self._encode_uri_component(path_to_member)) response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[200]) return response_json - - def copy_dataset_or_member(self,from_dataset_name,to_dataset_name,from_member_name=None,volser=None,alias=None, - to_member_name=None,enq=None,replace=False): + + def copy_dataset_or_member( + self, + from_dataset_name, + to_dataset_name, + from_member_name=None, + volser=None, + alias=None, + to_member_name=None, + enq=None, + replace=False, + ): """ Copy a dataset or member to another dataset or member. Parameters @@ -208,7 +212,7 @@ def copy_dataset_or_member(self,from_dataset_name,to_dataset_name,from_member_na Name of the member to copy from volser: str Volume serial number of the dataset to copy from - alias: bool + alias: bool Alias of the dataset to copy from to_member_name: str Name of the member to copy to @@ -216,39 +220,35 @@ def copy_dataset_or_member(self,from_dataset_name,to_dataset_name,from_member_na Enqueue type for the dataset to copy from replace: bool If true, members in the target data set are replaced. - Returns + Returns ------- json A JSON containing the result of the operation """ - - data={ - "request":"copy", - "from-dataset":{ - "dsn":from_dataset_name.strip(), - "member":from_member_name - }, - "replace":replace + + data = { + "request": "copy", + "from-dataset": {"dsn": from_dataset_name.strip(), "member": from_member_name}, + "replace": replace, } - - + path_to_member = f"{to_dataset_name}({to_member_name})" if to_member_name else to_dataset_name if enq: - if enq in ("SHR","SHRW","EXCLU"): + if enq in ("SHR", "SHRW", "EXCLU"): data["enq"] = enq else: raise ValueError("Invalid value for enq.") if volser: - data["from-dataset"]["volser"]=volser - if alias is not None: #because it can be false so - data["from-dataset"]["alias"]=alias - + data["from-dataset"]["volser"] = volser + if alias is not None: # because it can be false so + data["from-dataset"]["alias"] = alias + custom_args = self._create_custom_request_arguments() - custom_args['json'] = data - custom_args["url"] = "{}ds/{}".format(self.request_endpoint, path_to_member) + custom_args["json"] = data + custom_args["url"] = "{}ds/{}".format(self.request_endpoint, self._encode_uri_component(path_to_member)) response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[200]) return response_json - + def get_dsn_content(self, dataset_name): """Retrieve the contents of a given dataset. @@ -258,12 +258,11 @@ def get_dsn_content(self, dataset_name): A JSON with the contents of a given dataset """ custom_args = self._create_custom_request_arguments() - custom_args["url"] = "{}ds/{}".format(self.request_endpoint, dataset_name) + custom_args["url"] = "{}ds/{}".format(self.request_endpoint, self._encode_uri_component(dataset_name)) response_json = self.request_handler.perform_request("GET", custom_args) return response_json - def create_data_set(self, dataset_name, options = {}): - + def create_data_set(self, dataset_name, options={}): """ Create a sequential or partitioned dataset. Parameters @@ -278,11 +277,24 @@ def create_data_set(self, dataset_name, options = {}): if options.get("primary") is None or options.get("lrecl") is None: raise ValueError("If 'like' is not specified, you must specify 'primary' or 'lrecl'.") - for opt in ("volser", "unit", "dsorg", "alcunit", - "primary", "secondary", "dirblk", "avgblk", "recfm", - "blksize", "lrecl", "storclass", "mgntclass", "dataclass", - "dsntype", "like"): - + for opt in ( + "volser", + "unit", + "dsorg", + "alcunit", + "primary", + "secondary", + "dirblk", + "avgblk", + "recfm", + "blksize", + "lrecl", + "storclass", + "mgntclass", + "dataclass", + "dsntype", + "like", + ): if opt == "dsorg": if options.get(opt) is not None and options[opt] not in ("PO", "PS"): raise KeyError @@ -327,9 +339,9 @@ def create_data_set(self, dataset_name, options = {}): options[opt] = options["lrecl"] custom_args = self._create_custom_request_arguments() - custom_args["url"] = "{}ds/{}".format(self.request_endpoint, dataset_name) + custom_args["url"] = "{}ds/{}".format(self.request_endpoint, self._encode_uri_component(dataset_name)) custom_args["json"] = options - response_json = self.request_handler.perform_request("POST", custom_args, expected_code = [201]) + response_json = self.request_handler.perform_request("POST", custom_args, expected_code=[201]) return response_json def create_default_data_set(self, dataset_name: str, default_type: str): @@ -361,7 +373,7 @@ def create_default_data_set(self, dataset_name: str, default_type: str): "dirblk": 5, "recfm": "FB", "blksize": 6160, - "lrecl": 80 + "lrecl": 80, } elif default_type == "sequential": custom_args["json"] = { @@ -370,7 +382,7 @@ def create_default_data_set(self, dataset_name: str, default_type: str): "primary": 1, "recfm": "FB", "blksize": 6160, - "lrecl": 80 + "lrecl": 80, } elif default_type == "classic": custom_args["json"] = { @@ -380,7 +392,7 @@ def create_default_data_set(self, dataset_name: str, default_type: str): "recfm": "FB", "blksize": 6160, "lrecl": 80, - "dirblk": 25 + "dirblk": 25, } elif default_type == "c": custom_args["json"] = { @@ -390,7 +402,7 @@ def create_default_data_set(self, dataset_name: str, default_type: str): "recfm": "VB", "blksize": 32760, "lrecl": 260, - "dirblk": 25 + "dirblk": 25, } elif default_type == "binary": custom_args["json"] = { @@ -400,14 +412,14 @@ def create_default_data_set(self, dataset_name: str, default_type: str): "recfm": "U", "blksize": 27998, "lrecl": 27998, - "dirblk": 25 + "dirblk": 25, } - custom_args["url"] = "{}ds/{}".format(self.request_endpoint, dataset_name) + custom_args["url"] = "{}ds/{}".format(self.request_endpoint, self._encode_uri_component(dataset_name)) response_json = self.request_handler.perform_request("POST", custom_args, expected_code=[201]) return response_json - def create_uss(self, file_path, type, mode = None): + def create_uss(self, file_path, type, mode=None): """ Add a file or directory Parameters @@ -418,15 +430,12 @@ def create_uss(self, file_path, type, mode = None): """ - data = { - "type": type, - "mode": mode - } - + data = {"type": type, "mode": mode} + custom_args = self._create_custom_request_arguments() custom_args["json"] = data custom_args["url"] = "{}fs/{}".format(self.request_endpoint, file_path.lstrip("/")) - response_json = self.request_handler.perform_request("POST", custom_args, expected_code = [201]) + response_json = self.request_handler.perform_request("POST", custom_args, expected_code=[201]) return response_json def get_dsn_content_streamed(self, dataset_name): @@ -438,7 +447,7 @@ def get_dsn_content_streamed(self, dataset_name): A raw socket response """ custom_args = self._create_custom_request_arguments() - custom_args["url"] = "{}ds/{}".format(self.request_endpoint, dataset_name) + custom_args["url"] = "{}ds/{}".format(self.request_endpoint, self._encode_uri_component(dataset_name)) raw_response = self.request_handler.perform_streamed_request("GET", custom_args) return raw_response @@ -457,12 +466,12 @@ def get_dsn_binary_content(self, dataset_name, with_prefixes=False): The contents of the dataset with no transformation """ custom_args = self._create_custom_request_arguments() - custom_args["url"] = "{}ds/{}".format(self.request_endpoint, dataset_name) + custom_args["url"] = "{}ds/{}".format(self.request_endpoint, self._encode_uri_component(dataset_name)) custom_args["headers"]["Accept"] = "application/octet-stream" if with_prefixes: - custom_args["headers"]["X-IBM-Data-Type"] = 'record' + custom_args["headers"]["X-IBM-Data-Type"] = "record" else: - custom_args["headers"]["X-IBM-Data-Type"] = 'binary' + custom_args["headers"]["X-IBM-Data-Type"] = "binary" content = self.request_handler.perform_request("GET", custom_args) return content @@ -474,19 +483,19 @@ def get_dsn_binary_content_streamed(self, dataset_name, with_prefixes=False): ---------- dataset_name: str - Name of the dataset to retrieve with_prefixes: boolean - if True include a 4 byte big endian record len prefix - default: False + default: False Returns ------- raw The raw socket response """ custom_args = self._create_custom_request_arguments() - custom_args["url"] = "{}ds/{}".format(self.request_endpoint, dataset_name) + custom_args["url"] = "{}ds/{}".format(self.request_endpoint, self._encode_uri_component(dataset_name)) custom_args["headers"]["Accept"] = "application/octet-stream" if with_prefixes: - custom_args["headers"]["X-IBM-Data-Type"] = 'record' + custom_args["headers"]["X-IBM-Data-Type"] = "record" else: - custom_args["headers"]["X-IBM-Data-Type"] = 'binary' + custom_args["headers"]["X-IBM-Data-Type"] = "binary" content = self.request_handler.perform_streamed_request("GET", custom_args) return content @@ -499,18 +508,16 @@ def write_to_dsn(self, dataset_name, data, encoding=_ZOWE_FILES_DEFAULT_ENCODING A JSON containing the result of the operation """ custom_args = self._create_custom_request_arguments() - custom_args["url"] = "{}ds/{}".format(self.request_endpoint, dataset_name) + custom_args["url"] = "{}ds/{}".format(self.request_endpoint, self._encode_uri_component(dataset_name)) custom_args["data"] = data - custom_args['headers']['Content-Type'] = 'text/plain; charset={}'.format(encoding) - response_json = self.request_handler.perform_request( - "PUT", custom_args, expected_code=[204, 201] - ) + custom_args["headers"]["Content-Type"] = "text/plain; charset={}".format(encoding) + response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[204, 201]) return response_json def download_dsn(self, dataset_name, output_file): """Retrieve the contents of a dataset and saves it to a given file.""" raw_response = self.get_dsn_content_streamed(dataset_name) - with open(output_file, 'w') as f: + with open(output_file, "w") as f: shutil.copyfileobj(raw_response, f) def download_binary_dsn(self, dataset_name, output_file, with_prefixes=False): @@ -529,13 +536,13 @@ def download_binary_dsn(self, dataset_name, output_file, with_prefixes=False): Binary content of the dataset. """ content = self.get_dsn_binary_content_streamed(dataset_name, with_prefixes=with_prefixes) - with open(output_file, 'wb') as f: + with open(output_file, "wb") as f: shutil.copyfileobj(content, f) def upload_file_to_dsn(self, input_file, dataset_name, encoding=_ZOWE_FILES_DEFAULT_ENCODING): """Upload contents of a given file and uploads it to a dataset.""" if os.path.isfile(input_file): - with open(input_file, 'rb') as in_file: + with open(input_file, "rb") as in_file: response_json = self.write_to_dsn(dataset_name, in_file) else: raise FileNotFound(input_file) @@ -550,16 +557,14 @@ def write_to_uss(self, filepath_name, data, encoding=_ZOWE_FILES_DEFAULT_ENCODIN custom_args = self._create_custom_request_arguments() custom_args["url"] = "{}fs/{}".format(self.request_endpoint, filepath_name.lstrip("/")) custom_args["data"] = data - custom_args['headers']['Content-Type'] = 'text/plain; charset={}'.format(encoding) - response_json = self.request_handler.perform_request( - "PUT", custom_args, expected_code=[204, 201] - ) + custom_args["headers"]["Content-Type"] = "text/plain; charset={}".format(encoding) + response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[204, 201]) return response_json def upload_file_to_uss(self, input_file, filepath_name, encoding=_ZOWE_FILES_DEFAULT_ENCODING): """Upload contents of a given file and uploads it to UNIX file""" if os.path.isfile(input_file): - in_file = open(input_file, 'r') + in_file = open(input_file, "r") file_contents = in_file.read() response_json = self.write_to_uss(filepath_name, file_contents) else: @@ -569,40 +574,39 @@ def delete_data_set(self, dataset_name, volume=None, member_name=None): """Deletes a sequential or partitioned data.""" custom_args = self._create_custom_request_arguments() if member_name is not None: - dataset_name = f'{dataset_name}({member_name})' + dataset_name = f"{dataset_name}({member_name})" url = "{}ds/{}".format(self.request_endpoint, dataset_name) if volume is not None: url = "{}ds/-{}/{}".format(self.request_endpoint, volume, dataset_name) - custom_args["url"] = url - response_json = self.request_handler.perform_request( - "DELETE", custom_args, expected_code=[200, 202, 204]) + custom_args["url"] = self._encode_uri_component(url) + response_json = self.request_handler.perform_request("DELETE", custom_args, expected_code=[200, 202, 204]) return response_json def create_zFS_file_system(self, file_system_name, options={}): """ Create a z/OS UNIX zFS Filesystem. - + Parameter --------- file_system_name: str - the name for the file system - + Returns ------- json - A JSON containing the result of the operation """ for key, value in options.items(): - if key == 'perms': + if key == "perms": if value < 0 or value > 777: raise exceptions.InvalidPermsOption(value) - + if key == "cylsPri" or key == "cylsSec": - if value > constants.zos_file_constants['MaxAllocationQuantity']: + if value > constants.zos_file_constants["MaxAllocationQuantity"]: raise exceptions.MaxAllocationQuantityExceeded custom_args = self._create_custom_request_arguments() custom_args["url"] = "{}mfs/zfs/{}".format(self.request_endpoint, file_system_name) custom_args["json"] = options - response_json = self.request_handler.perform_request("POST", custom_args, expected_code = [201]) + response_json = self.request_handler.perform_request("POST", custom_args, expected_code=[201]) return response_json def delete_zFS_file_system(self, file_system_name): @@ -613,7 +617,7 @@ def delete_zFS_file_system(self, file_system_name): custom_args["url"] = "{}mfs/zfs/{}".format(self.request_endpoint, file_system_name) response_json = self.request_handler.perform_request("DELETE", custom_args, expected_code=[204]) return response_json - + def mount_file_system(self, file_system_name, mount_point, options={}, encoding=_ZOWE_FILES_DEFAULT_ENCODING): """Mounts a z/OS UNIX file system on a specified directory. Parameter @@ -631,7 +635,7 @@ def mount_file_system(self, file_system_name, mount_point, options={}, encoding= custom_args = self._create_custom_request_arguments() custom_args["url"] = "{}mfs/{}".format(self.request_endpoint, file_system_name) custom_args["json"] = options - custom_args['headers']['Content-Type'] = 'text/plain; charset={}'.format(encoding) + custom_args["headers"]["Content-Type"] = "text/plain; charset={}".format(encoding) response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[204]) return response_json @@ -642,7 +646,7 @@ def unmount_file_system(self, file_system_name, options={}, encoding=_ZOWE_FILES --------- file_system_name: str - the name for the file system options: dict - A JSON of request body options - + Returns ------- json - A JSON containing the result of the operation @@ -651,7 +655,7 @@ def unmount_file_system(self, file_system_name, options={}, encoding=_ZOWE_FILES custom_args = self._create_custom_request_arguments() custom_args["url"] = "{}mfs/{}".format(self.request_endpoint, file_system_name) custom_args["json"] = options - custom_args['headers']['Content-Type'] = 'text/plain; charset={}'.format(encoding) + custom_args["headers"]["Content-Type"] = "text/plain; charset={}".format(encoding) response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[204]) return response_json @@ -664,14 +668,14 @@ def list_unix_file_systems(self, file_path_name=None, file_system_name=None): --------- file_path: str - the UNIX directory that contains the files and directories to be listed. file_system_name: str - the name for the file system to be listed - + Returns ------- json - A JSON containing the result of the operation """ custom_args = self._create_custom_request_arguments() - custom_args["params"] = {"path":file_path_name, "fsname": file_system_name} + custom_args["params"] = {"path": file_path_name, "fsname": file_system_name} custom_args["url"] = "{}mfs".format(self.request_endpoint) response_json = self.request_handler.perform_request("GET", custom_args, expected_code=[200]) return response_json @@ -693,14 +697,11 @@ def recall_migrated_dataset(self, dataset_name: str, wait=False): json - A JSON containing the result of the operation """ - data = { - "request": "hrecall", - "wait": wait - } + data = {"request": "hrecall", "wait": wait} custom_args = self._create_custom_request_arguments() custom_args["json"] = data - custom_args["url"] = "{}ds/{}".format(self.request_endpoint, dataset_name) + custom_args["url"] = "{}ds/{}".format(self.request_endpoint, self._encode_uri_component(dataset_name)) response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[200]) return response_json @@ -713,10 +714,10 @@ def delete_migrated_data_set(self, dataset_name: str, purge=False, wait=False): ---------- dataset_name: str Name of the data set - + purge: bool If true, the function uses the PURGE=YES on ARCHDEL request, otherwise it uses the PURGE=NO. - + wait: bool If true, the function waits for completion of the request, otherwise the request is queued. @@ -733,7 +734,7 @@ def delete_migrated_data_set(self, dataset_name: str, purge=False, wait=False): custom_args = self._create_custom_request_arguments() custom_args["json"] = data - custom_args["url"] = "{}ds/{}".format(self.request_endpoint, dataset_name) + custom_args["url"] = "{}ds/{}".format(self.request_endpoint, self._encode_uri_component(dataset_name)) response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[200]) return response_json @@ -755,14 +756,11 @@ def migrate_data_set(self, dataset_name: str, wait=False): json - A JSON containing the result of the operation """ - data = { - "request": "hmigrate", - "wait": wait - } + data = {"request": "hmigrate", "wait": wait} custom_args = self._create_custom_request_arguments() custom_args["json"] = data - custom_args["url"] = "{}ds/{}".format(self.request_endpoint, dataset_name) + custom_args["url"] = "{}ds/{}".format(self.request_endpoint, self._encode_uri_component(dataset_name)) response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[200]) return response_json @@ -783,17 +781,14 @@ def rename_dataset(self, before_dataset_name: str, after_dataset_name: str): ------- json - A JSON containing the result of the operation """ - - data = { - "request": "rename", - "from-dataset": { - "dsn": before_dataset_name.strip() - } - } + + data = {"request": "rename", "from-dataset": {"dsn": before_dataset_name.strip()}} custom_args = self._create_custom_request_arguments() custom_args["json"] = data - custom_args["url"] = "{}ds/{}".format(self.request_endpoint, after_dataset_name.strip()) + custom_args["url"] = "{}ds/{}".format( + self.request_endpoint, self._encode_uri_component(after_dataset_name).strip() + ) response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[200]) return response_json @@ -826,7 +821,7 @@ def rename_dataset_member(self, dataset_name: str, before_member_name: str, afte "from-dataset": { "dsn": dataset_name.strip(), "member": before_member_name.strip(), - } + }, } path_to_member = dataset_name.strip() + "(" + after_member_name.strip() + ")" @@ -838,8 +833,8 @@ def rename_dataset_member(self, dataset_name: str, before_member_name: str, afte raise ValueError("Invalid value for enq.") custom_args = self._create_custom_request_arguments() - custom_args['json'] = data - custom_args["url"] = "{}ds/{}".format(self.request_endpoint, path_to_member) + custom_args["json"] = data + custom_args["url"] = "{}ds/{}".format(self.request_endpoint, self._encode_uri_component(path_to_member)) response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[200]) return response_json diff --git a/src/zos_jobs/setup.py b/src/zos_jobs/setup.py index 88a506e3..8c677401 100644 --- a/src/zos_jobs/setup.py +++ b/src/zos_jobs/setup.py @@ -1,13 +1,17 @@ import sys -from setuptools import setup, find_namespace_packages + +from setuptools import find_namespace_packages, setup + sys.path.insert(0, "..") from _version import __version__ from setup import resolve_sdk_dep setup( - name='zowe_zos_jobs_for_zowe_sdk', + name="zowe_zos_jobs_for_zowe_sdk", version=__version__, - description='Zowe Python SDK - z/OS Jobs package', + description="Zowe Python SDK - z/OS Jobs package", + long_description=open("README.md", 'r').read(), + long_description_content_type="text/markdown", url="https://github.com/zowe/zowe-client-python-sdk", author="Guilherme Cartier", author_email="gcartier94@gmail.com", @@ -15,7 +19,8 @@ classifiers=[ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", - "License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)"], - install_requires=[resolve_sdk_dep('core', '~=' + __version__)], - packages=find_namespace_packages(include=['zowe.*']) + "License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)", + ], + install_requires=[resolve_sdk_dep("core", "~=" + __version__)], + packages=find_namespace_packages(include=["zowe.*"]), ) diff --git a/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/jobs.py b/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/jobs.py index e40e4dcb..482d2be6 100644 --- a/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/jobs.py +++ b/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/jobs.py @@ -9,9 +9,10 @@ Copyright Contributors to the Zowe Project. """ -from zowe.core_for_zowe_sdk import SdkApi import os +from zowe.core_for_zowe_sdk import SdkApi + class Jobs(SdkApi): """ @@ -51,7 +52,7 @@ def get_job_status(self, jobname, jobid): """ custom_args = self._create_custom_request_arguments() job_url = "{}/{}".format(jobname, jobid) - request_url = "{}{}".format(self.request_endpoint, job_url) + request_url = "{}{}".format(self.request_endpoint, self._encode_uri_component(job_url)) custom_args["url"] = request_url response_json = self.request_handler.perform_request("GET", custom_args) return response_json @@ -78,12 +79,9 @@ def cancel_job(self, jobname: str, jobid: str, modify_version="2.0"): custom_args = self._create_custom_request_arguments() job_url = "{}/{}".format(jobname, jobid) - request_url = "{}{}".format(self.request_endpoint, job_url) + request_url = "{}{}".format(self.request_endpoint, self._encode_uri_component(job_url)) custom_args["url"] = request_url - custom_args["json"] = { - "request": "cancel", - "version": modify_version - } + custom_args["json"] = {"request": "cancel", "version": modify_version} response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[202, 200]) return response_json @@ -110,7 +108,7 @@ def delete_job(self, jobname, jobid, modify_version="2.0"): custom_args = self._create_custom_request_arguments() job_url = "{}/{}".format(jobname, jobid) - request_url = "{}{}".format(self.request_endpoint, job_url) + request_url = "{}{}".format(self.request_endpoint, self._encode_uri_component(job_url)) custom_args["url"] = request_url custom_args["headers"]["X-IBM-Job-Modify-Version"] = modify_version @@ -118,21 +116,17 @@ def delete_job(self, jobname, jobid, modify_version="2.0"): return response_json def _issue_job_request(self, req: dict, jobname: str, jobid: str, modify_version): - custom_args = self._create_custom_request_arguments() job_url = "{}/{}".format(jobname, jobid) - request_url = "{}{}".format(self.request_endpoint, job_url) + request_url = "{}{}".format(self.request_endpoint, self._encode_uri_component(job_url)) custom_args["url"] = request_url - custom_args["json"] = { - **req, - "version": modify_version - } + custom_args["json"] = {**req, "version": modify_version} custom_args["headers"]["X-IBM-Job-Modify-Version"] = modify_version - + response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[202, 200]) return response_json - + def change_job_class(self, jobname: str, jobid: str, class_name: str, modify_version="2.0"): """Changes the job class @@ -158,7 +152,7 @@ def change_job_class(self, jobname: str, jobid: str, class_name: str, modify_ver def hold_job(self, jobname: str, jobid: str, modify_version="2.0"): """Hold the given job on JES - + Parameters ---------- jobname: str @@ -175,13 +169,13 @@ def hold_job(self, jobname: str, jobid: str, modify_version="2.0"): """ if modify_version not in ("1.0", "2.0"): raise ValueError('Accepted values for modify_version: "1.0" or "2.0"') - + response_json = self._issue_job_request({"request": "hold"}, jobname, jobid, modify_version) return response_json def release_job(self, jobname: str, jobid: str, modify_version="2.0"): """Release the given job on JES - + Parameters ---------- jobname: str @@ -198,11 +192,11 @@ def release_job(self, jobname: str, jobid: str, modify_version="2.0"): """ if modify_version not in ("1.0", "2.0"): raise ValueError('Accepted values for modify_version: "1.0" or "2.0"') - + response_json = self._issue_job_request({"request": "release"}, jobname, jobid, modify_version) return response_json - def list_jobs(self, owner=None, prefix="*", max_jobs=1000, user_correlator=None): + def list_jobs(self, owner=None, prefix="*", max_jobs=1000, user_correlator=None): """Retrieve list of jobs on JES based on the provided arguments. Parameters @@ -244,11 +238,9 @@ def submit_from_mainframe(self, jcl_path): A JSON containing the result of the request execution """ custom_args = self._create_custom_request_arguments() - request_body = {"file": "//\'%s\'" % jcl_path} + request_body = {"file": "//'%s'" % jcl_path} custom_args["json"] = request_body - response_json = self.request_handler.perform_request( - "PUT", custom_args, expected_code=[201] - ) + response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[201]) return response_json def submit_from_local_file(self, jcl_path): @@ -297,9 +289,7 @@ def submit_plaintext(self, jcl): custom_args = self._create_custom_request_arguments() custom_args["data"] = str(jcl) custom_args["headers"] = {"Content-Type": "text/plain", "X-CSRF-ZOSMF-HEADER": ""} - response_json = self.request_handler.perform_request( - "PUT", custom_args, expected_code=[201] - ) + response_json = self.request_handler.perform_request("PUT", custom_args, expected_code=[201]) return response_json def get_spool_files(self, correlator): @@ -317,11 +307,11 @@ def get_spool_files(self, correlator): """ custom_args = self._create_custom_request_arguments() job_url = "{}/files".format(correlator) - request_url = "{}{}".format(self.request_endpoint, job_url) + request_url = "{}{}".format(self.request_endpoint, self._encode_uri_component(job_url)) custom_args["url"] = request_url response_json = self.request_handler.perform_request("GET", custom_args) return response_json - + def get_jcl_text(self, correlator): """Retrieve the input JCL text for job with specified correlator Parameters @@ -336,10 +326,10 @@ def get_jcl_text(self, correlator): """ custom_args = self._create_custom_request_arguments() job_url = "{}/files/JCL/records".format(correlator) - request_url = "{}{}".format(self.request_endpoint, job_url) + request_url = "{}{}".format(self.request_endpoint, self._encode_uri_component(job_url)) custom_args["url"] = request_url response_json = self.request_handler.perform_request("GET", custom_args) - return response_json + return response_json def get_spool_file_contents(self, correlator, id): """Retrieve the contents of a single spool file from a job @@ -360,7 +350,7 @@ def get_spool_file_contents(self, correlator, id): """ custom_args = self._create_custom_request_arguments() job_url = "{}/files/{}/records".format(correlator, id) - request_url = "{}{}".format(self.request_endpoint, job_url) + request_url = "{}{}".format(self.request_endpoint, self._encode_uri_component(job_url)) custom_args["url"] = request_url response_json = self.request_handler.perform_request("GET", custom_args) return response_json @@ -380,7 +370,7 @@ def get_job_output_as_files(self, status, output_dir): dir: stepname | file: spool file - ... + ... Parameters @@ -397,31 +387,31 @@ def get_job_output_as_files(self, status, output_dir): A JSON containing the result of the request execution """ - _job_name = status['jobname'] - _job_id = status['jobid'] - _job_correlator = status['job-correlator'] + _job_name = status["jobname"] + _job_id = status["jobid"] + _job_correlator = status["job-correlator"] _output_dir = os.path.join(output_dir, _job_name, _job_id) os.makedirs(_output_dir, exist_ok=True) - _output_file = os.path.join(output_dir, _job_name, _job_id, 'jcl.txt') + _output_file = os.path.join(output_dir, _job_name, _job_id, "jcl.txt") _data_spool_file = self.get_jcl_text(_job_correlator) - _dataset_content = _data_spool_file['response'] - _out_file = open(_output_file, 'w', encoding="utf-8") + _dataset_content = _data_spool_file["response"] + _out_file = open(_output_file, "w", encoding="utf-8") _out_file.write(_dataset_content) _out_file.close() _spool = self.get_spool_files(_job_correlator) for _spool_file in _spool: - _stepname = _spool_file['stepname'] - _ddname = _spool_file['ddname'] - _spoolfile_id = _spool_file['id'] + _stepname = _spool_file["stepname"] + _ddname = _spool_file["ddname"] + _spoolfile_id = _spool_file["id"] _output_dir = os.path.join(output_dir, _job_name, _job_id, _stepname) os.makedirs(_output_dir, exist_ok=True) - + _output_file = os.path.join(output_dir, _job_name, _job_id, _stepname, _ddname) _data_spool_file = self.get_spool_file_contents(_job_correlator, _spoolfile_id) - _dataset_content = _data_spool_file['response'] - _out_file = open(_output_file, 'w', encoding="utf-8") + _dataset_content = _data_spool_file["response"] + _out_file = open(_output_file, "w", encoding="utf-8") _out_file.write(_dataset_content) _out_file.close() diff --git a/src/zos_tso/setup.py b/src/zos_tso/setup.py index 1f54857d..07e73a34 100644 --- a/src/zos_tso/setup.py +++ b/src/zos_tso/setup.py @@ -1,13 +1,17 @@ import sys -from setuptools import setup, find_namespace_packages + +from setuptools import find_namespace_packages, setup + sys.path.insert(0, "..") from _version import __version__ from setup import resolve_sdk_dep setup( - name='zowe_zos_tso_for_zowe_sdk', + name="zowe_zos_tso_for_zowe_sdk", version=__version__, - description='Zowe Python SDK - z/OS TSO package', + description="Zowe Python SDK - z/OS TSO package", + long_description=open("README.md", 'r').read(), + long_description_content_type="text/markdown", url="https://github.com/zowe/zowe-client-python-sdk", author="Guilherme Cartier", author_email="gcartier94@gmail.com", @@ -15,7 +19,8 @@ classifiers=[ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", - "License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)"], - install_requires=[resolve_sdk_dep('core', '~=' + __version__)], - packages=find_namespace_packages(include=['zowe.*']) + "License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)", + ], + install_requires=[resolve_sdk_dep("core", "~=" + __version__)], + packages=find_namespace_packages(include=["zowe.*"]), ) diff --git a/src/zos_tso/zowe/zos_tso_for_zowe_sdk/tso.py b/src/zos_tso/zowe/zos_tso_for_zowe_sdk/tso.py index 6854cdcf..07972314 100644 --- a/src/zos_tso/zowe/zos_tso_for_zowe_sdk/tso.py +++ b/src/zos_tso/zowe/zos_tso_for_zowe_sdk/tso.py @@ -125,7 +125,7 @@ def send_tso_message(self, session_key, message): """ custom_args = self._create_custom_request_arguments() custom_args["url"] = "{}/{}".format(self.request_endpoint, str(session_key)) - custom_args["json"] = {"TSO RESPONSE":{"VERSION":"0100","DATA":str(message)}} + custom_args["json"] = {"TSO RESPONSE": {"VERSION": "0100", "DATA": str(message)}} response_json = self.request_handler.perform_request("PUT", custom_args) return response_json["tsoData"] @@ -144,16 +144,10 @@ def ping_tso_session(self, session_key): Where the options are: 'Ping successful' or 'Ping failed' """ custom_args = self._create_custom_request_arguments() - custom_args["url"] = "{}/{}/{}".format( - self.request_endpoint, "ping", str(session_key) - ) + custom_args["url"] = "{}/{}/{}".format(self.request_endpoint, "ping", str(session_key)) response_json = self.request_handler.perform_request("PUT", custom_args) message_id_list = self.parse_message_ids(response_json) - return ( - "Ping successful" - if self.session_not_found not in message_id_list - else "Ping failed" - ) + return "Ping successful" if self.session_not_found not in message_id_list else "Ping failed" def end_tso_session(self, session_key): """Terminates an existing TSO session. @@ -172,11 +166,7 @@ def end_tso_session(self, session_key): custom_args["url"] = "{}/{}".format(self.request_endpoint, session_key) response_json = self.request_handler.perform_request("DELETE", custom_args) message_id_list = self.parse_message_ids(response_json) - return ( - "Session ended" - if self.session_not_found not in message_id_list - else "Session already ended" - ) + return "Session ended" if self.session_not_found not in message_id_list else "Session already ended" def parse_message_ids(self, response_json): """Parse TSO response and retrieve only the message ids. @@ -191,11 +181,7 @@ def parse_message_ids(self, response_json): list A list containing the TSO response message ids """ - return ( - [message["messageId"] for message in response_json["msgData"]] - if "msgData" in response_json - else [] - ) + return [message["messageId"] for message in response_json["msgData"]] if "msgData" in response_json else [] def retrieve_tso_messages(self, response_json): """Parse the TSO response and retrieve all messages. @@ -210,8 +196,4 @@ def retrieve_tso_messages(self, response_json): list A list containing the TSO response messages """ - return [ - message["TSO MESSAGE"]["DATA"] - for message in response_json - if "TSO MESSAGE" in message - ] + return [message["TSO MESSAGE"]["DATA"] for message in response_json if "TSO MESSAGE" in message] diff --git a/src/zosmf/setup.py b/src/zosmf/setup.py index d971de17..73f38107 100644 --- a/src/zosmf/setup.py +++ b/src/zosmf/setup.py @@ -1,13 +1,17 @@ import sys -from setuptools import setup, find_namespace_packages + +from setuptools import find_namespace_packages, setup + sys.path.insert(0, "..") from _version import __version__ from setup import resolve_sdk_dep setup( - name='zowe_zosmf_for_zowe_sdk', + name="zowe_zosmf_for_zowe_sdk", version=__version__, - description='Zowe Python SDK - z/OSMF package', + description="Zowe Python SDK - z/OSMF package", + long_description=open("README.md", 'r').read(), + long_description_content_type="text/markdown", url="https://github.com/zowe/zowe-client-python-sdk", author="Guilherme Cartier", author_email="gcartier94@gmail.com", @@ -15,7 +19,8 @@ classifiers=[ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", - "License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)"], - install_requires=[resolve_sdk_dep('core', '~=' + __version__)], - packages=find_namespace_packages(include=['zowe.*']) + "License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)", + ], + install_requires=[resolve_sdk_dep("core", "~=" + __version__)], + packages=find_namespace_packages(include=["zowe.*"]), ) diff --git a/src/zosmf/zowe/zosmf_for_zowe_sdk/zosmf.py b/src/zosmf/zowe/zosmf_for_zowe_sdk/zosmf.py index f611a6ad..22e4fa65 100644 --- a/src/zosmf/zowe/zosmf_for_zowe_sdk/zosmf.py +++ b/src/zosmf/zowe/zosmf_for_zowe_sdk/zosmf.py @@ -41,9 +41,7 @@ def get_info(self): json A JSON containing the z/OSMF Info REST API data """ - response_json = self.request_handler.perform_request( - "GET", self.request_arguments - ) + response_json = self.request_handler.perform_request("GET", self.request_arguments) return response_json def list_systems(self): @@ -56,5 +54,5 @@ def list_systems(self): custom_args = self._create_custom_request_arguments() custom_args["url"] = "{}/systems".format(self.request_endpoint) - response_json = self.request_handler.perform_request("GET", custom_args, expected_code = [200]) - return response_json \ No newline at end of file + response_json = self.request_handler.perform_request("GET", custom_args, expected_code=[200]) + return response_json diff --git a/tests/integration/test_zos_console.py b/tests/integration/test_zos_console.py index dba76fb2..4423982b 100644 --- a/tests/integration/test_zos_console.py +++ b/tests/integration/test_zos_console.py @@ -1,7 +1,8 @@ """Integration tests for the Zowe Python SDK z/OS Console package.""" import unittest -from zowe.zos_console_for_zowe_sdk import Console + from zowe.core_for_zowe_sdk import ProfileManager +from zowe.zos_console_for_zowe_sdk import Console class TestConsoleIntegration(unittest.TestCase): @@ -11,15 +12,14 @@ def setUp(self): """Setup fixtures for Console class.""" test_profile = ProfileManager().load(profile_type="zosmf") self.console = Console(test_profile) - + def test_console_command_time_should_return_time(self): """Test the execution of the time command should return the current time""" command_output = self.console.issue_command("D T") - self.assertTrue(command_output['cmd-response'].strip().startswith("IEE136I")) + self.assertTrue(command_output["cmd-response"].strip().startswith("IEE136I")) def test_get_response_should_return_messages(self): """Test that response message can be received from the console""" command_output = self.console.issue_command("D T") response = self.console.get_response(command_output["cmd-response-key"]) self.assertTrue("cmd-response" in response) - diff --git a/tests/integration/test_zos_files.py b/tests/integration/test_zos_files.py index f77a2f91..bba80195 100644 --- a/tests/integration/test_zos_files.py +++ b/tests/integration/test_zos_files.py @@ -1,13 +1,14 @@ """Integration tests for the Zowe Python SDK z/OS Files package.""" -import unittest import json import os -from zowe.zos_files_for_zowe_sdk import Files +import unittest + import urllib3 from zowe.core_for_zowe_sdk import ProfileManager +from zowe.zos_files_for_zowe_sdk import Files -FIXTURES_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),'fixtures') -FILES_FIXTURES_PATH = os.path.join(FIXTURES_PATH, 'files.json') +FIXTURES_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures") +FILES_FIXTURES_PATH = os.path.join(FIXTURES_PATH, "files.json") class TestFilesIntegration(unittest.TestCase): @@ -17,44 +18,43 @@ def setUp(self): """Setup fixtures for Files class.""" test_profile = ProfileManager().load(profile_type="zosmf") self.user_name = test_profile["user"] - with open(FILES_FIXTURES_PATH, 'r') as fixtures_json: + with open(FILES_FIXTURES_PATH, "r") as fixtures_json: self.files_fixtures = json.load(fixtures_json) self.files = Files(test_profile) self.test_member_jcl = f'{self.files_fixtures["TEST_PDS"]}({self.files_fixtures["TEST_MEMBER"]})' self.test_member_generic = f'{self.files_fixtures["TEST_PDS"]}(TEST)' self.test1_zfs_file_system = f'{self.user_name}.{self.files_fixtures["TEST1_ZFS"]}' self.test2_zfs_file_system = f'{self.user_name}.{self.files_fixtures["TEST2_ZFS"]}' - self.create_zfs_options = {"perms": 755,"cylsPri": 10,"cylsSec": 2,"timeout": 20, "volumes": ["VPMVSC"]} + self.create_zfs_options = {"perms": 755, "cylsPri": 10, "cylsSec": 2, "timeout": 20, "volumes": ["VPMVSC"]} self.mount_zfs_file_system_options = {"fs-type": "ZFS", "mode": "rdonly"} def test_list_dsn_should_return_a_list_of_datasets(self): """Executing list_dsn method should return a list of found datasets.""" - + scenarios = [ {"attributes": False, "expected_attributes": ["dsname"]}, - {"attributes": True, "expected_attributes": ["dsname", "migr","vol"]} + {"attributes": True, "expected_attributes": ["dsname", "migr", "vol"]}, ] - - for scenario in scenarios: + for scenario in scenarios: # Get the command output command_output = self.files.list_dsn(self.files_fixtures["TEST_HLQ"], scenario["attributes"]) - + # Assert that command_output['items'] is a list - self.assertIsInstance(command_output['items'], list) - + self.assertIsInstance(command_output["items"], list) + # Assert that command_output['items'] contains at least one item - self.assertGreater(len(command_output['items']), 0) - + self.assertGreater(len(command_output["items"]), 0) + # Assert that the first item in the list has 'dsname' defined - first_item = command_output['items'][0] - self.assertIn('dsname', first_item) - + first_item = command_output["items"][0] + self.assertIn("dsname", first_item) + # Assert that the first item in the list has the expected attributes defined attributes = first_item.keys() for expected_attr in scenario["expected_attributes"]: self.assertIn(expected_attr, attributes) - + def test_list_members_should_return_a_list_of_members(self): """Executing list_dsn_members should return a list of members.""" command_output = self.files.list_dsn_members(self.files_fixtures["TEST_PDS"]) @@ -63,7 +63,7 @@ def test_list_members_should_return_a_list_of_members(self): def test_get_dsn_content_should_return_content_from_dataset(self): """Executing get_dsn_content should return content from dataset.""" command_output = self.files.get_dsn_content(self.test_member_jcl) - self.assertIsInstance(command_output['response'], str) + self.assertIsInstance(command_output["response"], str) def test_get_dsn_content_streamed_should_return_a_raw_response_content(self): """Executing get_dsn_content_streamed should return raw socket response from the server.""" @@ -78,49 +78,51 @@ def test_get_dsn_binary_content_streamed_should_return_a_raw_response_content(se def test_write_to_dsn_should_be_possible(self): """Executing write_to_dsn should be possible.""" command_output = self.files.write_to_dsn(self.test_member_generic, "HELLO WORLD") - self.assertTrue(command_output['response'] == '') - - + self.assertTrue(command_output["response"] == "") + def test_copy_uss_to_dataset_should_be_possible(self): """Executing copy_uss_to_dataset should be possible.""" - command_output = self.files.copy_uss_to_dataset(self.files_fixtures["TEST_USS"],"ZOWE.TESTS.JCL(TEST2)",replace=True) - self.assertTrue(command_output['response']=="") + command_output = self.files.copy_uss_to_dataset( + self.files_fixtures["TEST_USS"], "ZOWE.TESTS.JCL(TEST2)", replace=True + ) + self.assertTrue(command_output["response"] == "") def test_copy_dataset_or_member_should_be_possible(self): """Executing copy_dataset_or_member should be possible.""" test_case = { - "from_dataset_name": self.files_fixtures["TEST_PDS"], - "to_dataset_name": self.files_fixtures["TEST_PDS"], - "from_member_name": self.files_fixtures["TEST_MEMBER"], - "to_member_name": "TEST", - "replace": True + "from_dataset_name": self.files_fixtures["TEST_PDS"], + "to_dataset_name": self.files_fixtures["TEST_PDS"], + "from_member_name": self.files_fixtures["TEST_MEMBER"], + "to_member_name": "TEST", + "replace": True, } command_output = self.files.copy_dataset_or_member(**test_case) - self.assertTrue(command_output['response'] =="") + self.assertTrue(command_output["response"] == "") def test_mount_unmount_zfs_file_system(self): """Mounting a zfs filesystem should be possible""" username = self.user_name.lower() - mount_point = f"/u/{username}/mount" # Assuming a dir called mount exist in zOS USS + mount_point = f"/u/{username}/mount" # Assuming a dir called mount exist in zOS USS # Create a zfs file system zfs_file_system = self.files.create_zFS_file_system(self.test2_zfs_file_system, self.create_zfs_options) - # Mount file system - command_output = self.files.mount_file_system(self.test2_zfs_file_system, mount_point, self.mount_zfs_file_system_options) - self.assertTrue(command_output['response'] == '') + command_output = self.files.mount_file_system( + self.test2_zfs_file_system, mount_point, self.mount_zfs_file_system_options + ) + self.assertTrue(command_output["response"] == "") # List a zfs file system command_output = self.files.list_unix_file_systems(file_system_name=self.test2_zfs_file_system) - self.assertTrue(len(command_output['items']) > 0) + self.assertTrue(len(command_output["items"]) > 0) # Unmount file system command_output = self.files.unmount_file_system(self.test2_zfs_file_system) - self.assertTrue(command_output['response'] == '') + self.assertTrue(command_output["response"] == "") # Delete file system command_output = self.files.delete_zFS_file_system(self.test2_zfs_file_system) - self.assertTrue(command_output['response'] == '') + self.assertTrue(command_output["response"] == "") - #TODO implement tests for download/upload datasets + # TODO implement tests for download/upload datasets diff --git a/tests/integration/test_zos_jobs.py b/tests/integration/test_zos_jobs.py index 224d7b15..e27c5863 100644 --- a/tests/integration/test_zos_jobs.py +++ b/tests/integration/test_zos_jobs.py @@ -1,13 +1,14 @@ """Integration tests for the Zowe Python SDK z/OS Jobs package.""" -import unittest import json import os -from zowe.zos_jobs_for_zowe_sdk import Jobs +import unittest + from zowe.core_for_zowe_sdk import ProfileManager +from zowe.zos_jobs_for_zowe_sdk import Jobs -FIXTURES_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),'fixtures') -JOBS_FIXTURES_JSON_JSON_PATH = os.path.join(FIXTURES_PATH, 'jobs.json') -SAMPLE_JCL_FIXTURE_PATH = os.path.join(FIXTURES_PATH, 'sample.jcl') +FIXTURES_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures") +JOBS_FIXTURES_JSON_JSON_PATH = os.path.join(FIXTURES_PATH, "jobs.json") +SAMPLE_JCL_FIXTURE_PATH = os.path.join(FIXTURES_PATH, "sample.jcl") class TestJobsIntegration(unittest.TestCase): @@ -16,57 +17,57 @@ class TestJobsIntegration(unittest.TestCase): def setUp(self): """Setup fixtures for Jobs class.""" test_profile = ProfileManager().load(profile_type="zosmf") - with open(JOBS_FIXTURES_JSON_JSON_PATH, 'r') as fixtures_json: + with open(JOBS_FIXTURES_JSON_JSON_PATH, "r") as fixtures_json: self.jobs_fixtures_json = json.load(fixtures_json) self.jobs = Jobs(test_profile) def test_get_job_status_should_return_the_status_of_a_job(self): """Executing the get_job_status method should return the status of a given job""" - execution_output = self.jobs.submit_from_mainframe(self.jobs_fixtures_json['TEST_JCL_MEMBER']) - jobname = execution_output['jobname'] - jobid = execution_output['jobid'] + execution_output = self.jobs.submit_from_mainframe(self.jobs_fixtures_json["TEST_JCL_MEMBER"]) + jobname = execution_output["jobname"] + jobid = execution_output["jobid"] command_output = self.jobs.get_job_status(jobname, jobid) - self.assertIsNotNone(command_output['status']) + self.assertIsNotNone(command_output["status"]) def test_list_jobs_should_return_valid_spool_information(self): """Executing the list_jobs method should return a list of found jobs in JES spool.""" - command_output = self.jobs.list_jobs(owner=self.jobs_fixtures_json['TEST_JCL_OWNER']) + command_output = self.jobs.list_jobs(owner=self.jobs_fixtures_json["TEST_JCL_OWNER"]) self.assertIsInstance(command_output, list) - + def test_change_job_class(self): """Execute the change_jobs_class should execute successfully.""" - execution_output = self.jobs.submit_from_mainframe(self.jobs_fixtures_json['TEST_JCL_MEMBER']) - jobname = execution_output['jobname'] - jobid = execution_output['jobid'] - classname = self.jobs_fixtures_json['TEST_JCL_CLASS'] + execution_output = self.jobs.submit_from_mainframe(self.jobs_fixtures_json["TEST_JCL_MEMBER"]) + jobname = execution_output["jobname"] + jobid = execution_output["jobid"] + classname = self.jobs_fixtures_json["TEST_JCL_CLASS"] command_output = self.jobs.change_job_class(jobname, jobid, classname) expected_class = self.jobs.get_job_status(jobname, jobid) - self.assertEqual(expected_class['class'], classname) + self.assertEqual(expected_class["class"], classname) def test_submit_hold_and_release_job_should_execute_properly(self): """Execute the hold_job should execute successfully.""" - execution_output = self.jobs.submit_from_mainframe(self.jobs_fixtures_json['TEST_JCL_MEMBER']) - jobname = execution_output['jobname'] - jobid = execution_output['jobid'] - command_output = self.jobs.submit_from_mainframe(self.jobs_fixtures_json['TEST_JCL_MEMBER']) + execution_output = self.jobs.submit_from_mainframe(self.jobs_fixtures_json["TEST_JCL_MEMBER"]) + jobname = execution_output["jobname"] + jobid = execution_output["jobid"] + command_output = self.jobs.submit_from_mainframe(self.jobs_fixtures_json["TEST_JCL_MEMBER"]) command_output = self.jobs.hold_job(jobname, jobid) command_output = self.jobs.release_job(jobname, jobid) - self.assertIsNotNone(command_output['jobid']) + self.assertIsNotNone(command_output["jobid"]) def test_submit_from_mainframe_should_execute_properly(self): """Executing the submit_from_mainframe method should execute successfully.""" - command_output = self.jobs.submit_from_mainframe(self.jobs_fixtures_json['TEST_JCL_MEMBER']) - jobid = command_output['jobid'] + command_output = self.jobs.submit_from_mainframe(self.jobs_fixtures_json["TEST_JCL_MEMBER"]) + jobid = command_output["jobid"] self.assertIsNotNone(jobid) def test_submit_from_local_file_should_execute_properly(self): """Executing the submit_from_local_file method should execute successfully.""" command_output = self.jobs.submit_from_local_file(SAMPLE_JCL_FIXTURE_PATH) - jobid = command_output['jobid'] + jobid = command_output["jobid"] self.assertIsNotNone(jobid) def test_submit_plaintext_should_execute_properly(self): """Executing the submit_plaintext method should execute successfully.""" - command_output = self.jobs.submit_plaintext('\n'.join(self.jobs_fixtures_json['TEST_JCL_CODE'])) - jobid = command_output['jobid'] + command_output = self.jobs.submit_plaintext("\n".join(self.jobs_fixtures_json["TEST_JCL_CODE"])) + jobid = command_output["jobid"] self.assertIsNotNone(jobid) diff --git a/tests/integration/test_zos_tso.py b/tests/integration/test_zos_tso.py index 90c87208..6df85a5c 100644 --- a/tests/integration/test_zos_tso.py +++ b/tests/integration/test_zos_tso.py @@ -1,7 +1,8 @@ """Integration tests for the Zowe Python SDK z/OS Tso package.""" import unittest -from zowe.zos_tso_for_zowe_sdk import Tso + from zowe.core_for_zowe_sdk import ProfileManager +from zowe.zos_tso_for_zowe_sdk import Tso class TestTsoIntegration(unittest.TestCase): @@ -35,4 +36,3 @@ def test_ping_tso_session_should_return_failure_for_invalid_session(self): """Executing the ping_tso_session method should return a failure message for invalid TSO session.""" command_output = self.tso.ping_tso_session("INVALID") self.assertEqual(command_output, "Ping failed") - diff --git a/tests/integration/test_zosmf.py b/tests/integration/test_zosmf.py index 10ca2e14..3d8a87ab 100644 --- a/tests/integration/test_zosmf.py +++ b/tests/integration/test_zosmf.py @@ -1,7 +1,8 @@ """Integration tests for the Zowe Python SDK z/OSMF package.""" import unittest -from zowe.zosmf_for_zowe_sdk import Zosmf + from zowe.core_for_zowe_sdk import ProfileManager +from zowe.zosmf_for_zowe_sdk import Zosmf class TestZosmfIntegration(unittest.TestCase): @@ -20,4 +21,4 @@ def test_get_info_should_return_valid_response(self): def test_list_systems_should_return_valid_response(self): """Executing the list_systems method should return a valid response.""" command_output = self.zosmf.list_systems() - self.assertIsInstance(command_output, dict) \ No newline at end of file + self.assertIsInstance(command_output, dict) diff --git a/tests/unit/fixtures/invalid.zowe.config.json b/tests/unit/fixtures/invalid.zowe.config.json index bf06a900..89be0a69 100644 --- a/tests/unit/fixtures/invalid.zowe.config.json +++ b/tests/unit/fixtures/invalid.zowe.config.json @@ -1,5 +1,5 @@ { - "$schema": "./zowe.schema.json", + "$schema": "./invalid.zowe.schema.json", "profiles": { "zosmf": { "type": "zosmf", diff --git a/tests/unit/fixtures/invalidUri.zowe.config.json b/tests/unit/fixtures/invalidUri.zowe.config.json new file mode 100644 index 00000000..68b06dbe --- /dev/null +++ b/tests/unit/fixtures/invalidUri.zowe.config.json @@ -0,0 +1,55 @@ +{ + "$schema": "./invalidUri.zowe.schema.json", + "profiles": { + "zosmf": { + "type": "zosmf", + "properties": { + "port": 10443 + }, + "secure": [] + }, + "tso": { + "type": "tso", + "properties": { + "account": "", + "codePage": "1047", + "logonProcedure": "IZUFPROC" + }, + "secure": [] + }, + "ssh": { + "type": "ssh", + "properties": { + "port": 22 + }, + "secure": ["user"] + }, + "zftp": { + "type": "zftp", + "properties": { + "port": 21, + "secureFtp": true + }, + "secure": [] + }, + "base": { + "type": "base", + "properties": { + "host": "zowe.test.cloud", + "rejectUnauthorized": false + }, + "secure": [ + "user", + "password" + ] + } + }, + "defaults": { + "zosmf": "zosmf", + "tso": "tso", + "ssh": "ssh", + "zftp": "zftp", + "base": "base" + }, + "autoStore": true +} \ No newline at end of file diff --git a/tests/unit/fixtures/invalidUri.zowe.schema.json b/tests/unit/fixtures/invalidUri.zowe.schema.json new file mode 100644 index 00000000..3ef45c26 --- /dev/null +++ b/tests/unit/fixtures/invalidUri.zowe.schema.json @@ -0,0 +1,427 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$version": "1.0", + "type": "invalid", + "description": "Zowe configuration", + "properties": { + "profiles": { + "type": "object", + "description": "Mapping of profile names to profile configurations", + "patternProperties": { + "^\\S*$": { + "type": "object", + "description": "Profile configuration object", + "properties": { + "type": { + "description": "Profile type", + "type": "boolean", + "enum": [ + "zosmf", + "tso", + "ssh", + "zftp", + "base" + ] + }, + "properties": { + "description": "Profile properties object", + "type": "object" + }, + "profiles": { + "description": "Optional subprofile configurations", + "type": "object", + "$ref": "#/properties/profiles" + }, + "secure": { + "description": "Secure property names", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "allOf": [ + { + "if": { + "properties": { + "type": false + } + }, + "then": { + "properties": { + "properties": { + "title": "Missing profile type" + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "zosmf" + } + } + }, + "then": { + "properties": { + "properties": { + "type": "object", + "title": "z/OSMF Profile", + "description": "z/OSMF Profile", + "properties": { + "host": { + "type": "string", + "description": "The z/OSMF server host name." + }, + "port": { + "type": "number", + "description": "The z/OSMF server port.", + "default": 443 + }, + "user": { + "type": "string", + "description": "Mainframe (z/OSMF) user name, which can be the same as your TSO login." + }, + "password": { + "type": "string", + "description": "Mainframe (z/OSMF) password, which can be the same as your TSO password." + }, + "rejectUnauthorized": { + "type": "boolean", + "description": "Reject self-signed certificates.", + "default": true + }, + "certFile": { + "type": "string", + "description": "The file path to a certificate file to use for authentication" + }, + "certKeyFile": { + "type": "string", + "description": "The file path to a certificate key file to use for authentication" + }, + "basePath": { + "type": "string", + "description": "The base path for your API mediation layer instance. Specify this option to prepend the base path to all z/OSMF resources when making REST requests. Do not specify this option if you are not using an API mediation layer." + }, + "protocol": { + "type": "string", + "description": "The protocol used (HTTP or HTTPS)", + "default": "https", + "enum": [ + "http", + "https" + ] + }, + "encoding": { + "type": "string", + "description": "The encoding for download and upload of z/OS data set and USS files. The default encoding if not specified is IBM-1047." + }, + "responseTimeout": { + "type": "number", + "description": "The maximum amount of time in seconds the z/OSMF Files TSO servlet should run before returning a response. Any request exceeding this amount of time will be terminated and return an error. Allowed values: 5 - 600" + } + }, + "required": [] + }, + "secure": { + "items": { + "enum": [ + "user", + "password" + ] + } + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "tso" + } + } + }, + "then": { + "properties": { + "properties": { + "type": "object", + "title": "TSO Profile", + "description": "z/OS TSO/E User Profile", + "properties": { + "account": { + "type": "string", + "description": "Your z/OS TSO/E accounting information." + }, + "characterSet": { + "type": "string", + "description": "Character set for address space to convert messages and responses from UTF-8 to EBCDIC.", + "default": "697" + }, + "codePage": { + "type": "string", + "description": "Codepage value for TSO/E address space to convert messages and responses from UTF-8 to EBCDIC.", + "default": "1047" + }, + "columns": { + "type": "number", + "description": "The number of columns on a screen.", + "default": 80 + }, + "logonProcedure": { + "type": "string", + "description": "The logon procedure to use when creating TSO procedures on your behalf.", + "default": "IZUFPROC" + }, + "regionSize": { + "type": "number", + "description": "Region size for the TSO/E address space.", + "default": 4096 + }, + "rows": { + "type": "number", + "description": "The number of rows on a screen.", + "default": 24 + } + }, + "required": [] + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "ssh" + } + } + }, + "then": { + "properties": { + "properties": { + "type": "object", + "title": "z/OS SSH Profile", + "description": "z/OS SSH Profile", + "properties": { + "host": { + "type": "string", + "description": "The z/OS SSH server host name." + }, + "port": { + "type": "number", + "description": "The z/OS SSH server port.", + "default": 22 + }, + "user": { + "type": "string", + "description": "Mainframe user name, which can be the same as your TSO login." + }, + "password": { + "type": "string", + "description": "Mainframe password, which can be the same as your TSO password." + }, + "privateKey": { + "type": "string", + "description": "Path to a file containing your private key, that must match a public key stored in the server for authentication" + }, + "keyPassphrase": { + "type": "string", + "description": "Private key passphrase, which unlocks the private key." + }, + "handshakeTimeout": { + "type": "number", + "description": "How long in milliseconds to wait for the SSH handshake to complete." + } + }, + "required": [] + }, + "secure": { + "items": { + "enum": [ + "user", + "password", + "keyPassphrase" + ] + } + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "zftp" + } + } + }, + "then": { + "properties": { + "properties": { + "type": "object", + "title": "Configuration profile for z/OS FTP", + "description": "Configuration profile for z/OS FTP", + "properties": { + "host": { + "type": "string", + "description": "The hostname or IP address of the z/OS server to connect to." + }, + "port": { + "type": "number", + "description": "The port of the z/OS FTP server.", + "default": 21 + }, + "user": { + "type": "string", + "description": "Username for authentication on z/OS" + }, + "password": { + "type": "string", + "description": "Password to authenticate to FTP." + }, + "secureFtp": { + "type": [ + "boolean", + "null" + ], + "description": "Set to true for both control and data connection encryption, 'control' for control connection encryption only, or 'implicit' for implicitly encrypted control connection (this mode is deprecated in modern times, but usually uses port 990). Note: Unfortunately, this plugin's functionality only works with FTP and FTPS, not 'SFTP' which is FTP over SSH.", + "default": true + }, + "rejectUnauthorized": { + "type": [ + "boolean", + "null" + ], + "description": "Reject self-signed certificates. Only specify this if you are connecting to a secure FTP instance." + }, + "servername": { + "type": [ + "string", + "null" + ], + "description": "Server name for the SNI (Server Name Indication) TLS extension. Only specify if you are connecting securely" + }, + "connectionTimeout": { + "type": "number", + "description": "How long (in milliseconds) to wait for the control connection to be established.", + "default": 10000 + } + } + }, + "secure": { + "items": { + "enum": [ + "user", + "password" + ] + } + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "base" + } + } + }, + "then": { + "properties": { + "properties": { + "type": "object", + "title": "Base Profile", + "description": "Base profile that stores values shared by multiple service profiles", + "properties": { + "host": { + "type": "string", + "description": "Host name of service on the mainframe." + }, + "port": { + "type": "number", + "description": "Port number of service on the mainframe." + }, + "user": { + "type": "string", + "description": "User name to authenticate to service on the mainframe." + }, + "password": { + "type": "string", + "description": "Password to authenticate to service on the mainframe." + }, + "rejectUnauthorized": { + "type": "boolean", + "description": "Reject self-signed certificates.", + "default": true + }, + "tokenType": { + "type": "string", + "description": "The type of token to get and use for the API. Omit this option to use the default token type, which is provided by 'zowe auth login'." + }, + "tokenValue": { + "type": "string", + "description": "The value of the token to pass to the API." + }, + "certFile": { + "type": "string", + "description": "The file path to a certificate file to use for authentication" + }, + "certKeyFile": { + "type": "string", + "description": "The file path to a certificate key file to use for authentication" + } + }, + "required": [] + }, + "secure": { + "items": { + "enum": [ + "user", + "password", + "tokenValue" + ] + } + } + } + } + } + ] + } + } + }, + "defaults": { + "type": "object", + "description": "Mapping of profile types to default profile names", + "properties": { + "zosmf": { + "description": "Default zosmf profile", + "type": "string" + }, + "tso": { + "description": "Default tso profile", + "type": "string" + }, + "ssh": { + "description": "Default ssh profile", + "type": "string" + }, + "zftp": { + "description": "Default zftp profile", + "type": "string" + }, + "base": { + "description": "Default base profile", + "type": "string" + } + } + }, + "autoStore": { + "type": "boolean", + "description": "If true, values you enter when prompted are stored for future use" + } + } +} \ No newline at end of file diff --git a/tests/unit/test_zos_console.py b/tests/unit/test_zos_console.py index 3a7c494d..52e05ac2 100644 --- a/tests/unit/test_zos_console.py +++ b/tests/unit/test_zos_console.py @@ -1,28 +1,50 @@ """Unit tests for the Zowe Python SDK z/OS Console package.""" import unittest -from zowe.zos_console_for_zowe_sdk import Console from unittest import mock +from zowe.zos_console_for_zowe_sdk import Console + class TestConsoleClass(unittest.TestCase): """Console class unit tests.""" def setUp(self): """Setup fixtures for Console class.""" - self.session_details = {"host": "mock-url.com", - "user": "Username", - "password": "Password", - "port": 443, - "rejectUnauthorized": True - } + self.session_details = { + "host": "mock-url.com", + "user": "Username", + "password": "Password", + "port": 443, + "rejectUnauthorized": True, + } def test_object_should_be_instance_of_class(self): """Created object should be instance of Console class.""" console = Console(self.session_details) self.assertIsInstance(console, Console) - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") + def test_issue_command_makes_request_to_the_default_console(self, mock_send): + """Issued command should be sent to the correct default console name if no name is specified""" + is_console_name_correct = False + def send_request_side_effect(self, **other_args): + assert "/defcn" in self.url + return mock.Mock(headers={"Content-type": "application/json"}, status_code=200) + mock_send.side_effect = send_request_side_effect + Console(self.session_details).issue_command("TESTCMD") + + @mock.patch("requests.Session.send") + def test_issue_command_makes_request_to_the_custom_console(self, mock_send): + """Issued command should be sent to the correct custom console name if the console name is specified""" + is_console_name_correct = False + def send_request_side_effect(self, **other_args): + assert "/TESTCNSL" in self.url + return mock.Mock(headers={"Content-type": "application/json"}, status_code=200) + mock_send.side_effect = send_request_side_effect + Console(self.session_details).issue_command("TESTCMD", "TESTCNSL") + + @mock.patch("requests.Session.send") def test_get_response_should_return_messages(self, mock_send_request): """Getting z/OS Console response messages on sending a response key""" mock_send_request.return_value = mock.Mock(headers={"Content-type": "application/json"}, status_code=200) diff --git a/tests/unit/test_zos_files.py b/tests/unit/test_zos_files.py index 1a147a2b..927e80d5 100644 --- a/tests/unit/test_zos_files.py +++ b/tests/unit/test_zos_files.py @@ -1,5 +1,7 @@ """Unit tests for the Zowe Python SDK z/OS Files package.""" +import re from unittest import TestCase, mock + from zowe.zos_files_for_zowe_sdk import Files, exceptions @@ -8,19 +10,20 @@ class TestFilesClass(TestCase): def setUp(self): """Setup fixtures for File class.""" - self.test_profile = {"host": "mock-url.com", - "user": "Username", - "password": "Password", - "port": 443, - "rejectUnauthorized": True - } + self.test_profile = { + "host": "mock-url.com", + "user": "Username", + "password": "Password", + "port": 443, + "rejectUnauthorized": True, + } def test_object_should_be_instance_of_class(self): """Created object should be instance of Files class.""" files = Files(self.test_profile) self.assertIsInstance(files, Files) - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") def test_delete_uss(self, mock_send_request): """Test deleting a directory recursively sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=204) @@ -28,35 +31,41 @@ def test_delete_uss(self, mock_send_request): Files(self.test_profile).delete_uss("filepath_name", recursive=True) mock_send_request.assert_called_once() - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") def test_create_zFS_file_system(self, mock_send_request): """Test creating a zfs sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=201) - Files(self.test_profile).create_zFS_file_system("file_system_name", {"perms":100, "cylsPri": 16777213, "cylsSec": 16777215}) + Files(self.test_profile).create_zFS_file_system( + "file_system_name", {"perms": 100, "cylsPri": 16777213, "cylsSec": 16777215} + ) mock_send_request.assert_called_once() - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") def test_delete_zFS_file_system(self, mock_send_request): """Test deleting a zfs sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=204) Files(self.test_profile).delete_zFS_file_system("file_system_name") mock_send_request.assert_called_once() - + def test_invalid_permission(self): """Test that the correct exception is raised when an invalid permission option is provided""" with self.assertRaises(exceptions.InvalidPermsOption) as e_info: - Files(self.test_profile).create_zFS_file_system("file_system_name", {"perms": -1, "cylsPri": 16777213, "cylsSec": 16777215}) + Files(self.test_profile).create_zFS_file_system( + "file_system_name", {"perms": -1, "cylsPri": 16777213, "cylsSec": 16777215} + ) self.assertEqual(str(e_info.exception), "Invalid zos-files create command 'perms' option: -1") def test_invalid_memory_allocation(self): """Test that the correct exception is raised when an invalid memory allocation option is provided""" with self.assertRaises(exceptions.MaxAllocationQuantityExceeded) as e_info: - Files(self.test_profile).create_zFS_file_system("file_system_name", {"perms": 775, "cylsPri": 1677755513, "cylsSec": 16777215}) + Files(self.test_profile).create_zFS_file_system( + "file_system_name", {"perms": 775, "cylsPri": 1677755513, "cylsSec": 16777215} + ) self.assertEqual(str(e_info.exception), "Maximum allocation quantity of 16777215 exceeded") - - @mock.patch('requests.Session.send') + + @mock.patch("requests.Session.send") def test_mount_zFS_file_system(self, mock_send_request): """Test mounting a zfs sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=204) @@ -64,7 +73,7 @@ def test_mount_zFS_file_system(self, mock_send_request): Files(self.test_profile).mount_file_system("file_system_name", "mount_point") mock_send_request.assert_called_once() - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") def test_unmount_zFS_file_system(self, mock_send_request): """Test unmounting a zfs sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=204) @@ -72,22 +81,17 @@ def test_unmount_zFS_file_system(self, mock_send_request): Files(self.test_profile).unmount_file_system("file_system_name") mock_send_request.assert_called_once() - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") def test_list_dsn(self, mock_send_request): - """Test creating a zfs sends a request""" + """Test list DSN sends request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) - test_values = [ - ('MY.DSN',False), - ('MY.DSN',True) - ] + test_values = [("MY.DSN", False), ("MY.DSN", True)] for test_case in test_values: Files(self.test_profile).list_dsn(*test_case) mock_send_request.assert_called() - - - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") def test_list_zFS_file_system(self, mock_send_request): """Test unmounting a zfs sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) @@ -95,71 +99,70 @@ def test_list_zFS_file_system(self, mock_send_request): Files(self.test_profile).list_unix_file_systems("file_system_name") mock_send_request.assert_called_once() - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") def test_recall_migrated_dataset(self, mock_send_request): """Test recalling migrated data set sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) Files(self.test_profile).recall_migrated_dataset("dataset_name") mock_send_request.assert_called_once() - - @mock.patch('requests.Session.send') + + @mock.patch("requests.Session.send") def test_copy_uss_to_dataset(self, mock_send_request): """Test copy_uss_to_dataset sends a request""" - + mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) - Files(self.test_profile).copy_uss_to_dataset("from_filename","to_dataset_name","to_member_name",replace=True) - - + Files(self.test_profile).copy_uss_to_dataset("from_filename", "to_dataset_name", "to_member_name", replace=True) + mock_send_request.assert_called_once() - + def test_copy_dataset_or_member_raises_exception(self): """Test copying a data set or member raises error when assigning invalid values to enq parameter""" test_case = { - "from_dataset_name": "MY.OLD.DSN", - "to_dataset_name": "MY.NEW.DSN", - "from_member_name": "MYMEM1", - "to_member_name": "MYMEM2", - "enq": "RANDOM", - "replace": True + "from_dataset_name": "MY.OLD.DSN", + "to_dataset_name": "MY.NEW.DSN", + "from_member_name": "MYMEM1", + "to_member_name": "MYMEM2", + "enq": "RANDOM", + "replace": True, } with self.assertRaises(ValueError) as e_info: Files(self.test_profile).copy_dataset_or_member(**test_case) self.assertEqual(str(e_info.exception), "Invalid value for enq.") - - @mock.patch('requests.Session.send') + + @mock.patch("requests.Session.send") def test_copy_dataset_or_member(self, mock_send_request): """Test copying a data set or member sends a request""" - + mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) test_values = [ - { - "from_dataset_name": "MY.OLD.DSN", - "to_dataset_name": "MY.NEW.DSN", - "from_member_name": "MYMEM1", - "to_member_name": "MYMEM2", - "volser":'ABC', - "alias":False, - "enq": "SHRW", - "replace": False - }, - { - "from_dataset_name": "MY.OLD.DSN", - "to_dataset_name": "MY.NEW.DSN", - "from_member_name": "MYMEM1", - "to_member_name": "MYMEM2", - "volser":'ABC', - "alias":True, - "enq": "SHRW", - "replace": True - } + { + "from_dataset_name": "MY.OLD.DSN", + "to_dataset_name": "MY.NEW.DSN", + "from_member_name": "MYMEM1", + "to_member_name": "MYMEM2", + "volser": "ABC", + "alias": False, + "enq": "SHRW", + "replace": False, + }, + { + "from_dataset_name": "MY.OLD.DSN", + "to_dataset_name": "MY.NEW.DSN", + "from_member_name": "MYMEM1", + "to_member_name": "MYMEM2", + "volser": "ABC", + "alias": True, + "enq": "SHRW", + "replace": True, + }, ] for test_case in test_values: Files(self.test_profile).copy_dataset_or_member(**test_case) mock_send_request.assert_called() - + def test_recall_migrated_dataset_parameterized(self): """Testing recall migrated_dataset with different values""" @@ -175,18 +178,17 @@ def test_recall_migrated_dataset_parameterized(self): for test_case in test_values: files_test_profile.request_handler.perform_request = mock.Mock() - data = { - "request": "hrecall", - "wait": test_case[1] - } + data = {"request": "hrecall", "wait": test_case[1]} files_test_profile.recall_migrated_dataset(test_case[0], test_case[1]) custom_args = files_test_profile._create_custom_request_arguments() custom_args["json"] = data custom_args["url"] = "https://mock-url.com:443/zosmf/restfiles/ds/{}".format(test_case[0]) - files_test_profile.request_handler.perform_request.assert_called_once_with("PUT", custom_args, expected_code=[200]) + files_test_profile.request_handler.perform_request.assert_called_once_with( + "PUT", custom_args, expected_code=[200] + ) - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") def test_delete_migrated_data_set(self, mock_send_request): """Test deleting a migrated data set sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) @@ -215,16 +217,17 @@ def test_delete_migrated_data_set_parameterized(self): "request": "hdelete", "purge": test_case[1], "wait": test_case[2], - } files_test_profile.delete_migrated_data_set(test_case[0], test_case[1], test_case[2]) custom_args = files_test_profile._create_custom_request_arguments() custom_args["json"] = data custom_args["url"] = "https://mock-url.com:443/zosmf/restfiles/ds/{}".format(test_case[0]) - files_test_profile.request_handler.perform_request.assert_called_once_with("PUT", custom_args, expected_code=[200]) + files_test_profile.request_handler.perform_request.assert_called_once_with( + "PUT", custom_args, expected_code=[200] + ) - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") def test_migrate_data_set(self, mock_send_request): """Test migrating a data set sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) @@ -257,9 +260,11 @@ def test_migrate_data_set_parameterized(self): custom_args = files_test_profile._create_custom_request_arguments() custom_args["json"] = data custom_args["url"] = "https://mock-url.com:443/zosmf/restfiles/ds/{}".format(test_case[0]) - files_test_profile.request_handler.perform_request.assert_called_once_with("PUT", custom_args, expected_code=[200]) + files_test_profile.request_handler.perform_request.assert_called_once_with( + "PUT", custom_args, expected_code=[200] + ) - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") def test_rename_dataset(self, mock_send_request): """Test renaming dataset sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) @@ -270,9 +275,9 @@ def test_rename_dataset(self, mock_send_request): def test_rename_dataset_parametrized(self): """Test renaming a dataset with different values""" test_values = [ - (('DSN.OLD', "DSN.NEW"), True), - (('DS.NAME.CURRENT', "DS.NAME.NEW"), True), - (('MY.OLD.DSN', "MY.NEW.DSN"), True), + (("DSN.OLD", "DSN.NEW"), True), + (("DS.NAME.CURRENT", "DS.NAME.NEW"), True), + (("MY.OLD.DSN", "MY.NEW.DSN"), True), ] files_test_profile = Files(self.test_profile) @@ -284,7 +289,7 @@ def test_rename_dataset_parametrized(self): "request": "rename", "from-dataset": { "dsn": test_case[0][0].strip(), - } + }, } files_test_profile.rename_dataset(test_case[0][0], test_case[0][1]) @@ -292,9 +297,11 @@ def test_rename_dataset_parametrized(self): custom_args = files_test_profile._create_custom_request_arguments() custom_args["json"] = data custom_args["url"] = "https://mock-url.com:443/zosmf/restfiles/ds/{}".format(test_case[0][1]) - files_test_profile.request_handler.perform_request.assert_called_once_with("PUT", custom_args, expected_code=[200]) + files_test_profile.request_handler.perform_request.assert_called_once_with( + "PUT", custom_args, expected_code=[200] + ) - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") def test_rename_dataset_member(self, mock_send_request): """Test renaming dataset member sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) @@ -311,11 +318,11 @@ def test_rename_dataset_member_raises_exception(self): def test_rename_dataset_member_parametrized(self): """Test renaming a dataset member with different values""" test_values = [ - (('DSN', "MBROLD", "MBRNEW", "EXCLU"), True), - (('DSN', "MBROLD", "MBRNEW", "SHRW"), True), - (('DSN', "MBROLD", "MBRNEW", "INVALID"), False), - (('DATA.SET.NAME', 'MEMBEROLD', 'MEMBERNEW'), True), - (('DS.NAME', "MONAME", "MNNAME"), True), + (("DSN", "MBROLD$", "MBRNEW$", "EXCLU"), True), + (("DSN", "MBROLD#", "MBRNE#", "SHRW"), True), + (("DSN", "MBROLD", "MBRNEW", "INVALID"), False), + (("DATA.SET.@NAME", "MEMBEROLD", "MEMBERNEW"), True), + (("DS.NAME", "MONAME", "MNNAME"), True), ] files_test_profile = Files(self.test_profile) @@ -328,7 +335,7 @@ def test_rename_dataset_member_parametrized(self): "from-dataset": { "dsn": test_case[0][0].strip(), "member": test_case[0][1].strip(), - } + }, } if len(test_case[0]) > 3: @@ -337,10 +344,14 @@ def test_rename_dataset_member_parametrized(self): files_test_profile.rename_dataset_member(*test_case[0]) custom_args = files_test_profile._create_custom_request_arguments() custom_args["json"] = data - custom_args["url"] = "https://mock-url.com:443/zosmf/restfiles/ds/{}({})".format( - test_case[0][0], test_case[0][2]) - files_test_profile.request_handler.perform_request.assert_called_once_with("PUT", custom_args, - expected_code=[200]) + ds_path = "{}({})".format(test_case[0][0], test_case[0][2]) + ds_path_adjusted = files_test_profile._encode_uri_component(ds_path) + self.assertNotRegex(ds_path_adjusted, r"[\$\@\#]") + self.assertRegex(ds_path_adjusted, r"[\(" + re.escape(test_case[0][2]) + r"\)]") + custom_args["url"] = "https://mock-url.com:443/zosmf/restfiles/ds/{}".format(ds_path_adjusted) + files_test_profile.request_handler.perform_request.assert_called_once_with( + "PUT", custom_args, expected_code=[200] + ) else: with self.assertRaises(ValueError) as e_info: files_test_profile.rename_dataset_member(*test_case[0]) @@ -349,13 +360,9 @@ def test_rename_dataset_member_parametrized(self): def test_create_data_set_raises_error_without_required_arguments(self): """Test not providing required arguments raises an error""" with self.assertRaises(ValueError) as e_info: - obj = Files(self.test_profile).create_data_set("DSNAME123", options={ - "alcunit": "CYL", - "dsorg": "PO", - "recfm": "FB", - "blksize": 6160, - "dirblk": 25 - }) + obj = Files(self.test_profile).create_data_set( + "DSNAME123", options={"alcunit": "CYL", "dsorg": "PO", "recfm": "FB", "blksize": 6160, "dirblk": 25} + ) self.assertEqual(str(e_info.exception), "If 'like' is not specified, you must specify 'primary' or 'lrecl'.") def test_create_data_set_raises_error_with_invalid_arguments_parameterized(self): @@ -368,7 +375,7 @@ def test_create_data_set_raises_error_with_invalid_arguments_parameterized(self) "dirblk": 5, "recfm": "FB", "blksize": 6160, - "lrecl": 80 + "lrecl": 80, }, { "dsorg": "PO", @@ -377,7 +384,7 @@ def test_create_data_set_raises_error_with_invalid_arguments_parameterized(self) "recfm": "invalid", "blksize": 32760, "lrecl": 260, - "dirblk": 25 + "dirblk": 25, }, { "alcunit": "CYL", @@ -386,7 +393,7 @@ def test_create_data_set_raises_error_with_invalid_arguments_parameterized(self) "dirblk": 5, "recfm": "FB", "blksize": 6160, - "lrecl": 80 + "lrecl": 80, }, { "dsorg": "PO", @@ -395,7 +402,7 @@ def test_create_data_set_raises_error_with_invalid_arguments_parameterized(self) "recfm": "U", "blksize": 27998, "lrecl": 27998, - "dirblk": 0 + "dirblk": 0, }, { "alcunit": "CYL", @@ -404,8 +411,8 @@ def test_create_data_set_raises_error_with_invalid_arguments_parameterized(self) "dirblk": 5, "recfm": "FB", "blksize": 6160, - "lrecl": 80 - } + "lrecl": 80, + }, ] for test_case in test_values: @@ -415,47 +422,67 @@ def test_create_data_set_raises_error_with_invalid_arguments_parameterized(self) def test_create_dataset_parameterized(self): """Test create dataset with different values""" test_values = [ - (("DSN", { - "alcunit": "CYL", - "dsorg": "PO", - "primary": 1, - "dirblk": 5, - "recfm": "FB", - "blksize": 6160, - "lrecl": 80 - }), True), - (("DSN", { - "alcunit": "CYL", - "dsorg": "PO", - "primary": 1, - "recfm": "FB", - "blksize": 6160, - "lrecl": 80, - "dirblk": 25 - }), True), - (("DSN", { - "dsorg": "PO", - "alcunit": "CYL", - "primary": 1, - "recfm": "VB", - "blksize": 32760, - "lrecl": 260, - "dirblk": 25 - }), True), - (("DSN", { - "alcunit": "CYL", - "dsorg": "PS", - "primary": 1, - "recfm": "FB", - "blksize": 6160, - "lrecl": 80 - }), True), - (("DSN", { - "alcunit": "CYL", - "dsorg": "PS", - "recfm": "FB", - "blksize": 6160, - }), False), + ( + ( + "DSN", + { + "alcunit": "CYL", + "dsorg": "PO", + "primary": 1, + "dirblk": 5, + "recfm": "FB", + "blksize": 6160, + "lrecl": 80, + }, + ), + True, + ), + ( + ( + "DSN", + { + "alcunit": "CYL", + "dsorg": "PO", + "primary": 1, + "recfm": "FB", + "blksize": 6160, + "lrecl": 80, + "dirblk": 25, + }, + ), + True, + ), + ( + ( + "DSN", + { + "dsorg": "PO", + "alcunit": "CYL", + "primary": 1, + "recfm": "VB", + "blksize": 32760, + "lrecl": 260, + "dirblk": 25, + }, + ), + True, + ), + ( + ("DSN", {"alcunit": "CYL", "dsorg": "PS", "primary": 1, "recfm": "FB", "blksize": 6160, "lrecl": 80}), + True, + ), + ( + ( + "DSN", + { + "alcunit": "CYL", + "dsorg": "PS", + "recfm": "FB", + "blksize": 6160, + }, + ), + False, + ), ] files_test_profile = Files(self.test_profile) @@ -468,13 +495,17 @@ def test_create_dataset_parameterized(self): custom_args = files_test_profile._create_custom_request_arguments() custom_args["json"] = test_case[0][1] custom_args["url"] = "https://mock-url.com:443/zosmf/restfiles/ds/{}".format(test_case[0][0]) - files_test_profile.request_handler.perform_request.assert_called_once_with("POST", custom_args, expected_code=[201]) + files_test_profile.request_handler.perform_request.assert_called_once_with( + "POST", custom_args, expected_code=[201] + ) else: with self.assertRaises(ValueError) as e_info: files_test_profile.create_data_set(*test_case[0]) - self.assertEqual(str(e_info.exception), "If 'like' is not specified, you must specify 'primary' or 'lrecl'.") + self.assertEqual( + str(e_info.exception), "If 'like' is not specified, you must specify 'primary' or 'lrecl'." + ) - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") def test_create_default_dataset(self, mock_send_request): """Test creating a default data set sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=201) @@ -506,7 +537,7 @@ def test_create_default_dataset_parameterized(self): "dirblk": 5, "recfm": "FB", "blksize": 6160, - "lrecl": 80 + "lrecl": 80, }, "sequential": { "alcunit": "CYL", @@ -514,7 +545,7 @@ def test_create_default_dataset_parameterized(self): "primary": 1, "recfm": "FB", "blksize": 6160, - "lrecl": 80 + "lrecl": 80, }, "classic": { "alcunit": "CYL", @@ -523,7 +554,7 @@ def test_create_default_dataset_parameterized(self): "recfm": "FB", "blksize": 6160, "lrecl": 80, - "dirblk": 25 + "dirblk": 25, }, "c": { "dsorg": "PO", @@ -532,7 +563,7 @@ def test_create_default_dataset_parameterized(self): "recfm": "VB", "blksize": 32760, "lrecl": 260, - "dirblk": 25 + "dirblk": 25, }, "binary": { "dsorg": "PO", @@ -541,8 +572,8 @@ def test_create_default_dataset_parameterized(self): "recfm": "U", "blksize": 27998, "lrecl": 27998, - "dirblk": 25 - } + "dirblk": 25, + }, } if test_case[1]: @@ -550,7 +581,9 @@ def test_create_default_dataset_parameterized(self): custom_args = files_test_profile._create_custom_request_arguments() custom_args["json"] = options.get(test_case[0][1]) custom_args["url"] = "https://mock-url.com:443/zosmf/restfiles/ds/{}".format(test_case[0][0]) - files_test_profile.request_handler.perform_request.assert_called_once_with("POST", custom_args, expected_code=[201]) + files_test_profile.request_handler.perform_request.assert_called_once_with( + "POST", custom_args, expected_code=[201] + ) else: with self.assertRaises(ValueError) as e_info: files_test_profile.create_default_data_set(*test_case[0]) diff --git a/tests/unit/test_zos_jobs.py b/tests/unit/test_zos_jobs.py index e64b12cb..5bb93528 100644 --- a/tests/unit/test_zos_jobs.py +++ b/tests/unit/test_zos_jobs.py @@ -1,6 +1,7 @@ """Unit tests for the Zowe Python SDK z/OS Jobs package.""" from unittest import TestCase, mock + from zowe.zos_jobs_for_zowe_sdk import Jobs @@ -14,74 +15,74 @@ def setUp(self): "user": "Username", "password": "Password", "port": 443, - "rejectUnauthorized": True + "rejectUnauthorized": True, } def test_object_should_be_instance_of_class(self): """Created object should be instance of Jobs class.""" jobs = Jobs(self.test_profile) self.assertIsInstance(jobs, Jobs) - - @mock.patch('requests.Session.send') + + @mock.patch("requests.Session.send") def test_cancel_job(self, mock_send_request): """Test cancelling a job sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) - Jobs(self.test_profile).cancel_job("TESTJOB2","JOB00084") + Jobs(self.test_profile).cancel_job("TESTJOB2", "JOB00084") mock_send_request.assert_called_once() - - @mock.patch('requests.Session.send') + + @mock.patch("requests.Session.send") def test_hold_job(self, mock_send_request): """Test holding a job sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) - Jobs(self.test_profile).hold_job("TESTJOB2","JOB00084") + Jobs(self.test_profile).hold_job("TESTJOB2", "JOB00084") mock_send_request.assert_called_once() - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") def test_modified_version_hold_job(self, mock_send_request): """Test holding a job sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) with self.assertRaises(ValueError): - Jobs(self.test_profile).hold_job("TESTJOB2","JOB00084",modify_version="3.0") - - @mock.patch('requests.Session.send') + Jobs(self.test_profile).hold_job("TESTJOB2", "JOB00084", modify_version="3.0") + + @mock.patch("requests.Session.send") def test_modified_version_release_job(self, mock_send_request): """Test holding a job sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) with self.assertRaises(ValueError): - Jobs(self.test_profile).release_job("TESTJOB2","JOB00084",modify_version="3.0") - - @mock.patch('requests.Session.send') + Jobs(self.test_profile).release_job("TESTJOB2", "JOB00084", modify_version="3.0") + + @mock.patch("requests.Session.send") def test_release_job(self, mock_send_request): """Test releasing a job sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) - Jobs(self.test_profile).release_job("TESTJOB2","JOB00084") + Jobs(self.test_profile).release_job("TESTJOB2", "JOB00084") mock_send_request.assert_called_once() - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") def test_change_job_class(self, mock_send_request): """Test changing the job class sends a request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) - Jobs(self.test_profile).change_job_class("TESTJOB2","JOB00084","A") + Jobs(self.test_profile).change_job_class("TESTJOB2", "JOB00084", "A") mock_send_request.assert_called_once() - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") def test_modified_version_error(self, mock_send_request): """Test modified version should raise value error""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) with self.assertRaises(ValueError): - Jobs(self.test_profile).change_job_class("TESTJOB2","JOB00084","A",modify_version="3.0") + Jobs(self.test_profile).change_job_class("TESTJOB2", "JOB00084", "A", modify_version="3.0") def test_cancel_job_modify_version_parameterized(self): """Test cancelling a job with different values sends the expected request""" test_values = [ - (("TESTJOB", "JOB00010", "1.0"), True), + (("TESTJOB", "JOB$0010", "1.0"), True), (("TESTJOBN", "JOB00011", "2.0"), True), (("TESTJOB", "JOB00012", "2"), False), (("TESTJOBN", "JOB00113", "3.0"), False), @@ -100,8 +101,13 @@ def test_cancel_job_modify_version_parameterized(self): "request": "cancel", "version": test_case[0][2], } - custom_args["url"] = "https://mock-url.com:443/zosmf/restjobs/jobs/{}/{}".format(test_case[0][0], test_case[0][1]) - jobs_test_object.request_handler.perform_request.assert_called_once_with("PUT", custom_args, expected_code=[202, 200]) + job_url = "{}/{}".format(test_case[0][0], test_case[0][1]) + job_url_adjusted = jobs_test_object._encode_uri_component(job_url) + self.assertNotRegex(job_url_adjusted, r"\$") + custom_args["url"] = "https://mock-url.com:443/zosmf/restjobs/jobs/{}".format(job_url_adjusted) + jobs_test_object.request_handler.perform_request.assert_called_once_with( + "PUT", custom_args, expected_code=[202, 200] + ) else: with self.assertRaises(ValueError) as e_info: jobs_test_object.cancel_job(*test_case[0]) diff --git a/tests/unit/test_zos_tso.py b/tests/unit/test_zos_tso.py index fea0594a..adb4ece2 100644 --- a/tests/unit/test_zos_tso.py +++ b/tests/unit/test_zos_tso.py @@ -1,6 +1,7 @@ """Unit tests for the Zowe Python SDK z/OS TSO package.""" import unittest + from zowe.zos_tso_for_zowe_sdk import Tso @@ -9,14 +10,15 @@ class TestTsoClass(unittest.TestCase): def setUp(self): """Setup fixtures for Tso class.""" - self.connection_dict = {"host": "mock-url.com", - "user": "Username", - "password": "Password", - "port": 443, - "rejectUnauthorized": True - } + self.connection_dict = { + "host": "mock-url.com", + "user": "Username", + "password": "Password", + "port": 443, + "rejectUnauthorized": True, + } def test_object_should_be_instance_of_class(self): """Created object should be instance of Tso class.""" tso = Tso(self.connection_dict) - self.assertIsInstance(tso, Tso) \ No newline at end of file + self.assertIsInstance(tso, Tso) diff --git a/tests/unit/test_zosmf.py b/tests/unit/test_zosmf.py index 00e38a52..91a81e10 100644 --- a/tests/unit/test_zosmf.py +++ b/tests/unit/test_zosmf.py @@ -2,6 +2,7 @@ import unittest from unittest import mock + from zowe.zosmf_for_zowe_sdk import Zosmf @@ -10,21 +11,22 @@ class TestZosmfClass(unittest.TestCase): def setUp(self): """Setup fixtures for Zosmf class.""" - self.connection_dict = {"host": "mock-url.com", - "user": "Username", - "password": "Password", - "port": 443, - "rejectUnauthorized": True - } + self.connection_dict = { + "host": "mock-url.com", + "user": "Username", + "password": "Password", + "port": 443, + "rejectUnauthorized": True, + } def test_object_should_be_instance_of_class(self): """Created object should be instance of Zosmf class.""" zosmf = Zosmf(self.connection_dict) self.assertIsInstance(zosmf, Zosmf) - @mock.patch('requests.Session.send') + @mock.patch("requests.Session.send") def test_list_systems(self, mock_send_request): """Listing z/OSMF systems should send a REST request""" mock_send_request.return_value = mock.Mock(headers={"Content-Type": "application/json"}, status_code=200) Zosmf(self.connection_dict).list_systems() - mock_send_request.assert_called_once() \ No newline at end of file + mock_send_request.assert_called_once() diff --git a/tests/unit/test_zowe_core.py b/tests/unit/test_zowe_core.py index 518c51c5..202fab0e 100644 --- a/tests/unit/test_zowe_core.py +++ b/tests/unit/test_zowe_core.py @@ -2,29 +2,31 @@ # Including necessary paths import base64 +import importlib.util import json import os import shutil -import sys import unittest from unittest import mock -from unittest.mock import patch -from jsonschema import validate, ValidationError -from zowe.core_for_zowe_sdk.validators import validate_config_json -import commentjson +import commentjson +from jsonschema import SchemaError, ValidationError, validate from pyfakefs.fake_filesystem_unittest import TestCase from zowe.core_for_zowe_sdk import ( ApiConnection, ConfigFile, + CredentialManager, ProfileManager, RequestHandler, SdkApi, ZosmfProfile, + constants, + custom_warnings, exceptions, session_constants, - custom_warnings, ) +from zowe.core_for_zowe_sdk.validators import validate_config_json +from zowe.secrets_for_zowe_sdk import keyring FIXTURES_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures") CWD = os.getcwd() @@ -76,12 +78,7 @@ class TestSdkApiClass(TestCase): def setUp(self): """Setup fixtures for SdkApi class.""" - common_props = { - "host": "mock-url.com", - "port": 443, - "protocol": "https", - "rejectUnauthorized": True - } + common_props = {"host": "mock-url.com", "port": 443, "protocol": "https", "rejectUnauthorized": True} self.basic_props = {**common_props, "user": "Username", "password": "Password"} self.bearer_props = {**common_props, "tokenValue": "BearerToken"} self.token_props = { @@ -123,6 +120,23 @@ def test_should_handle_token_auth(self): self.token_props["tokenType"] + "=" + self.token_props["tokenValue"], ) + def test_encode_uri_component(self): + """Test string is being adjusted to the correct URL parameter""" + + sdk_api = SdkApi(self.basic_props, self.default_url) + + actual_not_empty = sdk_api._encode_uri_component("MY.STRING@.TEST#.$HERE(MBR#NAME)") + expected_not_empty = "MY.STRING%40.TEST%23.%24HERE(MBR%23NAME)" + self.assertEqual(actual_not_empty, expected_not_empty) + + actual_wildcard = sdk_api._encode_uri_component("GET.#DS.*") + expected_wildcard = "GET.%23DS.*" + self.assertEqual(actual_wildcard, expected_wildcard) + + actual_none = sdk_api._encode_uri_component(None) + expected_none = None + self.assertEqual(actual_none, expected_none) + class TestRequestHandlerClass(unittest.TestCase): """RequestHandler class unit tests.""" @@ -165,22 +179,29 @@ class TestZosmfProfileManager(TestCase): def setUp(self): """Setup fixtures for ZosmfProfile class.""" # setup pyfakefs + self.session_arguments = {"verify": False} self.setUpPyfakefs() self.original_file_path = os.path.join(FIXTURES_PATH, "zowe.config.json") - self.original_user_file_path = os.path.join( - FIXTURES_PATH, "zowe.config.user.json" - ) - self.original_nested_file_path = os.path.join( - FIXTURES_PATH, "nested.zowe.config.json" - ) - self.original_schema_file_path = os.path.join( - FIXTURES_PATH, "zowe.schema.json" - ) + self.original_user_file_path = os.path.join(FIXTURES_PATH, "zowe.config.user.json") + self.original_invalid_file_path = os.path.join(FIXTURES_PATH, "invalid.zowe.config.json") + self.original_nested_file_path = os.path.join(FIXTURES_PATH, "nested.zowe.config.json") + self.original_schema_file_path = os.path.join(FIXTURES_PATH, "zowe.schema.json") + self.original_invalid_schema_file_path = os.path.join(FIXTURES_PATH, "invalid.zowe.schema.json") + self.original_invalidUri_file_path = os.path.join(FIXTURES_PATH, "invalidUri.zowe.config.json") + self.original_invalidUri_schema_file_path = os.path.join(FIXTURES_PATH, "invalidUri.zowe.schema.json") + + loader = importlib.util.find_spec("jsonschema") + module_path = loader.origin + self.fs.add_real_directory(os.path.dirname(module_path)) + self.fs.add_real_file(self.original_file_path) self.fs.add_real_file(self.original_user_file_path) self.fs.add_real_file(self.original_nested_file_path) self.fs.add_real_file(self.original_schema_file_path) - + self.fs.add_real_file(self.original_invalid_file_path) + self.fs.add_real_file(self.original_invalid_schema_file_path) + self.fs.add_real_file(self.original_invalidUri_file_path) + self.fs.add_real_file(self.original_invalidUri_schema_file_path) self.custom_dir = os.path.dirname(FIXTURES_PATH) self.custom_appname = "zowe_abcd" self.custom_filename = f"{self.custom_appname}.config.json" @@ -199,11 +220,9 @@ def setUpCreds(self, file_path, secure_props): } global SECURE_CONFIG_PROPS - SECURE_CONFIG_PROPS = base64.b64encode((json.dumps(CRED_DICT)).encode("utf-8")) - if sys.platform == "win32": - SECURE_CONFIG_PROPS = SECURE_CONFIG_PROPS.decode("utf-16") + SECURE_CONFIG_PROPS = base64.b64encode((json.dumps(CRED_DICT)).encode()).decode() - @patch("keyring.get_password", side_effect=keyring_get_password) + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) def test_autodiscovery_and_base_profile_loading(self, get_pass_func): """ Test loading of correct file by autodiscovering from current working directory @@ -217,14 +236,17 @@ def test_autodiscovery_and_base_profile_loading(self, get_pass_func): os.chdir(CWD) shutil.copy(self.original_file_path, cwd_up_file_path) - self.setUpCreds(cwd_up_file_path, { - "profiles.base.properties.user": "user", - "profiles.base.properties.password": "password", - }) + self.setUpCreds( + cwd_up_file_path, + { + "profiles.base.properties.user": "user", + "profiles.base.properties.password": "password", + }, + ) # Test prof_manager = ProfileManager() - props: dict = prof_manager.load(profile_type="base") + props: dict = prof_manager.load(profile_type="base", validate_schema=False) self.assertEqual(prof_manager.config_filepath, cwd_up_file_path) expected_props = { @@ -235,7 +257,7 @@ def test_autodiscovery_and_base_profile_loading(self, get_pass_func): } self.assertEqual(props, expected_props) - @patch("keyring.get_password", side_effect=keyring_get_password) + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) def test_custom_file_and_custom_profile_loading(self, get_pass_func): """ Test loading of correct file given a filename and directory, @@ -246,15 +268,18 @@ def test_custom_file_and_custom_profile_loading(self, get_pass_func): custom_file_path = os.path.join(self.custom_dir, self.custom_filename) shutil.copy(self.original_file_path, custom_file_path) - self.setUpCreds(custom_file_path, { - "profiles.zosmf.properties.user": "user", - "profiles.zosmf.properties.password": "password", - }) + self.setUpCreds( + custom_file_path, + { + "profiles.zosmf.properties.user": "user", + "profiles.zosmf.properties.password": "password", + }, + ) # Test prof_manager = ProfileManager(appname=self.custom_appname) prof_manager.config_dir = self.custom_dir - props: dict = prof_manager.load(profile_name="zosmf") + props: dict = prof_manager.load(profile_name="zosmf", validate_schema=False) self.assertEqual(prof_manager.config_filepath, custom_file_path) expected_props = { @@ -266,7 +291,7 @@ def test_custom_file_and_custom_profile_loading(self, get_pass_func): } self.assertEqual(props, expected_props) - @patch("keyring.get_password", side_effect=keyring_get_password) + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) def test_custom_file_and_custom_profile_loading_with_nested_profile(self, get_pass_func): """ Test loading of correct file given a filename and directory, @@ -277,26 +302,25 @@ def test_custom_file_and_custom_profile_loading_with_nested_profile(self, get_pa custom_file_path = os.path.join(self.custom_dir, self.custom_filename) shutil.copy(self.original_nested_file_path, custom_file_path) - self.setUpCreds(custom_file_path, { - "profiles.zosmf.properties.user": "user", - "profiles.zosmf.properties.password": "password", - }) + self.setUpCreds( + custom_file_path, + { + "profiles.zosmf.properties.user": "user", + "profiles.zosmf.properties.password": "password", + }, + ) # Test prof_manager = ProfileManager(appname=self.custom_appname) prof_manager.config_dir = self.custom_dir - props: dict = prof_manager.load(profile_name="lpar1.zosmf") + props: dict = prof_manager.load(profile_name="lpar1.zosmf", validate_schema=False) self.assertEqual(prof_manager.config_filepath, custom_file_path) - expected_props = { - "host": "example1.com", - "rejectUnauthorized": True, - "port": 443 - } + expected_props = {"host": "example1.com", "rejectUnauthorized": True, "port": 443} self.assertEqual(props, expected_props) - @patch("keyring.get_password", side_effect=keyring_get_password) - def test_profile_loading_with_user_overriden_properties(self, get_pass_func): + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) + def test_profile_loading_with_user_overridden_properties(self, get_pass_func): """ Test overriding of properties from user config, also load by profile_name correctly populating fields from base profile @@ -309,14 +333,17 @@ def test_profile_loading_with_user_overriden_properties(self, get_pass_func): shutil.copy(self.original_file_path, cwd_up_file_path) shutil.copy(self.original_user_file_path, cwd_up_dir_path) - self.setUpCreds(cwd_up_file_path, { - "profiles.zosmf.properties.user": "user", - "profiles.zosmf.properties.password": "password", - }) + self.setUpCreds( + cwd_up_file_path, + { + "profiles.zosmf.properties.user": "user", + "profiles.zosmf.properties.password": "password", + }, + ) # Test prof_manager = ProfileManager() - props: dict = prof_manager.load(profile_type="zosmf") + props: dict = prof_manager.load(profile_type="zosmf", validate_schema=False) self.assertEqual(prof_manager.config_filepath, cwd_up_file_path) expected_props = { @@ -328,7 +355,7 @@ def test_profile_loading_with_user_overriden_properties(self, get_pass_func): } self.assertEqual(props, expected_props) - @patch("keyring.get_password", side_effect=keyring_get_password) + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) def test_profile_loading_exception(self, get_pass_func): """ Test correct exceptions are being thrown when a profile is @@ -339,17 +366,15 @@ def test_profile_loading_exception(self, get_pass_func): with self.assertWarns(custom_warnings.ProfileNotFoundWarning): # Setup cwd_up_dir_path = os.path.dirname(CWD) - cwd_up_file_path = os.path.join( - cwd_up_dir_path, f"{self.custom_appname}.config.json" - ) + cwd_up_file_path = os.path.join(cwd_up_dir_path, f"{self.custom_appname}.config.json") os.chdir(CWD) shutil.copy(self.original_file_path, cwd_up_file_path) # Test config_file = ConfigFile(name=self.custom_appname, type="team_config") - props: dict = config_file.get_profile(profile_name="non_existent_profile") + props: dict = config_file.get_profile(profile_name="non_existent_profile", validate_schema=False) - @patch("keyring.get_password", side_effect=keyring_get_password_exception) + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password_exception) def test_secure_props_loading_warning(self, get_pass_func): """ Test correct warnings are being thrown when secure properties @@ -365,9 +390,9 @@ def test_secure_props_loading_warning(self, get_pass_func): # Test prof_manager = ProfileManager() prof_manager.config_dir = self.custom_dir - props: dict = prof_manager.load("base") + props: dict = prof_manager.load("base", validate_schema=False) - @patch("keyring.get_password", side_effect=keyring_get_password) + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) def test_profile_not_found_warning(self, get_pass_func): """ Test correct warnings are being thrown when profile is not found @@ -383,9 +408,240 @@ def test_profile_not_found_warning(self, get_pass_func): # Test prof_manager = ProfileManager() prof_manager.config_dir = self.custom_dir - props: dict = prof_manager.load("non_existent_profile") + props: dict = prof_manager.load("non_existent_profile", validate_schema=False) + + @mock.patch("sys.platform", "win32") + @mock.patch("zowe.core_for_zowe_sdk.CredentialManager._get_credential") + def test_load_secure_props(self, retrieve_cred_func): + """ + Test loading secure_props from keyring or storage. + """ + service_name = constants["ZoweServiceName"] + account_name = constants["ZoweAccountName"] + # Setup - copy profile to fake filesystem created by pyfakefs + cwd_up_dir_path = os.path.dirname(CWD) + cwd_up_file_path = os.path.join(cwd_up_dir_path, "zowe.config.json") + os.chdir(CWD) + shutil.copy(self.original_file_path, cwd_up_file_path) + credential = { + cwd_up_file_path: {"profiles.base.properties.user": "user", "profiles.base.properties.password": "password"} + } + self.setUpCreds(cwd_up_file_path, credential) + encoded_credential = base64.b64encode(commentjson.dumps(credential).encode()).decode() + retrieve_cred_func.return_value = encoded_credential + + # call the load_secure_props method + credential_manager = CredentialManager() + credential_manager.load_secure_props() + retrieve_cred_func.assert_called_once_with(service_name, account_name) + # Verify the secure_props + expected_secure_props = credential + self.assertEqual(credential_manager.secure_props, expected_secure_props) + + @mock.patch("sys.platform", "win32") + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.delete_password") + def test_delete_credential(self, delete_pass_func): + """ + Test the delete_credential method for deleting credentials from keyring. + """ + + def side_effect(*args, **kwargs): + if side_effect.counter < 2: + side_effect.counter += 1 + return False + else: + return True + + side_effect.counter = 0 + + # custom side effect function for the mock + delete_pass_func.side_effect = side_effect + credential_manager = CredentialManager() + service_name = constants["ZoweServiceName"] + account_name = constants["ZoweAccountName"] + # Delete the credential + credential_manager._delete_credential(service_name, account_name) + expected_calls = [ + mock.call(service_name, account_name), + mock.call(service_name, f"{account_name}-1"), + ] + delete_pass_func.assert_has_calls(expected_calls) + + @mock.patch("sys.platform", "win32") + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=["password", None, "part1", "part2\0", None]) + def test_retrieve_credential(self, get_pass_func): + """ + Test the _retrieve_credential method for retrieving credentials from keyring. + """ + credential_manager = CredentialManager() + + # Scenario 1: Retrieve password directly + expected_password1 = "password" + retrieve_credential1 = credential_manager._get_credential(constants["ZoweServiceName"], constants["ZoweAccountName"]) + self.assertEqual(retrieve_credential1, expected_password1) + get_pass_func.assert_called_with(constants["ZoweServiceName"], constants["ZoweAccountName"]) + + # Scenario 2: Retrieve password in parts + expected_password2 = "part1part2" + retrieve_credential2 = credential_manager._get_credential(constants["ZoweServiceName"], constants["ZoweAccountName"]) + self.assertEqual(retrieve_credential2, expected_password2) + get_pass_func.assert_any_call(constants["ZoweServiceName"], constants["ZoweAccountName"]) + get_pass_func.assert_any_call(constants["ZoweServiceName"], f"{constants['ZoweAccountName']}-1") + get_pass_func.assert_any_call(constants["ZoweServiceName"], f"{constants['ZoweAccountName']}-2") + + @mock.patch("sys.platform", "win32") + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=[None, None]) + def test_retrieve_credential_encoding_errors(self, get_pass_func): + """ + Test the _retrieve_credential method for handling encoding errors and None values. + """ + result = CredentialManager._get_credential(constants["ZoweServiceName"], constants["ZoweAccountName"]) + self.assertIsNone(result) + get_pass_func.assert_called_with(constants["ZoweServiceName"], f"{constants['ZoweAccountName']}-1") + + @mock.patch("sys.platform", "win32") + @mock.patch("zowe.core_for_zowe_sdk.CredentialManager._set_credential") + @mock.patch("zowe.core_for_zowe_sdk.CredentialManager._get_credential") + @mock.patch("zowe.core_for_zowe_sdk.CredentialManager._delete_credential") + def test_save_secure_props_normal_credential(self, delete_pass_func, retrieve_cred_func, set_pass_func): + """ + Test the save_secure_props method for saving credentials to keyring. + """ - @patch("keyring.get_password", side_effect=keyring_get_password) + # Setup - copy profile to fake filesystem created by pyfakefs + cwd_up_dir_path = os.path.dirname(CWD) + cwd_up_file_path = os.path.join(cwd_up_dir_path, "zowe.config.json") + os.chdir(CWD) + shutil.copy(self.original_file_path, cwd_up_file_path) + credential = { + cwd_up_file_path: { + "profiles.base.properties.user": "samadpls", + "profiles.base.properties.password": "password", + } + } + self.setUpCreds(cwd_up_file_path, credential) + encoded_credential = base64.b64encode(commentjson.dumps(credential).encode()).decode() + retrieve_cred_func.return_value = None + + CredentialManager.secure_props = credential + CredentialManager.save_secure_props() + # delete the existing credential + delete_pass_func.return_value = None + # Verify the keyring function call + set_pass_func.assert_called_once_with(constants["ZoweServiceName"], constants["ZoweAccountName"], encoded_credential) + + @mock.patch("sys.platform", "win32") + @mock.patch("zowe.core_for_zowe_sdk.CredentialManager._get_credential") + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.set_password") + @mock.patch("zowe.core_for_zowe_sdk.CredentialManager._delete_credential") + def test_save_secure_props_exceed_limit(self, delete_pass_func, set_pass_func, retrieve_cred_func): + # Setup - copy profile to fake filesystem created by pyfakefs + cwd_up_dir_path = os.path.dirname(CWD) + cwd_up_file_path = os.path.join(cwd_up_dir_path, "zowe.config.json") + os.chdir(CWD) + shutil.copy(self.original_file_path, cwd_up_file_path) + credential = { + cwd_up_file_path: { + "profiles.base.properties.user": "user", + "profiles.base.properties.password": "a" * (constants["WIN32_CRED_MAX_STRING_LENGTH"] + 1), + } + } + self.setUpCreds(cwd_up_file_path, credential) + encoded_credential = base64.b64encode(commentjson.dumps(credential).encode()).decode() + encoded_credential += "\0" + retrieve_cred_func.return_value = encoded_credential + + CredentialManager.secure_props = credential + CredentialManager.save_secure_props() + + # delete the existing credential + delete_pass_func.return_value = None + + expected_calls = [] + chunk_size = constants["WIN32_CRED_MAX_STRING_LENGTH"] + chunks = [ + encoded_credential[i : i + chunk_size] for i in range(0, len(encoded_credential), chunk_size) + ] + for index, chunk in enumerate(chunks, start=1): + field_name = f"{constants['ZoweAccountName']}-{index}" + expected_calls.append(mock.call(constants["ZoweServiceName"], field_name, chunk)) + set_pass_func.assert_has_calls(expected_calls) + + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) + def test_profile_loading_with_valid_schema(self, get_pass_func): + """ + Test Validation, no error should be raised for valid schema + """ + # Setup - copy profile to fake filesystem created by pyfakefs + custom_file_path = os.path.join(self.custom_dir, "zowe.config.json") + shutil.copy(self.original_nested_file_path, custom_file_path) + shutil.copy(self.original_schema_file_path, self.custom_dir) + os.chdir(self.custom_dir) + + self.setUpCreds( + custom_file_path, + { + "profiles.zosmf.properties.user": "user", + "profiles.zosmf.properties.password": "password", + }, + ) + + # Test + prof_manager = ProfileManager(appname="zowe") + prof_manager.config_dir = self.custom_dir + props: dict = prof_manager.load(profile_name="zosmf") + + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) + def test_profile_loading_with_invalid_schema(self, get_pass_func): + """ + Test Validation, no error should be raised for valid schema + """ + # Setup - copy profile to fake filesystem created by pyfakefs + with self.assertRaises(ValidationError): + custom_file_path = os.path.join(self.custom_dir, "invalid.zowe.config.json") + shutil.copy(self.original_invalid_file_path, custom_file_path) + shutil.copy(self.original_invalid_schema_file_path, self.custom_dir) + os.chdir(self.custom_dir) + + self.setUpCreds( + custom_file_path, + { + "profiles.zosmf.properties.user": "user", + "profiles.zosmf.properties.password": "password", + }, + ) + + # Test + prof_manager = ProfileManager(appname="invalid.zowe") + prof_manager.config_dir = self.custom_dir + props: dict = prof_manager.load(profile_name="zosmf", validate_schema=True) + + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) + def test_profile_loading_with_invalid_schema_internet_URI(self, get_pass_func): + """ + Test Validation, no error should be raised for valid schema + """ + # Setup - copy profile to fake filesystem created by pyfakefs + with self.assertRaises(SchemaError): + custom_file_path = os.path.join(self.custom_dir, "invalidUri.zowe.config.json") + shutil.copy(self.original_invalidUri_file_path, custom_file_path) + shutil.copy(self.original_invalidUri_schema_file_path, self.custom_dir) + os.chdir(self.custom_dir) + + self.setUpCreds( + custom_file_path, + { + "profiles.zosmf.properties.user": "user", + "profiles.zosmf.properties.password": "password", + }, + ) + + # Test + prof_manager = ProfileManager(appname="invalidUri.zowe") + prof_manager.config_dir = self.custom_dir + props: dict = prof_manager.load(profile_name="zosmf", validate_schema=True) + + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) def test_profile_loading_with_env_variables(self, get_pass_func): """ Test loading of correct file given a filename and directory, @@ -399,10 +655,13 @@ def test_profile_loading_with_env_variables(self, get_pass_func): shutil.copy(self.original_schema_file_path, self.custom_dir) os.chdir(self.custom_dir) - self.setUpCreds(custom_file_path, { - "profiles.zosmf.properties.user": "user", - "profiles.zosmf.properties.password": "password", - }) + self.setUpCreds( + custom_file_path, + { + "profiles.zosmf.properties.user": "user", + "profiles.zosmf.properties.password": "password", + }, + ) # Test prof_manager = ProfileManager(appname="zowe") @@ -416,6 +675,222 @@ def test_profile_loading_with_env_variables(self, get_pass_func): } self.assertEqual(props, expected_props) + def test_get_highest_priority_layer(self): + """ + Test that get_highest_priority_layer returns the highest priority layer with a valid profile data dictionary. + """ + # Set up mock profiles in the layers + project_user_config = mock.MagicMock(spec=ConfigFile) + project_user_config.find_profile.return_value = mock.MagicMock() + project_user_config.find_profile.return_value.data = {"profiles": "zosmf"} + + # Set up the ProfileManager + profile_manager = ProfileManager() + profile_manager.project_user_config = project_user_config + project_user_config.get_profile_name_from_path.return_value = "zosmf" + # Call the function being tested + result_layer = profile_manager.get_highest_priority_layer("zosmf") + + # Assert the results + self.assertEqual(result_layer, project_user_config) + + @mock.patch("zowe.core_for_zowe_sdk.ProfileManager.get_highest_priority_layer") + def test_profile_manager_set_property(self, get_highest_priority_layer_mock): + """ + Test that set_property calls the set_property method of the highest priority layer. + """ + json_path = "profiles.zosmf.properties.user" + value = "samadpls" + secure = True + + # Set up mock for the highest priority layer + highest_priority_layer_mock = mock.MagicMock(spec=ConfigFile) + get_highest_priority_layer_mock.return_value = highest_priority_layer_mock + + profile_manager = ProfileManager() + + # Mock the behavior of _set_property method in highest_priority_layer + highest_priority_layer_mock.set_property.return_value = None + + # Call the method being tested + profile_manager.set_property(json_path, value, secure) + + # Assert the method calls + highest_priority_layer_mock.set_property.assert_called_with(json_path, value, secure=secure) + + @mock.patch("zowe.core_for_zowe_sdk.ConfigFile.save") + @mock.patch("zowe.core_for_zowe_sdk.CredentialManager.save_secure_props") + def test_profile_manager_save(self, mock_save_secure_props, mock_save): + """ + Test that save calls the save method of all layers. + """ + profile_manager = ProfileManager() + profile_manager.save() + expected_calls = [mock.call(False) for _ in range(4)] + mock_save.assert_has_calls(expected_calls) + mock_save_secure_props.assert_called_once() + + @mock.patch("zowe.core_for_zowe_sdk.ProfileManager.get_highest_priority_layer") + def test_profile_manager_set_profile(self, get_highest_priority_layer_mock): + """ + Test that set_profile calls the set_profile method of the highest priority layer. + """ + profile_path = "profiles.zosmf" + profile_data = {"properties": {"user": "admin", "password": "password1"}} + + highest_priority_layer_mock = mock.MagicMock(spec=ConfigFile) + get_highest_priority_layer_mock.return_value = highest_priority_layer_mock + profile_manager = ProfileManager() + + highest_priority_layer_mock.set_profile.return_value = None + profile_manager.set_profile(profile_path, profile_data) + + highest_priority_layer_mock.set_profile.assert_called_with(profile_path, profile_data) + + @mock.patch("zowe.core_for_zowe_sdk.ConfigFile.get_profile_path_from_name") + def test_set_or_create_nested_profile(self, mock_get_profile_path): + """ + Test that __set_or_create_nested_profile calls the get_profile_path_from_name method and sets the profile data. + """ + mock_get_profile_path.return_value = "profiles.zosmf" + config_file = ConfigFile(name="zowe_abcd", type="User Config", profiles={}) + profile_data = {"properties": {"user": "samadpls", "password": "password1"}} + config_file._ConfigFile__set_or_create_nested_profile("zosmf", profile_data) + expected_profiles = {"zosmf": {"properties": {"user": "samadpls", "password": "password1"}}} + self.assertEqual(config_file.profiles, expected_profiles) + + @mock.patch("zowe.core_for_zowe_sdk.ConfigFile.find_profile") + def test_is_secure(self, mock_find_profile): + """ + Test that __is_secure returns True if the property is secure and False otherwise. + """ + config_file = ConfigFile(name="zowe_abcd", type="User Config", profiles={}) + mock_find_profile.return_value = {"properties": {"user": "samadpls"}, "secure": ["password"]} + is_secure_secure = config_file._ConfigFile__is_secure("zosmf", "password") + is_secure_non_secure = config_file._ConfigFile__is_secure("zosmf", "user") + + self.assertTrue(is_secure_secure) + self.assertFalse(is_secure_non_secure) + + @mock.patch("zowe.core_for_zowe_sdk.ConfigFile.get_profile_name_from_path") + @mock.patch("zowe.core_for_zowe_sdk.ConfigFile.find_profile") + @mock.patch("zowe.core_for_zowe_sdk.ConfigFile._ConfigFile__is_secure") + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) + def test_config_file_set_property(self, get_pass_func, mock_is_secure, mock_find_profile, mock_get_profile_name): + """ + Test that set_property calls the __is_secure, find_profile and get_profile_name_from_path methods. + """ + cwd_up_dir_path = os.path.dirname(CWD) + cwd_up_file_path = os.path.join(cwd_up_dir_path, "zowe.config.json") + os.chdir(CWD) + shutil.copy(self.original_file_path, cwd_up_file_path) + self.setUpCreds(cwd_up_file_path, {"profiles.zosmf.properties.user": "admin"}) + config_file = ConfigFile(name="zowe_abcd", type="User Config", profiles={}) + mock_is_secure.return_value = False + mock_find_profile.return_value = {"properties": {"port": 1443}, "secure": []} + mock_get_profile_name.return_value = "zosmf" + + config_file.set_property("profiles.zosmf.properties.user", "admin", secure=True) + + mock_is_secure.assert_called_with("zosmf", "user") + mock_find_profile.assert_called_with("zosmf", config_file.profiles) + mock_get_profile_name.assert_called_with("profiles.zosmf.properties.user") + self.assertEqual( + config_file.profiles, {"zosmf": {"properties": {"port": 1443, "user": "admin"}, "secure": ["user"]}} + ) + + def test_get_profile_name_from_path(self): + """ + Test that get_profile_name_from_path returns the profile name from the path. + """ + config_file = ConfigFile(name="zowe_abcd", type="User Config") + profile_name = config_file.get_profile_name_from_path("profiles.lpar1.profiles.zosmf.properties.user") + self.assertEqual(profile_name, "lpar1.zosmf") + + def test_get_profile_path_from_name(self): + """ + Test that get_profile_path_from_name returns the profile path from the name. + """ + config_file = ConfigFile(name="zowe_abcd", type="User Config") + profile_path_1 = config_file.get_profile_path_from_name("lpar1.zosmf") + self.assertEqual(profile_path_1, "profiles.lpar1.profiles.zosmf") + + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) + def test_config_file_set_profile_and_save(self, get_pass_func): + """ + Test the set_profile method. + """ + cwd_up_dir_path = os.path.dirname(CWD) + cwd_up_file_path = os.path.join(cwd_up_dir_path, "zowe.config.json") + os.chdir(CWD) + shutil.copy(self.original_file_path, cwd_up_file_path) + self.setUpCreds( + cwd_up_file_path, {"profiles.zosmf.properties.user": "abc", "profiles.zosmf.properties.password": "def"} + ) + initial_profiles = {"zosmf": {"properties": {"port": 1443}, "secure": []}} + config_file = ConfigFile("User Config", "zowe.config.json", cwd_up_dir_path, profiles=initial_profiles) + profile_data = { + "type": "zosmf", + "properties": {"port": 443, "user": "abc", "password": "def"}, + "secure": ["user", "password"], + } + + with mock.patch("zowe.core_for_zowe_sdk.ConfigFile.get_profile_name_from_path", return_value="zosmf"): + with mock.patch("zowe.core_for_zowe_sdk.ConfigFile.find_profile", return_value=initial_profiles["zosmf"]): + config_file.set_profile("profiles.zosmf", profile_data) + + expected_profiles = {"zosmf": profile_data} + self.assertEqual(config_file.profiles, expected_profiles) + + config_file.jsonc = {"profiles": expected_profiles} + with mock.patch("builtins.open", mock.mock_open()): + config_file.save(False) + + expected_secure_props = { + cwd_up_file_path: {"profiles.zosmf.properties.user": "abc", "profiles.zosmf.properties.password": "def"} + } + expected_profiles = { + "zosmf": { + "type": "zosmf", + "properties": { + "port": 443, + }, + "secure": ["user", "password"], + } + } + self.assertEqual(CredentialManager.secure_props, expected_secure_props) + self.assertEqual(config_file.jsonc["profiles"], expected_profiles) + + @mock.patch("zowe.core_for_zowe_sdk.CredentialManager.save_secure_props") + def test_config_file_save(self, mock_save_secure_props): + """ + Test saving a config file with secure properties. + """ + cwd_up_dir_path = os.path.dirname(CWD) + cwd_up_file_path = os.path.join(cwd_up_dir_path, "zowe.config.json") + os.chdir(CWD) + shutil.copy(self.original_file_path, cwd_up_file_path) + profile_data = { + "lpar1": { + "profiles": {"zosmf": {"properties": {"port": 1443, "password": "secret"}, "secure": ["password"]}}, + "properties": {"host": "example.com", "user": "admin"}, + "secure": ["user"], + } + } + with mock.patch("builtins.open", mock.mock_open()) as mock_file: + config_file = ConfigFile("User Config", "zowe.config.json", cwd_up_dir_path, profiles=profile_data) + config_file.jsonc = {"profiles": profile_data} + config_file.save() + + mock_save_secure_props.assert_called_once() + mock_file.assert_called_once_with(cwd_up_file_path, "w") + mock_file.return_value.write.assert_called() + self.assertIn("user", profile_data["lpar1"]["properties"]) + self.assertNotIn("user", config_file.jsonc["profiles"]["lpar1"]["properties"]) + self.assertEqual( + ["port"], list(config_file.jsonc["profiles"]["lpar1"]["profiles"]["zosmf"]["properties"].keys()) + ) + class TestValidateConfigJsonClass(unittest.TestCase): """Testing the validate_config_json function""" @@ -429,7 +904,7 @@ def test_validate_config_json_valid(self): schema_json = commentjson.load(open(path_to_schema)) expected = validate(config_json, schema_json) - result = validate_config_json(path_to_config, path_to_schema) + result = validate_config_json(path_to_config, path_to_schema, cwd=FIXTURES_PATH) self.assertEqual(result, expected) @@ -445,6 +920,6 @@ def test_validate_config_json_invalid(self): validate(invalid_config_json, invalid_schema_json) with self.assertRaises(ValidationError) as actual_info: - validate_config_json(path_to_invalid_config, path_to_invalid_schema) + validate_config_json(path_to_invalid_config, path_to_invalid_schema, cwd=FIXTURES_PATH) self.assertEqual(str(actual_info.exception), str(expected_info.exception))