summaryrefslogtreecommitdiffstats
path: root/ansible_collections/purestorage/fusion
diff options
context:
space:
mode:
Diffstat (limited to 'ansible_collections/purestorage/fusion')
-rw-r--r--ansible_collections/purestorage/fusion/.github/CONTRIBUTING.md19
-rw-r--r--ansible_collections/purestorage/fusion/.github/ISSUE_TEMPLATE/bug_report.md38
-rw-r--r--ansible_collections/purestorage/fusion/.github/ISSUE_TEMPLATE/feature_request.md20
-rw-r--r--ansible_collections/purestorage/fusion/.github/pull_request_template.md25
-rw-r--r--ansible_collections/purestorage/fusion/.github/workflows/ansible-lint.yaml10
-rw-r--r--ansible_collections/purestorage/fusion/.github/workflows/black.yaml11
-rw-r--r--ansible_collections/purestorage/fusion/.github/workflows/create-release.yaml117
-rw-r--r--ansible_collections/purestorage/fusion/.github/workflows/main.yml66
-rw-r--r--ansible_collections/purestorage/fusion/.github/workflows/stale.yml19
-rw-r--r--ansible_collections/purestorage/fusion/.gitignore12
-rw-r--r--ansible_collections/purestorage/fusion/.yamllint7
-rw-r--r--ansible_collections/purestorage/fusion/CHANGELOG.rst226
-rw-r--r--ansible_collections/purestorage/fusion/COPYING.GPLv3674
-rw-r--r--ansible_collections/purestorage/fusion/FILES.json1209
-rw-r--r--ansible_collections/purestorage/fusion/MANIFEST.json34
-rw-r--r--ansible_collections/purestorage/fusion/README.md98
-rw-r--r--ansible_collections/purestorage/fusion/changelogs/.plugin-cache.yaml114
-rw-r--r--ansible_collections/purestorage/fusion/changelogs/changelog.yaml345
-rw-r--r--ansible_collections/purestorage/fusion/changelogs/config.yaml32
-rw-r--r--ansible_collections/purestorage/fusion/meta/runtime.yml12
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/create_array.yml17
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/create_availability_zone.yml13
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/create_tenant_space.yml12
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_all.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_api_clients.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_arrays.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_availability_zones.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_hardware_types.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_hosts.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_interfaces.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_network_interface_groups.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_placement_groups.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_protection_policies.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_roles.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_snapshots.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_storage_classes.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_storage_endpoints.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_storage_services.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_tenant_spaces.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_tenants.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_users.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/list_volumes.yml14
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/make_tenant_admin.yml12
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/remove_array.yml17
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/setup_infrastructure.yml64
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/setup_protection_policies.yml13
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/setup_storage_service_class.yml24
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/simple/setup_workloads.yml71
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/tasks/.keep0
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/templates/.keep0
-rw-r--r--ansible_collections/purestorage/fusion/playbooks/vars/.keep0
-rw-r--r--ansible_collections/purestorage/fusion/plugins/doc_fragments/purestorage.py56
-rw-r--r--ansible_collections/purestorage/fusion/plugins/inventory/__init__.py0
-rw-r--r--ansible_collections/purestorage/fusion/plugins/module_utils/errors.py291
-rw-r--r--ansible_collections/purestorage/fusion/plugins/module_utils/fusion.py183
-rw-r--r--ansible_collections/purestorage/fusion/plugins/module_utils/getters.py99
-rw-r--r--ansible_collections/purestorage/fusion/plugins/module_utils/networking.py76
-rw-r--r--ansible_collections/purestorage/fusion/plugins/module_utils/operations.py42
-rw-r--r--ansible_collections/purestorage/fusion/plugins/module_utils/parsing.py75
-rw-r--r--ansible_collections/purestorage/fusion/plugins/module_utils/prerequisites.py162
-rw-r--r--ansible_collections/purestorage/fusion/plugins/module_utils/startup.py26
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_api_client.py139
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_array.py265
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_az.py162
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_hap.py312
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_hw.py88
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_info.py1130
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_ni.py244
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_nig.py274
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_pg.py278
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_pp.py187
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_ra.py281
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_region.py180
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_sc.py255
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_se.py507
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_ss.py208
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_tenant.py169
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_tn.py122
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_ts.py187
-rw-r--r--ansible_collections/purestorage/fusion/plugins/modules/fusion_volume.py450
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/README.md35
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/__init__.py0
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_api_client.py361
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_array.py1331
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_az.py717
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_hap.py889
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_hw.py115
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_info.py2383
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_nig.py1239
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_pg.py1595
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_pp.py528
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_ra.py813
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_region.py798
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_sc.py1240
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_se.py1039
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_ss.py930
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_tenant.py803
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_ts.py922
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/test_fusion_volume.py715
-rw-r--r--ansible_collections/purestorage/fusion/tests/functional/utils.py116
-rw-r--r--ansible_collections/purestorage/fusion/tests/helpers.py29
-rw-r--r--ansible_collections/purestorage/fusion/tests/integration/README.md10
-rw-r--r--ansible_collections/purestorage/fusion/tests/integration/integration_config.template6
-rw-r--r--ansible_collections/purestorage/fusion/tests/integration/targets/fusion_az/tasks/main.yml43
-rw-r--r--ansible_collections/purestorage/fusion/tests/integration/targets/fusion_hap/tasks/main.yml42
-rw-r--r--ansible_collections/purestorage/fusion/tests/integration/targets/fusion_ni/tasks/main.yml37
-rw-r--r--ansible_collections/purestorage/fusion/tests/integration/targets/fusion_nig/tasks/main.yml48
-rw-r--r--ansible_collections/purestorage/fusion/tests/integration/targets/fusion_pg/tasks/main.yml95
-rw-r--r--ansible_collections/purestorage/fusion/tests/integration/targets/fusion_pp/tasks/main.yml43
-rw-r--r--ansible_collections/purestorage/fusion/tests/integration/targets/fusion_region/tasks/main.yml53
-rw-r--r--ansible_collections/purestorage/fusion/tests/integration/targets/fusion_sc/tasks/main.yml94
-rw-r--r--ansible_collections/purestorage/fusion/tests/integration/targets/fusion_se/tasks/main.yml100
-rw-r--r--ansible_collections/purestorage/fusion/tests/integration/targets/fusion_ss/tasks/main.yml77
-rw-r--r--ansible_collections/purestorage/fusion/tests/integration/targets/fusion_tenant/tasks/main.yml41
-rw-r--r--ansible_collections/purestorage/fusion/tests/integration/targets/fusion_ts/tasks/main.yml62
-rw-r--r--ansible_collections/purestorage/fusion/tests/unit/README.md15
-rw-r--r--ansible_collections/purestorage/fusion/tests/unit/mocks/__init__.py0
-rw-r--r--ansible_collections/purestorage/fusion/tests/unit/mocks/module_mock.py38
-rw-r--r--ansible_collections/purestorage/fusion/tests/unit/mocks/operation_mock.py24
-rw-r--r--ansible_collections/purestorage/fusion/tests/unit/module_utils/__init__.py0
-rw-r--r--ansible_collections/purestorage/fusion/tests/unit/module_utils/test_networking.py58
-rw-r--r--ansible_collections/purestorage/fusion/tests/unit/module_utils/test_operations.py230
-rw-r--r--ansible_collections/purestorage/fusion/tests/unit/module_utils/test_parsing.py138
-rw-r--r--ansible_collections/purestorage/fusion/tests/unit/module_utils/test_prerequisites.py116
-rw-r--r--ansible_collections/purestorage/fusion/tests/unit/modules/__init__.py0
-rw-r--r--ansible_collections/purestorage/fusion/tests/unit/modules/test_fusion_az.py446
126 files changed, 28459 insertions, 0 deletions
diff --git a/ansible_collections/purestorage/fusion/.github/CONTRIBUTING.md b/ansible_collections/purestorage/fusion/.github/CONTRIBUTING.md
new file mode 100644
index 000000000..a9054d18c
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/.github/CONTRIBUTING.md
@@ -0,0 +1,19 @@
+# WELCOME TO PURE STORAGE FUSION ANSIBLE COLLECTION GITHUB
+
+Hi! Nice to see you here!
+
+## QUESTIONS ?
+
+The GitHub issue tracker is not the best place for questions for various reasons, but the [mailing list](mailto:pure-ansible-team@purestorage.com) is a very helpful places for those things.
+
+## CONTRIBUTING ?
+
+By contributing you agree that these contributions are your own (or approved by your employer) and you grant a full, complete, irrevocable copyright license to all users and developers of the project, present and future, pursuant to the license of the project.
+
+## BUG TO REPORT ?
+
+You can report bugs or make enhancement requests at the [Ansible GitHub issue page](http://github.com/Pure-Storage-Ansible/Fusion-Collection/issues/new/choose) by filling out the issue template that will be presented.
+
+Also please make sure you are testing on the latest released version of Ansible or the development branch; see the [Installation Guide](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html) for details.
+
+Thanks!
diff --git a/ansible_collections/purestorage/fusion/.github/ISSUE_TEMPLATE/bug_report.md b/ansible_collections/purestorage/fusion/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..dd84ea782
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/ansible_collections/purestorage/fusion/.github/ISSUE_TEMPLATE/feature_request.md b/ansible_collections/purestorage/fusion/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..bbcbbe7d6
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/ansible_collections/purestorage/fusion/.github/pull_request_template.md b/ansible_collections/purestorage/fusion/.github/pull_request_template.md
new file mode 100644
index 000000000..27079cb18
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/.github/pull_request_template.md
@@ -0,0 +1,25 @@
+##### SUMMARY
+<!--- Describe the change below, including rationale and design decisions -->
+
+<!--- HINT: Include "Fixes #nnn" if you are fixing an existing issue -->
+
+##### ISSUE TYPE
+<!--- Pick one below and delete the rest -->
+- Bugfix Pull Request
+- Docs Pull Request
+- Feature Pull Request
+- New Module Pull Request
+- New Role Pull Request
+
+##### COMPONENT NAME
+<!--- Write the short name of the module, plugin, task or feature below -->
+
+##### ADDITIONAL INFORMATION
+<!--- Include additional information to help people understand the change here -->
+<!--- A step-by-step reproduction of the problem is helpful if there is no related issue -->
+- All new PRs must include a changelog fragment
+- Details of naming convention and format can be found [here](https://docs.ansible.com/ansible/latest/community/development_process.html#creating-a-changelog-fragment)
+<!--- Paste verbatim command output below, e.g. before and after your change -->
+```paste below
+
+```
diff --git a/ansible_collections/purestorage/fusion/.github/workflows/ansible-lint.yaml b/ansible_collections/purestorage/fusion/.github/workflows/ansible-lint.yaml
new file mode 100644
index 000000000..0b2102184
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/.github/workflows/ansible-lint.yaml
@@ -0,0 +1,10 @@
+name: Ansible Lint # feel free to pick your own name
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Run ansible-lint
+ uses: ansible-community/ansible-lint-action@main
diff --git a/ansible_collections/purestorage/fusion/.github/workflows/black.yaml b/ansible_collections/purestorage/fusion/.github/workflows/black.yaml
new file mode 100644
index 000000000..68061652a
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/.github/workflows/black.yaml
@@ -0,0 +1,11 @@
+name: Black
+
+on: [push, pull_request]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-python@v3
+ - uses: psf/black@stable
diff --git a/ansible_collections/purestorage/fusion/.github/workflows/create-release.yaml b/ansible_collections/purestorage/fusion/.github/workflows/create-release.yaml
new file mode 100644
index 000000000..25725c15d
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/.github/workflows/create-release.yaml
@@ -0,0 +1,117 @@
+name: Release Collection
+
+on: workflow_dispatch
+jobs:
+ create_github_release:
+ runs-on: ubuntu-latest
+ environment: fusion-env
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Get version from galaxy.yml
+ run: |
+ RELEASE_VERSION=$(grep 'version:' galaxy.yml | awk '{print $2}')
+ echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV
+
+ - name: Check if tag exists
+ env:
+ GITHUB_TOKEN: ${{ github.TOKEN }}
+ run: |
+ trap "exit 0" EXIT
+ response=$(gh api -H "Accept: application/vnd.github+json" /repos/${{ github.repository }}/git/ref/tags/$RELEASE_VERSION)
+ if [[ "$response" == *"$RELEASE_VERSION"* ]]; then
+ trap "exit 1" EXIT
+ echo "Error: Tag $RELEASE_VERSION already exists"
+ exit 1
+ fi
+
+ - name: Extract changelog
+ run: |
+ awk -v version="$RELEASE_VERSION" '
+ BEGIN { targetVersionFound = 0; previousVersionFound = 0 }
+ {
+ if (match($0, "^v"version) > 0) {
+ targetVersionFound = 1;
+ } else if (targetVersionFound && match($0, "^v") > 0) {
+ previousVersionFound = 1;
+ }
+
+ if (targetVersionFound && !previousVersionFound) {
+ print $0;
+ }
+ }
+ ' CHANGELOG.rst > changelog
+
+ - name: Create a release
+ env:
+ GITHUB_TOKEN: ${{ github.TOKEN }}
+ run: gh release create "${{ env.RELEASE_VERSION }}" --title "Fusion Collection v${{ env.RELEASE_VERSION }}" --notes-file changelog
+
+ publish_collection_to_galaxy:
+ runs-on: ubuntu-latest
+ environment: fusion-env
+ needs: create_github_release
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Get version from galaxy.yml
+ run: |
+ RELEASE_VERSION=$(grep 'version:' galaxy.yml | awk '{print $2}')
+ echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV
+
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: "3.x"
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install ansible ansible-lint yamllint
+
+ - name: Build Ansible Collection
+ run: ansible-galaxy collection build
+
+ - name: Release to Ansible Galaxy
+ run: ansible-galaxy collection publish --api-key=${{ secrets.ANSIBLE_GALAXY_API_KEY }} ./purestorage-fusion-${{ env.RELEASE_VERSION }}.tar.gz
+
+ publish_collection_to_automation_hub:
+ runs-on: ubuntu-latest
+ environment: fusion-env
+ needs: create_github_release
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Get version from galaxy.yml
+ run: |
+ RELEASE_VERSION=$(grep 'version:' galaxy.yml | awk '{print $2}')
+ echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV
+
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: "3.x"
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install ansible ansible-lint yamllint
+
+ - name: Build Ansible Collection
+ run: ansible-galaxy collection build
+ - name: Create config for Automation Hub
+ run: |
+ cat << EOF > ansible.cfg
+ [galaxy]
+ server_list = rh_automation_hub
+ [galaxy_server.rh_automation_hub]
+ url=${{ secrets.RH_AUTOMATION_HUB_URL }}
+ auth_url=${{ secrets.RH_SSO_URL }}
+ token=${{ secrets.RH_AUTOMATION_HUB_TOKEN }}
+ EOF
+
+ - name: Release to Automation Hub
+ run: ansible-galaxy collection publish ./purestorage-fusion-${{ env.RELEASE_VERSION }}.tar.gz
diff --git a/ansible_collections/purestorage/fusion/.github/workflows/main.yml b/ansible_collections/purestorage/fusion/.github/workflows/main.yml
new file mode 100644
index 000000000..da0a69969
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/.github/workflows/main.yml
@@ -0,0 +1,66 @@
+name: Pure Storage Ansible CI
+
+on:
+ pull_request:
+ push:
+ schedule:
+ - cron: '25 10 * * *'
+
+jobs:
+ build:
+ name: Build purefusion on Ansible ${{ matrix.ansible }} (Python ${{ matrix.python-version }})
+ runs-on: ubuntu-20.04
+ strategy:
+ matrix:
+ ansible:
+ - stable-2.11
+ - stable-2.12
+ - stable-2.13
+ - stable-2.14
+ - stable-2.15
+ - devel
+ python-version:
+ - 3.8
+ - 3.9
+ - "3.10"
+ - "3.11"
+ exclude:
+ - python-version: "3.11"
+ ansible: stable-2.11
+ - python-version: "3.11"
+ ansible: stable-2.12
+ - python-version: "3.11"
+ ansible: stable-2.13
+ - python-version: "3.10"
+ ansible: stable-2.11
+ - python-version: 3.8
+ ansible: stable-2.14
+ - python-version: 3.8
+ ansible: stable-2.15
+ - python-version: 3.8
+ ansible: devel
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v3
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v3
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install dependencies
+ run: |
+ python${{ matrix.python }} -m pip install --upgrade pip
+ python${{ matrix.python }} -m pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check
+ python${{ matrix.python }} -m pip install purefusion pytest
+
+ - name: Run sanity tests
+ run: |
+ pwd
+ mkdir -p ansible_collections/purestorage/fusion
+ rsync -av . ansible_collections/purestorage/fusion --exclude ansible_collection/purestorage/fusion
+ cd ansible_collections/purestorage/fusion
+ ansible-test sanity -v --color --python ${{ matrix.python-version }} --docker
+
+ - name: Run unit tests
+ run: python -m pytest --import-mode=append tests
diff --git a/ansible_collections/purestorage/fusion/.github/workflows/stale.yml b/ansible_collections/purestorage/fusion/.github/workflows/stale.yml
new file mode 100644
index 000000000..7bbc0505b
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/.github/workflows/stale.yml
@@ -0,0 +1,19 @@
+name: Mark stale issues and pull requests
+
+on:
+ schedule:
+ - cron: "0 0 * * *"
+
+jobs:
+ stale:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/stale@v1
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ stale-issue-message: 'Stale issue message'
+ stale-pr-message: 'Stale pull request message'
+ stale-issue-label: 'no-issue-activity'
+ stale-pr-label: 'no-pr-activity'
diff --git a/ansible_collections/purestorage/fusion/.gitignore b/ansible_collections/purestorage/fusion/.gitignore
new file mode 100644
index 000000000..4d7880b52
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/.gitignore
@@ -0,0 +1,12 @@
+*.tar.gz
+.pylintrc
+tests/output/*
+changelogs/.plugin-cache.yaml
+**/__pycache__/
+
+# files produced by Visual Studio Code
+.vscode/
+.history/
+
+# files produces by JetBrains IDEs
+.idea \ No newline at end of file
diff --git a/ansible_collections/purestorage/fusion/.yamllint b/ansible_collections/purestorage/fusion/.yamllint
new file mode 100644
index 000000000..6c19f43f7
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/.yamllint
@@ -0,0 +1,7 @@
+extends: default
+
+rules:
+ document-start: disable
+ indentation: disable
+ line-length:
+ max: 200
diff --git a/ansible_collections/purestorage/fusion/CHANGELOG.rst b/ansible_collections/purestorage/fusion/CHANGELOG.rst
new file mode 100644
index 000000000..b4d9bd6ae
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/CHANGELOG.rst
@@ -0,0 +1,226 @@
+================================
+Purestorage.Fusion Release Notes
+================================
+
+.. contents:: Topics
+
+
+v1.5.0
+======
+
+Minor Changes
+-------------
+
+- FUSION_API_HOST && FUSION_HOST - changed logic, now this variables require host name without path
+- Fusion authentication - add 'access_token' module's parameter and 'FUSION_ACCESS_TOKEN' environment variable, as an alternative way of the authentication.
+- fusion - added private key password, which is used to decrypt private key files
+- fusion_info - `array` is None if missing in `volume`
+- fusion_info - `hardware_types` is None if missing in `storage_service`
+- fusion_info - `network_interface_groups` is None if missing in `iscsi_interfaces` in `storage_endpoint`
+- fusion_info - introduce 'availability_zones' subset option
+- fusion_info - introduce 'host_access_policies' subset option
+- fusion_info - introduce 'network_interfaces' subset option
+- fusion_info - introduce 'regions' subset option
+- fusion_info - rename 'appliances' in default dict to 'arrays' for consistency
+- fusion_info - rename 'hosts' dict to 'host_access_policies' for consistency
+- fusion_info - rename 'interfaces' dict to 'network_interfaces' for consistency
+- fusion_info - rename 'placements_groups' in default dict to 'placement_groups' for consistency
+- fusion_info - rename 'zones' dict to 'availability_zones' for consistency
+- fusion_info - rename hardware to hardware_types in response for consistency
+- fusion_info - rename storageclass to storage_classes in response for consistency
+- fusion_pp - duration parsing improved. Supports combination of time units (E.g 5H5M)
+- fusion_ra - added `api_client_key` argument, which can be used instead of `user` and `principal` argument
+- fusion_ra - added `principal` argument, which is an ID of either API client or User and can be used instead of `user` argument
+- fusion_se - add support for CBS Storage Endpoint
+
+Deprecated Features
+-------------------
+
+- fusion_api_client - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_array - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_az - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_hap - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_hap - parameters `nqn`, `wwns`, `host_password`, `host_user`, `target_password`and `target_user` were deprecated
+- fusion_hw - FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_info - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_info - 'hosts' subset is deprecated in favor of 'host_access_policies' and will be removed in the version 2.0.0
+- fusion_info - 'interfaces' subset is deprecated in favor of 'network_interfaces' and will be removed in the version 2.0.0
+- fusion_info - 'zones' subset is deprecated in favor of 'availability_zones' and will be removed in the version 2.0.0
+- fusion_ni - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_nig - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_pg - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_pp - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_ra - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_region - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_sc - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_se - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_se - `endpoint_type` parameter is now deprecated and will be removed in version 2.0.0
+- fusion_ss - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_tenant - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_tn - FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_ts - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+- fusion_volume - 'app_id' and 'key_file' parameters are deprecated in favor of 'issuer_id' and 'private_key_file' parameters and will be removed in the version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+
+Bugfixes
+--------
+
+- fusion_info - fix runtime errors caused when listing `interfaces`, `arrays` and `snapshots` dicts
+- fusion_pg - freshly created placement group is now moved to correct array
+- fusion_pp - 'local_rpo' changed to accept same input as 'local_retention'
+- fusion_pp - updated retention description
+- fusion_ra - 'name' deprecated and aliased to 'role'
+
+v1.4.2
+======
+
+Minor Changes
+-------------
+
+- added Python package dependency checks in prerequisites.py
+- fusion_hap - added missing 'windows' personality type
+
+Bugfixes
+--------
+
+- fusion_array - correct required parameters
+- fusion_hap - display name has now default value set to the value of name
+- fusion_hw - correct required parameters
+- fusion_pg - correct required parameters
+- fusion_pp - correct required parameters
+- fusion_sc - correct required parameters
+- fusion_ss - allow updating hardware types, correct required parameters
+- fusion_tn - fix attribute error
+- fusion_volume - protection policy can now be unset by using '' as name
+
+v1.4.1
+======
+
+v1.4.0
+======
+
+Major Changes
+-------------
+
+- Patching of resource properties was brought to parity with underlying Python SDK
+- fusion_volume - fixed and reorganized, arguments changed
+
+Minor Changes
+-------------
+
+- errors_py - added opt-in global exception handler which produces simpler and cleaner messages on REST errors
+- removed dependency on Python `netaddr` package
+
+Deprecated Features
+-------------------
+
+- fusion_hw - hardware module is being removed as changing hardware type has never been supported by Pure Storage Fusion
+- fusion_info - nigs subset is deprecated in favor of network_interface_groups and will be removed in the version 1.7.0
+- fusion_info - placements subset is deprecated in favor of placement_groups and will be removed in the version 1.7.0
+- fusion_pg - placement_engine option is deprecated because Fusion API does not longer support this parameter It will be removed in the version 2.0.0
+- fusion_se - parameters 'addresses', 'gateway' and 'network_interface_groups' are deprecated in favor of 'iscsi' and will be removed in version 2.0.0
+- fusion_tn - tenant networks are being replaced by storage endpoints ```fusion_se``` and Network Interface Groups ```fusion_nig```
+
+Bugfixes
+--------
+
+- fusion_api_client - error messages now mostly handled by errors_py
+- fusion_hap - could not delete host access policy without iqn option. Now it needs only name option for deletion
+- fusion_hap - error messages now mostly handled by errors_py
+- fusion_hap - uppercase names were not supported. Now uppercase names are allowed
+- fusion_info - fixes typo in output 'appiiances' -> 'appliances'
+- fusion_info - network_interface_groups subset returned nothing. Now it collects the same information as nigs subset
+- fusion_info - placements subset returned nothing. Now it collects the same information as placement_groups subset
+- fusion_nig - add missing 'availability_zone' format param in error message
+- fusion_nig - error messages now mostly handled by errors_py
+- fusion_pg - create_pg always broke runtime. Now it executes and creates placement group successfully
+- fusion_pg - error messages now mostly handled by errors_py
+- fusion_pp - error messages now mostly handled by errors_py
+- fusion_pp - fix call to parse_minutes where we were missing a required argument
+- fusion_sc - error messages now mostly handled by errors_py
+- fusion_se - add missing 'availability_zone' format param in error message
+- fusion_se - error messages now mostly handled by errors_py
+- fusion_se - fix call in get_nifg where provider_subnet was used instead of network_interface_group_name
+- fusion_ss - error messages now mostly handled by errors_py
+- fusion_tenant - error messages now mostly handled by errors_py
+- fusion_ts - add missing 'tenant' format param in error message
+- fusion_ts - error messages now mostly handled by errors_py
+- fusion_volume - error messages now mostly handled by errors_py
+
+v1.3.0
+======
+
+Bugfixes
+--------
+
+- fusion_pg - Add missing 'region' parameter
+- fusion_tn - Add missing 'region' parameter
+
+v1.2.0
+======
+
+Minor Changes
+-------------
+
+- fusion_info - Added API Client information
+
+Bugfixes
+--------
+
+- fusion_info - Fixed issue with storage endpoint dict formatting
+
+v1.1.1
+======
+
+v1.1.0
+======
+
+Minor Changes
+-------------
+
+- fusion_az - Add delete AZ option
+- fusion_az - Allow any region to be specified instead of limited to a known list
+- fusion_pp - Add delete PP option
+- fusion_sc - Add delete SC option
+- fusion_ss - Add delete SS option
+
+Bugfixes
+--------
+
+- Allow correct use of environmental variables for App ID and private file file
+
+New Modules
+-----------
+
+- purestorage.fusion.fusion_region - Manage Regions in Pure Storage Fusion
+
+v1.0.3
+======
+
+v1.0.2
+======
+
+v1.0.1
+======
+
+v1.0.0
+======
+
+New Modules
+-----------
+
+- purestorage.fusion.fusion_api_client - Manage API clients in Pure Storage Fusion
+- purestorage.fusion.fusion_array - Manage arrays in Pure Storage Fusion
+- purestorage.fusion.fusion_az - Create Availability Zones in Pure Storage Fusion
+- purestorage.fusion.fusion_hap - Manage host access policies in Pure Storage Fusion
+- purestorage.fusion.fusion_hw - Create hardware types in Pure Storage Fusion
+- purestorage.fusion.fusion_info - Collect information from Pure Fusion
+- purestorage.fusion.fusion_nig - Manage Network Interface Groups in Pure Storage Fusion
+- purestorage.fusion.fusion_pg - Manage placement groups in Pure Storage Fusion
+- purestorage.fusion.fusion_pp - Manage protection policies in Pure Storage Fusion
+- purestorage.fusion.fusion_ra - Manage role assignments in Pure Storage Fusion
+- purestorage.fusion.fusion_sc - Manage storage classes in Pure Storage Fusion
+- purestorage.fusion.fusion_ss - Manage storage services in Pure Storage Fusion
+- purestorage.fusion.fusion_tenant - Manage tenants in Pure Storage Fusion
+- purestorage.fusion.fusion_tn - Manage tenant networks in Pure Storage Fusion
+- purestorage.fusion.fusion_ts - Manage tenant spaces in Pure Storage Fusion
+- purestorage.fusion.fusion_volume - Manage volumes in Pure Storage Fusion
diff --git a/ansible_collections/purestorage/fusion/COPYING.GPLv3 b/ansible_collections/purestorage/fusion/COPYING.GPLv3
new file mode 100644
index 000000000..94a9ed024
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/COPYING.GPLv3
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/ansible_collections/purestorage/fusion/FILES.json b/ansible_collections/purestorage/fusion/FILES.json
new file mode 100644
index 000000000..b3f73b7e0
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/FILES.json
@@ -0,0 +1,1209 @@
+{
+ "files": [
+ {
+ "name": ".",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "changelogs",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "changelogs/changelog.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "b568495166ca2ef38576e62cc6f1eb2d1f4caa988b020112e14650d37510dd83",
+ "format": 1
+ },
+ {
+ "name": "changelogs/.plugin-cache.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "492bf617d0924a14708a862efd096e1a032e1a1243f25e2287e44a6e072e2f1a",
+ "format": 1
+ },
+ {
+ "name": "changelogs/config.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "1762f9f7012a7e135eacb3e2d2c35bdcb25570cdfc33da7a190fa36edc604aec",
+ "format": 1
+ },
+ {
+ "name": "tests",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "a20772cc1cfbb1676994a74cc05e703aaa7df0759926614ea6832f7232ffc9f7",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_region",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_region/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_region/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "13360be1e9c683c45a0019ea3a9fd55fe1414964773a69110f0cf6e3cd4ca081",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_pg",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_pg/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_pg/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "66add9c2d9f2c8a02d4cbd18b6a871266af0a364756416991e80a70c8eed76f5",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_ni",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_ni/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_ni/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "a14ec0c5ef2a4ce26fd806a960c7b4d2c55988028b2b406d298929e53547193d",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_tenant",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_tenant/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_tenant/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "d0061bd98f9da02af742c85e7f9f1a1ca52363d3935c9e0fa105db13466922bc",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_hap",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_hap/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_hap/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "97ac38033a460071019a230efc40f3b63085528fecdc72f98b683c3b888ed04a",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_se",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_se/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_se/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "ba77056fd00ca6a52382f4285a2452609cfda8b045cde1042963c1c58944e31d",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_ts",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_ts/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_ts/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "a1462a14b4dc7a7c0b0f76df30c2283338bc1b53838bd29537c00c7d0ea2a855",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_pp",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_pp/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_pp/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0839b0cf5da3154795a7e5a5357168b498b44fea9dd2c1b9729385b50784c68f",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_sc",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_sc/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_sc/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5f100551a2f19101772101bc413be17b08baada7c7b92d4fbefccfa4f3637c04",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_ss",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_ss/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_ss/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5b5be20d4cd09db5c3edbde5a6b723a93bb266834a8c1908525435f9ab813a7d",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_az",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_az/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_az/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "9b594ff2d1199f94997c63b8ba704b0497ea86a8a7215437541358603dcc68aa",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_nig",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_nig/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/fusion_nig/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "ec0586fc68e8ea986f752237f2f75b42a6a36724b71b01dbd401ca3d5fed0c2e",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/integration_config.template",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e55ed10c7dca1c84ffa8dd2e19087f630b8fd50fedbb96a167fb8e6231cbb6d2",
+ "format": 1
+ },
+ {
+ "name": "tests/unit",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/unit/modules",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/unit/modules/__init__.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/modules/test_fusion_az.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "75be72264bf7d95ddc73d72c4763b6e877a05feaab2f6d9b91a55448bb77af51",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/mocks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/unit/mocks/__init__.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/mocks/operation_mock.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "aaa5ad3b4a9bcd10a95947af5f06ec4153512927b56d94f4d442da6007d43c7b",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/mocks/module_mock.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3e72e4d7a51c8fcb35cfb7473f84e33de359a969140ebddc185dfce602966e75",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e74786237c54bd81d7b314e5871bd89bf1329f228087a0ecb17a64024b76396c",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/module_utils",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/unit/module_utils/__init__.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/module_utils/test_prerequisites.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "8ea9eec2b76c9b4ff0c5e2891972bdf12091b59770bfee4462ddb0014afb33a9",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/module_utils/test_networking.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "01fa89c92da97f0e9f66527663bddff99a46e6ef07b5601f5be7ae129956bfa1",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/module_utils/test_parsing.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "58c8e7b81680984e3e606cc56210aa8afb93c020939f1d3d585b5cf7de61c513",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/module_utils/test_operations.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "656c85c8580a65fac1cb42e806513233596126d18a8f93465bc79af50872f3e2",
+ "format": 1
+ },
+ {
+ "name": "tests/functional",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_region.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "8d108a21480c4cb9c9d2810e809ea876173b3d43621f417c0957c77d21f33f76",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_ss.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "676bba88edd7f73957c605356d31f4bd61cd144d354280c373beb3689196d5cd",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/__init__.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_volume.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "8b6a8f18e610fcd4f2aea719e5b51ff58ef6f6b06412afd98309255ccab7f8a4",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_ts.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "dc844fc260f337525396d47da3e012fbb2f1f5188a96c3d1071515bdac879583",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_pg.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "4da9f7a334491933d40fe8d32fbae767393f84d744537a7d03a57d84a1693b38",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_nig.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "f20b2ab1eed1bd182d68a00198537f960e9c7e844cfb8df3c99922e98e2365c1",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_se.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "79d30463a37430a6a697778bb58fe2ced187672ec74ddae6b98f191069931b04",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_az.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "d6b7e24d40c1268b1ce0de3557210fbd89e97441dcd384522263f5982a5922b5",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "05d60add74e73360eefd8679e808c2c5c5c774726a15c29d923dd077099e9537",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_hap.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0a8ffe64ef5a561e2eb102f58b20f62c99c8a79022be63976f6e8c19608178ab",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_hw.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e247eb15edad8ed402ee8e3bd6bb3add1426502f1ecf283882ac97d450e13753",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "d73fe992181a129802dda79cf3efd3a5ff6473dc5c9c6b99b907af629e5c9279",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_sc.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "cf1794f2f91b496adc54131be84e1ea81263ccf603cf648fecd0a52c7a1da467",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_ra.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "718b4be5026d83e2fe3fefe6838637efce66d9880c635a80603844266b3e926c",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_tenant.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "b5c6413737db89d121c98f7798b36bb736b367fb6c2ee1651645c742822f9b66",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/utils.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "d6e339b28c2b3ed78108244bde3950424b3acc81a6a3b9c8cd7b32a53fbd5ba9",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_api_client.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "44f1df7dfe3c53b30ae7c2c2fd2873b651f2306bba67a26310a8c2d86c53f04e",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_array.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "910cd4c0859534f5de3b8cb743c9f549b93f25c0f18399158adff63b933a8110",
+ "format": 1
+ },
+ {
+ "name": "tests/functional/test_fusion_pp.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3578756616fff28885b379a93221c3dfe8d083a9d685bd8b16878b7f9bf045c9",
+ "format": 1
+ },
+ {
+ "name": "tests/helpers.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "74d50a53434e0ca164aa41ea5f272755e9b6ad0dc105d3eec53f62d3e188034c",
+ "format": 1
+ },
+ {
+ "name": "COPYING.GPLv3",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903",
+ "format": 1
+ },
+ {
+ "name": "playbooks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "playbooks/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "playbooks/tasks/.keep",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "format": 1
+ },
+ {
+ "name": "playbooks/templates",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "playbooks/templates/.keep",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/create_availability_zone.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "b16b7c3f57b7e505ebd105b26d5dfef45b0eb51021aef23b1e39e87386fefc28",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/setup_infrastructure.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "d5883177ccab7b449f9c08100d3941c533186457b06b2dcbd6f12ed1f2382240",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_storage_classes.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "2e301ac27d5ba24bbb0e48757349f88fc3839b1fb3e2bf504526b4837a0d67f5",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_interfaces.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "26dcfb2bf6fd1ac96ef63c3de9b6b8a36a6297c078b296bb32a96a102dc93804",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_placement_groups.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "8d6fef0868932ba3a8f17b95e95d7def5c01906d679519d3ad141021e780045a",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_hardware_types.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "4cd99a9b6cec5a88f8d13a3359a8d5f3687465dd63a3d779ba43c27f4fd2b4ab",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_volumes.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "f130c119ae9ce955440ca3b29ed5a31653062d556bd835ce65c2ef9db8e12122",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_snapshots.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "ef36856102859719d6a3c3970685e7c9fde30b9ef071c1f720bb5a17823df004",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/setup_workloads.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "20cdb263572e18b7b6e13dd1c457c4dd8883eed7e332681003a33eb576874fce",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/remove_array.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "062b8643519792fc53b2fe912ca16ebf1138912e6bbedea99a36ad1ee06cc0cd",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_arrays.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "45c636e983761357f5087229ed5fe4d478b51d239b4620fef21cc635d931999a",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_storage_services.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "be8249059cb0948829e01156336c85bf7c22764541e49019b47a42f9801b2d15",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/make_tenant_admin.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "9fcfafc8e9d8f2a1d27e825ba62ff359ec29f3b34ec42e7610720b2fd5b88c94",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_api_clients.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5ff79a24e584c6922e819976cdd49ceb86b8fbc683df3fac5514544e464bff68",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/create_tenant_space.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "b0254c52265517a8d98720efc6e76b163c4a59b575d672a3069c0aef25afd7b4",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_protection_policies.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "f0257e0d3199f931562d43ab7bd00caff26aed9ca7991ae181b02812ce8012e9",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_network_interface_groups.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0ece2378f2b1e2f8085665f90da83f716a52e7a1d56f579c91480da003d2d6cc",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_all.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "cacd3575668cd03ebc1d13fb98f321b3c74eeb80b5f3d5a9b53cc0979d5b424a",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_hosts.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "adfde2160651de7b7697a736b12da24c967981167f82d6b7fd058fa408e5c0ad",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/setup_protection_policies.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3d4558b3e53bdb83cd1ebd98f9d91741811cfe2457905b7b9bb0ceb445d5546e",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_roles.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5fdba34ac2ad048deac5a3f906ae30b0b5b6c7621f6f4a16353ad1f53dd1a1e4",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_tenant_spaces.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "763263dbc9765ac7d270d18f4d5278dd3ce623673a04308c26750a6186d92dc0",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/create_array.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e7ec429c847303f52c63c61aefa8c3714e9ee57a523ce751efa9dd6fec7f4da7",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_availability_zones.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "37271a1ab28648a6604bba7e95ec3a41ae36a7510ebd4d93cc39a493cc9c6048",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_users.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "6a2cef1205c097069e3f5968de1abcf7b468519bf0c9f8d9e0effa6c5839262d",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_tenants.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "f5bb3ab4560bf4b46fec826e1183de35b2832957fde2978693711c21cb1f7e19",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/setup_storage_service_class.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "9ccffa6de2c433cbd99d0a30ccc41b5f282b8b98f33b4eb0f41c683410560dfd",
+ "format": 1
+ },
+ {
+ "name": "playbooks/simple/list_storage_endpoints.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5cb78b90c813503944af88630d9616d9ca9af238c1461cf816cdc9ad64f14e34",
+ "format": 1
+ },
+ {
+ "name": "playbooks/vars",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "playbooks/vars/.keep",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "format": 1
+ },
+ {
+ "name": "plugins",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/modules",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_ts.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "91e740ffbf27ab279cc6fbd07b5a59e92de941a4e88f437040eae89c1b8f1f3b",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_nig.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "59cd2a72c5544ebf83ff6fe239f623ec03b4de84efb7cb08fdf4b4159544bc2c",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_api_client.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5b92104038365e11b958a75a521a843c7b599e950e1d7815ff40a01a519dfff5",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_ni.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5edf635cb12288af965649f3941bac2f1eea781c2e23793ac40988faedd34735",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_ss.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "c278ef52dbd80a2c143b56ace8f31ebcca5ae76426bc7e38bea3e7e66a1a5742",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_pp.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "29b9019589464b7650892f84ebe112d03f38e03e8c804d6ce35401f85e31603f",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_tn.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "4b1f04a80642f68426a58f5146dedd45bda2c820d65c7c3892e3e2acff495f37",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_array.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0edaabce3e589722d0dd299f7f81d66811351e678046068fae179ad3f331fa4e",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_az.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "f0e9ea0a969913323e917d5b7d631c38e33b3e55a3b641cf553c8ea01228f0a5",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_pg.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3c03eb5a59d749a954fe09d4c2a57ec36d30f9bdd51565c8e1e3d3e293d2bbc5",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_volume.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e2b6a4837e1abc3efc2fa88707cfa80f618b800bccdad6bd5a5ac3f60ba77d14",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_tenant.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "03823b7283e0de940ee3e95bf5645595e4330759ad7dd18f0411c319774ec173",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_se.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "355892de73b5d265e1e57e8ff31b3dd0775c04a191ded999131ebbfdbbcd7231",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "de84aa2313d454b404ffc3791805b29cc8cbc49ad6daae83adfefa029ae2aecc",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_ra.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "4a1bd14fe1038fbf09d4196143f4ac262ef7627ee550ea2efbaeceaa3e0a6176",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_sc.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "7302c71a969dbbc3fb636455ee16ef807a3e2c212d307c305b86504b2b42603c",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_region.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "d6cb89588cca8681cfc12651ace18375eba88d2aaaacf2ece2d7652d9821fde9",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_hw.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5a5b7a795057365965db00615af3e349a9aba55f0d5d3e02ccf17ab6b77bf018",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/fusion_hap.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "ffc4d041a552ac1d45ab51868428f9281829d2f345581eef8f379b1692e50a1a",
+ "format": 1
+ },
+ {
+ "name": "plugins/inventory",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/inventory/__init__.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/getters.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5bf8895d303da94229d2492c46c621bd9fc45d046d22f1f804acb4d81d7b00d4",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/prerequisites.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "589f5ad7eed9dfe57263a9d3ec7dd6b179da0406aa2a6706ec056f3ab60af5cd",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/fusion.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "35d1ef53acaac66c4aadb35d8e259e97a465094db322a27191bf9dd9de1068ed",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/errors.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "fa7c577ce38810b137980e87e6e5b87e95fb43e101d02652df7cbb434f630699",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/parsing.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "efe7b474e24d7fa53dc134f7fd1e4062542a22d5ea9f8b16715ab8a353d1d953",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/operations.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "83b10433689009c69ed3a926d2b3cbe70b047fef29fbd432d69f836b48375354",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/startup.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "42f7acffe52f3038f483f0698072de73307fa0664eac245901683a509b241a85",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/networking.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "42ac372c6e7bf8cd23747ac6d9684a4f9b8f1378a4ada923f6d55bc8606e3df8",
+ "format": 1
+ },
+ {
+ "name": "plugins/doc_fragments",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/doc_fragments/purestorage.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e231135e8559211663aea10e5ccc9ffbc0504bf09ac218cc4efefcf883275165",
+ "format": 1
+ },
+ {
+ "name": ".github",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": ".github/pull_request_template.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "565ead1b588caaa10cd6f2ed1bb6c809eb2ad93bf75da3a198690cac778432d6",
+ "format": 1
+ },
+ {
+ "name": ".github/ISSUE_TEMPLATE",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": ".github/ISSUE_TEMPLATE/bug_report.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0c8d64f29fb4536513653bf8c97da30f3340e2041b91c8952db1515d6b23a7b3",
+ "format": 1
+ },
+ {
+ "name": ".github/ISSUE_TEMPLATE/feature_request.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "1f48c52f209a971b8e7eae4120144d28fcf8ee38a7778a7b4d8cf1ab356617d2",
+ "format": 1
+ },
+ {
+ "name": ".github/workflows",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": ".github/workflows/ansible-lint.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "4c85688d98b71e3a6594530a362cd5d2cf83842ceaccd0e0fc76e233777c1cef",
+ "format": 1
+ },
+ {
+ "name": ".github/workflows/stale.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0bdef4889afabcd627fc30711a0809c7468b8c9e64cbcebe1334f794a41e7bd9",
+ "format": 1
+ },
+ {
+ "name": ".github/workflows/black.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "c62a1a4fcc1e00f3e8f295863e304db520124bfd3e9b0c2cccd6d78343b679c5",
+ "format": 1
+ },
+ {
+ "name": ".github/workflows/create-release.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "43ea888cb2b22ddc86ea989f75967accaff0065cc43c39a0043ba6cf2f424378",
+ "format": 1
+ },
+ {
+ "name": ".github/workflows/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0c8c2578e81d44e4a9611c57a59c6fbc7dd947ff149a169ea65f497484d6d4a4",
+ "format": 1
+ },
+ {
+ "name": ".github/CONTRIBUTING.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "ed15d670a6a61e8159059927017e7ba7c8bcc94623884ced7beb39ef445c7b95",
+ "format": 1
+ },
+ {
+ "name": "README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "a753d4c6dc5cdd493fd60f147cf68f644ec6f301b895fc249093914db1cf3ab1",
+ "format": 1
+ },
+ {
+ "name": "meta",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "meta/runtime.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "9f829699b200db8a8282ce6f44d6ae28a2e3377e0e611b0d327db64b0cbba321",
+ "format": 1
+ },
+ {
+ "name": ".gitignore",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3f4f6972c0df5e698a758d74ab5778f3941fca83f509a00f9665b338c220d762",
+ "format": 1
+ },
+ {
+ "name": ".yamllint",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "2970fa4875092f99825ac0da3c82d2413ce973087b9945e68fdfa7b3b1e2012e",
+ "format": 1
+ },
+ {
+ "name": "CHANGELOG.rst",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "28eab01a890a0719cf1908791d9575a4d47014547796bb077f44702dbbc5632a",
+ "format": 1
+ }
+ ],
+ "format": 1
+} \ No newline at end of file
diff --git a/ansible_collections/purestorage/fusion/MANIFEST.json b/ansible_collections/purestorage/fusion/MANIFEST.json
new file mode 100644
index 000000000..4fe3bc8b5
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/MANIFEST.json
@@ -0,0 +1,34 @@
+{
+ "collection_info": {
+ "namespace": "purestorage",
+ "name": "fusion",
+ "version": "1.5.0",
+ "authors": [
+ "Pure Storage Ansible Team <pure-ansible-team@purestorage.com>"
+ ],
+ "readme": "README.md",
+ "tags": [
+ "purestorage",
+ "fusion",
+ "storage"
+ ],
+ "description": "Collection of modules to manage Pure Fusion",
+ "license": [
+ "GPL-3.0-or-later"
+ ],
+ "license_file": null,
+ "dependencies": {},
+ "repository": "https://github.com/Pure-Storage-Ansible/Fusion-Collection",
+ "documentation": "https://github.com/Pure-Storage-Ansible/Fusion-Collection",
+ "homepage": null,
+ "issues": "https://github.com/Pure-Storage-Ansible/Fusion-Collection/issues"
+ },
+ "file_manifest_file": {
+ "name": "FILES.json",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3e406206ea2f67e0a9846219a9d5d2813aef76437e1b05d12d341aded53cfd13",
+ "format": 1
+ },
+ "format": 1
+} \ No newline at end of file
diff --git a/ansible_collections/purestorage/fusion/README.md b/ansible_collections/purestorage/fusion/README.md
new file mode 100644
index 000000000..b2a36de10
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/README.md
@@ -0,0 +1,98 @@
+<a href="https://github.com/Pure-Storage-Ansible/Fusion-Collection/releases/latest"><img src="https://img.shields.io/github/v/tag/Pure-Storage-Ansible/Fusion-Collection?label=release">
+<a href="COPYING.GPLv3"><img src="https://img.shields.io/badge/license-GPL%20v3.0-brightgreen.svg"></a>
+<img src="https://cla-assistant.io/readme/badge/Pure-Storage-Ansible/Fusion-Collection">
+<img src="https://github.com/Pure-Storage-Ansible/Fusion-Collection/workflows/Pure%20Storage%20Ansible%20CI/badge.svg">
+<a href="https://github.com/psf/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
+
+# Pure Storage Fusion Collection
+
+The Pure Storage Fusion collection consists of the latest versions of the Fusion modules.
+
+## Requirements
+
+- ansible-core >= 2.11
+- Python >= 3.8
+- Authorized API Application ID for Pure Storage Pure1 and associated Private Key
+ - Refer to Pure Storage documentation on how to create these.
+- purefusion >= 1.0.4
+- time
+
+## Available Modules
+
+- fusion_api_client: Manage API clients in Pure Storage Fusion
+- fusion_array: Manage arrays in Pure Storage Fusion
+- fusion_az: Create Availability Zones in Pure Storage Fusion
+- fusion_hap: Manage host access policies in Pure Storage Fusion
+- fusion_hw: Create hardware types in Pure Storage Fusion
+- fusion_info: Collect information from Pure Fusion
+- fusion_ni: Manage Network Interfaces in Pure Storage Fusion
+- fusion_nig: Manage Network Interface Groups in Pure Storage Fusion
+- fusion_pg: Manage placement groups in Pure Storage Fusion
+- fusion_pp: Manage protection policies in Pure Storage Fusion
+- fusion_ra: Manage role assignments in Pure Storage Fusion
+- fusion_region: Manage regions in Pure Storage Fusion
+- fusion_sc: Manage storage classes in Pure Storage Fusion
+- fusion_se: Manage storage endpoints in Pure Storage Fusion
+- fusion_ss: Manage storage services in Pure Storage Fusion
+- fusion_tenant: Manage tenants in Pure Storage Fusion
+- fusion_tn: Manage tenant networks in Pure Storage Fusion
+- fusion_ts: Manage tenant spaces in Pure Storage Fusion
+- fusion_volume: Manage volumes in Pure Storage Fusion
+
+## Instructions
+
+Ansible must be installed [Install guide](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html)
+```
+sudo pip install ansible
+```
+
+Python PureFusion SDK must be installed
+```
+sudo pip install purefusion
+```
+
+Install the Pure Storage Fusion collection on your Ansible management host - [Galaxy link](https://galaxy.ansible.com/purestorage/fusion)
+```
+ansible-galaxy collection install purestorage.fusion
+```
+
+## Example Playbook
+```yaml
+- hosts: localhost
+ tasks:
+ - name: Collect information for Pure Storage fleet in Pure1
+ purestorage.fusion.fusion_info:
+ gather_subset: all
+ issuer_id: <Pure1 API Application ID>
+ private_key_file: <private key file name>
+```
+
+You can find more examples in our [example-playbooks](https://github.com/PureStorage-OpenConnect/ansible-playbook-examples/tree/master/fusion) repository.
+
+## Contributing to this collection
+
+Ongoing development efforts and contributions to this collection are tracked as issues in this repository.
+
+We welcome community contributions to this collection. If you find problems, need an enhancement or need a new module, please open an issue or create a PR against the [Pure Storage Fusion Ansible collection repository](https://github.com/Pure-Storage-Ansible/Fusion-Collection/issues).
+
+Code of Conduct
+---------------
+This collection follows the Ansible project's
+[Code of Conduct](https://docs.ansible.com/ansible/devel/community/code_of_conduct.html).
+Please read and familiarize yourself with this document.
+
+Releasing, Versioning and Deprecation
+-------------------------------------
+
+This collection follows [Semantic Versioning](https://semver.org). More details on versioning can be found [in the Ansible docs](https://docs.ansible.com/ansible/latest/dev_guide/developing_collections.html#collection-versions).
+
+New minor and major releases as well as deprecations will follow new releases and deprecations of the Pure Storage Fusion product, its REST API and the corresponding Python SDK, which this project relies on.
+
+## License
+
+[BSD-2-Clause](https://directory.fsf.org/wiki?title=License:FreeBSD)
+[GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.en.html)
+
+## Author
+
+This collection was created in 2022 by [Simon Dodsley](@sdodsley) for, and on behalf of, the [Pure Storage Ansible Team](pure-ansible-team@purestorage.com)
diff --git a/ansible_collections/purestorage/fusion/changelogs/.plugin-cache.yaml b/ansible_collections/purestorage/fusion/changelogs/.plugin-cache.yaml
new file mode 100644
index 000000000..23a38bf01
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/changelogs/.plugin-cache.yaml
@@ -0,0 +1,114 @@
+objects:
+ role: {}
+plugins:
+ become: {}
+ cache: {}
+ callback: {}
+ cliconf: {}
+ connection: {}
+ filter: {}
+ httpapi: {}
+ inventory: {}
+ lookup: {}
+ module:
+ fusion_api_client:
+ description: Manage API clients in Pure Storage Fusion
+ name: fusion_api_client
+ namespace: ''
+ version_added: 1.0.0
+ fusion_array:
+ description: Manage arrays in Pure Storage Fusion
+ name: fusion_array
+ namespace: ''
+ version_added: 1.0.0
+ fusion_az:
+ description: Create Availability Zones in Pure Storage Fusion
+ name: fusion_az
+ namespace: ''
+ version_added: 1.0.0
+ fusion_hap:
+ description: Manage host access policies in Pure Storage Fusion
+ name: fusion_hap
+ namespace: ''
+ version_added: 1.0.0
+ fusion_hw:
+ description: Create hardware types in Pure Storage Fusion
+ name: fusion_hw
+ namespace: ''
+ version_added: 1.0.0
+ fusion_info:
+ description: Collect information from Pure Fusion
+ name: fusion_info
+ namespace: ''
+ version_added: 1.0.0
+ fusion_ni:
+ description: Manage network interfaces in Pure Storage Fusion
+ name: fusion_ni
+ namespace: ''
+ version_added: 1.0.0
+ fusion_nig:
+ description: Manage Network Interface Groups in Pure Storage Fusion
+ name: fusion_nig
+ namespace: ''
+ version_added: 1.0.0
+ fusion_pg:
+ description: Manage placement groups in Pure Storage Fusion
+ name: fusion_pg
+ namespace: ''
+ version_added: 1.0.0
+ fusion_pp:
+ description: Manage protection policies in Pure Storage Fusion
+ name: fusion_pp
+ namespace: ''
+ version_added: 1.0.0
+ fusion_ra:
+ description: Manage role assignments in Pure Storage Fusion
+ name: fusion_ra
+ namespace: ''
+ version_added: 1.0.0
+ fusion_region:
+ description: Manage Regions in Pure Storage Fusion
+ name: fusion_region
+ namespace: ''
+ version_added: 1.1.0
+ fusion_sc:
+ description: Manage storage classes in Pure Storage Fusion
+ name: fusion_sc
+ namespace: ''
+ version_added: 1.0.0
+ fusion_se:
+ description: Manage storage endpoints in Pure Storage Fusion
+ name: fusion_se
+ namespace: ''
+ version_added: 1.0.0
+ fusion_ss:
+ description: Manage storage services in Pure Storage Fusion
+ name: fusion_ss
+ namespace: ''
+ version_added: 1.0.0
+ fusion_tenant:
+ description: Manage tenants in Pure Storage Fusion
+ name: fusion_tenant
+ namespace: ''
+ version_added: 1.0.0
+ fusion_tn:
+ description: Manage tenant networks in Pure Storage Fusion
+ name: fusion_tn
+ namespace: ''
+ version_added: 1.0.0
+ fusion_ts:
+ description: Manage tenant spaces in Pure Storage Fusion
+ name: fusion_ts
+ namespace: ''
+ version_added: 1.0.0
+ fusion_volume:
+ description: Manage volumes in Pure Storage Fusion
+ name: fusion_volume
+ namespace: ''
+ version_added: 1.0.0
+ netconf: {}
+ shell: {}
+ strategy: {}
+ test: {}
+ vars: {}
+version: 1.5.0
diff --git a/ansible_collections/purestorage/fusion/changelogs/changelog.yaml b/ansible_collections/purestorage/fusion/changelogs/changelog.yaml
new file mode 100644
index 000000000..82ef323c8
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/changelogs/changelog.yaml
@@ -0,0 +1,345 @@
+ancestor: null
+releases:
+ 1.0.0:
+ modules:
+ - description: Manage API clients in Pure Storage Fusion
+ name: fusion_api_client
+ namespace: ''
+ - description: Manage arrays in Pure Storage Fusion
+ name: fusion_array
+ namespace: ''
+ - description: Create Availability Zones in Pure Storage Fusion
+ name: fusion_az
+ namespace: ''
+ - description: Manage host access policies in Pure Storage Fusion
+ name: fusion_hap
+ namespace: ''
+ - description: Create hardware types in Pure Storage Fusion
+ name: fusion_hw
+ namespace: ''
+ - description: Collect information from Pure Fusion
+ name: fusion_info
+ namespace: ''
+ - description: Manage Network Interface Groups in Pure Storage Fusion
+ name: fusion_nig
+ namespace: ''
+ - description: Manage placement groups in Pure Storage Fusion
+ name: fusion_pg
+ namespace: ''
+ - description: Manage protection policies in Pure Storage Fusion
+ name: fusion_pp
+ namespace: ''
+ - description: Manage role assignments in Pure Storage Fusion
+ name: fusion_ra
+ namespace: ''
+ - description: Manage storage classes in Pure Storage Fusion
+ name: fusion_sc
+ namespace: ''
+ - description: Manage storage services in Pure Storage Fusion
+ name: fusion_ss
+ namespace: ''
+ - description: Manage tenants in Pure Storage Fusion
+ name: fusion_tenant
+ namespace: ''
+ - description: Manage tenant networks in Pure Storage Fusion
+ name: fusion_tn
+ namespace: ''
+ - description: Manage tenant spaces in Pure Storage Fusion
+ name: fusion_ts
+ namespace: ''
+ - description: Manage volumes in Pure Storage Fusion
+ name: fusion_volume
+ namespace: ''
+ release_date: '2022-05-25'
+ 1.0.1:
+ release_date: '2022-05-27'
+ 1.0.2:
+ release_date: '2022-06-13'
+ 1.0.3:
+ fragments:
+ - 27_review_fixes.yaml
+ release_date: '2022-06-27'
+ 1.1.0:
+ changes:
+ bugfixes:
+ - Allow correct use of environmental variables for App ID and private file file
+ minor_changes:
+ - fusion_az - Add delete AZ option
+ - fusion_az - Allow any region to be specified instead of limited to a known
+ list
+ - fusion_pp - Add delete PP option
+ - fusion_sc - Add delete SC option
+ - fusion_ss - Add delete SS option
+ fragments:
+ - 27_review_fixes.yaml
+ - 28_any_region.yaml
+ - 29_use_env.yaml
+ - 31_add_delete_az.yaml
+ - 32_add_delete_pp.yaml
+ - 33_add_delete_sc.yaml
+ - 34_add_delete_ss.yaml
+ modules:
+ - description: Manage Regions in Pure Storage Fusion
+ name: fusion_region
+ namespace: ''
+ release_date: '2022-09-01'
+ 1.1.1:
+ release_date: '2022-09-23'
+ 1.2.0:
+ changes:
+ bugfixes:
+ - fusion_info - Fixed issue with storage endpoint dict formatting
+ minor_changes:
+ - fusion_info - Added API Client information
+ fragments:
+ - 39_fix_info.yaml
+ - 41_add_api_client.yaml
+ release_date: '2022-12-02'
+ 1.3.0:
+ changes:
+ bugfixes:
+ - fusion_pg - Add missing 'region' parameter
+ - fusion_tn - Add missing 'region' parameter
+ fragments:
+ - 44_fix_missing_regions.yaml
+ release_date: '2022-12-21'
+ 1.4.0:
+ changes:
+ bugfixes:
+ - fusion_api_client - error messages now mostly handled by errors_py
+ - fusion_hap - could not delete host access policy without iqn option. Now it
+ needs only name option for deletion
+ - fusion_hap - error messages now mostly handled by errors_py
+ - fusion_hap - uppercase names were not supported. Now uppercase names are allowed
+ - fusion_info - fixes typo in output 'appiiances' -> 'appliances'
+ - fusion_info - network_interface_groups subset returned nothing. Now it collects
+ the same information as nigs subset
+ - fusion_info - placements subset returned nothing. Now it collects the same
+ information as placement_groups subset
+ - fusion_nig - add missing 'availability_zone' format param in error message
+ - fusion_nig - error messages now mostly handled by errors_py
+ - fusion_pg - create_pg always broke runtime. Now it executes and creates placement
+ group successfully
+ - fusion_pg - error messages now mostly handled by errors_py
+ - fusion_pp - error messages now mostly handled by errors_py
+ - fusion_pp - fix call to parse_minutes where we were missing a required argument
+ - fusion_sc - error messages now mostly handled by errors_py
+ - fusion_se - add missing 'availability_zone' format param in error message
+ - fusion_se - error messages now mostly handled by errors_py
+ - fusion_se - fix call in get_nifg where provider_subnet was used instead of
+ network_interface_group_name
+ - fusion_ss - error messages now mostly handled by errors_py
+ - fusion_tenant - error messages now mostly handled by errors_py
+ - fusion_ts - add missing 'tenant' format param in error message
+ - fusion_ts - error messages now mostly handled by errors_py
+ - fusion_volume - error messages now mostly handled by errors_py
+ deprecated_features:
+ - fusion_hw - hardware module is being removed as changing hardware type has
+ never been supported by Pure Storage Fusion
+ - fusion_info - nigs subset is deprecated in favor of network_interface_groups
+ and will be removed in the version 1.7.0
+ - fusion_info - placements subset is deprecated in favor of placement_groups
+ and will be removed in the version 1.7.0
+ - fusion_pg - placement_engine option is deprecated because Fusion API does
+ not longer support this parameter It will be removed in the version 2.0.0
+ - fusion_se - parameters 'addresses', 'gateway' and 'network_interface_groups'
+ are deprecated in favor of 'iscsi' and will be removed in version 2.0.0
+ - fusion_tn - tenant networks are being replaced by storage endpoints ```fusion_se```
+ and Network Interface Groups ```fusion_nig```
+ major_changes:
+ - Patching of resource properties was brought to parity with underlying Python
+ SDK
+ - fusion_volume - fixed and reorganized, arguments changed
+ minor_changes:
+ - errors_py - added opt-in global exception handler which produces simpler and
+ cleaner messages on REST errors
+ - removed dependency on Python `netaddr` package
+ fragments:
+ - 46_deprecate fusion_tn.yaml
+ - 47_fix_volumes.yaml
+ - 63_update_resource_patching.yaml
+ - 67_deprecate fusion_hw.yaml
+ - 68_update_error_handling_for_ts_volume.yaml
+ - 69_use_exc_handler_in_most_modules.yaml
+ - 70_fix_typo_appiances.yaml
+ - 71_fix_few_missing_format_params.yaml
+ - 72_fix_typo_getnifg.yaml
+ - 73_fix_missing_module_params.yaml
+ - 88_deprecate parameters in fusion_se.yaml
+ - PR46_network_interface_groups.yaml
+ - PR53_create_pg.yaml
+ - PR55_host_access_policy.yaml
+ - PR62_fix_placements.yaml
+ release_date: '2023-03-16'
+ 1.4.1:
+ release_date: '2023-03-17'
+ 1.4.2:
+ changes:
+ bugfixes:
+ - fusion_array - correct required parameters
+ - fusion_hap - display name has now default value set to the value of name
+ - fusion_hw - correct required parameters
+ - fusion_pg - correct required parameters
+ - fusion_pp - correct required parameters
+ - fusion_sc - correct required parameters
+ - fusion_ss - allow updating hardware types, correct required parameters
+ - fusion_tn - fix attribute error
+ - fusion_volume - protection policy can now be unset by using '' as name
+ minor_changes:
+ - added Python package dependency checks in prerequisites.py
+ - fusion_hap - added missing 'windows' personality type
+ fragments:
+ - 102_set_hap_display_name_default_value.yaml
+ - 103_correct_required_parameters_in_all_modules.yaml
+ - 108_add_missing_hap_personality.yaml
+ - 114_fix_fusion_tn_error.yaml
+ - 1483_allow_to_unset_protection_policy.yaml
+ - 1538_improve_missing_python_deps_error.yaml
+ release_date: '2023-04-11'
+ 1.5.0:
+ changes:
+ bugfixes:
+ - fusion_info - fix runtime errors caused when listing `interfaces`, `arrays`
+ and `snapshots` dicts
+ - fusion_pg - freshly created placement group is now moved to correct array
+ - fusion_pp - 'local_rpo' changed to accept same input as 'local_retention'
+ - fusion_pp - updated retention description
+ - fusion_ra - 'name' deprecated and aliased to 'role'
+ deprecated_features:
+ - fusion_api_client - 'app_id' and 'key_file' parameters are deprecated in favor
+ of 'issuer_id' and 'private_key_file' parameters and will be removed in the
+ version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated
+ in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version
+ 2.0.0
+ - fusion_array - 'app_id' and 'key_file' parameters are deprecated in favor
+ of 'issuer_id' and 'private_key_file' parameters and will be removed in the
+ version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated
+ in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version
+ 2.0.0
+ - fusion_az - 'app_id' and 'key_file' parameters are deprecated in favor of
+ 'issuer_id' and 'private_key_file' parameters and will be removed in the version
+ 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor
+ of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+ - fusion_hap - 'app_id' and 'key_file' parameters are deprecated in favor of
+ 'issuer_id' and 'private_key_file' parameters and will be removed in the version
+ 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor
+ of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+ - fusion_hap - parameters `nqn`, `wwns`, `host_password`, `host_user`, `target_password`and
+ `target_user` were deprecated
+ - fusion_hw - FUSION_APP_ID and FUSION_HOST env variables are deprecated in
+ favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version
+ 2.0.0
+ - fusion_info - 'app_id' and 'key_file' parameters are deprecated in favor of
+ 'issuer_id' and 'private_key_file' parameters and will be removed in the version
+ 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor
+ of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+ - fusion_info - 'hosts' subset is deprecated in favor of 'host_access_policies'
+ and will be removed in the version 2.0.0
+ - fusion_info - 'interfaces' subset is deprecated in favor of 'network_interfaces'
+ and will be removed in the version 2.0.0
+ - fusion_info - 'zones' subset is deprecated in favor of 'availability_zones'
+ and will be removed in the version 2.0.0
+ - fusion_ni - 'app_id' and 'key_file' parameters are deprecated in favor of
+ 'issuer_id' and 'private_key_file' parameters and will be removed in the version
+ 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor
+ of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+ - fusion_nig - 'app_id' and 'key_file' parameters are deprecated in favor of
+ 'issuer_id' and 'private_key_file' parameters and will be removed in the version
+ 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor
+ of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+ - fusion_pg - 'app_id' and 'key_file' parameters are deprecated in favor of
+ 'issuer_id' and 'private_key_file' parameters and will be removed in the version
+ 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor
+ of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+ - fusion_pp - 'app_id' and 'key_file' parameters are deprecated in favor of
+ 'issuer_id' and 'private_key_file' parameters and will be removed in the version
+ 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor
+ of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+ - fusion_ra - 'app_id' and 'key_file' parameters are deprecated in favor of
+ 'issuer_id' and 'private_key_file' parameters and will be removed in the version
+ 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor
+ of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+ - fusion_region - 'app_id' and 'key_file' parameters are deprecated in favor
+ of 'issuer_id' and 'private_key_file' parameters and will be removed in the
+ version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated
+ in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version
+ 2.0.0
+ - fusion_sc - 'app_id' and 'key_file' parameters are deprecated in favor of
+ 'issuer_id' and 'private_key_file' parameters and will be removed in the version
+ 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor
+ of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+ - fusion_se - 'app_id' and 'key_file' parameters are deprecated in favor of
+ 'issuer_id' and 'private_key_file' parameters and will be removed in the version
+ 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor
+ of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+ - fusion_se - `endpoint_type` parameter is now deprecated and will be removed
+ in version 2.0.0
+ - fusion_ss - 'app_id' and 'key_file' parameters are deprecated in favor of
+ 'issuer_id' and 'private_key_file' parameters and will be removed in the version
+ 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor
+ of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+ - fusion_tenant - 'app_id' and 'key_file' parameters are deprecated in favor
+ of 'issuer_id' and 'private_key_file' parameters and will be removed in the
+ version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated
+ in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version
+ 2.0.0
+ - fusion_tn - FUSION_APP_ID and FUSION_HOST env variables are deprecated in
+ favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version
+ 2.0.0
+ - fusion_ts - 'app_id' and 'key_file' parameters are deprecated in favor of
+ 'issuer_id' and 'private_key_file' parameters and will be removed in the version
+ 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated in favor
+ of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version 2.0.0
+ - fusion_volume - 'app_id' and 'key_file' parameters are deprecated in favor
+ of 'issuer_id' and 'private_key_file' parameters and will be removed in the
+ version 2.0.0, FUSION_APP_ID and FUSION_HOST env variables are deprecated
+ in favor of FUSION_ISSUER_ID and FUSION_HOST and will be removed in the version
+ 2.0.0
+ minor_changes:
+ - FUSION_API_HOST && FUSION_HOST - changed logic, now this variables require
+ host name without path
+ - Fusion authentication - add 'access_token' module's parameter and 'FUSION_ACCESS_TOKEN'
+ environment variable, as an alternative way of the authentication.
+ - fusion - added private key password, which is used to decrypt private key
+ files
+ - fusion_info - `array` is None if missing in `volume`
+ - fusion_info - `hardware_types` is None if missing in `storage_service`
+ - fusion_info - `network_interface_groups` is None if missing in `iscsi_interfaces`
+ in `storage_endpoint`
+ - fusion_info - introduce 'availability_zones' subset option
+ - fusion_info - introduce 'host_access_policies' subset option
+ - fusion_info - introduce 'network_interfaces' subset option
+ - fusion_info - introduce 'regions' subset option
+ - fusion_info - rename 'appliances' in default dict to 'arrays' for consistency
+ - fusion_info - rename 'hosts' dict to 'host_access_policies' for consistency
+ - fusion_info - rename 'interfaces' dict to 'network_interfaces' for consistency
+ - fusion_info - rename 'placements_groups' in default dict to 'placement_groups'
+ for consistency
+ - fusion_info - rename 'zones' dict to 'availability_zones' for consistency
+ - fusion_info - rename hardware to hardware_types in response for consistency
+ - fusion_info - rename storageclass to storage_classes in response for consistency
+ - fusion_pp - duration parsing improved. Supports combination of time units
+ (E.g 5H5M)
+ - fusion_ra - added `api_client_key` argument, which can be used instead of
+ `user` and `principal` argument
+ - fusion_ra - added `principal` argument, which is an ID of either API client
+ or User and can be used instead of `user` argument
+ - fusion_se - add support for CBS Storage Endpoint
+ fragments:
+ - 101_improve_duration_parsing.yml
+ - 109_unify_storage_classes_key_in_fusion_info.yml
+ - 112_unify_hardware_types_key_in_fusion_info.yml
+ - 121_unify_parameters_and_env_variables_across_different_clients.yml
+ - 127_change_logic_of_fusion_api_host.yml
+ - 129_add_access_token.yml
+ - 130_add_private_key_password.yml
+ - 132_deprecate_unused_parameters_in_hap_module.yml
+ - 134_add_cbs_storage_endpoint_support.yml
+ - 135_add_principal_to_fusion_ra.yml
+ - 138_add_api_client_key_to_fusion_ra.yml
+ - 139_fix_bugs_in_fusion_info.yml
+ - 142_add_missing_none_fields.yml
+ - 143_unify_keys_in_fusion_info.yml
+ - 3289_functests_pp_pg_ra.yml
+ - 99_update_protection_policy_retention_description.yaml
+ release_date: '2023-05-31'
diff --git a/ansible_collections/purestorage/fusion/changelogs/config.yaml b/ansible_collections/purestorage/fusion/changelogs/config.yaml
new file mode 100644
index 000000000..0c1851805
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/changelogs/config.yaml
@@ -0,0 +1,32 @@
+changelog_filename_template: ../CHANGELOG.rst
+changelog_filename_version_depth: 0
+changes_file: changelog.yaml
+changes_format: combined
+ignore_other_fragment_extensions: true
+keep_fragments: false
+mention_ancestor: true
+new_plugins_after_name: removed_features
+notesdir: fragments
+prelude_section_name: release_summary
+prelude_section_title: Release Summary
+sanitize_changelog: true
+sections:
+ - - major_changes
+ - Major Changes
+ - - minor_changes
+ - Minor Changes
+ - - breaking_changes
+ - Breaking Changes / Porting Guide
+ - - deprecated_features
+ - Deprecated Features
+ - - removed_features
+ - Removed Features (previously deprecated)
+ - - security_fixes
+ - Security Fixes
+ - - bugfixes
+ - Bugfixes
+ - - known_issues
+ - Known Issues
+title: Purestorage.Fusion
+trivial_section_name: trivial
+use_fqcn: true
diff --git a/ansible_collections/purestorage/fusion/meta/runtime.yml b/ansible_collections/purestorage/fusion/meta/runtime.yml
new file mode 100644
index 000000000..1812440b2
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/meta/runtime.yml
@@ -0,0 +1,12 @@
+---
+requires_ansible: ">=2.11.0"
+plugin_routing:
+ modules:
+ fusion_tn:
+ deprecation:
+ removal_date: "2023-07-26"
+ warning_text: "Use fusion_se + fusion_nig instead."
+ fusion_hw:
+ deprecation:
+ removal_date: "2023-08-09"
+ warning_text: "Don't use this module. This functionality isn't supported."
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/create_array.yml b/ansible_collections/purestorage/fusion/playbooks/simple/create_array.yml
new file mode 100644
index 000000000..b4109f7f9
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/create_array.yml
@@ -0,0 +1,17 @@
+---
+- name: Create array
+ hosts: localhost
+ tasks:
+
+ - name: Register array
+ purestorage.fusion.fusion_array:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ state: present # or absent
+ name: flasharray3
+ display_name: "flasharray3"
+ host_name: "flasharray3"
+ hardware_type: flash-array-x
+ appliance_id: 1187351-242133817-5976825671211737520
+ az: az1
+ region: region1
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/create_availability_zone.yml b/ansible_collections/purestorage/fusion/playbooks/simple/create_availability_zone.yml
new file mode 100644
index 000000000..37f4264c6
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/create_availability_zone.yml
@@ -0,0 +1,13 @@
+---
+- name: Create availability zone
+ hosts: localhost
+ tasks:
+
+ - name: Create new Availability Zone
+ purestorage.fusion.fusion_az:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ state: present # or absent
+ name: "az2"
+ display_name: "az2"
+ region: pure-us-west
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/create_tenant_space.yml b/ansible_collections/purestorage/fusion/playbooks/simple/create_tenant_space.yml
new file mode 100644
index 000000000..a71723c00
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/create_tenant_space.yml
@@ -0,0 +1,12 @@
+---
+- name: Create tenant space
+ hosts: localhost
+ tasks:
+
+ - name: Create new tenant space db_tenant_space for tenant_name
+ purestorage.fusion.fusion_ts:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ state: "present" # or absent
+ name: "db_tenant_space"
+ tenant: "tenant_name"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_all.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_all.yml
new file mode 100644
index 000000000..ff9d15cce
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_all.yml
@@ -0,0 +1,14 @@
+---
+- name: Print all Fusion resources
+ hosts: localhost
+ tasks:
+ - name: Collect all for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: all
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_api_clients.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_api_clients.yml
new file mode 100644
index 000000000..3e9c7ccb4
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_api_clients.yml
@@ -0,0 +1,14 @@
+---
+- name: Print API clients
+ hosts: localhost
+ tasks:
+ - name: Collect api_clients for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: api_clients
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_arrays.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_arrays.yml
new file mode 100644
index 000000000..4c8cad76c
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_arrays.yml
@@ -0,0 +1,14 @@
+---
+- name: Print arrays
+ hosts: localhost
+ tasks:
+ - name: Collect arrays for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: arrays
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_availability_zones.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_availability_zones.yml
new file mode 100644
index 000000000..3f4fc6ccf
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_availability_zones.yml
@@ -0,0 +1,14 @@
+---
+- name: Print availability zones
+ hosts: localhost
+ tasks:
+ - name: Collect Availability Zones for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: zones
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_hardware_types.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_hardware_types.yml
new file mode 100644
index 000000000..4c1e20db0
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_hardware_types.yml
@@ -0,0 +1,14 @@
+---
+- name: Print hardware types
+ hosts: localhost
+ tasks:
+ - name: Collect hardware_types for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: hardware_types
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_hosts.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_hosts.yml
new file mode 100644
index 000000000..1c60fdf7b
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_hosts.yml
@@ -0,0 +1,14 @@
+---
+- name: List hosts
+ hosts: localhost
+ tasks:
+ - name: Collect hosts for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: hosts
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_interfaces.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_interfaces.yml
new file mode 100644
index 000000000..edee38234
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_interfaces.yml
@@ -0,0 +1,14 @@
+---
+- name: Print interfaces
+ hosts: localhost
+ tasks:
+ - name: Collect interfaces for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: interfaces
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_network_interface_groups.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_network_interface_groups.yml
new file mode 100644
index 000000000..6b2cbc260
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_network_interface_groups.yml
@@ -0,0 +1,14 @@
+---
+- name: Print network interface groups
+ hosts: localhost
+ tasks:
+ - name: Collect network_interface_groups for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: network_interface_groups
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_placement_groups.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_placement_groups.yml
new file mode 100644
index 000000000..2b7da696d
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_placement_groups.yml
@@ -0,0 +1,14 @@
+---
+- name: Print placement groups
+ hosts: localhost
+ tasks:
+ - name: Collect placement_groups for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: placement_groups
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_protection_policies.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_protection_policies.yml
new file mode 100644
index 000000000..f9b8249ec
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_protection_policies.yml
@@ -0,0 +1,14 @@
+---
+- name: Print protection policies
+ hosts: localhost
+ tasks:
+ - name: Collect protection_policies for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: protection_policies
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_roles.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_roles.yml
new file mode 100644
index 000000000..c224f54ab
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_roles.yml
@@ -0,0 +1,14 @@
+---
+- name: Print roles
+ hosts: localhost
+ tasks:
+ - name: Collect roles for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: roles
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_snapshots.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_snapshots.yml
new file mode 100644
index 000000000..fda1c1665
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_snapshots.yml
@@ -0,0 +1,14 @@
+---
+- name: Print snapshots
+ hosts: localhost
+ tasks:
+ - name: Collect snapshots for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: snapshots
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_storage_classes.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_storage_classes.yml
new file mode 100644
index 000000000..1d361e329
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_storage_classes.yml
@@ -0,0 +1,14 @@
+---
+- name: Print storage classes
+ hosts: localhost
+ tasks:
+ - name: Collect storage_classes for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: storage_classes
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_storage_endpoints.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_storage_endpoints.yml
new file mode 100644
index 000000000..806cceab1
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_storage_endpoints.yml
@@ -0,0 +1,14 @@
+---
+- name: Print storage endpoints
+ hosts: localhost
+ tasks:
+ - name: Collect storage_endpoints for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: storage_endpoints
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_storage_services.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_storage_services.yml
new file mode 100644
index 000000000..2d4414867
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_storage_services.yml
@@ -0,0 +1,14 @@
+---
+- name: Print storage services
+ hosts: localhost
+ tasks:
+ - name: Collect storage_services for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: storage_services
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_tenant_spaces.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_tenant_spaces.yml
new file mode 100644
index 000000000..dd392c131
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_tenant_spaces.yml
@@ -0,0 +1,14 @@
+---
+- name: Print tenant spaces
+ hosts: localhost
+ tasks:
+ - name: Collect tenant_spaces for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: tenant_spaces
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_tenants.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_tenants.yml
new file mode 100644
index 000000000..f15c62848
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_tenants.yml
@@ -0,0 +1,14 @@
+---
+- name: Print tenants
+ hosts: localhost
+ tasks:
+ - name: Collect tenants for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: tenants
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_users.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_users.yml
new file mode 100644
index 000000000..e64852513
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_users.yml
@@ -0,0 +1,14 @@
+---
+- name: Print users
+ hosts: localhost
+ tasks:
+ - name: Collect users for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: users
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/list_volumes.yml b/ansible_collections/purestorage/fusion/playbooks/simple/list_volumes.yml
new file mode 100644
index 000000000..8c97e6a6c
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/list_volumes.yml
@@ -0,0 +1,14 @@
+---
+- name: Print volumes
+ hosts: localhost
+ tasks:
+ - name: Collect volumes for Pure Storage
+ purestorage.fusion.fusion_info:
+ gather_subset: volumes
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ register: fusion_info
+
+ - name: Print Fusion resources
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/make_tenant_admin.yml b/ansible_collections/purestorage/fusion/playbooks/simple/make_tenant_admin.yml
new file mode 100644
index 000000000..f55a30023
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/make_tenant_admin.yml
@@ -0,0 +1,12 @@
+---
+- name: Add tenant admin role
+ hosts: localhost
+ tasks:
+ - name: Add tenant-admin role to api-client
+ purestorage.fusion.fusion_ra:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ state: present
+ role: "tenant-admin"
+ scope: "organization" # "organization" "tenant_space"
+ user: "{{ ansible_env.FUSION_ISSUER_ID }}"
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/remove_array.yml b/ansible_collections/purestorage/fusion/playbooks/simple/remove_array.yml
new file mode 100644
index 000000000..b50031743
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/remove_array.yml
@@ -0,0 +1,17 @@
+---
+- name: Remote array
+ hosts: localhost
+ tasks:
+
+ - name: Remove array
+ purestorage.fusion.fusion_array:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ state: absent # or present
+ name: flasharray3
+ display_name: "flasharray3"
+ host_name: "flasharray3"
+ hardware_type: flash-array-x
+ appliance_id: 1187351-242133817-5976825671211737520
+ az: az1
+ region: region1
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/setup_infrastructure.yml b/ansible_collections/purestorage/fusion/playbooks/simple/setup_infrastructure.yml
new file mode 100644
index 000000000..baea6e59d
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/setup_infrastructure.yml
@@ -0,0 +1,64 @@
+---
+- name: Setup Fusion resources
+ hosts: localhost
+ tasks:
+
+ - name: Create new region
+ purestorage.fusion.fusion_region:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ state: present # or absent
+ name: region1
+ display_name: "region1"
+
+ - name: Create new Availability Zone
+ purestorage.fusion.fusion_az:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ state: present # or absent
+ name: az1
+ region: region1
+ display_name: "az1"
+
+ - name: Create new network interface group
+ purestorage.fusion.fusion_nig:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ state: present # or absent
+ name: "interface_group1"
+ availability_zone: "az1"
+ region: region1
+ mtu: 1500
+ gateway: 172.17.1.1
+ prefix: 172.17.1.0/24
+
+ - name: Create new Storage Endpoint
+ purestorage.fusion.fusion_se:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ state: present # or absent
+ region: region1
+ name: default
+ display_name: default
+ availability_zone: az1
+ endpoint_type: iscsi
+ iscsi:
+ - address: "172.17.1.2/24"
+ gateway: "172.17.1.1"
+ network_interface_groups: ["interface_group1"]
+ - address: "172.17.1.1/24"
+ gateway: "172.17.1.1"
+ network_interface_groups: ["interface_group1"]
+
+ - name: Register new array
+ purestorage.fusion.fusion_array:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ state: present # or absent
+ name: flasharray1
+ display_name: "flasharray1"
+ az: az1
+ hardware_type: flash-array-x
+ appliance_id: 1187351-242133817-5976825671211737520
+ region: region1
+ host_name: flasharray1
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/setup_protection_policies.yml b/ansible_collections/purestorage/fusion/playbooks/simple/setup_protection_policies.yml
new file mode 100644
index 000000000..f88c35e7a
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/setup_protection_policies.yml
@@ -0,0 +1,13 @@
+---
+- name: Create protection policy
+ hosts: localhost
+ tasks:
+ - name: Create new Protection Policy PP_name
+ purestorage.fusion.fusion_pp:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ name: "PP_name"
+ display_name: "PP_name"
+ local_rpo: "15" # in minutes
+ local_retention: "24h" # m(inutes), h(ours), d(ays), w(eeks) or y(ears)
+ state: present # or absent
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/setup_storage_service_class.yml b/ansible_collections/purestorage/fusion/playbooks/simple/setup_storage_service_class.yml
new file mode 100644
index 000000000..16326bc93
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/setup_storage_service_class.yml
@@ -0,0 +1,24 @@
+---
+- name: Create storage service & class
+ hosts: localhost
+ tasks:
+ - name: Create new storage service called storage_service_1
+ purestorage.fusion.fusion_ss:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ name: storage_service_1
+ display_name: "storage_service_1"
+ hardware_types: flash-array-c
+ state: present # or absent
+
+ - name: Create new storage class storage_class_1
+ purestorage.fusion.fusion_sc:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ name: storage_class_1
+ display_name: "storage_class_1"
+ storage_service: storage_service_1
+ size_limit: 100M # 100M 100G 100T 100P - Default 4PB
+ iops_limit: 10000 # Must be between 100 and 100000000.
+ bw_limit: 4194M # 524288000
+ state: present # or absent
diff --git a/ansible_collections/purestorage/fusion/playbooks/simple/setup_workloads.yml b/ansible_collections/purestorage/fusion/playbooks/simple/setup_workloads.yml
new file mode 100644
index 000000000..3a601514c
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/simple/setup_workloads.yml
@@ -0,0 +1,71 @@
+---
+- name: Setup workloads
+ hosts: localhost
+ # This playbook create:
+
+ # *- Tenant space: db_tenant_space
+ # *- Placement Group: pg1
+ # *- AIX host access policy: customer_host_access
+ # *- volume: data_vol1
+ # *- volume: data_vol2
+
+ # require:
+ # *- Storage class: db_high_performance
+ # *- Tenant: oracle_dbas
+ # *- Region: region1
+
+ tasks:
+ - name: Create new tenant space db_tenant_space for oracle_dbas
+ purestorage.fusion.fusion_ts:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ name: "db_tenant_space"
+ tenant: "oracle_dbas"
+ state: "present" # or absent
+
+ - name: Create new placement group named pg1
+ purestorage.fusion.fusion_pg:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ name: "pg1"
+ tenant: "oracle_dbas"
+ tenant_space: "db_tenant_space"
+ region: "region1"
+ availability_zone: "az1"
+ storage_service: "db_xl"
+ state: "present" # or absent
+
+ - name: Create new host access policy
+ purestorage.fusion.fusion_hap:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ name: "customer_host_access"
+ personality: "linux"
+ iqn: "iqn.1994-05.com.redhat:9dd57693efb"
+ state: "present" # or absent
+
+ - name: Create new volume data_vol1 in storage_class db_high_performance
+ purestorage.fusion.fusion_volume:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ name: "data_vol1"
+ storage_class: "db_high_performance"
+ size: "500G" # Volume size in M, G, T or P units.
+ tenant: "oracle_dbas"
+ tenant_space: "db_tenant_space"
+ placement_group: "pg1"
+ host_access_policies: "customer_host_access"
+ state: "present" # or absent
+
+ - name: Create new volume data_vol2 in storage_class db_high_performance
+ purestorage.fusion.fusion_volume:
+ issuer_id: "{{ ansible_env.FUSION_ISSUER_ID }}"
+ private_key_file: "{{ ansible_env.FUSION_PRIVATE_KEY_FILE }}"
+ name: "data_vol2"
+ storage_class: "db_high_performance"
+ size: "500G" # Volume size in M, G, T or P units.
+ tenant: "oracle_dbas"
+ tenant_space: "db_tenant_space"
+ placement_group: "pg1"
+ host_access_policies: "customer_host_access"
+ state: "present" # or absent
diff --git a/ansible_collections/purestorage/fusion/playbooks/tasks/.keep b/ansible_collections/purestorage/fusion/playbooks/tasks/.keep
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/tasks/.keep
diff --git a/ansible_collections/purestorage/fusion/playbooks/templates/.keep b/ansible_collections/purestorage/fusion/playbooks/templates/.keep
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/templates/.keep
diff --git a/ansible_collections/purestorage/fusion/playbooks/vars/.keep b/ansible_collections/purestorage/fusion/playbooks/vars/.keep
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/playbooks/vars/.keep
diff --git a/ansible_collections/purestorage/fusion/plugins/doc_fragments/purestorage.py b/ansible_collections/purestorage/fusion/plugins/doc_fragments/purestorage.py
new file mode 100644
index 000000000..a2f933161
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/doc_fragments/purestorage.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2021, Simon Dodsley <simon@purestorage.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+ # Standard Pure Storage documentation fragment
+ DOCUMENTATION = r"""
+options:
+ - See separate platform section for more details
+requirements:
+ - See separate platform section for more details
+notes:
+ - Ansible modules are available for the following Pure Storage products: FlashArray, FlashBlade, Pure1, Fusion
+"""
+
+ # Documentation fragment for Fusion
+ FUSION = r"""
+options:
+ private_key_file:
+ aliases: [ key_file ]
+ description:
+ - Path to the private key file
+ - Defaults to the set environment variable under FUSION_PRIVATE_KEY_FILE.
+ type: str
+ private_key_password:
+ description:
+ - Password of the encrypted private key file
+ type: str
+ issuer_id:
+ aliases: [ app_id ]
+ description:
+ - Application ID from Pure1 Registration page
+ - eg. pure1:apikey:dssf2331sd
+ - Defaults to the set environment variable under FUSION_ISSUER_ID
+ type: str
+ access_token:
+ description:
+ - Access token for Fusion Service
+ - Defaults to the set environment variable under FUSION_ACCESS_TOKEN
+ type: str
+notes:
+ - This module requires the I(purefusion) Python library
+ - You must set C(FUSION_ISSUER_ID) and C(FUSION_PRIVATE_KEY_FILE) environment variables
+ if I(issuer_id) and I(private_key_file) arguments are not passed to the module directly
+ - If you want to use access token for authentication, you must use C(FUSION_ACCESS_TOKEN) environment variable
+ if I(access_token) argument is not passed to the module directly
+requirements:
+ - python >= 3.8
+ - purefusion
+"""
diff --git a/ansible_collections/purestorage/fusion/plugins/inventory/__init__.py b/ansible_collections/purestorage/fusion/plugins/inventory/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/inventory/__init__.py
diff --git a/ansible_collections/purestorage/fusion/plugins/module_utils/errors.py b/ansible_collections/purestorage/fusion/plugins/module_utils/errors.py
new file mode 100644
index 000000000..0edf364cf
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/module_utils/errors.py
@@ -0,0 +1,291 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+try:
+ import fusion as purefusion
+ import urllib3
+except ImportError:
+ pass
+
+import sys
+import json
+import re
+import traceback as trace
+
+
+class OperationException(Exception):
+ """Raised if an asynchronous Operation fails."""
+
+ def __init__(self, op, http_error=None):
+ self._op = op
+ self._http_error = http_error
+
+ @property
+ def op(self):
+ return self._op
+
+ @property
+ def http_error(self):
+ return self._http_error
+
+
+def _get_verbosity(module):
+ # verbosity is a private member and Ansible does not really allow
+ # providing extra information only if the user wants it due to ideological
+ # reasons, so extract it as carefully as possible and assume non-verbose
+ # if something fails
+ try:
+ if module._verbosity is not None and isinstance(module._verbosity, int):
+ return module._verbosity
+ except Exception:
+ pass
+ return 0
+
+
+def _extract_rest_call_site(traceback):
+ # extracts first function in traceback that comes from 'fusion.api.*_api*',
+ # converts its name from something like 'get_volume' to 'Get volume' and returns
+ while traceback:
+ try:
+ frame = traceback.tb_frame
+ func_name = (
+ frame.f_code.co_name
+ ) # contains function name, e.g. 'get_volume'
+ mod_path = frame.f_globals[
+ "__name__"
+ ] # contains module path, e.g. 'fusion.api.volumes_api'
+ path_segments = mod_path.split(".")
+ if (
+ path_segments[0] == "fusion"
+ and path_segments[1] == "api"
+ and "_api" in path_segments[2]
+ ):
+ call_site = func_name.replace("_", " ").capitalize()
+ return call_site
+ except Exception:
+ pass
+ traceback = traceback.tb_next
+ return None
+
+
+class DetailsPrinter:
+ def __init__(self, target):
+ self._target = target
+ self._parenthesed = False
+
+ def append(self, what):
+ if not self._parenthesed:
+ self._target += " ("
+ self._parenthesed = True
+ else:
+ self._target += ", "
+
+ self._target += what
+
+ def finish(self):
+ if self._parenthesed:
+ self._target += ")"
+ return self._target
+
+
+def format_fusion_api_exception(exception, traceback=None):
+ """Formats `fusion.rest.ApiException` into a simple short form, suitable
+ for Ansible error output. Returns a (message: str, body: dict) tuple."""
+ message = None
+ code = None
+ resource_name = None
+ request_id = None
+ body = None
+ call_site = _extract_rest_call_site(traceback)
+ try:
+ body = json.loads(exception.body)
+ request_id = body.get("request_id", None)
+ error = body["error"]
+ message = error.get("message")
+ code = error.get("pure_code")
+ if not code:
+ code = exception.status
+ if not code:
+ code = error.get("http_code")
+ resource_name = error["details"]["name"]
+ except Exception:
+ pass
+
+ output = ""
+ if call_site:
+ output += "'{0}' failed".format(call_site)
+ else:
+ output += "request failed"
+
+ if message:
+ output += ", {0}".format(message.replace('"', "'"))
+
+ details = DetailsPrinter(output)
+ if resource_name:
+ details.append("resource: '{0}'".format(resource_name))
+ if code:
+ details.append("code: '{0}'".format(code))
+ if request_id:
+ details.append("request id: '{0}'".format(request_id))
+ output = details.finish()
+
+ return (output, body)
+
+
+def format_failed_fusion_operation_exception(exception):
+ """Formats failed `fusion.Operation` into a simple short form, suitable
+ for Ansible error output. Returns a (message: str, body: dict) tuple."""
+ op = exception.op
+ http_error = exception.http_error
+ if op.status != "Failed" and not http_error:
+ raise ValueError(
+ "BUG: can only format Operation exception with .status == Failed or http_error != None"
+ )
+
+ message = None
+ code = None
+ operation_name = None
+ operation_id = None
+
+ try:
+ if op.status == "Failed":
+ operation_id = op.id
+ error = op.error
+ message = error.message
+ code = error.pure_code
+ if not code:
+ code = error.http_code
+ operation_name = op.request_type
+ except Exception as e:
+ pass
+
+ output = ""
+ if operation_name:
+ # converts e.g. 'CreateVolume' to 'Create volume'
+ operation_name = re.sub("(.)([A-Z][a-z]+)", r"\1 \2", operation_name)
+ operation_name = re.sub(
+ "([a-z0-9])([A-Z])", r"\1 \2", operation_name
+ ).capitalize()
+ output += "{0}: ".format(operation_name)
+ output += "operation failed"
+
+ if message:
+ output += ", {0}".format(message.replace('"', "'"))
+
+ details = DetailsPrinter(output)
+ if code:
+ details.append("code: '{0}'".format(code))
+ if operation_id:
+ details.append("operation id: '{0}'".format(operation_id))
+ if http_error:
+ details.append("HTTP error: '{0}'".format(str(http_error).replace('"', "'")))
+
+ output = details.finish()
+
+ return output
+
+
+def format_http_exception(exception, traceback):
+ """Formats failed `urllib3.exceptions` exceptions into a simple short form,
+ suitable for Ansible error output. Returns a `str`."""
+ # urllib3 exceptions hide all details in a formatted message so all we
+ # can do is append the REST call that caused this
+ output = ""
+ call_site = _extract_rest_call_site(traceback)
+ if call_site:
+ output += "'{0}': ".format(call_site)
+ output += "HTTP request failed via "
+
+ inner = exception
+ while True:
+ try:
+ e = inner.reason
+ if e and isinstance(e, urllib3.exceptions.HTTPError):
+ inner = e
+ continue
+ break
+ except Exception:
+ break
+
+ if inner != exception:
+ output += "'{0}'/'{1}'".format(type(inner).__name__, type(exception).__name__)
+ else:
+ output += "'{0}'".format(type(exception).__name__)
+
+ output += " - {0}".format(str(exception).replace('"', "'"))
+
+ return output
+
+
+def _handle_api_exception(
+ module,
+ exception,
+ traceback,
+ verbosity,
+):
+ (error_message, body) = format_fusion_api_exception(exception, traceback)
+
+ if verbosity > 1:
+ module.fail_json(msg=error_message, call_details=body, traceback=str(traceback))
+ elif verbosity > 0:
+ module.fail_json(msg=error_message, call_details=body)
+ else:
+ module.fail_json(msg=error_message)
+
+
+def _handle_operation_exception(module, exception, traceback, verbosity):
+ op = exception.op
+
+ error_message = format_failed_fusion_operation_exception(exception)
+
+ if verbosity > 1:
+ module.fail_json(
+ msg=error_message, op_details=op.to_dict(), traceback=str(traceback)
+ )
+ elif verbosity > 0:
+ module.fail_json(msg=error_message, op_details=op.to_dict())
+ else:
+ module.fail_json(msg=error_message)
+
+
+def _handle_http_exception(module, exception, traceback, verbosity):
+ error_message = format_http_exception(exception, traceback)
+
+ if verbosity > 1:
+ module.fail_json(msg=error_message, traceback=trace.format_exception(exception))
+ else:
+ module.fail_json(msg=error_message)
+
+
+def _except_hook_callback(module, original_hook, type, value, traceback):
+ verbosity = _get_verbosity(module)
+ if type == purefusion.rest.ApiException:
+ _handle_api_exception(
+ module,
+ value,
+ traceback,
+ verbosity,
+ )
+ elif type == OperationException:
+ _handle_operation_exception(module, value, traceback, verbosity)
+ elif issubclass(type, urllib3.exceptions.HTTPError):
+ _handle_http_exception(module, value, traceback, verbosity)
+
+ # if we bubbled here the handlers were not able to process the exception
+ original_hook(type, value, traceback)
+
+
+def install_fusion_exception_hook(module):
+ """Installs a hook that catches `purefusion.rest.ApiException` and
+ `OperationException` and produces simpler and nicer error messages
+ for Ansible output."""
+ original_hook = sys.excepthook
+ sys.excepthook = lambda type, value, traceback: _except_hook_callback(
+ module, original_hook, type, value, traceback
+ )
diff --git a/ansible_collections/purestorage/fusion/plugins/module_utils/fusion.py b/ansible_collections/purestorage/fusion/plugins/module_utils/fusion.py
new file mode 100644
index 000000000..74b5f0e91
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/module_utils/fusion.py
@@ -0,0 +1,183 @@
+# -*- coding: utf-8 -*-
+
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# Copyright (c), Simon Dodsley <simon@purestorage.com>,2021
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+try:
+ import fusion
+except ImportError:
+ pass
+
+from os import environ
+from urllib.parse import urljoin
+import platform
+
+TOKEN_EXCHANGE_URL = "https://api.pure1.purestorage.com/oauth2/1.0/token"
+VERSION = 1.0
+USER_AGENT_BASE = "Ansible"
+
+PARAM_ISSUER_ID = "issuer_id"
+PARAM_PRIVATE_KEY_FILE = "private_key_file"
+PARAM_PRIVATE_KEY_PASSWORD = "private_key_password"
+PARAM_ACCESS_TOKEN = "access_token"
+ENV_ISSUER_ID = "FUSION_ISSUER_ID"
+ENV_API_HOST = "FUSION_API_HOST"
+ENV_PRIVATE_KEY_FILE = "FUSION_PRIVATE_KEY_FILE"
+ENV_TOKEN_ENDPOINT = "FUSION_TOKEN_ENDPOINT"
+ENV_ACCESS_TOKEN = "FUSION_ACCESS_TOKEN"
+
+# will be deprecated in 2.0.0
+PARAM_APP_ID = "app_id" # replaced by PARAM_ISSUER_ID
+PARAM_KEY_FILE = "key_file" # replaced by PARAM_PRIVATE_KEY_FILE
+ENV_APP_ID = "FUSION_APP_ID" # replaced by ENV_ISSUER_ID
+ENV_HOST = "FUSION_HOST" # replaced by ENV_API_HOST
+DEP_VER = "2.0.0"
+BASE_PATH = "/api/1.1"
+
+
+def _env_deprecation_warning(module, old_env, new_env, vers):
+ if old_env in environ:
+ if new_env in environ:
+ module.warn(
+ f"{old_env} env variable is ignored because {new_env} is specified."
+ f" {old_env} env variable is deprecated and will be removed in version {vers}"
+ f" Please use {new_env} env variable only."
+ )
+ else:
+ module.warn(
+ f"{old_env} env variable is deprecated and will be removed in version {vers}"
+ f" Please use {new_env} env variable instead."
+ )
+
+
+def _param_deprecation_warning(module, old_param, new_param, vers):
+ if old_param in module.params:
+ module.warn(
+ f"{old_param} parameter is deprecated and will be removed in version {vers}"
+ f" Please use {new_param} parameter instead."
+ f" Don't use both parameters simultaneously."
+ )
+
+
+def get_fusion(module):
+ """Return System Object or Fail"""
+ # deprecation warnings
+ _param_deprecation_warning(module, PARAM_APP_ID, PARAM_ISSUER_ID, DEP_VER)
+ _param_deprecation_warning(module, PARAM_KEY_FILE, PARAM_PRIVATE_KEY_FILE, DEP_VER)
+ _env_deprecation_warning(module, ENV_APP_ID, ENV_ISSUER_ID, DEP_VER)
+ _env_deprecation_warning(module, ENV_HOST, ENV_API_HOST, DEP_VER)
+
+ user_agent = "%(base)s %(class)s/%(version)s (%(platform)s)" % {
+ "base": USER_AGENT_BASE,
+ "class": __name__,
+ "version": VERSION,
+ "platform": platform.platform(),
+ }
+
+ issuer_id = module.params[PARAM_ISSUER_ID]
+ access_token = module.params[PARAM_ACCESS_TOKEN]
+ private_key_file = module.params[PARAM_PRIVATE_KEY_FILE]
+ private_key_password = module.params[PARAM_PRIVATE_KEY_PASSWORD]
+
+ if private_key_password is not None:
+ module.fail_on_missing_params([PARAM_PRIVATE_KEY_FILE])
+
+ config = fusion.Configuration()
+ if ENV_API_HOST in environ or ENV_HOST in environ:
+ host_url = environ.get(ENV_API_HOST, environ.get(ENV_HOST))
+ config.host = urljoin(host_url, BASE_PATH)
+ config.token_endpoint = environ.get(ENV_TOKEN_ENDPOINT, config.token_endpoint)
+
+ if access_token is not None:
+ config.access_token = access_token
+ elif issuer_id is not None and private_key_file is not None:
+ config.issuer_id = issuer_id
+ config.private_key_file = private_key_file
+ if private_key_password is not None:
+ config.private_key_password = private_key_password
+ elif ENV_ACCESS_TOKEN in environ:
+ config.access_token = environ.get(ENV_ACCESS_TOKEN)
+ elif (
+ ENV_ISSUER_ID in environ or ENV_APP_ID in environ
+ ) and ENV_PRIVATE_KEY_FILE in environ:
+ config.issuer_id = environ.get(ENV_ISSUER_ID, environ.get(ENV_APP_ID))
+ config.private_key_file = environ.get(ENV_PRIVATE_KEY_FILE)
+ else:
+ module.fail_json(
+ msg=f"You must set either {ENV_ISSUER_ID} and {ENV_PRIVATE_KEY_FILE} or {ENV_ACCESS_TOKEN} environment variables. "
+ f"Or module arguments either {PARAM_ISSUER_ID} and {PARAM_PRIVATE_KEY_FILE} or {PARAM_ACCESS_TOKEN}"
+ )
+
+ try:
+ client = fusion.ApiClient(config)
+ client.set_default_header("User-Agent", user_agent)
+ api_instance = fusion.DefaultApi(client)
+ api_instance.get_version()
+ except Exception as err:
+ module.fail_json(msg="Fusion authentication failed: {0}".format(err))
+
+ return client
+
+
+def fusion_argument_spec():
+ """Return standard base dictionary used for the argument_spec argument in AnsibleModule"""
+
+ return {
+ PARAM_ISSUER_ID: {
+ "no_log": True,
+ "aliases": [PARAM_APP_ID],
+ "deprecated_aliases": [
+ {
+ "name": PARAM_APP_ID,
+ "version": DEP_VER,
+ "collection_name": "purefusion.fusion",
+ }
+ ],
+ },
+ PARAM_PRIVATE_KEY_FILE: {
+ "no_log": False,
+ "aliases": [PARAM_KEY_FILE],
+ "deprecated_aliases": [
+ {
+ "name": PARAM_KEY_FILE,
+ "version": DEP_VER,
+ "collection_name": "purefusion.fusion",
+ }
+ ],
+ },
+ PARAM_PRIVATE_KEY_PASSWORD: {
+ "no_log": True,
+ },
+ PARAM_ACCESS_TOKEN: {
+ "no_log": True,
+ },
+ }
diff --git a/ansible_collections/purestorage/fusion/plugins/module_utils/getters.py b/ansible_collections/purestorage/fusion/plugins/module_utils/getters.py
new file mode 100644
index 000000000..535de76ba
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/module_utils/getters.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Daniel Turecek (dturecek@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+
+def get_array(module, fusion, array_name=None):
+ """Return Array or None"""
+ array_api_instance = purefusion.ArraysApi(fusion)
+ try:
+ if array_name is None:
+ array_name = module.params["array"]
+
+ return array_api_instance.get_array(
+ array_name=array_name,
+ availability_zone_name=module.params["availability_zone"],
+ region_name=module.params["region"],
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def get_az(module, fusion, availability_zone_name=None):
+ """Get Availability Zone or None"""
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+ try:
+ if availability_zone_name is None:
+ availability_zone_name = module.params["availability_zone"]
+
+ return az_api_instance.get_availability_zone(
+ region_name=module.params["region"],
+ availability_zone_name=availability_zone_name,
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def get_region(module, fusion, region_name=None):
+ """Get Region or None"""
+ region_api_instance = purefusion.RegionsApi(fusion)
+ try:
+ if region_name is None:
+ region_name = module.params["region"]
+
+ return region_api_instance.get_region(
+ region_name=region_name,
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def get_ss(module, fusion, storage_service_name=None):
+ """Return Storage Service or None"""
+ ss_api_instance = purefusion.StorageServicesApi(fusion)
+ try:
+ if storage_service_name is None:
+ storage_service_name = module.params["storage_service"]
+
+ return ss_api_instance.get_storage_service(
+ storage_service_name=storage_service_name
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def get_tenant(module, fusion, tenant_name=None):
+ """Return Tenant or None"""
+ api_instance = purefusion.TenantsApi(fusion)
+ try:
+ if tenant_name is None:
+ tenant_name = module.params["tenant"]
+
+ return api_instance.get_tenant(tenant_name=tenant_name)
+ except purefusion.rest.ApiException:
+ return None
+
+
+def get_ts(module, fusion, tenant_space_name=None):
+ """Tenant Space or None"""
+ ts_api_instance = purefusion.TenantSpacesApi(fusion)
+ try:
+ if tenant_space_name is None:
+ tenant_space_name = module.params["tenant_space"]
+
+ return ts_api_instance.get_tenant_space(
+ tenant_name=module.params["tenant"],
+ tenant_space_name=tenant_space_name,
+ )
+ except purefusion.rest.ApiException:
+ return None
diff --git a/ansible_collections/purestorage/fusion/plugins/module_utils/networking.py b/ansible_collections/purestorage/fusion/plugins/module_utils/networking.py
new file mode 100644
index 000000000..a00d8200a
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/module_utils/networking.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+import ipaddress
+
+# while regexes are hard to maintain, they are used anyways for few reasons:
+# a) REST backend accepts fairly restricted input and we need to match that input instead of all
+# the esoteric extra forms various packages are usually capable of parsing (like dotted-decimal
+# subnet masks, octal octets, hexadecimal octets, zero-extended addresses etc.)
+# b) manually written parsing routines are usually complex to write, verify and think about
+import re
+
+# IPv4 octet regex part, matches only simple decimal 0-255 without leading zeroes
+_octet = (
+ "((?:[0-9])|" # matches 0-9
+ "(?:[1-9][0-9])|" # matches 10-99
+ "(?:1[0-9][0-9])|" # matches 100-199
+ "(?:2[0-4][0-9])|" # matches 200-249
+ "(?:25[0-5]))" # matches 250-255
+)
+
+# IPv4 subnet mask regex part, matches decimal 8-32
+_subnet_mask = (
+ "((?:[8-9])|" # matches 8-9
+ "(?:[1-2][0-9])|" # matches 10-29
+ "(?:3[0-2]))" # matches 30-32
+)
+
+# matches IPv4 addresses
+_addr_pattern = re.compile(r"^{octet}\.{octet}\.{octet}\.{octet}$".format(octet=_octet))
+# matches IPv4 networks in CIDR format, i.e. addresses in the form 'a.b.c.d/e'
+_cidr_pattern = re.compile(
+ r"^{octet}\.{octet}\.{octet}\.{octet}\/{0}$".format(_subnet_mask, octet=_octet)
+)
+
+
+def is_valid_network(addr):
+ """Returns True if `addr` is IPv4 address/submask in bit CIDR notation, False otherwise."""
+ match = re.match(_cidr_pattern, addr)
+ if match is None:
+ return False
+ for i in range(4):
+ if int(match.group(i + 1)) > 255:
+ return False
+ mask = int(match.group(5))
+ if mask < 8 or mask > 32:
+ return False
+ return True
+
+
+def is_valid_address(addr):
+ """Returns True if `addr` is a valid IPv4 address, False otherwise. Does not support
+ octal/hex notations."""
+ match = re.match(_addr_pattern, addr)
+ if match is None:
+ return False
+ for i in range(4):
+ if int(match.group(i + 1)) > 255:
+ return False
+ return True
+
+
+def is_address_in_network(addr, network):
+ """Returns True if `addr` and `network` are a valid IPv4 address and
+ IPv4 network respectively and if `addr` is in `network`, False otherwise."""
+ if not is_valid_address(addr) or not is_valid_network(network):
+ return False
+ parsed_addr = ipaddress.ip_address(addr)
+ parsed_net = ipaddress.ip_network(network)
+ return parsed_addr in parsed_net
diff --git a/ansible_collections/purestorage/fusion/plugins/module_utils/operations.py b/ansible_collections/purestorage/fusion/plugins/module_utils/operations.py
new file mode 100644
index 000000000..dc80aefe3
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/module_utils/operations.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+import time
+import math
+
+try:
+ import fusion as purefusion
+ from urllib3.exceptions import HTTPError
+except ImportError:
+ pass
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+
+
+def await_operation(fusion, operation, fail_playbook_if_operation_fails=True):
+ """
+ Waits for given operation to finish.
+ Throws an exception by default if the operation fails.
+ """
+ op_api = purefusion.OperationsApi(fusion)
+ operation_get = None
+ while True:
+ try:
+ operation_get = op_api.get_operation(operation.id)
+ if operation_get.status == "Succeeded":
+ return operation_get
+ if operation_get.status == "Failed":
+ if fail_playbook_if_operation_fails:
+ raise OperationException(operation_get)
+ return operation_get
+ except HTTPError as err:
+ raise OperationException(operation, http_error=err)
+ time.sleep(int(math.ceil(operation_get.retry_in / 1000)))
diff --git a/ansible_collections/purestorage/fusion/plugins/module_utils/parsing.py b/ansible_collections/purestorage/fusion/plugins/module_utils/parsing.py
new file mode 100644
index 000000000..a2cd75245
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/module_utils/parsing.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+import re
+
+__metaclass__ = type
+
+METRIC_SUFFIXES = ["K", "M", "G", "T", "P"]
+
+duration_pattern = re.compile(
+ r"^((?P<Y>[1-9]\d*)Y)?((?P<W>[1-9]\d*)W)?((?P<D>[1-9]\d*)D)?(((?P<H>[1-9]\d*)H)?((?P<M>[1-9]\d*)M)?)?$"
+)
+duration_transformation = {
+ "Y": 365 * 24 * 60,
+ "W": 7 * 24 * 60,
+ "D": 24 * 60,
+ "H": 60,
+ "M": 1,
+}
+
+
+def parse_number_with_metric_suffix(module, number, factor=1024):
+ """Given a human-readable string (e.g. 2G, 30M, 400),
+ return the resolved integer.
+ Will call `module.fail_json()` for invalid inputs.
+ """
+ try:
+ stripped_num = number.strip()
+ if stripped_num[-1].isdigit():
+ return int(stripped_num)
+ # has unit prefix
+ result = float(stripped_num[:-1])
+ suffix = stripped_num[-1].upper()
+ factor_count = METRIC_SUFFIXES.index(suffix) + 1
+ for _i in range(0, factor_count):
+ result = result * float(factor)
+ return int(result)
+ except Exception:
+ module.fail_json(
+ msg="'{0}' is not a valid number, use '400', '1K', '2M', ...".format(number)
+ )
+ return 0
+
+
+def parse_duration(period):
+ if period.isdigit():
+ return int(period)
+
+ match = duration_pattern.match(period.upper())
+ if not match or period == "":
+ raise ValueError("Invalid format")
+
+ durations = {
+ "Y": int(match.group("Y")) if match.group("Y") else 0,
+ "W": int(match.group("W")) if match.group("W") else 0,
+ "D": int(match.group("D")) if match.group("D") else 0,
+ "H": int(match.group("H")) if match.group("H") else 0,
+ "M": int(match.group("M")) if match.group("M") else 0,
+ }
+ return sum(value * duration_transformation[key] for key, value in durations.items())
+
+
+def parse_minutes(module, period):
+ try:
+ return parse_duration(period)
+ except ValueError:
+ module.fail_json(
+ msg=(
+ "'{0}' is not a valid time period, use combination of data units (Y,W,D,H,M)"
+ "e.g. 4W3D5H, 5D8H5M, 3D, 5W, 1Y5W..."
+ ).format(period)
+ )
diff --git a/ansible_collections/purestorage/fusion/plugins/module_utils/prerequisites.py b/ansible_collections/purestorage/fusion/plugins/module_utils/prerequisites.py
new file mode 100644
index 000000000..a4edaf341
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/module_utils/prerequisites.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+import re
+import importlib
+import importlib.metadata
+
+# This file exists because Ansible currently cannot declare dependencies on Python modules.
+# see https://github.com/ansible/ansible/issues/62733 for more info about lack of req support
+
+#############################
+
+# 'module_name, package_name, version_requirements' triplets
+DEPENDENCIES = [
+ ("fusion", "purefusion", ">=1.0.11,<2.0"),
+ ("urllib3", "urllib3", None),
+]
+
+#############################
+
+
+def _parse_version(val):
+ """
+ Parse a package version.
+ Takes in either MAJOR.MINOR or MAJOR.MINOR.PATCH form. PATCH
+ can have additional suffixes, e.g. '-prerelease', 'a1', ...
+
+ :param val: a string representation of the package version
+ :returns: tuple of ints (MAJOR, MINOR, PATCH) or None if not parsed
+ """
+ # regexes for this were really ugly
+ try:
+ parts = val.split(".")
+ if len(parts) < 2 or len(parts) > 3:
+ return None
+ major = int(parts[0])
+ minor = int(parts[1])
+ if len(parts) > 2:
+ patch = re.match(r"^\d+", parts[2])
+ patch = int(patch.group(0))
+ else:
+ patch = None
+ return (major, minor, patch)
+ except Exception:
+ return None
+
+
+# returns list of tuples [(COMPARATOR, (MAJOR, MINOR, PATCH)),...]
+def _parse_version_requirements(val):
+ """
+ Parse package requirements.
+
+ :param val: a string in the form ">=1.0.11,<2.0"
+ :returns: list of tuples in the form [(">=", (1, 0, 11)), ("<", (2, 0, None))] or None if not parsed
+ """
+ reqs = []
+ try:
+ parts = val.split(",")
+ for part in parts:
+ match = re.match(r"\s*(>=|<=|==|=|<|>|!=)\s*([^\s]+)", part)
+ op = match.group(1)
+ ver = match.group(2)
+ ver_tuple = _parse_version(ver)
+ if not ver_tuple:
+ raise ValueError("invalid version {0}".format(ver))
+ reqs.append((op, ver_tuple))
+ return reqs
+ except Exception as e:
+ raise ValueError("invalid version requirement '{0}' {1}".format(val, e))
+
+
+def _compare_version(op, ver, req):
+ """
+ Compare two versions.
+
+ :param op: a string, one of comparators ">=", "<=", "=", "==", ">" or "<"
+ :param ver: version tuple in _parse_version() return form
+ :param req: version tuple in _parse_version() return form
+ :returns: True if ver 'op' req; False otherwise
+ """
+
+ def _cmp(a, b):
+ return (a > b) - (a < b)
+
+ major = _cmp(ver[0], req[0])
+ minor = _cmp(ver[1], req[1])
+ patch = None
+ if req[2] is not None:
+ patch = _cmp(ver[2] or 0, req[2])
+ result = {
+ ">=": major > 0 or (major == 0 and (minor > 0 or patch is None or patch >= 0)),
+ "<=": major < 0 or (major == 0 and (minor < 0 or patch is None or patch <= 0)),
+ ">": major > 0
+ or (major == 0 and (minor > 0 or patch is not None and patch > 0)),
+ "<": major < 0
+ or (major == 0 and (minor < 0 or patch is not None and patch < 0)),
+ "=": major == 0 and minor == 0 and (patch is None or patch == 0),
+ "==": major == 0 and minor == 0 and (patch is None or patch == 0),
+ "!=": major != 0 or minor != 0 or (patch is not None and patch != 0),
+ }.get(op)
+ return result
+
+
+def _version_satisfied(version, requirements):
+ """
+ Checks whether version matches given version requirements.
+
+ :param version: a string, in input form to _parse_version()
+ :param requirements: as string, in input form to _parse_version_requirements()
+ :returns: True if 'version' matches 'requirements'; False otherwise
+ """
+
+ version = _parse_version(version)
+ requirements = _parse_version_requirements(requirements)
+ for req in requirements:
+ if not _compare_version(req[0], version, req[1]):
+ return False
+ return True
+
+
+# poor helper to work around the fact Ansible is unable to manage python dependencies
+def _check_import(ansible_module, module, package=None, version_requirements=None):
+ """
+ Tries to import a module and optionally validates its package version.
+ Calls AnsibleModule.fail_json() if not satisfied.
+
+ :param ansible_module: an AnsibleModule instance
+ :param module: a string with module name to try to import
+ :param package: a string, package to check version for; must be specified with 'version_requirements'
+ :param version_requirements: a string, version requirements for 'package'
+ """
+ try:
+ mod = importlib.import_module(module)
+ except ImportError:
+ ansible_module.fail_json(
+ msg="Error: Python package '{0}' required and missing".format(module)
+ )
+
+ if package and version_requirements:
+ # silently ignore version checks and hope for the best if we can't fetch
+ # the package version since we can't know how the user installs packages
+ try:
+ version = importlib.metadata.version(package)
+ if version and not _version_satisfied(version, version_requirements):
+ ansible_module.fail_json(
+ msg="Error: Python package '{0}' version '{1}' does not satisfy requirements '{2}'".format(
+ module, version, version_requirements
+ )
+ )
+ except Exception:
+ pass # ignore package loads
+
+
+def check_dependencies(ansible_module):
+ for module, package, version_requirements in DEPENDENCIES:
+ _check_import(ansible_module, module, package, version_requirements)
diff --git a/ansible_collections/purestorage/fusion/plugins/module_utils/startup.py b/ansible_collections/purestorage/fusion/plugins/module_utils/startup.py
new file mode 100644
index 000000000..55d7f11a2
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/module_utils/startup.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ install_fusion_exception_hook,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.prerequisites import (
+ check_dependencies,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ get_fusion,
+)
+
+
+def setup_fusion(module):
+ check_dependencies(module)
+ install_fusion_exception_hook(module)
+ return get_fusion(module)
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_api_client.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_api_client.py
new file mode 100644
index 000000000..39860449d
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_api_client.py
@@ -0,0 +1,139 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_api_client
+version_added: '1.0.0'
+short_description: Manage API clients in Pure Storage Fusion
+description:
+- Create or delete an API Client in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the client.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the client should exist or not.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ public_key:
+ description:
+ - The API clients PEM formatted (Base64 encoded) RSA public key.
+ - Include the C(—–BEGIN PUBLIC KEY—–) and C(—–END PUBLIC KEY—–) lines.
+ type: str
+ required: true
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new API client foo
+ purestorage.fusion.fusion_api_client:
+ name: "foo client"
+ public_key: "{{lookup('file', 'public_pem_file') }}"
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+
+
+def get_client_id(module, fusion):
+ """Get API Client ID, or None if not available"""
+ id_api_instance = purefusion.IdentityManagerApi(fusion)
+ try:
+ clients = id_api_instance.list_api_clients()
+ for client in clients:
+ if (
+ client.public_key == module.params["public_key"]
+ and client.display_name == module.params["name"]
+ ):
+ return client.id
+ return None
+ except purefusion.rest.ApiException:
+ return None
+
+
+def delete_client(module, fusion, client_id):
+ """Delete API Client"""
+ id_api_instance = purefusion.IdentityManagerApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ id_api_instance.delete_api_client(api_client_id=client_id)
+ module.exit_json(changed=changed)
+
+
+def create_client(module, fusion):
+ """Create API Client"""
+
+ id_api_instance = purefusion.IdentityManagerApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ client = purefusion.APIClientPost(
+ public_key=module.params["public_key"],
+ display_name=module.params["name"],
+ )
+ id_api_instance.create_api_client(client)
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ public_key=dict(type="str", required=True),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ client_id = get_client_id(module, fusion)
+ if client_id is None and state == "present":
+ create_client(module, fusion)
+ elif client_id is not None and state == "absent":
+ delete_client(module, fusion, client_id)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_array.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_array.py
new file mode 100644
index 000000000..f7933eabe
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_array.py
@@ -0,0 +1,265 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_array
+version_added: '1.0.0'
+short_description: Manage arrays in Pure Storage Fusion
+description:
+- Create or delete an array in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the array.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the array should exist or not.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ display_name:
+ description:
+ - The human name of the array.
+ - If not provided, defaults to I(name).
+ type: str
+ region:
+ description:
+ - The region the AZ is in.
+ type: str
+ required: true
+ availability_zone:
+ aliases: [ az ]
+ description:
+ - The availability zone the array is located in.
+ type: str
+ required: true
+ hardware_type:
+ description:
+ - Hardware type to which the storage class applies.
+ choices: [ flash-array-x, flash-array-c, flash-array-x-optane, flash-array-xl ]
+ type: str
+ host_name:
+ description:
+ - Management IP address of the array, or FQDN.
+ type: str
+ appliance_id:
+ description:
+ - Appliance ID of the array.
+ type: str
+ maintenance_mode:
+ description:
+ - "Switch the array into maintenance mode or back.
+ Array in maintenance mode can have placement groups migrated out but not in.
+ Intended use cases are for example safe decommissioning or to prevent use
+ of an array that has not yet been fully configured."
+ type: bool
+ unavailable_mode:
+ description:
+ - "Switch the array into unavailable mode or back.
+ Fusion tries to exclude unavailable arrays from virtually any operation it
+ can. This is to prevent stalling operations in case of e.g. a networking
+ failure. As of the moment arrays have to be marked unavailable manually."
+ type: bool
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new array foo
+ purestorage.fusion.fusion_array:
+ name: foo
+ az: zone_1
+ region: region1
+ hardware_type: flash-array-x
+ host_name: foo_array
+ display_name: "foo array"
+ appliance_id: 1227571-198887878-35016350232000707
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils import getters
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+
+
+def get_array(module, fusion):
+ """Return Array or None"""
+ return getters.get_array(module, fusion, array_name=module.params["name"])
+
+
+def create_array(module, fusion):
+ """Create Array"""
+
+ array_api_instance = purefusion.ArraysApi(fusion)
+
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ array = purefusion.ArrayPost(
+ hardware_type=module.params["hardware_type"],
+ display_name=display_name,
+ host_name=module.params["host_name"],
+ name=module.params["name"],
+ appliance_id=module.params["appliance_id"],
+ )
+ res = array_api_instance.create_array(
+ array,
+ availability_zone_name=module.params["availability_zone"],
+ region_name=module.params["region"],
+ )
+ await_operation(fusion, res)
+ return True
+
+
+def update_array(module, fusion):
+ """Update Array"""
+ array = get_array(module, fusion)
+ patches = []
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != array.display_name
+ ):
+ patch = purefusion.ArrayPatch(
+ display_name=purefusion.NullableString(module.params["display_name"]),
+ )
+ patches.append(patch)
+
+ if module.params["host_name"] and module.params["host_name"] != array.host_name:
+ patch = purefusion.ArrayPatch(
+ host_name=purefusion.NullableString(module.params["host_name"])
+ )
+ patches.append(patch)
+
+ if (
+ module.params["maintenance_mode"] is not None
+ and module.params["maintenance_mode"] != array.maintenance_mode
+ ):
+ patch = purefusion.ArrayPatch(
+ maintenance_mode=purefusion.NullableBoolean(
+ module.params["maintenance_mode"]
+ )
+ )
+ patches.append(patch)
+ if (
+ module.params["unavailable_mode"] is not None
+ and module.params["unavailable_mode"] != array.unavailable_mode
+ ):
+ patch = purefusion.ArrayPatch(
+ unavailable_mode=purefusion.NullableBoolean(
+ module.params["unavailable_mode"]
+ )
+ )
+ patches.append(patch)
+
+ if not module.check_mode:
+ array_api_instance = purefusion.ArraysApi(fusion)
+ for patch in patches:
+ op = array_api_instance.update_array(
+ patch,
+ availability_zone_name=module.params["availability_zone"],
+ region_name=module.params["region"],
+ array_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ changed = len(patches) != 0
+ return changed
+
+
+def delete_array(module, fusion):
+ """Delete Array - not currently available"""
+ array_api_instance = purefusion.ArraysApi(fusion)
+ if not module.check_mode:
+ res = array_api_instance.delete_array(
+ region_name=module.params["region"],
+ availability_zone_name=module.params["availability_zone"],
+ array_name=module.params["name"],
+ )
+ await_operation(fusion, res)
+ return True
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ availability_zone=dict(type="str", required=True, aliases=["az"]),
+ display_name=dict(type="str"),
+ region=dict(type="str", required=True),
+ appliance_id=dict(type="str"),
+ host_name=dict(type="str"),
+ hardware_type=dict(
+ type="str",
+ choices=[
+ "flash-array-x",
+ "flash-array-c",
+ "flash-array-x-optane",
+ "flash-array-xl",
+ ],
+ ),
+ maintenance_mode=dict(type="bool"),
+ unavailable_mode=dict(type="bool"),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ array = get_array(module, fusion)
+
+ changed = False
+ if not array and state == "present":
+ module.fail_on_missing_params(["hardware_type", "host_name", "appliance_id"])
+ changed = create_array(module, fusion) | update_array(
+ module, fusion
+ ) # update is run to set properties which cannot be set on creation and instead use defaults
+ elif array and state == "present":
+ changed = changed | update_array(module, fusion)
+ elif array and state == "absent":
+ changed = changed | delete_array(module, fusion)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=changed)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_az.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_az.py
new file mode 100644
index 000000000..02647d397
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_az.py
@@ -0,0 +1,162 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_az
+version_added: '1.0.0'
+short_description: Create Availability Zones in Pure Storage Fusion
+description:
+- Manage an Availability Zone in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the Availability Zone.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the Availability Zone should exist or not.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ display_name:
+ description:
+ - The human name of the Availability Zone.
+ - If not provided, defaults to I(name).
+ type: str
+ region:
+ description:
+ - Region within which the AZ is created.
+ type: str
+ required: true
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new AZ foo
+ purestorage.fusion.fusion_az:
+ name: foo
+ display_name: "foo AZ"
+ region: region1
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete AZ foo
+ purestorage.fusion.fusion_az:
+ name: foo
+ state: absent
+ region: region1
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils import getters
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+
+
+def get_az(module, fusion):
+ """Get Availability Zone or None"""
+ return getters.get_az(module, fusion, availability_zone_name=module.params["name"])
+
+
+def delete_az(module, fusion):
+ """Delete Availability Zone"""
+
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ op = az_api_instance.delete_availability_zone(
+ region_name=module.params["region"],
+ availability_zone_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def create_az(module, fusion):
+ """Create Availability Zone"""
+
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+
+ azone = purefusion.AvailabilityZonePost(
+ name=module.params["name"],
+ display_name=display_name,
+ )
+ op = az_api_instance.create_availability_zone(
+ azone, region_name=module.params["region"]
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ region=dict(type="str", required=True),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ azone = get_az(module, fusion)
+
+ if not azone and state == "present":
+ create_az(module, fusion)
+ elif azone and state == "absent":
+ delete_az(module, fusion)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_hap.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_hap.py
new file mode 100644
index 000000000..3f45ea2dd
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_hap.py
@@ -0,0 +1,312 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_hap
+version_added: '1.0.0'
+short_description: Manage host access policies in Pure Storage Fusion
+description:
+- Create or delete host access policies in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+- Setting passwords is not an idempotent action.
+- Only iSCSI transport is currently supported.
+- iSCSI CHAP is not yet supported.
+options:
+ name:
+ description:
+ - The name of the host access policy.
+ type: str
+ required: true
+ display_name:
+ description:
+ - The human name of the host access policy.
+ type: str
+ state:
+ description:
+ - Define whether the host access policy should exist or not.
+ - When removing host access policy all connected volumes must
+ have been previously disconnected.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ wwns:
+ type: list
+ elements: str
+ description:
+ - CURRENTLY NOT SUPPORTED.
+ - List of wwns for the host access policy.
+ iqn:
+ type: str
+ description:
+ - IQN for the host access policy.
+ nqn:
+ type: str
+ description:
+ - CURRENTLY NOT SUPPORTED.
+ - NQN for the host access policy.
+ personality:
+ type: str
+ description:
+ - Define which operating system the host is.
+ default: linux
+ choices: ['linux', 'windows', 'hpux', 'vms', 'aix', 'esxi', 'solaris', 'hitachi-vsp', 'oracle-vm-server']
+ target_user:
+ type: str
+ description:
+ - CURRENTLY NOT SUPPORTED.
+ - Sets the target user name for CHAP authentication.
+ - Required with I(target_password).
+ - To clear the username/password pair use C(clear) as the password.
+ target_password:
+ type: str
+ description:
+ - CURRENTLY NOT SUPPORTED.
+ - Sets the target password for CHAP authentication.
+ - Password length between 12 and 255 characters.
+ - To clear the username/password pair use C(clear) as the password.
+ host_user:
+ type: str
+ description:
+ - CURRENTLY NOT SUPPORTED.
+ - Sets the host user name for CHAP authentication.
+ - Required with I(host_password).
+ - To clear the username/password pair use C(clear) as the password.
+ host_password:
+ type: str
+ description:
+ - CURRENTLY NOT SUPPORTED.
+ - Sets the host password for CHAP authentication.
+ - Password length between 12 and 255 characters.
+ - To clear the username/password pair use C(clear) as the password.
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new AIX host access policy
+ purestorage.fusion.fusion_hap:
+ name: foo
+ personality: aix
+ iqn: "iqn.2005-03.com.RedHat:linux-host1"
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete host access policy
+ purestorage.fusion.fusion_hap:
+ name: foo
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+import re
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def _check_iqn(module, fusion):
+ hap_api_instance = purefusion.HostAccessPoliciesApi(fusion)
+ hosts = hap_api_instance.list_host_access_policies().items
+ for host in hosts:
+ if host.iqn == module.params["iqn"] and host.name != module.params["name"]:
+ module.fail_json(
+ msg="Supplied IQN {0} already used by host access policy {1}".format(
+ module.params["iqn"], host.name
+ )
+ )
+
+
+def get_host(module, fusion):
+ """Return host or None"""
+ hap_api_instance = purefusion.HostAccessPoliciesApi(fusion)
+ try:
+ return hap_api_instance.get_host_access_policy(
+ host_access_policy_name=module.params["name"]
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def create_hap(module, fusion):
+ """Create a new host access policy"""
+ hap_api_instance = purefusion.HostAccessPoliciesApi(fusion)
+ changed = True
+ if not module.check_mode:
+ display_name = module.params["display_name"] or module.params["name"]
+
+ op = hap_api_instance.create_host_access_policy(
+ purefusion.HostAccessPoliciesPost(
+ iqn=module.params["iqn"],
+ personality=module.params["personality"],
+ name=module.params["name"],
+ display_name=display_name,
+ )
+ )
+ await_operation(fusion, op)
+ module.exit_json(changed=changed)
+
+
+def delete_hap(module, fusion):
+ """Delete a Host Access Policy"""
+ hap_api_instance = purefusion.HostAccessPoliciesApi(fusion)
+ changed = True
+ if not module.check_mode:
+ op = hap_api_instance.delete_host_access_policy(
+ host_access_policy_name=module.params["name"]
+ )
+ await_operation(fusion, op)
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ nqn=dict(
+ type="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ iqn=dict(type="str"),
+ wwns=dict(
+ type="list",
+ elements="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ host_password=dict(
+ type="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ no_log=True,
+ ),
+ host_user=dict(
+ type="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ target_password=dict(
+ type="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ no_log=True,
+ ),
+ target_user=dict(
+ type="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ display_name=dict(type="str"),
+ personality=dict(
+ type="str",
+ default="linux",
+ choices=[
+ "linux",
+ "windows",
+ "hpux",
+ "vms",
+ "aix",
+ "esxi",
+ "solaris",
+ "hitachi-vsp",
+ "oracle-vm-server",
+ ],
+ ),
+ )
+ )
+
+ required_if = [["state", "present", ["personality", "iqn"]]]
+
+ module = AnsibleModule(
+ argument_spec,
+ supports_check_mode=True,
+ required_if=required_if,
+ )
+ fusion = setup_fusion(module)
+
+ if module.params["nqn"]:
+ module.warn(
+ "`nqn` parameter is deprecated and will be removed in version 2.0.0"
+ )
+ if module.params["wwns"]:
+ module.warn(
+ "`wwns` parameter is deprecated and will be removed in version 2.0.0"
+ )
+ if module.params["host_password"]:
+ module.warn(
+ "`host_password` parameter is deprecated and will be removed in version 2.0.0"
+ )
+ if module.params["host_user"]:
+ module.warn(
+ "`host_user` parameter is deprecated and will be removed in version 2.0.0"
+ )
+ if module.params["target_password"]:
+ module.warn(
+ "`target_password` parameter is deprecated and will be removed in version 2.0.0"
+ )
+ if module.params["target_user"]:
+ module.warn(
+ "`target_user` parameter is deprecated and will be removed in version 2.0.0"
+ )
+
+ hap_pattern = re.compile("^[a-zA-Z0-9]([a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$")
+ iqn_pattern = re.compile(
+ r"^iqn\.\d{4}-\d{2}((?<!-)\.(?!-)[a-zA-Z0-9\-]+){1,63}(?<!-)(?<!\.)(:(?!:)[^,\s'\"]+)?$"
+ )
+
+ if not hap_pattern.match(module.params["name"]):
+ module.fail_json(
+ msg="Host Access Policy {0} does not conform to naming convention".format(
+ module.params["name"]
+ )
+ )
+
+ if module.params["iqn"] is not None and not iqn_pattern.match(module.params["iqn"]):
+ module.fail_json(
+ msg="IQN {0} is not a valid iSCSI IQN".format(module.params["name"])
+ )
+
+ state = module.params["state"]
+ host = get_host(module, fusion)
+ _check_iqn(module, fusion)
+
+ if host is None and state == "present":
+ create_hap(module, fusion)
+ elif host is not None and state == "absent":
+ delete_hap(module, fusion)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_hw.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_hw.py
new file mode 100644
index 000000000..31d313e9d
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_hw.py
@@ -0,0 +1,88 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_hw
+version_added: '1.0.0'
+deprecated:
+ removed_at_date: "2023-08-09"
+ why: Hardware type cannot be modified in Pure Storage Fusion
+ alternative: there's no alternative as this functionality has never worked before
+short_description: Create hardware types in Pure Storage Fusion
+description:
+- Create a hardware type in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the hardware type.
+ type: str
+ state:
+ description:
+ - Define whether the hardware type should exist or not.
+ - Currently there is no mechanism to delete a hardware type.
+ default: present
+ choices: [ present ]
+ type: str
+ display_name:
+ description:
+ - The human name of the hardware type.
+ - If not provided, defaults to I(name).
+ type: str
+ media_type:
+ description:
+ - Volume size limit in M, G, T or P units.
+ type: str
+ array_type:
+ description:
+ - The array type for the hardware type.
+ choices: [ FA//X, FA//C ]
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+# this module does nothing, thus no example is provided
+EXAMPLES = r"""
+"""
+
+RETURN = r"""
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str"),
+ display_name=dict(type="str"),
+ array_type=dict(type="str", choices=["FA//X", "FA//C"]),
+ media_type=dict(type="str"),
+ state=dict(type="str", default="present", choices=["present"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_info.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_info.py
new file mode 100644
index 000000000..be019d3d2
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_info.py
@@ -0,0 +1,1130 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2021, Simon Dodsley (simon@purestorage.com), Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_info
+version_added: '1.0.0'
+short_description: Collect information from Pure Fusion
+description:
+ - Collect information from a Pure Fusion environment.
+ - By default, the module will collect basic
+ information including counts for arrays, availability_zones, volumes, snapshots
+ . Fleet capacity and data reduction rates are also provided.
+ - Additional information can be collected based on the configured set of arguments.
+author:
+ - Pure Storage ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ gather_subset:
+ description:
+ - When supplied, this argument will define the information to be collected.
+ Possible values for this include all, minimum, roles, users, arrays, hardware_types,
+ volumes, host_access_policies, storage_classes, protection_policies, placement_groups,
+ network_interfaces, availability_zones, network_interface_groups, storage_endpoints,
+ snapshots, regions, storage_services, tenants, tenant_spaces, network_interface_groups and api_clients.
+ type: list
+ elements: str
+ required: false
+ default: minimum
+extends_documentation_fragment:
+ - purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Collect default set of information
+ purestorage.fusion.fusion_info:
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+ register: fusion_info
+
+- name: Show default information
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info']['default'] }}"
+
+- name: Collect all information
+ purestorage.fusion.fusion_info:
+ gather_subset:
+ - all
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Show all information
+ ansible.builtin.debug:
+ msg: "{{ fusion_info['fusion_info'] }}"
+"""
+
+RETURN = r"""
+fusion_info:
+ description: Returns the information collected from Fusion
+ returned: always
+ type: dict
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+import time
+import http
+
+
+def _convert_microseconds(micros):
+ seconds = (micros / 1000) % 60
+ minutes = (micros / (1000 * 60)) % 60
+ hours = (micros / (1000 * 60 * 60)) % 24
+ return seconds, minutes, hours
+
+
+def _api_permission_denied_handler(name):
+ """Return decorator which catches #403 errors"""
+
+ def inner(func):
+ def wrapper(module, fusion, *args, **kwargs):
+ try:
+ return func(module, fusion, *args, **kwargs)
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ module.warn(f"Cannot get [{name} dict], reason: Permission denied")
+ return None
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ return wrapper
+
+ return inner
+
+
+def generate_default_dict(module, fusion):
+ def warning_api_exception(name):
+ module.warn(f"Cannot get {name} in [default dict], reason: Permission denied")
+
+ def warning_argument_none(name, requirement):
+ module.warn(
+ f"Cannot get {name} in [default dict], reason: Required argument `{requirement}` not available."
+ )
+
+ # All values are independent on each other - if getting one value fails, we will show warning and continue.
+ # That's also the reason why there's so many nested for loops repeating all over again.
+ version = None
+ users_num = None
+ protection_policies_num = None
+ host_access_policies_num = None
+ hardware_types_num = None
+ storage_services = None
+ storage_services_num = None
+ tenants = None
+ tenants_num = None
+ regions = None
+ regions_num = None
+ roles = None
+ roles_num = None
+ storage_classes_num = None
+ role_assignments_num = None
+ tenant_spaces_num = None
+ volumes_num = None
+ placement_groups_num = None
+ snapshots_num = None
+ availability_zones_num = None
+ arrays_num = None
+ network_interfaces_num = None
+ network_interface_groups_num = None
+ storage_endpoints_num = None
+
+ try:
+ version = purefusion.DefaultApi(fusion).get_version().version
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("API version")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ users_num = len(purefusion.IdentityManagerApi(fusion).list_users())
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Users")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ protection_policies_num = len(
+ purefusion.ProtectionPoliciesApi(fusion).list_protection_policies().items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Protection Policies")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ host_access_policies_num = len(
+ purefusion.HostAccessPoliciesApi(fusion).list_host_access_policies().items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Host Access Policies")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ hardware_types_num = len(
+ purefusion.HardwareTypesApi(fusion).list_hardware_types().items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Hardware Types")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ storage_services = purefusion.StorageServicesApi(fusion).list_storage_services()
+ storage_services_num = len(storage_services.items)
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Storage Services")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ tenants = purefusion.TenantsApi(fusion).list_tenants()
+ tenants_num = len(tenants.items)
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Tenants")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ regions = purefusion.RegionsApi(fusion).list_regions()
+ regions_num = len(regions.items)
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Regions")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ roles = purefusion.RolesApi(fusion).list_roles()
+ roles_num = len(roles)
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Roles")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ if storage_services is not None:
+ try:
+ storage_class_api_instance = purefusion.StorageClassesApi(fusion)
+ storage_classes_num = sum(
+ len(
+ storage_class_api_instance.list_storage_classes(
+ storage_service_name=storage_service.name
+ ).items
+ )
+ for storage_service in storage_services.items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Storage Classes")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+ else:
+ warning_argument_none("Storage Classes", "storage_services")
+
+ if roles is not None:
+ try:
+ role_assign_api_instance = purefusion.RoleAssignmentsApi(fusion)
+ role_assignments_num = sum(
+ len(role_assign_api_instance.list_role_assignments(role_name=role.name))
+ for role in roles
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Role Assignments")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+ else:
+ warning_argument_none("Role Assignments", "roles")
+
+ if tenants is not None:
+ tenantspace_api_instance = purefusion.TenantSpacesApi(fusion)
+
+ try:
+ tenant_spaces_num = sum(
+ len(
+ tenantspace_api_instance.list_tenant_spaces(
+ tenant_name=tenant.name
+ ).items
+ )
+ for tenant in tenants.items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Tenant Spaces")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ vol_api_instance = purefusion.VolumesApi(fusion)
+ volumes_num = sum(
+ len(
+ vol_api_instance.list_volumes(
+ tenant_name=tenant.name,
+ tenant_space_name=tenant_space.name,
+ ).items
+ )
+ for tenant in tenants.items
+ for tenant_space in tenantspace_api_instance.list_tenant_spaces(
+ tenant_name=tenant.name
+ ).items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Volumes")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ plgrp_api_instance = purefusion.PlacementGroupsApi(fusion)
+ placement_groups_num = sum(
+ len(
+ plgrp_api_instance.list_placement_groups(
+ tenant_name=tenant.name,
+ tenant_space_name=tenant_space.name,
+ ).items
+ )
+ for tenant in tenants.items
+ for tenant_space in tenantspace_api_instance.list_tenant_spaces(
+ tenant_name=tenant.name
+ ).items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Placement Groups")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ snapshot_api_instance = purefusion.SnapshotsApi(fusion)
+ snapshots_num = sum(
+ len(
+ snapshot_api_instance.list_snapshots(
+ tenant_name=tenant.name,
+ tenant_space_name=tenant_space.name,
+ ).items
+ )
+ for tenant in tenants.items
+ for tenant_space in tenantspace_api_instance.list_tenant_spaces(
+ tenant_name=tenant.name
+ ).items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Snapshots")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+ else:
+ warning_argument_none("Tenant Spaces", "tenants")
+ warning_argument_none("Volumes", "tenants")
+ warning_argument_none("Placement Groups", "tenants")
+ warning_argument_none("Snapshots", "tenants")
+
+ if regions is not None:
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+
+ try:
+ availability_zones_num = sum(
+ len(
+ az_api_instance.list_availability_zones(
+ region_name=region.name
+ ).items
+ )
+ for region in regions.items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Availability Zones")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ arrays_api_instance = purefusion.ArraysApi(fusion)
+ arrays_num = sum(
+ len(
+ arrays_api_instance.list_arrays(
+ availability_zone_name=availability_zone.name,
+ region_name=region.name,
+ ).items
+ )
+ for region in regions.items
+ for availability_zone in az_api_instance.list_availability_zones(
+ region_name=region.name
+ ).items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Arrays")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ nig_api_instance = purefusion.NetworkInterfaceGroupsApi(fusion)
+ network_interface_groups_num = sum(
+ len(
+ nig_api_instance.list_network_interface_groups(
+ availability_zone_name=availability_zone.name,
+ region_name=region.name,
+ ).items
+ )
+ for region in regions.items
+ for availability_zone in az_api_instance.list_availability_zones(
+ region_name=region.name
+ ).items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Network Interface Groups")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ send_api_instance = purefusion.StorageEndpointsApi(fusion)
+ storage_endpoints_num = sum(
+ len(
+ send_api_instance.list_storage_endpoints(
+ availability_zone_name=availability_zone.name,
+ region_name=region.name,
+ ).items
+ )
+ for region in regions.items
+ for availability_zone in az_api_instance.list_availability_zones(
+ region_name=region.name
+ ).items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Storage Endpoints")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+
+ try:
+ nic_api_instance = purefusion.NetworkInterfacesApi(fusion)
+ network_interfaces_num = sum(
+ len(
+ nic_api_instance.list_network_interfaces(
+ availability_zone_name=availability_zone.name,
+ region_name=region.name,
+ array_name=array_detail.name,
+ ).items
+ )
+ for region in regions.items
+ for availability_zone in az_api_instance.list_availability_zones(
+ region_name=region.name
+ ).items
+ for array_detail in arrays_api_instance.list_arrays(
+ availability_zone_name=availability_zone.name,
+ region_name=region.name,
+ ).items
+ )
+ except purefusion.rest.ApiException as exc:
+ if exc.status == http.HTTPStatus.FORBIDDEN:
+ warning_api_exception("Network Interfaces")
+ else:
+ # other exceptions will be handled by our exception hook
+ raise exc
+ else:
+ warning_argument_none("Availability Zones", "regions")
+ warning_argument_none("Network Interfaces", "regions")
+ warning_argument_none("Network Interface Groups", "regions")
+ warning_argument_none("Storage Endpoints", "regions")
+ warning_argument_none("Arrays", "regions")
+
+ return {
+ "version": version,
+ "users": users_num,
+ "protection_policies": protection_policies_num,
+ "host_access_policies": host_access_policies_num,
+ "hardware_types": hardware_types_num,
+ "storage_services": storage_services_num,
+ "tenants": tenants_num,
+ "regions": regions_num,
+ "storage_classes": storage_classes_num,
+ "roles": roles_num,
+ "role_assignments": role_assignments_num,
+ "tenant_spaces": tenant_spaces_num,
+ "volumes": volumes_num,
+ "placement_groups": placement_groups_num,
+ "snapshots": snapshots_num,
+ "availability_zones": availability_zones_num,
+ "arrays": arrays_num,
+ "network_interfaces": network_interfaces_num,
+ "network_interface_groups": network_interface_groups_num,
+ "storage_endpoints": storage_endpoints_num,
+ }
+
+
+@_api_permission_denied_handler("network_interfaces")
+def generate_nics_dict(module, fusion):
+ nics_info = {}
+ nic_api_instance = purefusion.NetworkInterfacesApi(fusion)
+ arrays_api_instance = purefusion.ArraysApi(fusion)
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+ regions_api_instance = purefusion.RegionsApi(fusion)
+ regions = regions_api_instance.list_regions()
+ for region in regions.items:
+ azs = az_api_instance.list_availability_zones(region_name=region.name)
+ for az in azs.items:
+ array_details = arrays_api_instance.list_arrays(
+ availability_zone_name=az.name,
+ region_name=region.name,
+ )
+ for array_detail in array_details.items:
+ array_name = az.name + "/" + array_detail.name
+ nics_info[array_name] = {}
+ nics = nic_api_instance.list_network_interfaces(
+ availability_zone_name=az.name,
+ region_name=region.name,
+ array_name=array_detail.name,
+ )
+
+ for nic in nics.items:
+ nics_info[array_name][nic.name] = {
+ "enabled": nic.enabled,
+ "display_name": nic.display_name,
+ "interface_type": nic.interface_type,
+ "services": nic.services,
+ "max_speed": nic.max_speed,
+ "vlan": nic.eth.vlan,
+ "address": nic.eth.address,
+ "mac_address": nic.eth.mac_address,
+ "gateway": nic.eth.gateway,
+ "mtu": nic.eth.mtu,
+ "network_interface_group": nic.network_interface_group.name,
+ "availability_zone": nic.availability_zone.name,
+ }
+ return nics_info
+
+
+@_api_permission_denied_handler("host_access_policies")
+def generate_hap_dict(module, fusion):
+ hap_info = {}
+ api_instance = purefusion.HostAccessPoliciesApi(fusion)
+ hosts = api_instance.list_host_access_policies()
+ for host in hosts.items:
+ name = host.name
+ hap_info[name] = {
+ "personality": host.personality,
+ "display_name": host.display_name,
+ "iqn": host.iqn,
+ }
+ return hap_info
+
+
+@_api_permission_denied_handler("arrays")
+def generate_array_dict(module, fusion):
+ array_info = {}
+ array_api_instance = purefusion.ArraysApi(fusion)
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+ regions_api_instance = purefusion.RegionsApi(fusion)
+ regions = regions_api_instance.list_regions()
+ for region in regions.items:
+ azs = az_api_instance.list_availability_zones(region_name=region.name)
+ for az in azs.items:
+ arrays = array_api_instance.list_arrays(
+ availability_zone_name=az.name,
+ region_name=region.name,
+ )
+ for array in arrays.items:
+ array_name = array.name
+ array_space = array_api_instance.get_array_space(
+ availability_zone_name=az.name,
+ array_name=array_name,
+ region_name=region.name,
+ )
+ array_perf = array_api_instance.get_array_performance(
+ availability_zone_name=az.name,
+ array_name=array_name,
+ region_name=region.name,
+ )
+ array_info[array_name] = {
+ "region": region.name,
+ "availability_zone": az.name,
+ "host_name": array.host_name,
+ "maintenance_mode": array.maintenance_mode,
+ "unavailable_mode": array.unavailable_mode,
+ "display_name": array.display_name,
+ "hardware_type": array.hardware_type.name,
+ "appliance_id": array.appliance_id,
+ "apartment_id": getattr(array, "apartment_id", None),
+ "space": {
+ "total_physical_space": array_space.total_physical_space,
+ },
+ "performance": {
+ "read_bandwidth": array_perf.read_bandwidth,
+ "read_latency_us": array_perf.read_latency_us,
+ "reads_per_sec": array_perf.reads_per_sec,
+ "write_bandwidth": array_perf.write_bandwidth,
+ "write_latency_us": array_perf.write_latency_us,
+ "writes_per_sec": array_perf.writes_per_sec,
+ },
+ }
+ return array_info
+
+
+@_api_permission_denied_handler("placement_groups")
+def generate_pg_dict(module, fusion):
+ pg_info = {}
+ tenant_api_instance = purefusion.TenantsApi(fusion)
+ tenantspace_api_instance = purefusion.TenantSpacesApi(fusion)
+ pg_api_instance = purefusion.PlacementGroupsApi(fusion)
+ tenants = tenant_api_instance.list_tenants()
+ for tenant in tenants.items:
+ tenant_spaces = tenantspace_api_instance.list_tenant_spaces(
+ tenant_name=tenant.name
+ ).items
+ for tenant_space in tenant_spaces:
+ groups = pg_api_instance.list_placement_groups(
+ tenant_name=tenant.name,
+ tenant_space_name=tenant_space.name,
+ )
+ for group in groups.items:
+ group_name = tenant.name + "/" + tenant_space.name + "/" + group.name
+ pg_info[group_name] = {
+ "tenant": group.tenant.name,
+ "display_name": group.display_name,
+ "placement_engine": group.placement_engine,
+ "tenant_space": group.tenant_space.name,
+ "az": group.availability_zone.name,
+ "array": getattr(group.array, "name", None),
+ }
+ return pg_info
+
+
+@_api_permission_denied_handler("tenant_spaces")
+def generate_ts_dict(module, fusion):
+ ts_info = {}
+ tenant_api_instance = purefusion.TenantsApi(fusion)
+ tenantspace_api_instance = purefusion.TenantSpacesApi(fusion)
+ tenants = tenant_api_instance.list_tenants()
+ for tenant in tenants.items:
+ tenant_spaces = tenantspace_api_instance.list_tenant_spaces(
+ tenant_name=tenant.name
+ ).items
+ for tenant_space in tenant_spaces:
+ ts_name = tenant.name + "/" + tenant_space.name
+ ts_info[ts_name] = {
+ "tenant": tenant.name,
+ "display_name": tenant_space.display_name,
+ }
+ return ts_info
+
+
+@_api_permission_denied_handler("protection_policies")
+def generate_pp_dict(module, fusion):
+ pp_info = {}
+ api_instance = purefusion.ProtectionPoliciesApi(fusion)
+ policies = api_instance.list_protection_policies()
+ for policy in policies.items:
+ policy_name = policy.name
+ pp_info[policy_name] = {
+ "objectives": policy.objectives,
+ }
+ return pp_info
+
+
+@_api_permission_denied_handler("tenants")
+def generate_tenant_dict(module, fusion):
+ tenants_api_instance = purefusion.TenantsApi(fusion)
+ return {
+ tenant.name: {
+ "display_name": tenant.display_name,
+ }
+ for tenant in tenants_api_instance.list_tenants().items
+ }
+
+
+@_api_permission_denied_handler("regions")
+def generate_regions_dict(module, fusion):
+ regions_api_instance = purefusion.RegionsApi(fusion)
+ return {
+ region.name: {
+ "display_name": region.display_name,
+ }
+ for region in regions_api_instance.list_regions().items
+ }
+
+
+@_api_permission_denied_handler("availability_zones")
+def generate_zones_dict(module, fusion):
+ zones_info = {}
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+ regions_api_instance = purefusion.RegionsApi(fusion)
+ regions = regions_api_instance.list_regions()
+ for region in regions.items:
+ zones = az_api_instance.list_availability_zones(region_name=region.name)
+ for zone in zones.items:
+ az_name = zone.name
+ zones_info[az_name] = {
+ "display_name": zone.display_name,
+ "region": zone.region.name,
+ }
+ return zones_info
+
+
+@_api_permission_denied_handler("role_assignments")
+def generate_ras_dict(module, fusion):
+ ras_info = {}
+ ras_api_instance = purefusion.RoleAssignmentsApi(fusion)
+ role_api_instance = purefusion.RolesApi(fusion)
+ roles = role_api_instance.list_roles()
+ for role in roles:
+ ras = ras_api_instance.list_role_assignments(role_name=role.name)
+ for assignment in ras:
+ name = assignment.name
+ ras_info[name] = {
+ "display_name": assignment.display_name,
+ "role": assignment.role.name,
+ "scope": assignment.scope.name,
+ }
+ return ras_info
+
+
+@_api_permission_denied_handler("roles")
+def generate_roles_dict(module, fusion):
+ roles_info = {}
+ api_instance = purefusion.RolesApi(fusion)
+ roles = api_instance.list_roles()
+ for role in roles:
+ name = role.name
+ roles_info[name] = {
+ "display_name": role.display_name,
+ "scopes": role.assignable_scopes,
+ }
+ return roles_info
+
+
+@_api_permission_denied_handler("api_clients")
+def generate_api_client_dict(module, fusion):
+ client_info = {}
+ api_instance = purefusion.IdentityManagerApi(fusion)
+ clients = api_instance.list_api_clients()
+ for client in clients:
+ client_info[client.name] = {
+ "display_name": client.display_name,
+ "issuer": client.issuer,
+ "public_key": client.public_key,
+ "creator_id": client.creator_id,
+ "last_key_update": time.strftime(
+ "%a, %d %b %Y %H:%M:%S %Z",
+ time.localtime(client.last_key_update / 1000),
+ ),
+ "last_used": time.strftime(
+ "%a, %d %b %Y %H:%M:%S %Z",
+ time.localtime(client.last_used / 1000),
+ ),
+ }
+ return client_info
+
+
+@_api_permission_denied_handler("users")
+def generate_users_dict(module, fusion):
+ users_info = {}
+ api_instance = purefusion.IdentityManagerApi(fusion)
+ users = api_instance.list_users()
+ for user in users:
+ users_info[user.name] = {
+ "display_name": user.display_name,
+ "email": user.email,
+ "id": user.id,
+ }
+ return users_info
+
+
+@_api_permission_denied_handler("hardware_types")
+def generate_hardware_types_dict(module, fusion):
+ hardware_info = {}
+ api_instance = purefusion.HardwareTypesApi(fusion)
+ hw_types = api_instance.list_hardware_types()
+ for hw_type in hw_types.items:
+ hardware_info[hw_type.name] = {
+ "array_type": hw_type.array_type,
+ "display_name": hw_type.display_name,
+ "media_type": hw_type.media_type,
+ }
+ return hardware_info
+
+
+@_api_permission_denied_handler("storage_classes")
+def generate_sc_dict(module, fusion):
+ sc_info = {}
+ ss_api_instance = purefusion.StorageServicesApi(fusion)
+ sc_api_instance = purefusion.StorageClassesApi(fusion)
+ services = ss_api_instance.list_storage_services()
+ for service in services.items:
+ classes = sc_api_instance.list_storage_classes(
+ storage_service_name=service.name,
+ )
+ for s_class in classes.items:
+ sc_info[s_class.name] = {
+ "bandwidth_limit": getattr(s_class, "bandwidth_limit", None),
+ "iops_limit": getattr(s_class, "iops_limit", None),
+ "size_limit": getattr(s_class, "size_limit", None),
+ "display_name": s_class.display_name,
+ "storage_service": service.name,
+ }
+ return sc_info
+
+
+@_api_permission_denied_handler("storage_services")
+def generate_storserv_dict(module, fusion):
+ ss_dict = {}
+ ss_api_instance = purefusion.StorageServicesApi(fusion)
+ services = ss_api_instance.list_storage_services()
+ for service in services.items:
+ ss_dict[service.name] = {
+ "display_name": service.display_name,
+ "hardware_types": None,
+ }
+ # can be None if we don't have permission to see this
+ if service.hardware_types is not None:
+ ss_dict[service.name]["hardware_types"] = []
+ for hwtype in service.hardware_types:
+ ss_dict[service.name]["hardware_types"].append(hwtype.name)
+ return ss_dict
+
+
+@_api_permission_denied_handler("storage_endpoints")
+def generate_se_dict(module, fusion):
+ se_dict = {}
+ se_api_instance = purefusion.StorageEndpointsApi(fusion)
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+ regions_api_instance = purefusion.RegionsApi(fusion)
+ regions = regions_api_instance.list_regions()
+ for region in regions.items:
+ azs = az_api_instance.list_availability_zones(region_name=region.name)
+ for az in azs.items:
+ endpoints = se_api_instance.list_storage_endpoints(
+ region_name=region.name,
+ availability_zone_name=az.name,
+ )
+ for endpoint in endpoints.items:
+ name = region.name + "/" + az.name + "/" + endpoint.name
+ se_dict[name] = {
+ "display_name": endpoint.display_name,
+ "endpoint_type": endpoint.endpoint_type,
+ "iscsi_interfaces": [],
+ }
+ for iface in endpoint.iscsi.discovery_interfaces:
+ dct = {
+ "address": iface.address,
+ "gateway": iface.gateway,
+ "mtu": iface.mtu,
+ "network_interface_groups": None,
+ }
+ if iface.network_interface_groups is not None:
+ dct["network_interface_groups"] = [
+ nig.name for nig in iface.network_interface_groups
+ ]
+ se_dict[name]["iscsi_interfaces"].append(dct)
+ return se_dict
+
+
+@_api_permission_denied_handler("network_interface_groups")
+def generate_nigs_dict(module, fusion):
+ nigs_dict = {}
+ nig_api_instance = purefusion.NetworkInterfaceGroupsApi(fusion)
+ az_api_instance = purefusion.AvailabilityZonesApi(fusion)
+ regions_api_instance = purefusion.RegionsApi(fusion)
+ regions = regions_api_instance.list_regions()
+ for region in regions.items:
+ azs = az_api_instance.list_availability_zones(region_name=region.name)
+ for az in azs.items:
+ nigs = nig_api_instance.list_network_interface_groups(
+ region_name=region.name,
+ availability_zone_name=az.name,
+ )
+ for nig in nigs.items:
+ name = region.name + "/" + az.name + "/" + nig.name
+ nigs_dict[name] = {
+ "display_name": nig.display_name,
+ "gateway": nig.eth.gateway,
+ "prefix": nig.eth.prefix,
+ "mtu": nig.eth.mtu,
+ }
+ return nigs_dict
+
+
+@_api_permission_denied_handler("snapshots")
+def generate_snap_dicts(module, fusion):
+ snap_dict = {}
+ vsnap_dict = {}
+ tenant_api_instance = purefusion.TenantsApi(fusion)
+ tenantspace_api_instance = purefusion.TenantSpacesApi(fusion)
+ snap_api_instance = purefusion.SnapshotsApi(fusion)
+ vsnap_api_instance = purefusion.VolumeSnapshotsApi(fusion)
+ tenants = tenant_api_instance.list_tenants()
+ for tenant in tenants.items:
+ tenant_spaces = tenantspace_api_instance.list_tenant_spaces(
+ tenant_name=tenant.name
+ ).items
+ for tenant_space in tenant_spaces:
+ snaps = snap_api_instance.list_snapshots(
+ tenant_name=tenant.name,
+ tenant_space_name=tenant_space.name,
+ )
+ for snap in snaps.items:
+ snap_name = tenant.name + "/" + tenant_space.name + "/" + snap.name
+ secs, mins, hours = _convert_microseconds(snap.time_remaining)
+ snap_dict[snap_name] = {
+ "display_name": snap.display_name,
+ "protection_policy": snap.protection_policy,
+ "time_remaining": "{0} hours, {1} mins, {2} secs".format(
+ int(hours), int(mins), int(secs)
+ ),
+ "volume_snapshots_link": snap.volume_snapshots_link,
+ }
+ vsnaps = vsnap_api_instance.list_volume_snapshots(
+ tenant_name=tenant.name,
+ tenant_space_name=tenant_space.name,
+ snapshot_name=snap.name,
+ )
+ for vsnap in vsnaps.items:
+ vsnap_name = (
+ tenant.name
+ + "/"
+ + tenant_space.name
+ + "/"
+ + snap.name
+ + "/"
+ + vsnap.name
+ )
+ secs, mins, hours = _convert_microseconds(vsnap.time_remaining)
+ vsnap_dict[vsnap_name] = {
+ "size": vsnap.size,
+ "display_name": vsnap.display_name,
+ "protection_policy": vsnap.protection_policy,
+ "serial_number": vsnap.serial_number,
+ "created_at": time.strftime(
+ "%a, %d %b %Y %H:%M:%S %Z",
+ time.localtime(vsnap.created_at / 1000),
+ ),
+ "time_remaining": "{0} hours, {1} mins, {2} secs".format(
+ int(hours), int(mins), int(secs)
+ ),
+ "placement_group": vsnap.placement_group.name,
+ }
+ return snap_dict, vsnap_dict
+
+
+@_api_permission_denied_handler("volumes")
+def generate_volumes_dict(module, fusion):
+ volume_info = {}
+
+ tenant_api_instance = purefusion.TenantsApi(fusion)
+ vol_api_instance = purefusion.VolumesApi(fusion)
+ tenant_space_api_instance = purefusion.TenantSpacesApi(fusion)
+
+ tenants = tenant_api_instance.list_tenants()
+ for tenant in tenants.items:
+ tenant_spaces = tenant_space_api_instance.list_tenant_spaces(
+ tenant_name=tenant.name
+ ).items
+ for tenant_space in tenant_spaces:
+ volumes = vol_api_instance.list_volumes(
+ tenant_name=tenant.name,
+ tenant_space_name=tenant_space.name,
+ )
+ for volume in volumes.items:
+ vol_name = tenant.name + "/" + tenant_space.name + "/" + volume.name
+ volume_info[vol_name] = {
+ "tenant": tenant.name,
+ "tenant_space": tenant_space.name,
+ "name": volume.name,
+ "size": volume.size,
+ "display_name": volume.display_name,
+ "placement_group": volume.placement_group.name,
+ "source_volume_snapshot": getattr(
+ volume.source_volume_snapshot, "name", None
+ ),
+ "protection_policy": getattr(
+ volume.protection_policy, "name", None
+ ),
+ "storage_class": volume.storage_class.name,
+ "serial_number": volume.serial_number,
+ "target": {},
+ "array": getattr(volume.array, "name", None),
+ }
+
+ volume_info[vol_name]["target"] = {
+ "iscsi": {
+ "addresses": volume.target.iscsi.addresses,
+ "iqn": volume.target.iscsi.iqn,
+ },
+ "nvme": {
+ "addresses": None,
+ "nqn": None,
+ },
+ "fc": {
+ "addresses": None,
+ "wwns": None,
+ },
+ }
+ return volume_info
+
+
+def main():
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(gather_subset=dict(default="minimum", type="list", elements="str"))
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ # will handle all errors (except #403 which should be handled in code)
+ fusion = setup_fusion(module)
+
+ subset = [test.lower() for test in module.params["gather_subset"]]
+ valid_subsets = (
+ "all",
+ "minimum",
+ "roles",
+ "users",
+ "placements",
+ "arrays",
+ "hardware_types",
+ "volumes",
+ "hosts",
+ "storage_classes",
+ "protection_policies",
+ "placement_groups",
+ "interfaces",
+ "zones",
+ "nigs",
+ "storage_endpoints",
+ "snapshots",
+ "storage_services",
+ "tenants",
+ "tenant_spaces",
+ "network_interface_groups",
+ "api_clients",
+ "availability_zones",
+ "host_access_policies",
+ "network_interfaces",
+ "regions",
+ )
+ for option in subset:
+ if option not in valid_subsets:
+ module.fail_json(
+ msg=f"value gather_subset must be one or more of: {','.join(valid_subsets)}, got: {','.join(subset)}\nvalue {option} is not allowed"
+ )
+
+ info = {}
+
+ if "minimum" in subset or "all" in subset:
+ info["default"] = generate_default_dict(module, fusion)
+ if "hardware_types" in subset or "all" in subset:
+ info["hardware_types"] = generate_hardware_types_dict(module, fusion)
+ if "users" in subset or "all" in subset:
+ info["users"] = generate_users_dict(module, fusion)
+ if "regions" in subset or "all" in subset:
+ info["regions"] = generate_regions_dict(module, fusion)
+ if "availability_zones" in subset or "all" in subset or "zones" in subset:
+ info["availability_zones"] = generate_zones_dict(module, fusion)
+ if "zones" in subset:
+ module.warn(
+ "The 'zones' subset is deprecated and will be removed in the version 2.0.0\nUse 'availability_zones' subset instead."
+ )
+ if "roles" in subset or "all" in subset:
+ info["roles"] = generate_roles_dict(module, fusion)
+ info["role_assignments"] = generate_ras_dict(module, fusion)
+ if "storage_services" in subset or "all" in subset:
+ info["storage_services"] = generate_storserv_dict(module, fusion)
+ if "volumes" in subset or "all" in subset:
+ info["volumes"] = generate_volumes_dict(module, fusion)
+ if "protection_policies" in subset or "all" in subset:
+ info["protection_policies"] = generate_pp_dict(module, fusion)
+ if "placement_groups" in subset or "all" in subset or "placements" in subset:
+ info["placement_groups"] = generate_pg_dict(module, fusion)
+ if "placements" in subset:
+ module.warn(
+ "The 'placements' subset is deprecated and will be removed in the version 1.7.0"
+ )
+ if "storage_classes" in subset or "all" in subset:
+ info["storage_classes"] = generate_sc_dict(module, fusion)
+ if "network_interfaces" in subset or "all" in subset or "interfaces" in subset:
+ info["network_interfaces"] = generate_nics_dict(module, fusion)
+ if "interfaces" in subset:
+ module.warn(
+ "The 'interfaces' subset is deprecated and will be removed in the version 2.0.0\nUse 'network_interfaces' subset instead."
+ )
+ if "host_access_policies" in subset or "all" in subset or "hosts" in subset:
+ info["host_access_policies"] = generate_hap_dict(module, fusion)
+ if "hosts" in subset:
+ module.warn(
+ "The 'hosts' subset is deprecated and will be removed in the version 2.0.0\nUse 'host_access_policies' subset instead."
+ )
+ if "arrays" in subset or "all" in subset:
+ info["arrays"] = generate_array_dict(module, fusion)
+ if "tenants" in subset or "all" in subset:
+ info["tenants"] = generate_tenant_dict(module, fusion)
+ if "tenant_spaces" in subset or "all" in subset:
+ info["tenant_spaces"] = generate_ts_dict(module, fusion)
+ if "storage_endpoints" in subset or "all" in subset:
+ info["storage_endpoints"] = generate_se_dict(module, fusion)
+ if "api_clients" in subset or "all" in subset:
+ info["api_clients"] = generate_api_client_dict(module, fusion)
+ if "network_interface_groups" in subset or "all" in subset or "nigs" in subset:
+ info["network_interface_groups"] = generate_nigs_dict(module, fusion)
+ if "nigs" in subset:
+ module.warn(
+ "The 'nigs' subset is deprecated and will be removed in the version 1.7.0"
+ )
+ if "snapshots" in subset or "all" in subset:
+ snap_dicts = generate_snap_dicts(module, fusion)
+ if snap_dicts is not None:
+ info["snapshots"], info["volume_snapshots"] = snap_dicts
+ else:
+ info["snapshots"], info["volume_snapshots"] = None, None
+
+ module.exit_json(changed=False, fusion_info=info)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_ni.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_ni.py
new file mode 100644
index 000000000..6816ed841
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_ni.py
@@ -0,0 +1,244 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_ni
+version_added: '1.0.0'
+short_description: Manage network interfaces in Pure Storage Fusion
+description:
+- Update parameters of network interfaces in Pure Storage Fusion.
+notes:
+- Supports C(check_mode).
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - The name of the network interface.
+ type: str
+ required: true
+ display_name:
+ description:
+ - The human name of the network interface.
+ - If not provided, defaults to I(name).
+ type: str
+ region:
+ description:
+ - The name of the region the availability zone is in.
+ type: str
+ required: true
+ availability_zone:
+ aliases: [ az ]
+ description:
+ - The name of the availability zone for the network interface.
+ type: str
+ required: true
+ array:
+ description:
+ - The name of the array the network interface belongs to.
+ type: str
+ required: true
+ eth:
+ description:
+ - The IP address associated with the network interface.
+ - IP address must include a CIDR notation.
+ - Only IPv4 is supported at the moment.
+ - Required together with `network_interface_group` parameter.
+ type: str
+ enabled:
+ description:
+ - True if network interface is in use.
+ type: bool
+ network_interface_group:
+ description:
+ - The name of the network interface group this network interface belongs to.
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Patch network interface
+ purestorage.fusion.fusion_ni:
+ name: foo
+ region: us-west
+ availability_zone: bar
+ array: array0
+ eth: 10.21.200.124/24
+ enabled: true
+ network_interface_group: subnet-0
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.getters import (
+ get_array,
+ get_az,
+ get_region,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.networking import (
+ is_valid_network,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_ni(module, fusion):
+ """Get Network Interface or None"""
+ ni_api_instance = purefusion.NetworkInterfacesApi(fusion)
+ try:
+ return ni_api_instance.get_network_interface(
+ region_name=module.params["region"],
+ availability_zone_name=module.params["availability_zone"],
+ array_name=module.params["array"],
+ net_intf_name=module.params["name"],
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def update_ni(module, fusion, ni):
+ """Update Network Interface"""
+ ni_api_instance = purefusion.NetworkInterfacesApi(fusion)
+
+ patches = []
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != ni.display_name
+ ):
+ patch = purefusion.NetworkInterfacePatch(
+ display_name=purefusion.NullableString(module.params["display_name"]),
+ )
+ patches.append(patch)
+
+ if module.params["enabled"] is not None and module.params["enabled"] != ni.enabled:
+ patch = purefusion.NetworkInterfacePatch(
+ enabled=purefusion.NullableBoolean(module.params["enabled"]),
+ )
+ patches.append(patch)
+
+ if (
+ module.params["network_interface_group"]
+ and module.params["network_interface_group"] != ni.network_interface_group
+ ):
+ if module.params["eth"] and module.params["eth"] != ni.eth:
+ patch = purefusion.NetworkInterfacePatch(
+ eth=purefusion.NetworkInterfacePatchEth(
+ purefusion.NullableString(module.params["eth"])
+ ),
+ network_interface_group=purefusion.NullableString(
+ module.params["network_interface_group"]
+ ),
+ )
+ else:
+ patch = purefusion.NetworkInterfacePatch(
+ network_interface_group=purefusion.NullableString(
+ module.params["network_interface_group"]
+ ),
+ )
+ patches.append(patch)
+
+ if not module.check_mode:
+ for patch in patches:
+ op = ni_api_instance.update_network_interface(
+ patch,
+ region_name=module.params["region"],
+ availability_zone_name=module.params["availability_zone"],
+ array_name=module.params["array"],
+ net_intf_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ changed = len(patches) != 0
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ region=dict(type="str", required=True),
+ availability_zone=dict(type="str", required=True, aliases=["az"]),
+ array=dict(type="str", required=True),
+ eth=dict(type="str"),
+ enabled=dict(type="bool"),
+ network_interface_group=dict(type="str"),
+ )
+ )
+
+ required_by = {
+ "eth": "network_interface_group",
+ }
+
+ module = AnsibleModule(
+ argument_spec,
+ supports_check_mode=True,
+ required_by=required_by,
+ )
+
+ fusion = setup_fusion(module)
+
+ if module.params["eth"] and not is_valid_network(module.params["eth"]):
+ module.fail_json(
+ msg="`eth` '{0}' is not a valid address in CIDR notation".format(
+ module.params["eth"]
+ )
+ )
+
+ if not get_region(module, fusion):
+ module.fail_json(
+ msg="Region {0} does not exist.".format(module.params["region"])
+ )
+
+ if not get_az(module, fusion):
+ module.fail_json(
+ msg="Availability Zone {0} does not exist.".format(
+ module.params["availability_zone"]
+ )
+ )
+
+ if not get_array(module, fusion):
+ module.fail_json(msg="Array {0} does not exist.".format(module.params["array"]))
+
+ ni = get_ni(module, fusion)
+ if not ni:
+ module.fail_json(
+ msg="Network Interface {0} does not exist".format(module.params["name"])
+ )
+
+ update_ni(module, fusion, ni)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_nig.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_nig.py
new file mode 100644
index 000000000..d6056fd5a
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_nig.py
@@ -0,0 +1,274 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_nig
+version_added: '1.0.0'
+short_description: Manage Network Interface Groups in Pure Storage Fusion
+description:
+- Create, delete and modify network interface groups in Pure Storage Fusion.
+- Currently this only supports a single tenant subnet per tenant network
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the network interface group.
+ type: str
+ required: true
+ display_name:
+ description:
+ - The human name of the network interface group.
+ - If not provided, defaults to I(name).
+ type: str
+ state:
+ description:
+ - Define whether the network interface group should exist or not.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ availability_zone:
+ aliases: [ az ]
+ description:
+ - The name of the availability zone for the network interface group.
+ type: str
+ required: true
+ region:
+ description:
+ - Region for the network interface group.
+ type: str
+ required: true
+ gateway:
+ description:
+ - "Address of the subnet gateway.
+ Currently must be a valid IPv4 address."
+ type: str
+ mtu:
+ description:
+ - MTU setting for the subnet.
+ default: 1500
+ type: int
+ group_type:
+ description:
+ - The type of network interface group.
+ type: str
+ default: eth
+ choices: [ eth ]
+ prefix:
+ description:
+ - "Network prefix in CIDR notation.
+ Required to create a new network interface group.
+ Currently only IPv4 addresses with subnet mask are supported."
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new network interface group foo in AZ bar
+ purestorage.fusion.fusion_nig:
+ name: foo
+ availability_zone: bar
+ region: region1
+ mtu: 9000
+ gateway: 10.21.200.1
+ prefix: 10.21.200.0/24
+ state: present
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete network interface group foo in AZ bar
+ purestorage.fusion.fusion_nig:
+ name: foo
+ availability_zone: bar
+ region: region1
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.networking import (
+ is_valid_address,
+ is_valid_network,
+ is_address_in_network,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_nig(module, fusion):
+ """Check Network Interface Group"""
+ nig_api_instance = purefusion.NetworkInterfaceGroupsApi(fusion)
+ try:
+ return nig_api_instance.get_network_interface_group(
+ availability_zone_name=module.params["availability_zone"],
+ region_name=module.params["region"],
+ network_interface_group_name=module.params["name"],
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def create_nig(module, fusion):
+ """Create Network Interface Group"""
+
+ nig_api_instance = purefusion.NetworkInterfaceGroupsApi(fusion)
+
+ changed = False
+ if module.params["gateway"] and not is_address_in_network(
+ module.params["gateway"], module.params["prefix"]
+ ):
+ module.fail_json(msg="`gateway` must be an address in subnet `prefix`")
+
+ if not module.check_mode:
+ display_name = module.params["display_name"] or module.params["name"]
+ if module.params["group_type"] == "eth":
+ if module.params["gateway"]:
+ eth = purefusion.NetworkInterfaceGroupEthPost(
+ prefix=module.params["prefix"],
+ gateway=module.params["gateway"],
+ mtu=module.params["mtu"],
+ )
+ else:
+ eth = purefusion.NetworkInterfaceGroupEthPost(
+ prefix=module.params["prefix"],
+ mtu=module.params["mtu"],
+ )
+ nig = purefusion.NetworkInterfaceGroupPost(
+ group_type="eth",
+ eth=eth,
+ name=module.params["name"],
+ display_name=display_name,
+ )
+ op = nig_api_instance.create_network_interface_group(
+ nig,
+ availability_zone_name=module.params["availability_zone"],
+ region_name=module.params["region"],
+ )
+ await_operation(fusion, op)
+ changed = True
+ else:
+ # to prevent future unintended error
+ module.warn(f"group_type={module.params['group_type']} is not implemented")
+
+ module.exit_json(changed=changed)
+
+
+def delete_nig(module, fusion):
+ """Delete Network Interface Group"""
+ changed = True
+ nig_api_instance = purefusion.NetworkInterfaceGroupsApi(fusion)
+ if not module.check_mode:
+ op = nig_api_instance.delete_network_interface_group(
+ availability_zone_name=module.params["availability_zone"],
+ region_name=module.params["region"],
+ network_interface_group_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+ module.exit_json(changed=changed)
+
+
+def update_nig(module, fusion, nig):
+ """Update Network Interface Group"""
+
+ nifg_api_instance = purefusion.NetworkInterfaceGroupsApi(fusion)
+ patches = []
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != nig.display_name
+ ):
+ patch = purefusion.NetworkInterfaceGroupPatch(
+ display_name=purefusion.NullableString(module.params["display_name"]),
+ )
+ patches.append(patch)
+
+ if not module.check_mode:
+ for patch in patches:
+ op = nifg_api_instance.update_network_interface_group(
+ patch,
+ availability_zone_name=module.params["availability_zone"],
+ region_name=module.params["region"],
+ network_interface_group_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ changed = len(patches) != 0
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ availability_zone=dict(type="str", required=True, aliases=["az"]),
+ region=dict(type="str", required=True),
+ prefix=dict(type="str"),
+ gateway=dict(type="str"),
+ mtu=dict(type="int", default=1500),
+ group_type=dict(type="str", default="eth", choices=["eth"]),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ if module.params["prefix"] and not is_valid_network(module.params["prefix"]):
+ module.fail_json(
+ msg="`prefix` '{0}' is not a valid address in CIDR notation".format(
+ module.params["prefix"]
+ )
+ )
+ if module.params["gateway"] and not is_valid_address(module.params["gateway"]):
+ module.fail_json(
+ msg="`gateway` '{0}' is not a valid address".format(
+ module.params["gateway"]
+ )
+ )
+
+ nig = get_nig(module, fusion)
+
+ if state == "present" and not nig:
+ module.fail_on_missing_params(["prefix"])
+ create_nig(module, fusion)
+ elif state == "present" and nig:
+ update_nig(module, fusion, nig)
+ elif state == "absent" and nig:
+ delete_nig(module, fusion)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_pg.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_pg.py
new file mode 100644
index 000000000..57843d896
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_pg.py
@@ -0,0 +1,278 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_pg
+version_added: '1.0.0'
+short_description: Manage placement groups in Pure Storage Fusion
+description:
+- Create, update or delete a placement groups in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the placement group.
+ type: str
+ required: true
+ display_name:
+ description:
+ - The human name of the placement group.
+ - If not provided, defaults to I(name).
+ type: str
+ state:
+ description:
+ - Define whether the placement group should exist or not.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ tenant:
+ description:
+ - The name of the tenant.
+ type: str
+ required: true
+ tenant_space:
+ description:
+ - The name of the tenant space.
+ type: str
+ required: true
+ region:
+ description:
+ - The name of the region the availability zone is in.
+ type: str
+ availability_zone:
+ aliases: [ az ]
+ description:
+ - The name of the availability zone the placement group is in.
+ type: str
+ storage_service:
+ description:
+ - The name of the storage service to create the placement group for.
+ type: str
+ array:
+ description:
+ - "Array to place the placement group to. Changing it (i.e. manual migration)
+ is an elevated operation."
+ type: str
+ placement_engine:
+ description:
+ - For workload placement recommendations from Pure1 Meta, use C(pure1meta).
+ - Please note that this might increase volume creation time.
+ type: str
+ choices: [ heuristics, pure1meta ]
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new placement group named foo
+ purestorage.fusion.fusion_pg:
+ name: foo
+ tenant: test
+ tenant_space: space_1
+ availability_zone: az1
+ region: region1
+ storage_service: storage_service_1
+ state: present
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete placement group foo
+ purestorage.fusion.fusion_pg:
+ name: foo
+ tenant: test
+ tenant_space: space_1
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_pg(module, fusion):
+ """Return Placement Group or None"""
+ pg_api_instance = purefusion.PlacementGroupsApi(fusion)
+ try:
+ return pg_api_instance.get_placement_group(
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["tenant_space"],
+ placement_group_name=module.params["name"],
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def create_pg(module, fusion):
+ """Create Placement Group"""
+
+ pg_api_instance = purefusion.PlacementGroupsApi(fusion)
+
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ group = purefusion.PlacementGroupPost(
+ availability_zone=module.params["availability_zone"],
+ name=module.params["name"],
+ display_name=display_name,
+ region=module.params["region"],
+ storage_service=module.params["storage_service"],
+ )
+ op = pg_api_instance.create_placement_group(
+ group,
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["tenant_space"],
+ )
+ await_operation(fusion, op)
+
+ return True
+
+
+def update_display_name(module, fusion, patches, pg):
+ if not module.params["display_name"]:
+ return
+ if module.params["display_name"] == pg.display_name:
+ return
+ patch = purefusion.PlacementGroupPatch(
+ display_name=purefusion.NullableString(module.params["display_name"]),
+ )
+ patches.append(patch)
+
+
+def update_array(module, fusion, patches, pg):
+ if not module.params["array"]:
+ return
+ if not pg.array:
+ module.warn(
+ "cannot see placement group array, probably missing required permissions to change it"
+ )
+ return
+ if pg.array.name == module.params["array"]:
+ return
+
+ patch = purefusion.PlacementGroupPatch(
+ array=purefusion.NullableString(module.params["array"]),
+ )
+ patches.append(patch)
+
+
+def update_pg(module, fusion, pg):
+ """Update Placement Group"""
+
+ pg_api_instance = purefusion.PlacementGroupsApi(fusion)
+ patches = []
+
+ update_display_name(module, fusion, patches, pg)
+ update_array(module, fusion, patches, pg)
+
+ if not module.check_mode:
+ for patch in patches:
+ op = pg_api_instance.update_placement_group(
+ patch,
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["tenant_space"],
+ placement_group_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ changed = len(patches) != 0
+ return changed
+
+
+def delete_pg(module, fusion):
+ """Delete Placement Group"""
+ pg_api_instance = purefusion.PlacementGroupsApi(fusion)
+ if not module.check_mode:
+ op = pg_api_instance.delete_placement_group(
+ placement_group_name=module.params["name"],
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["tenant_space"],
+ )
+ await_operation(fusion, op)
+
+ return True
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ tenant=dict(type="str", required=True),
+ tenant_space=dict(type="str", required=True),
+ region=dict(type="str"),
+ availability_zone=dict(type="str", aliases=["az"]),
+ storage_service=dict(type="str"),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ array=dict(type="str"),
+ placement_engine=dict(
+ type="str",
+ choices=["heuristics", "pure1meta"],
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ if module.params["placement_engine"]:
+ module.warn("placement_engine parameter will be deprecated in version 2.0.0")
+
+ changed = False
+
+ state = module.params["state"]
+ pgroup = get_pg(module, fusion)
+
+ if state == "present" and not pgroup:
+ module.fail_on_missing_params(
+ ["region", "availability_zone", "storage_service"]
+ )
+ changed = create_pg(module, fusion) or changed
+ if module.params["array"]:
+ # changing placement requires additional update
+ pgroup = get_pg(module, fusion)
+ changed = update_pg(module, fusion, pgroup) or changed
+ elif state == "present" and pgroup:
+ changed = update_pg(module, fusion, pgroup) or changed
+ elif state == "absent" and pgroup:
+ changed = delete_pg(module, fusion) or changed
+
+ module.exit_json(changed=changed)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_pp.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_pp.py
new file mode 100644
index 000000000..abce9195c
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_pp.py
@@ -0,0 +1,187 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_pp
+version_added: '1.0.0'
+short_description: Manage protection policies in Pure Storage Fusion
+description:
+- Manage protection policies in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the protection policy.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the protection policy should exist or not.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ display_name:
+ description:
+ - The human name of the protection policy.
+ - If not provided, defaults to I(name).
+ type: str
+ local_rpo:
+ description:
+ - Recovery Point Objective for snapshots.
+ - Value should be specified in minutes.
+ - Minimum value is 10 minutes.
+ type: str
+ local_retention:
+ description:
+ - Retention Duration for periodic snapshots.
+ - Minimum value is 10 minutes.
+ - Value can be provided as m(inutes), h(ours),
+ d(ays), w(eeks), or y(ears).
+ - If no unit is provided, minutes are assumed.
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new protection policy foo
+ purestorage.fusion.fusion_pp:
+ name: foo
+ local_rpo: 10
+ local_retention: 4d
+ display_name: "foo pp"
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete protection policy foo
+ purestorage.fusion.fusion_pp:
+ name: foo
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.parsing import (
+ parse_minutes,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_pp(module, fusion):
+ """Return Protection Policy or None"""
+ pp_api_instance = purefusion.ProtectionPoliciesApi(fusion)
+ try:
+ return pp_api_instance.get_protection_policy(
+ protection_policy_name=module.params["name"]
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def create_pp(module, fusion):
+ """Create Protection Policy"""
+
+ pp_api_instance = purefusion.ProtectionPoliciesApi(fusion)
+ local_rpo = parse_minutes(module, module.params["local_rpo"])
+ local_retention = parse_minutes(module, module.params["local_retention"])
+ if local_retention < 1:
+ module.fail_json(msg="Local Retention must be a minimum of 1 minutes")
+ if local_rpo < 10:
+ module.fail_json(msg="Local RPO must be a minimum of 10 minutes")
+ changed = True
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ op = pp_api_instance.create_protection_policy(
+ purefusion.ProtectionPolicyPost(
+ name=module.params["name"],
+ display_name=display_name,
+ objectives=[
+ purefusion.RPO(type="RPO", rpo="PT" + str(local_rpo) + "M"),
+ purefusion.Retention(
+ type="Retention", after="PT" + str(local_retention) + "M"
+ ),
+ ],
+ )
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def delete_pp(module, fusion):
+ """Delete Protection Policy"""
+ pp_api_instance = purefusion.ProtectionPoliciesApi(fusion)
+ changed = True
+ if not module.check_mode:
+ op = pp_api_instance.delete_protection_policy(
+ protection_policy_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ local_rpo=dict(type="str"),
+ local_retention=dict(type="str"),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ policy = get_pp(module, fusion)
+
+ if not policy and state == "present":
+ module.fail_on_missing_params(["local_rpo", "local_retention"])
+ create_pp(module, fusion)
+ elif policy and state == "absent":
+ delete_pp(module, fusion)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_ra.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_ra.py
new file mode 100644
index 000000000..7cfc7d866
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_ra.py
@@ -0,0 +1,281 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_ra
+version_added: '1.0.0'
+short_description: Manage role assignments in Pure Storage Fusion
+description:
+- Create or delete a storage class in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ role:
+ description:
+ - The name of the role to be assigned/unassigned.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the role assingment should exist or not.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ user:
+ description:
+ - The username to assign the role to.
+ - Currently this only supports the Pure1 App ID.
+ - This should be provide in the same format as I(issuer_id).
+ type: str
+ principal:
+ description:
+ - The unique ID of the principal (User or API Client) to assign to the role.
+ type: str
+ api_client_key:
+ description:
+ - The key of API client to assign the role to.
+ type: str
+ scope:
+ description:
+ - The level to which the role is assigned.
+ choices: [ organization, tenant, tenant_space ]
+ default: organization
+ type: str
+ tenant:
+ description:
+ - The name of the tenant the user has the role applied to.
+ - Must be provided if I(scope) is set to either C(tenant) or C(tenant_space).
+ type: str
+ tenant_space:
+ description:
+ - The name of the tenant_space the user has the role applied to.
+ - Must be provided if I(scope) is set to C(tenant_space).
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Assign role foo to user in tenant bar
+ purestorage.fusion.fusion_ra:
+ name: foo
+ user: key_name
+ tenant: bar
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete role foo from user in tenant bar
+ purestorage.fusion.fusion_ra:
+ name: foo
+ user: key_name
+ tenant: bar
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+
+
+def get_principal(module, fusion):
+ if module.params["principal"]:
+ return module.params["principal"]
+ if module.params["user"]:
+ principal = user_to_principal(fusion, module.params["user"])
+ if not principal:
+ module.fail_json(
+ msg="User {0} does not exist".format(module.params["user"])
+ )
+ return principal
+ if module.params["api_client_key"]:
+ principal = apiclient_to_principal(fusion, module.params["api_client_key"])
+ if not principal:
+ module.fail_json(
+ msg="API Client with key {0} does not exist".format(
+ module.params["api_client_key"]
+ )
+ )
+ return principal
+
+
+def user_to_principal(fusion, user_id):
+ """Given a human readable Fusion user, such as a Pure 1 App ID
+ return the associated principal
+ """
+ id_api_instance = purefusion.IdentityManagerApi(fusion)
+ users = id_api_instance.list_users()
+ for user in users:
+ if user.name == user_id:
+ return user.id
+ return None
+
+
+def apiclient_to_principal(fusion, api_client_key):
+ """Given an API client key, such as "pure1:apikey:123xXxyYyzYzASDF" (also known as issuer_id),
+ return the associated principal
+ """
+ id_api_instance = purefusion.IdentityManagerApi(fusion)
+ api_clients = id_api_instance.list_users(name=api_client_key)
+ if len(api_clients) > 0:
+ return api_clients[0].id
+ return None
+
+
+def get_scope(params):
+ """Given a scope type and associated tenant
+ and tenant_space, return the scope_link
+ """
+ scope_link = None
+ if params["scope"] == "organization":
+ scope_link = "/"
+ elif params["scope"] == "tenant":
+ scope_link = "/tenants/" + params["tenant"]
+ elif params["scope"] == "tenant_space":
+ scope_link = (
+ "/tenants/" + params["tenant"] + "/tenant-spaces/" + params["tenant_space"]
+ )
+ return scope_link
+
+
+def get_ra(module, fusion):
+ """Return Role Assignment or None"""
+ ra_api_instance = purefusion.RoleAssignmentsApi(fusion)
+ try:
+ principal = get_principal(module, fusion)
+ assignments = ra_api_instance.list_role_assignments(
+ role_name=module.params["role"],
+ principal=principal,
+ )
+ for assign in assignments:
+ scope = get_scope(module.params)
+ if assign.scope.self_link == scope:
+ return assign
+ return None
+ except purefusion.rest.ApiException:
+ return None
+
+
+def create_ra(module, fusion):
+ """Create Role Assignment"""
+
+ ra_api_instance = purefusion.RoleAssignmentsApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ principal = get_principal(module, fusion)
+ scope = get_scope(module.params)
+ assignment = purefusion.RoleAssignmentPost(scope=scope, principal=principal)
+ op = ra_api_instance.create_role_assignment(
+ assignment, role_name=module.params["role"]
+ )
+ await_operation(fusion, op)
+ module.exit_json(changed=changed)
+
+
+def delete_ra(module, fusion):
+ """Delete Role Assignment"""
+ changed = True
+ ra_api_instance = purefusion.RoleAssignmentsApi(fusion)
+ if not module.check_mode:
+ ra_name = get_ra(module, fusion).name
+ op = ra_api_instance.delete_role_assignment(
+ role_name=module.params["role"], role_assignment_name=ra_name
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ api_client_key=dict(type="str", no_log=True),
+ principal=dict(type="str"),
+ role=dict(
+ type="str",
+ required=True,
+ deprecated_aliases=[
+ dict(
+ name="name",
+ date="2023-07-26",
+ collection_name="purefusion.fusion",
+ )
+ ],
+ ),
+ scope=dict(
+ type="str",
+ default="organization",
+ choices=["organization", "tenant", "tenant_space"],
+ ),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ tenant=dict(type="str"),
+ tenant_space=dict(type="str"),
+ user=dict(type="str"),
+ )
+ )
+
+ required_if = [
+ ["scope", "tenant", ["tenant"]],
+ ["scope", "tenant_space", ["tenant", "tenant_space"]],
+ ]
+ mutually_exclusive = [
+ ("user", "principal", "api_client_key"),
+ ]
+ required_one_of = [
+ ("user", "principal", "api_client_key"),
+ ]
+
+ module = AnsibleModule(
+ argument_spec,
+ required_if=required_if,
+ supports_check_mode=True,
+ mutually_exclusive=mutually_exclusive,
+ required_one_of=required_one_of,
+ )
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ role_assignment = get_ra(module, fusion)
+
+ if not role_assignment and state == "present":
+ create_ra(module, fusion)
+ elif role_assignment and state == "absent":
+ delete_ra(module, fusion)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_region.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_region.py
new file mode 100644
index 000000000..fbcbff4b0
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_region.py
@@ -0,0 +1,180 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_region
+version_added: '1.1.0'
+short_description: Manage Regions in Pure Storage Fusion
+description:
+- Manage regions in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the Region.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the Region should exist or not.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ display_name:
+ description:
+ - The human name of the Region.
+ - If not provided, defaults to I(name).
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new region foo
+ purestorage.fusion.fusion_region:
+ name: foo
+ display_name: "foo Region"
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Update region foo
+ purestorage.fusion.fusion_region:
+ name: foo
+ display_name: "new foo Region"
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete region foo
+ purestorage.fusion.fusion_region:
+ name: foo
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils import getters
+
+
+def get_region(module, fusion):
+ """Get Region or None"""
+ return getters.get_region(module, fusion, module.params["name"])
+
+
+def create_region(module, fusion):
+ """Create Region"""
+
+ reg_api_instance = purefusion.RegionsApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ region = purefusion.RegionPost(
+ name=module.params["name"],
+ display_name=display_name,
+ )
+ op = reg_api_instance.create_region(region)
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def delete_region(module, fusion):
+ """Delete Region"""
+
+ reg_api_instance = purefusion.RegionsApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ op = reg_api_instance.delete_region(region_name=module.params["name"])
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def update_region(module, fusion, region):
+ """Update Region settings"""
+ changed = False
+ reg_api_instance = purefusion.RegionsApi(fusion)
+
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != region.display_name
+ ):
+ changed = True
+ if not module.check_mode:
+ reg = purefusion.RegionPatch(
+ display_name=purefusion.NullableString(module.params["display_name"])
+ )
+ op = reg_api_instance.update_region(
+ reg,
+ region_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ region = get_region(module, fusion)
+
+ if not region and state == "present":
+ create_region(module, fusion)
+ elif region and state == "present":
+ update_region(module, fusion, region)
+ elif region and state == "absent":
+ delete_region(module, fusion)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_sc.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_sc.py
new file mode 100644
index 000000000..2327b8d48
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_sc.py
@@ -0,0 +1,255 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_sc
+version_added: '1.0.0'
+short_description: Manage storage classes in Pure Storage Fusion
+description:
+- Manage a storage class in Pure Storage Fusion.
+notes:
+- Supports C(check_mode).
+- It is not currently possible to update bw_limit or
+ iops_limit after a storage class has been created.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - The name of the storage class.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the storage class should exist or not.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ display_name:
+ description:
+ - The human name of the storage class.
+ - If not provided, defaults to I(name).
+ type: str
+ size_limit:
+ description:
+ - Volume size limit in M, G, T or P units.
+ - Must be between 1MB and 4PB.
+ - If not provided at creation, this will default to 4PB.
+ type: str
+ bw_limit:
+ description:
+ - The bandwidth limit in M or G units.
+ M will set MB/s.
+ G will set GB/s.
+ - Must be between 1MB/s and 512GB/s.
+ - If not provided at creation, this will default to 512GB/s.
+ type: str
+ iops_limit:
+ description:
+ - The IOPs limit - use value or K or M.
+ K will mean 1000.
+ M will mean 1000000.
+ - Must be between 100 and 100000000.
+ - If not provided at creation, this will default to 100000000.
+ type: str
+ storage_service:
+ description:
+ - Storage service to which the storage class belongs.
+ type: str
+ required: true
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new storage class foo
+ purestorage.fusion.fusion_sc:
+ name: foo
+ size_limit: 100G
+ iops_limit: 100000
+ bw_limit: 25M
+ storage_service: service1
+ display_name: "test class"
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Update storage class (only display_name change is supported)
+ purestorage.fusion.fusion_sc:
+ name: foo
+ display_name: "main class"
+ storage_service: service1
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete storage class
+ purestorage.fusion.fusion_sc:
+ name: foo
+ storage_service: service1
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.parsing import (
+ parse_number_with_metric_suffix,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_sc(module, fusion):
+ """Return Storage Class or None"""
+ sc_api_instance = purefusion.StorageClassesApi(fusion)
+ try:
+ return sc_api_instance.get_storage_class(
+ storage_class_name=module.params["name"],
+ storage_service_name=module.params["storage_service"],
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def create_sc(module, fusion):
+ """Create Storage Class"""
+
+ sc_api_instance = purefusion.StorageClassesApi(fusion)
+
+ if not module.params["size_limit"]:
+ module.params["size_limit"] = "4P"
+ if not module.params["iops_limit"]:
+ module.params["iops_limit"] = "100000000"
+ if not module.params["bw_limit"]:
+ module.params["bw_limit"] = "512G"
+ size_limit = parse_number_with_metric_suffix(module, module.params["size_limit"])
+ iops_limit = int(
+ parse_number_with_metric_suffix(
+ module, module.params["iops_limit"], factor=1000
+ )
+ )
+ bw_limit = parse_number_with_metric_suffix(module, module.params["bw_limit"])
+ if bw_limit < 1048576 or bw_limit > 549755813888: # 1MB/s to 512GB/s
+ module.fail_json(msg="Bandwidth limit is not within the required range")
+ if iops_limit < 100 or iops_limit > 100_000_000:
+ module.fail_json(msg="IOPs limit is not within the required range")
+ if size_limit < 1048576 or size_limit > 4503599627370496: # 1MB to 4PB
+ module.fail_json(msg="Size limit is not within the required range")
+
+ changed = True
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ s_class = purefusion.StorageClassPost(
+ name=module.params["name"],
+ size_limit=size_limit,
+ iops_limit=iops_limit,
+ bandwidth_limit=bw_limit,
+ display_name=display_name,
+ )
+ op = sc_api_instance.create_storage_class(
+ s_class, storage_service_name=module.params["storage_service"]
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def update_sc(module, fusion, s_class):
+ """Update Storage Class settings"""
+ changed = False
+ sc_api_instance = purefusion.StorageClassesApi(fusion)
+
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != s_class.display_name
+ ):
+ changed = True
+ if not module.check_mode:
+ sclass = purefusion.StorageClassPatch(
+ display_name=purefusion.NullableString(module.params["display_name"])
+ )
+ op = sc_api_instance.update_storage_class(
+ sclass,
+ storage_service_name=module.params["storage_service"],
+ storage_class_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def delete_sc(module, fusion):
+ """Delete Storage Class"""
+ sc_api_instance = purefusion.StorageClassesApi(fusion)
+ changed = True
+ if not module.check_mode:
+ op = sc_api_instance.delete_storage_class(
+ storage_class_name=module.params["name"],
+ storage_service_name=module.params["storage_service"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ iops_limit=dict(type="str"),
+ bw_limit=dict(type="str"),
+ size_limit=dict(type="str"),
+ storage_service=dict(type="str", required=True),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ s_class = get_sc(module, fusion)
+
+ if not s_class and state == "present":
+ create_sc(module, fusion)
+ elif s_class and state == "present":
+ update_sc(module, fusion, s_class)
+ elif s_class and state == "absent":
+ delete_sc(module, fusion)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_se.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_se.py
new file mode 100644
index 000000000..9eed4bea0
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_se.py
@@ -0,0 +1,507 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Simon Dodsley (simon@purestorage.com), Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_se
+version_added: '1.0.0'
+short_description: Manage storage endpoints in Pure Storage Fusion
+description:
+- Create or delete storage endpoints in Pure Storage Fusion.
+notes:
+- Supports C(check_mode).
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - The name of the storage endpoint.
+ type: str
+ required: true
+ display_name:
+ description:
+ - The human name of the storage endpoint.
+ - If not provided, defaults to I(name).
+ type: str
+ state:
+ description:
+ - Define whether the storage endpoint should exist or not.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ region:
+ description:
+ - The name of the region the availability zone is in
+ type: str
+ required: true
+ availability_zone:
+ aliases: [ az ]
+ description:
+ - The name of the availability zone for the storage endpoint.
+ type: str
+ required: true
+ endpoint_type:
+ description:
+ - "DEPRECATED: Will be removed in version 2.0.0"
+ - Type of the storage endpoint. Only iSCSI is available at the moment.
+ type: str
+ iscsi:
+ description:
+ - List of discovery interfaces.
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ description:
+ - IP address to be used in the subnet of the storage endpoint.
+ - IP address must include a CIDR notation.
+ - Only IPv4 is supported at the moment.
+ type: str
+ gateway:
+ description:
+ - Address of the subnet gateway.
+ type: str
+ network_interface_groups:
+ description:
+ - List of network interface groups to assign to the address.
+ type: list
+ elements: str
+ cbs_azure_iscsi:
+ description:
+ - CBS Azure iSCSI
+ type: dict
+ suboptions:
+ storage_endpoint_collection_identity:
+ description:
+ - The Storage Endpoint Collection Identity which belongs to the Azure entities.
+ type: str
+ load_balancer:
+ description:
+ - The Load Balancer id which gives permissions to CBS array applications to modify the Load Balancer.
+ type: str
+ load_balancer_addresses:
+ description:
+ - The IPv4 addresses of the Load Balancer.
+ type: list
+ elements: str
+ network_interface_groups:
+ description:
+ - "DEPRECATED: Will be removed in version 2.0.0"
+ - List of network interface groups to assign to the storage endpoints.
+ type: list
+ elements: str
+ addresses:
+ description:
+ - "DEPRECATED: Will be removed in version 2.0.0"
+ - List of IP addresses to be used in the subnet of the storage endpoint.
+ - IP addresses must include a CIDR notation.
+ - Only IPv4 is supported at the moment.
+ type: list
+ elements: str
+ gateway:
+ description:
+ - "DEPRECATED: Will be removed in version 2.0.0"
+ - Address of the subnet gateway.
+ - Currently this must be provided.
+ type: str
+
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new storage endpoint foo in AZ bar
+ purestorage.fusion.fusion_se:
+ name: foo
+ availability_zone: bar
+ region: us-west
+ iscsi:
+ - address: 10.21.200.124/24
+ gateway: 10.21.200.1
+ network_interface_groups:
+ - subnet-0
+ - address: 10.21.200.36/24
+ gateway: 10.21.200.2
+ network_interface_groups:
+ - subnet-0
+ - subnet-1
+ state: present
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Create new CBS storage endpoint foo in AZ bar
+ purestorage.fusion.fusion_se:
+ name: foo
+ availability_zone: bar
+ region: us-west
+ cbs_azure_iscsi:
+ storage_endpoint_collection_identity: "/subscriptions/sub/resourcegroups/sec/providers/ms/userAssignedIdentities/secId"
+ load_balancer: "/subscriptions/sub/resourcegroups/sec/providers/ms/loadBalancers/sec-lb"
+ load_balancer_addresses:
+ - 10.21.200.1
+ - 10.21.200.2
+ state: present
+ app_id: key_name
+ key_file: "az-admin-private-key.pem"
+
+- name: Delete storage endpoint foo in AZ bar
+ purestorage.fusion.fusion_se:
+ name: foo
+ availability_zone: bar
+ region: us-west
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: (DEPRECATED) Create new storage endpoint foo in AZ bar
+ purestorage.fusion.fusion_se:
+ name: foo
+ availability_zone: bar
+ gateway: 10.21.200.1
+ region: us-west
+ addresses:
+ - 10.21.200.124/24
+ - 10.21.200.36/24
+ network_interface_groups:
+ - subnet-0
+ state: present
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.networking import (
+ is_valid_network,
+ is_valid_address,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+#######################################################################
+# DEPRECATED CODE SECTION STARTS
+
+
+def create_se_old(module, fusion):
+ """Create Storage Endpoint"""
+
+ se_api_instance = purefusion.StorageEndpointsApi(fusion)
+
+ changed = True
+
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ ifaces = []
+ for address in module.params["addresses"]:
+ if module.params["gateway"]:
+ iface = purefusion.StorageEndpointIscsiDiscoveryInterfacePost(
+ address=address,
+ gateway=module.params["gateway"],
+ network_interface_groups=module.params["network_interface_groups"],
+ )
+ else:
+ iface = purefusion.StorageEndpointIscsiDiscoveryInterfacePost(
+ address=address,
+ network_interface_groups=module.params["network_interface_groups"],
+ )
+ ifaces.append(iface)
+ op = purefusion.StorageEndpointPost(
+ endpoint_type="iscsi",
+ iscsi=purefusion.StorageEndpointIscsiPost(
+ discovery_interfaces=ifaces,
+ ),
+ name=module.params["name"],
+ display_name=display_name,
+ )
+ op = se_api_instance.create_storage_endpoint(
+ op,
+ region_name=module.params["region"],
+ availability_zone_name=module.params["availability_zone"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+# DEPRECATED CODE SECTION ENDS
+#######################################################################
+
+
+def get_se(module, fusion):
+ """Storage Endpoint or None"""
+ se_api_instance = purefusion.StorageEndpointsApi(fusion)
+ try:
+ return se_api_instance.get_storage_endpoint(
+ region_name=module.params["region"],
+ storage_endpoint_name=module.params["name"],
+ availability_zone_name=module.params["availability_zone"],
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def create_se(module, fusion):
+ """Create Storage Endpoint"""
+ se_api_instance = purefusion.StorageEndpointsApi(fusion)
+
+ if not module.check_mode:
+ endpoint_type = None
+
+ iscsi = None
+ if module.params["iscsi"] is not None:
+ iscsi = purefusion.StorageEndpointIscsiPost(
+ discovery_interfaces=[
+ purefusion.StorageEndpointIscsiDiscoveryInterfacePost(**endpoint)
+ for endpoint in module.params["iscsi"]
+ ]
+ )
+ endpoint_type = "iscsi"
+
+ cbs_azure_iscsi = None
+ if module.params["cbs_azure_iscsi"] is not None:
+ cbs_azure_iscsi = purefusion.StorageEndpointCbsAzureIscsiPost(
+ storage_endpoint_collection_identity=module.params["cbs_azure_iscsi"][
+ "storage_endpoint_collection_identity"
+ ],
+ load_balancer=module.params["cbs_azure_iscsi"]["load_balancer"],
+ load_balancer_addresses=module.params["cbs_azure_iscsi"][
+ "load_balancer_addresses"
+ ],
+ )
+ endpoint_type = "cbs-azure-iscsi"
+
+ op = se_api_instance.create_storage_endpoint(
+ purefusion.StorageEndpointPost(
+ name=module.params["name"],
+ display_name=module.params["display_name"] or module.params["name"],
+ endpoint_type=endpoint_type,
+ iscsi=iscsi,
+ cbs_azure_iscsi=cbs_azure_iscsi,
+ ),
+ region_name=module.params["region"],
+ availability_zone_name=module.params["availability_zone"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=True)
+
+
+def delete_se(module, fusion):
+ """Delete Storage Endpoint"""
+ se_api_instance = purefusion.StorageEndpointsApi(fusion)
+ if not module.check_mode:
+ op = se_api_instance.delete_storage_endpoint(
+ region_name=module.params["region"],
+ availability_zone_name=module.params["availability_zone"],
+ storage_endpoint_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+ module.exit_json(changed=True)
+
+
+def update_se(module, fusion, se):
+ """Update Storage Endpoint"""
+
+ se_api_instance = purefusion.StorageEndpointsApi(fusion)
+ patches = []
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != se.display_name
+ ):
+ patch = purefusion.StorageEndpointPatch(
+ display_name=purefusion.NullableString(module.params["display_name"]),
+ )
+ patches.append(patch)
+
+ if not module.check_mode:
+ for patch in patches:
+ op = se_api_instance.update_storage_endpoint(
+ patch,
+ region_name=module.params["region"],
+ availability_zone_name=module.params["availability_zone"],
+ storage_endpoint_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ changed = len(patches) != 0
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ region=dict(type="str", required=True),
+ availability_zone=dict(type="str", required=True, aliases=["az"]),
+ iscsi=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ address=dict(type="str"),
+ gateway=dict(type="str"),
+ network_interface_groups=dict(type="list", elements="str"),
+ ),
+ ),
+ cbs_azure_iscsi=dict(
+ type="dict",
+ options=dict(
+ storage_endpoint_collection_identity=dict(type="str"),
+ load_balancer=dict(type="str"),
+ load_balancer_addresses=dict(type="list", elements="str"),
+ ),
+ ),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ # deprecated, will be removed in 2.0.0
+ endpoint_type=dict(
+ type="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ addresses=dict(
+ type="list",
+ elements="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ gateway=dict(
+ type="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ network_interface_groups=dict(
+ type="list",
+ elements="str",
+ removed_in_version="2.0.0",
+ removed_from_collection="purestorage.fusion",
+ ),
+ )
+ )
+
+ mutually_exclusive = [
+ ("iscsi", "cbs_azure_iscsi"),
+ # can not use both deprecated and new fields at the same time
+ ("iscsi", "cbs_azure_iscsi", "addresses"),
+ ("iscsi", "cbs_azure_iscsi", "gateway"),
+ ("iscsi", "cbs_azure_iscsi", "network_interface_groups"),
+ ]
+
+ module = AnsibleModule(
+ argument_spec,
+ mutually_exclusive=mutually_exclusive,
+ supports_check_mode=True,
+ )
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+
+ if module.params["endpoint_type"] is not None:
+ module.warn(
+ "'endpoint_type' parameter is deprecated and will be removed in the version 2.0"
+ )
+
+ deprecated_parameters = {"addresses", "gateway", "network_interface_groups"}
+ used_deprecated_parameters = [
+ key
+ for key in list(deprecated_parameters & module.params.keys())
+ if module.params[key] is not None
+ ]
+
+ if len(used_deprecated_parameters) > 0:
+ # user uses deprecated module interface
+ for param_name in used_deprecated_parameters:
+ module.warn(
+ f"'{param_name}' parameter is deprecated and will be removed in the version 2.0"
+ )
+
+ if module.params["addresses"]:
+ for address in module.params["addresses"]:
+ if not is_valid_network(address):
+ module.fail_json(
+ msg=f"'{address}' is not a valid address in CIDR notation"
+ )
+
+ sendp = get_se(module, fusion)
+
+ if state == "present" and not sendp:
+ module.fail_on_missing_params(["addresses"])
+ if not (module.params["addresses"]):
+ module.fail_json(
+ msg="At least one entry in 'addresses' is required to create new storage endpoint"
+ )
+ create_se_old(module, fusion)
+ elif state == "present" and sendp:
+ update_se(module, fusion, sendp)
+ elif state == "absent" and sendp:
+ delete_se(module, fusion)
+ else:
+ # user uses new module interface
+ if module.params["iscsi"] is not None:
+ for endpoint in module.params["iscsi"]:
+ address = endpoint["address"]
+ if not is_valid_network(address):
+ module.fail_json(
+ msg=f"'{address}' is not a valid address in CIDR notation"
+ )
+ gateway = endpoint["gateway"]
+ if not is_valid_address(gateway):
+ module.fail_json(
+ msg=f"'{gateway}' is not a valid IPv4 address notation"
+ )
+ if module.params["cbs_azure_iscsi"] is not None:
+ for address in module.params["cbs_azure_iscsi"]["load_balancer_addresses"]:
+ if not is_valid_address(address):
+ module.fail_json(
+ msg=f"'{address}' is not a valid IPv4 address notation"
+ )
+
+ sendp = get_se(module, fusion)
+
+ if state == "present" and not sendp:
+ if (
+ module.params["iscsi"] is None
+ and module.params["cbs_azure_iscsi"] is None
+ ):
+ module.fail_json(
+ msg="either 'iscsi' or `cbs_azure_iscsi` parameter is required when creating storage endpoint"
+ )
+ create_se(module, fusion)
+ elif state == "present" and sendp:
+ update_se(module, fusion, sendp)
+ elif state == "absent" and sendp:
+ delete_se(module, fusion)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_ss.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_ss.py
new file mode 100644
index 000000000..3fdbb07dd
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_ss.py
@@ -0,0 +1,208 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_ss
+version_added: '1.0.0'
+short_description: Manage storage services in Pure Storage Fusion
+description:
+- Manage a storage services in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the storage service.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the storage service should exist or not.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ display_name:
+ description:
+ - The human name of the storage service.
+ - If not provided, defaults to I(name).
+ type: str
+ hardware_types:
+ description:
+ - Hardware types to which the storage service applies.
+ type: list
+ elements: str
+ choices: [ flash-array-x, flash-array-c, flash-array-x-optane, flash-array-xl ]
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new storage service foo
+ purestorage.fusion.fusion_ss:
+ name: foo
+ hardware_types:
+ - flash-array-x
+ - flash-array-x-optane
+ display_name: "test class"
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Update storage service
+ purestorage.fusion.fusion_ss:
+ name: foo
+ display_name: "main class"
+ hardware_types:
+ - flash-array-c
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete storage service
+ purestorage.fusion.fusion_ss:
+ name: foo
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils import getters
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_ss(module, fusion):
+ """Return Storage Service or None"""
+ return getters.get_ss(module, fusion, storage_service_name=module.params["name"])
+
+
+def create_ss(module, fusion):
+ """Create Storage Service"""
+
+ ss_api_instance = purefusion.StorageServicesApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ s_service = purefusion.StorageServicePost(
+ name=module.params["name"],
+ display_name=display_name,
+ hardware_types=module.params["hardware_types"],
+ )
+ op = ss_api_instance.create_storage_service(s_service)
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def delete_ss(module, fusion):
+ """Delete Storage Service"""
+
+ ss_api_instance = purefusion.StorageServicesApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ op = ss_api_instance.delete_storage_service(
+ storage_service_name=module.params["name"]
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def update_ss(module, fusion, ss):
+ """Update Storage Service"""
+
+ ss_api_instance = purefusion.StorageServicesApi(fusion)
+ patches = []
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != ss.display_name
+ ):
+ patch = purefusion.StorageServicePatch(
+ display_name=purefusion.NullableString(module.params["display_name"]),
+ )
+ patches.append(patch)
+
+ if not module.check_mode:
+ for patch in patches:
+ op = ss_api_instance.update_storage_service(
+ patch,
+ storage_service_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ changed = len(patches) != 0
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ hardware_types=dict(
+ type="list",
+ elements="str",
+ choices=[
+ "flash-array-x",
+ "flash-array-c",
+ "flash-array-x-optane",
+ "flash-array-xl",
+ ],
+ ),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ s_service = get_ss(module, fusion)
+
+ if not s_service and state == "present":
+ module.fail_on_missing_params(["hardware_types"])
+ create_ss(module, fusion)
+ elif s_service and state == "present":
+ update_ss(module, fusion, s_service)
+ elif s_service and state == "absent":
+ delete_ss(module, fusion)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_tenant.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_tenant.py
new file mode 100644
index 000000000..96e890a6b
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_tenant.py
@@ -0,0 +1,169 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_tenant
+version_added: '1.0.0'
+short_description: Manage tenants in Pure Storage Fusion
+description:
+- Create,delete or update a tenant in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the tenant.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the tenant should exist or not.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ display_name:
+ description:
+ - The human name of the tenant.
+ - If not provided, defaults to I(name).
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new tenat foo
+ purestorage.fusion.fusion_tenant:
+ name: foo
+ display_name: "tenant foo"
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete tenat foo
+ purestorage.fusion.fusion_tenant:
+ name: foo
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils import getters
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_tenant(module, fusion):
+ """Return Tenant or None"""
+ return getters.get_tenant(module, fusion, tenant_name=module.params["name"])
+
+
+def create_tenant(module, fusion):
+ """Create Tenant"""
+
+ api_instance = purefusion.TenantsApi(fusion)
+ changed = True
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ tenant = purefusion.TenantPost(
+ name=module.params["name"],
+ display_name=display_name,
+ )
+ op = api_instance.create_tenant(tenant)
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def update_tenant(module, fusion, tenant):
+ """Update Tenant settings"""
+ changed = False
+ api_instance = purefusion.TenantsApi(fusion)
+
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != tenant.display_name
+ ):
+ changed = True
+ if not module.check_mode:
+ new_tenant = purefusion.TenantPatch(
+ display_name=purefusion.NullableString(module.params["display_name"]),
+ )
+ op = api_instance.update_tenant(
+ new_tenant,
+ tenant_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def delete_tenant(module, fusion):
+ """Delete Tenant"""
+ changed = True
+ api_instance = purefusion.TenantsApi(fusion)
+ if not module.check_mode:
+ op = api_instance.delete_tenant(tenant_name=module.params["name"])
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ tenant = get_tenant(module, fusion)
+
+ if not tenant and state == "present":
+ create_tenant(module, fusion)
+ elif tenant and state == "present":
+ update_tenant(module, fusion, tenant)
+ elif tenant and state == "absent":
+ delete_tenant(module, fusion)
+ else:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_tn.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_tn.py
new file mode 100644
index 000000000..717b1e46f
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_tn.py
@@ -0,0 +1,122 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_tn
+version_added: '1.0.0'
+deprecated:
+ removed_at_date: "2023-07-26"
+ why: Tenant Networks were removed as a concept in Pure Storage Fusion
+ alternative: most of the functionality can be replicated using M(purestorage.fusion.fusion_se) and M(purestorage.fusion.fusion_nig)
+short_description: Manage tenant networks in Pure Storage Fusion
+description:
+- Create or delete tenant networks in Pure Storage Fusion.
+notes:
+- Supports C(check_mode).
+- Currently this only supports a single tenant subnet per tenant network.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - The name of the tenant network.
+ type: str
+ display_name:
+ description:
+ - The human name of the tenant network.
+ - If not provided, defaults to I(name).
+ type: str
+ state:
+ description:
+ - Define whether the tenant network should exist or not.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ region:
+ description:
+ - The name of the region the availability zone is in
+ type: str
+ availability_zone:
+ aliases: [ az ]
+ description:
+ - The name of the availability zone for the tenant network.
+ type: str
+ provider_subnets:
+ description:
+ - List of provider subnets to assign to the tenant networks subnet.
+ type: list
+ elements: str
+ addresses:
+ description:
+ - List of IP addresses to be used in the subnet of the tenant network.
+ - IP addresses must include a CIDR notation.
+ - IPv4 and IPv6 are fully supported.
+ type: list
+ elements: str
+ gateway:
+ description:
+ - Address of the subnet gateway.
+ - Currently this must be provided.
+ type: str
+ mtu:
+ description:
+ - MTU setting for the subnet.
+ default: 1500
+ type: int
+ prefix:
+ description:
+ - Network prefix in CIDR format.
+ - This will be deprecated soon.
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+# this module does nothing, thus no example is provided
+EXAMPLES = r"""
+"""
+
+RETURN = r"""
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str"),
+ region=dict(type="str"),
+ display_name=dict(type="str"),
+ availability_zone=dict(type="str", aliases=["az"]),
+ prefix=dict(type="str"),
+ gateway=dict(type="str"),
+ mtu=dict(type="int", default=1500),
+ provider_subnets=dict(type="list", elements="str"),
+ addresses=dict(type="list", elements="str"),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ )
+ )
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ module.warn(
+ "This module is deprecated, doesn't work, and will be removed in the version 2.0."
+ " Please, use purestorage.fusion.fusion_se and purestorage.fusion.fusion_nig instead."
+ )
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_ts.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_ts.py
new file mode 100644
index 000000000..33fb0187a
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_ts.py
@@ -0,0 +1,187 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_ts
+version_added: '1.0.0'
+short_description: Manage tenant spaces in Pure Storage Fusion
+description:
+- Create, update or delete a tenant spaces in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the tenant space.
+ type: str
+ required: true
+ display_name:
+ description:
+ - The human name of the tenant space.
+ - If not provided, defaults to I(name).
+ type: str
+ state:
+ description:
+ - Define whether the tenant space should exist or not.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ tenant:
+ description:
+ - The name of the tenant.
+ type: str
+ required: true
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new tenant space foo for tenant bar
+ purestorage.fusion.fusion_ts:
+ name: foo
+ tenant: bar
+ state: present
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete tenant space foo in tenant bar
+ purestorage.fusion.fusion_ts:
+ name: foo
+ tenant: bar
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils import getters
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_ts(module, fusion):
+ """Tenant Space or None"""
+ return getters.get_ts(module, fusion, tenant_space_name=module.params["name"])
+
+
+def create_ts(module, fusion):
+ """Create Tenant Space"""
+
+ ts_api_instance = purefusion.TenantSpacesApi(fusion)
+
+ changed = True
+ if not module.check_mode:
+ if not module.params["display_name"]:
+ display_name = module.params["name"]
+ else:
+ display_name = module.params["display_name"]
+ tspace = purefusion.TenantSpacePost(
+ name=module.params["name"],
+ display_name=display_name,
+ )
+ op = ts_api_instance.create_tenant_space(
+ tspace,
+ tenant_name=module.params["tenant"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def update_ts(module, fusion, ts):
+ """Update Tenant Space"""
+
+ ts_api_instance = purefusion.TenantSpacesApi(fusion)
+ patches = []
+ if (
+ module.params["display_name"]
+ and module.params["display_name"] != ts.display_name
+ ):
+ patch = purefusion.TenantSpacePatch(
+ display_name=purefusion.NullableString(module.params["display_name"]),
+ )
+ patches.append(patch)
+
+ if not module.check_mode:
+ for patch in patches:
+ op = ts_api_instance.update_tenant_space(
+ patch,
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ changed = len(patches) != 0
+
+ module.exit_json(changed=changed)
+
+
+def delete_ts(module, fusion):
+ """Delete Tenant Space"""
+ changed = True
+ ts_api_instance = purefusion.TenantSpacesApi(fusion)
+ if not module.check_mode:
+ op = ts_api_instance.delete_tenant_space(
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["name"],
+ )
+ await_operation(fusion, op)
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ tenant=dict(type="str", required=True),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+ tspace = get_ts(module, fusion)
+
+ if state == "present" and not tspace:
+ create_ts(module, fusion)
+ elif state == "present" and tspace:
+ update_ts(module, fusion, tspace)
+ elif state == "absent" and tspace:
+ delete_ts(module, fusion)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/plugins/modules/fusion_volume.py b/ansible_collections/purestorage/fusion/plugins/modules/fusion_volume.py
new file mode 100644
index 000000000..5b19064f5
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/plugins/modules/fusion_volume.py
@@ -0,0 +1,450 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Simon Dodsley (simon@purestorage.com), Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: fusion_volume
+version_added: '1.0.0'
+short_description: Manage volumes in Pure Storage Fusion
+description:
+- Create, update or delete a volume in Pure Storage Fusion.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+notes:
+- Supports C(check mode).
+options:
+ name:
+ description:
+ - The name of the volume.
+ type: str
+ required: true
+ display_name:
+ description:
+ - The human name of the volume.
+ - If not provided, defaults to I(name).
+ type: str
+ state:
+ description:
+ - Define whether the volume should exist or not.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ tenant:
+ description:
+ - The name of the tenant.
+ type: str
+ required: true
+ tenant_space:
+ description:
+ - The name of the tenant space.
+ type: str
+ required: true
+ eradicate:
+ description:
+ - "Wipes the volume instead of a soft delete if true. Must be used with `state: absent`."
+ type: bool
+ default: false
+ size:
+ description:
+ - Volume size in M, G, T or P units.
+ type: str
+ storage_class:
+ description:
+ - The name of the storage class.
+ type: str
+ placement_group:
+ description:
+ - The name of the placement group.
+ type: str
+ protection_policy:
+ description:
+ - The name of the protection policy.
+ type: str
+ host_access_policies:
+ description:
+ - 'A list of host access policies to connect the volume to.
+ To clear, assign empty list: host_access_policies: []'
+ type: list
+ elements: str
+ rename:
+ description:
+ - New name for volume.
+ type: str
+extends_documentation_fragment:
+- purestorage.fusion.purestorage.fusion
+"""
+
+EXAMPLES = r"""
+- name: Create new volume named foo in storage_class fred
+ purestorage.fusion.fusion_volume:
+ name: foo
+ storage_class: fred
+ size: 1T
+ tenant: test
+ tenant_space: space_1
+ state: present
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Extend the size of an existing volume named foo
+ purestorage.fusion.fusion_volume:
+ name: foo
+ size: 2T
+ tenant: test
+ tenant_space: space_1
+ state: present
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+
+- name: Delete volume named foo
+ purestorage.fusion.fusion_volume:
+ name: foo
+ tenant: test
+ tenant_space: space_1
+ state: absent
+ issuer_id: key_name
+ private_key_file: "az-admin-private-key.pem"
+"""
+
+RETURN = r"""
+"""
+
+try:
+ import fusion as purefusion
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.fusion.plugins.module_utils.fusion import (
+ fusion_argument_spec,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.parsing import (
+ parse_number_with_metric_suffix,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.startup import (
+ setup_fusion,
+)
+from ansible_collections.purestorage.fusion.plugins.module_utils.operations import (
+ await_operation,
+)
+
+
+def get_volume(module, fusion):
+ """Return Volume or None"""
+ volume_api_instance = purefusion.VolumesApi(fusion)
+ try:
+ return volume_api_instance.get_volume(
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["tenant_space"],
+ volume_name=module.params["name"],
+ )
+ except purefusion.rest.ApiException:
+ return None
+
+
+def get_wanted_haps(module):
+ """Return set of host access policies to assign"""
+ if not module.params["host_access_policies"]:
+ return set()
+ # looks like yaml parsing can leave in some spaces if coma-delimited .so strip() the names
+ return set([hap.strip() for hap in module.params["host_access_policies"]])
+
+
+def extract_current_haps(volume):
+ """Return set of host access policies that volume currently has"""
+ if not volume.host_access_policies:
+ return set()
+ return set([hap.name for hap in volume.host_access_policies])
+
+
+def create_volume(module, fusion):
+ """Create Volume"""
+
+ size = parse_number_with_metric_suffix(module, module.params["size"])
+
+ if not module.check_mode:
+ display_name = module.params["display_name"] or module.params["name"]
+ volume_api_instance = purefusion.VolumesApi(fusion)
+ volume = purefusion.VolumePost(
+ size=size,
+ storage_class=module.params["storage_class"],
+ placement_group=module.params["placement_group"],
+ name=module.params["name"],
+ display_name=display_name,
+ protection_policy=module.params["protection_policy"],
+ )
+ op = volume_api_instance.create_volume(
+ volume,
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["tenant_space"],
+ )
+ await_operation(fusion, op)
+
+ return True
+
+
+def update_host_access_policies(module, current, patches):
+ wanted = module.params
+ # 'wanted[...] is not None' to differentiate between empty list and no list
+ if wanted["host_access_policies"] is not None:
+ current_haps = extract_current_haps(current)
+ wanted_haps = get_wanted_haps(module)
+ if wanted_haps != current_haps:
+ patch = purefusion.VolumePatch(
+ host_access_policies=purefusion.NullableString(",".join(wanted_haps))
+ )
+ patches.append(patch)
+
+
+def update_destroyed(module, current, patches):
+ wanted = module.params
+ destroyed = wanted["state"] != "present"
+ if destroyed != current.destroyed:
+ patch = purefusion.VolumePatch(destroyed=purefusion.NullableBoolean(destroyed))
+ patches.append(patch)
+ if destroyed and not module.params["eradicate"]:
+ module.warn(
+ (
+ "Volume '{0}' is being soft deleted to prevent data loss, "
+ "if you want to wipe it immediately to reclaim used space, add 'eradicate: true'"
+ ).format(current.name)
+ )
+
+
+def update_display_name(module, current, patches):
+ wanted = module.params
+ if wanted["display_name"] and wanted["display_name"] != current.display_name:
+ patch = purefusion.VolumePatch(
+ display_name=purefusion.NullableString(wanted["display_name"])
+ )
+ patches.append(patch)
+
+
+def update_storage_class(module, current, patches):
+ wanted = module.params
+ if (
+ wanted["storage_class"]
+ and wanted["storage_class"] != current.storage_class.name
+ ):
+ patch = purefusion.VolumePatch(
+ storage_class=purefusion.NullableString(wanted["storage_class"])
+ )
+ patches.append(patch)
+
+
+def update_placement_group(module, current, patches):
+ wanted = module.params
+ if (
+ wanted["placement_group"]
+ and wanted["placement_group"] != current.placement_group.name
+ ):
+ patch = purefusion.VolumePatch(
+ placement_group=purefusion.NullableString(wanted["placement_group"])
+ )
+ patches.append(patch)
+
+
+def update_size(module, current, patches):
+ wanted = module.params
+ if wanted["size"]:
+ wanted_size = parse_number_with_metric_suffix(module, wanted["size"])
+ if wanted_size != current.size:
+ patch = purefusion.VolumePatch(size=purefusion.NullableSize(wanted_size))
+ patches.append(patch)
+
+
+def update_protection_policy(module, current, patches):
+ wanted = module.params
+ current_policy = current.protection_policy.name if current.protection_policy else ""
+ if (
+ wanted["protection_policy"] is not None
+ and wanted["protection_policy"] != current_policy
+ ):
+ patch = purefusion.VolumePatch(
+ protection_policy=purefusion.NullableString(wanted["protection_policy"])
+ )
+ patches.append(patch)
+
+
+def apply_patches(module, fusion, patches):
+ volume_api_instance = purefusion.VolumesApi(fusion)
+ for patch in patches:
+ op = volume_api_instance.update_volume(
+ patch,
+ volume_name=module.params["name"],
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["tenant_space"],
+ )
+ await_operation(fusion, op)
+
+
+def update_volume(module, fusion):
+ """Update Volume size, placement group, protection policy, storage class, HAPs"""
+ current = get_volume(module, fusion)
+ patches = []
+
+ if not current:
+ # cannot update nonexistent volume
+ # Note for check mode: the reasons this codepath is ran in check mode
+ # is to catch any argument errors and to compute 'changed'. Basically
+ # all argument checks are kept in validate_arguments() to filter the
+ # first part. The second part MAY diverge flow from the real run here if
+ # create_volume() created the volume and update was then run to update
+ # its properties. HOWEVER we don't really care in that case because
+ # create_volume() already sets 'changed' to true, so any 'changed'
+ # result from update_volume() would not change it.
+ return False
+
+ # volumes with 'destroyed' flag are kinda special because we can't change
+ # most of their properties while in this state, so we need to set it last
+ # and unset it first if changed, respectively
+ if module.params["state"] == "present":
+ update_destroyed(module, current, patches)
+ update_size(module, current, patches)
+ update_protection_policy(module, current, patches)
+ update_display_name(module, current, patches)
+ update_storage_class(module, current, patches)
+ update_placement_group(module, current, patches)
+ update_host_access_policies(module, current, patches)
+ elif module.params["state"] == "absent" and not current.destroyed:
+ update_size(module, current, patches)
+ update_protection_policy(module, current, patches)
+ update_display_name(module, current, patches)
+ update_storage_class(module, current, patches)
+ update_placement_group(module, current, patches)
+ update_host_access_policies(module, current, patches)
+ update_destroyed(module, current, patches)
+
+ if not module.check_mode:
+ apply_patches(module, fusion, patches)
+
+ changed = len(patches) != 0
+ return changed
+
+
+def eradicate_volume(module, fusion):
+ """Eradicate Volume"""
+ current = get_volume(module, fusion)
+ if module.check_mode:
+ return current or module.params["state"] == "present"
+ if not current:
+ return False
+
+ # update_volume() should be called before eradicate=True and it should
+ # ensure the volume is destroyed and HAPs are unassigned
+ if not current.destroyed or current.host_access_policies:
+ module.fail_json(
+ msg="BUG: inconsistent state, eradicate_volume() cannot be called with current.destroyed=False or any host_access_policies"
+ )
+
+ volume_api_instance = purefusion.VolumesApi(fusion)
+ op = volume_api_instance.delete_volume(
+ volume_name=module.params["name"],
+ tenant_name=module.params["tenant"],
+ tenant_space_name=module.params["tenant_space"],
+ )
+ await_operation(fusion, op)
+
+ return True
+
+
+def validate_arguments(module, volume):
+ """Validates most argument conditions and possible unacceptable argument combinations"""
+ state = module.params["state"]
+
+ if state == "present" and not volume:
+ module.fail_on_missing_params(["placement_group", "storage_class", "size"])
+
+ if module.params["state"] == "absent" and (
+ module.params["host_access_policies"]
+ or (volume and volume.host_access_policies)
+ ):
+ module.fail_json(
+ msg=(
+ "Volume must have no host access policies when destroyed, either revert the delete "
+ "by setting 'state: present' or remove all HAPs by 'host_access_policies: []'"
+ )
+ )
+
+ if state == "present" and module.params["eradicate"]:
+ module.fail_json(
+ msg="'eradicate: true' cannot be used together with 'state: present'"
+ )
+
+ if module.params["size"]:
+ size = parse_number_with_metric_suffix(module, module.params["size"])
+ if size < 1048576 or size > 4503599627370496: # 1MB to 4PB
+ module.fail_json(
+ msg="Size is not within the required range, size must be between 1MB and 4PB"
+ )
+
+
+def main():
+ """Main code"""
+ argument_spec = fusion_argument_spec()
+ deprecated_hosts = dict(
+ name="hosts", date="2023-07-26", collection_name="purefusion.fusion"
+ )
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ display_name=dict(type="str"),
+ rename=dict(
+ type="str",
+ removed_at_date="2023-07-26",
+ removed_from_collection="purestorage.fusion",
+ ),
+ tenant=dict(type="str", required=True),
+ tenant_space=dict(type="str", required=True),
+ placement_group=dict(type="str"),
+ storage_class=dict(type="str"),
+ protection_policy=dict(type="str"),
+ host_access_policies=dict(
+ type="list", elements="str", deprecated_aliases=[deprecated_hosts]
+ ),
+ eradicate=dict(type="bool", default=False),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ size=dict(type="str"),
+ )
+ )
+
+ required_by = {
+ "placement_group": "storage_class",
+ }
+
+ module = AnsibleModule(
+ argument_spec,
+ required_by=required_by,
+ supports_check_mode=True,
+ )
+ fusion = setup_fusion(module)
+
+ state = module.params["state"]
+
+ volume = get_volume(module, fusion)
+
+ validate_arguments(module, volume)
+
+ if state == "absent" and not volume:
+ module.exit_json(changed=False)
+
+ changed = False
+ if state == "present" and not volume:
+ changed = changed | create_volume(module, fusion)
+ # volume might exist even if soft-deleted, so we still have to update it
+ changed = changed | update_volume(module, fusion)
+ if module.params["eradicate"]:
+ changed = changed | eradicate_volume(module, fusion)
+
+ module.exit_json(changed=changed)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/fusion/tests/functional/README.md b/ansible_collections/purestorage/fusion/tests/functional/README.md
new file mode 100644
index 000000000..d7edc6609
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/README.md
@@ -0,0 +1,35 @@
+# Functional tests
+
+Functional tests aims at testing each module as a whole.
+They make sure the module parses given parameters into correct API calls.
+
+Specific functions of modules should be tested in Unit tests.
+
+## Running tests
+
+```bash
+pytest tests/functional
+```
+
+## Adding new tests
+
+Every module tested should consist (at least) of the following cases:
+
+- test_module_fails_on_wrong_parameters
+- test_NAME_create_name
+- test_NAME_create_without_display_name
+- test_NAME_create_exception
+- test_NAME_create_op_fails
+- test_NAME_create_op_exception
+- test_NAME_update
+- test_NAME_update_exception
+- test_NAME_update_op_fails
+- test_NAME_update_op_exception
+- test_NAME_present_not_changed
+- test_NAME_absent_not_changed
+- test_NAME_delete
+- test_NAME_delete_exception
+- test_NAME_delete_op_fails
+- test_NAME_delete_op_exception
+
+See already existing tests (e.g. `test_fusion_region.py`) for inspiration.
diff --git a/ansible_collections/purestorage/fusion/tests/functional/__init__.py b/ansible_collections/purestorage/fusion/tests/functional/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/__init__.py
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_api_client.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_api_client.py
new file mode 100644
index 000000000..77f753656
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_api_client.py
@@ -0,0 +1,361 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from dataclasses import asdict, dataclass
+from unittest.mock import MagicMock, patch
+
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_api_client
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ exit_json,
+ fail_json,
+ set_module_args,
+)
+from urllib3.exceptions import HTTPError
+
+# GLOBAL MOCKS
+fusion_api_client.setup_fusion = MagicMock(
+ return_value=purefusion.api_client.ApiClient()
+)
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+
+@dataclass
+class FakeApiClient:
+ id: str
+ self_link: str
+ name: str
+ display_name: str
+ issuer: str
+ public_key: str
+ last_key_update: float
+ last_used: float
+ creator_id: str
+
+
+@pytest.fixture
+def current_clients():
+ return [
+ FakeApiClient(
+ "1",
+ "self_link_value",
+ "client1",
+ "client1",
+ "apikey:name:thisisnotreal",
+ "0123456789",
+ 12345,
+ 12345,
+ "1234",
+ ),
+ FakeApiClient(
+ "2",
+ "self_link_value",
+ "client2",
+ "client2",
+ "apikey:name:thisisnotreal",
+ "0123456789",
+ 12345,
+ 12345,
+ "1234",
+ ),
+ FakeApiClient(
+ "3",
+ "self_link_value",
+ "client3",
+ "client3",
+ "apikey:name:thisisnotreal",
+ "0123456789",
+ 12345,
+ 12345,
+ "1234",
+ ),
+ ]
+
+
+@patch("fusion.IdentityManagerApi")
+@pytest.mark.parametrize(
+ "module_args",
+ [
+ # required parameter 'name` is missing
+ {
+ "state": "present",
+ "public_key": "0123456789",
+ },
+ # unknown parameter 'extra' is provided
+ {
+ "state": "present",
+ "name": "client1",
+ "public_key": "0123456789",
+ "extra": "value",
+ },
+ # parameter 'state` has incorrect value
+ {
+ "state": "cool",
+ "name": "client1",
+ "public_key": "0123456789",
+ },
+ ],
+)
+def test_module_fails_on_wrong_parameters(m_im_api, module_args, current_clients):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_api_clients = MagicMock(return_value=current_clients)
+ api_obj.get_api_client = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_api_client = MagicMock()
+ api_obj.delete_api_client = MagicMock()
+ m_im_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_api_client.main()
+
+ # check api was not called at all
+ api_obj.list_api_clients.assert_not_called()
+ api_obj.get_api_client.assert_not_called()
+ api_obj.create_api_client.assert_not_called()
+ api_obj.delete_api_client.assert_not_called()
+
+
+@patch("fusion.IdentityManagerApi")
+def test_api_client_create(m_im_api, current_clients):
+ module_args = {
+ "state": "present",
+ "name": "new_client",
+ "public_key": "0123456789",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_api_clients = MagicMock(return_value=current_clients)
+ api_obj.get_api_client = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_api_client = MagicMock()
+ api_obj.delete_api_client = MagicMock()
+ m_im_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_api_client.main()
+
+ assert exc.value.changed is True
+
+ # check api was called correctly
+ api_obj.list_api_clients.assert_called_once_with()
+ api_obj.get_api_client.assert_not_called()
+ api_obj.create_api_client.assert_called_once_with(
+ purefusion.APIClientPost(
+ public_key=module_args["public_key"],
+ display_name=module_args["name"],
+ )
+ )
+ api_obj.delete_api_client.assert_not_called()
+
+
+@patch("fusion.IdentityManagerApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_api_client_create_exception(
+ m_im_api, exec_original, exec_catch, current_clients
+):
+ module_args = {
+ "state": "present",
+ "name": "new_client",
+ "public_key": "0123456789",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_api_clients = MagicMock(return_value=current_clients)
+ api_obj.get_api_client = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_api_client = MagicMock(side_effect=exec_original)
+ api_obj.delete_api_client = MagicMock()
+ m_im_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_api_client.main()
+
+ # check api was called correctly
+ api_obj.list_api_clients.assert_called_once_with()
+ api_obj.get_api_client.assert_not_called()
+ api_obj.create_api_client.assert_called_once_with(
+ purefusion.APIClientPost(
+ public_key=module_args["public_key"],
+ display_name=module_args["name"],
+ )
+ )
+ api_obj.delete_api_client.assert_not_called()
+
+
+@patch("fusion.IdentityManagerApi")
+def test_api_client_present_not_changed(m_im_api, current_clients):
+ current_api_client = current_clients[0]
+ module_args = {
+ "state": "present",
+ "name": current_api_client.display_name,
+ "public_key": current_api_client.public_key,
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_api_clients = MagicMock(return_value=current_clients)
+ api_obj.get_api_client = MagicMock(
+ return_value=purefusion.APIClient(**asdict(current_api_client))
+ )
+ api_obj.create_api_client = MagicMock()
+ api_obj.delete_api_client = MagicMock()
+ m_im_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_api_client.main()
+
+ assert exc.value.changed is False
+
+ # check api was called correctly
+ api_obj.list_api_clients.assert_called_once_with()
+ api_obj.get_api_client.assert_not_called()
+ api_obj.create_api_client.assert_not_called()
+ api_obj.delete_api_client.assert_not_called()
+
+
+@patch("fusion.IdentityManagerApi")
+def test_api_client_absent_not_changed(m_im_api, current_clients):
+ module_args = {
+ "state": "absent",
+ "name": "non_existing_client",
+ "public_key": "0123456789",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_api_clients = MagicMock(return_value=current_clients)
+ api_obj.get_api_client = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_api_client = MagicMock()
+ api_obj.delete_api_client = MagicMock()
+ m_im_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_api_client.main()
+
+ assert exc.value.changed is False
+
+ # check api was called correctly
+ api_obj.list_api_clients.assert_called_once_with()
+ api_obj.get_api_client.assert_not_called()
+ api_obj.create_api_client.assert_not_called()
+ api_obj.delete_api_client.assert_not_called()
+
+
+@patch("fusion.IdentityManagerApi")
+def test_api_client_delete(m_im_api, current_clients):
+ current_api_client = current_clients[0]
+ module_args = {
+ "state": "absent",
+ "name": current_api_client.display_name,
+ "public_key": current_api_client.public_key,
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_api_clients = MagicMock(return_value=current_clients)
+ api_obj.get_api_client = MagicMock(
+ return_value=purefusion.APIClient(**asdict(current_api_client))
+ )
+ api_obj.create_api_client = MagicMock()
+ api_obj.delete_api_client = MagicMock()
+ m_im_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_api_client.main()
+
+ assert exc.value.changed is True
+
+ # check api was called correctly
+ api_obj.list_api_clients.assert_called_once_with()
+ api_obj.get_api_client.assert_not_called()
+ api_obj.create_api_client.assert_not_called()
+ api_obj.delete_api_client.assert_called_once_with(
+ api_client_id=current_api_client.id
+ )
+
+
+@patch("fusion.IdentityManagerApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_api_client_delete_exception(
+ m_im_api, exec_original, exec_catch, current_clients
+):
+ current_api_client = current_clients[0]
+ module_args = {
+ "state": "absent",
+ "name": current_api_client.display_name,
+ "public_key": current_api_client.public_key,
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_api_clients = MagicMock(return_value=current_clients)
+ api_obj.get_api_client = MagicMock(
+ return_value=purefusion.APIClient(**asdict(current_api_client))
+ )
+ api_obj.create_api_client = MagicMock()
+ api_obj.delete_api_client = MagicMock(side_effect=exec_original)
+ m_im_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_api_client.main()
+
+ # check api was called correctly
+ api_obj.list_api_clients.assert_called_once_with()
+ api_obj.get_api_client.assert_not_called()
+ api_obj.create_api_client.assert_not_called()
+ api_obj.delete_api_client.assert_called_once_with(
+ api_client_id=current_api_client.id
+ )
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_array.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_array.py
new file mode 100644
index 000000000..0343bb1dc
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_array.py
@@ -0,0 +1,1331 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock, call, patch
+
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_array
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ FailedOperationMock,
+ OperationMock,
+ SuccessfulOperationMock,
+ exit_json,
+ fail_json,
+ set_module_args,
+)
+from urllib3.exceptions import HTTPError
+
+# GLOBAL MOCKS
+fusion_array.setup_fusion = MagicMock(return_value=purefusion.api_client.ApiClient())
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+
+@pytest.fixture
+def module_args():
+ return {
+ "state": "present",
+ "name": "array1",
+ "display_name": "Array 1",
+ "region": "region1",
+ "availability_zone": "az1",
+ "appliance_id": "23984573498573",
+ "host_name": "array_1",
+ "hardware_type": "flash-array-x",
+ "maintenance_mode": False,
+ "unavailable_mode": False,
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+
+
+@pytest.fixture
+def current_array(module_args):
+ return {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"],
+ "display_name": module_args["display_name"],
+ "region": module_args["region"],
+ "availability_zone": module_args["availability_zone"],
+ "appliance_id": module_args["appliance_id"],
+ "apartment_id": "76586785687",
+ "host_name": module_args["host_name"],
+ "hardware_type": module_args["hardware_type"],
+ "maintenance_mode": module_args["maintenance_mode"],
+ "unavailable_mode": module_args["unavailable_mode"],
+ }
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+@pytest.mark.parametrize(
+ "module_args",
+ [
+ # required parameter 'name` is missing
+ {
+ "state": "present",
+ "display_name": "Array 1",
+ "region": "region1",
+ "availability_zone": "az1",
+ "appliance_id": "23984573498573",
+ "host_name": "array_1",
+ "hardware_type": "flash-array-x",
+ "maintenance_mode": False,
+ "unavailable_mode": False,
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # required parameter 'region` is missing
+ {
+ "state": "present",
+ "name": "array1",
+ "display_name": "Array 1",
+ "availability_zone": "az1",
+ "appliance_id": "23984573498573",
+ "host_name": "array_1",
+ "hardware_type": "flash-array-x",
+ "maintenance_mode": False,
+ "unavailable_mode": False,
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # required parameter 'availability_zone` is missing
+ {
+ "state": "present",
+ "name": "array1",
+ "display_name": "Array 1",
+ "region": "region1",
+ "appliance_id": "23984573498573",
+ "host_name": "array_1",
+ "hardware_type": "flash-array-x",
+ "maintenance_mode": False,
+ "unavailable_mode": False,
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # unknown parameter 'extra' is provided
+ {
+ "state": "present",
+ "name": "array1",
+ "display_name": "Array 1",
+ "availability_zone": "az1",
+ "region": "region1",
+ "appliance_id": "23984573498573",
+ "host_name": "array_1",
+ "hardware_type": "flash-array-x",
+ "maintenance_mode": False,
+ "unavailable_mode": False,
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ "extra": "value",
+ },
+ # parameter 'state` has incorrect value
+ {
+ "state": "cool",
+ "name": "array1",
+ "display_name": "Array 1",
+ "availability_zone": "az1",
+ "region": "region1",
+ "appliance_id": "23984573498573",
+ "host_name": "array_1",
+ "hardware_type": "flash-array-x",
+ "maintenance_mode": False,
+ "unavailable_mode": False,
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # parameter 'hardware_type` has incorrect value
+ {
+ "state": "present",
+ "name": "array1",
+ "display_name": "Array 1",
+ "availability_zone": "az1",
+ "region": "region1",
+ "appliance_id": "23984573498573",
+ "host_name": "array_1",
+ "hardware_type": "hdd-array-x",
+ "maintenance_mode": False,
+ "unavailable_mode": False,
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # parameter 'maintenance_mode` has incorrect value
+ {
+ "state": "present",
+ "name": "array1",
+ "display_name": "Array 1",
+ "availability_zone": "az1",
+ "region": "region1",
+ "appliance_id": "23984573498573",
+ "host_name": "array_1",
+ "hardware_type": "flash-array-x",
+ "maintenance_mode": "string",
+ "unavailable_mode": False,
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # parameter 'unavailable_mode` has incorrect value
+ {
+ "state": "present",
+ "name": "array1",
+ "display_name": "Array 1",
+ "availability_zone": "az1",
+ "region": "region1",
+ "appliance_id": "23984573498573",
+ "host_name": "array_1",
+ "hardware_type": "flash-array-x",
+ "maintenance_mode": False,
+ "unavailable_mode": "string",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ ],
+)
+def test_module_fails_on_wrong_parameters(m_array_api, m_op_api, module_args):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+ m_array_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_array.main()
+
+ # check api was not called at all
+ api_obj.get_array.assert_not_called()
+ api_obj.create_array.assert_not_called()
+ api_obj.update_array.assert_not_called()
+ api_obj.delete_array.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+@pytest.mark.parametrize(
+ "module_args",
+ [
+ # required parameter 'hardware_type` for creating resource is missing
+ {
+ "state": "present",
+ "name": "array1",
+ "display_name": "Array 1",
+ "region": "region1",
+ "availability_zone": "az1",
+ "appliance_id": "23984573498573",
+ "host_name": "array_1",
+ "maintenance_mode": False,
+ "unavailable_mode": False,
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # required parameter 'host_name` for creating resource is missing
+ {
+ "state": "present",
+ "name": "array1",
+ "display_name": "Array 1",
+ "region": "region1",
+ "availability_zone": "az1",
+ "appliance_id": "23984573498573",
+ "hardware_type": "flash-array-x",
+ "maintenance_mode": False,
+ "unavailable_mode": False,
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # required parameter 'appliance_id` for creating resource is missing
+ {
+ "state": "present",
+ "name": "array1",
+ "display_name": "Array 1",
+ "region": "region1",
+ "availability_zone": "az1",
+ "host_name": "array_1",
+ "hardware_type": "flash-array-x",
+ "maintenance_mode": False,
+ "unavailable_mode": False,
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ ],
+)
+def test_array_create_fails_on_wrong_parameters(m_array_api, m_op_api, module_args):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+ m_array_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_array.main()
+
+ # check api was not called at all
+ api_obj.get_array.assert_called_once_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_not_called()
+ api_obj.update_array.assert_not_called()
+ api_obj.delete_array.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+@pytest.mark.parametrize(
+ "hw_type",
+ [
+ "flash-array-x",
+ "flash-array-c",
+ "flash-array-x-optane",
+ "flash-array-xl",
+ ],
+)
+@pytest.mark.parametrize("main_m", [True, False])
+@pytest.mark.parametrize("unav_m", [True, False])
+def test_array_create(m_array_api, m_op_api, hw_type, main_m, unav_m, module_args):
+ module_args["hardware_type"] = hw_type
+ module_args["maintenance_mode"] = main_m
+ module_args["unavailable_mode"] = unav_m
+ created_array = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"],
+ "display_name": module_args["display_name"],
+ "region": module_args["region"],
+ "availability_zone": module_args["availability_zone"],
+ "appliance_id": module_args["appliance_id"],
+ "apartment_id": "76586785687",
+ "host_name": module_args["host_name"],
+ "hardware_type": module_args["hardware_type"],
+ "maintenance_mode": not module_args[
+ "maintenance_mode"
+ ], # so we can test patching
+ "unavailable_mode": not module_args[
+ "unavailable_mode"
+ ], # so we can test patching
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(
+ side_effect=[purefusion.rest.ApiException, purefusion.Array(**created_array)]
+ )
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_array.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_called_once_with(
+ purefusion.ArrayPost(
+ hardware_type=module_args["hardware_type"],
+ display_name=module_args["display_name"],
+ host_name=module_args["host_name"],
+ name=module_args["name"],
+ appliance_id=module_args["appliance_id"],
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.update_array.assert_has_calls(
+ [
+ call(
+ purefusion.ArrayPatch(
+ maintenance_mode=purefusion.NullableBoolean(
+ module_args["maintenance_mode"]
+ )
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ array_name=module_args["name"],
+ ),
+ call(
+ purefusion.ArrayPatch(
+ unavailable_mode=purefusion.NullableBoolean(
+ module_args["unavailable_mode"]
+ )
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ array_name=module_args["name"],
+ ),
+ ],
+ any_order=True,
+ )
+ api_obj.delete_array.assert_not_called()
+ op_obj.get_operation.assert_has_calls(
+ [
+ call(1),
+ call(2),
+ call(2),
+ ]
+ )
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+def test_array_create_without_display_name(m_array_api, m_op_api, module_args):
+ del module_args["display_name"]
+ created_array = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"],
+ "display_name": module_args["name"],
+ "region": module_args["region"],
+ "availability_zone": module_args["availability_zone"],
+ "appliance_id": module_args["appliance_id"],
+ "apartment_id": "76586785687",
+ "host_name": module_args["host_name"],
+ "hardware_type": module_args["hardware_type"],
+ "maintenance_mode": not module_args["maintenance_mode"],
+ "unavailable_mode": not module_args["unavailable_mode"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(
+ side_effect=[purefusion.rest.ApiException, purefusion.Array(**created_array)]
+ )
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_array.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_called_once_with(
+ purefusion.ArrayPost(
+ hardware_type=module_args["hardware_type"],
+ display_name=module_args["name"],
+ host_name=module_args["host_name"],
+ name=module_args["name"],
+ appliance_id=module_args["appliance_id"],
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.update_array.assert_has_calls(
+ [
+ call(
+ purefusion.ArrayPatch(
+ maintenance_mode=purefusion.NullableBoolean(
+ module_args["maintenance_mode"]
+ )
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ array_name=module_args["name"],
+ ),
+ call(
+ purefusion.ArrayPatch(
+ unavailable_mode=purefusion.NullableBoolean(
+ module_args["unavailable_mode"]
+ )
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ array_name=module_args["name"],
+ ),
+ ],
+ any_order=True,
+ )
+ api_obj.delete_array.assert_not_called()
+ op_obj.get_operation.assert_has_calls(
+ [
+ call(1),
+ call(2),
+ call(2),
+ ]
+ )
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_array_create_exception(
+ m_array_api, m_op_api, exec_original, exec_catch, module_args
+):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_array = MagicMock(side_effect=exec_original)
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_array.main()
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_once_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_called_once_with(
+ purefusion.ArrayPost(
+ hardware_type=module_args["hardware_type"],
+ display_name=module_args["display_name"],
+ host_name=module_args["host_name"],
+ name=module_args["name"],
+ appliance_id=module_args["appliance_id"],
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.update_array.assert_not_called()
+ api_obj.delete_array.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_array_create_second_exception(
+ m_array_api, m_op_api, exec_original, exec_catch, module_args
+):
+ created_array = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"],
+ "display_name": module_args["name"],
+ "region": module_args["region"],
+ "availability_zone": module_args["availability_zone"],
+ "appliance_id": module_args["appliance_id"],
+ "apartment_id": "76586785687",
+ "host_name": module_args["host_name"],
+ "hardware_type": module_args["hardware_type"],
+ "maintenance_mode": not module_args["maintenance_mode"],
+ "unavailable_mode": not module_args["unavailable_mode"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(
+ side_effect=[purefusion.rest.ApiException, purefusion.Array(**created_array)]
+ )
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(side_effect=exec_original)
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_array.main()
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_called_once_with(
+ purefusion.ArrayPost(
+ hardware_type=module_args["hardware_type"],
+ display_name=module_args["display_name"],
+ host_name=module_args["host_name"],
+ name=module_args["name"],
+ appliance_id=module_args["appliance_id"],
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.update_array.assert_called_once()
+ api_obj.delete_array.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+def test_array_create_op_fails(m_array_api, m_op_api, module_args):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_array.main()
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_once_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_called_once_with(
+ purefusion.ArrayPost(
+ hardware_type=module_args["hardware_type"],
+ display_name=module_args["display_name"],
+ host_name=module_args["host_name"],
+ name=module_args["name"],
+ appliance_id=module_args["appliance_id"],
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.update_array.assert_not_called()
+ api_obj.delete_array.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+def test_array_create_second_op_fails(m_array_api, m_op_api, module_args):
+ created_array = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"],
+ "display_name": module_args["name"],
+ "region": module_args["region"],
+ "availability_zone": module_args["availability_zone"],
+ "appliance_id": module_args["appliance_id"],
+ "apartment_id": "76586785687",
+ "host_name": module_args["host_name"],
+ "hardware_type": module_args["hardware_type"],
+ "maintenance_mode": not module_args["maintenance_mode"],
+ "unavailable_mode": not module_args["unavailable_mode"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(
+ side_effect=[purefusion.rest.ApiException, purefusion.Array(**created_array)]
+ )
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(
+ side_effect=[SuccessfulOperationMock, FailedOperationMock]
+ )
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_array.main()
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_called_once_with(
+ purefusion.ArrayPost(
+ hardware_type=module_args["hardware_type"],
+ display_name=module_args["display_name"],
+ host_name=module_args["host_name"],
+ name=module_args["name"],
+ appliance_id=module_args["appliance_id"],
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.update_array.assert_called_once()
+ api_obj.delete_array.assert_not_called()
+ op_obj.get_operation.assert_has_calls(
+ [
+ call(1),
+ call(2),
+ ]
+ )
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_array_create_op_exception(
+ m_array_api, m_op_api, exec_original, exec_catch, module_args
+):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_array.main()
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_once_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_called_once_with(
+ purefusion.ArrayPost(
+ hardware_type=module_args["hardware_type"],
+ display_name=module_args["display_name"],
+ host_name=module_args["host_name"],
+ name=module_args["name"],
+ appliance_id=module_args["appliance_id"],
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.update_array.assert_not_called()
+ api_obj.delete_array.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_array_create_second_op_exception(
+ m_array_api, m_op_api, exec_original, exec_catch, module_args
+):
+ created_array = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"],
+ "display_name": module_args["name"],
+ "region": module_args["region"],
+ "availability_zone": module_args["availability_zone"],
+ "appliance_id": module_args["appliance_id"],
+ "apartment_id": "76586785687",
+ "host_name": module_args["host_name"],
+ "hardware_type": module_args["hardware_type"],
+ "maintenance_mode": not module_args["maintenance_mode"],
+ "unavailable_mode": not module_args["unavailable_mode"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(
+ side_effect=[purefusion.rest.ApiException, purefusion.Array(**created_array)]
+ )
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(
+ side_effect=[SuccessfulOperationMock, exec_original]
+ )
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_array.main()
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_called_once_with(
+ purefusion.ArrayPost(
+ hardware_type=module_args["hardware_type"],
+ display_name=module_args["display_name"],
+ host_name=module_args["host_name"],
+ name=module_args["name"],
+ appliance_id=module_args["appliance_id"],
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.update_array.assert_called_once()
+ api_obj.delete_array.assert_not_called()
+ op_obj.get_operation.assert_has_calls(
+ [
+ call(1),
+ call(2),
+ ]
+ )
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+def test_array_update(m_array_api, m_op_api, module_args, current_array):
+ current_array["display_name"] = None
+ current_array["host_name"] = "something"
+ current_array["maintenance_mode"] = not current_array["maintenance_mode"]
+ current_array["unavailable_mode"] = not current_array["unavailable_mode"]
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(return_value=purefusion.Array(**current_array))
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_array.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_not_called()
+ api_obj.update_array.assert_has_calls(
+ [
+ call(
+ purefusion.ArrayPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ array_name=module_args["name"],
+ ),
+ call(
+ purefusion.ArrayPatch(
+ host_name=purefusion.NullableString(module_args["host_name"])
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ array_name=module_args["name"],
+ ),
+ call(
+ purefusion.ArrayPatch(
+ unavailable_mode=purefusion.NullableBoolean(
+ module_args["unavailable_mode"]
+ )
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ array_name=module_args["name"],
+ ),
+ call(
+ purefusion.ArrayPatch(
+ maintenance_mode=purefusion.NullableBoolean(
+ module_args["maintenance_mode"]
+ )
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ array_name=module_args["name"],
+ ),
+ ],
+ any_order=True,
+ )
+ api_obj.delete_array.assert_not_called()
+ op_obj.get_operation.assert_has_calls(
+ [
+ call(2),
+ call(2),
+ call(2),
+ call(2),
+ ]
+ )
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_array_update_exception(
+ m_array_api, m_op_api, exec_original, exec_catch, module_args, current_array
+):
+ current_array["display_name"] = None
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(return_value=purefusion.Array(**current_array))
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(side_effect=exec_original)
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_array.main()
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_not_called()
+ api_obj.update_array.assert_called_once()
+ api_obj.delete_array.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+def test_array_update_op_fails(m_array_api, m_op_api, module_args, current_array):
+ current_array["display_name"] = None
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(return_value=purefusion.Array(**current_array))
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_array.main()
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_not_called()
+ api_obj.update_array.assert_called_once()
+ api_obj.delete_array.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_array_update_op_exception(
+ m_array_api, m_op_api, exec_original, exec_catch, module_args, current_array
+):
+ current_array["display_name"] = None
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(return_value=purefusion.Array(**current_array))
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_array.main()
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_not_called()
+ api_obj.update_array.assert_called_once()
+ api_obj.delete_array.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+def test_array_present_not_changed(m_array_api, m_op_api, module_args, current_array):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(return_value=purefusion.Array(**current_array))
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_array.main()
+
+ assert not exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_not_called()
+ api_obj.update_array.assert_not_called()
+ api_obj.delete_array.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+def test_array_absent_not_changed(m_array_api, m_op_api, module_args):
+ module_args["state"] = "absent"
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_array.main()
+
+ assert not exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_not_called()
+ api_obj.update_array.assert_not_called()
+ api_obj.delete_array.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+def test_array_delete(m_array_api, m_op_api, module_args, current_array):
+ module_args["state"] = "absent"
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(return_value=purefusion.Array(**current_array))
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_array.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_once_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_not_called()
+ api_obj.update_array.assert_not_called()
+ api_obj.delete_array.assert_called_once_with(
+ region_name=module_args["region"],
+ availability_zone_name=module_args["availability_zone"],
+ array_name=module_args["name"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_array_delete_exception(
+ m_array_api, m_op_api, exec_original, exec_catch, module_args, current_array
+):
+ module_args["state"] = "absent"
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(return_value=purefusion.Array(**current_array))
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(side_effect=exec_original)
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_array.main()
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_once_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_not_called()
+ api_obj.update_array.assert_not_called()
+ api_obj.delete_array.assert_called_once_with(
+ region_name=module_args["region"],
+ availability_zone_name=module_args["availability_zone"],
+ array_name=module_args["name"],
+ )
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+def test_array_delete_op_fails(m_array_api, m_op_api, module_args, current_array):
+ module_args["state"] = "absent"
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(return_value=purefusion.Array(**current_array))
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_array.main()
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_once_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_not_called()
+ api_obj.update_array.assert_not_called()
+ api_obj.delete_array.assert_called_once_with(
+ region_name=module_args["region"],
+ availability_zone_name=module_args["availability_zone"],
+ array_name=module_args["name"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ArraysApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_array_delete_op_exception(
+ m_array_api, m_op_api, exec_original, exec_catch, module_args, current_array
+):
+ module_args["state"] = "absent"
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_array = MagicMock(return_value=purefusion.Array(**current_array))
+ api_obj.create_array = MagicMock(return_value=OperationMock(1))
+ api_obj.update_array = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_array = MagicMock(return_value=OperationMock(3))
+ m_array_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_array.main()
+
+ # check api was called correctly
+ api_obj.get_array.assert_called_once_with(
+ array_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_array.assert_not_called()
+ api_obj.update_array.assert_not_called()
+ api_obj.delete_array.assert_called_once_with(
+ region_name=module_args["region"],
+ availability_zone_name=module_args["availability_zone"],
+ array_name=module_args["name"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_az.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_az.py
new file mode 100644
index 000000000..c49f958a2
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_az.py
@@ -0,0 +1,717 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock, patch
+
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_az
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ FailedOperationMock,
+ OperationMock,
+ SuccessfulOperationMock,
+ exit_json,
+ fail_json,
+ set_module_args,
+)
+from urllib3.exceptions import HTTPError
+
+# GLOBAL MOCKS
+fusion_az.setup_fusion = MagicMock(return_value=purefusion.api_client.ApiClient())
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.AvailabilityZonesApi")
+@pytest.mark.parametrize(
+ "module_args",
+ [
+ # required parameter 'name` is missing
+ {
+ "state": "present",
+ "region": "region1",
+ "display_name": "Availability Zone 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # required parameter 'region` is missing
+ {
+ "state": "present",
+ "name": "az1",
+ "display_name": "Availability Zone 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # unknown parameter 'extra' is provided
+ {
+ "state": "present",
+ "name": "az1",
+ "region": "region1",
+ "display_name": "Availability Zone 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ "extra": "value",
+ },
+ # parameter 'state` has incorrect value
+ {
+ "state": "cool",
+ "name": "az1",
+ "region": "region1",
+ "display_name": "Availability Zone 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ ],
+)
+def test_module_op_fails_on_wrong_parameters(m_az_api, m_op_api, module_args):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_availability_zone = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_availability_zone = MagicMock(return_value=OperationMock(1))
+ api_obj.update_availability_zone = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_availability_zone = MagicMock(return_value=OperationMock(3))
+ m_az_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_az.main()
+
+ # check api was not called at all
+ api_obj.get_region.assert_not_called()
+ api_obj.create_availability_zone.assert_not_called()
+ api_obj.update_availability_zone.assert_not_called()
+ api_obj.delete_availability_zone.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.AvailabilityZonesApi")
+def test_az_create(m_az_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "az1",
+ "region": "region1",
+ "display_name": "Availability Zone 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_availability_zone = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_availability_zone = MagicMock(return_value=OperationMock(1))
+ api_obj.update_availability_zone = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_availability_zone = MagicMock(return_value=OperationMock(3))
+ m_az_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_az.main()
+
+ assert exc.value.changed
+
+ api_obj.get_region.get_availability_zone(
+ availability_zone_name=module_args["name"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_availability_zone.assert_called_once_with(
+ purefusion.AvailabilityZonePost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ ),
+ region_name=module_args["region"],
+ )
+ api_obj.update_availability_zone.assert_not_called()
+ api_obj.delete_availability_zone.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.AvailabilityZonesApi")
+def test_az_create_without_display_name(m_az_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "az1",
+ "region": "region1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_availability_zone = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_availability_zone = MagicMock(return_value=OperationMock(1))
+ api_obj.update_availability_zone = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_availability_zone = MagicMock(return_value=OperationMock(3))
+ m_az_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_az.main()
+
+ assert exc.value.changed
+
+ api_obj.get_region.get_availability_zone(
+ availability_zone_name=module_args["name"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_availability_zone.assert_called_once_with(
+ purefusion.AvailabilityZonePost(
+ name=module_args["name"],
+ display_name=module_args["name"],
+ ),
+ region_name=module_args["region"],
+ )
+ api_obj.update_availability_zone.assert_not_called()
+ api_obj.delete_availability_zone.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.AvailabilityZonesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_az_create_exception(m_az_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "az1",
+ "region": "region1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_availability_zone = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_availability_zone = MagicMock(side_effect=exec_original)
+ api_obj.update_availability_zone = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_availability_zone = MagicMock(return_value=OperationMock(3))
+ m_az_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_az.main()
+
+ api_obj.get_region.get_availability_zone(
+ availability_zone_name=module_args["name"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_availability_zone.assert_called_once_with(
+ purefusion.AvailabilityZonePost(
+ name=module_args["name"],
+ display_name=module_args["name"],
+ ),
+ region_name=module_args["region"],
+ )
+ api_obj.update_availability_zone.assert_not_called()
+ api_obj.delete_availability_zone.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.AvailabilityZonesApi")
+def test_az_create_op_fails(m_az_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "az1",
+ "region": "region1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_availability_zone = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_availability_zone = MagicMock(return_value=OperationMock(1))
+ api_obj.update_availability_zone = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_availability_zone = MagicMock(return_value=OperationMock(3))
+ m_az_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_az.main()
+
+ api_obj.get_region.get_availability_zone(
+ availability_zone_name=module_args["name"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_availability_zone.assert_called_once_with(
+ purefusion.AvailabilityZonePost(
+ name=module_args["name"],
+ display_name=module_args["name"],
+ ),
+ region_name=module_args["region"],
+ )
+ api_obj.update_availability_zone.assert_not_called()
+ api_obj.delete_availability_zone.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.AvailabilityZonesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_az_create_op_exception(m_az_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "az1",
+ "region": "region1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_availability_zone = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_availability_zone = MagicMock(return_value=OperationMock(1))
+ api_obj.update_availability_zone = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_availability_zone = MagicMock(return_value=OperationMock(3))
+ m_az_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_az.main()
+
+ api_obj.get_region.get_availability_zone(
+ availability_zone_name=module_args["name"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_availability_zone.assert_called_once_with(
+ purefusion.AvailabilityZonePost(
+ name=module_args["name"],
+ display_name=module_args["name"],
+ ),
+ region_name=module_args["region"],
+ )
+ api_obj.update_availability_zone.assert_not_called()
+ api_obj.delete_availability_zone.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.AvailabilityZonesApi")
+def test_az_update(m_az_api, m_op_api):
+ # NOTE: Availability Zone does not have PATCH method, thus no action is expected
+ module_args = {
+ "state": "present",
+ "name": "az1",
+ "region": "region1",
+ "display_name": "Availability Zone 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_az = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "region": module_args["region"], # region must match
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_availability_zone = MagicMock(
+ return_value=purefusion.AvailabilityZone(**current_az)
+ )
+ api_obj.create_availability_zone = MagicMock(return_value=OperationMock(1))
+ api_obj.update_availability_zone = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_availability_zone = MagicMock(return_value=OperationMock(3))
+ m_az_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_az.main()
+
+ assert not exc.value.changed
+
+ api_obj.get_region.get_availability_zone(
+ availability_zone_name=module_args["name"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_availability_zone.assert_not_called()
+ api_obj.update_availability_zone.assert_not_called()
+ api_obj.delete_availability_zone.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.AvailabilityZonesApi")
+def test_az_present_not_changed(m_az_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "az1",
+ "region": "region1",
+ "display_name": "Availability Zone 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_az = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "region": module_args["region"], # region must match
+ "display_name": module_args["display_name"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_availability_zone = MagicMock(
+ return_value=purefusion.AvailabilityZone(**current_az)
+ )
+ api_obj.create_availability_zone = MagicMock(return_value=OperationMock(1))
+ api_obj.update_availability_zone = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_availability_zone = MagicMock(return_value=OperationMock(3))
+ m_az_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_az.main()
+
+ assert not exc.value.changed
+
+ api_obj.get_region.get_availability_zone(
+ availability_zone_name=module_args["name"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_availability_zone.assert_not_called()
+ api_obj.update_availability_zone.assert_not_called()
+ api_obj.delete_availability_zone.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.AvailabilityZonesApi")
+def test_az_absent_not_changed(m_az_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "az1",
+ "region": "region1",
+ "display_name": "Availability Zone 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_availability_zone = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_availability_zone = MagicMock(return_value=OperationMock(1))
+ api_obj.update_availability_zone = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_availability_zone = MagicMock(return_value=OperationMock(3))
+ m_az_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_az.main()
+
+ assert not exc.value.changed
+
+ api_obj.get_region.get_availability_zone(
+ availability_zone_name=module_args["name"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_availability_zone.assert_not_called()
+ api_obj.update_availability_zone.assert_not_called()
+ api_obj.delete_availability_zone.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.AvailabilityZonesApi")
+def test_az_delete(m_az_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "az1",
+ "region": "region1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_az = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "region": module_args["region"], # region must match
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_availability_zone = MagicMock(
+ return_value=purefusion.AvailabilityZone(**current_az)
+ )
+ api_obj.create_availability_zone = MagicMock(return_value=OperationMock(1))
+ api_obj.update_availability_zone = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_availability_zone = MagicMock(return_value=OperationMock(3))
+ m_az_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_az.main()
+
+ assert exc.value.changed
+
+ api_obj.get_region.get_availability_zone(
+ availability_zone_name=module_args["name"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_availability_zone.assert_not_called()
+ api_obj.update_availability_zone.assert_not_called()
+ api_obj.delete_availability_zone.assert_called_once_with(
+ availability_zone_name=module_args["name"],
+ region_name=module_args["region"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.AvailabilityZonesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_az_delete_exception(m_az_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "absent",
+ "name": "az1",
+ "region": "region1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_az = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "region": module_args["region"], # region must match
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_availability_zone = MagicMock(
+ return_value=purefusion.AvailabilityZone(**current_az)
+ )
+ api_obj.create_availability_zone = MagicMock(return_value=OperationMock(1))
+ api_obj.update_availability_zone = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_availability_zone = MagicMock(side_effect=exec_original)
+ m_az_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_az.main()
+
+ api_obj.get_region.get_availability_zone(
+ availability_zone_name=module_args["name"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_availability_zone.assert_not_called()
+ api_obj.update_availability_zone.assert_not_called()
+ api_obj.delete_availability_zone.assert_called_once_with(
+ availability_zone_name=module_args["name"],
+ region_name=module_args["region"],
+ )
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.AvailabilityZonesApi")
+def test_az_delete_op_fails(m_az_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "az1",
+ "region": "region1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_az = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "region": module_args["region"], # region must match
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_availability_zone = MagicMock(
+ return_value=purefusion.AvailabilityZone(**current_az)
+ )
+ api_obj.create_availability_zone = MagicMock(return_value=OperationMock(1))
+ api_obj.update_availability_zone = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_availability_zone = MagicMock(return_value=OperationMock(3))
+ m_az_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_az.main()
+
+ api_obj.get_region.get_availability_zone(
+ availability_zone_name=module_args["name"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_availability_zone.assert_not_called()
+ api_obj.update_availability_zone.assert_not_called()
+ api_obj.delete_availability_zone.assert_called_once_with(
+ availability_zone_name=module_args["name"],
+ region_name=module_args["region"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.AvailabilityZonesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_az_delete_op_exception(m_az_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "absent",
+ "name": "az1",
+ "region": "region1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_az = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "region": module_args["region"], # region must match
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_availability_zone = MagicMock(
+ return_value=purefusion.AvailabilityZone(**current_az)
+ )
+ api_obj.create_availability_zone = MagicMock(return_value=OperationMock(1))
+ api_obj.update_availability_zone = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_availability_zone = MagicMock(return_value=OperationMock(3))
+ m_az_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_az.main()
+
+ api_obj.get_region.get_availability_zone(
+ availability_zone_name=module_args["name"],
+ region_name=module_args["region"],
+ )
+ api_obj.create_availability_zone.assert_not_called()
+ api_obj.update_availability_zone.assert_not_called()
+ api_obj.delete_availability_zone.assert_called_once_with(
+ availability_zone_name=module_args["name"],
+ region_name=module_args["region"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_hap.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_hap.py
new file mode 100644
index 000000000..6491c71da
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_hap.py
@@ -0,0 +1,889 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock, patch
+
+import fusion
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_hap
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ FailedOperationMock,
+ OperationMock,
+ SuccessfulOperationMock,
+ exit_json,
+ fail_json,
+ set_module_args,
+)
+from urllib3.exceptions import HTTPError
+
+# GLOBAL MOCKS
+fusion_hap.setup_fusion = MagicMock(return_value=purefusion.api_client.ApiClient())
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+
+@pytest.fixture
+def module_args():
+ """Module arguments to create new HAP."""
+ return {
+ "state": "present",
+ "name": "hap_new",
+ "display_name": "Host Access Policy New",
+ "iqn": "iqn.2023-05.com.purestorage:420qp2c0699",
+ "personality": "aix",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+
+
+@pytest.fixture
+def current_hap_list():
+ return fusion.HostAccessPolicyList(
+ count=3,
+ more_items_remaining=False,
+ items=[
+ fusion.HostAccessPolicy(
+ id="1",
+ self_link="self_link_value",
+ name="hap1",
+ display_name="Host Access Policy 1",
+ iqn="iqn.2023-05.com.purestorage:420qp2c0261",
+ personality="aix",
+ ),
+ fusion.HostAccessPolicy(
+ id="2",
+ self_link="self_link_value",
+ name="hap2",
+ display_name="Host Access Policy 2",
+ iqn="iqn.2023-05.com.purestorage:420qp2c0262",
+ personality="windows",
+ ),
+ fusion.HostAccessPolicy(
+ id="3",
+ self_link="self_link_value",
+ name="hap3",
+ display_name="Host Access Policy 3",
+ iqn="iqn.2023-05.com.purestorage:420qp2c0263",
+ personality="solaris",
+ ),
+ ],
+ )
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+@pytest.mark.parametrize(
+ "module_args",
+ [
+ # required parameter 'name` is missing
+ {
+ "state": "present",
+ "display_name": "Host Access Policy 1",
+ "iqn": "iqn.2023-05.com.purestorage:420qp2c0261",
+ "personality": "aix",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ },
+ # 'state' is 'present' but 'iqn' is not provided
+ {
+ "state": "present",
+ "name": "hap1",
+ "display_name": "Host Access Policy 1",
+ "personality": "aix",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ },
+ # unknown parameter 'extra' is provided
+ {
+ "state": "present",
+ "name": "hap1",
+ "display_name": "Host Access Policy 1",
+ "iqn": "iqn.2023-05.com.purestorage:420qp2c0261",
+ "personality": "aix",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ "extra": "value",
+ },
+ # parameter 'state` has incorrect value
+ {
+ "state": "cool",
+ "name": "hap1",
+ "display_name": "Host Access Policy 1",
+ "iqn": "iqn.2023-05.com.purestorage:420qp2c0261",
+ "personality": "aix",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ },
+ # parameter 'personality` has incorrect value
+ {
+ "state": "present",
+ "name": "hap1",
+ "display_name": "Host Access Policy 1",
+ "iqn": "iqn.2023-05.com.purestorage:420qp2c0261",
+ "personality": "cool",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ },
+ ],
+)
+def test_module_fails_on_wrong_parameters(
+ m_hap_api, m_op_api, module_args, current_hap_list
+):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(return_value=current_hap_list)
+ api_obj.get_host_access_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_host_access_policy = MagicMock(return_value=OperationMock(1))
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(return_value=OperationMock(3))
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+ m_hap_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_hap.main()
+
+ # check api was not called at all
+ api_obj.list_host_access_policies.assert_not_called()
+ api_obj.get_host_access_policy.assert_not_called()
+ api_obj.create_host_access_policy.assert_not_called()
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+@pytest.mark.parametrize(
+ "name",
+ [
+ "",
+ "space space",
+ "toolongname_toolongname_toolongname_toolongname_toolongname_toolongname",
+ "end_with_underscore_",
+ "_start_with_underscore",
+ ],
+)
+def test_hap_fail_on_invalid_name(
+ m_hap_api, m_op_api, module_args, current_hap_list, name
+):
+ module_args["name"] = name
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(
+ side_effect=purefusion.rest.ApiException
+ )
+ api_obj.get_host_access_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_host_access_policy = MagicMock(return_value=OperationMock(1))
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(return_value=OperationMock(3))
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+ m_hap_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_hap.main()
+
+ # check api was not called at all
+ api_obj.list_host_access_policies.assert_not_called()
+ api_obj.get_host_access_policy.assert_not_called()
+ api_obj.create_host_access_policy.assert_not_called()
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+@pytest.mark.parametrize(
+ "iqn",
+ [
+ "qn.2023-05.com.purestorage:420qp2c0261",
+ "iqn2023-05.com.purestorage:420qp2c0261",
+ "iqn.202305.com.purestorage:420qp2c0261",
+ "iqn.2023-05com.purestorage:420qp2c0261",
+ "iqn.2023-05.com.purestorage:",
+ "iqn.2023-05..purestorage:420qp2c0261",
+ ".2023-05.com.purestorage:420qp2c0261",
+ "2023-05.com.purestorage:420qp2c0261",
+ ],
+)
+def test_hap_fail_on_invalid_iqn(
+ m_hap_api, m_op_api, module_args, current_hap_list, iqn
+):
+ module_args["iqn"] = iqn
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(
+ side_effect=purefusion.rest.ApiException
+ )
+ api_obj.get_host_access_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_host_access_policy = MagicMock(return_value=OperationMock(1))
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(return_value=OperationMock(3))
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+ m_hap_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_hap.main()
+
+ # check api was not called at all
+ api_obj.list_host_access_policies.assert_not_called()
+ api_obj.get_host_access_policy.assert_not_called()
+ api_obj.create_host_access_policy.assert_not_called()
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+def test_hap_create(m_hap_api, m_op_api, module_args, current_hap_list):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(return_value=current_hap_list)
+ api_obj.get_host_access_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_host_access_policy = MagicMock(return_value=OperationMock(1))
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(return_value=OperationMock(3))
+ m_hap_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_hap.main()
+
+ assert exc.value.changed is True
+
+ # check api was called correctly
+ api_obj.list_host_access_policies.assert_called_once_with()
+ api_obj.get_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ api_obj.create_host_access_policy.assert_called_once_with(
+ purefusion.HostAccessPoliciesPost(
+ iqn=module_args["iqn"],
+ personality=module_args["personality"],
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ )
+ )
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+def test_hap_create_without_display_name(
+ m_hap_api, m_op_api, module_args, current_hap_list
+):
+ del module_args["display_name"]
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(return_value=current_hap_list)
+ api_obj.get_host_access_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_host_access_policy = MagicMock(return_value=OperationMock(1))
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(return_value=OperationMock(3))
+ m_hap_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_hap.main()
+
+ assert exc.value.changed is True
+
+ # check api was called correctly
+ api_obj.list_host_access_policies.assert_called_once_with()
+ api_obj.get_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ api_obj.create_host_access_policy.assert_called_once_with(
+ purefusion.HostAccessPoliciesPost(
+ iqn=module_args["iqn"],
+ personality=module_args["personality"],
+ name=module_args["name"],
+ display_name=module_args["name"],
+ )
+ )
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+def test_hap_create_iqn_exists(m_hap_api, m_op_api, module_args, current_hap_list):
+ module_args["iqn"] = current_hap_list.items[0].iqn
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(return_value=current_hap_list)
+ api_obj.get_host_access_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_host_access_policy = MagicMock(return_value=OperationMock(1))
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(return_value=OperationMock(3))
+ m_hap_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson) as exc:
+ fusion_hap.main()
+
+ # check api was called correctly
+ api_obj.list_host_access_policies.assert_called_once_with()
+ api_obj.get_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ api_obj.create_host_access_policy.assert_not_called()
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_hap_create_exception(
+ m_hap_api, m_op_api, exec_original, exec_catch, module_args, current_hap_list
+):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(return_value=current_hap_list)
+ api_obj.get_host_access_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_host_access_policy = MagicMock(side_effect=exec_original)
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(return_value=OperationMock(3))
+ m_hap_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_hap.main()
+
+ # check api was called correctly
+ api_obj.list_host_access_policies.assert_called_once_with()
+ api_obj.get_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ api_obj.create_host_access_policy.assert_called_once_with(
+ purefusion.HostAccessPoliciesPost(
+ iqn=module_args["iqn"],
+ personality=module_args["personality"],
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ )
+ )
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+def test_hap_create_op_fails(m_hap_api, m_op_api, module_args, current_hap_list):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(return_value=current_hap_list)
+ api_obj.get_host_access_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_host_access_policy = MagicMock(return_value=OperationMock(1))
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(return_value=OperationMock(3))
+ m_hap_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_hap.main()
+
+ # check api was called correctly
+ api_obj.list_host_access_policies.assert_called_once_with()
+ api_obj.get_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ api_obj.create_host_access_policy.assert_called_once_with(
+ purefusion.HostAccessPoliciesPost(
+ iqn=module_args["iqn"],
+ personality=module_args["personality"],
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ )
+ )
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_hap_create_op_exception(
+ m_hap_api, m_op_api, exec_original, exec_catch, module_args, current_hap_list
+):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(return_value=current_hap_list)
+ api_obj.get_host_access_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_host_access_policy = MagicMock(return_value=OperationMock(1))
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(return_value=OperationMock(3))
+ m_hap_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_hap.main()
+
+ # check api was called correctly
+ api_obj.list_host_access_policies.assert_called_once_with()
+ api_obj.get_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ api_obj.create_host_access_policy.assert_called_once_with(
+ purefusion.HostAccessPoliciesPost(
+ iqn=module_args["iqn"],
+ personality=module_args["personality"],
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ )
+ )
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_hap_list_exception(
+ m_hap_api, m_op_api, exec_original, exec_catch, module_args, current_hap_list
+):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(side_effect=exec_original)
+ api_obj.get_host_access_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_host_access_policy = MagicMock(side_effect=OperationMock(1))
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(return_value=OperationMock(3))
+ m_hap_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_hap.main()
+
+ # check api was called correctly
+ api_obj.list_host_access_policies.assert_called_once_with()
+ api_obj.get_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ api_obj.create_host_access_policy.assert_not_called()
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+def test_hap_update(m_hap_api, m_op_api, module_args, current_hap_list):
+ # NOTE: Host Access Policy does not have PATCH method, thus no action is expected
+ current_hap = current_hap_list.items[0]
+ module_args["name"] = current_hap.name
+ module_args["display_name"] = "New Display Name"
+ module_args["iqn"] = current_hap.iqn
+ module_args["personality"] = (
+ "windows" if current_hap.personality != "windows" else "linux"
+ )
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(return_value=current_hap_list)
+ api_obj.get_host_access_policy = MagicMock(return_value=current_hap)
+ api_obj.create_host_access_policy = MagicMock(return_value=OperationMock(1))
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(return_value=OperationMock(3))
+ m_hap_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_hap.main()
+
+ assert exc.value.changed is False
+
+ # check api was called correctly
+ api_obj.list_host_access_policies.assert_called_once_with()
+ api_obj.get_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ api_obj.create_host_access_policy.assert_not_called()
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+def test_hap_present_not_changed(m_hap_api, m_op_api, module_args, current_hap_list):
+ current_hap = current_hap_list.items[0]
+ module_args["name"] = current_hap.name
+ module_args["display_name"] = current_hap.display_name
+ module_args["iqn"] = current_hap.iqn
+ module_args["personality"] = current_hap.personality
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(return_value=current_hap_list)
+ api_obj.get_host_access_policy = MagicMock(return_value=current_hap)
+ api_obj.create_host_access_policy = MagicMock(return_value=OperationMock(1))
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(return_value=OperationMock(3))
+ m_hap_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_hap.main()
+
+ assert exc.value.changed is False
+
+ # check api was called correctly
+ api_obj.list_host_access_policies.assert_called_once_with()
+ api_obj.get_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ api_obj.create_host_access_policy.assert_not_called()
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+def test_hap_absent_not_changed(m_hap_api, m_op_api, module_args, current_hap_list):
+ module_args["state"] = "absent"
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(return_value=current_hap_list)
+ api_obj.get_host_access_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_host_access_policy = MagicMock(return_value=OperationMock(1))
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(return_value=OperationMock(3))
+ m_hap_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_hap.main()
+
+ assert exc.value.changed is False
+
+ # check api was called correctly
+ api_obj.list_host_access_policies.assert_called_once_with()
+ api_obj.get_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ api_obj.create_host_access_policy.assert_not_called()
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+def test_hap_delete(m_hap_api, m_op_api, module_args, current_hap_list):
+ current_hap = current_hap_list.items[0]
+ module_args["state"] = "absent"
+ module_args["name"] = current_hap.name
+ module_args["display_name"] = current_hap.display_name
+ module_args["iqn"] = current_hap.iqn
+ module_args["personality"] = current_hap.personality
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(return_value=current_hap_list)
+ api_obj.get_host_access_policy = MagicMock(return_value=current_hap)
+ api_obj.create_host_access_policy = MagicMock(return_value=OperationMock(1))
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(return_value=OperationMock(3))
+ m_hap_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_hap.main()
+
+ assert exc.value.changed is True
+
+ # check api was called correctly
+ api_obj.list_host_access_policies.assert_called_once_with()
+ api_obj.get_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ api_obj.create_host_access_policy.assert_not_called()
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_hap_delete_exception(
+ m_hap_api, m_op_api, exec_original, exec_catch, module_args, current_hap_list
+):
+ current_hap = current_hap_list.items[0]
+ module_args["state"] = "absent"
+ module_args["name"] = current_hap.name
+ module_args["display_name"] = current_hap.display_name
+ module_args["iqn"] = current_hap.iqn
+ module_args["personality"] = current_hap.personality
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(return_value=current_hap_list)
+ api_obj.get_host_access_policy = MagicMock(return_value=current_hap)
+ api_obj.create_host_access_policy = MagicMock(return_value=OperationMock(1))
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(side_effect=exec_original)
+ m_hap_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_hap.main()
+
+ # check api was called correctly
+ api_obj.list_host_access_policies.assert_called_once_with()
+ api_obj.get_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ api_obj.create_host_access_policy.assert_not_called()
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+def test_hap_delete_op_fails(m_hap_api, m_op_api, module_args, current_hap_list):
+ current_hap = current_hap_list.items[0]
+ module_args["state"] = "absent"
+ module_args["name"] = current_hap.name
+ module_args["display_name"] = current_hap.display_name
+ module_args["iqn"] = current_hap.iqn
+ module_args["personality"] = current_hap.personality
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(return_value=current_hap_list)
+ api_obj.get_host_access_policy = MagicMock(return_value=current_hap)
+ api_obj.create_host_access_policy = MagicMock(return_value=OperationMock(1))
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(return_value=OperationMock(3))
+ m_hap_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_hap.main()
+
+ # check api was called correctly
+ api_obj.list_host_access_policies.assert_called_once_with()
+ api_obj.get_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ api_obj.create_host_access_policy.assert_not_called()
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.HostAccessPoliciesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_hap_delete_op_exception(
+ m_hap_api, m_op_api, exec_original, exec_catch, module_args, current_hap_list
+):
+ current_hap = current_hap_list.items[0]
+ module_args["state"] = "absent"
+ module_args["name"] = current_hap.name
+ module_args["display_name"] = current_hap.display_name
+ module_args["iqn"] = current_hap.iqn
+ module_args["personality"] = current_hap.personality
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.list_host_access_policies = MagicMock(return_value=current_hap_list)
+ api_obj.get_host_access_policy = MagicMock(return_value=current_hap)
+ api_obj.create_host_access_policy = MagicMock(return_value=OperationMock(1))
+ api_obj.update_host_access_policy = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_host_access_policy = MagicMock(return_value=OperationMock(3))
+ m_hap_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_hap.main()
+
+ # check api was called correctly
+ api_obj.list_host_access_policies.assert_called_once_with()
+ api_obj.get_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ api_obj.create_host_access_policy.assert_not_called()
+ api_obj.update_host_access_policy.assert_not_called()
+ api_obj.delete_host_access_policy.assert_called_once_with(
+ host_access_policy_name=module_args["name"]
+ )
+ op_obj.get_operation.assert_called_once_with(3)
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_hw.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_hw.py
new file mode 100644
index 000000000..3ad109b64
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_hw.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock
+
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_hw
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ exit_json,
+ fail_json,
+ set_module_args,
+)
+
+# GLOBAL MOCKS
+fusion_hw.setup_fusion = MagicMock(return_value=purefusion.api_client.ApiClient())
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+
+@pytest.mark.parametrize(
+ "module_args",
+ [
+ # unknown parameter 'extra' is provided
+ {
+ "state": "present",
+ "name": "hw1",
+ "display_name": "Hardware Type 1",
+ "array_type": "FA//X",
+ "media_type": "random",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ "extra": "value",
+ },
+ # parameter 'state` has incorrect value
+ {
+ "state": "cool",
+ "name": "hw1",
+ "display_name": "Hardware Type 1",
+ "array_type": "FA//X",
+ "media_type": "random",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ },
+ # parameter 'state` has incorrect value
+ {
+ "state": "absent",
+ "name": "hw1",
+ "display_name": "Hardware Type 1",
+ "array_type": "FA//X",
+ "media_type": "random",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ },
+ # parameter 'array_type` has incorrect value
+ {
+ "state": "present",
+ "name": "hw1",
+ "display_name": "Hardware Type 1",
+ "array_type": "wrong",
+ "media_type": "random",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ },
+ ],
+)
+def test_module_fails_on_wrong_parameters(module_args):
+ set_module_args(module_args)
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_hw.main()
+
+
+@pytest.mark.parametrize("state", [None, "present"])
+@pytest.mark.parametrize("name", [None, "hw_type_name1"])
+@pytest.mark.parametrize("display_name", [None, "Super weird Display Name 12 3"])
+@pytest.mark.parametrize("array_type", [None, "FA//X", "FA//C"])
+@pytest.mark.parametrize("media_type", [None, "random"])
+def test_hw_does_not_call_api(state, name, display_name, array_type, media_type):
+ module_args = {
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ if state is not None:
+ module_args["state"] = state
+ if name is not None:
+ module_args["name"] = name
+ if display_name is not None:
+ module_args["display_name"] = display_name
+ if array_type is not None:
+ module_args["array_type"] = array_type
+ if media_type is not None:
+ module_args["media_type"] = media_type
+ set_module_args(module_args)
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_hw.main()
+
+ assert exc.value.changed is False
+
+ # NOTE: api call assertion is handled by global mock
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_info.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_info.py
new file mode 100644
index 000000000..784b550cd
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_info.py
@@ -0,0 +1,2383 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+import os
+from itertools import combinations
+from unittest.mock import MagicMock, call, patch
+
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_info
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ exit_json,
+ fail_json,
+ set_module_args,
+)
+from ansible_collections.purestorage.fusion.tests.helpers import (
+ ApiExceptionsMockGenerator,
+)
+from urllib3.exceptions import HTTPError
+import time
+
+# GLOBAL MOCKS
+fusion_info.setup_fusion = MagicMock(return_value=purefusion.api_client.ApiClient())
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+VALID_SUBSETS = {
+ "all",
+ "minimum",
+ "roles",
+ "users",
+ "arrays",
+ "hardware_types",
+ "volumes",
+ "host_access_policies",
+ "storage_classes",
+ "protection_policies",
+ "placement_groups",
+ "network_interfaces",
+ "availability_zones",
+ "storage_endpoints",
+ "snapshots",
+ "storage_services",
+ "tenants",
+ "tenant_spaces",
+ "network_interface_groups",
+ "api_clients",
+ "regions",
+}
+
+EXPECTED_KEYS = {
+ "all": {
+ "default",
+ "hardware_types",
+ "users",
+ "availability_zones",
+ "roles",
+ "role_assignments",
+ "storage_services",
+ "volumes",
+ "protection_policies",
+ "placement_groups",
+ "storage_classes",
+ "network_interfaces",
+ "host_access_policies",
+ "tenants",
+ "tenant_spaces",
+ "storage_endpoints",
+ "api_clients",
+ "network_interface_groups",
+ "volume_snapshots",
+ "snapshots",
+ "arrays",
+ "regions",
+ },
+ "minimum": {"default"},
+ "arrays": {"arrays"},
+ "hardware_types": {"hardware_types"},
+ "users": {"users"},
+ "availability_zones": {"availability_zones"},
+ "roles": {"roles", "role_assignments"},
+ "storage_services": {"storage_services"},
+ "volumes": {"volumes"},
+ "protection_policies": {"protection_policies"},
+ "placement_groups": {"placement_groups"},
+ "storage_classes": {"storage_classes"},
+ "network_interfaces": {"network_interfaces"},
+ "host_access_policies": {"host_access_policies"},
+ "tenants": {"tenants"},
+ "tenant_spaces": {"tenant_spaces"},
+ "storage_endpoints": {"storage_endpoints"},
+ "api_clients": {"api_clients"},
+ "network_interface_groups": {"network_interface_groups"},
+ "snapshots": {"snapshots", "volume_snapshots"},
+ "regions": {"regions"},
+}
+
+RESP_VERSION = purefusion.Version(version=1)
+RESP_AS = purefusion.Space(
+ resource=purefusion.ResourceReference(
+ id="333",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ total_physical_space=1,
+ unique_space=1,
+ snapshot_space=1,
+)
+RESP_AP = purefusion.Performance(
+ resource=purefusion.ResourceReference(
+ id="222",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ reads_per_sec=12345,
+ read_latency_us=1000,
+ read_bandwidth=5000000,
+ writes_per_sec=12611,
+ write_latency_us=2000,
+ write_bandwidth=4000000,
+)
+RESP_LU = [
+ purefusion.User(
+ id="390",
+ name="username1",
+ self_link="self_link_value",
+ display_name="User's Name 1",
+ email="user1@email.com",
+ ),
+ purefusion.User(
+ id="391",
+ name="username2",
+ self_link="self_link_value",
+ display_name="User's Name 2",
+ email="user2@email.com",
+ ),
+]
+RESP_PP = purefusion.ProtectionPolicyList(
+ count=2,
+ more_items_remaining=False,
+ items=[
+ purefusion.ProtectionPolicy(
+ id="098",
+ name="pp1",
+ self_link="self_link_value",
+ display_name="Protection Policy 1",
+ objectives=[],
+ ),
+ purefusion.ProtectionPolicy(
+ id="099",
+ name="pp2",
+ self_link="self_link_value",
+ display_name="Protection Policy 2",
+ objectives=[],
+ ),
+ ],
+)
+RESP_HAP = purefusion.HostAccessPolicyList(
+ count=2,
+ more_items_remaining=False,
+ items=[
+ purefusion.HostAccessPolicy(
+ id="900",
+ name="hap1",
+ self_link="self_link_value",
+ display_name="Host Access Policy 1",
+ iqn="iqn.2023-05.com.purestorage:420qp2c0261",
+ personality="aix",
+ ),
+ purefusion.HostAccessPolicy(
+ id="901",
+ name="hap2",
+ self_link="self_link_value",
+ display_name="Host Access Policy 2",
+ iqn="iqn.2023-05.com.purestorage:420qp2c0262",
+ personality="linux",
+ ),
+ ],
+)
+RESP_HT = purefusion.HardwareTypeList(
+ count=2,
+ more_items_remaining=False,
+ items=[
+ purefusion.HardwareType(
+ id="500",
+ name="ht1",
+ self_link="self_link_value",
+ display_name="Hardware Type 1",
+ array_type="FA//X",
+ media_type="whatever",
+ ),
+ purefusion.HardwareType(
+ id="501",
+ name="ht2",
+ self_link="self_link_value",
+ display_name="Hardware Type 2",
+ array_type="FA//C",
+ media_type="whatever",
+ ),
+ ],
+)
+RESP_SS = purefusion.StorageServiceList(
+ count=2,
+ more_items_remaining=False,
+ items=[
+ purefusion.StorageService(
+ id="502",
+ name="ss1",
+ self_link="self_link_value",
+ display_name="Storage Service 1",
+ hardware_types=[
+ purefusion.HardwareTypeRef(
+ id="910",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ purefusion.HardwareTypeRef(
+ id="911",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ ],
+ ),
+ purefusion.StorageService(
+ id="503",
+ name="ss2",
+ self_link="self_link_value",
+ display_name="Storage Service 3",
+ hardware_types=[
+ purefusion.HardwareTypeRef(
+ id="912",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ )
+ ],
+ ),
+ ],
+)
+RESP_TENANTS = purefusion.TenantList(
+ count=2,
+ more_items_remaining=False,
+ items=[
+ purefusion.Tenant(
+ id="504",
+ name="t1",
+ self_link="self_link_value",
+ display_name="Tenant 1",
+ tenant_spaces_link="ts_link",
+ ),
+ purefusion.Tenant(
+ id="505",
+ name="t2",
+ self_link="self_link_value",
+ display_name="Tenant 2",
+ tenant_spaces_link="ts_link",
+ ),
+ ],
+)
+RESP_REGIONS = purefusion.RegionList(
+ count=2,
+ more_items_remaining=False,
+ items=[
+ purefusion.Region(
+ id="506",
+ name="region1",
+ self_link="self_link_value",
+ display_name="Region 1",
+ ),
+ purefusion.Region(
+ id="507",
+ name="region2",
+ self_link="self_link_value",
+ display_name="Region 2",
+ ),
+ ],
+)
+RESP_ROLES = [
+ purefusion.Role(
+ id="902",
+ name="role1",
+ self_link="self_link_value",
+ display_name="Role 1",
+ description="nice description",
+ assignable_scopes=["scope1", "scope2"],
+ ),
+ purefusion.Role(
+ id="903",
+ name="role2",
+ self_link="self_link_value",
+ display_name="Role 2",
+ description="not so nice description",
+ assignable_scopes=["scope3", "scope2"],
+ ),
+]
+RESP_SC = purefusion.StorageClassList(
+ count=2,
+ more_items_remaining=False,
+ items=[
+ purefusion.StorageClass(
+ id="508",
+ name="sc1",
+ self_link="self_link_value",
+ display_name="Storage Class 1",
+ storage_service=purefusion.StorageServiceRef(
+ id="509",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ size_limit=12345678,
+ iops_limit=10000,
+ bandwidth_limit=2000000,
+ ),
+ purefusion.StorageClass(
+ id="510",
+ name="sc2",
+ self_link="self_link_value",
+ display_name="Storage Class 2",
+ storage_service=purefusion.StorageServiceRef(
+ id="511",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ size_limit=12345679,
+ iops_limit=10001,
+ bandwidth_limit=2000001,
+ ),
+ ],
+)
+RESP_RA = [
+ purefusion.RoleAssignment(
+ id="904",
+ name="ra1",
+ self_link="self_link_value",
+ display_name="Role Assignment 1",
+ role=purefusion.RoleRef(
+ id="512",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ scope=purefusion.ResourceReference(
+ id="513",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ principal="user1",
+ ),
+ purefusion.RoleAssignment(
+ id="905",
+ name="ra2",
+ self_link="self_link_value",
+ display_name="Role Assignment 2",
+ role=purefusion.RoleRef(
+ id="513",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ scope=purefusion.ResourceReference(
+ id="514",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ principal="user2",
+ ),
+]
+RESP_TS = purefusion.TenantSpaceList(
+ count=2,
+ more_items_remaining=False,
+ items=[
+ purefusion.TenantSpace(
+ id="515",
+ name="ts1",
+ self_link="self_link_value",
+ display_name="Tenant Space 1",
+ tenant=RESP_TENANTS.items[0].name,
+ volumes_link="link_value1",
+ snapshots_link="link_value2",
+ placement_groups_link="link_value3",
+ ),
+ purefusion.TenantSpace(
+ id="516",
+ name="ts2",
+ self_link="self_link_value",
+ display_name="Tenant Space 2",
+ tenant=RESP_TENANTS.items[1].name,
+ volumes_link="link_value4",
+ snapshots_link="link_value5",
+ placement_groups_link="link_value6",
+ ),
+ ],
+)
+RESP_VOLUMES = purefusion.VolumeList(
+ count=1,
+ more_items_remaining=False,
+ items=[
+ purefusion.Volume(
+ id="517",
+ name="volume1",
+ self_link="self_link_value",
+ display_name="Volume 1",
+ size=4000000,
+ tenant=purefusion.TenantRef(
+ id="518",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="519",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ storage_class=purefusion.StorageClassRef(
+ id="520",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ protection_policy=purefusion.ProtectionPolicyRef(
+ id="521",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ placement_group=purefusion.PlacementGroupRef(
+ id="522",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ array=purefusion.ArrayRef(
+ id="905",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ created_at=485743825,
+ source_volume_snapshot=purefusion.VolumeSnapshotRef(
+ id="523",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ host_access_policies=[
+ purefusion.HostAccessPolicyRef(
+ id="524",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ )
+ ],
+ serial_number="123923482034",
+ target=purefusion.Target(
+ iscsi=purefusion.Iscsi(
+ iqn="iqn.2023-05.com.purestorage:420qp2c0222",
+ addresses=["125.1.2.4"],
+ )
+ ),
+ time_remaining=1000000,
+ destroyed=False,
+ )
+ ],
+)
+RESP_PG = purefusion.PlacementGroupList(
+ count=1,
+ more_items_remaining=False,
+ items=[
+ purefusion.PlacementGroup(
+ id="525",
+ name="pg1",
+ self_link="self_link_value",
+ display_name="Placement Group 1",
+ tenant=purefusion.TenantRef(
+ id="526",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="527",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ placement_engine=purefusion.PlacementEngine(),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="528",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ protocols=purefusion.Target(
+ iscsi=purefusion.Iscsi(
+ iqn="iqn.2023-05.com.purestorage:420qp2c0211",
+ addresses=["125.1.2.5"],
+ )
+ ),
+ storage_service=purefusion.StorageServiceRef(
+ id="529",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ array=purefusion.ArrayRef(
+ id="530",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ )
+ ],
+)
+RESP_SNAPSHOTS = purefusion.SnapshotList(
+ count=1,
+ more_items_remaining=False,
+ items=[
+ purefusion.Snapshot(
+ id="531",
+ name="snapshot1",
+ self_link="self_link_value",
+ display_name="Snapshot 1",
+ tenant=purefusion.TenantRef(
+ id="531",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="532",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ volume_snapshots_link="link_to_volume_snapshot",
+ protection_policy=purefusion.ProtectionPolicyRef(
+ id="533",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ time_remaining=23432,
+ destroyed=False,
+ )
+ ],
+)
+RESP_AZ = purefusion.AvailabilityZoneList(
+ count=3,
+ more_items_remaining=False,
+ items=[
+ purefusion.AvailabilityZone(
+ id="534",
+ name="az1",
+ self_link="self_link_value",
+ display_name="Availability Zone 1",
+ region=purefusion.RegionRef(
+ id="535",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ ),
+ purefusion.AvailabilityZone(
+ id="536",
+ name="az2",
+ self_link="self_link_value",
+ display_name="Availability Zone 2",
+ region=purefusion.RegionRef(
+ id="537",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ ),
+ purefusion.AvailabilityZone(
+ id="537",
+ name="az3",
+ self_link="self_link_value",
+ display_name="Availability Zone 3",
+ region=purefusion.RegionRef(
+ id="538",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ ),
+ ],
+)
+RESP_NIG = purefusion.NetworkInterfaceGroupList(
+ count=1,
+ more_items_remaining=False,
+ items=[
+ purefusion.NetworkInterfaceGroup(
+ id="538",
+ name="nig1",
+ self_link="self_link_value",
+ display_name="Network Interface Group 1",
+ region=purefusion.RegionRef(
+ id="539",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="540",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ group_type="eth",
+ eth=purefusion.NetworkInterfaceGroupEth(
+ prefix="10.21.200.0/24", gateway="10.21.200.1", vlan=None, mtu=1600
+ ),
+ )
+ ],
+)
+RESP_SE = purefusion.StorageEndpointList(
+ count=1,
+ more_items_remaining=False,
+ items=[
+ purefusion.StorageEndpoint(
+ id="541",
+ name="se1",
+ self_link="self_link_value",
+ display_name="Storage Endpoint 1",
+ region=purefusion.RegionRef(
+ id="542",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="543",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ endpoint_type="iscsi",
+ iscsi=purefusion.StorageEndpointIscsi(
+ discovery_interfaces=[
+ purefusion.StorageEndpointIscsiDiscoveryInterface(
+ address="10.21.200.5/24",
+ gateway="10.21.200.0",
+ mtu=2000,
+ network_interface_groups=[
+ purefusion.NetworkInterfaceGroupRef(
+ id="544",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ ],
+ ),
+ purefusion.StorageEndpointIscsiDiscoveryInterface(
+ address="10.21.200.6/24",
+ gateway="10.21.200.0",
+ mtu=2100,
+ network_interface_groups=[],
+ ),
+ ]
+ ),
+ )
+ ],
+)
+RESP_NI = purefusion.NetworkInterfaceList(
+ count=1,
+ more_items_remaining=False,
+ items=[
+ purefusion.NetworkInterface(
+ id="545",
+ name="ni1",
+ self_link="self_link_value",
+ display_name="Network Interface 1",
+ region=purefusion.RegionRef(
+ id="546",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="547",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ array=purefusion.ArrayRef(
+ id="548",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ interface_type="eth",
+ eth=purefusion.NetworkInterfaceEth(
+ address="10.21.200.6/24",
+ gateway="10.21.200.0",
+ mac_address="E3-18-55-D8-8C-F4",
+ mtu=1233,
+ vlan=2,
+ ),
+ services=["a", "b"],
+ enabled=True,
+ network_interface_group=purefusion.NetworkInterfaceGroupRef(
+ id="906",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ max_speed=3224232,
+ )
+ ],
+)
+RESP_ARRAYS = purefusion.ArrayList(
+ count=1,
+ more_items_remaining=False,
+ items=[
+ purefusion.Array(
+ id="549",
+ name="array1",
+ self_link="self_link_value",
+ display_name="Array 1",
+ apartment_id="234214351",
+ hardware_type=purefusion.HardwareTypeRef(
+ id="550",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ region=purefusion.RegionRef(
+ id="551",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="552",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ appliance_id="2734298849",
+ host_name="super_host",
+ maintenance_mode=False,
+ unavailable_mode=False,
+ )
+ ],
+)
+RESP_AC = [
+ purefusion.APIClient(
+ id="553",
+ name="client1",
+ self_link="self_link_value",
+ display_name="API Client 1",
+ issuer="apikey:name:thisisnotreal",
+ public_key="0123456789",
+ last_key_update=1684421184201,
+ last_used=1684421290201,
+ creator_id="1234",
+ ),
+ purefusion.APIClient(
+ id="554",
+ name="client2",
+ self_link="self_link_value",
+ display_name="API Client 2",
+ issuer="apikey:name:thisissuperreal",
+ public_key="0987654321",
+ last_key_update=1684421184201,
+ last_used=1684421290201,
+ creator_id="4321",
+ ),
+]
+RESP_VS = purefusion.VolumeSnapshotList(
+ count=1,
+ more_items_remaining=False,
+ items=[
+ purefusion.VolumeSnapshot(
+ id="555",
+ name="vs1",
+ self_link="self_link_value",
+ display_name="Volume Snapshot 1",
+ serial_number="235235235345",
+ volume_serial_number="544236456346345",
+ created_at=1684421184201,
+ consistency_id="666666",
+ destroyed=False,
+ time_remaining=1684421290201,
+ size=19264036,
+ tenant=purefusion.TenantRef(
+ id="556",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="557",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ snapshot=purefusion.VolumeSnapshotRef(
+ id="558",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ volume=purefusion.VolumeRef(
+ id="559",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ protection_policy=purefusion.ProtectionPolicyRef(
+ id="560",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ placement_group=purefusion.PlacementGroupRef(
+ id="561",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ )
+ ],
+)
+
+
+@patch.dict(os.environ, {"TZ": "UTC"})
+@patch("fusion.DefaultApi")
+@patch("fusion.IdentityManagerApi")
+@patch("fusion.ProtectionPoliciesApi")
+@patch("fusion.HostAccessPoliciesApi")
+@patch("fusion.HardwareTypesApi")
+@patch("fusion.StorageServicesApi")
+@patch("fusion.TenantsApi")
+@patch("fusion.RegionsApi")
+@patch("fusion.RolesApi")
+@patch("fusion.StorageClassesApi")
+@patch("fusion.RoleAssignmentsApi")
+@patch("fusion.TenantSpacesApi")
+@patch("fusion.VolumesApi")
+@patch("fusion.VolumeSnapshotsApi")
+@patch("fusion.PlacementGroupsApi")
+@patch("fusion.SnapshotsApi")
+@patch("fusion.AvailabilityZonesApi")
+@patch("fusion.ArraysApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+@patch("fusion.StorageEndpointsApi")
+@patch("fusion.NetworkInterfacesApi")
+@pytest.mark.parametrize(
+ # all single options + all subsets of two options
+ "gather_subset",
+ [
+ *combinations(
+ VALID_SUBSETS,
+ 2,
+ ),
+ *[[option] for option in VALID_SUBSETS],
+ ],
+)
+def test_info_gather_subset(
+ # API mocks
+ m_ni_api,
+ m_se_api,
+ m_nig_api,
+ m_array_api,
+ m_az_api,
+ m_snapshot_api,
+ m_pg_api,
+ m_vs_api,
+ m_volume_api,
+ m_ts_api,
+ m_ra_api,
+ m_sc_api,
+ m_role_api,
+ m_region_api,
+ m_tenant_api,
+ m_ss_api,
+ m_hw_api,
+ m_hap_api,
+ m_pp_api,
+ m_im_api,
+ m_default_api,
+ # test parameters
+ gather_subset,
+):
+ """
+ Test that fusion_info module accepts single 'gather_subset' options and all subsets of two 'gather_subset' options.
+ """
+ # NOTE: here we use the same MagicMock object for all APIs to make the test simpler, this has no harm to the logic of the test
+ api_obj = MagicMock()
+ api_obj.get_version = MagicMock(return_value=RESP_VERSION)
+ api_obj.get_array_space = MagicMock(return_value=RESP_AS)
+ api_obj.get_array_performance = MagicMock(return_value=RESP_AP)
+ api_obj.list_users = MagicMock(return_value=RESP_LU)
+ api_obj.list_protection_policies = MagicMock(return_value=RESP_PP)
+ api_obj.list_host_access_policies = MagicMock(return_value=RESP_HAP)
+ api_obj.list_hardware_types = MagicMock(return_value=RESP_HT)
+ api_obj.list_storage_services = MagicMock(return_value=RESP_SS)
+ api_obj.list_tenants = MagicMock(return_value=RESP_TENANTS)
+ api_obj.list_regions = MagicMock(return_value=RESP_REGIONS)
+ api_obj.list_roles = MagicMock(return_value=RESP_ROLES)
+ api_obj.list_storage_classes = MagicMock(return_value=RESP_SC)
+ api_obj.list_role_assignments = MagicMock(return_value=RESP_RA)
+ api_obj.list_tenant_spaces = MagicMock(return_value=RESP_TS)
+ api_obj.list_volumes = MagicMock(return_value=RESP_VOLUMES)
+ api_obj.list_placement_groups = MagicMock(return_value=RESP_PG)
+ api_obj.list_snapshots = MagicMock(return_value=RESP_SNAPSHOTS)
+ api_obj.list_availability_zones = MagicMock(return_value=RESP_AZ)
+ api_obj.list_network_interface_groups = MagicMock(return_value=RESP_NIG)
+ api_obj.list_storage_endpoints = MagicMock(return_value=RESP_SE)
+ api_obj.list_network_interfaces = MagicMock(return_value=RESP_NI)
+ api_obj.list_arrays = MagicMock(return_value=RESP_ARRAYS)
+ api_obj.list_api_clients = MagicMock(return_value=RESP_AC)
+ api_obj.list_volume_snapshots = MagicMock(return_value=RESP_VS)
+ m_ni_api.return_value = api_obj
+ m_se_api.return_value = api_obj
+ m_nig_api.return_value = api_obj
+ m_array_api.return_value = api_obj
+ m_az_api.return_value = api_obj
+ m_snapshot_api.return_value = api_obj
+ m_pg_api.return_value = api_obj
+ m_vs_api.return_value = api_obj
+ m_volume_api.return_value = api_obj
+ m_ts_api.return_value = api_obj
+ m_ra_api.return_value = api_obj
+ m_sc_api.return_value = api_obj
+ m_role_api.return_value = api_obj
+ m_region_api.return_value = api_obj
+ m_tenant_api.return_value = api_obj
+ m_ss_api.return_value = api_obj
+ m_hw_api.return_value = api_obj
+ m_hap_api.return_value = api_obj
+ m_pp_api.return_value = api_obj
+ m_im_api.return_value = api_obj
+ m_default_api.return_value = api_obj
+
+ time.tzset()
+
+ set_module_args(
+ {
+ "gather_subset": gather_subset,
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ )
+
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_info.main()
+
+ assert exc.value.changed is False
+
+ expected_keys = {}
+ for option in gather_subset:
+ expected_keys = {*expected_keys, *EXPECTED_KEYS[option]}
+
+ assert exc.value.fusion_info.keys() == expected_keys
+
+ if "hardware_types" in gather_subset or "all" in gather_subset:
+ api_obj.list_hardware_types.assert_called_with()
+ assert "hardware_types" in exc.value.fusion_info
+ assert exc.value.fusion_info["hardware_types"] == {
+ hw_type.name: {
+ "array_type": hw_type.array_type,
+ "display_name": hw_type.display_name,
+ "media_type": hw_type.media_type,
+ }
+ for hw_type in RESP_HT.items
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_hardware_types.assert_called_with()
+ assert "default" in exc.value.fusion_info
+ assert "hardware_types" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["hardware_types"] == len(RESP_HT.items)
+ else:
+ api_obj.list_hardware_types.assert_not_called()
+
+ if "users" in gather_subset or "all" in gather_subset:
+ api_obj.list_users.assert_called_with()
+ assert "users" in exc.value.fusion_info
+ assert exc.value.fusion_info["users"] == {
+ user.name: {
+ "display_name": user.display_name,
+ "email": user.email,
+ "id": user.id,
+ }
+ for user in RESP_LU
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_users.assert_called_with()
+ assert "default" in exc.value.fusion_info
+ assert "users" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["users"] == len(RESP_LU)
+ else:
+ api_obj.list_users.assert_not_called()
+
+ if "availability_zones" in gather_subset or "all" in gather_subset:
+ api_obj.list_regions.assert_called_with()
+ api_obj.list_availability_zones.assert_has_calls(
+ [call(region_name=region.name) for region in RESP_REGIONS.items],
+ any_order=True,
+ )
+ assert "availability_zones" in exc.value.fusion_info
+ assert exc.value.fusion_info["availability_zones"] == {
+ zone.name: {
+ "display_name": zone.display_name,
+ "region": zone.region.name,
+ }
+ for zone in RESP_AZ.items
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_regions.assert_called_with()
+ api_obj.list_availability_zones.assert_has_calls(
+ [call(region_name=region.name) for region in RESP_REGIONS.items],
+ any_order=True,
+ )
+ assert "default" in exc.value.fusion_info
+ assert "availability_zones" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["availability_zones"] == len(
+ RESP_REGIONS.items
+ ) * len(RESP_AZ.items)
+
+ if "roles" in gather_subset or "all" in gather_subset:
+ api_obj.list_roles.assert_called_with()
+ api_obj.list_role_assignments.assert_has_calls(
+ [call(role_name=role.name) for role in RESP_ROLES],
+ any_order=True,
+ )
+ assert "roles" in exc.value.fusion_info
+ assert "role_assignments" in exc.value.fusion_info
+ assert exc.value.fusion_info["roles"] == {
+ role.name: {
+ "display_name": role.display_name,
+ "scopes": role.assignable_scopes,
+ }
+ for role in RESP_ROLES
+ }
+ assert exc.value.fusion_info["role_assignments"] == {
+ ra.name: {
+ "display_name": ra.display_name,
+ "role": ra.role.name,
+ "scope": ra.scope.name,
+ }
+ for ra in RESP_RA
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_roles.assert_called_with()
+ api_obj.list_role_assignments.assert_has_calls(
+ [call(role_name=role.name) for role in RESP_ROLES],
+ any_order=True,
+ )
+ assert "default" in exc.value.fusion_info
+ assert "roles" in exc.value.fusion_info["default"]
+ assert "role_assignments" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["roles"] == len(RESP_ROLES)
+ assert exc.value.fusion_info["default"]["role_assignments"] == len(
+ RESP_ROLES
+ ) * len(RESP_RA)
+ else:
+ api_obj.list_roles.assert_not_called()
+ api_obj.list_role_assignments.assert_not_called()
+
+ if "storage_services" in gather_subset or "all" in gather_subset:
+ api_obj.list_storage_services.assert_called_with()
+ assert "storage_services" in exc.value.fusion_info
+ assert exc.value.fusion_info["storage_services"] == {
+ service.name: {
+ "display_name": service.display_name,
+ "hardware_types": [hwtype.name for hwtype in service.hardware_types],
+ }
+ for service in RESP_SS.items
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_storage_services.assert_called_with()
+ assert "default" in exc.value.fusion_info
+ assert "storage_services" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["storage_services"] == len(
+ RESP_SS.items
+ )
+
+ if "volumes" in gather_subset or "all" in gather_subset:
+ api_obj.list_tenants.assert_called_with()
+ api_obj.list_tenant_spaces.assert_has_calls(
+ [call(tenant_name=tenant.name) for tenant in RESP_TENANTS.items],
+ any_order=True,
+ )
+ api_obj.list_volumes.assert_has_calls(
+ [
+ call(
+ tenant_name=tenant.name,
+ tenant_space_name=ts.name,
+ )
+ for ts in RESP_TS.items
+ for tenant in RESP_TENANTS.items
+ ],
+ any_order=True,
+ )
+ assert "volumes" in exc.value.fusion_info
+ assert exc.value.fusion_info["volumes"] == {
+ tenant.name
+ + "/"
+ + tenant_space.name
+ + "/"
+ + volume.name: {
+ "tenant": tenant.name,
+ "tenant_space": tenant_space.name,
+ "name": volume.name,
+ "size": volume.size,
+ "display_name": volume.display_name,
+ "placement_group": volume.placement_group.name,
+ "source_volume_snapshot": getattr(
+ volume.source_volume_snapshot, "name", None
+ ),
+ "protection_policy": getattr(volume.protection_policy, "name", None),
+ "storage_class": volume.storage_class.name,
+ "serial_number": volume.serial_number,
+ "target": {
+ "iscsi": {
+ "addresses": volume.target.iscsi.addresses,
+ "iqn": volume.target.iscsi.iqn,
+ },
+ "nvme": {
+ "addresses": None,
+ "nqn": None,
+ },
+ "fc": {
+ "addresses": None,
+ "wwns": None,
+ },
+ },
+ "array": volume.array.name,
+ }
+ for volume in RESP_VOLUMES.items
+ for tenant_space in RESP_TS.items
+ for tenant in RESP_TENANTS.items
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_tenants.assert_called_with()
+ api_obj.list_tenant_spaces.assert_has_calls(
+ [call(tenant_name=tenant.name) for tenant in RESP_TENANTS.items],
+ any_order=True,
+ )
+ api_obj.list_volumes.assert_has_calls(
+ [
+ call(
+ tenant_name=tenant.name,
+ tenant_space_name=ts.name,
+ )
+ for ts in RESP_TS.items
+ for tenant in RESP_TENANTS.items
+ ],
+ any_order=True,
+ )
+ assert "default" in exc.value.fusion_info
+ assert "volumes" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["volumes"] == len(
+ RESP_TENANTS.items
+ ) * len(RESP_TS.items) * len(RESP_VOLUMES.items)
+ else:
+ api_obj.list_volumes.assert_not_called()
+
+ if "protection_policies" in gather_subset or "all" in gather_subset:
+ api_obj.list_protection_policies.assert_called_with()
+ assert "protection_policies" in exc.value.fusion_info
+ assert exc.value.fusion_info["protection_policies"] == {
+ policy.name: {
+ "objectives": policy.objectives,
+ }
+ for policy in RESP_PP.items
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_protection_policies.assert_called_with()
+ assert "default" in exc.value.fusion_info
+ assert "protection_policies" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["protection_policies"] == len(
+ RESP_PP.items
+ )
+ else:
+ api_obj.list_protection_policies.assert_not_called()
+
+ if "storage_classes" in gather_subset or "all" in gather_subset:
+ api_obj.list_storage_services.assert_called_with()
+ api_obj.list_storage_classes.assert_has_calls(
+ [call(storage_service_name=service.name) for service in RESP_SS.items],
+ any_order=True,
+ )
+ assert "storage_classes" in exc.value.fusion_info
+ assert exc.value.fusion_info["storage_classes"] == {
+ s_class.name: {
+ "bandwidth_limit": getattr(s_class, "bandwidth_limit", None),
+ "iops_limit": getattr(s_class, "iops_limit", None),
+ "size_limit": getattr(s_class, "size_limit", None),
+ "display_name": s_class.display_name,
+ "storage_service": service.name,
+ }
+ for s_class in RESP_SC.items
+ for service in RESP_SS.items
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_storage_services.assert_called_with()
+ api_obj.list_storage_classes.assert_has_calls(
+ [call(storage_service_name=service.name) for service in RESP_SS.items],
+ any_order=True,
+ )
+ assert "default" in exc.value.fusion_info
+ assert "storage_classes" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["storage_classes"] == len(
+ RESP_SC.items
+ ) * len(RESP_SS.items)
+ else:
+ api_obj.list_storage_classes.assert_not_called()
+
+ if "network_interfaces" in gather_subset or "all" in gather_subset:
+ api_obj.list_regions.assert_called_with()
+ api_obj.list_availability_zones.assert_has_calls(
+ [call(region_name=region.name) for region in RESP_REGIONS.items],
+ any_order=True,
+ )
+ api_obj.list_arrays.assert_has_calls(
+ [
+ call(
+ availability_zone_name=az.name,
+ region_name=region.name,
+ )
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ ],
+ any_order=True,
+ )
+ api_obj.list_network_interfaces.assert_has_calls(
+ [
+ call(
+ array_name=array.name,
+ availability_zone_name=az.name,
+ region_name=region.name,
+ )
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ for array in RESP_ARRAYS.items
+ ],
+ any_order=True,
+ )
+ assert "network_interfaces" in exc.value.fusion_info
+ assert exc.value.fusion_info["network_interfaces"] == {
+ az.name
+ + "/"
+ + array.name: {
+ nic.name: {
+ "enabled": nic.enabled,
+ "display_name": nic.display_name,
+ "interface_type": nic.interface_type,
+ "services": nic.services,
+ "max_speed": nic.max_speed,
+ "vlan": nic.eth.vlan,
+ "address": nic.eth.address,
+ "mac_address": nic.eth.mac_address,
+ "gateway": nic.eth.gateway,
+ "mtu": nic.eth.mtu,
+ "network_interface_group": nic.network_interface_group.name,
+ "availability_zone": nic.availability_zone.name,
+ }
+ for nic in RESP_NI.items
+ }
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ for array in RESP_ARRAYS.items
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_regions.assert_called_with()
+ api_obj.list_availability_zones.assert_has_calls(
+ [call(region_name=region.name) for region in RESP_REGIONS.items],
+ any_order=True,
+ )
+ api_obj.list_arrays.assert_has_calls(
+ [
+ call(
+ availability_zone_name=az.name,
+ region_name=region.name,
+ )
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ ],
+ any_order=True,
+ )
+ api_obj.list_network_interfaces.assert_has_calls(
+ [
+ call(
+ array_name=array.name,
+ availability_zone_name=az.name,
+ region_name=region.name,
+ )
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ for array in RESP_ARRAYS.items
+ ],
+ any_order=True,
+ )
+ assert "default" in exc.value.fusion_info
+ assert "network_interfaces" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["network_interfaces"] == len(
+ RESP_REGIONS.items
+ ) * len(RESP_AZ.items) * len(RESP_ARRAYS.items) * len(RESP_NI.items)
+ else:
+ api_obj.list_network_interfaces.assert_not_called()
+
+ if "host_access_policies" in gather_subset or "all" in gather_subset:
+ api_obj.list_host_access_policies.assert_called_with()
+ assert "host_access_policies" in exc.value.fusion_info
+ assert exc.value.fusion_info["host_access_policies"] == {
+ host.name: {
+ "personality": host.personality,
+ "display_name": host.display_name,
+ "iqn": host.iqn,
+ }
+ for host in RESP_HAP.items
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_host_access_policies.assert_called_with()
+ assert "default" in exc.value.fusion_info
+ assert "host_access_policies" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["host_access_policies"] == len(
+ RESP_HAP.items
+ )
+ else:
+ api_obj.list_host_access_policies.assert_not_called()
+
+ if "arrays" in gather_subset or "all" in gather_subset:
+ api_obj.list_regions.assert_called_with()
+ api_obj.list_availability_zones.assert_has_calls(
+ [call(region_name=region.name) for region in RESP_REGIONS.items],
+ any_order=True,
+ )
+ api_obj.list_arrays.assert_has_calls(
+ [
+ call(
+ availability_zone_name=az.name,
+ region_name=region.name,
+ )
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ ],
+ any_order=True,
+ )
+ api_obj.get_array_space.assert_has_calls(
+ [
+ call(
+ array_name=array.name,
+ availability_zone_name=az.name,
+ region_name=region.name,
+ )
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ for array in RESP_ARRAYS.items
+ ],
+ any_order=True,
+ )
+ api_obj.get_array_performance.assert_has_calls(
+ [
+ call(
+ array_name=array.name,
+ availability_zone_name=az.name,
+ region_name=region.name,
+ )
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ for array in RESP_ARRAYS.items
+ ],
+ any_order=True,
+ )
+ assert "arrays" in exc.value.fusion_info
+ assert exc.value.fusion_info["arrays"] == {
+ array.name: {
+ "region": region.name,
+ "availability_zone": az.name,
+ "host_name": array.host_name,
+ "maintenance_mode": array.maintenance_mode,
+ "unavailable_mode": array.unavailable_mode,
+ "display_name": array.display_name,
+ "hardware_type": array.hardware_type.name,
+ "appliance_id": array.appliance_id,
+ "apartment_id": getattr(array, "apartment_id", None),
+ "space": {
+ "total_physical_space": RESP_AS.total_physical_space,
+ },
+ "performance": {
+ "read_bandwidth": RESP_AP.read_bandwidth,
+ "read_latency_us": RESP_AP.read_latency_us,
+ "reads_per_sec": RESP_AP.reads_per_sec,
+ "write_bandwidth": RESP_AP.write_bandwidth,
+ "write_latency_us": RESP_AP.write_latency_us,
+ "writes_per_sec": RESP_AP.writes_per_sec,
+ },
+ }
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ for array in RESP_ARRAYS.items
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_regions.assert_called_with()
+ api_obj.list_availability_zones.assert_has_calls(
+ [call(region_name=region.name) for region in RESP_REGIONS.items],
+ any_order=True,
+ )
+ api_obj.list_arrays.assert_has_calls(
+ [
+ call(
+ availability_zone_name=az.name,
+ region_name=region.name,
+ )
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ ],
+ any_order=True,
+ )
+ api_obj.get_array_space.assert_not_called()
+ api_obj.get_array_performance.assert_not_called()
+ assert "default" in exc.value.fusion_info
+ assert "arrays" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["arrays"] == len(
+ RESP_REGIONS.items
+ ) * len(RESP_AZ.items) * len(RESP_ARRAYS.items)
+ else:
+ api_obj.get_array_space.assert_not_called()
+ api_obj.get_array_performance.assert_not_called()
+
+ if "tenants" in gather_subset or "all" in gather_subset:
+ api_obj.list_tenants.assert_called_with()
+ assert "tenants" in exc.value.fusion_info
+ assert exc.value.fusion_info["tenants"] == {
+ tenant.name: {
+ "display_name": tenant.display_name,
+ }
+ for tenant in RESP_TENANTS.items
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_tenants.assert_called_with()
+ assert "default" in exc.value.fusion_info
+ assert "tenants" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["tenants"] == len(RESP_TENANTS.items)
+
+ if "tenant_spaces" in gather_subset or "all" in gather_subset:
+ api_obj.list_tenants.assert_called_with()
+ api_obj.list_tenant_spaces.assert_has_calls(
+ [call(tenant_name=tenant.name) for tenant in RESP_TENANTS.items],
+ any_order=True,
+ )
+ assert "tenant_spaces" in exc.value.fusion_info
+ assert exc.value.fusion_info["tenant_spaces"] == {
+ tenant.name
+ + "/"
+ + ts.name: {
+ "tenant": tenant.name,
+ "display_name": ts.display_name,
+ }
+ for tenant in RESP_TENANTS.items
+ for ts in RESP_TS.items
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_tenants.assert_called_with()
+ api_obj.list_tenant_spaces.assert_has_calls(
+ [call(tenant_name=tenant.name) for tenant in RESP_TENANTS.items],
+ any_order=True,
+ )
+ assert "default" in exc.value.fusion_info
+ assert "tenant_spaces" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["tenant_spaces"] == len(
+ RESP_TENANTS.items
+ ) * len(RESP_TS.items)
+
+ if "storage_endpoints" in gather_subset or "all" in gather_subset:
+ api_obj.list_regions.assert_called_with()
+ api_obj.list_availability_zones.assert_has_calls(
+ [call(region_name=region.name) for region in RESP_REGIONS.items],
+ any_order=True,
+ )
+ api_obj.list_storage_endpoints.assert_has_calls(
+ [
+ call(
+ availability_zone_name=az.name,
+ region_name=region.name,
+ )
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ ],
+ any_order=True,
+ )
+ assert "storage_endpoints" in exc.value.fusion_info
+ assert exc.value.fusion_info["storage_endpoints"] == {
+ region.name
+ + "/"
+ + az.name
+ + "/"
+ + endpoint.name: {
+ "display_name": endpoint.display_name,
+ "endpoint_type": endpoint.endpoint_type,
+ "iscsi_interfaces": [
+ {
+ "address": iface.address,
+ "gateway": iface.gateway,
+ "mtu": iface.mtu,
+ "network_interface_groups": [
+ nig.name for nig in iface.network_interface_groups
+ ],
+ }
+ for iface in endpoint.iscsi.discovery_interfaces
+ ],
+ }
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ for endpoint in RESP_SE.items
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_regions.assert_called_with()
+ api_obj.list_availability_zones.assert_has_calls(
+ [call(region_name=region.name) for region in RESP_REGIONS.items],
+ any_order=True,
+ )
+ api_obj.list_storage_endpoints.assert_has_calls(
+ [
+ call(
+ availability_zone_name=az.name,
+ region_name=region.name,
+ )
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ ],
+ any_order=True,
+ )
+ assert "default" in exc.value.fusion_info
+ assert "storage_endpoints" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["storage_endpoints"] == len(
+ RESP_REGIONS.items
+ ) * len(RESP_AZ.items) * len(RESP_SE.items)
+ else:
+ api_obj.list_storage_endpoints.assert_not_called()
+
+ if "api_clients" in gather_subset or "all" in gather_subset:
+ api_obj.list_api_clients.assert_called_with()
+ assert "api_clients" in exc.value.fusion_info
+ assert exc.value.fusion_info["api_clients"] == {
+ client.name: {
+ "display_name": client.display_name,
+ "issuer": client.issuer,
+ "public_key": client.public_key,
+ "creator_id": client.creator_id,
+ "last_key_update": "Thu, 18 May 2023 14:46:24 UTC",
+ "last_used": "Thu, 18 May 2023 14:48:10 UTC",
+ }
+ for client in RESP_AC
+ }
+ elif "minimum" in gather_subset:
+ # api_clients is not in default dict
+ api_obj.list_api_clients.assert_not_called()
+ assert "default" in exc.value.fusion_info
+ assert "api_clients" not in exc.value.fusion_info["default"]
+ else:
+ api_obj.list_api_clients.assert_not_called()
+
+ if "snapshots" in gather_subset or "all" in gather_subset:
+ api_obj.list_tenants.assert_called_with()
+ api_obj.list_tenant_spaces.assert_has_calls(
+ [call(tenant_name=tenant.name) for tenant in RESP_TENANTS.items],
+ any_order=True,
+ )
+ api_obj.list_snapshots.assert_has_calls(
+ [
+ call(
+ tenant_name=tenant.name,
+ tenant_space_name=ts.name,
+ )
+ for ts in RESP_TS.items
+ for tenant in RESP_TENANTS.items
+ ],
+ any_order=True,
+ )
+ api_obj.list_volume_snapshots.assert_has_calls(
+ [
+ call(
+ tenant_name=tenant.name,
+ tenant_space_name=ts.name,
+ snapshot_name=snap.name,
+ )
+ for snap in RESP_SNAPSHOTS.items
+ for ts in RESP_TS.items
+ for tenant in RESP_TENANTS.items
+ ],
+ any_order=True,
+ )
+ assert "snapshots" in exc.value.fusion_info
+ assert "volume_snapshots" in exc.value.fusion_info
+ assert exc.value.fusion_info["snapshots"] == {
+ tenant.name
+ + "/"
+ + ts.name
+ + "/"
+ + snap.name: {
+ "display_name": snap.display_name,
+ "protection_policy": snap.protection_policy,
+ "time_remaining": "0 hours, 0 mins, 23 secs",
+ "volume_snapshots_link": snap.volume_snapshots_link,
+ }
+ for snap in RESP_SNAPSHOTS.items
+ for ts in RESP_TS.items
+ for tenant in RESP_TENANTS.items
+ }
+
+ assert exc.value.fusion_info["volume_snapshots"] == {
+ tenant.name
+ + "/"
+ + ts.name
+ + "/"
+ + snap.name
+ + "/"
+ + vsnap.name: {
+ "size": vsnap.size,
+ "display_name": vsnap.display_name,
+ "protection_policy": vsnap.protection_policy,
+ "serial_number": vsnap.serial_number,
+ "created_at": "Thu, 18 May 2023 14:46:24 UTC",
+ "time_remaining": "14 hours, 48 mins, 10 secs",
+ "placement_group": vsnap.placement_group.name,
+ }
+ for vsnap in RESP_VS.items
+ for snap in RESP_SNAPSHOTS.items
+ for ts in RESP_TS.items
+ for tenant in RESP_TENANTS.items
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_tenants.assert_called_with()
+ api_obj.list_tenant_spaces.assert_has_calls(
+ [call(tenant_name=tenant.name) for tenant in RESP_TENANTS.items],
+ any_order=True,
+ )
+ api_obj.list_snapshots.assert_has_calls(
+ [
+ call(
+ tenant_name=tenant.name,
+ tenant_space_name=ts.name,
+ )
+ for ts in RESP_TS.items
+ for tenant in RESP_TENANTS.items
+ ],
+ any_order=True,
+ )
+ api_obj.list_volume_snapshots.assert_not_called()
+ assert "default" in exc.value.fusion_info
+ assert "snapshots" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["snapshots"] == len(
+ RESP_TENANTS.items
+ ) * len(RESP_TS.items) * len(RESP_SNAPSHOTS.items)
+ else:
+ api_obj.list_snapshots.assert_not_called()
+ api_obj.list_volume_snapshots.assert_not_called()
+
+ if "network_interface_groups" in gather_subset or "all" in gather_subset:
+ api_obj.list_regions.assert_called_with()
+ api_obj.list_availability_zones.assert_has_calls(
+ [call(region_name=region.name) for region in RESP_REGIONS.items],
+ any_order=True,
+ )
+ api_obj.list_network_interface_groups.assert_has_calls(
+ [
+ call(
+ availability_zone_name=az.name,
+ region_name=region.name,
+ )
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ ],
+ any_order=True,
+ )
+ assert "network_interface_groups" in exc.value.fusion_info
+ assert exc.value.fusion_info["network_interface_groups"] == {
+ region.name
+ + "/"
+ + az.name
+ + "/"
+ + nig.name: {
+ "display_name": nig.display_name,
+ "gateway": nig.eth.gateway,
+ "prefix": nig.eth.prefix,
+ "mtu": nig.eth.mtu,
+ }
+ for nig in RESP_NIG.items
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_regions.assert_called_with()
+ api_obj.list_availability_zones.assert_has_calls(
+ [call(region_name=region.name) for region in RESP_REGIONS.items],
+ any_order=True,
+ )
+ api_obj.list_network_interface_groups.assert_has_calls(
+ [
+ call(
+ availability_zone_name=az.name,
+ region_name=region.name,
+ )
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ ],
+ any_order=True,
+ )
+ assert "default" in exc.value.fusion_info
+ assert "network_interface_groups" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["network_interface_groups"] == len(
+ RESP_NIG.items
+ ) * len(RESP_REGIONS.items) * len(RESP_AZ.items)
+ else:
+ api_obj.list_network_interface_groups.assert_not_called()
+
+ if "placement_groups" in gather_subset or "all" in gather_subset:
+ api_obj.list_tenants.assert_called_with()
+ api_obj.list_tenant_spaces.assert_has_calls(
+ [call(tenant_name=tenant.name) for tenant in RESP_TENANTS.items],
+ any_order=True,
+ )
+ api_obj.list_volumes.list_placement_groups(
+ [
+ call(
+ tenant_name=tenant.name,
+ tenant_space_name=ts.name,
+ )
+ for ts in RESP_TS.items
+ for tenant in RESP_TENANTS.items
+ ],
+ any_order=True,
+ )
+ assert "placement_groups" in exc.value.fusion_info
+ assert exc.value.fusion_info["placement_groups"] == {
+ tenant.name
+ + "/"
+ + ts.name
+ + "/"
+ + group.name: {
+ "tenant": group.tenant.name,
+ "display_name": group.display_name,
+ "placement_engine": group.placement_engine,
+ "tenant_space": group.tenant_space.name,
+ "az": group.availability_zone.name,
+ "array": getattr(group.array, "name", None),
+ }
+ for group in RESP_PG.items
+ for ts in RESP_TS.items
+ for tenant in RESP_TENANTS.items
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_tenants.assert_called_with()
+ api_obj.list_tenant_spaces.assert_has_calls(
+ [call(tenant_name=tenant.name) for tenant in RESP_TENANTS.items],
+ any_order=True,
+ )
+ api_obj.list_volumes.list_placement_groups(
+ [
+ call(
+ tenant_name=tenant.name,
+ tenant_space_name=ts.name,
+ )
+ for ts in RESP_TS.items
+ for tenant in RESP_TENANTS.items
+ ],
+ any_order=True,
+ )
+ assert "default" in exc.value.fusion_info
+ assert "placement_groups" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["placement_groups"] == len(
+ RESP_PG.items
+ ) * len(RESP_TENANTS.items) * len(RESP_TS.items)
+ else:
+ api_obj.list_placement_groups.assert_not_called()
+
+ if "regions" in gather_subset or "all" in gather_subset:
+ api_obj.list_regions.assert_called_with()
+ assert "regions" in exc.value.fusion_info
+ assert exc.value.fusion_info["regions"] == {
+ region.name: {
+ "display_name": region.display_name,
+ }
+ for region in RESP_REGIONS.items
+ }
+ elif "minimum" in gather_subset:
+ api_obj.list_regions.assert_called_with()
+ assert "default" in exc.value.fusion_info
+ assert "regions" in exc.value.fusion_info["default"]
+ assert exc.value.fusion_info["default"]["regions"] == len(RESP_REGIONS.items)
+
+
+@patch("fusion.DefaultApi")
+@patch("fusion.IdentityManagerApi")
+@patch("fusion.ProtectionPoliciesApi")
+@patch("fusion.HostAccessPoliciesApi")
+@patch("fusion.HardwareTypesApi")
+@patch("fusion.StorageServicesApi")
+@patch("fusion.TenantsApi")
+@patch("fusion.RegionsApi")
+@patch("fusion.RolesApi")
+@patch("fusion.StorageClassesApi")
+@patch("fusion.RoleAssignmentsApi")
+@patch("fusion.TenantSpacesApi")
+@patch("fusion.VolumesApi")
+@patch("fusion.VolumeSnapshotsApi")
+@patch("fusion.PlacementGroupsApi")
+@patch("fusion.SnapshotsApi")
+@patch("fusion.AvailabilityZonesApi")
+@patch("fusion.ArraysApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+@patch("fusion.StorageEndpointsApi")
+@patch("fusion.NetworkInterfacesApi")
+@pytest.mark.parametrize("subset", VALID_SUBSETS)
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ (ApiExceptionsMockGenerator.create_permission_denied(), AnsibleExitJson),
+ ],
+)
+def test_info_exception(
+ # API mocks
+ m_ni_api,
+ m_se_api,
+ m_nig_api,
+ m_array_api,
+ m_az_api,
+ m_snapshot_api,
+ m_pg_api,
+ m_vs_api,
+ m_volume_api,
+ m_ts_api,
+ m_ra_api,
+ m_sc_api,
+ m_role_api,
+ m_region_api,
+ m_tenant_api,
+ m_ss_api,
+ m_hw_api,
+ m_hap_api,
+ m_pp_api,
+ m_im_api,
+ m_default_api,
+ # test parameter
+ subset,
+ # exceptions
+ exec_original,
+ exec_catch,
+):
+ """
+ Test that fusion_info propagates exceptions correctly.
+ """
+ # NOTE: here we use the same MagicMock object for all APIs to make the test simpler, this has no harm to the logic of the test
+ api_obj = MagicMock()
+ api_obj.get_version = MagicMock(
+ return_value=RESP_VERSION, side_effect=exec_original
+ )
+ api_obj.get_array_space = MagicMock(return_value=RESP_AS, side_effect=exec_original)
+ api_obj.get_array_performance = MagicMock(
+ return_value=RESP_AP, side_effect=exec_original
+ )
+ api_obj.list_users = MagicMock(return_value=RESP_LU, side_effect=exec_original)
+ api_obj.list_protection_policies = MagicMock(
+ return_value=RESP_PP, side_effect=exec_original
+ )
+ api_obj.list_host_access_policies = MagicMock(
+ return_value=RESP_HAP, side_effect=exec_original
+ )
+ api_obj.list_hardware_types = MagicMock(
+ return_value=RESP_HT, side_effect=exec_original
+ )
+ api_obj.list_storage_services = MagicMock(
+ return_value=RESP_SS, side_effect=exec_original
+ )
+ api_obj.list_tenants = MagicMock(
+ return_value=RESP_TENANTS, side_effect=exec_original
+ )
+ api_obj.list_regions = MagicMock(
+ return_value=RESP_REGIONS, side_effect=exec_original
+ )
+ api_obj.list_roles = MagicMock(return_value=RESP_ROLES, side_effect=exec_original)
+ api_obj.list_storage_classes = MagicMock(
+ return_value=RESP_SC, side_effect=exec_original
+ )
+ api_obj.list_role_assignments = MagicMock(
+ return_value=RESP_RA, side_effect=exec_original
+ )
+ api_obj.list_tenant_spaces = MagicMock(
+ return_value=RESP_TS, side_effect=exec_original
+ )
+ api_obj.list_volumes = MagicMock(
+ return_value=RESP_VOLUMES, side_effect=exec_original
+ )
+ api_obj.list_placement_groups = MagicMock(
+ return_value=RESP_PG, side_effect=exec_original
+ )
+ api_obj.list_snapshots = MagicMock(
+ return_value=RESP_SNAPSHOTS, side_effect=exec_original
+ )
+ api_obj.list_availability_zones = MagicMock(
+ return_value=RESP_AZ, side_effect=exec_original
+ )
+ api_obj.list_network_interface_groups = MagicMock(
+ return_value=RESP_NIG, side_effect=exec_original
+ )
+ api_obj.list_storage_endpoints = MagicMock(
+ return_value=RESP_SE, side_effect=exec_original
+ )
+ api_obj.list_network_interfaces = MagicMock(
+ return_value=RESP_NI, side_effect=exec_original
+ )
+ api_obj.list_arrays = MagicMock(return_value=RESP_ARRAYS, side_effect=exec_original)
+ api_obj.list_api_clients = MagicMock(
+ return_value=RESP_AC, side_effect=exec_original
+ )
+ api_obj.list_volume_snapshots = MagicMock(
+ return_value=RESP_VS, side_effect=exec_original
+ )
+ m_ni_api.return_value = api_obj
+ m_se_api.return_value = api_obj
+ m_nig_api.return_value = api_obj
+ m_array_api.return_value = api_obj
+ m_az_api.return_value = api_obj
+ m_snapshot_api.return_value = api_obj
+ m_pg_api.return_value = api_obj
+ m_vs_api.return_value = api_obj
+ m_volume_api.return_value = api_obj
+ m_ts_api.return_value = api_obj
+ m_ra_api.return_value = api_obj
+ m_sc_api.return_value = api_obj
+ m_role_api.return_value = api_obj
+ m_region_api.return_value = api_obj
+ m_tenant_api.return_value = api_obj
+ m_ss_api.return_value = api_obj
+ m_hw_api.return_value = api_obj
+ m_hap_api.return_value = api_obj
+ m_pp_api.return_value = api_obj
+ m_im_api.return_value = api_obj
+ m_default_api.return_value = api_obj
+
+ set_module_args(
+ {
+ "gather_subset": [subset],
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ )
+
+ with pytest.raises(exec_catch) as exc:
+ fusion_info.main()
+
+ # in case of permission denied error, check correct output
+ if exec_catch == AnsibleExitJson:
+ assert exc.value.changed is False
+
+ expected_keys = EXPECTED_KEYS[subset]
+ assert exc.value.fusion_info.keys() == expected_keys
+ for key in expected_keys:
+ if key == "default":
+ for k in exc.value.fusion_info[key]:
+ assert exc.value.fusion_info[key][k] is None
+ else:
+ assert exc.value.fusion_info[key] is None
+
+
+@patch("fusion.StorageServicesApi")
+def test_info_hidden_fields_storage_services(m_ss_api):
+ set_module_args(
+ {
+ "gather_subset": ["storage_services"],
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ )
+
+ response = purefusion.StorageServiceList(
+ count=1,
+ more_items_remaining=False,
+ items=[
+ purefusion.StorageService(
+ id="503",
+ name="ss2",
+ self_link="self_link_value",
+ display_name="Storage Service 3",
+ hardware_types=None, # can be None if not enough permissions
+ ),
+ ],
+ )
+
+ api_obj = MagicMock()
+ api_obj.list_storage_services = MagicMock(return_value=response)
+ m_ss_api.return_value = api_obj
+
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_info.main()
+
+ expected = {
+ "storage_services": {
+ service.name: {
+ "display_name": service.display_name,
+ "hardware_types": None,
+ }
+ for service in response.items
+ },
+ }
+ assert exc.value.fusion_info == expected
+
+
+@patch("fusion.RegionsApi")
+@patch("fusion.AvailabilityZonesApi")
+@patch("fusion.StorageEndpointsApi")
+def test_info_hidden_fields_storage_endpoints(m_ss_api, m_az_api, m_region_api):
+ set_module_args(
+ {
+ "gather_subset": ["storage_endpoints"],
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ )
+
+ response = purefusion.StorageEndpointList(
+ count=1,
+ more_items_remaining=False,
+ items=[
+ purefusion.StorageEndpoint(
+ id="541",
+ name="se1",
+ self_link="self_link_value",
+ display_name="Storage Endpoint 1",
+ region=purefusion.RegionRef(
+ id="542",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="543",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ endpoint_type="iscsi",
+ iscsi=purefusion.StorageEndpointIscsi(
+ discovery_interfaces=[
+ purefusion.StorageEndpointIscsiDiscoveryInterface(
+ address="10.21.200.5/24",
+ gateway="10.21.200.0",
+ mtu=2000,
+ network_interface_groups=None,
+ ),
+ ]
+ ),
+ )
+ ],
+ )
+
+ api_obj = MagicMock()
+ api_obj.list_regions = MagicMock(return_value=RESP_REGIONS)
+ api_obj.list_availability_zones = MagicMock(return_value=RESP_AZ)
+ api_obj.list_storage_endpoints = MagicMock(return_value=response)
+ m_ss_api.return_value = api_obj
+ m_az_api.return_value = api_obj
+ m_region_api.return_value = api_obj
+
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_info.main()
+
+ expected = {
+ "storage_endpoints": {
+ region.name
+ + "/"
+ + az.name
+ + "/"
+ + endpoint.name: {
+ "display_name": endpoint.display_name,
+ "endpoint_type": endpoint.endpoint_type,
+ "iscsi_interfaces": [
+ {
+ "address": iface.address,
+ "gateway": iface.gateway,
+ "mtu": iface.mtu,
+ "network_interface_groups": None,
+ }
+ for iface in endpoint.iscsi.discovery_interfaces
+ ],
+ }
+ for region in RESP_REGIONS.items
+ for az in RESP_AZ.items
+ for endpoint in response.items
+ },
+ }
+ assert exc.value.fusion_info == expected
+
+
+@patch("fusion.TenantsApi")
+@patch("fusion.TenantSpacesApi")
+@patch("fusion.VolumesApi")
+def test_info_hidden_fields_volumes(m_ss_api, m_az_api, m_region_api):
+ set_module_args(
+ {
+ "gather_subset": ["volumes"],
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ )
+
+ response = purefusion.VolumeList(
+ count=1,
+ more_items_remaining=False,
+ items=[
+ purefusion.Volume(
+ id="517",
+ name="volume1",
+ self_link="self_link_value",
+ display_name="Volume 1",
+ size=4000000,
+ tenant=purefusion.TenantRef(
+ id="518",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="519",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ storage_class=purefusion.StorageClassRef(
+ id="520",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ protection_policy=purefusion.ProtectionPolicyRef(
+ id="521",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ placement_group=purefusion.PlacementGroupRef(
+ id="522",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ array=None,
+ created_at=485743825,
+ source_volume_snapshot=purefusion.VolumeSnapshotRef(
+ id="523",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ ),
+ host_access_policies=[
+ purefusion.HostAccessPolicyRef(
+ id="524",
+ name="res_ref_name",
+ kind="kind_value",
+ self_link="self_link_value",
+ )
+ ],
+ serial_number="123923482034",
+ target=purefusion.Target(
+ iscsi=purefusion.Iscsi(
+ iqn="iqn.2023-05.com.purestorage:420qp2c0222",
+ addresses=["125.1.2.4"],
+ )
+ ),
+ time_remaining=1000000,
+ destroyed=False,
+ )
+ ],
+ )
+
+ api_obj = MagicMock()
+ api_obj.list_tenants = MagicMock(return_value=RESP_TENANTS)
+ api_obj.list_tenant_spaces = MagicMock(return_value=RESP_TS)
+ api_obj.list_volumes = MagicMock(return_value=response)
+ m_ss_api.return_value = api_obj
+ m_az_api.return_value = api_obj
+ m_region_api.return_value = api_obj
+
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_info.main()
+
+ expected = {
+ "volumes": {
+ tenant.name
+ + "/"
+ + tenant_space.name
+ + "/"
+ + volume.name: {
+ "tenant": tenant.name,
+ "tenant_space": tenant_space.name,
+ "name": volume.name,
+ "size": volume.size,
+ "display_name": volume.display_name,
+ "placement_group": volume.placement_group.name,
+ "source_volume_snapshot": getattr(
+ volume.source_volume_snapshot, "name", None
+ ),
+ "protection_policy": getattr(volume.protection_policy, "name", None),
+ "storage_class": volume.storage_class.name,
+ "serial_number": volume.serial_number,
+ "target": {
+ "iscsi": {
+ "addresses": volume.target.iscsi.addresses,
+ "iqn": volume.target.iscsi.iqn,
+ },
+ "nvme": {
+ "addresses": None,
+ "nqn": None,
+ },
+ "fc": {
+ "addresses": None,
+ "wwns": None,
+ },
+ },
+ "array": None,
+ }
+ for volume in response.items
+ for tenant_space in RESP_TS.items
+ for tenant in RESP_TENANTS.items
+ }
+ }
+ assert exc.value.fusion_info == expected
+
+
+@patch("fusion.DefaultApi")
+@patch("fusion.IdentityManagerApi")
+@patch("fusion.ProtectionPoliciesApi")
+@patch("fusion.HostAccessPoliciesApi")
+@patch("fusion.HardwareTypesApi")
+@patch("fusion.StorageServicesApi")
+@patch("fusion.TenantsApi")
+@patch("fusion.RegionsApi")
+@patch("fusion.RolesApi")
+@patch("fusion.StorageClassesApi")
+@patch("fusion.RoleAssignmentsApi")
+@patch("fusion.TenantSpacesApi")
+@patch("fusion.VolumesApi")
+@patch("fusion.VolumeSnapshotsApi")
+@patch("fusion.PlacementGroupsApi")
+@patch("fusion.SnapshotsApi")
+@patch("fusion.AvailabilityZonesApi")
+@patch("fusion.ArraysApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+@patch("fusion.StorageEndpointsApi")
+@patch("fusion.NetworkInterfacesApi")
+def test_info_permission_denied_minimum(
+ m_ni_api,
+ m_se_api,
+ m_nig_api,
+ m_array_api,
+ m_az_api,
+ m_snapshot_api,
+ m_pg_api,
+ m_vs_api,
+ m_volume_api,
+ m_ts_api,
+ m_ra_api,
+ m_sc_api,
+ m_role_api,
+ m_region_api,
+ m_tenant_api,
+ m_ss_api,
+ m_hw_api,
+ m_hap_api,
+ m_pp_api,
+ m_im_api,
+ m_default_api,
+):
+ """
+ Test that default dict works correctly with permission denied errors.
+ """
+ exec = ApiExceptionsMockGenerator.create_permission_denied()
+
+ api_obj = MagicMock()
+ api_obj.get_version = MagicMock(return_value=RESP_VERSION, side_effect=exec)
+ api_obj.get_array_space = MagicMock(return_value=RESP_AS, side_effect=exec)
+ api_obj.get_array_performance = MagicMock(return_value=RESP_AP, side_effect=exec)
+ api_obj.list_users = MagicMock(return_value=RESP_LU, side_effect=exec)
+ api_obj.list_protection_policies = MagicMock(return_value=RESP_PP, side_effect=exec)
+ api_obj.list_host_access_policies = MagicMock(
+ return_value=RESP_HAP, side_effect=exec
+ )
+ api_obj.list_hardware_types = MagicMock(return_value=RESP_HT, side_effect=exec)
+ api_obj.list_storage_services = MagicMock(return_value=RESP_SS, side_effect=exec)
+ api_obj.list_tenants = MagicMock(return_value=RESP_TENANTS, side_effect=exec)
+ api_obj.list_regions = MagicMock(return_value=RESP_REGIONS, side_effect=exec)
+ api_obj.list_roles = MagicMock(return_value=RESP_ROLES, side_effect=exec)
+ api_obj.list_storage_classes = MagicMock(return_value=RESP_SC, side_effect=exec)
+ api_obj.list_role_assignments = MagicMock(return_value=RESP_RA, side_effect=exec)
+ api_obj.list_tenant_spaces = MagicMock(return_value=RESP_TS, side_effect=exec)
+ api_obj.list_volumes = MagicMock(return_value=RESP_VOLUMES, side_effect=exec)
+ api_obj.list_placement_groups = MagicMock(return_value=RESP_PG, side_effect=exec)
+ api_obj.list_snapshots = MagicMock(return_value=RESP_SNAPSHOTS, side_effect=exec)
+ api_obj.list_availability_zones = MagicMock(return_value=RESP_AZ, side_effect=exec)
+ api_obj.list_network_interface_groups = MagicMock(
+ return_value=RESP_NIG, side_effect=exec
+ )
+ api_obj.list_storage_endpoints = MagicMock(return_value=RESP_SE, side_effect=exec)
+ api_obj.list_network_interfaces = MagicMock(return_value=RESP_NI, side_effect=exec)
+ api_obj.list_arrays = MagicMock(return_value=RESP_ARRAYS, side_effect=exec)
+ api_obj.list_api_clients = MagicMock(return_value=RESP_AC, side_effect=exec)
+ api_obj.list_volume_snapshots = MagicMock(return_value=RESP_VS, side_effect=exec)
+ m_ni_api.return_value = api_obj
+ m_se_api.return_value = api_obj
+ m_nig_api.return_value = api_obj
+ m_array_api.return_value = api_obj
+ m_az_api.return_value = api_obj
+ m_snapshot_api.return_value = api_obj
+ m_pg_api.return_value = api_obj
+ m_vs_api.return_value = api_obj
+ m_volume_api.return_value = api_obj
+ m_ts_api.return_value = api_obj
+ m_ra_api.return_value = api_obj
+ m_sc_api.return_value = api_obj
+ m_role_api.return_value = api_obj
+ m_region_api.return_value = api_obj
+ m_tenant_api.return_value = api_obj
+ m_ss_api.return_value = api_obj
+ m_hw_api.return_value = api_obj
+ m_hap_api.return_value = api_obj
+ m_pp_api.return_value = api_obj
+ m_im_api.return_value = api_obj
+ m_default_api.return_value = api_obj
+
+ set_module_args(
+ {
+ "gather_subset": ["minimum"],
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ )
+
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_info.main()
+
+ assert exc.value.changed is False
+ assert "default" in exc.value.fusion_info
+ assert {
+ "version": None,
+ "users": None,
+ "protection_policies": None,
+ "host_access_policies": None,
+ "hardware_types": None,
+ "storage_services": None,
+ "tenants": None,
+ "regions": None,
+ "storage_classes": None,
+ "roles": None,
+ "role_assignments": None,
+ "tenant_spaces": None,
+ "volumes": None,
+ "placement_groups": None,
+ "snapshots": None,
+ "availability_zones": None,
+ "arrays": None,
+ "network_interfaces": None,
+ "network_interface_groups": None,
+ "storage_endpoints": None,
+ } == exc.value.fusion_info["default"]
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_nig.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_nig.py
new file mode 100644
index 000000000..3a7b7ca5c
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_nig.py
@@ -0,0 +1,1239 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock, patch
+
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_nig
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ FailedOperationMock,
+ OperationMock,
+ SuccessfulOperationMock,
+ exit_json,
+ fail_json,
+ set_module_args,
+)
+from urllib3.exceptions import HTTPError
+
+# GLOBAL MOCKS
+fusion_nig.setup_fusion = MagicMock(return_value=purefusion.api_client.ApiClient())
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+@pytest.mark.parametrize(
+ "module_args",
+ [
+ # required parameter 'name` is missing
+ {
+ "state": "present",
+ "display_name": "Network Interface Group 1",
+ "availability_zone": "az1",
+ "region": "region1",
+ "prefix": "10.21.200.0/24",
+ "gateway": "10.21.200.1",
+ "mtu": 1300,
+ "group_type": "eth",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ },
+ # required parameter 'availability_zone` is missing
+ {
+ "state": "present",
+ "name": "nig1",
+ "display_name": "Network Interface Group 1",
+ "region": "region1",
+ "prefix": "10.21.200.0/24",
+ "gateway": "10.21.200.1",
+ "mtu": 1300,
+ "group_type": "eth",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ },
+ # required parameter 'region` is missing
+ {
+ "state": "present",
+ "name": "nig1",
+ "display_name": "Network Interface Group 1",
+ "availability_zone": "az1",
+ "prefix": "10.21.200.0/24",
+ "gateway": "10.21.200.1",
+ "mtu": 1300,
+ "group_type": "eth",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ },
+ # unknown parameter 'extra' is provided
+ {
+ "state": "present",
+ "name": "nig1",
+ "display_name": "Network Interface Group 1",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ "extra": "value",
+ },
+ # parameter 'state` has incorrect value
+ {
+ "state": "cool",
+ "name": "nig1",
+ "display_name": "Network Interface Group 1",
+ "availability_zone": "az1",
+ "region": "region1",
+ "prefix": "10.21.200.0/24",
+ "gateway": "10.21.200.1",
+ "mtu": 1300,
+ "group_type": "eth",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ },
+ # parameter 'group_type` has incorrect value
+ {
+ "state": "present",
+ "name": "nig1",
+ "display_name": "Network Interface Group 1",
+ "availability_zone": "az1",
+ "region": "region1",
+ "prefix": "10.21.200.0/24",
+ "gateway": "10.21.200.1",
+ "mtu": 1300,
+ "group_type": "supergroup",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ },
+ ],
+)
+def test_module_fails_on_wrong_parameters(m_nig_api, m_op_api, module_args):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(
+ side_effect=purefusion.rest.ApiException
+ )
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+ m_nig_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_nig.main()
+
+ # check api was not called at all
+ api_obj.get_network_interface_group.assert_not_called()
+ api_obj.create_network_interface_group.assert_not_called()
+ api_obj.update_network_interface_group.assert_not_called()
+ api_obj.delete_network_interface_group.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+def test_nig_create_fails_on_missing_prefix(m_nig_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "nig1",
+ "display_name": "Network Interface Group 1",
+ "availability_zone": "az1",
+ "region": "region1",
+ "gateway": "10.21.200.1",
+ "mtu": 1300,
+ "group_type": "eth",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(
+ side_effect=purefusion.rest.ApiException
+ )
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+ m_nig_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_nig.main()
+
+ # check api was not called at all
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_not_called()
+ api_obj.update_network_interface_group.assert_not_called()
+ api_obj.delete_network_interface_group.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+def test_nig_create(m_nig_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "nig1",
+ "display_name": "Network Interface Group 1",
+ "availability_zone": "az1",
+ "region": "region1",
+ "prefix": "10.21.200.0/24",
+ "gateway": "10.21.200.1",
+ "mtu": 1300,
+ "group_type": "eth",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(
+ side_effect=purefusion.rest.ApiException
+ )
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+ m_nig_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_nig.main()
+
+ assert exc.value.changed is True
+
+ # check api was called correctly
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_called_once_with(
+ purefusion.NetworkInterfaceGroupPost(
+ group_type="eth",
+ eth=purefusion.NetworkInterfaceGroupEthPost(
+ prefix=module_args["prefix"],
+ gateway=module_args["gateway"],
+ mtu=module_args["mtu"],
+ ),
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.update_network_interface_group.assert_not_called()
+ api_obj.delete_network_interface_group.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+def test_nig_create_without_display_name(m_nig_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "nig1",
+ "availability_zone": "az1",
+ "region": "region1",
+ "prefix": "10.21.200.0/24",
+ "gateway": "10.21.200.1",
+ "mtu": 1300,
+ "group_type": "eth",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(
+ side_effect=purefusion.rest.ApiException
+ )
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+ m_nig_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_nig.main()
+
+ assert exc.value.changed is True
+
+ # check api was called correctly
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_called_once_with(
+ purefusion.NetworkInterfaceGroupPost(
+ group_type="eth",
+ eth=purefusion.NetworkInterfaceGroupEthPost(
+ prefix=module_args["prefix"],
+ gateway=module_args["gateway"],
+ mtu=module_args["mtu"],
+ ),
+ name=module_args["name"],
+ display_name=module_args["name"],
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.update_network_interface_group.assert_not_called()
+ api_obj.delete_network_interface_group.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+def test_nig_create_without_gateway(m_nig_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "nig1",
+ "display_name": "Network Interface Group 1",
+ "availability_zone": "az1",
+ "region": "region1",
+ "prefix": "10.21.200.0/24",
+ "mtu": 1300,
+ "group_type": "eth",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(
+ side_effect=purefusion.rest.ApiException
+ )
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+ m_nig_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_nig.main()
+
+ assert exc.value.changed is True
+
+ # check api was called correctly
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_called_once_with(
+ purefusion.NetworkInterfaceGroupPost(
+ group_type="eth",
+ eth=purefusion.NetworkInterfaceGroupEthPost(
+ prefix=module_args["prefix"],
+ mtu=module_args["mtu"],
+ ),
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.update_network_interface_group.assert_not_called()
+ api_obj.delete_network_interface_group.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_nig_create_exception(m_nig_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "nig1",
+ "display_name": "Network Interface Group 1",
+ "availability_zone": "az1",
+ "region": "region1",
+ "prefix": "10.21.200.0/24",
+ "mtu": 1300,
+ "group_type": "eth",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(
+ side_effect=purefusion.rest.ApiException
+ )
+ api_obj.create_network_interface_group = MagicMock(side_effect=exec_original)
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+ m_nig_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_nig.main()
+
+ # check api was called correctly
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_called_once_with(
+ purefusion.NetworkInterfaceGroupPost(
+ group_type="eth",
+ eth=purefusion.NetworkInterfaceGroupEthPost(
+ prefix=module_args["prefix"],
+ mtu=module_args["mtu"],
+ ),
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.update_network_interface_group.assert_not_called()
+ api_obj.delete_network_interface_group.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+def test_nig_create_op_fails(m_nig_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "nig1",
+ "display_name": "Network Interface Group 1",
+ "availability_zone": "az1",
+ "region": "region1",
+ "prefix": "10.21.200.0/24",
+ "mtu": 1300,
+ "group_type": "eth",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(
+ side_effect=purefusion.rest.ApiException
+ )
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+ m_nig_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_nig.main()
+
+ # check api was called correctly
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_called_once_with(
+ purefusion.NetworkInterfaceGroupPost(
+ group_type="eth",
+ eth=purefusion.NetworkInterfaceGroupEthPost(
+ prefix=module_args["prefix"],
+ mtu=module_args["mtu"],
+ ),
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.update_network_interface_group.assert_not_called()
+ api_obj.delete_network_interface_group.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_nig_create_op_exception(m_nig_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "nig1",
+ "display_name": "Network Interface Group 1",
+ "availability_zone": "az1",
+ "region": "region1",
+ "prefix": "10.21.200.0/24",
+ "mtu": 1300,
+ "group_type": "eth",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(
+ side_effect=purefusion.rest.ApiException
+ )
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+ m_nig_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_nig.main()
+
+ # check api was called correctly
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_called_once_with(
+ purefusion.NetworkInterfaceGroupPost(
+ group_type="eth",
+ eth=purefusion.NetworkInterfaceGroupEthPost(
+ prefix=module_args["prefix"],
+ mtu=module_args["mtu"],
+ ),
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ )
+ api_obj.update_network_interface_group.assert_not_called()
+ api_obj.delete_network_interface_group.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+def test_nig_update(m_nig_api, m_op_api):
+ current_nig = purefusion.NetworkInterfaceGroup(
+ id="1",
+ self_link="self_link_value",
+ name="nig1",
+ display_name=None,
+ region="region1",
+ availability_zone="az1",
+ group_type="eth",
+ eth=purefusion.NetworkInterfaceGroupEth(
+ prefix="str",
+ gateway="str",
+ vlan=3,
+ mtu=1300,
+ ),
+ )
+ module_args = {
+ "state": "present",
+ "name": current_nig.name, # must match
+ "display_name": "New Name", # should be updated
+ "availability_zone": current_nig.availability_zone, # must match
+ "region": current_nig.region, # must match
+ "prefix": "12.19.150.0/23", # should not be updated
+ "mtu": current_nig.eth.mtu + 100, # should not be updated
+ "group_type": "eth",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(return_value=current_nig)
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+ m_nig_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_nig.main()
+
+ assert exc.value.changed is True
+
+ # check api was called correctly
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_not_called()
+ api_obj.update_network_interface_group.assert_called_once_with(
+ purefusion.NetworkInterfaceGroupPatch(
+ display_name=purefusion.NullableString(module_args["display_name"]),
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.delete_network_interface_group.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_nig_update_exception(m_nig_api, m_op_api, exec_original, exec_catch):
+ current_nig = purefusion.NetworkInterfaceGroup(
+ id="1",
+ self_link="self_link_value",
+ name="nig1",
+ display_name=None,
+ region="region1",
+ availability_zone="az1",
+ group_type="eth",
+ eth=purefusion.NetworkInterfaceGroupEth(
+ prefix="str",
+ gateway="str",
+ vlan=3,
+ mtu=1300,
+ ),
+ )
+ module_args = {
+ "state": "present",
+ "name": current_nig.name, # must match
+ "display_name": "New Name", # should be updated
+ "availability_zone": current_nig.availability_zone, # must match
+ "region": current_nig.region, # must match
+ "prefix": "12.19.150.0/23", # should not be updated
+ "mtu": current_nig.eth.mtu + 100, # should not be updated
+ "group_type": "eth",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(return_value=current_nig)
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(side_effect=exec_original)
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+ m_nig_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_nig.main()
+
+ # check api was called correctly
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_not_called()
+ api_obj.update_network_interface_group.assert_called_once_with(
+ purefusion.NetworkInterfaceGroupPatch(
+ display_name=purefusion.NullableString(module_args["display_name"]),
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.delete_network_interface_group.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+def test_nig_update_op_fails(m_nig_api, m_op_api):
+ current_nig = purefusion.NetworkInterfaceGroup(
+ id="1",
+ self_link="self_link_value",
+ name="nig1",
+ display_name=None,
+ region="region1",
+ availability_zone="az1",
+ group_type="eth",
+ eth=purefusion.NetworkInterfaceGroupEth(
+ prefix="str",
+ gateway="str",
+ vlan=3,
+ mtu=1300,
+ ),
+ )
+ module_args = {
+ "state": "present",
+ "name": current_nig.name, # must match
+ "display_name": "New Name", # should be updated
+ "availability_zone": current_nig.availability_zone, # must match
+ "region": current_nig.region, # must match
+ "prefix": "12.19.150.0/23", # should not be updated
+ "mtu": current_nig.eth.mtu + 100, # should not be updated
+ "group_type": "eth",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(return_value=current_nig)
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+ m_nig_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_nig.main()
+
+ # check api was called correctly
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_not_called()
+ api_obj.update_network_interface_group.assert_called_once_with(
+ purefusion.NetworkInterfaceGroupPatch(
+ display_name=purefusion.NullableString(module_args["display_name"]),
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.delete_network_interface_group.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_nig_update_op_exception(m_nig_api, m_op_api, exec_original, exec_catch):
+ current_nig = purefusion.NetworkInterfaceGroup(
+ id="1",
+ self_link="self_link_value",
+ name="nig1",
+ display_name=None,
+ region="region1",
+ availability_zone="az1",
+ group_type="eth",
+ eth=purefusion.NetworkInterfaceGroupEth(
+ prefix="str",
+ gateway="str",
+ vlan=3,
+ mtu=1300,
+ ),
+ )
+ module_args = {
+ "state": "present",
+ "name": current_nig.name, # must match
+ "display_name": "New Name", # should be updated
+ "availability_zone": current_nig.availability_zone, # must match
+ "region": current_nig.region, # must match
+ "prefix": "12.19.150.0/23", # should not be updated
+ "mtu": current_nig.eth.mtu + 100, # should not be updated
+ "group_type": "eth",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(return_value=current_nig)
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+ m_nig_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_nig.main()
+
+ # check api was called correctly
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_not_called()
+ api_obj.update_network_interface_group.assert_called_once_with(
+ purefusion.NetworkInterfaceGroupPatch(
+ display_name=purefusion.NullableString(module_args["display_name"]),
+ ),
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.delete_network_interface_group.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+def test_nig_present_not_changed(m_nig_api, m_op_api):
+ current_nig = purefusion.NetworkInterfaceGroup(
+ id="1",
+ self_link="self_link_value",
+ name="nig1",
+ display_name="Display Name",
+ region="region1",
+ availability_zone="az1",
+ group_type="eth",
+ eth=purefusion.NetworkInterfaceGroupEth(
+ prefix="str",
+ gateway="str",
+ vlan=3,
+ mtu=1300,
+ ),
+ )
+ module_args = {
+ "state": "present",
+ "name": current_nig.name, # must match
+ "display_name": current_nig.display_name, # should not be updated
+ "availability_zone": current_nig.availability_zone, # must match
+ "region": current_nig.region, # must match
+ "prefix": "12.19.150.0/23", # should not be updated
+ "mtu": current_nig.eth.mtu + 100, # should not be updated
+ "group_type": "eth",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(return_value=current_nig)
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+ m_nig_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_nig.main()
+
+ assert exc.value.changed is False
+
+ # check api was called correctly
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_not_called()
+ api_obj.update_network_interface_group.assert_not_called()
+ api_obj.delete_network_interface_group.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+def test_nig_absent_not_changed(m_nig_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "nig1",
+ "display_name": "Network Interface Group 1",
+ "availability_zone": "az1",
+ "region": "region1",
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(
+ side_effect=purefusion.rest.ApiException
+ )
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+ m_nig_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_nig.main()
+
+ assert exc.value.changed is False
+
+ # check api was called correctly
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_not_called()
+ api_obj.update_network_interface_group.assert_not_called()
+ api_obj.delete_network_interface_group.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+def test_nig_delete(m_nig_api, m_op_api):
+ current_nig = purefusion.NetworkInterfaceGroup(
+ id="1",
+ self_link="self_link_value",
+ name="nig1",
+ display_name="Display Name",
+ region="region1",
+ availability_zone="az1",
+ group_type="eth",
+ eth=purefusion.NetworkInterfaceGroupEth(
+ prefix="str",
+ gateway="str",
+ vlan=3,
+ mtu=1300,
+ ),
+ )
+ module_args = {
+ "state": "absent",
+ "name": current_nig.name, # must match
+ "availability_zone": current_nig.availability_zone, # must match
+ "region": current_nig.region, # must match
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(return_value=current_nig)
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+ m_nig_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_nig.main()
+
+ assert exc.value.changed is True
+
+ # check api was called correctly
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_not_called()
+ api_obj.update_network_interface_group.assert_not_called()
+ api_obj.delete_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_nig_delete_exception(m_nig_api, m_op_api, exec_original, exec_catch):
+ current_nig = purefusion.NetworkInterfaceGroup(
+ id="1",
+ self_link="self_link_value",
+ name="nig1",
+ display_name="Display Name",
+ region="region1",
+ availability_zone="az1",
+ group_type="eth",
+ eth=purefusion.NetworkInterfaceGroupEth(
+ prefix="str",
+ gateway="str",
+ vlan=3,
+ mtu=1300,
+ ),
+ )
+ module_args = {
+ "state": "absent",
+ "name": current_nig.name, # must match
+ "availability_zone": current_nig.availability_zone, # must match
+ "region": current_nig.region, # must match
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(return_value=current_nig)
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(side_effect=exec_original)
+ m_nig_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_nig.main()
+
+ # check api was called correctly
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_not_called()
+ api_obj.update_network_interface_group.assert_not_called()
+ api_obj.delete_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+def test_nig_delete_op_fails(m_nig_api, m_op_api):
+ current_nig = purefusion.NetworkInterfaceGroup(
+ id="1",
+ self_link="self_link_value",
+ name="nig1",
+ display_name="Display Name",
+ region="region1",
+ availability_zone="az1",
+ group_type="eth",
+ eth=purefusion.NetworkInterfaceGroupEth(
+ prefix="str",
+ gateway="str",
+ vlan=3,
+ mtu=1300,
+ ),
+ )
+ module_args = {
+ "state": "absent",
+ "name": current_nig.name, # must match
+ "availability_zone": current_nig.availability_zone, # must match
+ "region": current_nig.region, # must match
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(return_value=current_nig)
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+ m_nig_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_nig.main()
+
+ # check api was called correctly
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_not_called()
+ api_obj.update_network_interface_group.assert_not_called()
+ api_obj.delete_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.NetworkInterfaceGroupsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_nig_delete_op_exception(m_nig_api, m_op_api, exec_original, exec_catch):
+ current_nig = purefusion.NetworkInterfaceGroup(
+ id="1",
+ self_link="self_link_value",
+ name="nig1",
+ display_name="Display Name",
+ region="region1",
+ availability_zone="az1",
+ group_type="eth",
+ eth=purefusion.NetworkInterfaceGroupEth(
+ prefix="str",
+ gateway="str",
+ vlan=3,
+ mtu=1300,
+ ),
+ )
+ module_args = {
+ "state": "absent",
+ "name": current_nig.name, # must match
+ "availability_zone": current_nig.availability_zone, # must match
+ "region": current_nig.region, # must match
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_network_interface_group = MagicMock(return_value=current_nig)
+ api_obj.create_network_interface_group = MagicMock(return_value=OperationMock(1))
+ api_obj.update_network_interface_group = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_network_interface_group = MagicMock(return_value=OperationMock(3))
+ m_nig_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_nig.main()
+
+ # check api was called correctly
+ api_obj.get_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ api_obj.create_network_interface_group.assert_not_called()
+ api_obj.update_network_interface_group.assert_not_called()
+ api_obj.delete_network_interface_group.assert_called_once_with(
+ availability_zone_name=module_args["availability_zone"],
+ region_name=module_args["region"],
+ network_interface_group_name=module_args["name"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_pg.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_pg.py
new file mode 100644
index 000000000..2f0601e12
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_pg.py
@@ -0,0 +1,1595 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023 Pure Storage, Inc.
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock, patch, call
+
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_pg
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ OperationMock,
+ exit_json,
+ fail_json,
+ set_module_args,
+ side_effects_with_exceptions,
+)
+from urllib3.exceptions import HTTPError
+
+# GLOBAL MOCKS
+fusion_pg.setup_fusion = MagicMock(return_value=purefusion.api_client.ApiClient())
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+
+@pytest.fixture
+def module_args_present():
+ return {
+ "name": "placement_group1",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "region": "region1",
+ "availability_zone": "availability_zone1",
+ "storage_service": "storage_service1",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+
+
+@pytest.fixture
+def module_args_absent():
+ return {
+ "name": "placement_group1",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "state": "absent",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+@pytest.mark.parametrize(
+ ("module_args", "get_not_called"),
+ [
+ # 'name` is missing
+ (
+ {
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "region": "region1",
+ "availability_zone": "availability_zone1",
+ "storage_service": "storage_service1",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ True,
+ ),
+ # 'tenant` is missing
+ (
+ {
+ "name": "placement_group1",
+ "tenant_space": "tenant_space1",
+ "region": "region1",
+ "availability_zone": "availability_zone1",
+ "storage_service": "storage_service1",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ True,
+ ),
+ # 'tenant space` is missing
+ (
+ {
+ "name": "placement_group1",
+ "tenant": "tenant1",
+ "region": "region1",
+ "availability_zone": "availability_zone1",
+ "storage_service": "storage_service1",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ True,
+ ),
+ # 'region` is missing
+ (
+ {
+ "name": "placement_group1",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "availability_zone": "availability_zone1",
+ "storage_service": "storage_service1",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ False,
+ ),
+ # 'availability_zone` is missing
+ (
+ {
+ "name": "placement_group1",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "region": "region1",
+ "storage_service": "storage_service1",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ False,
+ ),
+ # 'storage_service` is missing
+ (
+ {
+ "name": "placement_group1",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "region": "region1",
+ "availability_zone": "availability_zone1",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ False,
+ ),
+ # 'state` is invalid
+ (
+ {
+ "name": "placement_group1",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "region": "region1",
+ "availability_zone": "availability_zone1",
+ "storage_service": "storage_service1",
+ "state": "past",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ False,
+ ),
+ ],
+)
+def test_module_args_wrong(pg_api_init, op_api_init, module_args, get_not_called):
+ set_module_args(module_args)
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(side_effect=purefusion.rest.ApiException)
+ pg_mock.create_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.update_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.delete_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(side_effect=purefusion.rest.ApiException)
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleFailJson):
+ fusion_pg.main()
+
+ if get_not_called:
+ pg_mock.get_placement_group.assert_not_called()
+ elif pg_mock.get_placement_group.called:
+ pg_mock.get_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.create_placement_group.assert_not_called()
+ pg_mock.update_placement_group.assert_not_called()
+ pg_mock.delete_placement_group.assert_not_called()
+ op_mock.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+def test_pg_create_ok(pg_api_init, op_api_init, module_args_present):
+ module_args = module_args_present
+ module_args["display_name"] = "some_display_name"
+ set_module_args(module_args)
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(side_effect=purefusion.rest.ApiException)
+ pg_mock.create_placement_group = MagicMock(return_value=OperationMock("op1"))
+ pg_mock.update_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.delete_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(return_value=OperationMock("op1", success=True))
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleExitJson) as excinfo:
+ fusion_pg.main()
+ assert excinfo.value.changed
+
+ pg_mock.get_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.create_placement_group.assert_called_with(
+ purefusion.PlacementGroupPost(
+ name="placement_group1",
+ display_name="some_display_name",
+ availability_zone="availability_zone1",
+ region="region1",
+ storage_service="storage_service1",
+ ),
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ )
+ pg_mock.update_placement_group.assert_not_called()
+ pg_mock.delete_placement_group.assert_not_called()
+ op_mock.get_operation.assert_called_with("op1")
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+def test_pg_create_without_display_name_ok(
+ pg_api_init, op_api_init, module_args_present
+):
+ module_args = module_args_present
+ set_module_args(module_args)
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(side_effect=purefusion.rest.ApiException)
+ pg_mock.create_placement_group = MagicMock(return_value=OperationMock("op1"))
+ pg_mock.update_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.delete_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(return_value=OperationMock("op1", success=True))
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleExitJson) as excinfo:
+ fusion_pg.main()
+ assert excinfo.value.changed
+
+ pg_mock.get_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.create_placement_group.assert_called_with(
+ purefusion.PlacementGroupPost(
+ name="placement_group1",
+ display_name="placement_group1",
+ availability_zone="availability_zone1",
+ region="region1",
+ storage_service="storage_service1",
+ ),
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ )
+ pg_mock.update_placement_group.assert_not_called()
+ pg_mock.delete_placement_group.assert_not_called()
+ op_mock.get_operation.assert_called_with("op1")
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+@pytest.mark.parametrize(
+ ("raised_exception", "expected_exception"),
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_pg_create_exception(
+ pg_api_init, op_api_init, raised_exception, expected_exception, module_args_present
+):
+ set_module_args(module_args_present)
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(side_effect=purefusion.rest.ApiException)
+ pg_mock.create_placement_group = MagicMock(side_effect=raised_exception)
+ pg_mock.update_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.delete_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(side_effect=NotImplementedError())
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(expected_exception):
+ fusion_pg.main()
+
+ pg_mock.get_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.create_placement_group.assert_called_with(
+ purefusion.PlacementGroupPost(
+ name="placement_group1",
+ display_name="placement_group1",
+ availability_zone="availability_zone1",
+ region="region1",
+ storage_service="storage_service1",
+ ),
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ )
+ pg_mock.delete_placement_group.assert_not_called()
+ pg_mock.update_placement_group.assert_not_called()
+ op_mock.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+def test_pg_create_op_fails(pg_api_init, op_api_init, module_args_present):
+ module_args = module_args_present
+ set_module_args(module_args)
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(side_effect=purefusion.rest.ApiException)
+ pg_mock.create_placement_group = MagicMock(return_value=OperationMock(id="op1"))
+ pg_mock.update_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.delete_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(return_value=OperationMock("op1", success=False))
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(OperationException):
+ fusion_pg.main()
+
+ pg_mock.get_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.create_placement_group.assert_called_with(
+ purefusion.PlacementGroupPost(
+ name="placement_group1",
+ display_name="placement_group1",
+ availability_zone="availability_zone1",
+ region="region1",
+ storage_service="storage_service1",
+ ),
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ )
+ pg_mock.update_placement_group.assert_not_called()
+ pg_mock.delete_placement_group.assert_not_called()
+ op_mock.get_operation.assert_called_with("op1")
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+def test_pg_create_triggers_update_ok(pg_api_init, op_api_init):
+ module_args = {
+ "name": "placement_group1",
+ "display_name": "some_display_name",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "region": "region1",
+ "availability_zone": "availability_zone1",
+ "storage_service": "storage_service1",
+ "array": "array2",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ get_placement_group_effects = [
+ purefusion.rest.ApiException(),
+ purefusion.PlacementGroup(
+ id="placement_group1_id",
+ name="placement_group1",
+ display_name="some_display_name",
+ self_link="test_self_link",
+ tenant=purefusion.TenantRef(
+ id="tenant1_id",
+ name="tenant1",
+ kind="Tenant",
+ self_link="some_self_link",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="tenant_space1_id",
+ name="tenant_space1",
+ kind="TenantSpace",
+ self_link="some_self_link",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="availability_zone1_id",
+ name="availability_zone1",
+ kind="AvailabilityZone",
+ self_link="some_self_link",
+ ),
+ placement_engine="heuristics",
+ protocols=[],
+ storage_service=purefusion.StorageServiceRef(
+ id="storage_service1_id",
+ name="storage_service",
+ kind="StorageService",
+ self_link="some_self_link",
+ ),
+ array=purefusion.ArrayRef(
+ id="array1_id", name="array1", kind="Array", self_link="some_self_link"
+ ),
+ ),
+ ]
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(
+ side_effect=side_effects_with_exceptions(get_placement_group_effects)
+ )
+ pg_mock.create_placement_group = MagicMock(return_value=OperationMock("op1"))
+ pg_mock.update_placement_group = MagicMock(return_value=OperationMock("op2"))
+ pg_mock.delete_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(return_value=OperationMock("op1", success=True))
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleExitJson) as excinfo:
+ fusion_pg.main()
+ assert excinfo.value.changed
+
+ pg_mock.get_placement_group.assert_has_calls(
+ [
+ call(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ ),
+ call(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ ),
+ ],
+ any_order=True,
+ )
+ pg_mock.create_placement_group.assert_called_with(
+ purefusion.PlacementGroupPost(
+ name="placement_group1",
+ display_name="some_display_name",
+ availability_zone="availability_zone1",
+ region="region1",
+ storage_service="storage_service1",
+ ),
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ )
+ pg_mock.update_placement_group.assert_called_with(
+ purefusion.PlacementGroupPatch(array=purefusion.NullableString(value="array2")),
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.delete_placement_group.assert_not_called()
+ op_mock.get_operation.assert_has_calls([call("op1"), call("op2")], any_order=True)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+@pytest.mark.parametrize(
+ ("raised_exception", "expected_exception"),
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_pg_create_triggers_update_exception(
+ pg_api_init, op_api_init, raised_exception, expected_exception
+):
+ module_args = {
+ "name": "placement_group1",
+ "display_name": "some_display_name",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "region": "region1",
+ "availability_zone": "availability_zone1",
+ "storage_service": "storage_service1",
+ "array": "array2",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ get_placement_group_effects = [
+ purefusion.rest.ApiException(),
+ purefusion.PlacementGroup(
+ id="placement_group1_id",
+ name="placement_group1",
+ display_name="some_display_name",
+ self_link="test_self_link",
+ tenant=purefusion.TenantRef(
+ id="tenant1_id",
+ name="tenant1",
+ kind="Tenant",
+ self_link="some_self_link",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="tenant_space1_id",
+ name="tenant_space1",
+ kind="TenantSpace",
+ self_link="some_self_link",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="availability_zone1_id",
+ name="availability_zone1",
+ kind="AvailabilityZone",
+ self_link="some_self_link",
+ ),
+ placement_engine="heuristics",
+ protocols=[],
+ storage_service=purefusion.StorageServiceRef(
+ id="storage_service1_id",
+ name="storage_service",
+ kind="StorageService",
+ self_link="some_self_link",
+ ),
+ array=purefusion.ArrayRef(
+ id="array1_id", name="array1", kind="Array", self_link="some_self_link"
+ ),
+ ),
+ ]
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(
+ side_effect=side_effects_with_exceptions(get_placement_group_effects)
+ )
+ pg_mock.create_placement_group = MagicMock(return_value=OperationMock("op1"))
+ pg_mock.update_placement_group = MagicMock(side_effect=raised_exception)
+ pg_mock.delete_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(return_value=OperationMock("op1", success=True))
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(expected_exception):
+ fusion_pg.main()
+
+ pg_mock.get_placement_group.assert_has_calls(
+ [
+ call(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ ),
+ call(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ ),
+ ],
+ any_order=True,
+ )
+ pg_mock.create_placement_group.assert_called_with(
+ purefusion.PlacementGroupPost(
+ name="placement_group1",
+ display_name="some_display_name",
+ availability_zone="availability_zone1",
+ region="region1",
+ storage_service="storage_service1",
+ ),
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ )
+ pg_mock.update_placement_group.assert_called_with(
+ purefusion.PlacementGroupPatch(array=purefusion.NullableString(value="array2")),
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.delete_placement_group.assert_not_called()
+ op_mock.get_operation.assert_called_with("op1")
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+def test_pg_create_triggers_update_op_fails(pg_api_init, op_api_init):
+ module_args = {
+ "name": "placement_group1",
+ "display_name": "some_display_name",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "region": "region1",
+ "availability_zone": "availability_zone1",
+ "storage_service": "storage_service1",
+ "array": "array2",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ get_placement_group_effects = [
+ purefusion.rest.ApiException(),
+ purefusion.PlacementGroup(
+ id="placement_group1_id",
+ name="placement_group1",
+ display_name="some_display_name",
+ self_link="test_self_link",
+ tenant=purefusion.TenantRef(
+ id="tenant1_id",
+ name="tenant1",
+ kind="Tenant",
+ self_link="some_self_link",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="tenant_space1_id",
+ name="tenant_space1",
+ kind="TenantSpace",
+ self_link="some_self_link",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="availability_zone1_id",
+ name="availability_zone1",
+ kind="AvailabilityZone",
+ self_link="some_self_link",
+ ),
+ placement_engine="heuristics",
+ protocols=[],
+ storage_service=purefusion.StorageServiceRef(
+ id="storage_service1_id",
+ name="storage_service",
+ kind="StorageService",
+ self_link="some_self_link",
+ ),
+ array=purefusion.ArrayRef(
+ id="array1_id", name="array1", kind="Array", self_link="some_self_link"
+ ),
+ ),
+ ]
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(
+ side_effect=side_effects_with_exceptions(get_placement_group_effects)
+ )
+ pg_mock.create_placement_group = MagicMock(return_value=OperationMock("op1"))
+ pg_mock.update_placement_group = MagicMock(return_value=OperationMock("op2"))
+ pg_mock.delete_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(
+ side_effect=[
+ OperationMock("op1", success=True),
+ OperationMock("op2", success=False),
+ ]
+ )
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(OperationException):
+ fusion_pg.main()
+
+ pg_mock.get_placement_group.assert_has_calls(
+ [
+ call(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ ),
+ call(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ ),
+ ],
+ any_order=True,
+ )
+ pg_mock.create_placement_group.assert_called_with(
+ purefusion.PlacementGroupPost(
+ name="placement_group1",
+ display_name="some_display_name",
+ availability_zone="availability_zone1",
+ region="region1",
+ storage_service="storage_service1",
+ ),
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ )
+ pg_mock.update_placement_group.assert_called_with(
+ purefusion.PlacementGroupPatch(array=purefusion.NullableString(value="array2")),
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.delete_placement_group.assert_not_called()
+ op_mock.get_operation.assert_has_calls([call("op1"), call("op2")])
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+@pytest.mark.parametrize(
+ "test_case",
+ [
+ # patch 'display_name`
+ {
+ "current_state": purefusion.PlacementGroup(
+ id="placement_group1_id",
+ name="placement_group1",
+ display_name="placement_group1_display_name",
+ self_link="test_self_link",
+ tenant=purefusion.TenantRef(
+ id="tenant1_id",
+ name="tenant1",
+ kind="Tenant",
+ self_link="some_self_link",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="tenant_space1_id",
+ name="tenant_space1",
+ kind="TenantSpace",
+ self_link="some_self_link",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="availability_zone1_id",
+ name="availability_zone1",
+ kind="AvailabilityZone",
+ self_link="some_self_link",
+ ),
+ placement_engine="heuristics",
+ protocols=[],
+ storage_service=purefusion.StorageServiceRef(
+ id="storage_service1_id",
+ name="storage_service",
+ kind="StorageService",
+ self_link="some_self_link",
+ ),
+ array=purefusion.ArrayRef(
+ id="array1_id",
+ name="array1",
+ kind="Array",
+ self_link="some_self_link",
+ ),
+ ),
+ "module_args": {
+ "name": "placement_group1",
+ "display_name": "different_display_name",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "region": "region1",
+ "availability_zone": "availability_zone1",
+ "storage_service": "storage_service1",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ "patches": [
+ purefusion.PlacementGroupPatch(
+ display_name=purefusion.NullableString(
+ value="different_display_name"
+ ),
+ ),
+ ],
+ },
+ # patch 'array`
+ {
+ "current_state": purefusion.PlacementGroup(
+ id="placement_group1_id",
+ name="placement_group1",
+ display_name="placement_group1",
+ self_link="test_self_link",
+ tenant=purefusion.TenantRef(
+ id="tenant1_id",
+ name="tenant1",
+ kind="Tenant",
+ self_link="some_self_link",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="tenant_space1_id",
+ name="tenant_space1",
+ kind="TenantSpace",
+ self_link="some_self_link",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="availability_zone1_id",
+ name="availability_zone1",
+ kind="AvailabilityZone",
+ self_link="some_self_link",
+ ),
+ placement_engine="heuristics",
+ protocols=[],
+ storage_service=purefusion.StorageServiceRef(
+ id="storage_service1_id",
+ name="storage_service",
+ kind="StorageService",
+ self_link="some_self_link",
+ ),
+ array=purefusion.ArrayRef(
+ id="array1_id",
+ name="array1",
+ kind="Array",
+ self_link="some_self_link",
+ ),
+ ),
+ "module_args": {
+ "name": "placement_group1",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "region": "region1",
+ "availability_zone": "availability_zone1",
+ "storage_service": "storage_service1",
+ "array": "array2",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ "patches": [
+ purefusion.PlacementGroupPatch(
+ array=purefusion.NullableString(value="array2"),
+ ),
+ ],
+ },
+ # patch all
+ {
+ "current_state": purefusion.PlacementGroup(
+ id="placement_group1_id",
+ name="placement_group1",
+ display_name="placement_group1_display_name",
+ self_link="test_self_link",
+ tenant=purefusion.TenantRef(
+ id="tenant1_id",
+ name="tenant1",
+ kind="Tenant",
+ self_link="some_self_link",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="tenant_space1_id",
+ name="tenant_space1",
+ kind="TenantSpace",
+ self_link="some_self_link",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="availability_zone1_id",
+ name="availability_zone1",
+ kind="AvailabilityZone",
+ self_link="some_self_link",
+ ),
+ placement_engine="heuristics",
+ protocols=[],
+ storage_service=purefusion.StorageServiceRef(
+ id="storage_service1_id",
+ name="storage_service",
+ kind="StorageService",
+ self_link="some_self_link",
+ ),
+ array=purefusion.ArrayRef(
+ id="array1_id",
+ name="array1",
+ kind="Array",
+ self_link="some_self_link",
+ ),
+ ),
+ "module_args": {
+ "name": "placement_group1",
+ "display_name": "different_display_name",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "region": "region1",
+ "availability_zone": "availability_zone1",
+ "storage_service": "storage_service1",
+ "array": "array2",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ "patches": [
+ purefusion.PlacementGroupPatch(
+ display_name=purefusion.NullableString(
+ value="different_display_name"
+ ),
+ ),
+ purefusion.PlacementGroupPatch(
+ array=purefusion.NullableString(value="array2"),
+ ),
+ ],
+ },
+ ],
+)
+def test_pg_update_ok(pg_api_init, op_api_init, test_case):
+ module_args = test_case["module_args"]
+ set_module_args(module_args)
+
+ get_operation_calls = [
+ call("op{0}".format(i)) for i in range(len(test_case["patches"]))
+ ]
+ update_placement_group_return_vals = [
+ OperationMock(id="op{0}".format(i)) for i in range(len(test_case["patches"]))
+ ]
+ update_placement_group_calls = [
+ call(
+ p,
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ for p in test_case["patches"]
+ ]
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(return_value=test_case["current_state"])
+ pg_mock.create_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.update_placement_group = MagicMock(
+ side_effect=update_placement_group_return_vals
+ )
+ pg_mock.delete_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(
+ side_effect=lambda op_id: OperationMock(id=op_id, success=True)
+ )
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleExitJson) as excinfo:
+ fusion_pg.main()
+ assert excinfo.value.changed
+
+ pg_mock.get_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.create_placement_group.assert_not_called()
+ pg_mock.update_placement_group.assert_has_calls(
+ update_placement_group_calls, any_order=True
+ )
+ pg_mock.delete_placement_group.assert_not_called()
+ op_mock.get_operation.assert_has_calls(get_operation_calls, any_order=True)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+@pytest.mark.parametrize("failing_patch", [0, 1])
+@pytest.mark.parametrize(
+ ("raised_exception", "expected_exception"),
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_pg_update_exception(
+ pg_api_init, op_api_init, failing_patch, raised_exception, expected_exception
+):
+ module_args = {
+ "name": "placement_group1",
+ "display_name": "different_display_name",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "region": "region1",
+ "availability_zone": "availability_zone1",
+ "storage_service": "storage_service1",
+ "array": "array2",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ patches = [
+ purefusion.PlacementGroupPatch(
+ display_name=purefusion.NullableString(value="different_display_name"),
+ ),
+ purefusion.PlacementGroupPatch(
+ array=purefusion.NullableString(value="array2"),
+ ),
+ ]
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(
+ return_value=purefusion.PlacementGroup(
+ id="placement_group1_id",
+ name="placement_group1",
+ display_name="placement_group1_display_name",
+ self_link="test_self_link",
+ tenant=purefusion.TenantRef(
+ id="tenant1_id",
+ name="tenant1",
+ kind="Tenant",
+ self_link="some_self_link",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="tenant_space1_id",
+ name="tenant_space1",
+ kind="TenantSpace",
+ self_link="some_self_link",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="availability_zone1_id",
+ name="availability_zone1",
+ kind="AvailabilityZone",
+ self_link="some_self_link",
+ ),
+ placement_engine="heuristics",
+ protocols=[],
+ storage_service=purefusion.StorageServiceRef(
+ id="storage_service1_id",
+ name="storage_service",
+ kind="StorageService",
+ self_link="some_self_link",
+ ),
+ array=purefusion.ArrayRef(
+ id="array1_id", name="array1", kind="Array", self_link="some_self_link"
+ ),
+ )
+ )
+ pg_mock.create_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.update_placement_group = MagicMock(
+ side_effect=throw_on_specific_patch(patches, failing_patch, raised_exception, 0)
+ )
+ pg_mock.delete_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(
+ side_effect=lambda op_id: OperationMock(id=op_id, success=True)
+ )
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(expected_exception):
+ fusion_pg.main()
+
+ pg_mock.get_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.create_placement_group.assert_not_called()
+ pg_mock.delete_placement_group.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+@pytest.mark.parametrize("failing_patch", [0, 1])
+@pytest.mark.parametrize(
+ ("raised_exception", "expected_exception"),
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_pg_update_exception(
+ pg_api_init, op_api_init, failing_patch, raised_exception, expected_exception
+):
+ module_args = {
+ "name": "placement_group1",
+ "display_name": "different_display_name",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "region": "region1",
+ "availability_zone": "availability_zone1",
+ "storage_service": "storage_service1",
+ "array": "array2",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ patches = [
+ purefusion.PlacementGroupPatch(
+ display_name=purefusion.NullableString(value="different_display_name"),
+ ),
+ purefusion.PlacementGroupPatch(
+ array=purefusion.NullableString(value="array2"),
+ ),
+ ]
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(
+ return_value=purefusion.PlacementGroup(
+ id="placement_group1_id",
+ name="placement_group1",
+ display_name="placement_group1_display_name",
+ self_link="test_self_link",
+ tenant=purefusion.TenantRef(
+ id="tenant1_id",
+ name="tenant1",
+ kind="Tenant",
+ self_link="some_self_link",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="tenant_space1_id",
+ name="tenant_space1",
+ kind="TenantSpace",
+ self_link="some_self_link",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="availability_zone1_id",
+ name="availability_zone1",
+ kind="AvailabilityZone",
+ self_link="some_self_link",
+ ),
+ placement_engine="heuristics",
+ protocols=[],
+ storage_service=purefusion.StorageServiceRef(
+ id="storage_service1_id",
+ name="storage_service",
+ kind="StorageService",
+ self_link="some_self_link",
+ ),
+ array=purefusion.ArrayRef(
+ id="array1_id", name="array1", kind="Array", self_link="some_self_link"
+ ),
+ )
+ )
+ pg_mock.create_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.update_placement_group = MagicMock(
+ side_effect=throw_on_specific_patch(patches, failing_patch, raised_exception, 0)
+ )
+ pg_mock.delete_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(
+ side_effect=lambda op_id: OperationMock(id=op_id, success=True)
+ )
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(expected_exception) as excinfo:
+ fusion_pg.main()
+
+ pg_mock.get_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.create_placement_group.assert_not_called()
+ pg_mock.delete_placement_group.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+@pytest.mark.parametrize("failing_patch", [0, 1])
+def test_pg_update_op_fails(pg_api_init, op_api_init, failing_patch):
+ module_args = {
+ "name": "placement_group1",
+ "display_name": "different_display_name",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "region": "region1",
+ "availability_zone": "availability_zone1",
+ "storage_service": "storage_service1",
+ "array": "array2",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ patches = [
+ purefusion.PlacementGroupPatch(
+ display_name=purefusion.NullableString(value="different_display_name"),
+ ),
+ purefusion.PlacementGroupPatch(
+ array=purefusion.NullableString(value="array2"),
+ ),
+ ]
+ ops = ["op0", "op1"]
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(
+ return_value=purefusion.PlacementGroup(
+ id="placement_group1_id",
+ name="placement_group1",
+ display_name="placement_group1_display_name",
+ self_link="test_self_link",
+ tenant=purefusion.TenantRef(
+ id="tenant1_id",
+ name="tenant1",
+ kind="Tenant",
+ self_link="some_self_link",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="tenant_space1_id",
+ name="tenant_space1",
+ kind="TenantSpace",
+ self_link="some_self_link",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="availability_zone1_id",
+ name="availability_zone1",
+ kind="AvailabilityZone",
+ self_link="some_self_link",
+ ),
+ placement_engine="heuristics",
+ protocols=[],
+ storage_service=purefusion.StorageServiceRef(
+ id="storage_service1_id",
+ name="storage_service",
+ kind="StorageService",
+ self_link="some_self_link",
+ ),
+ array=purefusion.ArrayRef(
+ id="array1_id", name="array1", kind="Array", self_link="some_self_link"
+ ),
+ )
+ )
+ pg_mock.create_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.update_placement_group = MagicMock(
+ side_effect=lambda patch, tenant_name, tenant_space_name, placement_group_name: OperationMock(
+ id="op{0}".format(patches.index(patch))
+ )
+ )
+ pg_mock.delete_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(
+ side_effect=lambda id: OperationMock(
+ id=id, success=ops.index(id) != failing_patch
+ )
+ )
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(OperationException):
+ fusion_pg.main()
+
+ pg_mock.get_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.create_placement_group.assert_not_called()
+ pg_mock.delete_placement_group.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+def test_pg_delete_ok(pg_api_init, op_api_init, module_args_absent):
+ module_args = module_args_absent
+ set_module_args(module_args)
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(
+ return_value=purefusion.PlacementGroup(
+ id="placement_group1_id",
+ name="placement_group1",
+ display_name="placement_group1_display_name",
+ self_link="test_self_link",
+ tenant=purefusion.TenantRef(
+ id="tenant1_id",
+ name="tenant1",
+ kind="Tenant",
+ self_link="some_self_link",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="tenant_space1_id",
+ name="tenant_space1",
+ kind="TenantSpace",
+ self_link="some_self_link",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="availability_zone1_id",
+ name="availability_zone1",
+ kind="AvailabilityZone",
+ self_link="some_self_link",
+ ),
+ placement_engine="heuristics",
+ protocols=[],
+ storage_service=purefusion.StorageServiceRef(
+ id="storage_service1_id",
+ name="storage_service",
+ kind="StorageService",
+ self_link="some_self_link",
+ ),
+ array=purefusion.ArrayRef(
+ id="array1_id", name="array1", kind="Array", self_link="some_self_link"
+ ),
+ )
+ )
+ pg_mock.create_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.update_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.delete_placement_group = MagicMock(return_value=OperationMock(id="op1"))
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(
+ return_value=OperationMock(id="op1", success=True)
+ )
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleExitJson) as excinfo:
+ fusion_pg.main()
+ assert excinfo.value.changed
+
+ pg_mock.get_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.create_placement_group.assert_not_called()
+ pg_mock.update_placement_group.assert_not_called()
+ pg_mock.delete_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ op_mock.get_operation.assert_called_with("op1")
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+@pytest.mark.parametrize(
+ ("raised_exception", "expected_exception"),
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_pg_delete_exception(
+ pg_api_init, op_api_init, raised_exception, expected_exception, module_args_absent
+):
+ module_args = module_args_absent
+ set_module_args(module_args)
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(
+ return_value=purefusion.PlacementGroup(
+ id="placement_group1_id",
+ name="placement_group1",
+ display_name="placement_group1_display_name",
+ self_link="test_self_link",
+ tenant=purefusion.TenantRef(
+ id="tenant1_id",
+ name="tenant1",
+ kind="Tenant",
+ self_link="some_self_link",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="tenant_space1_id",
+ name="tenant_space1",
+ kind="TenantSpace",
+ self_link="some_self_link",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="availability_zone1_id",
+ name="availability_zone1",
+ kind="AvailabilityZone",
+ self_link="some_self_link",
+ ),
+ placement_engine="heuristics",
+ protocols=[],
+ storage_service=purefusion.StorageServiceRef(
+ id="storage_service1_id",
+ name="storage_service",
+ kind="StorageService",
+ self_link="some_self_link",
+ ),
+ array=purefusion.ArrayRef(
+ id="array1_id", name="array1", kind="Array", self_link="some_self_link"
+ ),
+ )
+ )
+ pg_mock.create_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.update_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.delete_placement_group = MagicMock(side_effect=raised_exception)
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(side_effect=NotImplementedError())
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(expected_exception):
+ fusion_pg.main()
+
+ pg_mock.get_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.create_placement_group.assert_not_called()
+ pg_mock.update_placement_group.assert_not_called()
+ pg_mock.delete_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ op_mock.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+def test_pg_delete_op_fails(pg_api_init, op_api_init, module_args_absent):
+ module_args = module_args_absent
+ set_module_args(module_args)
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(
+ return_value=purefusion.PlacementGroup(
+ id="placement_group1_id",
+ name="placement_group1",
+ display_name="placement_group1_display_name",
+ self_link="test_self_link",
+ tenant=purefusion.TenantRef(
+ id="tenant1_id",
+ name="tenant1",
+ kind="Tenant",
+ self_link="some_self_link",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="tenant_space1_id",
+ name="tenant_space1",
+ kind="TenantSpace",
+ self_link="some_self_link",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="availability_zone1_id",
+ name="availability_zone1",
+ kind="AvailabilityZone",
+ self_link="some_self_link",
+ ),
+ placement_engine="heuristics",
+ protocols=[],
+ storage_service=purefusion.StorageServiceRef(
+ id="storage_service1_id",
+ name="storage_service",
+ kind="StorageService",
+ self_link="some_self_link",
+ ),
+ array=purefusion.ArrayRef(
+ id="array1_id", name="array1", kind="Array", self_link="some_self_link"
+ ),
+ )
+ )
+ pg_mock.create_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.update_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.delete_placement_group = MagicMock(return_value=OperationMock(id="op1"))
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(
+ return_value=OperationMock(id="op1", success=False)
+ )
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(OperationException):
+ fusion_pg.main()
+
+ pg_mock.get_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.create_placement_group.assert_not_called()
+ pg_mock.update_placement_group.assert_not_called()
+ pg_mock.delete_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ op_mock.get_operation.assert_called_with("op1")
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+def test_pg_present_not_changed(pg_api_init, op_api_init, module_args_present):
+ module_args = module_args_present
+ set_module_args(module_args)
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(
+ return_value=purefusion.PlacementGroup(
+ id="placement_group1_id",
+ name="placement_group1",
+ display_name="placement_group1_display_name",
+ self_link="test_self_link",
+ tenant=purefusion.TenantRef(
+ id="tenant1_id",
+ name="tenant1",
+ kind="Tenant",
+ self_link="some_self_link",
+ ),
+ tenant_space=purefusion.TenantSpaceRef(
+ id="tenant_space1_id",
+ name="tenant_space1",
+ kind="TenantSpace",
+ self_link="some_self_link",
+ ),
+ availability_zone=purefusion.AvailabilityZoneRef(
+ id="availability_zone1_id",
+ name="availability_zone1",
+ kind="AvailabilityZone",
+ self_link="some_self_link",
+ ),
+ placement_engine="heuristics",
+ protocols=[],
+ storage_service=purefusion.StorageServiceRef(
+ id="storage_service1_id",
+ name="storage_service",
+ kind="StorageService",
+ self_link="some_self_link",
+ ),
+ array=purefusion.ArrayRef(
+ id="array1_id", name="array1", kind="Array", self_link="some_self_link"
+ ),
+ )
+ )
+ pg_mock.create_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.update_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.delete_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(side_effect=NotImplementedError())
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleExitJson) as excinfo:
+ fusion_pg.main()
+ assert not excinfo.value.changed
+
+ pg_mock.get_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.create_placement_group.assert_not_called()
+ pg_mock.update_placement_group.assert_not_called()
+ pg_mock.delete_placement_group.assert_not_called()
+ op_mock.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.PlacementGroupsApi")
+def test_pg_absent_not_changed(pg_api_init, op_api_init, module_args_absent):
+ module_args = module_args_absent
+ set_module_args(module_args)
+
+ pg_mock = MagicMock()
+ pg_mock.get_placement_group = MagicMock(side_effect=purefusion.rest.ApiException)
+ pg_mock.create_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.update_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_mock.delete_placement_group = MagicMock(side_effect=NotImplementedError())
+ pg_api_init.return_value = pg_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(side_effect=NotImplementedError())
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleExitJson) as excinfo:
+ fusion_pg.main()
+ assert not excinfo.value.changed
+
+ pg_mock.get_placement_group.assert_called_with(
+ tenant_name="tenant1",
+ tenant_space_name="tenant_space1",
+ placement_group_name="placement_group1",
+ )
+ pg_mock.create_placement_group.assert_not_called()
+ pg_mock.update_placement_group.assert_not_called()
+ pg_mock.delete_placement_group.assert_not_called()
+ op_mock.get_operation.assert_not_called()
+
+
+def throw_on_specific_patch(patches, failing_patch_idx, raised_exception, op_offset):
+ patches = patches.copy()
+
+ def _update_side_effect(patch, **kwargs):
+ idx = patches.index(patch)
+ if idx == failing_patch_idx:
+ raise raised_exception()
+ return OperationMock(id="op{0}".format(op_offset + idx))
+
+ return _update_side_effect
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_pp.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_pp.py
new file mode 100644
index 000000000..519caea40
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_pp.py
@@ -0,0 +1,528 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023 Pure Storage, Inc.
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock, patch
+
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_pp
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ OperationMock,
+ exit_json,
+ fail_json,
+ set_module_args,
+)
+from urllib3.exceptions import HTTPError
+
+# GLOBAL MOCKS
+fusion_pp.setup_fusion = MagicMock(return_value=purefusion.api_client.ApiClient())
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+
+@pytest.fixture
+def module_args_present():
+ return {
+ "name": "protection_policy1",
+ "local_rpo": 43,
+ "local_retention": "2H",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+
+
+@pytest.fixture
+def module_args_absent():
+ return {
+ "name": "protection_policy1",
+ "state": "absent",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ProtectionPoliciesApi")
+@pytest.mark.parametrize(
+ ("module_args", "get_not_called"),
+ [
+ # 'name` is missing
+ (
+ {
+ "local_rpo": 10,
+ "local_retention": "10M",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ True,
+ ),
+ # 'local_rpo` is missing
+ (
+ {
+ "name": "protection_policy1",
+ "local_retention": "10M",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ False,
+ ),
+ # 'local_retention` is missing
+ (
+ {
+ "name": "protection_policy1",
+ "local_rpo": 10,
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ False,
+ ),
+ # 'local_rpo` is invalid
+ (
+ {
+ "name": "protection_policy1",
+ "local_rpo": 10,
+ "local_retention": "10yen",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ False,
+ ),
+ # 'local_retention` is invalid
+ (
+ {
+ "name": "protection_policy1",
+ "local_rpo": "10bread",
+ "local_retention": "bre",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ False,
+ ),
+ # 'state` is invalid
+ (
+ {
+ "name": "protection_policy1",
+ "local_rpo": 10,
+ "local_retention": 10,
+ "state": "past",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ False,
+ ),
+ ],
+)
+def test_module_args_wrong(pp_api_init, op_api_init, module_args, get_not_called):
+ set_module_args(module_args)
+
+ pp_mock = MagicMock()
+ pp_mock.get_protection_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ pp_mock.create_protection_policy = MagicMock(side_effect=NotImplementedError())
+ pp_mock.delete_protection_policy = MagicMock(side_effect=NotImplementedError())
+ pp_api_init.return_value = pp_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(side_effect=purefusion.rest.ApiException)
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleFailJson):
+ fusion_pp.main()
+
+ if get_not_called:
+ pp_mock.get_protection_policy.assert_not_called()
+ if pp_mock.get_protection_policy.called:
+ pp_mock.get_protection_policy.assert_called_with(
+ protection_policy_name="protection_policy1"
+ )
+ pp_mock.create_protection_policy.assert_not_called()
+ pp_mock.delete_protection_policy.assert_not_called()
+ op_mock.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ProtectionPoliciesApi")
+def test_pp_create_ok(pp_api_init, op_api_init, module_args_present):
+ module_args = module_args_present
+ module_args["display_name"] = "some_display_name"
+
+ set_module_args(module_args)
+
+ pp_mock = MagicMock()
+ pp_mock.get_protection_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ pp_mock.create_protection_policy = MagicMock(return_value=OperationMock("op1"))
+ pp_mock.delete_protection_policy = MagicMock(side_effect=NotImplementedError())
+ pp_api_init.return_value = pp_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(return_value=OperationMock("op1", success=True))
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleExitJson) as excinfo:
+ fusion_pp.main()
+ assert excinfo.value.changed
+
+ pp_mock.get_protection_policy.assert_called_with(
+ protection_policy_name="protection_policy1"
+ )
+ pp_mock.create_protection_policy.assert_called_with(
+ purefusion.ProtectionPolicyPost(
+ name="protection_policy1",
+ display_name="some_display_name",
+ objectives=[
+ purefusion.RPO(type="RPO", rpo="PT43M"),
+ purefusion.Retention(type="Retention", after="PT120M"),
+ ],
+ )
+ )
+ pp_mock.delete_protection_policy.assert_not_called()
+ op_mock.get_operation.assert_called_with("op1")
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ProtectionPoliciesApi")
+def test_pp_create_without_display_name_ok(
+ pp_api_init, op_api_init, module_args_present
+):
+ module_args = module_args_present
+ set_module_args(module_args)
+
+ pp_mock = MagicMock()
+ pp_mock.get_protection_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ pp_mock.create_protection_policy = MagicMock(return_value=OperationMock("op1"))
+ pp_mock.delete_protection_policy = MagicMock(side_effect=NotImplementedError())
+ pp_api_init.return_value = pp_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(return_value=OperationMock("op1", success=True))
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleExitJson) as excinfo:
+ fusion_pp.main()
+ assert excinfo.value.changed
+
+ pp_mock.get_protection_policy.assert_called_with(
+ protection_policy_name="protection_policy1"
+ )
+ pp_mock.create_protection_policy.assert_called_with(
+ purefusion.ProtectionPolicyPost(
+ name="protection_policy1",
+ display_name="protection_policy1",
+ objectives=[
+ purefusion.RPO(type="RPO", rpo="PT43M"),
+ purefusion.Retention(type="Retention", after="PT120M"),
+ ],
+ )
+ )
+ pp_mock.delete_protection_policy.assert_not_called()
+ op_mock.get_operation.assert_called_with("op1")
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ProtectionPoliciesApi")
+@pytest.mark.parametrize(
+ ("raised_exception", "expected_exception"),
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_pp_create_exception(
+ pp_api_init, op_api_init, raised_exception, expected_exception, module_args_present
+):
+ module_args = module_args_present
+ set_module_args(module_args)
+
+ pp_mock = MagicMock()
+ pp_mock.get_protection_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ pp_mock.create_protection_policy = MagicMock(side_effect=raised_exception)
+ pp_mock.delete_protection_policy = MagicMock(side_effect=NotImplementedError())
+ pp_api_init.return_value = pp_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(side_effect=NotImplementedError())
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(expected_exception):
+ fusion_pp.main()
+
+ pp_mock.get_protection_policy.assert_called_with(
+ protection_policy_name="protection_policy1"
+ )
+ pp_mock.create_protection_policy.assert_called_with(
+ purefusion.ProtectionPolicyPost(
+ name="protection_policy1",
+ display_name="protection_policy1",
+ objectives=[
+ purefusion.RPO(type="RPO", rpo="PT43M"),
+ purefusion.Retention(type="Retention", after="PT120M"),
+ ],
+ )
+ )
+ pp_mock.delete_protection_policy.assert_not_called()
+ op_mock.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ProtectionPoliciesApi")
+def test_pp_create_op_fails(pp_api_init, op_api_init, module_args_present):
+ module_args = module_args_present
+ set_module_args(module_args)
+
+ pp_mock = MagicMock()
+ pp_mock.get_protection_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ pp_mock.create_protection_policy = MagicMock(return_value=OperationMock(id="op1"))
+ pp_mock.delete_protection_policy = MagicMock(side_effect=NotImplementedError())
+ pp_api_init.return_value = pp_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(return_value=OperationMock("op1", success=False))
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(OperationException):
+ fusion_pp.main()
+
+ pp_mock.get_protection_policy.assert_called_with(
+ protection_policy_name="protection_policy1"
+ )
+ pp_mock.create_protection_policy.assert_called_with(
+ purefusion.ProtectionPolicyPost(
+ name="protection_policy1",
+ display_name="protection_policy1",
+ objectives=[
+ purefusion.RPO(type="RPO", rpo="PT43M"),
+ purefusion.Retention(type="Retention", after="PT120M"),
+ ],
+ )
+ )
+ pp_mock.delete_protection_policy.assert_not_called()
+ op_mock.get_operation.assert_called_with("op1")
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ProtectionPoliciesApi")
+def test_pp_delete_ok(pp_api_init, op_api_init, module_args_absent):
+ module_args = module_args_absent
+ set_module_args(module_args)
+
+ pp_mock = MagicMock()
+ pp_mock.get_protection_policy = MagicMock(
+ return_value=purefusion.ProtectionPolicy(
+ id="protection_policy1_id",
+ name="protection_policy1",
+ display_name="protection_policy1_display_name",
+ self_link="test_self_link",
+ objectives=[
+ purefusion.RPO(type="RPO", rpo="PT43M"),
+ purefusion.Retention(type="Retention", after="PT120M"),
+ ],
+ )
+ )
+ pp_mock.create_protection_policy = MagicMock(side_effect=NotImplementedError())
+ pp_mock.delete_protection_policy = MagicMock(return_value=OperationMock(id="op1"))
+ pp_api_init.return_value = pp_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(
+ return_value=OperationMock(id="op1", success=True)
+ )
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleExitJson) as excinfo:
+ fusion_pp.main()
+ assert excinfo.value.changed
+
+ pp_mock.get_protection_policy.assert_called_with(
+ protection_policy_name="protection_policy1"
+ )
+ pp_mock.create_protection_policy.assert_not_called()
+ pp_mock.delete_protection_policy.assert_called_with(
+ protection_policy_name="protection_policy1"
+ )
+ op_mock.get_operation.assert_called_with("op1")
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ProtectionPoliciesApi")
+@pytest.mark.parametrize(
+ ("raised_exception", "expected_exception"),
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_pp_delete_exception(
+ pp_api_init, op_api_init, raised_exception, expected_exception, module_args_absent
+):
+ module_args = module_args_absent
+ set_module_args(module_args)
+
+ pp_mock = MagicMock()
+ pp_mock.get_protection_policy = MagicMock(
+ return_value=purefusion.ProtectionPolicy(
+ id="protection_policy1_id",
+ name="protection_policy1",
+ display_name="protection_policy1_display_name",
+ self_link="test_self_link",
+ objectives=[
+ purefusion.RPO(type="RPO", rpo="PT43M"),
+ purefusion.Retention(type="Retention", after="PT120M"),
+ ],
+ )
+ )
+ pp_mock.create_protection_policy = MagicMock(side_effect=NotImplementedError())
+ pp_mock.delete_protection_policy = MagicMock(side_effect=raised_exception)
+ pp_api_init.return_value = pp_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(side_effect=NotImplementedError())
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(expected_exception):
+ fusion_pp.main()
+
+ pp_mock.get_protection_policy.assert_called_with(
+ protection_policy_name="protection_policy1"
+ )
+ pp_mock.create_protection_policy.assert_not_called()
+ pp_mock.delete_protection_policy.assert_called_with(
+ protection_policy_name="protection_policy1"
+ )
+ op_mock.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ProtectionPoliciesApi")
+def test_pp_delete_op_fails(pp_api_init, op_api_init, module_args_absent):
+ module_args = module_args_absent
+ set_module_args(module_args)
+
+ pp_mock = MagicMock()
+ pp_mock.get_protection_policy = MagicMock(
+ return_value=purefusion.ProtectionPolicy(
+ id="protection_policy1_id",
+ name="protection_policy1",
+ display_name="protection_policy1_display_name",
+ self_link="test_self_link",
+ objectives=[
+ purefusion.RPO(type="RPO", rpo="PT43M"),
+ purefusion.Retention(type="Retention", after="PT120M"),
+ ],
+ )
+ )
+ pp_mock.create_protection_policy = MagicMock(side_effect=NotImplementedError())
+ pp_mock.delete_protection_policy = MagicMock(return_value=OperationMock(id="op1"))
+ pp_api_init.return_value = pp_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(
+ return_value=OperationMock(id="op1", success=False)
+ )
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(OperationException):
+ fusion_pp.main()
+
+ pp_mock.get_protection_policy.assert_called_with(
+ protection_policy_name="protection_policy1"
+ )
+ pp_mock.create_protection_policy.assert_not_called()
+ pp_mock.delete_protection_policy.assert_called_with(
+ protection_policy_name="protection_policy1"
+ )
+ op_mock.get_operation.assert_called_with("op1")
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ProtectionPoliciesApi")
+def test_pp_present_not_changed(pp_api_init, op_api_init):
+ module_args = {
+ "name": "protection_policy1",
+ "display_name": "some_display_name",
+ "local_rpo": 43,
+ "local_retention": "2H",
+ "state": "present",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ pp_mock = MagicMock()
+ pp_mock.get_protection_policy = MagicMock(
+ return_value=purefusion.ProtectionPolicy(
+ id="protection_policy1_id",
+ name="protection_policy1",
+ display_name="some_display_name",
+ self_link="test_self_link",
+ objectives=[
+ purefusion.RPO(type="RPO", rpo="PT43M"),
+ purefusion.Retention(type="Retention", after="PT120M"),
+ ],
+ )
+ )
+ pp_mock.create_protection_policy = MagicMock(side_effect=NotImplementedError())
+ pp_mock.delete_protection_policy = MagicMock(side_effect=NotImplementedError())
+ pp_api_init.return_value = pp_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(side_effect=NotImplementedError())
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleExitJson) as excinfo:
+ fusion_pp.main()
+ assert not excinfo.value.changed
+
+ pp_mock.get_protection_policy.assert_called_with(
+ protection_policy_name="protection_policy1"
+ )
+ pp_mock.create_protection_policy.assert_not_called()
+ pp_mock.delete_protection_policy.assert_not_called()
+ op_mock.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.ProtectionPoliciesApi")
+def test_pp_absent_not_changed(pp_api_init, op_api_init, module_args_absent):
+ module_args = module_args_absent
+ set_module_args(module_args)
+
+ pp_mock = MagicMock()
+ pp_mock.get_protection_policy = MagicMock(side_effect=purefusion.rest.ApiException)
+ pp_mock.create_protection_policy = MagicMock(side_effect=NotImplementedError())
+ pp_mock.delete_protection_policy = MagicMock(side_effect=NotImplementedError())
+ pp_api_init.return_value = pp_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(side_effect=NotImplementedError())
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleExitJson) as excinfo:
+ fusion_pp.main()
+ assert not excinfo.value.changed
+
+ pp_mock.get_protection_policy.assert_called_with(
+ protection_policy_name="protection_policy1"
+ )
+ pp_mock.create_protection_policy.assert_not_called()
+ pp_mock.delete_protection_policy.assert_not_called()
+ op_mock.get_operation.assert_not_called()
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_ra.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_ra.py
new file mode 100644
index 000000000..6456fa7d7
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_ra.py
@@ -0,0 +1,813 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023 Pure Storage, Inc.
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock, patch
+
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_ra
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ OperationMock,
+ exit_json,
+ fail_json,
+ set_module_args,
+)
+from urllib3.exceptions import HTTPError
+
+# GLOBAL MOCKS
+fusion_ra.setup_fusion = MagicMock(return_value=purefusion.api_client.ApiClient())
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+
+@pytest.fixture
+def module_args_present():
+ return {
+ "state": "present",
+ "role": "az-admin",
+ "user": "user1",
+ "scope": "organization",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+
+
+@pytest.fixture
+def module_args_absent():
+ return {
+ "state": "absent",
+ "role": "az-admin",
+ "user": "user1",
+ "scope": "organization",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.IdentityManagerApi")
+@patch("fusion.RoleAssignmentsApi")
+@pytest.mark.parametrize(
+ "module_args",
+ [
+ # 'role` is missing
+ {
+ "state": "present",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "user": "user1",
+ "scope": "tenant_space",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # 'user` is missing
+ {
+ "state": "present",
+ "role": "tenant-space-admin",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "scope": "tenant_space",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # 'scope` is invalid
+ {
+ "state": "present",
+ "role": "tenant-space-admin",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "user": "user1",
+ "scope": "bikini_bottom",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # 'state` is invalid
+ {
+ "state": "past",
+ "role": "tenant-space-admin",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "user": "user1",
+ "scope": "tenant_space",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # 'tenant` is missing #1
+ {
+ "state": "present",
+ "role": "tenant-space-admin",
+ "tenant_space": "tenant_space1",
+ "user": "user1",
+ "scope": "tenant_space",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # 'tenant` is missing #2
+ {
+ "state": "present",
+ "role": "tenant-space-admin",
+ "tenant_space": "tenant_space1",
+ "user": "user1",
+ "scope": "tenant",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # 'tenant_space` is missing
+ {
+ "state": "present",
+ "role": "tenant-space-admin",
+ "tenant": "tenant1",
+ "user": "user1",
+ "scope": "tenant_space",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # both 'principal` and `user` are specified
+ {
+ "state": "present",
+ "role": "tenant-space-admin",
+ "tenant": "tenant1",
+ "user": "user1",
+ "principal": "123456",
+ "scope": "tenant_space",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # both 'principal` and `api_client_key` are specified
+ {
+ "state": "present",
+ "role": "tenant-space-admin",
+ "tenant": "tenant1",
+ "api_client_key": "pure1:apikey:asdf123XYZ",
+ "principal": "123456",
+ "scope": "tenant_space",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ ],
+)
+def test_module_args_wrong(ra_api_init, im_api_init, op_api_init, module_args):
+ set_module_args(module_args)
+
+ ra_mock = MagicMock()
+ ra_mock.list_role_assignments = MagicMock(side_effect=NotImplementedError())
+ ra_mock.create_role_assignment = MagicMock(side_effect=NotImplementedError())
+ ra_mock.delete_role_assignment = MagicMock(side_effect=NotImplementedError())
+ ra_api_init.return_value = ra_mock
+
+ im_mock = MagicMock()
+ im_mock.list_users = MagicMock(
+ return_value=[
+ purefusion.User(
+ id="principal1",
+ self_link="test_value",
+ name="user1",
+ email="example@example.com",
+ )
+ ]
+ )
+ im_api_init.return_value = im_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(side_effect=purefusion.rest.ApiException)
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleFailJson):
+ fusion_ra.main()
+
+ ra_mock.list_role_assignments.assert_not_called()
+ ra_mock.create_role_assignment.assert_not_called()
+ ra_mock.delete_role_assignment.assert_not_called()
+ op_mock.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.IdentityManagerApi")
+@patch("fusion.RoleAssignmentsApi")
+def test_ra_user_does_not_exist(
+ ra_api_init, im_api_init, op_api_init, module_args_present
+):
+ module_args = module_args_present
+ set_module_args(module_args)
+
+ ra_mock = MagicMock()
+ ra_mock.list_role_assignments = MagicMock(side_effect=purefusion.rest.ApiException)
+ ra_mock.create_role_assignment = MagicMock(side_effect=NotImplementedError())
+ ra_mock.delete_role_assignment = MagicMock(side_effect=NotImplementedError())
+ ra_api_init.return_value = ra_mock
+
+ im_mock = MagicMock()
+ im_mock.list_users = MagicMock(return_value=[])
+ im_api_init.return_value = im_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(side_effect=purefusion.rest.ApiException)
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleFailJson):
+ fusion_ra.main()
+
+ ra_mock.list_role_assignments.assert_not_called()
+ ra_mock.create_role_assignment.assert_not_called()
+ ra_mock.delete_role_assignment.assert_not_called()
+ op_mock.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.IdentityManagerApi")
+@patch("fusion.RoleAssignmentsApi")
+@pytest.mark.parametrize(
+ "args_and_scope",
+ [
+ # organization scope
+ (
+ {
+ "state": "present",
+ "role": "az-admin",
+ "user": "user1",
+ "scope": "organization",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ "/",
+ ),
+ # tenant scope
+ (
+ {
+ "state": "present",
+ "role": "tenant-admin",
+ "user": "user1",
+ "scope": "tenant",
+ "tenant": "tenant1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ "/tenants/tenant1",
+ ),
+ # tenant space scope
+ (
+ {
+ "state": "present",
+ "role": "tenant-space-admin",
+ "user": "user1",
+ "scope": "tenant_space",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ "/tenants/tenant1/tenant-spaces/tenant_space1",
+ ),
+ # principal instead of user
+ (
+ {
+ "state": "present",
+ "role": "az-admin",
+ "principal": "principal1",
+ "scope": "organization",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ "/",
+ ),
+ # api_client_key instead of user
+ (
+ {
+ "state": "present",
+ "role": "az-admin",
+ "api_client_key": "pure1:apikey:asdf123XYZ",
+ "scope": "organization",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ "/",
+ ),
+ ],
+)
+def test_ra_create_ok(ra_api_init, im_api_init, op_api_init, args_and_scope):
+ module_args = args_and_scope[0]
+ ra_scope = args_and_scope[1]
+ set_module_args(module_args)
+
+ ra_mock = MagicMock()
+ ra_mock.list_role_assignments = MagicMock(return_value=[])
+ ra_mock.create_role_assignment = MagicMock(return_value=OperationMock("op1"))
+ ra_mock.delete_role_assignment = MagicMock(side_effect=NotImplementedError())
+ ra_api_init.return_value = ra_mock
+
+ im_mock = MagicMock()
+ im_mock.list_users = MagicMock(
+ return_value=[
+ purefusion.User(
+ id="principal1",
+ self_link="test_value",
+ name="user1",
+ email="example@example.com",
+ )
+ ]
+ )
+ im_api_init.return_value = im_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(return_value=OperationMock("op1", success=True))
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleExitJson) as excinfo:
+ fusion_ra.main()
+ assert excinfo.value.changed
+
+ ra_mock.list_role_assignments.assert_called_with(
+ role_name=module_args["role"], principal="principal1"
+ )
+ ra_mock.create_role_assignment.assert_called_with(
+ purefusion.RoleAssignmentPost(scope=ra_scope, principal="principal1"),
+ role_name=module_args["role"],
+ )
+ ra_mock.delete_role_assignment.assert_not_called()
+ op_mock.get_operation.assert_called_with("op1")
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.IdentityManagerApi")
+@patch("fusion.RoleAssignmentsApi")
+@pytest.mark.parametrize(
+ ("raised_exception", "expected_exception"),
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_ra_create_exception(
+ ra_api_init,
+ im_api_init,
+ op_api_init,
+ raised_exception,
+ expected_exception,
+ module_args_present,
+):
+ module_args = module_args_present
+ set_module_args(module_args)
+
+ ra_mock = MagicMock()
+ ra_mock.list_role_assignments = MagicMock(return_value=[])
+ ra_mock.create_role_assignment = MagicMock(side_effect=raised_exception)
+ ra_mock.delete_role_assignment = MagicMock(side_effect=NotImplementedError())
+ ra_api_init.return_value = ra_mock
+
+ im_mock = MagicMock()
+ im_mock.list_users = MagicMock(
+ return_value=[
+ purefusion.User(
+ id="principal1",
+ self_link="test_value",
+ name="user1",
+ email="example@example.com",
+ )
+ ]
+ )
+ im_api_init.return_value = im_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(side_effect=NotImplementedError())
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(expected_exception):
+ fusion_ra.main()
+
+ ra_mock.list_role_assignments.assert_called_with(
+ role_name="az-admin", principal="principal1"
+ )
+ ra_mock.create_role_assignment.assert_called_with(
+ purefusion.RoleAssignmentPost(scope="/", principal="principal1"),
+ role_name="az-admin",
+ )
+ ra_mock.delete_role_assignment.assert_not_called()
+ op_mock.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.IdentityManagerApi")
+@patch("fusion.RoleAssignmentsApi")
+def test_ra_create_op_fails(ra_api_init, im_api_init, op_api_init, module_args_present):
+ module_args = module_args_present
+ set_module_args(module_args)
+
+ ra_mock = MagicMock()
+ ra_mock.list_role_assignments = MagicMock(return_value=[])
+ ra_mock.create_role_assignment = MagicMock(return_value=OperationMock(id="op1"))
+ ra_mock.delete_role_assignment = MagicMock(side_effect=NotImplementedError())
+ ra_api_init.return_value = ra_mock
+
+ im_mock = MagicMock()
+ im_mock.list_users = MagicMock(
+ return_value=[
+ purefusion.User(
+ id="principal1",
+ self_link="test_value",
+ name="user1",
+ email="example@example.com",
+ )
+ ]
+ )
+ im_api_init.return_value = im_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(return_value=OperationMock("op1", success=False))
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(OperationException):
+ fusion_ra.main()
+
+ ra_mock.list_role_assignments.assert_called_with(
+ role_name="az-admin", principal="principal1"
+ )
+ ra_mock.create_role_assignment.assert_called_with(
+ purefusion.RoleAssignmentPost(scope="/", principal="principal1"),
+ role_name="az-admin",
+ )
+ ra_mock.delete_role_assignment.assert_not_called()
+ op_mock.get_operation.assert_called_with("op1")
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.IdentityManagerApi")
+@patch("fusion.RoleAssignmentsApi")
+@pytest.mark.parametrize(
+ "args_and_scope",
+ [
+ # organization scope
+ (
+ {
+ "state": "absent",
+ "role": "az-admin",
+ "user": "user1",
+ "scope": "organization",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ purefusion.ResourceMetadata(
+ id="org_id",
+ name="org",
+ self_link="/",
+ ),
+ ),
+ # tenant scope
+ (
+ {
+ "state": "absent",
+ "role": "tenant-admin",
+ "user": "user1",
+ "scope": "tenant",
+ "tenant": "tenant1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ purefusion.ResourceMetadata(
+ id="tenant1_id",
+ name="tenant1",
+ self_link="/tenants/tenant1",
+ ),
+ ),
+ # tenant space scope
+ (
+ {
+ "state": "absent",
+ "role": "tenant-space-admin",
+ "user": "user1",
+ "scope": "tenant_space",
+ "tenant": "tenant1",
+ "tenant_space": "tenant_space1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ purefusion.ResourceMetadata(
+ id="tenant_space1_id",
+ name="tenant_space1",
+ self_link="/tenants/tenant1/tenant-spaces/tenant_space1",
+ ),
+ ),
+ ],
+)
+def test_ra_delete_ok(ra_api_init, im_api_init, op_api_init, args_and_scope):
+ module_args = args_and_scope[0]
+ ra_scope = args_and_scope[1]
+ set_module_args(module_args)
+
+ ra_mock = MagicMock()
+ ra_mock.list_role_assignments = MagicMock(
+ return_value=[
+ purefusion.RoleAssignment(
+ id="ra1_id",
+ name="ra1",
+ self_link="test_value",
+ role=purefusion.RoleRef(
+ id="role1_id",
+ name=module_args["role"],
+ kind="Role",
+ self_link="test_value",
+ ),
+ scope=ra_scope,
+ principal="principal1",
+ )
+ ]
+ )
+ ra_mock.create_role_assignment = MagicMock(side_effect=NotImplementedError())
+ ra_mock.delete_role_assignment = MagicMock(return_value=OperationMock(id="op1"))
+ ra_api_init.return_value = ra_mock
+
+ im_mock = MagicMock()
+ im_mock.list_users = MagicMock(
+ return_value=[
+ purefusion.User(
+ id="principal1",
+ self_link="test_value",
+ name="user1",
+ email="example@example.com",
+ )
+ ]
+ )
+ im_api_init.return_value = im_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(
+ return_value=OperationMock(id="op1", success=True)
+ )
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleExitJson) as excinfo:
+ fusion_ra.main()
+ assert excinfo.value.changed
+
+ ra_mock.list_role_assignments.assert_called_with(
+ role_name=module_args["role"], principal="principal1"
+ )
+ ra_mock.create_role_assignment.assert_not_called()
+ ra_mock.delete_role_assignment.assert_called_with(
+ role_name=module_args["role"], role_assignment_name="ra1"
+ )
+ op_mock.get_operation.assert_called_with("op1")
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.IdentityManagerApi")
+@patch("fusion.RoleAssignmentsApi")
+@pytest.mark.parametrize(
+ ("raised_exception", "expected_exception"),
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_ra_delete_exception(
+ ra_api_init,
+ im_api_init,
+ op_api_init,
+ raised_exception,
+ expected_exception,
+ module_args_absent,
+):
+ module_args = module_args_absent
+ set_module_args(module_args)
+
+ ra_mock = MagicMock()
+ ra_mock.list_role_assignments = MagicMock(
+ return_value=[
+ purefusion.RoleAssignment(
+ id="ra1_id",
+ name="ra1",
+ self_link="test_value",
+ role=purefusion.RoleRef(
+ id="role1_id",
+ name=module_args["role"],
+ kind="Role",
+ self_link="test_value",
+ ),
+ scope=purefusion.ResourceMetadata(
+ id="org_id",
+ name="org",
+ self_link="/",
+ ),
+ principal="principal1",
+ )
+ ]
+ )
+ ra_mock.create_role_assignment = MagicMock(side_effect=NotImplementedError())
+ ra_mock.delete_role_assignment = MagicMock(side_effect=raised_exception)
+ ra_api_init.return_value = ra_mock
+
+ im_mock = MagicMock()
+ im_mock.list_users = MagicMock(
+ return_value=[
+ purefusion.User(
+ id="principal1",
+ self_link="test_value",
+ name="user1",
+ email="example@example.com",
+ )
+ ]
+ )
+ im_api_init.return_value = im_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(side_effect=NotImplementedError())
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(expected_exception):
+ fusion_ra.main()
+
+ ra_mock.list_role_assignments.assert_called_with(
+ role_name=module_args["role"], principal="principal1"
+ )
+ ra_mock.create_role_assignment.assert_not_called()
+ ra_mock.delete_role_assignment.assert_called_with(
+ role_name=module_args["role"], role_assignment_name="ra1"
+ )
+ op_mock.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.IdentityManagerApi")
+@patch("fusion.RoleAssignmentsApi")
+def test_ra_delete_op_fails(ra_api_init, im_api_init, op_api_init, module_args_absent):
+ module_args = module_args_absent
+ set_module_args(module_args)
+
+ ra_mock = MagicMock()
+ ra_mock.list_role_assignments = MagicMock(
+ return_value=[
+ purefusion.RoleAssignment(
+ id="ra1_id",
+ name="ra1",
+ self_link="test_value",
+ role=purefusion.RoleRef(
+ id="role1_id",
+ name=module_args["role"],
+ kind="Role",
+ self_link="test_value",
+ ),
+ scope=purefusion.ResourceMetadata(
+ id="org_id",
+ name="org",
+ self_link="/",
+ ),
+ principal="principal1",
+ )
+ ]
+ )
+ ra_mock.create_role_assignment = MagicMock(side_effect=NotImplementedError())
+ ra_mock.delete_role_assignment = MagicMock(return_value=OperationMock(id="op1"))
+ ra_api_init.return_value = ra_mock
+
+ im_mock = MagicMock()
+ im_mock.list_users = MagicMock(
+ return_value=[
+ purefusion.User(
+ id="principal1",
+ self_link="test_value",
+ name="user1",
+ email="example@example.com",
+ )
+ ]
+ )
+ im_api_init.return_value = im_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(
+ return_value=OperationMock(id="op1", success=False)
+ )
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(OperationException):
+ fusion_ra.main()
+
+ ra_mock.list_role_assignments.assert_called_with(
+ role_name=module_args["role"], principal="principal1"
+ )
+ ra_mock.create_role_assignment.assert_not_called()
+ ra_mock.delete_role_assignment.assert_called_with(
+ role_name=module_args["role"], role_assignment_name="ra1"
+ )
+ op_mock.get_operation.assert_called_with("op1")
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.IdentityManagerApi")
+@patch("fusion.RoleAssignmentsApi")
+def test_ra_present_not_changed(
+ ra_api_init, im_api_init, op_api_init, module_args_present
+):
+ module_args = module_args_present
+ set_module_args(module_args)
+
+ ra_mock = MagicMock()
+ ra_mock.list_role_assignments = MagicMock(
+ return_value=[
+ purefusion.RoleAssignment(
+ id="ra1_id",
+ name="ra1",
+ self_link="test_value",
+ role=purefusion.RoleRef(
+ id="role1_id",
+ name=module_args["role"],
+ kind="Role",
+ self_link="test_value",
+ ),
+ scope=purefusion.ResourceMetadata(
+ id="org_id",
+ name="org",
+ self_link="/",
+ ),
+ principal="principal1",
+ )
+ ]
+ )
+ ra_mock.create_role_assignment = MagicMock(side_effect=NotImplementedError())
+ ra_mock.delete_role_assignment = MagicMock(side_effect=NotImplementedError())
+ ra_api_init.return_value = ra_mock
+
+ im_mock = MagicMock()
+ im_mock.list_users = MagicMock(
+ return_value=[
+ purefusion.User(
+ id="principal1",
+ self_link="test_value",
+ name="user1",
+ email="example@example.com",
+ )
+ ]
+ )
+ im_api_init.return_value = im_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(side_effect=NotImplementedError())
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleExitJson) as excinfo:
+ fusion_ra.main()
+ assert not excinfo.value.changed
+
+ ra_mock.list_role_assignments.assert_called_with(
+ role_name=module_args["role"], principal="principal1"
+ )
+ ra_mock.create_role_assignment.assert_not_called()
+ ra_mock.delete_role_assignment.assert_not_called()
+ op_mock.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.IdentityManagerApi")
+@patch("fusion.RoleAssignmentsApi")
+def test_ra_absent_not_changed(
+ ra_api_init, im_api_init, op_api_init, module_args_absent
+):
+ module_args = module_args_absent
+ set_module_args(module_args)
+
+ ra_mock = MagicMock()
+ ra_mock.list_role_assignments = MagicMock(return_value=[])
+ ra_mock.create_role_assignment = MagicMock(side_effect=NotImplementedError())
+ ra_mock.delete_role_assignment = MagicMock(side_effect=NotImplementedError())
+ ra_api_init.return_value = ra_mock
+
+ im_mock = MagicMock()
+ im_mock.list_users = MagicMock(
+ return_value=[
+ purefusion.User(
+ id="principal1",
+ self_link="test_value",
+ name="user1",
+ email="example@example.com",
+ )
+ ]
+ )
+ im_api_init.return_value = im_mock
+
+ op_mock = MagicMock()
+ op_mock.get_operation = MagicMock(side_effect=NotImplementedError())
+ op_api_init.return_value = op_mock
+
+ with pytest.raises(AnsibleExitJson) as excinfo:
+ fusion_ra.main()
+ assert not excinfo.value.changed
+
+ ra_mock.list_role_assignments.assert_called_with(
+ role_name=module_args["role"], principal="principal1"
+ )
+ ra_mock.create_role_assignment.assert_not_called()
+ ra_mock.delete_role_assignment.assert_not_called()
+ op_mock.get_operation.assert_not_called()
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_region.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_region.py
new file mode 100644
index 000000000..6b13adecf
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_region.py
@@ -0,0 +1,798 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock, patch
+
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_region
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ FailedOperationMock,
+ OperationMock,
+ SuccessfulOperationMock,
+ exit_json,
+ fail_json,
+ set_module_args,
+)
+from urllib3.exceptions import HTTPError
+
+# GLOBAL MOCKS
+fusion_region.setup_fusion = MagicMock(return_value=purefusion.api_client.ApiClient())
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.RegionsApi")
+@pytest.mark.parametrize(
+ "module_args",
+ [
+ # required parameter 'name` is missing
+ {
+ "state": "present",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # unknown parameter 'extra' is provided
+ {
+ "state": "present",
+ "name": "region1",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ "extra": "value",
+ },
+ # parameter 'state` has incorrect value
+ {
+ "state": "cool",
+ "name": "region1",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ ],
+)
+def test_module_fails_on_wrong_parameters(m_region_api, m_op_api, module_args):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_region = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_region = MagicMock(return_value=OperationMock(1))
+ api_obj.update_region = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_region = MagicMock(return_value=OperationMock(3))
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+ m_region_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_region.main()
+
+ # check api was not called at all
+ api_obj.get_region.assert_not_called()
+ api_obj.create_region.assert_not_called()
+ api_obj.update_region.assert_not_called()
+ api_obj.delete_region.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.RegionsApi")
+def test_region_create(m_region_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "region1",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_region = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_region = MagicMock(return_value=OperationMock(1))
+ api_obj.update_region = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_region = MagicMock(return_value=OperationMock(3))
+ m_region_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_region.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_region.assert_called_once_with(region_name=module_args["name"])
+ api_obj.create_region.assert_called_once_with(
+ purefusion.RegionPost(
+ name=module_args["name"], display_name=module_args["display_name"]
+ )
+ )
+ api_obj.update_region.assert_not_called()
+ api_obj.delete_region.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.RegionsApi")
+def test_region_create_without_display_name(m_region_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "region1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_region = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_region = MagicMock(return_value=OperationMock(1))
+ api_obj.update_region = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_region = MagicMock(return_value=OperationMock(3))
+ m_region_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_region.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_region.assert_called_once_with(region_name=module_args["name"])
+ api_obj.create_region.assert_called_once_with(
+ purefusion.RegionPost(
+ name=module_args["name"], display_name=module_args["name"]
+ )
+ )
+ api_obj.update_region.assert_not_called()
+ api_obj.delete_region.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.RegionsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_region_create_exception(m_region_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "region1",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_region = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_region = MagicMock(side_effect=exec_original)
+ api_obj.update_region = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_region = MagicMock(return_value=OperationMock(3))
+ m_region_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_region.main()
+
+ # check api was called correctly
+ api_obj.get_region.assert_called_once_with(region_name=module_args["name"])
+ api_obj.create_region.assert_called_once_with(
+ purefusion.RegionPost(
+ name=module_args["name"], display_name=module_args["display_name"]
+ )
+ )
+ api_obj.update_region.assert_not_called()
+ api_obj.delete_region.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.RegionsApi")
+def test_region_create_op_fails(m_region_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "region1",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_region = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_region = MagicMock(return_value=OperationMock(1))
+ api_obj.update_region = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_region = MagicMock(return_value=OperationMock(3))
+ m_region_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_region.main()
+
+ # check api was called correctly
+ api_obj.get_region.assert_called_once_with(region_name=module_args["name"])
+ api_obj.create_region.assert_called_once_with(
+ purefusion.RegionPost(
+ name=module_args["name"], display_name=module_args["display_name"]
+ )
+ )
+ api_obj.update_region.assert_not_called()
+ api_obj.delete_region.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.RegionsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_region_create_op_exception(m_region_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "region1",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_region = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_region = MagicMock(return_value=OperationMock(1))
+ api_obj.update_region = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_region = MagicMock(return_value=OperationMock(3))
+ m_region_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_region.main()
+
+ # check api was called correctly
+ api_obj.get_region.assert_called_once_with(region_name=module_args["name"])
+ api_obj.create_region.assert_called_once_with(
+ purefusion.RegionPost(
+ name=module_args["name"], display_name=module_args["display_name"]
+ )
+ )
+ api_obj.update_region.assert_not_called()
+ api_obj.delete_region.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.RegionsApi")
+def test_region_update(m_region_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "region1",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_region = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_region = MagicMock(return_value=purefusion.Region(**current_region))
+ api_obj.create_region = MagicMock(return_value=OperationMock(1))
+ api_obj.update_region = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_region = MagicMock(return_value=OperationMock(3))
+ m_region_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_region.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_region.assert_called_once_with(region_name=module_args["name"])
+ api_obj.create_region.assert_not_called()
+ api_obj.update_region.assert_called_once_with(
+ purefusion.RegionPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ region_name=module_args["name"],
+ )
+ api_obj.delete_region.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.RegionsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_region_update_exception(m_region_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "region1",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_region = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_region = MagicMock(return_value=purefusion.Region(**current_region))
+ api_obj.create_region = MagicMock(return_value=OperationMock(1))
+ api_obj.update_region = MagicMock(side_effect=exec_original)
+ api_obj.delete_region = MagicMock(return_value=OperationMock(3))
+ m_region_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_region.main()
+
+ # check api was called correctly
+ api_obj.get_region.assert_called_once_with(region_name=module_args["name"])
+ api_obj.create_region.assert_not_called()
+ api_obj.update_region.assert_called_once_with(
+ purefusion.RegionPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ region_name=module_args["name"],
+ )
+ api_obj.delete_region.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.RegionsApi")
+def test_region_update_op_fails(m_region_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "region1",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_region = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_region = MagicMock(return_value=purefusion.Region(**current_region))
+ api_obj.create_region = MagicMock(return_value=OperationMock(1))
+ api_obj.update_region = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_region = MagicMock(return_value=OperationMock(3))
+ m_region_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_region.main()
+
+ # check api was called correctly
+ api_obj.get_region.assert_called_once_with(region_name=module_args["name"])
+ api_obj.create_region.assert_not_called()
+ api_obj.update_region.assert_called_once_with(
+ purefusion.RegionPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ region_name=module_args["name"],
+ )
+ api_obj.delete_region.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.RegionsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_region_update_op_exception(m_region_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "region1",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_region = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_region = MagicMock(return_value=purefusion.Region(**current_region))
+ api_obj.create_region = MagicMock(return_value=OperationMock(1))
+ api_obj.update_region = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_region = MagicMock(return_value=OperationMock(3))
+ m_region_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_region.main()
+
+ # check api was called correctly
+ api_obj.get_region.assert_called_once_with(region_name=module_args["name"])
+ api_obj.create_region.assert_not_called()
+ api_obj.update_region.assert_called_once_with(
+ purefusion.RegionPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ region_name=module_args["name"],
+ )
+ api_obj.delete_region.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.RegionsApi")
+def test_region_present_not_changed(m_region_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "region1",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_region = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": module_args["display_name"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_region = MagicMock(return_value=purefusion.Region(**current_region))
+ api_obj.create_region = MagicMock(return_value=OperationMock(1))
+ api_obj.update_region = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_region = MagicMock(return_value=OperationMock(3))
+ m_region_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_region.main()
+
+ assert not exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_region.assert_called_once_with(region_name=module_args["name"])
+ api_obj.create_region.assert_not_called()
+ api_obj.update_region.assert_not_called()
+ api_obj.delete_region.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.RegionsApi")
+def test_region_absent_not_changed(m_region_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "region1",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_region = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_region = MagicMock(return_value=OperationMock(1))
+ api_obj.update_region = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_region = MagicMock(return_value=OperationMock(3))
+ m_region_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_region.main()
+
+ assert not exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_region.assert_called_once_with(region_name=module_args["name"])
+ api_obj.create_region.assert_not_called()
+ api_obj.update_region.assert_not_called()
+ api_obj.delete_region.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.RegionsApi")
+def test_region_delete(m_region_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "region1",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_region = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": "different", # display_name doesn't match but UPDATE shouldn't be called
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_region = MagicMock(return_value=purefusion.Region(**current_region))
+ api_obj.create_region = MagicMock(return_value=OperationMock(1))
+ api_obj.update_region = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_region = MagicMock(return_value=OperationMock(3))
+ m_region_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_region.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_region.assert_called_once_with(region_name=module_args["name"])
+ api_obj.create_region.assert_not_called()
+ api_obj.update_region.assert_not_called()
+ api_obj.delete_region.assert_called_once_with(region_name=module_args["name"])
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.RegionsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_region_delete_exception(m_region_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "absent",
+ "name": "region1",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_region = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": "different", # display_name doesn't match but UPDATE shouldn't be called
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_region = MagicMock(return_value=purefusion.Region(**current_region))
+ api_obj.create_region = MagicMock(return_value=OperationMock(1))
+ api_obj.update_region = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_region = MagicMock(side_effect=exec_original)
+ m_region_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_region.main()
+
+ # check api was called correctly
+ api_obj.get_region.assert_called_once_with(region_name=module_args["name"])
+ api_obj.create_region.assert_not_called()
+ api_obj.update_region.assert_not_called()
+ api_obj.delete_region.assert_called_once_with(region_name=module_args["name"])
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.RegionsApi")
+def test_region_delete_op_fails(m_region_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "region1",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_region = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": "different", # display_name doesn't match but UPDATE shouldn't be called
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_region = MagicMock(return_value=purefusion.Region(**current_region))
+ api_obj.create_region = MagicMock(return_value=OperationMock(1))
+ api_obj.update_region = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_region = MagicMock(return_value=OperationMock(3))
+ m_region_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_region.main()
+
+ # check api was called correctly
+ api_obj.get_region.assert_called_once_with(region_name=module_args["name"])
+ api_obj.create_region.assert_not_called()
+ api_obj.update_region.assert_not_called()
+ api_obj.delete_region.assert_called_once_with(region_name=module_args["name"])
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.RegionsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_region_delete_op_exception(m_region_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "absent",
+ "name": "region1",
+ "display_name": "Region 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_region = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": "different", # display_name doesn't match but UPDATE shouldn't be called
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_region = MagicMock(return_value=purefusion.Region(**current_region))
+ api_obj.create_region = MagicMock(return_value=OperationMock(1))
+ api_obj.update_region = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_region = MagicMock(return_value=OperationMock(3))
+ m_region_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_region.main()
+
+ # check api was called correctly
+ api_obj.get_region.assert_called_once_with(region_name=module_args["name"])
+ api_obj.create_region.assert_not_called()
+ api_obj.update_region.assert_not_called()
+ api_obj.delete_region.assert_called_once_with(region_name=module_args["name"])
+ op_obj.get_operation.assert_called_once_with(3)
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_sc.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_sc.py
new file mode 100644
index 000000000..1a2db191c
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_sc.py
@@ -0,0 +1,1240 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock, patch
+
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_sc
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ FailedOperationMock,
+ OperationMock,
+ SuccessfulOperationMock,
+ exit_json,
+ fail_json,
+ set_module_args,
+)
+from urllib3.exceptions import HTTPError
+
+# GLOBAL MOCKS
+fusion_sc.setup_fusion = MagicMock(return_value=purefusion.api_client.ApiClient())
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+@pytest.mark.parametrize(
+ "module_args",
+ [
+ # required parameter 'name` is missing
+ {
+ "state": "present",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # required parameter 'storage_service` is missing
+ {
+ "state": "present",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # unknown parameter 'extra' is provided
+ {
+ "state": "present",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ "extra": "value",
+ },
+ # parameter 'state` has incorrect value
+ {
+ "state": "cool",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ ],
+)
+def test_module_fails_on_wrong_parameters(m_sc_api, m_op_api, module_args):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+ m_sc_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_sc.main()
+
+ # check api was not called at all
+ api_obj.get_storage_class.assert_not_called()
+ api_obj.create_storage_class.assert_not_called()
+ api_obj.update_storage_class.assert_not_called()
+ api_obj.delete_storage_class.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+@pytest.mark.parametrize(
+ "iops_arg,iops_exp",
+ [("2000000", 2_000_000), (None, 100_000_000)],
+)
+@pytest.mark.parametrize(
+ "bw_arg,bw_exp",
+ [("256G", 274877906944), (None, 549755813888)],
+)
+@pytest.mark.parametrize(
+ "size_arg,size_exp",
+ [("2P", 2251799813685248), (None, 4503599627370496)],
+)
+def test_sc_create(
+ m_sc_api, m_op_api, iops_arg, iops_exp, bw_arg, bw_exp, size_arg, size_exp
+):
+ module_args = {
+ "state": "present",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": iops_arg,
+ "bw_limit": bw_arg,
+ "size_limit": size_arg,
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_sc.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_called_once_with(
+ purefusion.StorageClassPost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ iops_limit=iops_exp,
+ bandwidth_limit=bw_exp,
+ size_limit=size_exp,
+ ),
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.update_storage_class.assert_not_called()
+ api_obj.delete_storage_class.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+def test_sc_create_without_display_name(m_sc_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "sc1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ parsed_size = 2251799813685248
+ parsed_bandwidth = 274877906944
+ parsed_iops = 2000000
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_sc.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_called_once_with(
+ purefusion.StorageClassPost(
+ name=module_args["name"],
+ display_name=module_args["name"],
+ iops_limit=parsed_iops,
+ bandwidth_limit=parsed_bandwidth,
+ size_limit=parsed_size,
+ ),
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.update_storage_class.assert_not_called()
+ api_obj.delete_storage_class.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+@pytest.mark.parametrize("iops_arg", [-100, 99, 100_000_001])
+def test_sc_create_iops_out_of_range(m_sc_api, m_op_api, iops_arg):
+ module_args = {
+ "state": "present",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": iops_arg,
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_sc.main()
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_not_called()
+ api_obj.update_storage_class.assert_not_called()
+ api_obj.delete_storage_class.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+@pytest.mark.parametrize("bw_arg", ["1023K", "513G"])
+def test_sc_create_bw_out_of_range(m_sc_api, m_op_api, bw_arg):
+ module_args = {
+ "state": "present",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": bw_arg,
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_sc.main()
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_not_called()
+ api_obj.update_storage_class.assert_not_called()
+ api_obj.delete_storage_class.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+@pytest.mark.parametrize("size_arg", ["1023K", "5P"])
+def test_sc_create_size_out_of_range(m_sc_api, m_op_api, size_arg):
+ module_args = {
+ "state": "present",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": size_arg,
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_sc.main()
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_not_called()
+ api_obj.update_storage_class.assert_not_called()
+ api_obj.delete_storage_class.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_sc_create_exception(m_sc_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ parsed_size = 2251799813685248
+ parsed_bandwidth = 274877906944
+ parsed_iops = 2000000
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_class = MagicMock(side_effect=exec_original)
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_sc.main()
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_called_once_with(
+ purefusion.StorageClassPost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ iops_limit=parsed_iops,
+ bandwidth_limit=parsed_bandwidth,
+ size_limit=parsed_size,
+ ),
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.update_storage_class.assert_not_called()
+ api_obj.delete_storage_class.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+def test_sc_create_op_fails(m_sc_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ parsed_size = 2251799813685248
+ parsed_bandwidth = 274877906944
+ parsed_iops = 2000000
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_sc.main()
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_called_once_with(
+ purefusion.StorageClassPost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ iops_limit=parsed_iops,
+ bandwidth_limit=parsed_bandwidth,
+ size_limit=parsed_size,
+ ),
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.update_storage_class.assert_not_called()
+ api_obj.delete_storage_class.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_sc_create_op_exception(m_sc_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ parsed_size = 2251799813685248
+ parsed_bandwidth = 274877906944
+ parsed_iops = 2000000
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_sc.main()
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_called_once_with(
+ purefusion.StorageClassPost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ iops_limit=parsed_iops,
+ bandwidth_limit=parsed_bandwidth,
+ size_limit=parsed_size,
+ ),
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.update_storage_class.assert_not_called()
+ api_obj.delete_storage_class.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+def test_sc_update(m_sc_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_sc = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ "iops_limit": "2000000",
+ "bandwidth_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": module_args["storage_service"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(
+ return_value=purefusion.StorageClass(**current_sc)
+ )
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_sc.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_not_called()
+ api_obj.update_storage_class.assert_called_once_with(
+ purefusion.StorageClassPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.delete_storage_class.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_sc_update_exception(m_sc_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_sc = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ "iops_limit": "2000000",
+ "bandwidth_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": module_args["storage_service"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(
+ return_value=purefusion.StorageClass(**current_sc)
+ )
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(side_effect=exec_original)
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_sc.main()
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_not_called()
+ api_obj.update_storage_class.assert_called_once_with(
+ purefusion.StorageClassPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.delete_storage_class.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+def test_sc_update_op_fails(m_sc_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_sc = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ "iops_limit": "2000000",
+ "bandwidth_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": module_args["storage_service"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(
+ return_value=purefusion.StorageClass(**current_sc)
+ )
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_sc.main()
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_not_called()
+ api_obj.update_storage_class.assert_called_once_with(
+ purefusion.StorageClassPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.delete_storage_class.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_sc_update_op_exception(m_sc_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_sc = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ "iops_limit": "2000000",
+ "bandwidth_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": module_args["storage_service"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(
+ return_value=purefusion.StorageClass(**current_sc)
+ )
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_sc.main()
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_not_called()
+ api_obj.update_storage_class.assert_called_once_with(
+ purefusion.StorageClassPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.delete_storage_class.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+def test_sc_present_not_changed(m_sc_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_sc = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": module_args["display_name"],
+ "iops_limit": "2000000",
+ "bandwidth_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": module_args["storage_service"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(
+ return_value=purefusion.StorageClass(**current_sc)
+ )
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_sc.main()
+
+ assert not exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_not_called()
+ api_obj.update_storage_class.assert_not_called()
+ api_obj.delete_storage_class.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+def test_sc_absent_not_changed(m_sc_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_sc.main()
+
+ assert not exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_not_called()
+ api_obj.update_storage_class.assert_not_called()
+ api_obj.delete_storage_class.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+def test_sc_update_limits_not_changed(m_sc_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_sc = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": module_args["display_name"],
+ "iops_limit": "1500000", # does not match but shouldn't be updated!
+ "bandwidth_limit": "300G", # does not match but shouldn't be updated!
+ "size_limit": "1P", # does not match but shouldn't be updated!
+ "storage_service": module_args["storage_service"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(
+ return_value=purefusion.StorageClass(**current_sc)
+ )
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_sc.main()
+
+ assert not exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_not_called()
+ api_obj.update_storage_class.assert_not_called()
+ api_obj.delete_storage_class.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+def test_sc_delete(m_sc_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_sc = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ "iops_limit": "2000000",
+ "bandwidth_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": module_args["storage_service"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(
+ return_value=purefusion.StorageClass(**current_sc)
+ )
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_sc.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_not_called()
+ api_obj.update_storage_class.assert_not_called()
+ api_obj.delete_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_sc_delete_exception(m_sc_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "absent",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_sc = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ "iops_limit": "2000000",
+ "bandwidth_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": module_args["storage_service"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(
+ return_value=purefusion.StorageClass(**current_sc)
+ )
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(side_effect=exec_original)
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_sc.main()
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_not_called()
+ api_obj.update_storage_class.assert_not_called()
+ api_obj.delete_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+def test_sc_delete_op_fails(m_sc_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_sc = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ "iops_limit": "2000000",
+ "bandwidth_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": module_args["storage_service"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(
+ return_value=purefusion.StorageClass(**current_sc)
+ )
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_sc.main()
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_not_called()
+ api_obj.update_storage_class.assert_not_called()
+ api_obj.delete_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageClassesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_sc_delete_op_exception(m_sc_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "absent",
+ "name": "sc1",
+ "display_name": "Storage Class 1",
+ "iops_limit": "2000000",
+ "bw_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": "ss1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_sc = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ "iops_limit": "2000000",
+ "bandwidth_limit": "256G",
+ "size_limit": "2P",
+ "storage_service": module_args["storage_service"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_class = MagicMock(
+ return_value=purefusion.StorageClass(**current_sc)
+ )
+ api_obj.create_storage_class = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_class = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_class = MagicMock(return_value=OperationMock(3))
+ m_sc_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_sc.main()
+
+ # check api was called correctly
+ api_obj.get_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ api_obj.create_storage_class.assert_not_called()
+ api_obj.update_storage_class.assert_not_called()
+ api_obj.delete_storage_class.assert_called_once_with(
+ storage_class_name=module_args["name"],
+ storage_service_name=module_args["storage_service"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_se.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_se.py
new file mode 100644
index 000000000..a071190db
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_se.py
@@ -0,0 +1,1039 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock, patch
+
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_se
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ FailedOperationMock,
+ OperationMock,
+ SuccessfulOperationMock,
+ exit_json,
+ fail_json,
+ set_module_args,
+)
+from urllib3.exceptions import HTTPError
+
+# GLOBAL MOCKS
+fusion_se.setup_fusion = MagicMock(return_value=purefusion.api_client.ApiClient())
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+
+@pytest.fixture
+def module_args():
+ return {
+ "state": "present",
+ "name": "se1",
+ "display_name": "Storage Endpoint 1",
+ "region": "region1",
+ "availability_zone": "az1",
+ "iscsi": [
+ {
+ "address": "10.21.200.124/24",
+ "gateway": "10.21.200.1",
+ "network_interface_groups": ["subnet-0", "subnet-1"],
+ }
+ ],
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+
+
+@pytest.fixture
+def current_se(module_args):
+ return {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"],
+ "display_name": module_args["display_name"],
+ "region": module_args["region"],
+ "availability_zone": module_args["availability_zone"],
+ "endpoint_type": "iscsi",
+ "iscsi": [
+ dict(discovery_interface) for discovery_interface in module_args["iscsi"]
+ ],
+ }
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+@pytest.mark.parametrize(
+ "module_args",
+ [
+ # required parameter 'name` is missing
+ {
+ "state": "present",
+ "display_name": "Storage Endpoint 1",
+ "region": "region1",
+ "availability_zone": "az1",
+ "iscsi": [
+ {
+ "address": "10.21.200.124/24",
+ "gateway": "10.21.200.1",
+ "network_interface_groups": ["subnet-0", "subnet-1"],
+ }
+ ],
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # required parameter 'region` is missing
+ {
+ "state": "present",
+ "name": "se1",
+ "display_name": "Storage Endpoint 1",
+ "availability_zone": "az1",
+ "iscsi": [
+ {
+ "address": "10.21.200.124/24",
+ "gateway": "10.21.200.1",
+ "network_interface_groups": ["subnet-0", "subnet-1"],
+ }
+ ],
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # required parameter 'availability_zone` is missing
+ {
+ "state": "present",
+ "name": "se1",
+ "display_name": "Storage Endpoint 1",
+ "region": "region1",
+ "iscsi": [
+ {
+ "address": "10.21.200.124/24",
+ "gateway": "10.21.200.1",
+ "network_interface_groups": ["subnet-0", "subnet-1"],
+ }
+ ],
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # unknown parameter 'extra' is provided
+ {
+ "state": "present",
+ "name": "se1",
+ "display_name": "Storage Endpoint 1",
+ "region": "region1",
+ "availability_zone": "az1",
+ "iscsi": [
+ {
+ "address": "10.21.200.124/24",
+ "gateway": "10.21.200.1",
+ "network_interface_groups": ["subnet-0", "subnet-1"],
+ }
+ ],
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ "extra": "value",
+ },
+ # parameter 'state` has incorrect value
+ {
+ "state": "cool",
+ "name": "se1",
+ "display_name": "Storage Endpoint 1",
+ "region": "region1",
+ "availability_zone": "az1",
+ "iscsi": [
+ {
+ "address": "10.21.200.124/24",
+ "gateway": "10.21.200.1",
+ "network_interface_groups": ["subnet-0", "subnet-1"],
+ }
+ ],
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # parameter 'iscsi` and 'cbs_azure_iscsi' are used at the same time
+ {
+ "state": "present",
+ "name": "se1",
+ "display_name": "Storage Endpoint 1",
+ "region": "region1",
+ "availability_zone": "az1",
+ "iscsi": [
+ {
+ "address": "10.21.200.124/24",
+ "gateway": "10.21.200.1",
+ "network_interface_groups": ["subnet-0", "subnet-1"],
+ }
+ ],
+ "cbs_azure_iscsi": {
+ "storage_endpoint_collection_identity": "/subscriptions/sub/resourcegroups/sec/providers/ms/userAssignedIdentities/secId",
+ "load_balancer": "/subscriptions/sub/resourcegroups/sec/providers/ms/loadBalancers/sec-lb",
+ "load_balancer_addresses": [],
+ },
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ },
+ # parameter 'cbs_azure_iscsi' has invalid address
+ {
+ "state": "present",
+ "name": "se1",
+ "display_name": "Storage Endpoint 1",
+ "region": "region1",
+ "availability_zone": "az1",
+ "cbs_azure_iscsi": {
+ "storage_endpoint_collection_identity": "/subscriptions/sub/resourcegroups/sec/providers/ms/userAssignedIdentities/secId",
+ "load_balancer": "/subscriptions/sub/resourcegroups/sec/providers/ms/loadBalancers/sec-lb",
+ "load_balancer_addresses": ["not an address"],
+ },
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ },
+ # parameter 'iscsi' has invalid 'gateway' address
+ {
+ "state": "present",
+ "name": "se1",
+ "display_name": "Storage Endpoint 1",
+ "region": "region1",
+ "availability_zone": "az1",
+ "iscsi": [
+ {
+ "address": "10.21.200.124/24",
+ "gateway": "not an address",
+ "network_interface_groups": ["subnet-0", "subnet-1"],
+ }
+ ],
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ },
+ # parameter 'iscsi' has invalid 'address' address
+ {
+ "state": "present",
+ "name": "se1",
+ "display_name": "Storage Endpoint 1",
+ "region": "region1",
+ "availability_zone": "az1",
+ "iscsi": [
+ {
+ "address": "not an address",
+ "gateway": "10.21.200.1",
+ "network_interface_groups": ["subnet-0", "subnet-1"],
+ }
+ ],
+ "app_id": "ABCD1234",
+ "key_file": "private-key.pem",
+ },
+ ],
+)
+def test_module_fails_on_wrong_parameters(m_se_api, m_op_api, module_args):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_endpoint = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_endpoint = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_endpoint = MagicMock(return_value=OperationMock(3))
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+ m_se_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_se.main()
+
+ # check api was not called at all
+ api_obj.get_storage_endpoint.assert_not_called()
+ api_obj.create_storage_endpoint.assert_not_called()
+ api_obj.update_storage_endpoint.assert_not_called()
+ api_obj.delete_storage_endpoint.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+def test_se_create_iscsi(m_se_api, m_op_api, module_args):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_endpoint = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_endpoint = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_endpoint = MagicMock(return_value=OperationMock(3))
+ m_se_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_se.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.create_storage_endpoint.assert_called_once_with(
+ purefusion.StorageEndpointPost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ endpoint_type="iscsi",
+ iscsi=purefusion.StorageEndpointIscsiPost(
+ discovery_interfaces=[
+ purefusion.StorageEndpointIscsiDiscoveryInterfacePost(**endpoint)
+ for endpoint in module_args["iscsi"]
+ ]
+ ),
+ ),
+ region_name=module_args["region"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.update_storage_endpoint.assert_not_called()
+ api_obj.delete_storage_endpoint.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+def test_se_create_cbs_azure_iscsi(m_se_api, m_op_api, module_args):
+ del module_args["iscsi"]
+ module_args["cbs_azure_iscsi"] = {
+ "storage_endpoint_collection_identity": "/subscriptions/sub/resourcegroups/sec/providers/ms/userAssignedIdentities/secId",
+ "load_balancer": "/subscriptions/sub/resourcegroups/sec/providers/ms/loadBalancers/sec-lb",
+ "load_balancer_addresses": ["234.1.2.3"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_endpoint = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_endpoint = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_endpoint = MagicMock(return_value=OperationMock(3))
+ m_se_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_se.main()
+
+ assert exc.value.changed is True
+
+ # check api was called correctly
+ api_obj.get_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.create_storage_endpoint.assert_called_once_with(
+ purefusion.StorageEndpointPost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ endpoint_type="cbs-azure-iscsi",
+ cbs_azure_iscsi=purefusion.StorageEndpointCbsAzureIscsiPost(
+ storage_endpoint_collection_identity=module_args["cbs_azure_iscsi"][
+ "storage_endpoint_collection_identity"
+ ],
+ load_balancer=module_args["cbs_azure_iscsi"]["load_balancer"],
+ load_balancer_addresses=module_args["cbs_azure_iscsi"][
+ "load_balancer_addresses"
+ ],
+ ),
+ ),
+ region_name=module_args["region"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.update_storage_endpoint.assert_not_called()
+ api_obj.delete_storage_endpoint.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+def test_se_create_without_display_name(m_se_api, m_op_api, module_args):
+ del module_args["display_name"]
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_endpoint = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_endpoint = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_endpoint = MagicMock(return_value=OperationMock(3))
+ m_se_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_se.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.create_storage_endpoint.assert_called_once_with(
+ purefusion.StorageEndpointPost(
+ name=module_args["name"],
+ display_name=module_args["name"],
+ endpoint_type="iscsi",
+ iscsi=purefusion.StorageEndpointIscsiPost(
+ discovery_interfaces=[
+ purefusion.StorageEndpointIscsiDiscoveryInterfacePost(**endpoint)
+ for endpoint in module_args["iscsi"]
+ ]
+ ),
+ ),
+ region_name=module_args["region"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.update_storage_endpoint.assert_not_called()
+ api_obj.delete_storage_endpoint.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_se_create_exception(
+ m_se_api, m_op_api, exec_original, exec_catch, module_args
+):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_endpoint = MagicMock(side_effect=exec_original)
+ api_obj.update_storage_endpoint = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_endpoint = MagicMock(return_value=OperationMock(3))
+ m_se_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_se.main()
+
+ # check api was called correctly
+ api_obj.get_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.create_storage_endpoint.assert_called_once_with(
+ purefusion.StorageEndpointPost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ endpoint_type="iscsi",
+ iscsi=purefusion.StorageEndpointIscsiPost(
+ discovery_interfaces=[
+ purefusion.StorageEndpointIscsiDiscoveryInterfacePost(**endpoint)
+ for endpoint in module_args["iscsi"]
+ ]
+ ),
+ ),
+ region_name=module_args["region"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.update_storage_endpoint.assert_not_called()
+ api_obj.delete_storage_endpoint.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+def test_se_create_op_fails(m_se_api, m_op_api, module_args):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_endpoint = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_endpoint = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_endpoint = MagicMock(return_value=OperationMock(3))
+ m_se_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_se.main()
+
+ # check api was called correctly
+ api_obj.get_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.create_storage_endpoint.assert_called_once_with(
+ purefusion.StorageEndpointPost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ endpoint_type="iscsi",
+ iscsi=purefusion.StorageEndpointIscsiPost(
+ discovery_interfaces=[
+ purefusion.StorageEndpointIscsiDiscoveryInterfacePost(**endpoint)
+ for endpoint in module_args["iscsi"]
+ ]
+ ),
+ ),
+ region_name=module_args["region"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.update_storage_endpoint.assert_not_called()
+ api_obj.delete_storage_endpoint.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_se_create_op_exception(
+ m_se_api, m_op_api, exec_original, exec_catch, module_args
+):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_endpoint = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_endpoint = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_endpoint = MagicMock(return_value=OperationMock(3))
+ m_se_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_se.main()
+
+ # check api was called correctly
+ api_obj.get_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.create_storage_endpoint.assert_called_once_with(
+ purefusion.StorageEndpointPost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ endpoint_type="iscsi",
+ iscsi=purefusion.StorageEndpointIscsiPost(
+ discovery_interfaces=[
+ purefusion.StorageEndpointIscsiDiscoveryInterfacePost(**endpoint)
+ for endpoint in module_args["iscsi"]
+ ]
+ ),
+ ),
+ region_name=module_args["region"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.update_storage_endpoint.assert_not_called()
+ api_obj.delete_storage_endpoint.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+def test_se_update(m_se_api, m_op_api, module_args, current_se):
+ current_se["display_name"] = None
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(
+ return_value=purefusion.StorageEndpoint(**current_se)
+ )
+ api_obj.create_storage_endpoint = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_endpoint = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_endpoint = MagicMock(return_value=OperationMock(3))
+ m_se_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_se.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.create_storage_endpoint.assert_not_called()
+ api_obj.update_storage_endpoint.assert_called_once_with(
+ purefusion.StorageEndpointPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.delete_storage_endpoint.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_se_update_exception(
+ m_se_api, m_op_api, exec_original, exec_catch, module_args, current_se
+):
+ current_se["display_name"] = None
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(
+ return_value=purefusion.StorageEndpoint(**current_se)
+ )
+ api_obj.create_storage_endpoint = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_endpoint = MagicMock(side_effect=exec_original)
+ api_obj.delete_storage_endpoint = MagicMock(return_value=OperationMock(3))
+ m_se_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_se.main()
+
+ # check api was called correctly
+ api_obj.get_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.create_storage_endpoint.assert_not_called()
+ api_obj.update_storage_endpoint.assert_called_once_with(
+ purefusion.StorageEndpointPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.delete_storage_endpoint.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+def test_se_update_op_fails(m_se_api, m_op_api, module_args, current_se):
+ current_se["display_name"] = None
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(
+ return_value=purefusion.StorageEndpoint(**current_se)
+ )
+ api_obj.create_storage_endpoint = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_endpoint = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_endpoint = MagicMock(return_value=OperationMock(3))
+ m_se_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_se.main()
+
+ # check api was called correctly
+ api_obj.get_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.create_storage_endpoint.assert_not_called()
+ api_obj.update_storage_endpoint.assert_called_once_with(
+ purefusion.StorageEndpointPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.delete_storage_endpoint.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_se_update_op_exception(
+ m_se_api, m_op_api, exec_original, exec_catch, module_args, current_se
+):
+ current_se["display_name"] = None
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(
+ return_value=purefusion.StorageEndpoint(**current_se)
+ )
+ api_obj.create_storage_endpoint = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_endpoint = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_endpoint = MagicMock(return_value=OperationMock(3))
+ m_se_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_se.main()
+
+ # check api was called correctly
+ api_obj.get_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.create_storage_endpoint.assert_not_called()
+ api_obj.update_storage_endpoint.assert_called_once_with(
+ purefusion.StorageEndpointPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.delete_storage_endpoint.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+def test_se_present_not_changed(m_se_api, m_op_api, module_args, current_se):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(
+ return_value=purefusion.StorageEndpoint(**current_se)
+ )
+ api_obj.create_storage_endpoint = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_endpoint = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_endpoint = MagicMock(return_value=OperationMock(3))
+ m_se_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_se.main()
+
+ assert not exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.create_storage_endpoint.assert_not_called()
+ api_obj.update_storage_endpoint.assert_not_called()
+ api_obj.delete_storage_endpoint.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+def test_se_absent_not_changed(m_se_api, m_op_api, module_args, current_se):
+ module_args["state"] = "absent"
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_endpoint = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_endpoint = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_endpoint = MagicMock(return_value=OperationMock(3))
+ m_se_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_se.main()
+
+ assert not exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.create_storage_endpoint.assert_not_called()
+ api_obj.update_storage_endpoint.assert_not_called()
+ api_obj.delete_storage_endpoint.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+def test_se_delete(m_se_api, m_op_api, module_args, current_se):
+ module_args["state"] = "absent"
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(
+ return_value=purefusion.StorageEndpoint(**current_se)
+ )
+ api_obj.create_storage_endpoint = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_endpoint = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_endpoint = MagicMock(return_value=OperationMock(3))
+ m_se_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_se.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.create_storage_endpoint.assert_not_called()
+ api_obj.update_storage_endpoint.assert_not_called()
+ api_obj.delete_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_se_delete_exception(
+ m_se_api, m_op_api, exec_original, exec_catch, module_args, current_se
+):
+ module_args["state"] = "absent"
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(
+ return_value=purefusion.StorageEndpoint(**current_se)
+ )
+ api_obj.create_storage_endpoint = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_endpoint = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_endpoint = MagicMock(side_effect=exec_original)
+ m_se_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_se.main()
+
+ # check api was called correctly
+ api_obj.get_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.create_storage_endpoint.assert_not_called()
+ api_obj.update_storage_endpoint.assert_not_called()
+ api_obj.delete_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+def test_se_delete_op_fails(m_se_api, m_op_api, module_args, current_se):
+ module_args["state"] = "absent"
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(
+ return_value=purefusion.StorageEndpoint(**current_se)
+ )
+ api_obj.create_storage_endpoint = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_endpoint = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_endpoint = MagicMock(return_value=OperationMock(3))
+ m_se_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_se.main()
+
+ # check api was called correctly
+ api_obj.get_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.create_storage_endpoint.assert_not_called()
+ api_obj.update_storage_endpoint.assert_not_called()
+ api_obj.delete_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageEndpointsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_se_delete_op_exception(
+ m_se_api, m_op_api, exec_original, exec_catch, module_args, current_se
+):
+ module_args["state"] = "absent"
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_endpoint = MagicMock(
+ return_value=purefusion.StorageEndpoint(**current_se)
+ )
+ api_obj.create_storage_endpoint = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_endpoint = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_endpoint = MagicMock(return_value=OperationMock(3))
+ m_se_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_se.main()
+
+ # check api was called correctly
+ api_obj.get_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ api_obj.create_storage_endpoint.assert_not_called()
+ api_obj.update_storage_endpoint.assert_not_called()
+ api_obj.delete_storage_endpoint.assert_called_once_with(
+ region_name=module_args["region"],
+ storage_endpoint_name=module_args["name"],
+ availability_zone_name=module_args["availability_zone"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_ss.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_ss.py
new file mode 100644
index 000000000..d784b1a52
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_ss.py
@@ -0,0 +1,930 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock, patch
+
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_ss
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ FailedOperationMock,
+ OperationMock,
+ SuccessfulOperationMock,
+ exit_json,
+ fail_json,
+ set_module_args,
+)
+from urllib3.exceptions import HTTPError
+
+# GLOBAL MOCKS
+fusion_ss.setup_fusion = MagicMock(return_value=purefusion.api_client.ApiClient())
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+@pytest.mark.parametrize(
+ "module_args",
+ [
+ # required parameter 'name` is missing
+ {
+ "state": "present",
+ "display_name": "Storage Service 1",
+ "hardware_types": ["flash-array-x"],
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # unknown parameter 'extra' is provided
+ {
+ "state": "present",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "hardware_types": ["flash-array-x"],
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ "extra": "value",
+ },
+ # parameter 'state` has incorrect value
+ {
+ "state": "cool",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "hardware_types": ["flash-array-x"],
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # parameter 'hardware_types` has incorrect value
+ {
+ "state": "present",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "hardware_types": ["hdd-array-x"],
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ ],
+)
+def test_module_fails_on_wrong_parameters(m_ss_api, m_op_api, module_args):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_service = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_service = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_service = MagicMock(return_value=OperationMock(3))
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+ m_ss_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_ss.main()
+
+ # check api was not called at all
+ api_obj.get_storage_service.assert_not_called()
+ api_obj.create_storage_service.assert_not_called()
+ api_obj.update_storage_service.assert_not_called()
+ api_obj.delete_storage_service.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+def test_ss_create(m_ss_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "hardware_types": ["flash-array-x"],
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_service = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_service = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_service = MagicMock(return_value=OperationMock(3))
+ m_ss_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_ss.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ api_obj.create_storage_service.assert_called_once_with(
+ purefusion.StorageServicePost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ hardware_types=module_args["hardware_types"],
+ )
+ )
+ api_obj.update_storage_service.assert_not_called()
+ api_obj.delete_storage_service.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+def test_ss_create_without_display_name(m_ss_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "ss1",
+ "hardware_types": ["flash-array-x"],
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_service = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_service = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_service = MagicMock(return_value=OperationMock(3))
+ m_ss_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_ss.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ api_obj.create_storage_service.assert_called_once_with(
+ purefusion.StorageServicePost(
+ name=module_args["name"],
+ display_name=module_args["name"],
+ hardware_types=module_args["hardware_types"],
+ )
+ )
+ api_obj.update_storage_service.assert_not_called()
+ api_obj.delete_storage_service.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+def test_array_create_without_hardware_type(m_ss_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_service = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_service = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_service = MagicMock(return_value=OperationMock(3))
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+ m_ss_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_ss.main()
+
+ # check api was not called at all
+ api_obj.get_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ api_obj.create_storage_service.assert_not_called()
+ api_obj.update_storage_service.assert_not_called()
+ api_obj.delete_storage_service.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_ss_create_exception(m_ss_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "hardware_types": ["flash-array-x"],
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_service = MagicMock(side_effect=exec_original)
+ api_obj.update_storage_service = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_service = MagicMock(return_value=OperationMock(3))
+ m_ss_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_ss.main()
+
+ # check api was called correctly
+ api_obj.get_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ api_obj.create_storage_service.assert_called_once_with(
+ purefusion.StorageServicePost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ hardware_types=module_args["hardware_types"],
+ )
+ )
+ api_obj.update_storage_service.assert_not_called()
+ api_obj.delete_storage_service.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+def test_ss_create_op_fails(m_ss_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "hardware_types": ["flash-array-x"],
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_service = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_service = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_service = MagicMock(return_value=OperationMock(3))
+ m_ss_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_ss.main()
+
+ # check api was called correctly
+ api_obj.get_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ api_obj.create_storage_service.assert_called_once_with(
+ purefusion.StorageServicePost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ hardware_types=module_args["hardware_types"],
+ )
+ )
+ api_obj.update_storage_service.assert_not_called()
+ api_obj.delete_storage_service.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_ss_create_op_exception(m_ss_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "hardware_types": ["flash-array-x"],
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_service = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_service = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_service = MagicMock(return_value=OperationMock(3))
+ m_ss_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_ss.main()
+
+ # check api was called correctly
+ api_obj.get_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ api_obj.create_storage_service.assert_called_once_with(
+ purefusion.StorageServicePost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ hardware_types=module_args["hardware_types"],
+ )
+ )
+ api_obj.update_storage_service.assert_not_called()
+ api_obj.delete_storage_service.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+def test_ss_update(m_ss_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "hardware_types": ["flash-array-x"],
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ss = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ "hardware_types": ["flash-array-c"], # is different but shouldn't be patched!
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(
+ return_value=purefusion.StorageService(**current_ss)
+ )
+ api_obj.create_storage_service = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_service = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_service = MagicMock(return_value=OperationMock(3))
+ m_ss_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_ss.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ api_obj.create_storage_service.assert_not_called()
+ api_obj.update_storage_service.assert_called_once_with(
+ purefusion.StorageServicePatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ storage_service_name=module_args["name"],
+ )
+ api_obj.delete_storage_service.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_ss_update_exception(m_ss_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ss = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ "hardware_types": ["flash-array-x"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(
+ return_value=purefusion.StorageService(**current_ss)
+ )
+ api_obj.create_storage_service = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_service = MagicMock(side_effect=exec_original)
+ api_obj.delete_storage_service = MagicMock(return_value=OperationMock(3))
+ m_ss_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_ss.main()
+
+ # check api was called correctly
+ api_obj.get_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ api_obj.create_storage_service.assert_not_called()
+ api_obj.update_storage_service.assert_called_once_with(
+ purefusion.StorageServicePatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ storage_service_name=module_args["name"],
+ )
+ api_obj.delete_storage_service.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+def test_ss_update_op_fails(m_ss_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ss = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ "hardware_types": ["flash-array-x"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(
+ return_value=purefusion.StorageService(**current_ss)
+ )
+ api_obj.create_storage_service = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_service = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_service = MagicMock(return_value=OperationMock(3))
+ m_ss_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_ss.main()
+
+ # check api was called correctly
+ api_obj.get_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ api_obj.create_storage_service.assert_not_called()
+ api_obj.update_storage_service.assert_called_once_with(
+ purefusion.StorageServicePatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ storage_service_name=module_args["name"],
+ )
+ api_obj.delete_storage_service.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_ss_update_op_exception(m_ss_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ss = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ "hardware_types": ["flash-array-x"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(
+ return_value=purefusion.StorageService(**current_ss)
+ )
+ api_obj.create_storage_service = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_service = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_service = MagicMock(return_value=OperationMock(3))
+ m_ss_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_ss.main()
+
+ # check api was called correctly
+ api_obj.get_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ api_obj.create_storage_service.assert_not_called()
+ api_obj.update_storage_service.assert_called_once_with(
+ purefusion.StorageServicePatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ storage_service_name=module_args["name"],
+ )
+ api_obj.delete_storage_service.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+def test_ss_present_not_changed(m_ss_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ss = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": module_args["display_name"],
+ "hardware_types": ["flash-array-x"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(
+ return_value=purefusion.StorageService(**current_ss)
+ )
+ api_obj.create_storage_service = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_service = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_service = MagicMock(return_value=OperationMock(3))
+ m_ss_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_ss.main()
+
+ assert not exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ api_obj.create_storage_service.assert_not_called()
+ api_obj.update_storage_service.assert_not_called()
+ api_obj.delete_storage_service.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+def test_ss_absent_not_changed(m_ss_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_storage_service = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_service = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_service = MagicMock(return_value=OperationMock(3))
+ m_ss_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_ss.main()
+
+ assert not exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ api_obj.create_storage_service.assert_not_called()
+ api_obj.update_storage_service.assert_not_called()
+ api_obj.delete_storage_service.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+def test_ss_delete(m_ss_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ss = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": "different", # display_name doesn't match but UPDATE shouldn't be called
+ "hardware_types": ["flash-array-x"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(
+ return_value=purefusion.StorageService(**current_ss)
+ )
+ api_obj.create_storage_service = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_service = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_service = MagicMock(return_value=OperationMock(3))
+ m_ss_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_ss.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ api_obj.create_storage_service.assert_not_called()
+ api_obj.update_storage_service.assert_not_called()
+ api_obj.delete_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_ss_delete_exception(m_ss_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "absent",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ss = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": "different", # display_name doesn't match but UPDATE shouldn't be called
+ "hardware_types": ["flash-array-x"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(
+ return_value=purefusion.StorageService(**current_ss)
+ )
+ api_obj.create_storage_service = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_service = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_service = MagicMock(side_effect=exec_original)
+ m_ss_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_ss.main()
+
+ # check api was called correctly
+ api_obj.get_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ api_obj.create_storage_service.assert_not_called()
+ api_obj.update_storage_service.assert_not_called()
+ api_obj.delete_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+def test_ss_delete_op_fails(m_ss_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ss = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": "different", # display_name doesn't match but UPDATE shouldn't be called
+ "hardware_types": ["flash-array-x"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(
+ return_value=purefusion.StorageService(**current_ss)
+ )
+ api_obj.create_storage_service = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_service = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_service = MagicMock(return_value=OperationMock(3))
+ m_ss_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_ss.main()
+
+ # check api was called correctly
+ api_obj.get_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ api_obj.create_storage_service.assert_not_called()
+ api_obj.update_storage_service.assert_not_called()
+ api_obj.delete_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.StorageServicesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_ss_delete_op_exception(m_ss_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "absent",
+ "name": "ss1",
+ "display_name": "Storage Service 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ss = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": "different", # display_name doesn't match but UPDATE shouldn't be called
+ "hardware_types": ["flash-array-x"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_storage_service = MagicMock(
+ return_value=purefusion.StorageService(**current_ss)
+ )
+ api_obj.create_storage_service = MagicMock(return_value=OperationMock(1))
+ api_obj.update_storage_service = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_storage_service = MagicMock(return_value=OperationMock(3))
+ m_ss_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_ss.main()
+
+ # check api was called correctly
+ api_obj.get_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ api_obj.create_storage_service.assert_not_called()
+ api_obj.update_storage_service.assert_not_called()
+ api_obj.delete_storage_service.assert_called_once_with(
+ storage_service_name=module_args["name"]
+ )
+ op_obj.get_operation.assert_called_once_with(3)
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_tenant.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_tenant.py
new file mode 100644
index 000000000..bb0521b01
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_tenant.py
@@ -0,0 +1,803 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock, patch
+
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_tenant
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ FailedOperationMock,
+ OperationMock,
+ SuccessfulOperationMock,
+ exit_json,
+ fail_json,
+ set_module_args,
+)
+from urllib3.exceptions import HTTPError
+
+# GLOBAL MOCKS
+fusion_tenant.setup_fusion = MagicMock(return_value=purefusion.api_client.ApiClient())
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantsApi")
+@pytest.mark.parametrize(
+ "module_args",
+ [
+ # required parameter 'name` is missing
+ {
+ "state": "present",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # unknown parameter 'extra' is provided
+ {
+ "state": "present",
+ "name": "tenant1",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ "extra": "value",
+ },
+ # parameter 'state` has incorrect value
+ {
+ "state": "cool",
+ "name": "tenant1",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ ],
+)
+def test_module_fails_on_wrong_parameters(m_tenant_api, m_op_api, module_args):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_tenant = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant = MagicMock(return_value=OperationMock(3))
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+ m_tenant_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_tenant.main()
+
+ # check api was not called at all
+ api_obj.get_tenant.assert_not_called()
+ api_obj.create_tenant.assert_not_called()
+ api_obj.update_tenant.assert_not_called()
+ api_obj.delete_tenant.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantsApi")
+def test_tenant_create(m_tenant_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "tenant1",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_tenant = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant = MagicMock(return_value=OperationMock(3))
+ m_tenant_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_tenant.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ api_obj.create_tenant.assert_called_once_with(
+ purefusion.TenantPost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ )
+ )
+ api_obj.update_tenant.assert_not_called()
+ api_obj.delete_tenant.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantsApi")
+def test_tenant_create_without_display_name(m_tenant_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "tenant1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_tenant = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant = MagicMock(return_value=OperationMock(3))
+ m_tenant_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_tenant.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ api_obj.create_tenant.assert_called_once_with(
+ purefusion.TenantPost(
+ name=module_args["name"],
+ display_name=module_args["name"],
+ )
+ )
+ api_obj.update_tenant.assert_not_called()
+ api_obj.delete_tenant.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_tenant_create_exception(m_tenant_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "tenant1",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_tenant = MagicMock(side_effect=exec_original)
+ api_obj.update_tenant = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant = MagicMock(return_value=OperationMock(3))
+ m_tenant_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_tenant.main()
+
+ # check api was called correctly
+ api_obj.get_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ api_obj.create_tenant.assert_called_once_with(
+ purefusion.TenantPost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ )
+ )
+ api_obj.update_tenant.assert_not_called()
+ api_obj.delete_tenant.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantsApi")
+def test_tenant_create_op_fails(m_tenant_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "tenant1",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_tenant = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant = MagicMock(return_value=OperationMock(3))
+ m_tenant_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_tenant.main()
+
+ # check api was called correctly
+ api_obj.get_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ api_obj.create_tenant.assert_called_once_with(
+ purefusion.TenantPost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ )
+ )
+ api_obj.update_tenant.assert_not_called()
+ api_obj.delete_tenant.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_tenant_create_op_exception(m_tenant_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "tenant1",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_tenant = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant = MagicMock(return_value=OperationMock(3))
+ m_tenant_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_tenant.main()
+
+ # check api was called correctly
+ api_obj.get_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ api_obj.create_tenant.assert_called_once_with(
+ purefusion.TenantPost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ )
+ )
+ api_obj.update_tenant.assert_not_called()
+ api_obj.delete_tenant.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantsApi")
+def test_tenant_update(m_tenant_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "tenant1",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_tenant = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant = MagicMock(return_value=purefusion.Tenant(**current_tenant))
+ api_obj.create_tenant = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant = MagicMock(return_value=OperationMock(3))
+ m_tenant_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_tenant.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ api_obj.create_tenant.assert_not_called()
+ api_obj.update_tenant.assert_called_once_with(
+ purefusion.TenantPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ tenant_name=module_args["name"],
+ )
+ api_obj.delete_tenant.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_tenant_update_exception(m_tenant_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "tenant1",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_tenant = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant = MagicMock(return_value=purefusion.Tenant(**current_tenant))
+ api_obj.create_tenant = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant = MagicMock(side_effect=exec_original)
+ api_obj.delete_tenant = MagicMock(return_value=OperationMock(3))
+ m_tenant_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_tenant.main()
+
+ # check api was called correctly
+ api_obj.get_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ api_obj.create_tenant.assert_not_called()
+ api_obj.update_tenant.assert_called_once_with(
+ purefusion.TenantPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ tenant_name=module_args["name"],
+ )
+ api_obj.delete_tenant.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantsApi")
+def test_tenant_update_op_fails(m_tenant_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "tenant1",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_tenant = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant = MagicMock(return_value=purefusion.Tenant(**current_tenant))
+ api_obj.create_tenant = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant = MagicMock(return_value=OperationMock(3))
+ m_tenant_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_tenant.main()
+
+ # check api was called correctly
+ api_obj.get_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ api_obj.create_tenant.assert_not_called()
+ api_obj.update_tenant.assert_called_once_with(
+ purefusion.TenantPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ tenant_name=module_args["name"],
+ )
+ api_obj.delete_tenant.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_tenant_update_op_exception(m_tenant_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "tenant1",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_tenant = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant = MagicMock(return_value=purefusion.Tenant(**current_tenant))
+ api_obj.create_tenant = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant = MagicMock(return_value=OperationMock(3))
+ m_tenant_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_tenant.main()
+
+ # check api was called correctly
+ api_obj.get_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ api_obj.create_tenant.assert_not_called()
+ api_obj.update_tenant.assert_called_once_with(
+ purefusion.TenantPatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ tenant_name=module_args["name"],
+ )
+ api_obj.delete_tenant.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantsApi")
+def test_tenant_present_not_changed(m_tenant_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "tenant1",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_tenant = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": module_args["display_name"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant = MagicMock(return_value=purefusion.Tenant(**current_tenant))
+ api_obj.create_tenant = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant = MagicMock(return_value=OperationMock(3))
+ m_tenant_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_tenant.main()
+
+ assert not exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ api_obj.create_tenant.assert_not_called()
+ api_obj.update_tenant.assert_not_called()
+ api_obj.delete_tenant.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantsApi")
+def test_tenant_absent_not_changed(m_tenant_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "tenant1",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_tenant = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant = MagicMock(return_value=OperationMock(3))
+ m_tenant_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_tenant.main()
+
+ assert not exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ api_obj.create_tenant.assert_not_called()
+ api_obj.update_tenant.assert_not_called()
+ api_obj.delete_tenant.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantsApi")
+def test_tenant_delete(m_tenant_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "tenant1",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_tenant = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": "different", # display_name doesn't match but UPDATE shouldn't be called
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant = MagicMock(return_value=purefusion.Tenant(**current_tenant))
+ api_obj.create_tenant = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant = MagicMock(return_value=OperationMock(3))
+ m_tenant_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_tenant.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ api_obj.create_tenant.assert_not_called()
+ api_obj.update_tenant.assert_not_called()
+ api_obj.delete_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_tenant_delete_exception(m_tenant_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "absent",
+ "name": "tenant1",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_tenant = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": "different", # display_name doesn't match but UPDATE shouldn't be called
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant = MagicMock(return_value=purefusion.Tenant(**current_tenant))
+ api_obj.create_tenant = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant = MagicMock(side_effect=exec_original)
+ m_tenant_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_tenant.main()
+
+ # check api was called correctly
+ api_obj.get_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ api_obj.create_tenant.assert_not_called()
+ api_obj.update_tenant.assert_not_called()
+ api_obj.delete_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantsApi")
+def test_tenant_delete_op_fails(m_tenant_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "tenant1",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_tenant = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": "different", # display_name doesn't match but UPDATE shouldn't be called
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant = MagicMock(return_value=purefusion.Tenant(**current_tenant))
+ api_obj.create_tenant = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant = MagicMock(return_value=OperationMock(3))
+ m_tenant_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_tenant.main()
+
+ # check api was called correctly
+ api_obj.get_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ api_obj.create_tenant.assert_not_called()
+ api_obj.update_tenant.assert_not_called()
+ api_obj.delete_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantsApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_tenant_delete_op_exception(m_tenant_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "absent",
+ "name": "tenant1",
+ "display_name": "Tenant 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_tenant = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "display_name": "different", # display_name doesn't match but UPDATE shouldn't be called
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant = MagicMock(return_value=purefusion.Tenant(**current_tenant))
+ api_obj.create_tenant = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant = MagicMock(return_value=OperationMock(3))
+ m_tenant_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_tenant.main()
+
+ # check api was called correctly
+ api_obj.get_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ api_obj.create_tenant.assert_not_called()
+ api_obj.update_tenant.assert_not_called()
+ api_obj.delete_tenant.assert_called_once_with(tenant_name=module_args["name"])
+ op_obj.get_operation.assert_called_once_with(3)
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_ts.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_ts.py
new file mode 100644
index 000000000..0d9cbb25a
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_ts.py
@@ -0,0 +1,922 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Andrej Pajtas (apajtas@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock, patch
+
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_ts
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ FailedOperationMock,
+ OperationMock,
+ SuccessfulOperationMock,
+ exit_json,
+ fail_json,
+ set_module_args,
+)
+from urllib3.exceptions import HTTPError
+
+# GLOBAL MOCKS
+fusion_ts.setup_fusion = MagicMock(return_value=purefusion.api_client.ApiClient())
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantSpacesApi")
+@pytest.mark.parametrize(
+ "module_args",
+ [
+ # required parameter 'name` is missing
+ {
+ "state": "present",
+ "display_name": "Tenanct Space 1",
+ "tenant": "tenant1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # required tenant 'name` is missing
+ {
+ "state": "present",
+ "name": "tenantspace1",
+ "display_name": "Tenanct Space 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ # unknown parameter 'extra' is provided
+ {
+ "state": "present",
+ "name": "tenantspace1",
+ "display_name": "Tenanct Space 1",
+ "tenant": "tenant1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ "extra": "value",
+ },
+ # parameter 'state` has incorrect value
+ {
+ "state": "cool",
+ "name": "tenantspace1",
+ "display_name": "Tenanct Space 1",
+ "tenant": "tenant1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ },
+ ],
+)
+def test_module_fails_on_wrong_parameters(m_ts_api, m_op_api, module_args):
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant_space = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_tenant_space = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant_space = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant_space = MagicMock(return_value=OperationMock(3))
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+ m_ts_api.return_value = api_obj
+
+ # run module
+ with pytest.raises(AnsibleFailJson):
+ fusion_ts.main()
+
+ # check api was not called at all
+ api_obj.get_tenant_space.assert_not_called()
+ api_obj.create_tenant_space.assert_not_called()
+ api_obj.update_tenant_space.assert_not_called()
+ api_obj.delete_tenant_space.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantSpacesApi")
+def test_ts_create(m_ts_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "tenantspace1",
+ "display_name": "Tenanct Space 1",
+ "tenant": "tenant1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant_space = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_tenant_space = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant_space = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant_space = MagicMock(return_value=OperationMock(3))
+ m_ts_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_ts.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.create_tenant_space.assert_called_once_with(
+ purefusion.TenantSpacePost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ ),
+ tenant_name=module_args["tenant"],
+ )
+ api_obj.update_tenant_space.assert_not_called()
+ api_obj.delete_tenant_space.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantSpacesApi")
+def test_ts_create_without_display_name(m_ts_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "tenantspace1",
+ "tenant": "tenant1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant_space = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_tenant_space = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant_space = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant_space = MagicMock(return_value=OperationMock(3))
+ m_ts_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_ts.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.create_tenant_space.assert_called_once_with(
+ purefusion.TenantSpacePost(
+ name=module_args["name"],
+ display_name=module_args["name"],
+ ),
+ tenant_name=module_args["tenant"],
+ )
+ api_obj.update_tenant_space.assert_not_called()
+ api_obj.delete_tenant_space.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantSpacesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_ts_create_exception(m_ts_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "tenantspace1",
+ "display_name": "Tenanct Space 1",
+ "tenant": "tenant1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant_space = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_tenant_space = MagicMock(side_effect=exec_original)
+ api_obj.update_tenant_space = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant_space = MagicMock(return_value=OperationMock(3))
+ m_ts_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_ts.main()
+
+ # check api was called correctly
+ api_obj.get_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.create_tenant_space.assert_called_once_with(
+ purefusion.TenantSpacePost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ ),
+ tenant_name=module_args["tenant"],
+ )
+ api_obj.update_tenant_space.assert_not_called()
+ api_obj.delete_tenant_space.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantSpacesApi")
+def test_ts_create_op_fails(m_ts_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "tenantspace1",
+ "display_name": "Tenanct Space 1",
+ "tenant": "tenant1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant_space = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_tenant_space = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant_space = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant_space = MagicMock(return_value=OperationMock(3))
+ m_ts_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_ts.main()
+
+ # check api was called correctly
+ api_obj.get_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.create_tenant_space.assert_called_once_with(
+ purefusion.TenantSpacePost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ ),
+ tenant_name=module_args["tenant"],
+ )
+ api_obj.update_tenant_space.assert_not_called()
+ api_obj.delete_tenant_space.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantSpacesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_ts_create_op_exception(m_ts_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "tenantspace1",
+ "display_name": "Tenanct Space 1",
+ "tenant": "tenant1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant_space = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_tenant_space = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant_space = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant_space = MagicMock(return_value=OperationMock(3))
+ m_ts_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_ts.main()
+
+ # check api was called correctly
+ api_obj.get_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.create_tenant_space.assert_called_once_with(
+ purefusion.TenantSpacePost(
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ ),
+ tenant_name=module_args["tenant"],
+ )
+ api_obj.update_tenant_space.assert_not_called()
+ api_obj.delete_tenant_space.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantSpacesApi")
+def test_ts_update(m_ts_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "tenantspace1",
+ "display_name": "Tenanct Space 1",
+ "tenant": "tenant1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ts = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "tenant": "tenant1",
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant_space = MagicMock(
+ return_value=purefusion.TenantSpace(**current_ts)
+ )
+ api_obj.create_tenant_space = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant_space = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant_space = MagicMock(return_value=OperationMock(3))
+ m_ts_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_ts.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.create_tenant_space.assert_not_called()
+ api_obj.update_tenant_space.assert_called_once_with(
+ purefusion.TenantSpacePatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.delete_tenant_space.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantSpacesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_ts_update_exception(m_ts_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "tenantspace1",
+ "display_name": "Tenanct Space 1",
+ "tenant": "tenant1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ts = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "tenant": "tenant1",
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant_space = MagicMock(
+ return_value=purefusion.TenantSpace(**current_ts)
+ )
+ api_obj.create_tenant_space = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant_space = MagicMock(side_effect=exec_original)
+ api_obj.delete_tenant_space = MagicMock(return_value=OperationMock(3))
+ m_ts_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_ts.main()
+
+ # check api was called correctly
+ api_obj.get_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.create_tenant_space.assert_not_called()
+ api_obj.update_tenant_space.assert_called_once_with(
+ purefusion.TenantSpacePatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.delete_tenant_space.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantSpacesApi")
+def test_ts_update_op_fails(m_ts_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "tenantspace1",
+ "display_name": "Tenanct Space 1",
+ "tenant": "tenant1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ts = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "tenant": "tenant1",
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant_space = MagicMock(
+ return_value=purefusion.TenantSpace(**current_ts)
+ )
+ api_obj.create_tenant_space = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant_space = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant_space = MagicMock(return_value=OperationMock(3))
+ m_ts_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_ts.main()
+
+ # check api was called correctly
+ api_obj.get_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.create_tenant_space.assert_not_called()
+ api_obj.update_tenant_space.assert_called_once_with(
+ purefusion.TenantSpacePatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.delete_tenant_space.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantSpacesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_ts_update_op_exception(m_ts_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "present",
+ "name": "tenantspace1",
+ "display_name": "Tenanct Space 1",
+ "tenant": "tenant1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ts = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "tenant": "tenant1",
+ "display_name": None,
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant_space = MagicMock(
+ return_value=purefusion.TenantSpace(**current_ts)
+ )
+ api_obj.create_tenant_space = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant_space = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant_space = MagicMock(return_value=OperationMock(3))
+ m_ts_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_ts.main()
+
+ # check api was called correctly
+ api_obj.get_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.create_tenant_space.assert_not_called()
+ api_obj.update_tenant_space.assert_called_once_with(
+ purefusion.TenantSpacePatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.delete_tenant_space.assert_not_called()
+ op_obj.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantSpacesApi")
+def test_ts_present_not_changed(m_ts_api, m_op_api):
+ module_args = {
+ "state": "present",
+ "name": "tenantspace1",
+ "display_name": "Tenanct Space 1",
+ "tenant": "tenant1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ts = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "tenant": "tenant1",
+ "display_name": module_args["display_name"],
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant_space = MagicMock(
+ return_value=purefusion.TenantSpace(**current_ts)
+ )
+ api_obj.create_tenant_space = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant_space = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant_space = MagicMock(return_value=OperationMock(3))
+ m_ts_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_ts.main()
+
+ assert not exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.create_tenant_space.assert_not_called()
+ api_obj.update_tenant_space.assert_not_called()
+ api_obj.delete_tenant_space.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantSpacesApi")
+def test_ts_absent_not_changed(m_ts_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "tenantspace1",
+ "tenant": "tenant1",
+ "display_name": "Tenanct Space 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant_space = MagicMock(side_effect=purefusion.rest.ApiException)
+ api_obj.create_tenant_space = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant_space = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant_space = MagicMock(return_value=OperationMock(3))
+ m_ts_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_ts.main()
+
+ assert not exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.create_tenant_space.assert_not_called()
+ api_obj.update_tenant_space.assert_not_called()
+ api_obj.delete_tenant_space.assert_not_called()
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantSpacesApi")
+def test_ts_delete(m_ts_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "tenantspace1",
+ "tenant": "tenant1",
+ "display_name": "Tenanct Space 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ts = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "tenant": "tenant1",
+ "display_name": "different", # display_name doesn't match but UPDATE shouldn't be called
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant_space = MagicMock(
+ return_value=purefusion.TenantSpace(**current_ts)
+ )
+ api_obj.create_tenant_space = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant_space = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant_space = MagicMock(return_value=OperationMock(3))
+ m_ts_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(AnsibleExitJson) as exc:
+ fusion_ts.main()
+
+ assert exc.value.changed
+
+ # check api was called correctly
+ api_obj.get_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.create_tenant_space.assert_not_called()
+ api_obj.update_tenant_space.assert_not_called()
+ api_obj.delete_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantSpacesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_ts_delete_exception(m_ts_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "absent",
+ "name": "tenantspace1",
+ "tenant": "tenant1",
+ "display_name": "Tenanct Space 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ts = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "tenant": "tenant1",
+ "display_name": "different", # display_name doesn't match but UPDATE shouldn't be called
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant_space = MagicMock(
+ return_value=purefusion.TenantSpace(**current_ts)
+ )
+ api_obj.create_tenant_space = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant_space = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant_space = MagicMock(side_effect=exec_original)
+ m_ts_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_ts.main()
+
+ # check api was called correctly
+ api_obj.get_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.create_tenant_space.assert_not_called()
+ api_obj.update_tenant_space.assert_not_called()
+ api_obj.delete_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ op_obj.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantSpacesApi")
+def test_ts_delete_op_fails(m_ts_api, m_op_api):
+ module_args = {
+ "state": "absent",
+ "name": "tenantspace1",
+ "tenant": "tenant1",
+ "display_name": "Tenanct Space 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ts = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "tenant": "tenant1",
+ "display_name": "different", # display_name doesn't match but UPDATE shouldn't be called
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant_space = MagicMock(
+ return_value=purefusion.TenantSpace(**current_ts)
+ )
+ api_obj.create_tenant_space = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant_space = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant_space = MagicMock(return_value=OperationMock(3))
+ m_ts_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(return_value=FailedOperationMock)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(OperationException):
+ fusion_ts.main()
+
+ # check api was called correctly
+ api_obj.get_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.create_tenant_space.assert_not_called()
+ api_obj.update_tenant_space.assert_not_called()
+ api_obj.delete_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.TenantSpacesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_ts_delete_op_exception(m_ts_api, m_op_api, exec_original, exec_catch):
+ module_args = {
+ "state": "absent",
+ "name": "tenantspace1",
+ "tenant": "tenant1",
+ "display_name": "Tenanct Space 1",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+ current_ts = {
+ "id": 1,
+ "self_link": "self_link_value",
+ "name": module_args["name"], # name must match
+ "tenant": "tenant1",
+ "display_name": "different", # display_name doesn't match but UPDATE shouldn't be called
+ }
+ set_module_args(module_args)
+
+ # mock api responses
+ api_obj = MagicMock()
+ api_obj.get_tenant_space = MagicMock(
+ return_value=purefusion.TenantSpace(**current_ts)
+ )
+ api_obj.create_tenant_space = MagicMock(return_value=OperationMock(1))
+ api_obj.update_tenant_space = MagicMock(return_value=OperationMock(2))
+ api_obj.delete_tenant_space = MagicMock(return_value=OperationMock(3))
+ m_ts_api.return_value = api_obj
+
+ # mock operation results
+ op_obj = MagicMock()
+ op_obj.get_operation = MagicMock(side_effect=exec_original)
+ m_op_api.return_value = op_obj
+
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_ts.main()
+
+ # check api was called correctly
+ api_obj.get_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ api_obj.create_tenant_space.assert_not_called()
+ api_obj.update_tenant_space.assert_not_called()
+ api_obj.delete_tenant_space.assert_called_once_with(
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["name"],
+ )
+ op_obj.get_operation.assert_called_once_with(3)
diff --git a/ansible_collections/purestorage/fusion/tests/functional/test_fusion_volume.py b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_volume.py
new file mode 100644
index 000000000..592bda32e
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/test_fusion_volume.py
@@ -0,0 +1,715 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Dmitriy Li (dmli@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock, patch
+
+import fusion as purefusion
+import pytest
+from ansible.module_utils import basic
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+from ansible_collections.purestorage.fusion.plugins.modules import (
+ fusion_volume,
+)
+from ansible_collections.purestorage.fusion.tests.functional.utils import (
+ AnsibleExitJson,
+ AnsibleFailJson,
+ OperationMock,
+ SuccessfulOperationMock,
+ exit_json,
+ fail_json,
+ set_module_args,
+)
+from urllib3.exceptions import HTTPError
+
+# GLOBAL MOCKS
+fusion_volume.setup_fusion = MagicMock(return_value=purefusion.api_client.ApiClient())
+purefusion.api_client.ApiClient.call_api = MagicMock(
+ side_effect=Exception("API call not mocked!")
+)
+basic.AnsibleModule.exit_json = exit_json
+basic.AnsibleModule.fail_json = fail_json
+
+
+@pytest.fixture
+def module_args():
+ return {
+ "name": "volume_1",
+ "state": "present",
+ "display_name": "Volume 1",
+ "tenant": "t1",
+ "tenant_space": "ts1",
+ "placement_group": "pg1",
+ "storage_class": "sc1",
+ "protection_policy": "pp1",
+ "host_access_policies": ["hap1"],
+ "eradicate": False,
+ "size": "1M",
+ "issuer_id": "ABCD1234",
+ "private_key_file": "private-key.pem",
+ }
+
+
+@pytest.fixture
+def absent_module_args(module_args):
+ module_args.update(
+ {"host_access_policies": [], "eradicate": True, "state": "absent"}
+ )
+ return module_args
+
+
+@pytest.fixture
+def volume():
+ return {
+ "name": "volume_1",
+ "display_name": "Volume 1",
+ "tenant": "t1",
+ "tenant_space": "ts1",
+ "storage_class": purefusion.StorageClassRef(
+ name="sc1", id="id_1", kind="storage_class", self_link="self_link"
+ ),
+ "placement_group": purefusion.PlacementGroupRef(
+ name="pg1", id="id_1", kind="placement_group", self_link="self_link"
+ ),
+ "protection_policy": purefusion.ProtectionPolicyRef(
+ name="pp1", id="id_1", kind="protection_policy", self_link="self_link"
+ ),
+ "host_access_policies": [
+ purefusion.HostAccessPolicyRef(
+ name="hap1", id="id_1", kind="host_access_policy", self_link="self_link"
+ )
+ ],
+ "serial_number": "sn1",
+ "destroyed": False,
+ "size": 1048576,
+ "id": "id_1",
+ "self_link": "self_link",
+ }
+
+
+@pytest.fixture
+def destroyed_volume(volume):
+ volume.update({"host_access_policies": [], "destroyed": True})
+ return volume
+
+
+@patch("fusion.VolumesApi")
+@pytest.mark.parametrize(
+ "field,expected_exception_regex",
+ [
+ (
+ "name",
+ "missing required arguments: name",
+ ),
+ (
+ "tenant",
+ "missing required arguments: tenant",
+ ),
+ (
+ "tenant_space",
+ "missing required arguments: tenant_space",
+ ),
+ (
+ "storage_class",
+ "missing parameter\\(s\\) required by 'placement_group': storage_class",
+ ),
+ (
+ "placement_group",
+ "missing required arguments: placement_group",
+ ),
+ (
+ "size",
+ "missing required arguments: size",
+ ),
+ ],
+)
+def test_module_fails_on_missing_parameters(
+ mock_volumes_api, field, expected_exception_regex, module_args
+):
+ volumes_api = purefusion.VolumesApi()
+ volumes_api.get_volume = MagicMock(side_effect=purefusion.rest.ApiException)
+ mock_volumes_api.return_value = volumes_api
+ del module_args[field]
+ set_module_args(module_args)
+ # run module
+ with pytest.raises(AnsibleFailJson) as ansible_error:
+ fusion_volume.main()
+ assert ansible_error.match(expected_exception_regex)
+
+
+@patch("fusion.VolumesApi")
+@pytest.mark.parametrize(
+ "dict_update,expected_exception_regex",
+ [
+ (
+ {"extra": "value"},
+ "Unsupported parameters for.*module: extra",
+ ),
+ (
+ {"state": "absent"},
+ "Volume must have no host access policies when destroyed",
+ ),
+ (
+ {"eradicate": True},
+ "'eradicate: true' cannot be used together with 'state: present'",
+ ),
+ (
+ {"size": "1K"},
+ "Size is not within the required range",
+ ),
+ ],
+)
+def test_module_fails_on_incorrect_parameters(
+ mock_volumes_api, dict_update, expected_exception_regex, module_args
+):
+ volumes_api = purefusion.VolumesApi()
+ volumes_api.get_volume = MagicMock(side_effect=purefusion.rest.ApiException)
+ mock_volumes_api.return_value = volumes_api
+ module_args.update(dict_update)
+ set_module_args(module_args)
+ # run module
+ with pytest.raises(AnsibleFailJson) as ansible_error:
+ fusion_volume.main()
+ assert ansible_error.match(expected_exception_regex)
+
+
+@patch("fusion.VolumesApi")
+def test_module_not_existent_volume_with_state_absent_not_changed(
+ mock_volumes_api, module_args
+):
+ volumes_api = purefusion.VolumesApi()
+ volumes_api.get_volume = MagicMock(side_effect=purefusion.rest.ApiException)
+ mock_volumes_api.return_value = volumes_api
+ del module_args["host_access_policies"]
+ module_args["state"] = "absent"
+ set_module_args(module_args)
+ # run module
+ with pytest.raises(AnsibleExitJson) as exception:
+ fusion_volume.main()
+ assert exception.value.changed is False
+ volumes_api.get_volume.assert_called_once_with(
+ volume_name=module_args["name"],
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["tenant_space"],
+ )
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.VolumesApi")
+def test_volume_create_successfully(mock_volumes_api, mock_operations_api, module_args):
+ operations_api = purefusion.OperationsApi()
+ volumes_api = purefusion.VolumesApi()
+ volumes_api.get_volume = MagicMock(side_effect=purefusion.rest.ApiException)
+ volumes_api.create_volume = MagicMock(return_value=OperationMock(1))
+ operations_api.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ mock_volumes_api.return_value = volumes_api
+ mock_operations_api.return_value = operations_api
+ set_module_args(module_args)
+ # run module
+ with pytest.raises(AnsibleExitJson) as exception:
+ fusion_volume.main()
+ assert exception.value.changed is True
+ volumes_api.get_volume.assert_called_with(
+ volume_name=module_args["name"],
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["tenant_space"],
+ )
+ volumes_api.create_volume.assert_called_once_with(
+ purefusion.VolumePost(
+ size=1048576,
+ storage_class=module_args["storage_class"],
+ placement_group=module_args["placement_group"],
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ protection_policy=module_args["protection_policy"],
+ ),
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["tenant_space"],
+ )
+ operations_api.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.VolumesApi")
+def test_volume_create_without_display_name_successfully(
+ mock_volumes_api, mock_operations_api, module_args
+):
+ del module_args["display_name"]
+ operations_api = purefusion.OperationsApi()
+ volumes_api = purefusion.VolumesApi()
+ volumes_api.get_volume = MagicMock(side_effect=purefusion.rest.ApiException)
+ volumes_api.create_volume = MagicMock(return_value=OperationMock(1))
+ operations_api.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ mock_volumes_api.return_value = volumes_api
+ mock_operations_api.return_value = operations_api
+ set_module_args(module_args)
+ # run module
+ with pytest.raises(AnsibleExitJson) as exception:
+ fusion_volume.main()
+ assert exception.value.changed is True
+ volumes_api.get_volume.assert_called_with(
+ volume_name=module_args["name"],
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["tenant_space"],
+ )
+ volumes_api.create_volume.assert_called_with(
+ purefusion.VolumePost(
+ size=1048576,
+ storage_class=module_args["storage_class"],
+ placement_group=module_args["placement_group"],
+ name=module_args["name"],
+ display_name=module_args["name"],
+ protection_policy=module_args["protection_policy"],
+ ),
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["tenant_space"],
+ )
+ operations_api.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.VolumesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_volume_create_throws_exception(
+ mock_volumes_api, mock_operations_api, exec_original, exec_catch, module_args
+):
+ operations_api = purefusion.OperationsApi()
+ volumes_api = purefusion.VolumesApi()
+ volumes_api.get_volume = MagicMock(side_effect=purefusion.rest.ApiException)
+ volumes_api.create_volume = MagicMock(side_effect=exec_original)
+ operations_api.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ mock_volumes_api.return_value = volumes_api
+ mock_operations_api.return_value = operations_api
+ set_module_args(module_args)
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_volume.main()
+ volumes_api.get_volume.assert_called_with(
+ volume_name=module_args["name"],
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["tenant_space"],
+ )
+ volumes_api.create_volume.assert_called_once_with(
+ purefusion.VolumePost(
+ size=1048576,
+ storage_class=module_args["storage_class"],
+ placement_group=module_args["placement_group"],
+ name=module_args["name"],
+ display_name=module_args["display_name"],
+ protection_policy=module_args["protection_policy"],
+ ),
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["tenant_space"],
+ )
+ operations_api.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.VolumesApi")
+@pytest.mark.parametrize(
+ "updated_volume,called_with",
+ [
+ (
+ {"destroyed": True},
+ purefusion.VolumePatch(destroyed=purefusion.NullableBoolean(False)),
+ ),
+ (
+ {"size": 1000000},
+ purefusion.VolumePatch(size=purefusion.NullableSize(1048576)),
+ ),
+ (
+ {
+ "protection_policy": purefusion.ProtectionPolicyRef(
+ name="pp2",
+ id="id_1",
+ kind="protection_policy",
+ self_link="self_link",
+ )
+ },
+ purefusion.VolumePatch(protection_policy=purefusion.NullableString("pp1")),
+ ),
+ (
+ {"display_name": "Volume"},
+ purefusion.VolumePatch(display_name=purefusion.NullableString("Volume 1")),
+ ),
+ (
+ {
+ "storage_class": purefusion.StorageClassRef(
+ name="sc2", id="id_1", kind="storage_class", self_link="self_link"
+ )
+ },
+ purefusion.VolumePatch(storage_class=purefusion.NullableString("sc1")),
+ ),
+ (
+ {
+ "placement_group": purefusion.PlacementGroupRef(
+ name="pg2", id="id_1", kind="placement_group", self_link="self_link"
+ )
+ },
+ purefusion.VolumePatch(placement_group=purefusion.NullableString("pg1")),
+ ),
+ (
+ {
+ "host_access_policies": [
+ purefusion.HostAccessPolicyRef(
+ name="hap2",
+ id="id_1",
+ kind="host_access_policy",
+ self_link="self_link",
+ )
+ ]
+ },
+ purefusion.VolumePatch(
+ host_access_policies=purefusion.NullableString("hap1")
+ ),
+ ),
+ ],
+)
+def test_volume_update_with_state_present_executed_correctly(
+ mock_volumes_api,
+ mock_operations_api,
+ updated_volume,
+ called_with,
+ module_args,
+ volume,
+):
+ volume.update(updated_volume)
+ operations_api = purefusion.OperationsApi()
+ volumes_api = purefusion.VolumesApi()
+ volumes_api.get_volume = MagicMock(return_value=purefusion.Volume(**volume))
+ volumes_api.update_volume = MagicMock(return_value=OperationMock(1))
+ operations_api.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ mock_volumes_api.return_value = volumes_api
+ mock_operations_api.return_value = operations_api
+ set_module_args(module_args)
+ # run module
+ with pytest.raises(AnsibleExitJson) as exception:
+ fusion_volume.main()
+ assert exception.value.changed is True
+ volumes_api.get_volume.assert_called_with(
+ volume_name=module_args["name"],
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["tenant_space"],
+ )
+ volumes_api.update_volume.assert_called_once_with(
+ called_with,
+ volume_name=module_args["name"],
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["tenant_space"],
+ )
+ operations_api.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.VolumesApi")
+@pytest.mark.parametrize(
+ "updated_volume,called_with",
+ [
+ (
+ {"destroyed": False, "host_access_policies": []},
+ purefusion.VolumePatch(destroyed=purefusion.NullableBoolean(True)),
+ )
+ ],
+)
+def test_volume_update_with_state_absent_executed_correctly(
+ mock_volumes_api,
+ mock_operations_api,
+ updated_volume,
+ called_with,
+ module_args,
+ volume,
+):
+ module_args["state"] = "absent"
+ del module_args["host_access_policies"]
+ volume.update(updated_volume)
+ operations_api = purefusion.OperationsApi()
+ volumes_api = purefusion.VolumesApi()
+ volumes_api.get_volume = MagicMock(return_value=purefusion.Volume(**volume))
+ volumes_api.update_volume = MagicMock(return_value=OperationMock(1))
+ operations_api.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ mock_volumes_api.return_value = volumes_api
+ mock_operations_api.return_value = operations_api
+ set_module_args(module_args)
+ # run module
+ with pytest.raises(AnsibleExitJson) as exception:
+ fusion_volume.main()
+ assert exception.value.changed is True
+ volumes_api.get_volume.assert_called_with(
+ volume_name=module_args["name"],
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["tenant_space"],
+ )
+ volumes_api.update_volume.assert_called_once_with(
+ called_with,
+ volume_name=module_args["name"],
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["tenant_space"],
+ )
+ operations_api.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.VolumesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_volume_update_throws_exception(
+ mock_volumes_api,
+ mock_operations_api,
+ exec_original,
+ exec_catch,
+ module_args,
+ volume,
+):
+ module_args["display_name"] = "volume"
+ operations_api = purefusion.OperationsApi()
+ volumes_api = purefusion.VolumesApi()
+ volumes_api.get_volume = MagicMock(return_value=purefusion.Volume(**volume))
+ volumes_api.update_volume = MagicMock(side_effect=exec_original)
+ operations_api.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ mock_volumes_api.return_value = volumes_api
+ mock_operations_api.return_value = operations_api
+ set_module_args(module_args)
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_volume.main()
+
+ volumes_api.get_volume.assert_called_with(
+ volume_name=module_args["name"],
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["tenant_space"],
+ )
+ volumes_api.update_volume.assert_called_once_with(
+ purefusion.VolumePatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ volume_name=module_args["name"],
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["tenant_space"],
+ )
+ operations_api.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.VolumesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_volume_update_operation_throws_exception(
+ mock_volumes_api,
+ mock_operations_api,
+ exec_original,
+ exec_catch,
+ module_args,
+ volume,
+):
+ module_args["display_name"] = "volume"
+ operations_api = purefusion.OperationsApi()
+ volumes_api = purefusion.VolumesApi()
+ volumes_api.get_volume = MagicMock(return_value=purefusion.Volume(**volume))
+ volumes_api.update_volume = MagicMock(return_value=OperationMock(1))
+ operations_api.get_operation = MagicMock(side_effect=exec_original)
+ mock_volumes_api.return_value = volumes_api
+ mock_operations_api.return_value = operations_api
+ set_module_args(module_args)
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_volume.main()
+
+ volumes_api.get_volume.assert_called_with(
+ volume_name=module_args["name"],
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["tenant_space"],
+ )
+ volumes_api.update_volume.assert_called_once_with(
+ purefusion.VolumePatch(
+ display_name=purefusion.NullableString(module_args["display_name"])
+ ),
+ volume_name=module_args["name"],
+ tenant_name=module_args["tenant"],
+ tenant_space_name=module_args["tenant_space"],
+ )
+ operations_api.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.VolumesApi")
+def test_volume_delete_throws_validation_error(
+ mock_volumes_api, mock_operations_api, absent_module_args, volume
+):
+ volume["host_access_policies"] = []
+ operations_api = purefusion.OperationsApi()
+ volumes_api = purefusion.VolumesApi()
+ volumes_api.get_volume = MagicMock(return_value=purefusion.Volume(**volume))
+ volumes_api.update_volume = MagicMock(return_value=OperationMock(1))
+ volumes_api.delete_volume = MagicMock(return_value=OperationMock(2))
+
+ operations_api.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ mock_volumes_api.return_value = volumes_api
+ mock_operations_api.return_value = operations_api
+ set_module_args(absent_module_args)
+ # run module
+ with pytest.raises(AnsibleFailJson) as ansible_fail:
+ fusion_volume.main()
+ assert ansible_fail.match(regexp="BUG: inconsistent state, eradicate_volume")
+ volumes_api.get_volume.assert_called_with(
+ volume_name=absent_module_args["name"],
+ tenant_name=absent_module_args["tenant"],
+ tenant_space_name=absent_module_args["tenant_space"],
+ )
+ volumes_api.update_volume.assert_called_once_with(
+ purefusion.VolumePatch(destroyed=purefusion.NullableBoolean(True)),
+ volume_name=absent_module_args["name"],
+ tenant_name=absent_module_args["tenant"],
+ tenant_space_name=absent_module_args["tenant_space"],
+ )
+ volumes_api.delete_volume.assert_not_called()
+ operations_api.get_operation.assert_called_once_with(1)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.VolumesApi")
+def test_volume_delete_executed_correctly(
+ mock_volumes_api, mock_operations_api, absent_module_args, destroyed_volume
+):
+ operations_api = purefusion.OperationsApi()
+ volumes_api = purefusion.VolumesApi()
+ volumes_api.get_volume = MagicMock(
+ return_value=purefusion.Volume(**destroyed_volume)
+ )
+ volumes_api.update_volume = MagicMock(return_value=OperationMock(1))
+ volumes_api.delete_volume = MagicMock(return_value=OperationMock(2))
+
+ operations_api.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ mock_volumes_api.return_value = volumes_api
+ mock_operations_api.return_value = operations_api
+ set_module_args(absent_module_args)
+ # run module
+ with pytest.raises(AnsibleExitJson):
+ fusion_volume.main()
+ volumes_api.get_volume.assert_called_with(
+ volume_name=absent_module_args["name"],
+ tenant_name=absent_module_args["tenant"],
+ tenant_space_name=absent_module_args["tenant_space"],
+ )
+ volumes_api.update_volume.assert_not_called()
+ volumes_api.delete_volume.assert_called_once_with(
+ volume_name=absent_module_args["name"],
+ tenant_name=absent_module_args["tenant"],
+ tenant_space_name=absent_module_args["tenant_space"],
+ )
+ operations_api.get_operation.assert_called_once_with(2)
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.VolumesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, HTTPError),
+ ],
+)
+def test_volume_delete_throws_exception(
+ mock_volumes_api,
+ mock_operations_api,
+ exec_original,
+ exec_catch,
+ absent_module_args,
+ destroyed_volume,
+):
+ operations_api = purefusion.OperationsApi()
+ volumes_api = purefusion.VolumesApi()
+ volumes_api.get_volume = MagicMock(
+ return_value=purefusion.Volume(**destroyed_volume)
+ )
+ volumes_api.update_volume = MagicMock(return_value=OperationMock(1))
+ volumes_api.delete_volume = MagicMock(side_effect=exec_original)
+
+ operations_api.get_operation = MagicMock(return_value=SuccessfulOperationMock)
+ mock_volumes_api.return_value = volumes_api
+ mock_operations_api.return_value = operations_api
+ set_module_args(absent_module_args)
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_volume.main()
+ volumes_api.get_volume.assert_called_with(
+ volume_name=absent_module_args["name"],
+ tenant_name=absent_module_args["tenant"],
+ tenant_space_name=absent_module_args["tenant_space"],
+ )
+ volumes_api.update_volume.assert_not_called()
+ volumes_api.delete_volume.assert_called_once_with(
+ volume_name=absent_module_args["name"],
+ tenant_name=absent_module_args["tenant"],
+ tenant_space_name=absent_module_args["tenant_space"],
+ )
+
+ operations_api.get_operation.assert_not_called()
+
+
+@patch("fusion.OperationsApi")
+@patch("fusion.VolumesApi")
+@pytest.mark.parametrize(
+ "exec_original,exec_catch",
+ [
+ (purefusion.rest.ApiException, purefusion.rest.ApiException),
+ (HTTPError, OperationException),
+ ],
+)
+def test_volume_delete_operation_throws_exception(
+ mock_volumes_api,
+ mock_operations_api,
+ exec_original,
+ exec_catch,
+ absent_module_args,
+ destroyed_volume,
+):
+ operations_api = purefusion.OperationsApi()
+ volumes_api = purefusion.VolumesApi()
+ volumes_api.get_volume = MagicMock(
+ return_value=purefusion.Volume(**destroyed_volume)
+ )
+ volumes_api.update_volume = MagicMock(return_value=OperationMock(1))
+ volumes_api.delete_volume = MagicMock(return_value=OperationMock(2))
+
+ operations_api.get_operation = MagicMock(side_effect=exec_original)
+ mock_volumes_api.return_value = volumes_api
+ mock_operations_api.return_value = operations_api
+ set_module_args(absent_module_args)
+ # run module
+ with pytest.raises(exec_catch):
+ fusion_volume.main()
+ volumes_api.get_volume.assert_called_with(
+ volume_name=absent_module_args["name"],
+ tenant_name=absent_module_args["tenant"],
+ tenant_space_name=absent_module_args["tenant_space"],
+ )
+ volumes_api.update_volume.assert_not_called()
+ volumes_api.delete_volume.assert_called_once_with(
+ volume_name=absent_module_args["name"],
+ tenant_name=absent_module_args["tenant"],
+ tenant_space_name=absent_module_args["tenant_space"],
+ )
+ operations_api.get_operation.assert_called_once_with(2)
diff --git a/ansible_collections/purestorage/fusion/tests/functional/utils.py b/ansible_collections/purestorage/fusion/tests/functional/utils.py
new file mode 100644
index 000000000..24d6f0328
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/functional/utils.py
@@ -0,0 +1,116 @@
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+import json
+from dataclasses import dataclass
+
+from ansible.module_utils import basic
+from ansible.module_utils.common.text.converters import to_bytes
+
+
+@dataclass
+class OperationMock:
+ """
+ Mock Operation object. This object should be returned by mocked api.
+ """
+
+ def __init__(self, id=None, success=None):
+ if success is None:
+ self.status = "Pending"
+ elif success:
+ self.status = "Succeeded"
+ else:
+ self.status = "Failed"
+ self.id = id
+
+
+class SuccessfulOperationMock:
+ """
+ Mock object for successful operation. This object is returned by mocked Operation API if the operation was successful.
+ """
+
+ status = "Succeeded"
+
+
+class FailedOperationMock:
+ """
+ Mock object for failed operation. This object is returned by mocked Operation API if the operation failed.
+ """
+
+ status = "Failed"
+
+
+def set_module_args(args):
+ """
+ Prepare arguments so that they will be picked up during module creation.
+ Docs: https://docs.ansible.com/ansible/latest/dev_guide/testing_units_modules.html
+ """
+
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args)
+
+
+class AnsibleExitJson(Exception):
+ """
+ Exception class to be raised by module.exit_json and caught by the test case
+ Docs: https://docs.ansible.com/ansible/latest/dev_guide/testing_units_modules.html
+ """
+
+ def __init__(self, kwargs):
+ self.kwargs = kwargs
+
+ @property
+ def changed(self):
+ return self.kwargs["changed"]
+
+ @property
+ def fusion_info(self):
+ return self.kwargs["fusion_info"] if "fusion_info" in self.kwargs else None
+
+
+class AnsibleFailJson(Exception):
+ """
+ Exception class to be raised by module.fail_json and caught by the test case
+ Docs: https://docs.ansible.com/ansible/latest/dev_guide/testing_units_modules.html
+ """
+
+ def __init__(self, msg, kwargs):
+ super().__init__(msg)
+ self.kwargs = kwargs
+
+
+def exit_json(self, **kwargs):
+ """
+ Function to patch over exit_json; package return data into an exception
+ Docs: https://docs.ansible.com/ansible/latest/dev_guide/testing_units_modules.html
+ """
+
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(self, msg, **kwargs):
+ """
+ Function to patch over fail_json; package return data into an exception
+ Docs: https://docs.ansible.com/ansible/latest/dev_guide/testing_units_modules.html
+ """
+ kwargs["failed"] = True
+ raise AnsibleFailJson(msg, kwargs)
+
+
+def side_effects_with_exceptions(side_effects):
+ """
+ Assumes side_effects is a list. Works similarly to `MagicMock(side_effect=side_effects)`,
+ but if item in the list is instance of an exception, it raises it instead of returning it.
+ """
+ side_effects = side_effects.copy()
+
+ def _pop_side_effect(*args, **kwargs):
+ i = side_effects.pop(0)
+ if isinstance(i, Exception):
+ raise i
+ return i
+
+ return _pop_side_effect
diff --git a/ansible_collections/purestorage/fusion/tests/helpers.py b/ansible_collections/purestorage/fusion/tests/helpers.py
new file mode 100644
index 000000000..40d98cf0e
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/helpers.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Denys Denysyev (ddenysyev@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+import fusion as purefusion
+
+from http import HTTPStatus
+
+
+class ApiExceptionsMockGenerator:
+ @staticmethod
+ def create_permission_denied():
+ status = HTTPStatus.FORBIDDEN
+ return purefusion.rest.ApiException(status=status, reason=status.phrase)
+
+ @staticmethod
+ def create_conflict():
+ status = HTTPStatus.CONFLICT
+ return purefusion.rest.ApiException(status=status, reason=status.phrase)
+
+ @staticmethod
+ def create_not_found():
+ status = HTTPStatus.NOT_FOUND
+ return purefusion.rest.ApiException(status=status, reason=status.phrase)
diff --git a/ansible_collections/purestorage/fusion/tests/integration/README.md b/ansible_collections/purestorage/fusion/tests/integration/README.md
new file mode 100644
index 000000000..7ca8ee497
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/integration/README.md
@@ -0,0 +1,10 @@
+# Integration tests
+
+## Running tests
+
+- Copy `tests/integration/integration_config.template` to `tests/integration/integration_config.yml` and fill out the
+ env variables
+- Run with the following command:
+ ```bash
+ ansible-test integration
+ ```
diff --git a/ansible_collections/purestorage/fusion/tests/integration/integration_config.template b/ansible_collections/purestorage/fusion/tests/integration/integration_config.template
new file mode 100644
index 000000000..3fd6e77d3
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/integration/integration_config.template
@@ -0,0 +1,6 @@
+---
+test_env:
+ FUSION_API_HOST: <FUSION_API_HOST>
+ FUSION_ISSUER_ID: <API_ID>
+ FUSION_PRIVATE_KEY_FILE: <PRIVATE_KEY>
+ FUSION_TOKEN_ENDPOINT: <STAGING_TOKEN_ENDPOINT> \ No newline at end of file
diff --git a/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_az/tasks/main.yml b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_az/tasks/main.yml
new file mode 100644
index 000000000..b9c23fc8c
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_az/tasks/main.yml
@@ -0,0 +1,43 @@
+- name: Create Availability Zone
+ environment: "{{ test_env }}"
+ purestorage.fusion.fusion_az:
+ name: "test_az"
+ display_name: "foo AZ"
+ region: pure-us-west
+ register: result
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect Availability Zones and verify the zone exists
+ environment: "{{ test_env }}"
+ purestorage.fusion.fusion_info:
+ gather_subset: availability_zones
+ register: fusion_info
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'test_az' in fusion_info['fusion_info']['availability_zones']"
+
+- name: Delete AZ
+ purestorage.fusion.fusion_az:
+ name: "test_az"
+ state: absent
+ region: pure-us-west
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect Availability Zones and verify the zone does not exist
+ environment: "{{ test_env }}"
+ purestorage.fusion.fusion_info:
+ gather_subset: availability_zones
+ register: fusion_info
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'test_az' not in fusion_info['fusion_info']['availability_zones']"
diff --git a/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_hap/tasks/main.yml b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_hap/tasks/main.yml
new file mode 100644
index 000000000..eaea92684
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_hap/tasks/main.yml
@@ -0,0 +1,42 @@
+- name: Create new Linux host access policy
+ purestorage.fusion.fusion_hap:
+ name: hap_foo
+ personality: linux
+ iqn: "iqn.2005-03.com.RedHat:linux-host1"
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect hosts and check the host exists
+ purestorage.fusion.fusion_info:
+ gather_subset: host_access_policies
+ register: fusion_info
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'hap_foo' in fusion_info['fusion_info']['host_access_policies']"
+
+- name: Delete host access policy
+ purestorage.fusion.fusion_hap:
+ name: hap_foo
+ state: absent
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect hosts and check the host does not exist
+ purestorage.fusion.fusion_info:
+ gather_subset: host_access_policies
+ register: fusion_info
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'hap_foo' not in fusion_info['fusion_info']['host_access_policies']"
diff --git a/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_ni/tasks/main.yml b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_ni/tasks/main.yml
new file mode 100644
index 000000000..16b5359de
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_ni/tasks/main.yml
@@ -0,0 +1,37 @@
+# TODO: Currently not possible to remove network interface group from network interface
+
+# Prepare
+- name: Create new network interface group foo in AZ bar
+ purestorage.fusion.fusion_nig:
+ name: "interface_group1"
+ availability_zone: az1
+ region: pure-us-west
+ state: present
+ mtu: 1500
+ gateway: 172.17.1.1
+ prefix: 172.17.1.0/24
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+
+# Test network interfaces
+
+- name: Patch the network interface
+ purestorage.fusion.fusion_ni:
+ name: ct0.eth4
+ region: pure-us-west
+ availability_zone: az1
+ array: doubleagent-2
+ eth: 172.17.1.2/24
+ enabled: true
+ network_interface_group: "interface_group1"
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
diff --git a/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_nig/tasks/main.yml b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_nig/tasks/main.yml
new file mode 100644
index 000000000..8e3ab298b
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_nig/tasks/main.yml
@@ -0,0 +1,48 @@
+- name: Create new network interface group
+ purestorage.fusion.fusion_nig:
+ name: "foo_group"
+ availability_zone: az1
+ region: pure-us-west
+ state: present
+ mtu: 1500
+ gateway: 172.17.17.1
+ prefix: 172.17.17.0/24
+ environment: "{{ test_env }}"
+ register: result
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect network_interface_groups and check the group exist
+ purestorage.fusion.fusion_info:
+ gather_subset: network_interface_groups
+ register: fusion_info
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'pure-us-west/az1/foo_group' in fusion_info['fusion_info']['network_interface_groups']"
+
+- name: Delete network interface group
+ purestorage.fusion.fusion_nig:
+ name: "foo_group"
+ availability_zone: az1
+ region: pure-us-west
+ state: absent
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect network_interface_groups and check the group does not exist
+ purestorage.fusion.fusion_info:
+ gather_subset: network_interface_groups
+ register: fusion_info
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'pure-us-west/az1/foo_group' not in fusion_info['fusion_info']['network_interface_groups']"
diff --git a/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_pg/tasks/main.yml b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_pg/tasks/main.yml
new file mode 100644
index 000000000..f4d50a653
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_pg/tasks/main.yml
@@ -0,0 +1,95 @@
+# Prepare dependencies
+- name: Create new tenat foo_tenant
+ purestorage.fusion.fusion_tenant:
+ name: foo_tenant
+ display_name: "tenant foo"
+ environment: "{{ test_env }}"
+
+- name: Create new tenant space foo_tenant_space for tenant_foo
+ purestorage.fusion.fusion_ts:
+ name: foo_tenant_space
+ tenant: foo_tenant
+ state: present
+ environment: "{{ test_env }}"
+
+- name: Create new storage service foo_service
+ purestorage.fusion.fusion_ss:
+ name: foo_service
+ hardware_types:
+ - flash-array-x
+ display_name: "test class"
+ environment: "{{ test_env }}"
+
+
+# Test placement groups
+
+- name: Create new placement group named foo_pg
+ purestorage.fusion.fusion_pg:
+ name: foo_pg
+ tenant: foo_tenant
+ tenant_space: foo_tenant_space
+ availability_zone: az1
+ region: pure-us-west
+ storage_service: foo_service
+ state: present
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect placement_groups and check the placement group exists
+ purestorage.fusion.fusion_info:
+ gather_subset: placement_groups
+ register: fusion_info
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'foo_tenant/foo_tenant_space/foo_pg' in fusion_info['fusion_info']['placement_groups']"
+
+- name: Delete placement group foo
+ purestorage.fusion.fusion_pg:
+ name: foo_pg
+ tenant: foo_tenant
+ tenant_space: foo_tenant_space
+ state: absent
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect placement_groups and check the placement group does not exist
+ purestorage.fusion.fusion_info:
+ gather_subset: placement_groups
+ register: fusion_info
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'foo_tenant/foo_tenant_space/foo_pg' not in fusion_info['fusion_info']['placement_groups']"
+
+
+# Teardown dependencies
+
+- name: Delete storage service foo_service
+ purestorage.fusion.fusion_ss:
+ name: foo_service
+ state: absent
+ environment: "{{ test_env }}"
+
+- name: Delete foo_tenant_space
+ purestorage.fusion.fusion_ts:
+ name: foo_tenant_space
+ tenant: foo_tenant
+ state: absent
+ environment: "{{ test_env }}"
+
+- name: Delete tenat foo
+ purestorage.fusion.fusion_tenant:
+ name: foo_tenant
+ state: absent
+ environment: "{{ test_env }}"
diff --git a/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_pp/tasks/main.yml b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_pp/tasks/main.yml
new file mode 100644
index 000000000..f8a126662
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_pp/tasks/main.yml
@@ -0,0 +1,43 @@
+- name: Create new protection policy foo_pp
+ purestorage.fusion.fusion_pp:
+ name: foo_pp
+ local_rpo: 10
+ local_retention: 4d
+ display_name: "foo pp"
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect protection policies and check the policy exists
+ purestorage.fusion.fusion_info:
+ gather_subset: protection_policies
+ register: fusion_info
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'foo_pp' in fusion_info['fusion_info']['protection_policies']"
+
+- name: Delete protection policy foo_pp
+ purestorage.fusion.fusion_pp:
+ name: foo_pp
+ state: absent
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect protection policies and check the policy does not exist
+ purestorage.fusion.fusion_info:
+ gather_subset: protection_policies
+ register: fusion_info
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'foo_pp' not in fusion_info['fusion_info']['protection_policies']"
diff --git a/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_region/tasks/main.yml b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_region/tasks/main.yml
new file mode 100644
index 000000000..8e0112f6d
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_region/tasks/main.yml
@@ -0,0 +1,53 @@
+- name: Create Region foo_region
+ purestorage.fusion.fusion_region:
+ name: "foo_region"
+ display_name: "foo Region"
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect Regions and verify the region exists
+ environment: "{{ test_env }}"
+ purestorage.fusion.fusion_info:
+ gather_subset: regions
+ register: fusion_info
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'foo_region' in fusion_info['fusion_info']['regions']"
+
+- name: Update Region display_name
+ purestorage.fusion.fusion_region:
+ name: "foo_region"
+ display_name: "new foo Region"
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Delete Region
+ purestorage.fusion.fusion_region:
+ name: "foo_region"
+ state: absent
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect Regions and verify the region does not exist
+ environment: "{{ test_env }}"
+ purestorage.fusion.fusion_info:
+ gather_subset: regions
+ register: fusion_info
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'foo_region' not in fusion_info['fusion_info']['regions']"
diff --git a/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_sc/tasks/main.yml b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_sc/tasks/main.yml
new file mode 100644
index 000000000..6420db28c
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_sc/tasks/main.yml
@@ -0,0 +1,94 @@
+# Prepare dependencies
+- name: Create new tenat foo_tenant
+ purestorage.fusion.fusion_tenant:
+ name: foo_tenant
+ display_name: "tenant foo"
+ environment: "{{ test_env }}"
+
+- name: Create new tenant space foo_tenant_space for tenant_foo
+ purestorage.fusion.fusion_ts:
+ name: foo_tenant_space
+ tenant: foo_tenant
+ state: present
+ environment: "{{ test_env }}"
+
+- name: Create new storage service foo_service
+ purestorage.fusion.fusion_ss:
+ name: foo_service
+ hardware_types:
+ - flash-array-x
+ display_name: "test class"
+ environment: "{{ test_env }}"
+
+
+# Test storage classes
+
+- name: Create new storage class foo_sc
+ purestorage.fusion.fusion_sc:
+ name: foo_sc
+ size_limit: 100G
+ iops_limit: 100000
+ bw_limit: 25M
+ storage_service: foo_service
+ display_name: "test class"
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+
+- name: Collect storage classes and verify the class exists
+ environment: "{{ test_env }}"
+ purestorage.fusion.fusion_info:
+ gather_subset: storage_classes
+ register: fusion_info
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'foo_sc' in fusion_info['fusion_info']['storage_classes']"
+
+- name: Delete storage class
+ purestorage.fusion.fusion_sc:
+ name: foo_sc
+ storage_service: foo_service
+ state: absent
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect storage classes and verify the class does not exist
+ environment: "{{ test_env }}"
+ purestorage.fusion.fusion_info:
+ gather_subset: storage_classes
+ register: fusion_info
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'foo_sc' not in fusion_info['fusion_info']['storage_classes']"
+
+
+# Teardown dependencies
+
+- name: Delete storage service foo_service
+ purestorage.fusion.fusion_ss:
+ name: foo_service
+ state: absent
+ environment: "{{ test_env }}"
+
+- name: Delete foo_tenant_space
+ purestorage.fusion.fusion_ts:
+ name: foo_tenant_space
+ tenant: foo_tenant
+ state: absent
+ environment: "{{ test_env }}"
+
+- name: Delete tenat foo
+ purestorage.fusion.fusion_tenant:
+ name: foo_tenant
+ state: absent
+ environment: "{{ test_env }}"
diff --git a/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_se/tasks/main.yml b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_se/tasks/main.yml
new file mode 100644
index 000000000..a900712ec
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_se/tasks/main.yml
@@ -0,0 +1,100 @@
+# Prepare dependencies
+
+- name: Create Availability Zone
+ environment: "{{ test_env }}"
+ purestorage.fusion.fusion_az:
+ name: "test_az"
+ display_name: "foo AZ"
+ region: pure-us-west
+ register: result
+
+- name: Create new network interface group
+ purestorage.fusion.fusion_nig:
+ name: "foo_group"
+ availability_zone: test_az
+ region: pure-us-west
+ state: present
+ mtu: 1500
+ gateway: 172.17.17.1
+ prefix: 172.17.17.0/24
+ environment: "{{ test_env }}"
+
+
+# Test storage classes
+
+- name: Create new Storage Endpoint
+ purestorage.fusion.fusion_se:
+ state: present # or absent
+ region: pure-us-west
+ name: foo_se
+ display_name: "foo se"
+ availability_zone: test_az
+ endpoint_type: iscsi
+ iscsi:
+ - address: "172.17.1.2/24"
+ gateway: "172.17.1.1"
+ network_interface_groups: ["foo_group"]
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect storage endpoints and verify the endpoint exists
+ environment: "{{ test_env }}"
+ purestorage.fusion.fusion_info:
+ gather_subset: storage_endpoints
+ register: fusion_info
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'pure-us-west/test_az/foo_se' in fusion_info['fusion_info']['storage_endpoints']"
+
+- name: Delete Storage Endpoint
+ purestorage.fusion.fusion_se:
+ state: absent
+ region: pure-us-west
+ name: foo_se
+ display_name: "foo se"
+ availability_zone: test_az
+ endpoint_type: iscsi
+ iscsi:
+ - address: "172.17.1.2/24"
+ gateway: "172.17.1.1"
+ network_interface_groups: ["foo_group"]
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect storage endpoints and verify the endpoint does not exist
+ environment: "{{ test_env }}"
+ purestorage.fusion.fusion_info:
+ gather_subset: storage_endpoints
+ register: fusion_info
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'pure-us-west/test_az/foo_se' not in fusion_info['fusion_info']['storage_endpoints']"
+
+
+# Teardown dependencies
+
+- name: Delete network interface group
+ purestorage.fusion.fusion_nig:
+ name: "foo_group"
+ availability_zone: test_az
+ region: pure-us-west
+ state: absent
+ environment: "{{ test_env }}"
+
+- name: Delete AZ
+ purestorage.fusion.fusion_az:
+ name: "test_az"
+ state: absent
+ region: pure-us-west
+ register: result
+ environment: "{{ test_env }}"
diff --git a/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_ss/tasks/main.yml b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_ss/tasks/main.yml
new file mode 100644
index 000000000..26332fcf7
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_ss/tasks/main.yml
@@ -0,0 +1,77 @@
+# Prepare dependencies
+- name: Create new tenat foo_tenant
+ purestorage.fusion.fusion_tenant:
+ name: foo_tenant
+ display_name: "tenant foo"
+ environment: "{{ test_env }}"
+
+- name: Create new tenant space foo_tenant_space for tenant_foo
+ purestorage.fusion.fusion_ts:
+ name: foo_tenant_space
+ tenant: foo_tenant
+ state: present
+ environment: "{{ test_env }}"
+
+
+# Test storage services
+
+- name: Create new storage service foo_service
+ purestorage.fusion.fusion_ss:
+ name: foo_service
+ hardware_types:
+ - flash-array-x
+ display_name: "test class"
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+
+- name: Collect storage services and check the service exists
+ purestorage.fusion.fusion_info:
+ gather_subset: storage_services
+ register: fusion_info
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'foo_service' in fusion_info['fusion_info']['storage_services']"
+
+- name: Delete storage service foo_service
+ purestorage.fusion.fusion_ss:
+ name: foo_service
+ state: absent
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect storage services and check the service does not exist
+ purestorage.fusion.fusion_info:
+ gather_subset: storage_services
+ register: fusion_info
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'foo_service' not in fusion_info['fusion_info']['storage_services']"
+
+
+# Teardown dependencies
+
+- name: Delete foo_tenant_space
+ purestorage.fusion.fusion_ts:
+ name: foo_tenant_space
+ tenant: foo_tenant
+ state: absent
+ environment: "{{ test_env }}"
+
+- name: Delete tenat foo
+ purestorage.fusion.fusion_tenant:
+ name: foo_tenant
+ state: absent
+ environment: "{{ test_env }}"
diff --git a/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_tenant/tasks/main.yml b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_tenant/tasks/main.yml
new file mode 100644
index 000000000..262c74a73
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_tenant/tasks/main.yml
@@ -0,0 +1,41 @@
+- name: Create new tenat foo_tenant
+ purestorage.fusion.fusion_tenant:
+ name: foo_tenant
+ display_name: "tenant foo"
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect tenants and check the tenant exists
+ purestorage.fusion.fusion_info:
+ gather_subset: tenants
+ register: fusion_info
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'foo_tenant' in fusion_info['fusion_info']['tenants']"
+
+- name: Delete tenat foo
+ purestorage.fusion.fusion_tenant:
+ name: foo_tenant
+ state: absent
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect tenants and check the tenant does notexist
+ purestorage.fusion.fusion_info:
+ gather_subset: tenants
+ register: fusion_info
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'foo_tenant' not in fusion_info['fusion_info']['tenants']"
diff --git a/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_ts/tasks/main.yml b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_ts/tasks/main.yml
new file mode 100644
index 000000000..101c3f251
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/integration/targets/fusion_ts/tasks/main.yml
@@ -0,0 +1,62 @@
+# Prepare dependencies
+- name: Create new tenat foo_tenant
+ purestorage.fusion.fusion_tenant:
+ name: foo_tenant
+ display_name: "tenant foo"
+ environment: "{{ test_env }}"
+
+
+# Test tenant spaces
+
+- name: Create new tenant space foo_tenant_space for tenant_foo
+ purestorage.fusion.fusion_ts:
+ name: foo_tenant_space
+ tenant: foo_tenant
+ state: present
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect tenant spaces and check the tenant space exists
+ purestorage.fusion.fusion_info:
+ gather_subset: tenant_spaces
+ register: fusion_info
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'foo_tenant/foo_tenant_space' in fusion_info['fusion_info']['tenant_spaces']"
+
+- name: Delete foo_tenant_space
+ purestorage.fusion.fusion_ts:
+ name: foo_tenant_space
+ tenant: foo_tenant
+ state: absent
+ register: result
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that:
+ - result is success
+ - result is changed
+
+- name: Collect tenant spaces and check the tenant space does not exist
+ purestorage.fusion.fusion_info:
+ gather_subset: tenant_spaces
+ register: fusion_info
+ environment: "{{ test_env }}"
+- name: Validate the task
+ ansible.builtin.assert:
+ that: "'foo_tenant/foo_tenant_space' not in fusion_info['fusion_info']['tenant_spaces']"
+
+
+# Teardown dependencies
+
+- name: Delete tenat foo
+ purestorage.fusion.fusion_tenant:
+ name: foo_tenant
+ state: absent
+ environment: "{{ test_env }}"
diff --git a/ansible_collections/purestorage/fusion/tests/unit/README.md b/ansible_collections/purestorage/fusion/tests/unit/README.md
new file mode 100644
index 000000000..248a608ba
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/unit/README.md
@@ -0,0 +1,15 @@
+# Unit tests
+
+Unit tests aims at testing specific functions of modules.
+
+Each module as a whole should be tested in Functional tests.
+
+## Running tests
+
+```bash
+pytest tests/unit
+```
+
+## Adding new tests
+
+See already existing tests for inspiration.
diff --git a/ansible_collections/purestorage/fusion/tests/unit/mocks/__init__.py b/ansible_collections/purestorage/fusion/tests/unit/mocks/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/unit/mocks/__init__.py
diff --git a/ansible_collections/purestorage/fusion/tests/unit/mocks/module_mock.py b/ansible_collections/purestorage/fusion/tests/unit/mocks/module_mock.py
new file mode 100644
index 000000000..cb6d489e1
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/unit/mocks/module_mock.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Denys Denysyev (ddenysyev@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock
+
+
+class ModuleSucceeded(Exception):
+ pass
+
+
+class ModuleFailed(Exception):
+ pass
+
+
+class ModuleMock(MagicMock):
+ def __init__(self, params, check_mode=False):
+ super().__init__()
+
+ self.params = params
+ self.check_mode = check_mode
+
+ # mocking exit_json function, so we can check if it was successfully called
+ self.exit_json = MagicMock()
+
+ def fail_json(self, **kwargs):
+ raise ModuleFailed(str(kwargs))
+
+ def fail_on_missing_params(self, required_params=None):
+ if required_params is not None:
+ for param in required_params:
+ if param not in self.params:
+ raise ModuleFailed(f"Parameter '{param}' is missing")
diff --git a/ansible_collections/purestorage/fusion/tests/unit/mocks/operation_mock.py b/ansible_collections/purestorage/fusion/tests/unit/mocks/operation_mock.py
new file mode 100644
index 000000000..99487ddfa
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/unit/mocks/operation_mock.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Denys Denysyev (ddenysyev@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from enum import Enum
+
+
+class OperationStatus(str, Enum):
+ PENDING = "Pending"
+ ABORTING = "Aborting"
+ FAILED = "Failed"
+ SUCCEDED = "Succeeded"
+
+
+class OperationMock:
+ def __init__(self, id, status, retry_in=1):
+ self.id = id
+ self.status = status
+ self.retry_in = retry_in
diff --git a/ansible_collections/purestorage/fusion/tests/unit/module_utils/__init__.py b/ansible_collections/purestorage/fusion/tests/unit/module_utils/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/unit/module_utils/__init__.py
diff --git a/ansible_collections/purestorage/fusion/tests/unit/module_utils/test_networking.py b/ansible_collections/purestorage/fusion/tests/unit/module_utils/test_networking.py
new file mode 100644
index 000000000..13437456a
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/unit/module_utils/test_networking.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.networking import (
+ is_valid_address,
+ is_valid_network,
+ is_address_in_network,
+)
+
+
+def test_valid_address():
+ assert is_valid_address("0.0.0.0")
+ assert is_valid_address("1.1.1.1")
+ assert is_valid_address("192.168.1.2")
+ assert is_valid_address("255.255.255.255")
+
+
+def test_invalid_address():
+ assert not is_valid_address("256.1.1.1")
+ assert not is_valid_address("1.256.1.1")
+ assert not is_valid_address("1.1.256.1")
+ assert not is_valid_address("1.1.1.256")
+ assert not is_valid_address("1.1.1.256")
+ assert not is_valid_address("010.010.010.010")
+ assert not is_valid_address("1.1.1")
+ assert not is_valid_address("hostname")
+ assert not is_valid_address("0x1.0x2.0x3.0x4")
+
+
+def test_valid_network():
+ assert is_valid_network("0.0.0.0/8")
+ assert is_valid_network("1.1.1.1/12")
+ assert is_valid_network("192.168.1.2/24")
+ assert is_valid_network("255.255.255.255/32")
+
+
+def test_invalid_network():
+ assert not is_valid_network("1.1.1.1")
+ assert not is_valid_network("1.1.1.1/")
+ assert not is_valid_network("1.1.1.1/1")
+ assert not is_valid_network("1.1.1.1/7")
+ assert not is_valid_network("1.1.1.1/33")
+
+
+def test_address_is_in_network():
+ assert is_address_in_network("1.1.1.1", "1.1.0.0/16")
+ assert is_address_in_network("1.1.1.1", "1.1.1.1/32")
+
+
+def test_address_is_not_in_network():
+ assert not is_address_in_network("1.1.1.1", "1.2.0.0/16")
+ assert not is_address_in_network("1.1.1.1", "1.1.1.2/32")
diff --git a/ansible_collections/purestorage/fusion/tests/unit/module_utils/test_operations.py b/ansible_collections/purestorage/fusion/tests/unit/module_utils/test_operations.py
new file mode 100644
index 000000000..6b42eb35f
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/unit/module_utils/test_operations.py
@@ -0,0 +1,230 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Denys Denysyev (ddenysyev@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+import time
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+from ansible_collections.purestorage.fusion.tests.helpers import (
+ ApiExceptionsMockGenerator,
+)
+from ansible_collections.purestorage.fusion.tests.unit.mocks.operation_mock import (
+ OperationMock,
+ OperationStatus,
+)
+
+__metaclass__ = type
+
+import fusion as purefusion
+from urllib3.exceptions import HTTPError
+
+from unittest.mock import Mock, MagicMock, call, patch
+import pytest
+from ansible_collections.purestorage.fusion.plugins.module_utils import operations
+
+time.sleep = MagicMock() # mock time.sleep function globally
+current_module = (
+ "ansible_collections.purestorage.fusion.tests.unit.module_utils.test_operations"
+)
+
+
+class TestAwaitOperations:
+ @patch(f"{current_module}.operations.purefusion.OperationsApi.__new__")
+ def test_await_success_op(self, mock_op_api):
+ """
+ Should return operation
+ """
+ # Mock operation
+ op = OperationMock("1", OperationStatus.SUCCEDED)
+
+ # Mock operations api
+ mock_op_api_obj = MagicMock()
+ mock_op_api.return_value = mock_op_api_obj
+ mock_op_api_obj.get_operation = Mock(return_value=op)
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Test function
+ op1 = operations.await_operation(fusion_mock, op)
+
+ # Assertions
+ assert op == op1
+ mock_op_api_obj.get_operation.assert_called_once_with(op.id)
+
+ @patch(f"{current_module}.operations.purefusion.OperationsApi.__new__")
+ def test_await_failed_op(self, mock_op_api):
+ """
+ Should raise OperationException
+ """
+ # Mock operation
+ op = OperationMock("1", OperationStatus.FAILED)
+
+ # Mock exception
+ op_exception = OperationException(op, None)
+
+ # Mock operations api
+ mock_op_api_obj = MagicMock()
+ mock_op_api.return_value = mock_op_api_obj
+ mock_op_api_obj.get_operation = Mock(return_value=op)
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Test function
+ with pytest.raises(Exception) as exception:
+ operations.await_operation(fusion_mock, op)
+
+ # Assertions
+ assert (
+ type(exception) is type(op_exception)
+ and exception.args == op_exception.args
+ )
+ mock_op_api_obj.get_operation.assert_called_once_with(op.id)
+
+ @patch(f"{current_module}.operations.purefusion.OperationsApi.__new__")
+ def test_await_pending_op(self, mock_op_api):
+ """
+ Should return operation
+ """
+ # Mock operation
+ op1 = OperationMock("1", OperationStatus.PENDING)
+ op2 = OperationMock("1", OperationStatus.SUCCEDED)
+
+ # Mock operations api
+ mock_op_api_obj = MagicMock()
+ mock_op_api.return_value = mock_op_api_obj
+ mock_op_api_obj.get_operation = Mock(side_effect=[op1, op2])
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Test function
+ op = operations.await_operation(fusion_mock, op1)
+
+ # Assertions
+ assert op == op2
+ calls = [call(op1.id), call(op1.id)]
+ mock_op_api_obj.get_operation.assert_has_calls(calls)
+
+ @patch(f"{current_module}.operations.purefusion.OperationsApi.__new__")
+ def test_await_failed_pending_op(self, mock_op_api):
+ """
+ Should raise OperationException
+ """
+ # Mock operation
+ op1 = OperationMock("1", OperationStatus.PENDING)
+ op2 = OperationMock("1", OperationStatus.FAILED)
+
+ # Mock exception
+ op_exception = OperationException(op2, None)
+
+ # Mock operations api
+ mock_op_api_obj = MagicMock()
+ mock_op_api.return_value = mock_op_api_obj
+ mock_op_api_obj.get_operation = Mock(side_effect=[op1, op2])
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Test function
+ with pytest.raises(Exception) as exception:
+ operations.await_operation(fusion_mock, op1)
+
+ # Assertions
+ assert (
+ type(exception) is type(op_exception)
+ and exception.args == op_exception.args
+ )
+ calls = [call(op1.id), call(op1.id)]
+ mock_op_api_obj.get_operation.assert_has_calls(calls)
+
+ @patch(f"{current_module}.operations.purefusion.OperationsApi.__new__")
+ def test_await_api_exception(self, mock_op_api):
+ """
+ Should raise ApiException
+ """
+ # Mock exceptions
+ api_exception = ApiExceptionsMockGenerator.create_conflict()
+
+ # Mock operation
+ op = OperationMock("1", OperationStatus.FAILED)
+
+ # Mock operations api
+ mock_op_api_obj = MagicMock()
+ mock_op_api.return_value = mock_op_api_obj
+ mock_op_api_obj.get_operation = Mock(side_effect=api_exception)
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Test function
+ with pytest.raises(purefusion.rest.ApiException) as exception:
+ operations.await_operation(fusion_mock, op)
+
+ # Assertions
+ assert (
+ type(exception) is type(api_exception)
+ and exception.args == api_exception.args
+ )
+ mock_op_api_obj.get_operation.assert_called_once_with(op)
+
+ @patch(f"{current_module}.operations.purefusion.OperationsApi.__new__")
+ def test_await_http_exception(self, mock_op_api):
+ """
+ Should raise OperationException
+ """
+ # Mock operation
+ op = OperationMock("1", OperationStatus.FAILED)
+
+ # Mock exceptions
+ http_error = HTTPError()
+ op_exception = OperationException(op, http_error)
+ # Mock operations api
+ mock_op_api_obj = MagicMock()
+ mock_op_api.return_value = mock_op_api_obj
+ mock_op_api_obj.get_operation = Mock(side_effect=http_error)
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Test function
+ with pytest.raises(OperationException) as exception:
+ operations.await_operation(fusion_mock, op)
+
+ # Assertions
+ assert (
+ type(exception) is type(op_exception)
+ and exception.args == op_exception.args
+ )
+ mock_op_api_obj.get_operation.assert_called_once_with(op)
+
+ @patch(f"{current_module}.operations.purefusion.OperationsApi.__new__")
+ def test_await_failed_op_without_failing(self, mock_op_api):
+ """
+ Should return failed operation
+ """
+ # Mock operation
+ op = OperationMock("1", OperationStatus.FAILED)
+
+ # Mock operations api
+ mock_op_api_obj = MagicMock()
+ mock_op_api.return_value = mock_op_api_obj
+ mock_op_api_obj.get_operation = Mock(return_value=op)
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Test function
+ op_res = operations.await_operation(
+ fusion_mock, op, fail_playbook_if_operation_fails=False
+ )
+
+ # Assertions
+ assert op_res == op
+ mock_op_api_obj.get_operation.assert_called_once_with(op.id)
diff --git a/ansible_collections/purestorage/fusion/tests/unit/module_utils/test_parsing.py b/ansible_collections/purestorage/fusion/tests/unit/module_utils/test_parsing.py
new file mode 100644
index 000000000..7e2a1cc78
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/unit/module_utils/test_parsing.py
@@ -0,0 +1,138 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.parsing import (
+ parse_number_with_metric_suffix,
+ parse_minutes,
+)
+
+import pytest
+
+
+class MockException(Exception):
+ pass
+
+
+class MockModule:
+ def fail_json(self, msg):
+ raise MockException()
+
+
+def test_parsing_valid_number():
+ module = MockModule()
+ assert parse_number_with_metric_suffix(module, "0") == 0
+ assert parse_number_with_metric_suffix(module, "1") == 1
+ assert parse_number_with_metric_suffix(module, "1K") == 1024
+ assert parse_number_with_metric_suffix(module, "1 K") == 1024
+ assert parse_number_with_metric_suffix(module, "124 M") == 124 * 1024 * 1024
+ assert parse_number_with_metric_suffix(module, "10 G") == 10 * 1024 * 1024 * 1024
+ assert (
+ parse_number_with_metric_suffix(module, "20 T")
+ == 20 * 1024 * 1024 * 1024 * 1024
+ )
+ assert (
+ parse_number_with_metric_suffix(module, "30 P")
+ == 30 * 1024 * 1024 * 1024 * 1024 * 1024
+ )
+ assert (
+ parse_number_with_metric_suffix(module, "30000 P")
+ == 30000 * 1024 * 1024 * 1024 * 1024 * 1024
+ )
+ assert parse_number_with_metric_suffix(module, "0", factor=1000) == 0
+ assert parse_number_with_metric_suffix(module, "1", factor=1000) == 1
+ assert parse_number_with_metric_suffix(module, "1K", factor=1000) == 1000
+ assert (
+ parse_number_with_metric_suffix(module, "124M", factor=1000)
+ == 124 * 1000 * 1000
+ )
+ assert parse_number_with_metric_suffix(module, "1.5K", factor=1000) == 1500
+ assert parse_number_with_metric_suffix(module, "1.5K", factor=1024) == 1536
+
+
+def test_parsing_invalid_number():
+ module = MockModule()
+ with pytest.raises(MockException):
+ assert parse_number_with_metric_suffix(module, "")
+ with pytest.raises(MockException):
+ assert parse_number_with_metric_suffix(module, "102X")
+ with pytest.raises(MockException):
+ assert parse_number_with_metric_suffix(module, "102 N")
+ with pytest.raises(MockException):
+ assert parse_number_with_metric_suffix(module, "102 N", factor=1000)
+ with pytest.raises(MockException):
+ assert parse_number_with_metric_suffix(module, "million")
+ with pytest.raises(MockException):
+ assert parse_number_with_metric_suffix(module, "K")
+ with pytest.raises(MockException):
+ assert parse_number_with_metric_suffix(module, "K1")
+ with pytest.raises(MockException):
+ assert parse_number_with_metric_suffix(module, "1K1")
+ with pytest.raises(MockException):
+ assert parse_number_with_metric_suffix(module, "1 K1")
+ with pytest.raises(MockException):
+ assert parse_number_with_metric_suffix(module, "M")
+ with pytest.raises(MockException):
+ assert parse_number_with_metric_suffix(module, "hello world")
+
+
+def test_parsing_valid_time_period():
+ module = MockModule()
+ assert parse_minutes(module, "10") == 10
+ assert parse_minutes(module, "2h") == 120
+ assert parse_minutes(module, "2H") == 120
+ assert parse_minutes(module, "14D") == 14 * 24 * 60
+ assert parse_minutes(module, "1W") == 7 * 24 * 60
+ assert parse_minutes(module, "12Y") == 12 * 365 * 24 * 60
+ assert (
+ parse_minutes(module, "10Y20W30D40H50M")
+ == 10 * 365 * 24 * 60 + 20 * 7 * 24 * 60 + 30 * 24 * 60 + 40 * 60 + 50
+ )
+ assert (
+ parse_minutes(module, "10Y20W30D40H")
+ == 10 * 365 * 24 * 60 + 20 * 7 * 24 * 60 + 30 * 24 * 60 + 40 * 60
+ )
+ assert (
+ parse_minutes(module, "10Y20W30D")
+ == 10 * 365 * 24 * 60 + 20 * 7 * 24 * 60 + 30 * 24 * 60
+ )
+ assert parse_minutes(module, "10Y20W") == 10 * 365 * 24 * 60 + 20 * 7 * 24 * 60
+ assert (
+ parse_minutes(module, "20W30D40H50M")
+ == 20 * 7 * 24 * 60 + 30 * 24 * 60 + 40 * 60 + 50
+ )
+ assert parse_minutes(module, "30D40H50M") == 30 * 24 * 60 + 40 * 60 + 50
+ assert parse_minutes(module, "40H50M") == 40 * 60 + 50
+ assert parse_minutes(module, "30D50M") == 30 * 24 * 60 + 50
+ assert parse_minutes(module, "20W40H") == 20 * 7 * 24 * 60 + 40 * 60
+
+
+def test_parsing_invalid_time_period():
+ module = MockModule()
+ with pytest.raises(MockException):
+ assert parse_minutes(module, "")
+ with pytest.raises(MockException):
+ assert parse_minutes(module, "1s")
+ with pytest.raises(MockException):
+ assert parse_minutes(module, "1S")
+ with pytest.raises(MockException):
+ assert parse_minutes(module, "1V")
+ with pytest.raises(MockException):
+ assert parse_minutes(module, "0M")
+ with pytest.raises(MockException):
+ assert parse_minutes(module, "0H10M")
+ with pytest.raises(MockException):
+ assert parse_minutes(module, "0H10M")
+ with pytest.raises(MockException):
+ assert parse_minutes(module, "0D10H10M")
+ with pytest.raises(MockException):
+ assert parse_minutes(module, "01W10D10H10M")
+ with pytest.raises(MockException):
+ assert parse_minutes(module, "01Y0H10M")
+ with pytest.raises(MockException):
+ assert parse_minutes(module, "1V")
diff --git a/ansible_collections/purestorage/fusion/tests/unit/module_utils/test_prerequisites.py b/ansible_collections/purestorage/fusion/tests/unit/module_utils/test_prerequisites.py
new file mode 100644
index 000000000..0158878cf
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/unit/module_utils/test_prerequisites.py
@@ -0,0 +1,116 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Jan Kodera (jkodera@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from ansible_collections.purestorage.fusion.plugins.module_utils.prerequisites import (
+ _parse_version,
+ _parse_version_requirements,
+ _version_satisfied,
+)
+
+import pytest
+
+
+def test_version():
+ # VALID
+ assert _parse_version("1.0") == (1, 0, None)
+ assert _parse_version("1.0.0") == (1, 0, 0)
+ assert _parse_version("2.3.4") == (2, 3, 4)
+ assert _parse_version("2.3.5a") == (2, 3, 5)
+ assert _parse_version("2.3.6-release") == (2, 3, 6)
+ # INVALID
+ assert _parse_version("1") is None
+ assert _parse_version("1.a") is None
+ assert _parse_version("1.1a") is None
+ assert _parse_version("a.1") is None
+ assert _parse_version("1.") is None
+ assert _parse_version("1..") is None
+ assert _parse_version("1.0.1.0") is None
+ assert _parse_version("1.0.1.a") is None
+
+
+def test_requirements():
+ # VALID
+ assert _parse_version_requirements(">= 1.0") == [(">=", (1, 0, None))]
+ assert _parse_version_requirements(">=1.0.1") == [(">=", (1, 0, 1))]
+ assert _parse_version_requirements(">= 2.0.2-release") == [(">=", (2, 0, 2))]
+ assert _parse_version_requirements(" >=3.0.3b") == [(">=", (3, 0, 3))]
+ assert _parse_version_requirements("<= 3.3.3") == [("<=", (3, 3, 3))]
+ assert _parse_version_requirements("= 3.0.3") == [("=", (3, 0, 3))]
+ assert _parse_version_requirements("== 5.3.1") == [("==", (5, 3, 1))]
+ assert _parse_version_requirements("< 4.1.2") == [("<", (4, 1, 2))]
+ assert _parse_version_requirements("> 1.3.4") == [(">", (1, 3, 4))]
+ assert _parse_version_requirements("> 1.3.4, < 2.0") == [
+ (">", (1, 3, 4)),
+ ("<", (2, 0, None)),
+ ]
+ assert _parse_version_requirements(">1.3.4 , <2.0") == [
+ (">", (1, 3, 4)),
+ ("<", (2, 0, None)),
+ ]
+ assert _parse_version_requirements("> 1.3.4 ,< 2.0") == [
+ (">", (1, 3, 4)),
+ ("<", (2, 0, None)),
+ ]
+ assert _parse_version_requirements(">1.3.4,<2.0") == [
+ (">", (1, 3, 4)),
+ ("<", (2, 0, None)),
+ ]
+ assert _parse_version_requirements(">1.3.4,<2.0, != 3.4.1") == [
+ (">", (1, 3, 4)),
+ ("<", (2, 0, None)),
+ ("!=", (3, 4, 1)),
+ ]
+ # INVALID
+ with pytest.raises(ValueError):
+ _parse_version_requirements(">>1.3.4")
+ with pytest.raises(ValueError):
+ _parse_version_requirements("<<1.3.4")
+ with pytest.raises(ValueError):
+ _parse_version_requirements("=>1.3.4,,3.0")
+ with pytest.raises(ValueError):
+ _parse_version_requirements("=<1.3.4,")
+ with pytest.raises(ValueError):
+ _parse_version_requirements("=<1.3.4")
+
+
+def test_version_satisfied():
+ assert _version_satisfied("1.0", ">=1.0, <2.0") is True
+ assert _version_satisfied("1.0.1", ">=1.0, <2.0") is True
+ assert _version_satisfied("2.0", ">=1.0, <2.0") is False
+ assert _version_satisfied("2.0.0", ">=1.0, <2.0") is False
+ assert _version_satisfied("2.0.1", ">=1.0, <2.0") is False
+ assert _version_satisfied("1.0.0", ">=1.0.0") is True
+ assert _version_satisfied("1.0", ">=1.0.0") is True
+ assert _version_satisfied("1.0", ">=1.0") is True
+ assert _version_satisfied("1.0.1", ">=1.0") is True
+ assert _version_satisfied("1.0.1", ">=1.0.0") is True
+ assert _version_satisfied("1.0.1", "<=1.0.0") is False
+ assert _version_satisfied("1.0.0", "<=1.0.0") is True
+ assert _version_satisfied("1.0", "<=1.0.0") is True
+ assert _version_satisfied("1.0", "<=1.0.1") is True
+ assert _version_satisfied("1.0", "<=1.0") is True
+ assert _version_satisfied("1.0", "<1.0") is False
+ assert _version_satisfied("1.0.0", "<1.0") is False
+ assert _version_satisfied("1.0.0", "<1.1") is True
+ assert _version_satisfied("1.0.0", "<1.0.1") is True
+ assert _version_satisfied("1.0", ">1.0") is False
+ assert _version_satisfied("1.0.1", ">1.0") is False
+ assert _version_satisfied("1.0", ">1.0.0") is False
+ assert _version_satisfied("1.0.0", ">1.0.0") is False
+ assert _version_satisfied("1.0.1", ">1.0.0") is True
+ assert _version_satisfied("1.0", "==1.0") is True
+ assert _version_satisfied("1.0", "=1.0") is True
+ assert _version_satisfied("1.0.0", "==1.0") is True
+ assert _version_satisfied("1.0.1", "==1.0") is True
+ assert _version_satisfied("1.0", "==1.0.0") is True
+ assert _version_satisfied("1.0", "==1.0.1") is False
+ assert _version_satisfied("1.0", "!=1.0.1") is True
+ assert _version_satisfied("1.0", "!=1.0.0") is False
+ assert _version_satisfied("1.0.1", "!=1.0") is False
+ assert _version_satisfied("1.0", "!=1.0") is False
diff --git a/ansible_collections/purestorage/fusion/tests/unit/modules/__init__.py b/ansible_collections/purestorage/fusion/tests/unit/modules/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/unit/modules/__init__.py
diff --git a/ansible_collections/purestorage/fusion/tests/unit/modules/test_fusion_az.py b/ansible_collections/purestorage/fusion/tests/unit/modules/test_fusion_az.py
new file mode 100644
index 000000000..a384506d8
--- /dev/null
+++ b/ansible_collections/purestorage/fusion/tests/unit/modules/test_fusion_az.py
@@ -0,0 +1,446 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Denys Denysyev (ddenysyev@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING.GPLv3 or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+from unittest.mock import MagicMock, Mock, patch
+
+import fusion as purefusion
+import pytest
+from ansible_collections.purestorage.fusion.plugins.module_utils.errors import (
+ OperationException,
+)
+from ansible_collections.purestorage.fusion.plugins.modules import fusion_az
+from ansible_collections.purestorage.fusion.tests.helpers import (
+ ApiExceptionsMockGenerator,
+)
+from ansible_collections.purestorage.fusion.tests.unit.mocks.module_mock import (
+ ModuleMock,
+)
+from ansible_collections.purestorage.fusion.tests.unit.mocks.operation_mock import (
+ OperationMock,
+ OperationStatus,
+)
+
+fusion_az.setup_fusion = MagicMock()
+current_module = (
+ "ansible_collections.purestorage.fusion.tests.unit.modules.test_fusion_az"
+)
+
+
+def default_module_az_params(state="present", display_name="foo_az"):
+ module_params = {
+ "state": state,
+ "name": "foo",
+ "region": "region1",
+ "display_name": display_name,
+ "issuer_id": "ABCD12345",
+ "private_key_file": "az-admin-private-key.pem",
+ }
+ return module_params
+
+
+class TestCreateAZ:
+ @patch(f"{current_module}.fusion_az.purefusion.AvailabilityZonesApi.__new__")
+ @patch(f"{current_module}.fusion_az.await_operation")
+ def test_create_az_without_disp_name(self, await_operation_mock, mock_az_api):
+ """
+ Should create az successfully
+ """
+ # Mock operation
+ op = OperationMock("1", OperationStatus.SUCCEDED)
+
+ # Mock az api
+ mock_az_api_obj = MagicMock()
+ mock_az_api.return_value = mock_az_api_obj
+ mock_az_api_obj.create_availability_zone = MagicMock(return_value=op)
+
+ # Mock await operation
+ await_operation_mock.return_value = op
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Mock Module
+ module_params = default_module_az_params("present", None)
+ moduleMock = ModuleMock(module_params)
+
+ # Test function
+ fusion_az.create_az(moduleMock, fusion_mock)
+
+ # Assertions
+ azone = purefusion.AvailabilityZonePost(
+ name=module_params["name"],
+ display_name=module_params["name"],
+ )
+ mock_az_api_obj.create_availability_zone.assert_called_with(
+ azone, region_name=module_params["region"]
+ )
+ await_operation_mock.assert_called_once_with(fusion_mock, op)
+ moduleMock.exit_json.assert_called_once_with(changed=True)
+
+ @patch(f"{current_module}.fusion_az.purefusion.AvailabilityZonesApi.__new__")
+ @patch(f"{current_module}.fusion_az.await_operation")
+ def test_create_az_check_mode(self, await_operation_mock, mock_az_api):
+ """
+ Should only exit_json
+ """
+ # Mock operation
+ op = OperationMock("1", OperationStatus.SUCCEDED)
+
+ # Mock az api
+ mock_az_api_obj = MagicMock()
+ mock_az_api.return_value = mock_az_api_obj
+ mock_az_api_obj.create_availability_zone = MagicMock(return_value=op)
+
+ # Mock await operation
+ await_operation_mock.return_value = op
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Mock Module
+ module_params = default_module_az_params("present")
+ moduleMock = ModuleMock(module_params, check_mode=True)
+
+ # Test function
+ fusion_az.create_az(moduleMock, fusion_mock)
+
+ # Assertions
+ mock_az_api_obj.create_availability_zone.assert_not_called()
+ await_operation_mock.assert_not_called()
+ moduleMock.exit_json.assert_called_once_with(changed=True)
+
+ @patch(f"{current_module}.fusion_az.purefusion.AvailabilityZonesApi.__new__")
+ @patch(f"{current_module}.fusion_az.await_operation")
+ def test_create_az_with_disp_name(self, await_operation_mock, mock_az_api):
+ """
+ Should create az successfully
+ """
+ # Mock operation
+ op = OperationMock("1", OperationStatus.SUCCEDED)
+
+ # Mock az api
+ mock_az_api_obj = MagicMock()
+ mock_az_api.return_value = mock_az_api_obj
+ mock_az_api_obj.create_availability_zone = MagicMock(return_value=op)
+
+ # Mock await operation
+ await_operation_mock.return_value = op
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Mock Module
+ module_params = default_module_az_params("present")
+ moduleMock = ModuleMock(module_params)
+
+ # Test function
+ fusion_az.create_az(moduleMock, fusion_mock)
+
+ # Assertions
+ azone = purefusion.AvailabilityZonePost(
+ name=module_params["name"],
+ display_name=module_params["display_name"],
+ )
+ mock_az_api_obj.create_availability_zone.assert_called_with(
+ azone, region_name=module_params["region"]
+ )
+ await_operation_mock.assert_called_once_with(fusion_mock, op)
+ moduleMock.exit_json.assert_called_once_with(changed=True)
+
+ @patch(f"{current_module}.fusion_az.purefusion.AvailabilityZonesApi.__new__")
+ @patch(f"{current_module}.fusion_az.await_operation")
+ def test_create_az_conflict(self, await_operation_mock, mock_az_api):
+ """
+ Should raise api exception
+ """
+ # Mock exceptions
+ api_exception = ApiExceptionsMockGenerator.create_conflict()
+
+ # Mock operation
+ op = OperationMock("1", OperationStatus.FAILED)
+
+ # Mock az api
+ mock_az_api_obj = MagicMock()
+ mock_az_api.return_value = mock_az_api_obj
+ mock_az_api_obj.create_availability_zone = Mock(side_effect=api_exception)
+
+ # Mock await operation
+ await_operation_mock.return_value = op
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Mock Module
+ module_params = default_module_az_params("present")
+ moduleMock = ModuleMock(module_params)
+
+ # Test function
+ with pytest.raises(purefusion.rest.ApiException) as exception:
+ fusion_az.create_az(moduleMock, fusion_mock)
+ azone = purefusion.AvailabilityZonePost(
+ name=module_params["name"],
+ display_name=module_params["display_name"],
+ )
+
+ # Assertions
+ assert (
+ type(exception) is type(api_exception)
+ and exception.args == api_exception.args
+ )
+ mock_az_api_obj.create_availability_zone.assert_called_with(
+ azone, region_name=module_params["region"]
+ )
+ await_operation_mock.assert_not_called()
+ moduleMock.exit_json.assert_not_called()
+
+ @patch(f"{current_module}.fusion_az.purefusion.AvailabilityZonesApi.__new__")
+ @patch(f"{current_module}.fusion_az.await_operation")
+ def test_create_az_not_found(self, await_operation_mock, mock_az_api):
+ """
+ Should raise api exception
+ """
+ # Mock exceptions
+ api_exception = ApiExceptionsMockGenerator.create_not_found()
+
+ # Mock operation
+ op = OperationMock("1", OperationStatus.FAILED)
+
+ # Mock az api
+ mock_az_api_obj = MagicMock()
+ mock_az_api.return_value = mock_az_api_obj
+ mock_az_api_obj.create_availability_zone = Mock(side_effect=api_exception)
+
+ # Mock await operation
+ await_operation_mock.return_value = op
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Mock Module
+ module_params = default_module_az_params("present")
+ moduleMock = ModuleMock(module_params)
+
+ # Test function
+ with pytest.raises(purefusion.rest.ApiException) as exception:
+ fusion_az.create_az(moduleMock, fusion_mock)
+ azone = purefusion.AvailabilityZonePost(
+ name=module_params["name"],
+ display_name=module_params["display_name"],
+ )
+
+ # Assertions
+ assert (
+ type(exception) is type(api_exception)
+ and exception.args == api_exception.args
+ )
+ mock_az_api_obj.create_availability_zone.assert_called_with(
+ azone, region_name=module_params["region"]
+ )
+ await_operation_mock.assert_not_called()
+ moduleMock.exit_json.assert_not_called()
+
+ @patch(f"{current_module}.fusion_az.purefusion.AvailabilityZonesApi.__new__")
+ @patch(f"{current_module}.fusion_az.await_operation")
+ def test_create_az_op_fails(self, await_operation_mock, mock_az_api):
+ """
+ Should raise operation exception
+ """
+ # Mock operation
+ op = OperationMock("1", OperationStatus.FAILED)
+
+ # Mock exception
+ op_exception = OperationException(op, None)
+
+ # Mock az api
+ mock_az_api_obj = MagicMock()
+ mock_az_api.return_value = mock_az_api_obj
+ mock_az_api_obj.create_availability_zone = MagicMock(return_value=op)
+
+ # Mock await operation
+ await_operation_mock.side_effect = op_exception
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Mock Module
+ module_params = default_module_az_params("present")
+ moduleMock = ModuleMock(module_params)
+
+ # Test function
+ with pytest.raises(Exception) as exception:
+ fusion_az.create_az(moduleMock, fusion_mock)
+ azone = purefusion.AvailabilityZonePost(
+ name=module_params["name"],
+ display_name=module_params["display_name"],
+ )
+
+ # Assertions
+ assert (
+ type(exception) is type(op_exception)
+ and exception.args == op_exception.args
+ )
+ mock_az_api_obj.create_availability_zone.assert_called_with(
+ azone, region_name=module_params["region"]
+ )
+ await_operation_mock.assert_called_once(fusion_mock, op)
+ moduleMock.exit_json.assert_not_called()
+
+
+class TestDeleteAZ:
+ @patch(f"{current_module}.fusion_az.purefusion.AvailabilityZonesApi.__new__")
+ @patch(f"{current_module}.fusion_az.await_operation")
+ def test_delete_az_successfully(self, await_operation_mock, mock_az_api):
+ """
+ Should delete az successfully
+ """
+ # Mock operation
+ op = OperationMock("1", OperationStatus.SUCCEDED)
+
+ # Mock az api
+ mock_az_api_obj = MagicMock()
+ mock_az_api.return_value = mock_az_api_obj
+ mock_az_api_obj.delete_availability_zone = MagicMock(return_value=op)
+
+ # Mock await operation
+ await_operation_mock.return_value = op
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Mock Module
+ module_params = default_module_az_params("absent")
+ moduleMock = ModuleMock(module_params)
+
+ # Test function
+ fusion_az.delete_az(moduleMock, fusion_mock)
+
+ # Assertions
+ mock_az_api_obj.delete_availability_zone.assert_called_with(
+ availability_zone_name=module_params["name"],
+ region_name=module_params["region"],
+ )
+ await_operation_mock.assert_called_once_with(fusion_mock, op)
+ moduleMock.exit_json.assert_called_once_with(changed=True)
+
+ @patch(f"{current_module}.fusion_az.purefusion.AvailabilityZonesApi.__new__")
+ @patch(f"{current_module}.fusion_az.await_operation")
+ def test_create_az_conflict(self, await_operation_mock, mock_az_api):
+ """
+ Should raise api exception
+ """
+ # Mock exceptions
+ api_exception = ApiExceptionsMockGenerator.create_conflict()
+
+ # Mock operation
+ op = OperationMock("1", OperationStatus.FAILED)
+
+ # Mock az api
+ mock_az_api_obj = MagicMock()
+ mock_az_api.return_value = mock_az_api_obj
+ mock_az_api_obj.delete_availability_zone = Mock(side_effect=api_exception)
+
+ # Mock await operation
+ await_operation_mock.return_value = op
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Mock Module
+ module_params = default_module_az_params("absent")
+ moduleMock = ModuleMock(module_params)
+
+ # Test function
+ with pytest.raises(purefusion.rest.ApiException) as exception:
+ fusion_az.delete_az(moduleMock, fusion_mock)
+
+ # Assertions
+ assert (
+ type(exception) is type(api_exception)
+ and exception.args == api_exception.args
+ )
+ mock_az_api_obj.delete_availability_zone.assert_called_with(
+ region_name=module_params["region"],
+ availability_zone_name=module_params["name"],
+ )
+ await_operation_mock.assert_not_called()
+ moduleMock.exit_json.assert_not_called()
+
+ @patch(f"{current_module}.fusion_az.purefusion.AvailabilityZonesApi.__new__")
+ @patch(f"{current_module}.fusion_az.await_operation")
+ def test_create_az_op_fails(self, await_operation_mock, mock_az_api):
+ """
+ Should raise operation exception
+ """
+ # Mock operation
+ op = OperationMock("1", OperationStatus.FAILED)
+
+ # Mock exception
+ op_exception = OperationException(op, None)
+
+ # Mock az api
+ mock_az_api_obj = MagicMock()
+ mock_az_api.return_value = mock_az_api_obj
+ mock_az_api_obj.delete_availability_zone = MagicMock(return_value=op)
+
+ # Mock await operation
+ await_operation_mock.side_effect = op_exception
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Mock Module
+ module_params = default_module_az_params("absent")
+ moduleMock = ModuleMock(module_params)
+
+ # Test function
+ with pytest.raises(OperationException) as exception:
+ fusion_az.delete_az(moduleMock, fusion_mock)
+ # Assertions
+ assert (
+ type(exception) is type(op_exception)
+ and exception.args == op_exception.args
+ )
+ mock_az_api_obj.delete_availability_zone.assert_called_with(
+ region_name=module_params["region"],
+ availability_zone_name=module_params["name"],
+ )
+ await_operation_mock.assert_called_once(fusion_mock, op)
+ moduleMock.exit_json.assert_not_called()
+
+ @patch(f"{current_module}.fusion_az.purefusion.AvailabilityZonesApi.__new__")
+ @patch(f"{current_module}.fusion_az.await_operation")
+ def test_delete_az_check_mode(self, await_operation_mock, mock_az_api):
+ """
+ Should only exit_json
+ """
+ # Mock operation
+ op = OperationMock("1", OperationStatus.SUCCEDED)
+
+ # Mock az api
+ mock_az_api_obj = MagicMock()
+ mock_az_api.return_value = mock_az_api_obj
+ mock_az_api_obj.delete_availability_zone = MagicMock(return_value=op)
+
+ # Mock await operation
+ await_operation_mock.return_value = op
+
+ # Mock fusion
+ fusion_mock = MagicMock()
+
+ # Mock Module
+ module_params = default_module_az_params("absent")
+ moduleMock = ModuleMock(module_params, check_mode=True)
+
+ # Test function
+ fusion_az.delete_az(moduleMock, fusion_mock)
+
+ # Assertions
+ mock_az_api_obj.delete_availability_zone.assert_not_called()
+ await_operation_mock.assert_not_called()
+ moduleMock.exit_json.assert_called_once_with(changed=True)