summaryrefslogtreecommitdiffstats
path: root/ansible_collections/netapp/storagegrid
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
commit975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch)
tree89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/netapp/storagegrid
parentInitial commit. (diff)
downloadansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz
ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/netapp/storagegrid')
-rw-r--r--ansible_collections/netapp/storagegrid/.github/workflows/coverage.yml45
-rw-r--r--ansible_collections/netapp/storagegrid/.github/workflows/main.yml48
-rw-r--r--ansible_collections/netapp/storagegrid/CHANGELOG.rst172
-rw-r--r--ansible_collections/netapp/storagegrid/COPYING674
-rw-r--r--ansible_collections/netapp/storagegrid/FILES.json572
-rw-r--r--ansible_collections/netapp/storagegrid/MANIFEST.json32
-rw-r--r--ansible_collections/netapp/storagegrid/README.md199
-rw-r--r--ansible_collections/netapp/storagegrid/changelogs/changelog.yaml171
-rw-r--r--ansible_collections/netapp/storagegrid/changelogs/config.yaml32
-rw-r--r--ansible_collections/netapp/storagegrid/changelogs/fragments/20.10.0.yaml14
-rw-r--r--ansible_collections/netapp/storagegrid/changelogs/fragments/20.6.1.yaml4
-rw-r--r--ansible_collections/netapp/storagegrid/changelogs/fragments/20.7.0.yaml2
-rw-r--r--ansible_collections/netapp/storagegrid/changelogs/fragments/21.11.0.yaml2
-rw-r--r--ansible_collections/netapp/storagegrid/changelogs/fragments/21.11.1.yaml2
-rw-r--r--ansible_collections/netapp/storagegrid/changelogs/fragments/21.6.0.yaml4
-rw-r--r--ansible_collections/netapp/storagegrid/changelogs/fragments/21.9.0.yaml6
-rw-r--r--ansible_collections/netapp/storagegrid/changelogs/fragments/DEVOPS-4416.yaml2
-rw-r--r--ansible_collections/netapp/storagegrid/changelogs/fragments/github-10.yaml2
-rw-r--r--ansible_collections/netapp/storagegrid/changelogs/fragments/github-66.yaml2
-rw-r--r--ansible_collections/netapp/storagegrid/changelogs/fragments/github-8.yaml2
-rw-r--r--ansible_collections/netapp/storagegrid/meta/runtime.yml23
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/doc_fragments/netapp.py41
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/module_utils/netapp.py211
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/module_utils/netapp_module.py237
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_account.py458
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_certificate.py226
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_client_certificate.py265
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_dns.py163
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_gateway.py532
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_group.py341
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_ha_group.py334
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_identity_federation.py335
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_info.py405
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_ntp.py173
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_regions.py163
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_traffic_classes.py375
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_user.py316
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_container.py352
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_group.py301
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_identity_federation.py335
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_info.py279
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_user.py335
-rw-r--r--ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_user_s3_key.py210
-rw-r--r--ansible_collections/netapp/storagegrid/requirements.txt1
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/compat/__init__.py0
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/compat/builtins.py34
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/compat/mock.py125
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/compat/unittest.py44
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_account.py380
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_certificate.py342
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_client_certificate.py347
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_dns.py241
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_gateway.py693
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_group.py317
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_ha_group.py408
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_identity_federation.py354
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_info.py362
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_ntp.py257
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_regions.py206
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_traffic_classes.py355
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_user.py476
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_container.py348
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_group.py403
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_identity_federation.py354
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_info.py263
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_user.py476
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_user_s3_key.py238
-rw-r--r--ansible_collections/netapp/storagegrid/tests/unit/requirements.txt1
68 files changed, 15422 insertions, 0 deletions
diff --git a/ansible_collections/netapp/storagegrid/.github/workflows/coverage.yml b/ansible_collections/netapp/storagegrid/.github/workflows/coverage.yml
new file mode 100644
index 000000000..d33950e7a
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/.github/workflows/coverage.yml
@@ -0,0 +1,45 @@
+name: NetApp.storagegrid Ansible Coverage
+
+on:
+ push:
+ pull_request:
+ schedule:
+ - cron: '0 6 * * *'
+
+jobs:
+ sanity:
+ name: Coverage on StorageGrid
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v2
+
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+
+ - name: Install ansible stable-2.11
+ run: pip install https://github.com/ansible/ansible/archive/stable-2.11.tar.gz --disable-pip-version-check
+
+ - name: Make directory to make ansible-test happy
+ run: |
+ pwd
+ mkdir -p ansible_collections/netapp/storagegrid/
+ rsync -av . ansible_collections/netapp/storagegrid/ --exclude ansible_collections/netapp/storagegrid/
+
+ - name: Run Unit Tests
+ run: ansible-test units --coverage --color --docker --python 3.8
+ working-directory: ansible_collections/netapp/storagegrid/
+
+ # ansible-test support producing code coverage date
+ - name: Generate coverage report
+ run: ansible-test coverage xml -v --requirements --group-by command --group-by version
+ working-directory: ansible_collections/netapp/storagegrid/
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v2
+ with:
+ working-directory: ansible_collections/netapp/storagegrid/
+ verbose: true \ No newline at end of file
diff --git a/ansible_collections/netapp/storagegrid/.github/workflows/main.yml b/ansible_collections/netapp/storagegrid/.github/workflows/main.yml
new file mode 100644
index 000000000..ec05c061d
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/.github/workflows/main.yml
@@ -0,0 +1,48 @@
+name: NetApp.storagegrid Ansible CI
+
+on:
+ push:
+ pull_request:
+ schedule:
+ - cron: '0 6 * * *'
+
+jobs:
+ sanity:
+ name: Sanity (${{ matrix.ansible }} on storagegrid
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ ansible:
+ - stable-2.9
+ - stable-2.10
+ - stable-2.11
+ - stable-2.12
+ - stable-2.13
+ - devel
+
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v2
+
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ with:
+ # Ansible 2.14 requires 3.9 as a minimum
+ python-version: 3.9
+
+ - name: Install ansible (${{ matrix.ansible }})
+ run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check
+
+ - name: Make directory to make ansible-test happy
+ run: |
+ pwd
+ mkdir -p ansible_collections/netapp/storagegrid/
+ rsync -av . ansible_collections/netapp/storagegrid/ --exclude ansible_collections/netapp/storagegrid/
+ - name: Run sanity tests storagegrid
+ run: ansible-test sanity --docker -v --color
+ working-directory: ansible_collections/netapp/storagegrid/
+
+ - name: Run Unit Tests
+ run: ansible-test units --docker -v --color
+ working-directory: ansible_collections/netapp/storagegrid/
diff --git a/ansible_collections/netapp/storagegrid/CHANGELOG.rst b/ansible_collections/netapp/storagegrid/CHANGELOG.rst
new file mode 100644
index 000000000..c3d64be86
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/CHANGELOG.rst
@@ -0,0 +1,172 @@
+===========================================
+NetApp StorageGRID Collection Release Notes
+===========================================
+
+.. contents:: Topics
+
+
+v21.11.1
+========
+
+Bugfixes
+--------
+
+- na_sg_org_container - fix versioning not enabled on initial bucket creation.
+
+v21.11.0
+========
+
+Minor Changes
+-------------
+
+- na_sg_org_container - supports versioning configuration for S3 buckets available in StorageGRID 11.6+.
+
+New Modules
+-----------
+
+- netapp.storagegrid.na_sg_grid_client_certificate - Manage Client Certificates on StorageGRID.
+
+v21.10.0
+========
+
+Minor Changes
+-------------
+
+- na_sg_grid_gateway - supports specifying HA Groups by name or UUID.
+
+Bugfixes
+--------
+
+- na_sg_org_group - fixed behaviour where update to ``s3_policy`` is ignored if ``management_policy`` is set.
+
+New Modules
+-----------
+
+- netapp.storagegrid.na_sg_grid_ha_group - Manage high availability (HA) group configuration on StorageGRID.
+- netapp.storagegrid.na_sg_grid_traffic_classes - Manage Traffic Classification Policy configuration on StorageGRID.
+
+v21.9.0
+=======
+
+Minor Changes
+-------------
+
+- PR2 - allow usage of Ansible module group defaults - for Ansible 2.12+.
+- na_sg_grid_gateway - supports load balancer endpoint binding available in StorageGRID 11.5+.
+- na_sg_org_container - supports creation of S3 Object Lock buckets available in StorageGRID 11.5+.
+
+Bugfixes
+--------
+
+- na_sg_grid_account - minor documentation fix.
+- na_sg_grid_gateway - existing endpoints matched by ``name`` and ``port``.
+
+v21.8.0
+=======
+
+Minor Changes
+-------------
+
+- PR2 - allow usage of Ansible module group defaults - for Ansible 2.12+.
+
+v21.7.0
+=======
+
+Minor Changes
+-------------
+
+- Updated documentation - added RETURN block for each module
+
+New Modules
+-----------
+
+- netapp.storagegrid.na_sg_grid_gateway - Manage Load balancer (gateway) endpoints on StorageGRID.
+
+v21.6.0
+=======
+
+Minor Changes
+-------------
+
+- na_sg_org_container - supports deletion of buckets when ``state`` is set to ``absent``.
+
+Bugfixes
+--------
+
+- na_sg_org_container - fix issue with applying compliance settings on buckets.
+
+New Modules
+-----------
+
+- netapp.storagegrid.na_sg_grid_certificate - Manage the Storage API and Grid Management certificates on StorageGRID.
+- netapp.storagegrid.na_sg_grid_identity_federation - NetApp StorageGRID manage Grid identity federation.
+- netapp.storagegrid.na_sg_org_identity_federation - NetApp StorageGRID manage Tenant identity federation.
+
+v20.11.0
+========
+
+Minor Changes
+-------------
+
+- na_sg_grid_account - New option ``root_access_account`` for granting initial root access permissions for the tenant to an existing federated group
+
+New Modules
+-----------
+
+- netapp.storagegrid.na_sg_grid_info - NetApp StorageGRID Grid information gatherer
+- netapp.storagegrid.na_sg_org_info - NetApp StorageGRID Org information gatherer
+
+v20.10.0
+========
+
+Minor Changes
+-------------
+
+- na_sg_grid_account - new option ``update_password`` for managing Tenant Account root password changes.
+- na_sg_grid_user - new option ``password`` and ``update_password`` for setting or updating Grid Admin User passwords.
+- na_sg_org_user - new option ``password`` and ``update_password`` for setting or updating Tenant User passwords.
+
+Breaking Changes / Porting Guide
+--------------------------------
+
+- This version introduces a breaking change.
+ All modules have been renamed from ``nac_sg_*`` to ``na_sg_*``.
+ Playbooks and Roles must be updated to match.
+
+Bugfixes
+--------
+
+- na_sg_grid_account - added ``no_log`` flag to password fields.
+- na_sg_grid_account - fixed documentation issue.
+- na_sg_grid_group - fixed group name parsing.
+- na_sg_org_group - fixed group name parsing.
+
+v20.6.1
+=======
+
+Minor Changes
+-------------
+
+- Fixed documentation issue in README.md
+
+Bugfixes
+--------
+
+- nac_sg_org_container - fixed documentation issue.
+
+v20.6.0
+=======
+
+New Modules
+-----------
+
+- netapp.storagegrid.nac_sg_grid_account - NetApp StorageGRID Manage Tenant account.
+- netapp.storagegrid.nac_sg_grid_dns - NetApp StorageGRID Manage Grid DNS servers.
+- netapp.storagegrid.nac_sg_grid_group - NetApp StorageGRID Manage Grid admin group.
+- netapp.storagegrid.nac_sg_grid_ntp - NetApp StorageGRID Manage Grid NTP servers.
+- netapp.storagegrid.nac_sg_grid_regions - NetApp StorageGRID Manage Grid Regions.
+- netapp.storagegrid.nac_sg_grid_user - NetApp StorageGRID Manage Grid admin user.
+- netapp.storagegrid.nac_sg_org_container - NetApp StorageGRID Manage S3 bucket.
+- netapp.storagegrid.nac_sg_org_group - NetApp StorageGRID Manage Tenant group.
+- netapp.storagegrid.nac_sg_org_user - NetApp StorageGRID Manage Tenant user.
+- netapp.storagegrid.nac_sg_org_user_s3_key - NetApp StorageGRID Manage S3 key.
diff --git a/ansible_collections/netapp/storagegrid/COPYING b/ansible_collections/netapp/storagegrid/COPYING
new file mode 100644
index 000000000..20d40b6bc
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/COPYING
@@ -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>. \ No newline at end of file
diff --git a/ansible_collections/netapp/storagegrid/FILES.json b/ansible_collections/netapp/storagegrid/FILES.json
new file mode 100644
index 000000000..86ed3f4c8
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/FILES.json
@@ -0,0 +1,572 @@
+{
+ "files": [
+ {
+ "name": ".",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "requirements.txt",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "7cc4959877dbe6b6c63a8eb1bfe3bfb545fa8fe5b28b1b2c13e4a7c1c0d1c4d4",
+ "format": 1
+ },
+ {
+ "name": "plugins",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/doc_fragments",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/doc_fragments/netapp.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e790e69d7116516a69110a233da28e21442e5fee8805b3b6f985854f27f26449",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/netapp.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "9f7a966f44fca740564887beea1232f8e89bad232cde62d9de9a12428ec442c4",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/netapp_module.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "b1a4b77fd27fd5bf2810c0db1d4692093ae5b310992fb183e3817e2e3903891a",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_grid_client_certificate.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e0df90a4c30e50481afe5f508798187ecfbe31b1f5bb7e29e3d724d41203373f",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_grid_traffic_classes.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e27c214bb0ff1e9022a575647d1c05d7e0f7bf5c2b35fa49b576f7beb64ce79c",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_org_user_s3_key.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "d71cb0c2c53424b5ce28ead1b06f9c3b3106c709e4d2f0b74879b0eef2d668b6",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_grid_certificate.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "6b10f9a744d3f78024f3b82f082338aed13f31b5aa71efa86f4ba8ed8cce3088",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_org_identity_federation.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "93bf629ba06db59b1c74bfda8422471a64d3804776207dc925b175ed3378ed9c",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_grid_account.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "a20270da62abd5c22c977081e1c4a11b9f96e39d56254711edb21f7c5cbe7f5b",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_grid_user.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "bc4be89bd9b2977d59fa51e1ef72b0522d2aad116e7dcb531ba0b214854d878b",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_org_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "a088c55bad0b8b4a1c5e19024ed922ff4388ab92b3edcdcd91353b27cdf3d05a",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_grid_ha_group.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "48017730eff1e3a04c47c9187488eb915b18ef9293ca06b9dd49832eb2877856",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_org_group.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "6b6e5bff028d0bd999f8b7a6998e59451c7bd4e5c0de63953ddd19ee13fafad5",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_grid_ntp.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "9e3659af27a8a5b26b5ff95eb407a1d0b393a74f22b8e9a66a762b75565dab69",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_grid_identity_federation.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "279f42f668b563bf22aa79be77fd3b90de7dab0a8bccbfe4773900acf820e64c",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_grid_group.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0fe8a0e6ce5aefad01638e493dda3964019360e0c04cd6aa3b1e432d586b00d9",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_org_user.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5766c6c0c639b848c6ce3007533059d570f66f9c48f477e0e915b3f5c90a96ae",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_grid_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "324308668d15dbc88894b4aea9f9838093d3dd38e09307c94038cff4d289a614",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_grid_gateway.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "036c8706d7ff3ca3a5b970e0fa4d10ba4d33f7cfcfa160acf868d5bb9dc51eaf",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_org_container.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "70b7ca86ed2853ca6fa99a5dc712770e342d1d5b95fea154a67d7c7db3df3c8c",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_grid_dns.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "ccc1e086f899279cce569309c183b358bce0620446fbc8602c94f3be1f5b793f",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/na_sg_grid_regions.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "7aa2472c25390e20db5437cb23e2824546854c18033b7e92e77eb627b92981a7",
+ "format": 1
+ },
+ {
+ "name": "tests",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/unit",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/unit/compat",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/unit/compat/unittest.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "cba95d18c5b39c6f49714eacf1ac77452c2e32fa087c03cf01aacd19ae597b0f",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/compat/builtins.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "ba13a350ade8ef804336f888d5883b8e54f8bddfb9d0fadc10277a8ca6540f4e",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/compat/__init__.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/compat/mock.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "d18988875cf9d824a5089d062935b2750d96e99e47925a2eb62c527d92c9cc13",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/requirements.txt",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "68a61b1d58a722f4ffabaa28da01c9837c93a582ea41c1bfb1c1fd54ea2d8fab",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_grid_dns.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "a96a6d0218e6e7764786af0bf8dc8fbb24c3b492d57a627a7cf827bb33b2c327",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_grid_regions.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "870928ef29b01babed679fb5d207eb997ed724aa97135528f45cfda0314705c3",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_org_group.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "ff42467a461b8a69a2a2895242852444c89e0471bc0101ae10f99bbcf54528cb",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_grid_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "8b713f3a28e2948c42996d06cc69f316ed2576999d2372ed354c474e0a52472e",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_org_user.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "c7fa1a8212a01dd81411129c7a31048d3e154c9729a0ac28a8016ace1fbd6e87",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_grid_ntp.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "7299330b0ddfa005c4976c10b3c564b4c84c5f3cc620d566f9baa0ebe63460b1",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_grid_group.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "15ad7caefc2c6fc3c9e473d176e032473f2065dfb249f3524263ec6129b61e6c",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_grid_identity_federation.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3943d740fc467010966238cce4d10aa484ea70d6e689704043cee7344e1916b5",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_grid_gateway.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "c87e92555fb3aafaa82e197910b8fa61976ce358e220da2e0968469ea0df505c",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_grid_client_certificate.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "1ab3f4d2b408d5943dfb334ed64729ba5b9a2974c78ef1c2cb48622289181e52",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_grid_certificate.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "d046329c9ba078773c442a8bc47b00c76b49eac762f05f1b43789689056e295a",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_org_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3d167cae9bf3aebf2b3b8b6b2736701a37e0c86ebae8ad89223deca373d4e4dd",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_grid_account.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "eebfa82a09b7b3413d77749c1afc10a69cde0d90e940b0d454af807cebc476be",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_org_identity_federation.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "cda86d8e2632609873d7ccda496ca69e0884ba82712418e23148025f0f3a5482",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_grid_traffic_classes.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "4ec3ad3f3716e69cd39a3dddbbffc1796d415b4a8a729f5d61289a3f39967b18",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_grid_ha_group.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "a0782c6e1a6a9185cf4724da8d7e4363f6af13934efd4152779fcbe9aa05c2b7",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_org_container.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "9593869384f8f9f0f0de69e63f72a90de414e858dbfead681966e54b9ebd4b23",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_org_user_s3_key.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "d39947658ff5fdbc740275959f4fc82fcbdfd84553b771d78632d461e1638b4f",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/modules/test_na_sg_grid_user.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "614c50a7ef9b7fe296025441e0350b78cb5e381f04d336f1cb49512d60404605",
+ "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": "fe82353b93dc1bedb4d4f8695b23d6a33537fd0401c406547d132d4497ff7ca2",
+ "format": 1
+ },
+ {
+ "name": "changelogs",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "changelogs/fragments",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "changelogs/fragments/github-66.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "49dfee10361876355f72737bd6f8d86ce6cea258bac589e20ec31125d6d4023a",
+ "format": 1
+ },
+ {
+ "name": "changelogs/fragments/github-10.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "82584dd77f4f48618a3ae519fdfe9347774691201e4fd98868d21cc782820ecd",
+ "format": 1
+ },
+ {
+ "name": "changelogs/fragments/20.10.0.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "2567ca0de5c5aa2b12772cf077003ea361b46b046e7fff165fbfb901dc86ff24",
+ "format": 1
+ },
+ {
+ "name": "changelogs/fragments/21.9.0.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "7b2062ed359ef484360534a91969fdb39e59f741cd5aa96b18e08de79bef81f1",
+ "format": 1
+ },
+ {
+ "name": "changelogs/fragments/20.7.0.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e197a5a1f5b3a2e38dfd7456429e59a3a01a8f80e86e445ff1b7d5b5acf7dc3e",
+ "format": 1
+ },
+ {
+ "name": "changelogs/fragments/github-8.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e99536aa20608eaee59a0f0bc586425750a4561b39b78fa50722be660f4333a3",
+ "format": 1
+ },
+ {
+ "name": "changelogs/fragments/21.11.1.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "c392f498059c8b2db2e7756553dde51d153bb7003f22c379641746e0bcf26188",
+ "format": 1
+ },
+ {
+ "name": "changelogs/fragments/20.6.1.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "54144fc957437d09f4efd29f0dbfd18cfe40e21d7196e1c0d9dca35acc644a70",
+ "format": 1
+ },
+ {
+ "name": "changelogs/fragments/21.6.0.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "1f058dcc3961d0dc37d26b6a3ea6aefd477378bb51f8bdbe0595c2bf1c145b73",
+ "format": 1
+ },
+ {
+ "name": "changelogs/fragments/21.11.0.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "b05d16dd6f1c82d0b43033d464d48f2c487cc90195fd1f0a8d79c4b8f97560a5",
+ "format": 1
+ },
+ {
+ "name": "changelogs/fragments/DEVOPS-4416.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "6742492ce83786ffcddc12818ef3771ef915123fbe3b0518a101044435701af3",
+ "format": 1
+ },
+ {
+ "name": "changelogs/config.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3c330af70628d6a33029dadb8c4e4aac81eb5e82946651f804cff46bd0736cbe",
+ "format": 1
+ },
+ {
+ "name": "changelogs/changelog.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "2d6b62b96b1a730bbb09d926e37a0dc4f1d3cf9f4218e8a2feb4a00c30c66171",
+ "format": 1
+ },
+ {
+ "name": "README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "1fb8b745f8becd6ea2348808e1979a31486ab9357ec1197cb3136c2727d712b2",
+ "format": 1
+ },
+ {
+ "name": "COPYING",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "c53a65c2fd561c87eaabf1072ef5dcab8653042bc15308465f52413585eb6271",
+ "format": 1
+ },
+ {
+ "name": ".github",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": ".github/workflows",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": ".github/workflows/coverage.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "faf067634d432b31207f4ad48bdc037d2ec518a7df4377cb0533126fe9a50a21",
+ "format": 1
+ },
+ {
+ "name": ".github/workflows/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "07c19a69adcb2c9c482cadf8785c3bc0bf621ff161a592b48db9458e0673b1c5",
+ "format": 1
+ },
+ {
+ "name": "CHANGELOG.rst",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "131e7c4ca2ddb153d23af2174d262f4b0ae62079e892ecf36f062e929d43f729",
+ "format": 1
+ }
+ ],
+ "format": 1
+} \ No newline at end of file
diff --git a/ansible_collections/netapp/storagegrid/MANIFEST.json b/ansible_collections/netapp/storagegrid/MANIFEST.json
new file mode 100644
index 000000000..53238fc0d
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/MANIFEST.json
@@ -0,0 +1,32 @@
+{
+ "collection_info": {
+ "namespace": "netapp",
+ "name": "storagegrid",
+ "version": "21.11.1",
+ "authors": [
+ "NetApp Ansible Team <ng-ansibleteam@netapp.com>"
+ ],
+ "readme": "README.md",
+ "tags": [
+ "storage",
+ "netapp",
+ "storagegrid"
+ ],
+ "description": "NetApp StorageGRID Collection",
+ "license": [],
+ "license_file": "COPYING",
+ "dependencies": {},
+ "repository": "https://github.com/ansible-collections/netapp.storagegrid",
+ "documentation": null,
+ "homepage": "https://netapp.io/configuration-management-and-automation/",
+ "issues": null
+ },
+ "file_manifest_file": {
+ "name": "FILES.json",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "c9021a8a169b205830d9914712b6c039123456973f905ab635b85e37192f132c",
+ "format": 1
+ },
+ "format": 1
+} \ No newline at end of file
diff --git a/ansible_collections/netapp/storagegrid/README.md b/ansible_collections/netapp/storagegrid/README.md
new file mode 100644
index 000000000..582a77329
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/README.md
@@ -0,0 +1,199 @@
+![example workflow](https://github.com/ansible-collections/netapp.storagegrid/actions/workflows/main.yml/badge.svg)
+[![codecov](https://codecov.io/gh/ansible-collections/netapp.storagegrid/branch/main/graph/badge.svg?token=weBYkksxSi)](https://codecov.io/gh/ansible-collections/netapp.storagegrid)
+[![Discord](https://img.shields.io/discord/855068651522490400)](https://discord.gg/NetApp)
+
+
+=============================================================
+
+ netapp.storagegrid
+
+ NetApp StorageGRID Collection
+
+ Copyright (c) 2020 NetApp, Inc. All rights reserved.
+ Specifications subject to change without notice.
+
+=============================================================
+
+# Installation
+
+```bash
+ansible-galaxy collection install netapp.storagegrid
+```
+To use this collection add the following to the top of your playbook.
+```
+collections:
+ - netapp.storagegrid
+```
+
+# Usage
+
+Each of the StorageGRID modules require an `auth_token` parameter to be specified. This can be obtained by executing a `uri` task against the StorageGRID Authorization API endpoint and registering the output as the first item in a Playbook.
+
+If you are performing a Tenant operation, ensure that the `accountId` parameter is also specified in the URI body and set to the Tenant Account ID. For example, `"accountId": "01234567890123456789"`
+
+```yaml
+- name: Get Grid Authorization token
+ uri:
+ url: "https://sgadmin.example.com/api/v3/authorize"
+ method: POST
+ body: {
+ "username": "root",
+ "password": "storagegrid123",
+ "cookie": false,
+ "csrfToken": false
+ }
+ body_format: json
+ validate_certs: false
+ register: auth
+```
+
+Subsequent tasks can leverage the registered auth token.
+
+```yaml
+- name: Create a StorageGRID Tenant Account
+ netapp.storagegrid.na_sg_grid_account:
+ api_url: "https://sgadmin.example.com"
+ auth_token: "{{ auth.json.data }}"
+ validate_certs: false
+ state: present
+ name: AnsibleTenant
+ protocol: s3
+ management: true
+ use_own_identity_source: true
+ allow_platform_services: true
+ password: "mytenantrootpassword"
+ quota_size: 10
+```
+
+# Versioning
+
+[Releasing, Versioning and Deprecation](https://github.com/ansible-collections/netapp/issues/93)
+
+# Need help
+
+Join our [Discord](https://discord.gg/NetApp)
+
+# Code of Conduct
+
+This collection follows the [Ansible project's Code of Conduct](https://docs.ansible.com/ansible/devel/community/code_of_conduct.html).
+
+# Release Notes
+
+## 21.11.1
+
+### Bug Fixes
+ - na_sg_org_container - fix versioning not enabled on initial bucket creation.
+
+## 21.11.0
+
+### Minor Changes
+ - na_sg_org_container - supports versioning configuration for S3 buckets available in StorageGRID 11.6+.
+
+### New Modules
+ - na_sg_grid_client_certificate - Manage Client Certificates on StorageGRID.
+
+## 21.10.0
+
+### Minor Changes
+ - na_sg_grid_gateway - supports specifying HA Groups by name or UUID.
+
+### Bug Fixes
+ - na_sg_org_group - fixed behaviour where update to ``s3_policy`` is ignored if ``management_policy`` is set.
+
+### New Modules
+ - na_sg_grid_ha_group - Manage high availability (HA) group configuration on StorageGRID.
+ - na_sg_grid_traffic_classes - Manage Traffic Classification Policy configuration on StorageGRID.
+
+## 21.9.0
+
+### Minor Changes
+ - na_sg_grid_gateway - supports load balancer endpoint binding available in StorageGRID 11.5+.
+ - na_sg_org_container - supports creation of S3 Object Lock buckets available in StorageGRID 11.5+.
+
+### Bug Fixes
+ - na_sg_grid_gateway - existing endpoints matched by ``name`` and ``port``.
+ - na_sg_grid_account - minor documentation fix.
+
+## 21.8.0
+
+### Minor Changes
+ - all modules - enable usage of Ansible module group defaults - for Ansible 2.12+.
+
+## 21.7.0
+
+### New Modules
+
+- na_sg_grid_gateway: Manage Load balancer (gateway) endpoints
+
+### Minor Changes
+- Updated documentation - added RETURN block for each module
+
+## 21.6.0
+
+### New Modules
+
+- na_sg_grid_certificate: Manage the Storage API and Grid Management certificates on StorageGRID.
+- na_sg_grid_identity_federation: Manage Grid identity federation.
+- na_sg_org_identity_federation: Manage Tenant identity federation.
+
+### Minor Changes
+- na_sg_org_container - supports deletion of buckets when `state` is set to `absent`.
+
+### Bug Fixes
+- na_sg_org_container - fix issue with applying compliance settings on buckets.
+
+## 20.11.0
+
+### New Modules
+
+- na_sg_grid_info: Gather StorageGRID Grig subset information
+- na_sg_org_info: Gather StorageGRID Org subset information
+
+### Minor Changes
+
+- na_sg_grid_account: new option `root_access_account` for granting initial root access permissions for the tenant to an existing federated group
+
+## 20.10.0
+
+### Breaking Changes
+
+This version introduces a breaking change. All modules have been renamed from `nac_sg_*` to `na_sg_*`. Playbooks and Roles must be updated to match.
+
+### Bug Fixes
+
+- na_sg_grid_account: fixed documentation issue.
+- na_sg_grid_account: added `no_log` flag to password fields
+- na_sg_grid_group: fixed group name parsing
+- na_sg_org_group: fixed group name parsing
+
+### New Options
+
+- na_sg_grid_account: new option `update_password` for managing Tenant Account root password changes
+- na_sg_org_user: new option `password` and `update_password` for setting or updating Tenant User passwords
+- na_sg_grid_user: new option `password` and `update_password` for setting or updating Grid Admin User passwords
+
+## 20.6.1
+
+### Minor Changes
+- Fixed documentation issue in README.md
+
+### Bug Fixes
+- nac_sg_org_container: fixed documentation issue.
+
+## 20.6.0
+
+Initial release of NetApp StorageGRID Ansible modules
+
+### New Modules
+
+- nac_sg_grid_account: create/modify/delete Tenant account
+- nac_sg_grid_dns: set Grid DNS servers
+- nac_sg_grid_group: create/modify/delete Grid admin group
+- nac_sg_grid_ntp: set Grid NTP servers
+- nac_sg_grid_regions: set Grid Regions
+- nac_sg_grid_user: create/modify/delete Grid admin user
+- nac_sg_org_container: create S3 bucket
+- nac_sg_org_group: create/modify/delete Tenant group
+- nac_sg_org_user: create/modify/delete Tenant user
+- nac_sg_org_user_s3_key: create/delete S3 key
+
diff --git a/ansible_collections/netapp/storagegrid/changelogs/changelog.yaml b/ansible_collections/netapp/storagegrid/changelogs/changelog.yaml
new file mode 100644
index 000000000..288cbdc34
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/changelogs/changelog.yaml
@@ -0,0 +1,171 @@
+ancestor: null
+releases:
+ 20.10.0:
+ changes:
+ breaking_changes:
+ - 'This version introduces a breaking change.
+
+ All modules have been renamed from ``nac_sg_*`` to ``na_sg_*``.
+
+ Playbooks and Roles must be updated to match.'
+ bugfixes:
+ - na_sg_grid_account - added ``no_log`` flag to password fields.
+ - na_sg_grid_account - fixed documentation issue.
+ - na_sg_grid_group - fixed group name parsing.
+ - na_sg_org_group - fixed group name parsing.
+ minor_changes:
+ - na_sg_grid_account - new option ``update_password`` for managing Tenant Account
+ root password changes.
+ - na_sg_grid_user - new option ``password`` and ``update_password`` for setting
+ or updating Grid Admin User passwords.
+ - na_sg_org_user - new option ``password`` and ``update_password`` for setting
+ or updating Tenant User passwords.
+ fragments:
+ - 20.10.0.yaml
+ release_date: '2020-10-15'
+ 20.11.0:
+ changes:
+ minor_changes:
+ - na_sg_grid_account - New option ``root_access_account`` for granting initial
+ root access permissions for the tenant to an existing federated group
+ fragments:
+ - github-66.yaml
+ modules:
+ - description: NetApp StorageGRID Grid information gatherer
+ name: na_sg_grid_info
+ namespace: ''
+ - description: NetApp StorageGRID Org information gatherer
+ name: na_sg_org_info
+ namespace: ''
+ release_date: '2020-11-18'
+ 20.6.0:
+ modules:
+ - description: NetApp StorageGRID Manage Tenant account.
+ name: nac_sg_grid_account
+ namespace: ''
+ - description: NetApp StorageGRID Manage Grid DNS servers.
+ name: nac_sg_grid_dns
+ namespace: ''
+ - description: NetApp StorageGRID Manage Grid admin group.
+ name: nac_sg_grid_group
+ namespace: ''
+ - description: NetApp StorageGRID Manage Grid NTP servers.
+ name: nac_sg_grid_ntp
+ namespace: ''
+ - description: NetApp StorageGRID Manage Grid Regions.
+ name: nac_sg_grid_regions
+ namespace: ''
+ - description: NetApp StorageGRID Manage Grid admin user.
+ name: nac_sg_grid_user
+ namespace: ''
+ - description: NetApp StorageGRID Manage S3 bucket.
+ name: nac_sg_org_container
+ namespace: ''
+ - description: NetApp StorageGRID Manage Tenant group.
+ name: nac_sg_org_group
+ namespace: ''
+ - description: NetApp StorageGRID Manage Tenant user.
+ name: nac_sg_org_user
+ namespace: ''
+ - description: NetApp StorageGRID Manage S3 key.
+ name: nac_sg_org_user_s3_key
+ namespace: ''
+ release_date: '2020-06-09'
+ 20.6.1:
+ changes:
+ bugfixes:
+ - nac_sg_org_container - fixed documentation issue.
+ minor_changes:
+ - Fixed documentation issue in README.md
+ fragments:
+ - 20.6.1.yaml
+ release_date: '2020-06-09'
+ 21.10.0:
+ changes:
+ bugfixes:
+ - na_sg_org_group - fixed behaviour where update to ``s3_policy`` is ignored
+ if ``management_policy`` is set.
+ minor_changes:
+ - na_sg_grid_gateway - supports specifying HA Groups by name or UUID.
+ fragments:
+ - github-10.yaml
+ - github-8.yaml
+ modules:
+ - description: Manage high availability (HA) group configuration on StorageGRID.
+ name: na_sg_grid_ha_group
+ namespace: ''
+ - description: Manage Traffic Classification Policy configuration on StorageGRID.
+ name: na_sg_grid_traffic_classes
+ namespace: ''
+ release_date: '2022-03-17'
+ 21.11.0:
+ changes:
+ minor_changes:
+ - na_sg_org_container - supports versioning configuration for S3 buckets available
+ in StorageGRID 11.6+.
+ fragments:
+ - 21.11.0.yaml
+ modules:
+ - description: Manage Client Certificates on StorageGRID.
+ name: na_sg_grid_client_certificate
+ namespace: ''
+ release_date: '2022-09-06'
+ 21.11.1:
+ changes:
+ bugfixes:
+ - na_sg_org_container - fix versioning not enabled on initial bucket creation.
+ fragments:
+ - 21.11.1.yaml
+ release_date: '2022-09-23'
+ 21.6.0:
+ changes:
+ bugfixes:
+ - na_sg_org_container - fix issue with applying compliance settings on buckets.
+ minor_changes:
+ - na_sg_org_container - supports deletion of buckets when ``state`` is set to
+ ``absent``.
+ fragments:
+ - 21.6.0.yaml
+ modules:
+ - description: Manage the Storage API and Grid Management certificates on StorageGRID.
+ name: na_sg_grid_certificate
+ namespace: ''
+ - description: NetApp StorageGRID manage Grid identity federation.
+ name: na_sg_grid_identity_federation
+ namespace: ''
+ - description: NetApp StorageGRID manage Tenant identity federation.
+ name: na_sg_org_identity_federation
+ namespace: ''
+ release_date: '2021-06-16'
+ 21.7.0:
+ changes:
+ minor_changes:
+ - Updated documentation - added RETURN block for each module
+ fragments:
+ - 20.7.0.yaml
+ modules:
+ - description: Manage Load balancer (gateway) endpoints on StorageGRID.
+ name: na_sg_grid_gateway
+ namespace: ''
+ release_date: '2021-10-05'
+ 21.8.0:
+ changes:
+ minor_changes:
+ - PR2 - allow usage of Ansible module group defaults - for Ansible 2.12+.
+ fragments:
+ - DEVOPS-4416.yaml
+ release_date: '2021-11-11'
+ 21.9.0:
+ changes:
+ bugfixes:
+ - na_sg_grid_account - minor documentation fix.
+ - na_sg_grid_gateway - existing endpoints matched by ``name`` and ``port``.
+ minor_changes:
+ - PR2 - allow usage of Ansible module group defaults - for Ansible 2.12+.
+ - na_sg_grid_gateway - supports load balancer endpoint binding available in
+ StorageGRID 11.5+.
+ - na_sg_org_container - supports creation of S3 Object Lock buckets available
+ in StorageGRID 11.5+.
+ fragments:
+ - 21.9.0.yaml
+ release_date: '2021-12-17'
diff --git a/ansible_collections/netapp/storagegrid/changelogs/config.yaml b/ansible_collections/netapp/storagegrid/changelogs/config.yaml
new file mode 100644
index 000000000..871e634c1
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/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: true
+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: NetApp StorageGRID Collection
+trivial_section_name: trivial
+use_fqcn: true
diff --git a/ansible_collections/netapp/storagegrid/changelogs/fragments/20.10.0.yaml b/ansible_collections/netapp/storagegrid/changelogs/fragments/20.10.0.yaml
new file mode 100644
index 000000000..350ee15dc
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/changelogs/fragments/20.10.0.yaml
@@ -0,0 +1,14 @@
+breaking_changes:
+ - |
+ This version introduces a breaking change.
+ All modules have been renamed from ``nac_sg_*`` to ``na_sg_*``.
+ Playbooks and Roles must be updated to match.
+minor_changes:
+ - na_sg_grid_account - new option ``update_password`` for managing Tenant Account root password changes.
+ - na_sg_org_user - new option ``password`` and ``update_password`` for setting or updating Tenant User passwords.
+ - na_sg_grid_user - new option ``password`` and ``update_password`` for setting or updating Grid Admin User passwords.
+bugfixes:
+ - na_sg_grid_account - fixed documentation issue.
+ - na_sg_grid_account - added ``no_log`` flag to password fields.
+ - na_sg_grid_group - fixed group name parsing.
+ - na_sg_org_group - fixed group name parsing.
diff --git a/ansible_collections/netapp/storagegrid/changelogs/fragments/20.6.1.yaml b/ansible_collections/netapp/storagegrid/changelogs/fragments/20.6.1.yaml
new file mode 100644
index 000000000..3d1bb11a0
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/changelogs/fragments/20.6.1.yaml
@@ -0,0 +1,4 @@
+minor_changes:
+ - Fixed documentation issue in README.md
+bugfixes:
+ - nac_sg_org_container - fixed documentation issue.
diff --git a/ansible_collections/netapp/storagegrid/changelogs/fragments/20.7.0.yaml b/ansible_collections/netapp/storagegrid/changelogs/fragments/20.7.0.yaml
new file mode 100644
index 000000000..a91168e87
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/changelogs/fragments/20.7.0.yaml
@@ -0,0 +1,2 @@
+minor_changes:
+ - Updated documentation - added RETURN block for each module
diff --git a/ansible_collections/netapp/storagegrid/changelogs/fragments/21.11.0.yaml b/ansible_collections/netapp/storagegrid/changelogs/fragments/21.11.0.yaml
new file mode 100644
index 000000000..a8fbff2df
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/changelogs/fragments/21.11.0.yaml
@@ -0,0 +1,2 @@
+minor_changes:
+ - na_sg_org_container - supports versioning configuration for S3 buckets available in StorageGRID 11.6+.
diff --git a/ansible_collections/netapp/storagegrid/changelogs/fragments/21.11.1.yaml b/ansible_collections/netapp/storagegrid/changelogs/fragments/21.11.1.yaml
new file mode 100644
index 000000000..229a6c51f
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/changelogs/fragments/21.11.1.yaml
@@ -0,0 +1,2 @@
+bugfixes:
+ - na_sg_org_container - fix versioning not enabled on initial bucket creation.
diff --git a/ansible_collections/netapp/storagegrid/changelogs/fragments/21.6.0.yaml b/ansible_collections/netapp/storagegrid/changelogs/fragments/21.6.0.yaml
new file mode 100644
index 000000000..6f8a660eb
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/changelogs/fragments/21.6.0.yaml
@@ -0,0 +1,4 @@
+minor_changes:
+ - na_sg_org_container - supports deletion of buckets when ``state`` is set to ``absent``.
+bugfixes:
+ - na_sg_org_container - fix issue with applying compliance settings on buckets.
diff --git a/ansible_collections/netapp/storagegrid/changelogs/fragments/21.9.0.yaml b/ansible_collections/netapp/storagegrid/changelogs/fragments/21.9.0.yaml
new file mode 100644
index 000000000..87aecb16c
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/changelogs/fragments/21.9.0.yaml
@@ -0,0 +1,6 @@
+minor_changes:
+ - na_sg_grid_gateway - supports load balancer endpoint binding available in StorageGRID 11.5+.
+ - na_sg_org_container - supports creation of S3 Object Lock buckets available in StorageGRID 11.5+.
+bugfixes:
+ - na_sg_grid_gateway - existing endpoints matched by ``name`` and ``port``.
+ - na_sg_grid_account - minor documentation fix.
diff --git a/ansible_collections/netapp/storagegrid/changelogs/fragments/DEVOPS-4416.yaml b/ansible_collections/netapp/storagegrid/changelogs/fragments/DEVOPS-4416.yaml
new file mode 100644
index 000000000..63514c5c5
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/changelogs/fragments/DEVOPS-4416.yaml
@@ -0,0 +1,2 @@
+minor_changes:
+ - PR2 - allow usage of Ansible module group defaults - for Ansible 2.12+.
diff --git a/ansible_collections/netapp/storagegrid/changelogs/fragments/github-10.yaml b/ansible_collections/netapp/storagegrid/changelogs/fragments/github-10.yaml
new file mode 100644
index 000000000..62e8845c6
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/changelogs/fragments/github-10.yaml
@@ -0,0 +1,2 @@
+minor_changes:
+ - na_sg_grid_gateway - supports specifying HA Groups by name or UUID. \ No newline at end of file
diff --git a/ansible_collections/netapp/storagegrid/changelogs/fragments/github-66.yaml b/ansible_collections/netapp/storagegrid/changelogs/fragments/github-66.yaml
new file mode 100644
index 000000000..a515be8c2
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/changelogs/fragments/github-66.yaml
@@ -0,0 +1,2 @@
+minor_changes:
+ - na_sg_grid_account - New option ``root_access_account`` for granting initial root access permissions for the tenant to an existing federated group
diff --git a/ansible_collections/netapp/storagegrid/changelogs/fragments/github-8.yaml b/ansible_collections/netapp/storagegrid/changelogs/fragments/github-8.yaml
new file mode 100644
index 000000000..f21ecbe46
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/changelogs/fragments/github-8.yaml
@@ -0,0 +1,2 @@
+bugfixes:
+ - na_sg_org_group - fixed behaviour where update to ``s3_policy`` is ignored if ``management_policy`` is set.
diff --git a/ansible_collections/netapp/storagegrid/meta/runtime.yml b/ansible_collections/netapp/storagegrid/meta/runtime.yml
new file mode 100644
index 000000000..4625ad259
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/meta/runtime.yml
@@ -0,0 +1,23 @@
+---
+requires_ansible: ">=2.9.10"
+action_groups:
+ netapp_storagegrid:
+ - na_sg_grid_account
+ - na_sg_grid_certificate
+ - na_sg_grid_client_certificate
+ - na_sg_grid_dns
+ - na_sg_grid_gateway
+ - na_sg_grid_group
+ - na_sg_grid_ha_group
+ - na_sg_grid_identity_federation
+ - na_sg_grid_info
+ - na_sg_grid_ntp
+ - na_sg_grid_regions
+ - na_sg_grid_traffic_classes
+ - na_sg_grid_user
+ - na_sg_org_container
+ - na_sg_org_group
+ - na_sg_org_identity_federation
+ - na_sg_org_info
+ - na_sg_org_user
+ - na_sg_org_user_s3_key
diff --git a/ansible_collections/netapp/storagegrid/plugins/doc_fragments/netapp.py b/ansible_collections/netapp/storagegrid/plugins/doc_fragments/netapp.py
new file mode 100644
index 000000000..8c06f9d99
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/doc_fragments/netapp.py
@@ -0,0 +1,41 @@
+# Copyright: (c) 2019, NetApp Ansible Team <ng-ansibleteam@netapp.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):
+
+ DOCUMENTATION = r"""
+options:
+ - See respective platform section for more details
+requirements:
+ - See respective platform section for more details
+notes:
+ - This is documentation for NetApp's StorageGRID modules.
+"""
+
+ # Documentation fragment for StorageGRID
+ SG = """
+options:
+ auth_token:
+ required: true
+ type: str
+ description:
+ - The authorization token for the API request
+ api_url:
+ required: true
+ type: str
+ description:
+ - The url to the StorageGRID Admin Node REST API.
+ validate_certs:
+ required: false
+ default: true
+ description:
+ - Should https certificates be validated?
+ type: bool
+notes:
+ - The modules prefixed with C(na_sg) are built to manage NetApp StorageGRID.
+"""
diff --git a/ansible_collections/netapp/storagegrid/plugins/module_utils/netapp.py b/ansible_collections/netapp/storagegrid/plugins/module_utils/netapp.py
new file mode 100644
index 000000000..9892ae7b5
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/module_utils/netapp.py
@@ -0,0 +1,211 @@
+# 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) 2020, NetApp Ansible Team <ng-ansibleteam@netapp.com>
+# 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
+
+import json
+import mimetypes
+import os
+import random
+
+from pprint import pformat
+from ansible.module_utils import six
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+from ansible.module_utils.six.moves.urllib.error import HTTPError, URLError
+from ansible.module_utils.urls import open_url
+from ansible.module_utils.api import basic_auth_argument_spec
+from ansible.module_utils._text import to_native
+
+COLLECTION_VERSION = "21.11.1"
+
+try:
+ import requests
+
+ HAS_REQUESTS = True
+except ImportError:
+ HAS_REQUESTS = False
+
+import ssl
+
+try:
+ from urlparse import urlparse, urlunparse
+except ImportError:
+ from urllib.parse import urlparse, urlunparse
+
+
+POW2_BYTE_MAP = dict(
+ # Here, 1 kb = 1024
+ bytes=1,
+ b=1,
+ kb=1024,
+ mb=1024**2,
+ gb=1024**3,
+ tb=1024**4,
+ pb=1024**5,
+ eb=1024**6,
+ zb=1024**7,
+ yb=1024**8,
+)
+
+
+def na_storagegrid_host_argument_spec():
+
+ return dict(
+ api_url=dict(required=True, type="str"),
+ validate_certs=dict(required=False, type="bool", default=True),
+ auth_token=dict(required=True, type="str", no_log=True),
+ )
+
+
+class SGRestAPI(object):
+ def __init__(self, module, timeout=60):
+ self.module = module
+ self.auth_token = self.module.params["auth_token"]
+ self.api_url = self.module.params["api_url"]
+ self.verify = self.module.params["validate_certs"]
+ self.timeout = timeout
+ self.check_required_library()
+ self.sg_version = dict(major=-1, minor=-1, full="", valid=False)
+
+ def check_required_library(self):
+ if not HAS_REQUESTS:
+ self.module.fail_json(msg=missing_required_lib("requests"))
+
+ def send_request(self, method, api, params, json=None):
+ """send http request and process reponse, including error conditions"""
+ url = "%s/%s" % (self.api_url, api)
+ status_code = None
+ content = None
+ json_dict = None
+ json_error = None
+ error_details = None
+ headers = {
+ "Content-type": "application/json",
+ "Authorization": self.auth_token,
+ "Cache-Control": "no-cache",
+ }
+
+ def get_json(response):
+ """extract json, and error message if present"""
+ try:
+ json = response.json()
+
+ except ValueError:
+ return None, None
+ success_code = [200, 201, 202, 204]
+ if response.status_code not in success_code:
+ error = json.get("message")
+ else:
+ error = None
+ return json, error
+
+ try:
+ response = requests.request(
+ method,
+ url,
+ headers=headers,
+ timeout=self.timeout,
+ json=json,
+ verify=self.verify,
+ params=params,
+ )
+ status_code = response.status_code
+ # If the response was successful, no Exception will be raised
+ json_dict, json_error = get_json(response)
+ except requests.exceptions.HTTPError as err:
+ __, json_error = get_json(response)
+ if json_error is None:
+ error_details = str(err)
+ except requests.exceptions.ConnectionError as err:
+ error_details = str(err)
+ except Exception as err:
+ error_details = str(err)
+ if json_error is not None:
+ error_details = json_error
+
+ return json_dict, error_details
+
+ # If an error was reported in the json payload, it is handled below
+ def get(self, api, params=None):
+ method = "GET"
+ return self.send_request(method, api, params)
+
+ def post(self, api, data, params=None):
+ method = "POST"
+ return self.send_request(method, api, params, json=data)
+
+ def patch(self, api, data, params=None):
+ method = "PATCH"
+ return self.send_request(method, api, params, json=data)
+
+ def put(self, api, data, params=None):
+ method = "PUT"
+ return self.send_request(method, api, params, json=data)
+
+ def delete(self, api, data, params=None):
+ method = "DELETE"
+ return self.send_request(method, api, params, json=data)
+
+ def get_sg_product_version(self, api_root="grid"):
+ method = "GET"
+ api = "api/v3/%s/config/product-version" % api_root
+ message, error = self.send_request(method, api, params={})
+ if error:
+ self.module.fail_json(msg=error)
+ self.set_version(message)
+
+ def set_version(self, message):
+ try:
+ product_version = message.get("data", "not found").get("productVersion", "not_found")
+ except AttributeError:
+ self.sg_version["valid"] = False
+ return
+
+ self.sg_version["major"], self.sg_version["minor"] = list(map(int, product_version.split(".")[0:2]))
+ self.sg_version["full"] = product_version
+ self.sg_version["valid"] = True
+
+ def get_sg_version(self):
+ if self.sg_version["valid"]:
+ return self.sg_version["major"], self.sg_version["minor"]
+ return -1, -1
+
+ def meets_sg_minimum_version(self, minimum_major, minimum_minor):
+ return self.get_sg_version() >= (minimum_major, minimum_minor)
+
+ def requires_sg_version(self, module_or_option, version):
+ return "%s requires StorageGRID %s or later." % (module_or_option, version)
+
+ def fail_if_not_sg_minimum_version(self, module_or_option, minimum_major, minimum_minor):
+ version = self.get_sg_version()
+ if version < (minimum_major, minimum_minor):
+ msg = "Error: " + self.requires_sg_version(module_or_option, "%d.%d" % (minimum_major, minimum_minor))
+ msg += " Found: %s.%s." % version
+ self.module.fail_json(msg=msg)
diff --git a/ansible_collections/netapp/storagegrid/plugins/module_utils/netapp_module.py b/ansible_collections/netapp/storagegrid/plugins/module_utils/netapp_module.py
new file mode 100644
index 000000000..f562938cf
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/module_utils/netapp_module.py
@@ -0,0 +1,237 @@
+# 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) 2018, Laurent Nicolas <laurentn@netapp.com>
+# 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.
+
+""" Support class for NetApp ansible modules """
+
+from __future__ import absolute_import, division, print_function
+
+from copy import deepcopy
+
+__metaclass__ = type
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+
+
+def cmp(obj1, obj2):
+ """
+ Python 3 does not have a cmp function, this will do the cmp.
+ :param obj1: first object to check
+ :param obj2: second object to check
+ :return:
+ """
+ # convert to lower case for string comparison.
+ if obj1 is None:
+ return -1
+ if isinstance(obj1, str) and isinstance(obj2, str):
+ obj1 = obj1.lower()
+ obj2 = obj2.lower()
+ # if list has string element, convert string to lower case.
+ if isinstance(obj1, list) and isinstance(obj2, list):
+ obj1 = [x.lower() if isinstance(x, str) else x for x in obj1]
+ obj2 = [x.lower() if isinstance(x, str) else x for x in obj2]
+ obj1.sort()
+ obj2.sort()
+ return (obj1 > obj2) - (obj1 < obj2)
+
+
+class NetAppModule(object):
+ """
+ Common class for NetApp modules
+ set of support functions to derive actions based
+ on the current state of the system, and a desired state
+ """
+
+ def __init__(self):
+ self.log = list()
+ self.changed = False
+ self.parameters = {"name": "not initialized"}
+
+ def set_parameters(self, ansible_params):
+ self.parameters = dict()
+ for param in ansible_params:
+ if ansible_params[param] is not None:
+ self.parameters[param] = ansible_params[param]
+ return self.parameters
+
+ def get_cd_action(self, current, desired):
+ """ takes a desired state and a current state, and return an action:
+ create, delete, None
+ eg:
+ is_present = 'absent'
+ some_object = self.get_object(source)
+ if some_object is not None:
+ is_present = 'present'
+ action = cd_action(current=is_present, desired = self.desired.state())
+ """
+ if "state" in desired:
+ desired_state = desired["state"]
+ else:
+ desired_state = "present"
+
+ if current is None and desired_state == "absent":
+ return None
+ if current is not None and desired_state == "present":
+ return None
+ # change in state
+ self.changed = True
+ if current is not None:
+ return "delete"
+ return "create"
+
+ def compare_and_update_values(self, current, desired, keys_to_compare):
+ updated_values = dict()
+ is_changed = False
+ for key in keys_to_compare:
+ if key in current:
+ if key in desired and desired[key] is not None:
+ if current[key] != desired[key]:
+ updated_values[key] = desired[key]
+ is_changed = True
+ else:
+ updated_values[key] = current[key]
+ else:
+ updated_values[key] = current[key]
+
+ return updated_values, is_changed
+
+ @staticmethod
+ def check_keys(current, desired):
+ ''' TODO: raise an error if keys do not match
+ with the exception of:
+ new_name, state in desired
+ '''
+
+ def is_rename_action(self, source, target):
+ """ takes a source and target object, and returns True
+ if a rename is required
+ eg:
+ source = self.get_object(source_name)
+ target = self.get_object(target_name)
+ action = is_rename_action(source, target)
+ :return: None for error, True for rename action, False otherwise
+ """
+ if source is None and target is None:
+ # error, do nothing
+ # cannot rename an non existent resource
+ # alternatively we could create B
+ return None
+ if source is not None and target is not None:
+ # error, do nothing
+ # idempotency (or) new_name_is_already_in_use
+ # alternatively we could delete B and rename A to B
+ return False
+ if source is None and target is not None:
+ # do nothing, maybe the rename was already done
+ return False
+ # source is not None and target is None:
+ # rename is in order
+ self.changed = True
+ return True
+
+ @staticmethod
+ def compare_lists(current, desired, get_list_diff):
+ ''' compares two lists and return a list of elements that are either the desired elements or elements that are
+ modified from the current state depending on the get_list_diff flag
+ :param: current: current item attribute in ONTAP
+ :param: desired: attributes from playbook
+ :param: get_list_diff: specifies whether to have a diff of desired list w.r.t current list for an attribute
+ :return: list of attributes to be modified
+ :rtype: list
+ '''
+ current_copy = deepcopy(current)
+ desired_copy = deepcopy(desired)
+
+ # get what in desired and not in current
+ desired_diff_list = list()
+ for item in desired:
+ if item in current_copy:
+ current_copy.remove(item)
+ else:
+ desired_diff_list.append(item)
+
+ # get what in current but not in desired
+ current_diff_list = []
+ for item in current:
+ if item in desired_copy:
+ desired_copy.remove(item)
+ else:
+ current_diff_list.append(item)
+
+ if desired_diff_list or current_diff_list:
+ # there are changes
+ if get_list_diff:
+ return desired_diff_list
+ else:
+ return desired
+ else:
+ return None
+
+ def get_modified_attributes(self, current, desired, get_list_diff=False):
+ ''' takes two dicts of attributes and return a dict of attributes that are
+ not in the current state
+ It is expected that all attributes of interest are listed in current and
+ desired.
+ :param: current: current attributes on StorageGRID
+ :param: desired: attributes from playbook
+ :param: get_list_diff: specifies whether to have a diff of desired list w.r.t current list for an attribute
+ :return: dict of attributes to be modified
+ :rtype: dict
+ NOTE: depending on the attribute, the caller may need to do a modify or a
+ different operation (eg move volume if the modified attribute is an
+ aggregate name)
+ '''
+ # if the object does not exist, we can't modify it
+ modified = {}
+ if current is None:
+ return modified
+
+ # error out if keys do not match
+ self.check_keys(current, desired)
+
+ # collect changed attributes
+ for key, value in current.items():
+ if key in desired and desired[key] is not None:
+ if isinstance(value, list):
+ modified_list = self.compare_lists(value, desired[key], get_list_diff) # get modified list from current and desired
+ if modified_list is not None:
+ modified[key] = modified_list
+ elif isinstance(value, dict):
+ modified_dict = self.get_modified_attributes(value, desired[key])
+ if modified_dict:
+ modified[key] = modified_dict
+ else:
+ try:
+ result = cmp(value, desired[key])
+ except TypeError as exc:
+ raise TypeError("%s, key: %s, value: %s, desired: %s" % (repr(exc), key, repr(value), repr(desired[key])))
+ else:
+ if result != 0:
+ modified[key] = desired[key]
+ if modified:
+ self.changed = True
+ return modified
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_account.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_account.py
new file mode 100644
index 000000000..88943c082
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_account.py
@@ -0,0 +1,458 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage Accounts"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+module: na_sg_grid_account
+short_description: NetApp StorageGRID manage accounts.
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '20.6.0'
+author: NetApp Ansible Team (@joshedmonds) <ng-ansibleteam@netapp.com>
+description:
+- Create, Update, Delete Tenant Accounts on NetApp StorageGRID.
+options:
+ state:
+ description:
+ - Whether the specified account should exist or not.
+ - Required for all operations.
+ type: str
+ choices: ['present', 'absent']
+ default: present
+ name:
+ description:
+ - Name of the tenant.
+ - Required for create or modify operation.
+ type: str
+ account_id:
+ description:
+ - Account Id of the tenant.
+ - May be used for modify or delete operation.
+ type: str
+ protocol:
+ description:
+ - Object Storage protocol used by the tenancy.
+ - Required for create operation.
+ type: str
+ choices: ['s3', 'swift']
+ management:
+ description:
+ - Whether the tenant can login to the StorageGRID tenant portal.
+ type: bool
+ default: true
+ use_own_identity_source:
+ description:
+ - Whether the tenant account should configure its own identity source.
+ type: bool
+ allow_platform_services:
+ description:
+ - Allows tenant to use platform services features such as CloudMirror.
+ type: bool
+ root_access_group:
+ description:
+ - Existing federated group to have initial Root Access permissions for the tenant.
+ - Must begin with C(federated-group/)
+ type: str
+ version_added: 20.11.0
+ quota_size:
+ description:
+ - Quota to apply to the tenant specified in I(quota_size_unit).
+ - If you intend to have no limits, assign C(0).
+ type: int
+ default: 0
+ quota_size_unit:
+ description:
+ - The unit used to interpret the size parameter.
+ choices: ['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb']
+ type: str
+ default: 'gb'
+ password:
+ description:
+ - Root password for tenant account.
+ - Requires root privilege.
+ type: str
+ update_password:
+ description:
+ - Choose when to update the password.
+ - When set to C(always), the password will always be updated.
+ - When set to C(on_create) the password will only be set upon a new user creation.
+ default: on_create
+ choices:
+ - on_create
+ - always
+ type: str
+"""
+
+EXAMPLES = """
+ - name: create a tenant account
+ netapp.storagegrid.na_sg_grid_account:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ name: storagegrid-tenant-1
+ protocol: s3
+ management: true
+ use_own_identity_source: false
+ allow_platform_services: false
+ password: "tenant-password"
+ quota_size: 0
+
+ - name: update a tenant account
+ netapp.storagegrid.na_sg_grid_account:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ name: storagegrid-tenant-1
+ protocol: s3
+ management: true
+ use_own_identity_source: false
+ allow_platform_services: true
+ password: "tenant-password"
+ quota_size: 10240
+
+ - name: delete a tenant account
+ netapp.storagegrid.na_sg_grid_account:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: absent
+ name: storagegrid-tenant-1
+ protocol: s3
+"""
+
+RETURN = """
+resp:
+ description: Returns information about the StorageGRID tenant account.
+ returned: success
+ type: dict
+ sample: {
+ "name": "Example Account",
+ "capabilities": ["management", "s3"],
+ "policy": {
+ "useAccountIdentitySource": true,
+ "allowPlatformServices": false,
+ "quotaObjectBytes": 100000000000
+ },
+ "id": "12345678901234567890"
+ }
+"""
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import (
+ NetAppModule,
+)
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import (
+ SGRestAPI,
+)
+
+
+class SgGridAccount(object):
+ """
+ Create, modify and delete StorageGRID Tenant Account
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ state=dict(required=False, type="str", choices=["present", "absent"], default="present"),
+ name=dict(required=False, type="str"),
+ account_id=dict(required=False, type="str"),
+ protocol=dict(required=False, choices=["s3", "swift"]),
+ management=dict(required=False, type="bool", default=True),
+ use_own_identity_source=dict(required=False, type="bool"),
+ allow_platform_services=dict(required=False, type="bool"),
+ root_access_group=dict(required=False, type="str"),
+ quota_size=dict(required=False, type="int", default=0),
+ quota_size_unit=dict(
+ default="gb",
+ choices=[
+ "bytes",
+ "b",
+ "kb",
+ "mb",
+ "gb",
+ "tb",
+ "pb",
+ "eb",
+ "zb",
+ "yb",
+ ],
+ type="str",
+ ),
+ password=dict(required=False, type="str", no_log=True),
+ update_password=dict(
+ default="on_create", choices=["on_create", "always"]
+ ),
+ )
+ )
+
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ required_if=[
+ (
+ "state",
+ "present",
+ [
+ "name",
+ "protocol",
+ "use_own_identity_source",
+ "allow_platform_services",
+ ],
+ )
+ ],
+ supports_check_mode=True,
+ )
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+
+ # Checking for the parameters passed and create new parameters list
+ self.data = {}
+ self.data["name"] = self.parameters["name"]
+ self.data["capabilities"] = [self.parameters["protocol"]]
+
+ if self.parameters.get("password") is not None:
+ self.data["password"] = self.parameters["password"]
+
+ # Append "management" to the capability list only if parameter is True
+ if self.parameters.get("management"):
+ self.data["capabilities"].append("management")
+
+ self.data["policy"] = {}
+
+ if "use_own_identity_source" in self.parameters:
+ self.data["policy"]["useAccountIdentitySource"] = self.parameters[
+ "use_own_identity_source"
+ ]
+
+ if "allow_platform_services" in self.parameters:
+ self.data["policy"]["allowPlatformServices"] = self.parameters[
+ "allow_platform_services"
+ ]
+
+ if self.parameters.get("root_access_group") is not None:
+ self.data["grantRootAccessToGroup"] = self.parameters["root_access_group"]
+
+ if self.parameters["quota_size"] > 0:
+ self.parameters["quota_size"] = (
+ self.parameters["quota_size"]
+ * netapp_utils.POW2_BYTE_MAP[
+ self.parameters["quota_size_unit"]
+ ]
+ )
+ self.data["policy"]["quotaObjectBytes"] = self.parameters[
+ "quota_size"
+ ]
+ elif self.parameters["quota_size"] == 0:
+ self.data["policy"]["quotaObjectBytes"] = None
+
+ self.pw_change = {}
+ if self.parameters.get("password") is not None:
+ self.pw_change["password"] = self.parameters["password"]
+
+ def get_tenant_account_id(self):
+ # Check if tenant account exists
+ # Return tenant account info if found, or None
+ api = "api/v3/grid/accounts?limit=350"
+
+ list_accounts, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ for account in list_accounts.get("data"):
+ if account["name"] == self.parameters["name"]:
+ return account["id"]
+
+ return None
+
+ def get_tenant_account(self, account_id):
+ api = "api/v3/grid/accounts/%s" % account_id
+ account, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+ else:
+ return account["data"]
+ return None
+
+ def create_tenant_account(self):
+ api = "api/v3/grid/accounts"
+
+ response, error = self.rest_api.post(api, self.data)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def delete_tenant_account(self, account_id):
+ api = "api/v3/grid/accounts/" + account_id
+
+ self.data = None
+ response, error = self.rest_api.delete(api, self.data)
+ if error:
+ self.module.fail_json(msg=error)
+
+ def update_tenant_account(self, account_id):
+ api = "api/v3/grid/accounts/" + account_id
+
+ if "password" in self.data:
+ del self.data["password"]
+
+ if "grantRootAccessToGroup" in self.data:
+ del self.data["grantRootAccessToGroup"]
+
+ response, error = self.rest_api.put(api, self.data)
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def set_tenant_root_password(self, account_id):
+ api = "api/v3/grid/accounts/%s/change-password" % account_id
+ response, error = self.rest_api.post(api, self.pw_change)
+
+ if error:
+ self.module.fail_json(msg=error["text"])
+
+ def apply(self):
+ """
+ Perform pre-checks, call functions and exit
+ """
+
+ tenant_account = None
+
+ if self.parameters.get("account_id"):
+ tenant_account = self.get_tenant_account(
+ self.parameters["account_id"]
+ )
+
+ else:
+ tenant_account_id = self.get_tenant_account_id()
+ if tenant_account_id:
+ tenant_account = self.get_tenant_account(tenant_account_id)
+
+ cd_action = self.na_helper.get_cd_action(
+ tenant_account, self.parameters
+ )
+
+ if cd_action is None and self.parameters["state"] == "present":
+ # let's see if we need to update parameters
+ update = False
+
+ capability_diff = [
+ i
+ for i in self.data["capabilities"]
+ + tenant_account["capabilities"]
+ if i not in self.data["capabilities"]
+ or i not in tenant_account["capabilities"]
+ ]
+
+ if self.parameters["quota_size"] > 0:
+ if (
+ tenant_account["policy"]["quotaObjectBytes"]
+ != self.parameters["quota_size"]
+ ):
+ update = True
+ elif (
+ self.parameters["quota_size"] == 0
+ and tenant_account["policy"]["quotaObjectBytes"] is not None
+ ):
+ update = True
+
+ if (
+ "use_own_identity_source" in self.parameters
+ and tenant_account["policy"]["useAccountIdentitySource"]
+ != self.parameters["use_own_identity_source"]
+ ):
+ update = True
+
+ elif (
+ "allow_platform_services" in self.parameters
+ and tenant_account["policy"]["allowPlatformServices"]
+ != self.parameters["allow_platform_services"]
+ ):
+ update = True
+
+ elif capability_diff:
+ update = True
+
+ if update:
+ self.na_helper.changed = True
+
+ result_message = ""
+ resp_data = tenant_account
+ if self.na_helper.changed:
+ if self.module.check_mode:
+ pass
+ else:
+ if cd_action == "delete":
+ self.delete_tenant_account(tenant_account["id"])
+ result_message = "Tenant Account deleted"
+ resp_data = None
+
+ elif cd_action == "create":
+ resp_data = self.create_tenant_account()
+ result_message = "Tenant Account created"
+
+ else:
+ resp_data = self.update_tenant_account(tenant_account["id"])
+ result_message = "Tenant Account updated"
+
+ # If a password has been set
+ if self.pw_change:
+ if self.module.check_mode:
+ pass
+ else:
+ # Only update the password if update_password is always
+ # On a create action, the password is set directly by the POST /grid/accounts method
+ if self.parameters["update_password"] == "always" and cd_action != "create":
+ self.set_tenant_root_password(tenant_account["id"])
+ self.na_helper.changed = True
+
+ results = [result_message, "Tenant Account root password updated"]
+ result_message = "; ".join(filter(None, results))
+
+ self.module.exit_json(
+ changed=self.na_helper.changed, msg=result_message, resp=resp_data
+ )
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_grid_account = SgGridAccount()
+ na_sg_grid_account.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_certificate.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_certificate.py
new file mode 100644
index 000000000..97f9ab972
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_certificate.py
@@ -0,0 +1,226 @@
+#!/usr/bin/python
+
+# (c) 2021, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage Certificates"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+module: na_sg_grid_certificate
+short_description: Manage the Storage API and Grid Management certificates on StorageGRID.
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '21.6.0'
+author: NetApp Ansible Team (@joshedmonds) <ng-ansibleteam@netapp.com>
+description:
+- Set and update the Storage API and Grid Management certificates on NetApp StorageGRID.
+options:
+ state:
+ description:
+ - Whether the specified certificate should be set.
+ type: str
+ choices: ['present', 'absent']
+ default: present
+ type:
+ description:
+ - Which certificate to update.
+ type: str
+ choices: ['storage-api', 'management']
+ required: true
+ server_certificate:
+ description:
+ - X.509 server certificate in PEM-encoding.
+ type: str
+ ca_bundle:
+ description:
+ - Intermediate CA certificate bundle in concatenated PEM-encoding.
+ - Omit if there is no intermediate CA.
+ type: str
+ private_key:
+ description:
+ - Certificate private key in PEM-encoding.
+ - Required if I(server_certificate) is specified.
+ type: str
+"""
+
+EXAMPLES = """
+ - name: set storage API certificate
+ netapp.storagegrid.na_sg_grid_certificate:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ type: storage-api
+ server_certificate: |
+ -----BEGIN CERTIFICATE-----
+ MIIC6DCCAdACCQC7l4WukhKD0zANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJB
+ BAMMHnNnYW4wMS5kZXYubWljcm9icmV3Lm5ldGFwcC5hdTCCASIwDQYJKoZIhvcN
+ AQEBBQADggEPADCCAQoCggEBAMvjm9I35lmKcC7ITVL8+QiZ/klvdkbfZCUQrfdy
+ 71inP+XmPjs0rnkhICA9ItODteRcVlO+t7nDTfm7HgG0mJFkcJm0ffyEYrcx24qu
+ S7gXYQjRsJmrep1awoaCa20BMGuqK2WKI3IvZ7YiT22qkBqKJD+hIFffX6u3Jy+B
+ 77pR6YcATtpMHW/AaOx+OX9l80dIRsRZKMDxYQ==
+ -----END CERTIFICATE-----
+ private_key: |
+ -----BEGIN PRIVATE KEY-----
+ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDL45vSN+ZZinAu
+ L25W0+cz1Oi69AKkI7d9nbFics2ay5+7o+4rKqf3en2R4MSxiJvy+iDlOmATib5O
+ x8TN5pJ9AgMBAAECggEADDLM8tHXXUoUFihzv+BUwff8p8YcbHcXFcSes+xTd5li
+ po8lNsx/v2pQx4ByBkuaYLZGIEXOWS6gkp44xhIXgQKBgQD4Hq7862u5HLbmhrV3
+ vs8nC69b3QKBgQDacCD8d8JpwPbg8t2VjXM3UvdmgAaLUfU7O1DWV+W3jqzmDOoN
+ zWVgPbPNj0UmzvLDbgxLoxe77wjn2BHsAJVAfJ9VeQKBgGqFAegYO+wHR8lJUoa5
+ ZEe8Upy2oBtvND/0dnwO2ym2FGsBJN0Gr4NKdG5vkzLsthKkcwRm0ikwEUOUZQKE
+ K8J5yEVeo9K2v3wggtq8fYn6
+ -----END PRIVATE KEY-----
+
+"""
+
+RETURN = """
+resp:
+ description: Returns information about the StorageGRID server certificates.
+ returned: success
+ type: dict
+ sample: {
+ "serverCertificateEncoded": "-----BEGIN CERTIFICATE-----MIIC6DCCAdACCQC7l4WukhKD0zANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGE...-----END CERTIFICATE-----",
+ "caBundleEncoded": "-----BEGIN CERTIFICATE-----MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELM...-----END CERTIFICATE-----"
+ }
+"""
+
+import json
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class SgGridCertificate:
+ """
+ Update StorageGRID certificates
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ state=dict(required=False, type="str", choices=["present", "absent"], default="present"),
+ type=dict(required=True, type="str", choices=["storage-api", "management"]),
+ server_certificate=dict(required=False, type="str"),
+ ca_bundle=dict(required=False, type="str"),
+ private_key=dict(required=False, type="str", no_log=True),
+ )
+ )
+
+ parameter_map = {
+ "server_certificate": "serverCertificateEncoded",
+ "ca_bundle": "caBundleEncoded",
+ "private_key": "privateKeyEncoded",
+ }
+
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ required_if=[("state", "present", ["server_certificate", "private_key"])],
+ supports_check_mode=True,
+ )
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+ # Checking for the parameters passed and create new parameters list
+ self.data = {}
+
+ if self.parameters["state"] == "present":
+ for k in parameter_map.keys():
+ if self.parameters.get(k) is not None:
+ self.data[parameter_map[k]] = self.parameters[k]
+
+ self.module.fail_json
+
+ def get_grid_certificate(self, cert_type):
+ api = "api/v3/grid/%s" % cert_type
+
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def update_grid_certificate(self, cert_type):
+ api = "api/v3/grid/%s/update" % cert_type
+
+ response, error = self.rest_api.post(api, self.data)
+ if error:
+ self.module.fail_json(msg=error)
+
+ def apply(self):
+ """
+ Perform pre-checks, call functions and exit
+ """
+
+ cert_type = ""
+ cd_action = None
+
+ if self.parameters.get("type") == "storage-api":
+ cert_type = "storage-api-certificate"
+ elif self.parameters.get("type") == "management":
+ cert_type = "management-certificate"
+
+ cert_data = self.get_grid_certificate(cert_type)
+
+ if cert_data["serverCertificateEncoded"] is None and cert_data["caBundleEncoded"] is None:
+ cd_action = self.na_helper.get_cd_action(None, self.parameters)
+ else:
+ cd_action = self.na_helper.get_cd_action(cert_data, self.parameters)
+
+ if cd_action is None and self.parameters["state"] == "present":
+ # let's see if we need to update parameters
+ update = False
+
+ if self.data.get("serverCertificateEncoded") is not None and self.data.get("privateKeyEncoded") is not None:
+ for item in ["serverCertificateEncoded", "caBundleEncoded"]:
+ if self.data.get(item) != cert_data.get(item):
+ update = True
+
+ if update:
+ self.na_helper.changed = True
+
+ result_message = ""
+ resp_data = cert_data
+ if self.na_helper.changed:
+ if self.module.check_mode:
+ pass
+ else:
+ if cd_action == "delete":
+ self.update_grid_certificate(cert_type)
+ resp_data = self.get_grid_certificate(cert_type)
+ result_message = "Grid %s removed" % cert_type
+
+ else:
+ self.update_grid_certificate(cert_type)
+ resp_data = self.get_grid_certificate(cert_type)
+ result_message = "Grid %s updated" % cert_type
+
+ self.module.exit_json(changed=self.na_helper.changed, msg=result_message, resp=resp_data)
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_grid_certificate = SgGridCertificate()
+ na_sg_grid_certificate.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_client_certificate.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_client_certificate.py
new file mode 100644
index 000000000..aa381b397
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_client_certificate.py
@@ -0,0 +1,265 @@
+#!/usr/bin/python
+
+# (c) 2022, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage Certificates"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+module: na_sg_grid_client_certificate
+short_description: Manage Client Certificates on StorageGRID
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '21.11.0'
+author: NetApp Ansible Team (@joshedmonds) <ng-ansibleteam@netapp.com>
+description:
+- Create, Update, Delete Client Certificates on NetApp StorageGRID.
+options:
+ state:
+ description:
+ - Whether the specified certificate should exist.
+ type: str
+ choices: ['present', 'absent']
+ default: present
+ certificate_id:
+ description:
+ - ID of the client certificate.
+ type: str
+ display_name:
+ description:
+ - A display name for the client certificate configuration.
+ - This parameter can be modified if I(certificate_id) is also specified.
+ type: str
+ public_key:
+ description:
+ - X.509 client certificate in PEM-encoding.
+ type: str
+ allow_prometheus:
+ description:
+ - Whether the external monitoring tool can access Prometheus metrics.
+ type: bool
+"""
+
+EXAMPLES = """
+ - name: create client certificate
+ netapp.storagegrid.na_sg_grid_client_certificate:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ display_name: client-cert1
+ public_key: |
+ -----BEGIN CERTIFICATE-----
+ MIIC6DCCAdACCQC7l4WukhKD0zANBgkqhkiG9w0BAQsFADA2..swCQYDVQQGEwJB
+ BAMMHnNnYW4wMS5kZXYubWljcm9icmV3Lm5ldGFwcC5hdTCC..IwDQYJKoZIhvcN
+ AQEBBQADggEPADCCAQoCggEBAMvjm9I35lmKcC7ITVL8+QiZ..lvdkbfZCUQrfdy
+ 71inP+XmPjs0rnkhICA9ItODteRcVlO+t7nDTfm7HgG0mJFk..m0ffyEYrcx24qu
+ S7gXYQjRsJmrep1awoaCa20BMGuqK2WKI3IvZ7YiT22qkBqK..+hIFffX6u3Jy+B
+ 77pR6YcATtpMHW/AaOx+OX9l80dIRsRZKMDxYQ==
+ -----END CERTIFICATE-----
+ allow_prometheus: true
+
+ - name: rename client certificate
+ netapp.storagegrid.na_sg_grid_client_certificate:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ certificate_id: 00000000-0000-0000-0000-000000000000
+ display_name: client-cert1-rename
+ public_key: |
+ -----BEGIN CERTIFICATE-----
+ MIIC6DCCAdACCQC7l4WukhKD0zANBgkqhkiG9w0BAQsFADA2..swCQYDVQQGEwJB
+ BAMMHnNnYW4wMS5kZXYubWljcm9icmV3Lm5ldGFwcC5hdTCC..IwDQYJKoZIhvcN
+ AQEBBQADggEPADCCAQoCggEBAMvjm9I35lmKcC7ITVL8+QiZ..lvdkbfZCUQrfdy
+ 71inP+XmPjs0rnkhICA9ItODteRcVlO+t7nDTfm7HgG0mJFk..m0ffyEYrcx24qu
+ S7gXYQjRsJmrep1awoaCa20BMGuqK2WKI3IvZ7YiT22qkBqK..+hIFffX6u3Jy+B
+ 77pR6YcATtpMHW/AaOx+OX9l80dIRsRZKMDxYQ==
+ -----END CERTIFICATE-----
+ allow_prometheus: true
+
+ - name: delete client certificate
+ netapp.storagegrid.na_sg_grid_client_certificate:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: absent
+ display_name: client-cert1-rename
+"""
+
+RETURN = """
+resp:
+ description: Returns information about the StorageGRID server certificates.
+ returned: success
+ type: dict
+ sample: {
+ "id": "abcABC_01234-0123456789abcABCabc0123456789==",
+ "displayName": "client-cert1",
+ "expiryDate": "2024-01-01T00:00:00.000Z",
+ "publicKey": "-----BEGIN CERTIFICATE-----MIIC6DCCAdACCQC7l4WukhKD0zANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGE...-----END CERTIFICATE-----",
+ "allowPrometheus": true
+ }
+"""
+
+import json
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class SgGridClientCertificate:
+ """
+ Update StorageGRID client certificates
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ state=dict(required=False, type="str", choices=["present", "absent"], default="present"),
+ certificate_id=dict(required=False, type="str"),
+ display_name=dict(required=False, type="str"),
+ public_key=dict(required=False, type="str"),
+ allow_prometheus=dict(required=False, type="bool"),
+ )
+ )
+
+ parameter_map = {
+ "display_name": "displayName",
+ "public_key": "publicKey",
+ "allow_prometheus": "allowPrometheus",
+ }
+
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ required_if=[("state", "present", ["display_name", "public_key"])],
+ required_one_of=[("display_name", "certificate_id")],
+ supports_check_mode=True,
+ )
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+ # Checking for the parameters passed and create new parameters list
+ self.data = {}
+
+ if self.parameters["state"] == "present":
+ for k in parameter_map.keys():
+ if self.parameters.get(k) is not None:
+ self.data[parameter_map[k]] = self.parameters[k]
+
+ self.module.fail_json
+
+ def get_grid_client_certificate_id(self):
+ # Check if certificate with name exists
+ # Return certificate ID if found, or None
+ api = "api/v3/grid/client-certificates"
+
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ for cert in response.get("data"):
+ if cert["displayName"] == self.parameters["display_name"]:
+ return cert["id"]
+ return None
+
+ def get_grid_client_certificate(self, cert_id):
+ api = "api/v3/grid/client-certificates/%s" % cert_id
+ account, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+ else:
+ return account["data"]
+ return None
+
+ def create_grid_client_certificate(self):
+ api = "api/v3/grid/client-certificates"
+
+ response, error = self.rest_api.post(api, self.data)
+
+ if error:
+ self.module.fail_json(msg=error["text"])
+
+ return response["data"]
+
+ def delete_grid_client_certificate(self, cert_id):
+ api = "api/v3/grid/client-certificates/" + cert_id
+
+ self.data = None
+ response, error = self.rest_api.delete(api, self.data)
+ if error:
+ self.module.fail_json(msg=error)
+
+ def update_grid_client_certificate(self, cert_id):
+ api = "api/v3/grid/client-certificates/" + cert_id
+
+ response, error = self.rest_api.put(api, self.data)
+ if error:
+ self.module.fail_json(msg=error["text"])
+
+ return response["data"]
+
+ def apply(self):
+ """
+ Perform pre-checks, call functions and exit
+ """
+
+ client_certificate = None
+
+ if self.parameters.get("certificate_id"):
+ client_certificate = self.get_grid_client_certificate(self.parameters["certificate_id"])
+
+ else:
+ client_cert_id = self.get_grid_client_certificate_id()
+ if client_cert_id:
+ client_certificate = self.get_grid_client_certificate(client_cert_id)
+
+ cd_action = self.na_helper.get_cd_action(client_certificate, self.parameters)
+
+ if cd_action is None and self.parameters["state"] == "present":
+ # let's see if we need to update parameters
+ modify = self.na_helper.get_modified_attributes(client_certificate, self.data)
+
+ result_message = ""
+ resp_data = client_certificate
+
+ if self.na_helper.changed and not self.module.check_mode:
+ if cd_action == "delete":
+ self.delete_grid_client_certificate(client_certificate["id"])
+ result_message = "Client Certificate deleted"
+ elif cd_action == "create":
+ resp_data = self.create_grid_client_certificate()
+ result_message = "Client Certificate created"
+ elif modify:
+ resp_data = self.update_grid_client_certificate(client_certificate["id"])
+ result_message = "Client Certificate updated"
+
+ self.module.exit_json(changed=self.na_helper.changed, msg=result_message, resp=resp_data)
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_grid_certificate = SgGridClientCertificate()
+ na_sg_grid_certificate.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_dns.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_dns.py
new file mode 100644
index 000000000..95e4e4594
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_dns.py
@@ -0,0 +1,163 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage Grid DNS Servers"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+module: na_sg_grid_dns
+short_description: NetApp StorageGRID manage external DNS servers for the grid.
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '20.6.0'
+author: NetApp Ansible Team (@joshedmonds) <ng-ansibleteam@netapp.com>
+description:
+- Update NetApp StorageGRID DNS addresses.
+options:
+ state:
+ description:
+ - Whether the specified DNS address should exist or not.
+ - Required for all operations.
+ type: str
+ choices: ['present']
+ default: present
+ dns_servers:
+ description:
+ - List of comma separated DNS Addresses to be updated or delete.
+ type: list
+ elements: str
+ required: true
+"""
+
+EXAMPLES = """
+ - name: update DNS servers on StorageGRID
+ netapp.storagegrid.na_sg_grid_dns:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ dns_servers: "x.x.x.x,xxx.xxx.xxx.xxx"
+"""
+
+RETURN = """
+resp:
+ description: Returns information about the configured DNS servers.
+ returned: success
+ type: list
+ elements: str
+ sample: ["8.8.8.8", "8.8.4.4"]
+"""
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class SgGridDns(object):
+ """
+ Create, modify and delete DNS entries for StorageGRID
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ state=dict(required=False, type="str", choices=["present"], default="present"),
+ dns_servers=dict(required=True, type="list", elements="str"),
+ )
+ )
+
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ # required_if=[("state", "present", ["state", "name", "protocol"])],
+ supports_check_mode=True,
+ )
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+ # Checking for the parameters passed and create new parameters list
+ self.data = self.parameters["dns_servers"]
+
+ def get_grid_dns(self):
+ # Check if tenant account exists
+ # Return tenant account info if found, or None
+ api = "api/v3/grid/dns-servers"
+
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def update_grid_dns(self):
+ api = "api/v3/grid/dns-servers"
+
+ response, error = self.rest_api.put(api, self.data)
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def apply(self):
+ """
+ Perform pre-checks, call functions and exit
+ """
+ grid_dns = self.get_grid_dns()
+
+ cd_action = self.na_helper.get_cd_action(grid_dns, self.parameters["dns_servers"])
+
+ if cd_action is None and self.parameters["state"] == "present":
+ # let's see if we need to update parameters
+ update = False
+
+ dns_diff = [i for i in self.data + grid_dns if i not in self.data or i not in grid_dns]
+ if dns_diff:
+ update = True
+
+ if update:
+ self.na_helper.changed = True
+ result_message = ""
+ resp_data = grid_dns
+ if self.na_helper.changed:
+ if self.module.check_mode:
+ pass
+ else:
+ resp_data = self.update_grid_dns()
+ result_message = "Grid DNS updated"
+
+ self.module.exit_json(changed=self.na_helper.changed, msg=result_message, resp=resp_data)
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_grid_dns = SgGridDns()
+ na_sg_grid_dns.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_gateway.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_gateway.py
new file mode 100644
index 000000000..9202decff
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_gateway.py
@@ -0,0 +1,532 @@
+#!/usr/bin/python
+
+# (c) 2021, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage Load Balancer Endpoints"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+module: na_sg_grid_gateway
+short_description: Manage Load balancer (gateway) endpoints on StorageGRID.
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '21.7.0'
+author: NetApp Ansible Team (@jkandati) <ng-sg-ansibleteam@netapp.com>
+description:
+- Create or Update Load Balancer Endpoints on StorageGRID.
+- This module is idempotent if I(private_key) is not specified.
+- The module will match an existing config based on I(port) and I(display_name).
+- If multiple load balancer endpoints exist utilizing the same port and display name, use I(gateway_id) to select the intended endpoint.
+options:
+ state:
+ description:
+ - Whether the specified load balancer endpoint should be configured.
+ type: str
+ choices: ['present', 'absent']
+ default: present
+ gateway_id:
+ description:
+ - ID of the load balancer endpoint.
+ type: str
+ version_added: '21.9.0'
+ display_name:
+ description:
+ - A display name for the configuration.
+ - This parameter can be modified if I(gateway_id) is also specified.
+ type: str
+ port:
+ description:
+ - The TCP port to serve traffic on.
+ - This parameter cannot be modified after the load balancer endpoint has been created.
+ type: int
+ required: true
+ secure:
+ description:
+ - Whether the load balancer endpoint serves HTTP or HTTPS traffic.
+ - This parameter cannot be modified after the load balancer endpoint has been created.
+ type: bool
+ default: true
+ enable_ipv4:
+ description:
+ - Indicates whether to listen for connections on IPv4.
+ type: bool
+ default: true
+ enable_ipv6:
+ description:
+ - Indicates whether to listen for connections on IPv6.
+ type: bool
+ default: true
+ binding_mode:
+ description:
+ - Binding mode to restrict accessibility of the load balancer endpoint.
+ - A binding mode other than I(global) requires StorageGRID 11.5 or greater.
+ type: str
+ choices: ['global', 'ha-groups', 'node-interfaces']
+ default: 'global'
+ version_added: '21.9.0'
+ ha_groups:
+ description:
+ - A set of StorageGRID HA Groups by name or UUID to bind the load balancer endpoint to.
+ - Option is ignored unless I(binding_mode=ha-groups).
+ type: list
+ elements: str
+ version_added: '21.9.0'
+ node_interfaces:
+ description:
+ - A set of StorageGRID node interfaces to bind the load balancer endpoint to.
+ type: list
+ elements: dict
+ suboptions:
+ node:
+ description:
+ - Name of the StorageGRID node.
+ type: str
+ interface:
+ description:
+ - The interface to bind to. eth0 corresponds to the Grid Network, eth1 to the Admin Network, and eth2 to the Client Network.
+ type: str
+ version_added: '21.9.0'
+ default_service_type:
+ description:
+ - The type of service to proxy through the load balancer.
+ type: str
+ choices: ['s3', 'swift']
+ default: 's3'
+ server_certificate:
+ description:
+ - X.509 server certificate in PEM-encoding.
+ - Omit if using default certificates.
+ type: str
+ required: false
+ private_key:
+ description:
+ - Certficate private key in PEM-encoding.
+ - Required if I(server_certificate) is not empty.
+ type: str
+ required: false
+ ca_bundle:
+ description:
+ - Intermediate CA certificate bundle in concatenated PEM-encoding.
+ - Omit when there is no intermediate CA.
+ type: str
+ required: false
+
+"""
+EXAMPLES = """
+ - name: Create and Upload Certificate to a Gateway Endpoint with global binding
+ netapp.storagegrid.na_sg_grid_gateway:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ displayName: "FabricPool Endpoint"
+ port: 10443
+ secure: True
+ enable_ipv4: True
+ enable_ipv6: True
+ default_service_type: "s3"
+ server_certificate: |
+ -----BEGIN CERTIFICATE-----
+ MIIC6DCCAdACCQC7l4WukhKD0zANBgkqhkiG9w0BAQsFADA2..swCQYDVQQGEwJB
+ BAMMHnNnYW4wMS5kZXYubWljcm9icmV3Lm5ldGFwcC5hdTCC..IwDQYJKoZIhvcN
+ AQEBBQADggEPADCCAQoCggEBAMvjm9I35lmKcC7ITVL8+QiZ..lvdkbfZCUQrfdy
+ 71inP+XmPjs0rnkhICA9ItODteRcVlO+t7nDTfm7HgG0mJFk..m0ffyEYrcx24qu
+ S7gXYQjRsJmrep1awoaCa20BMGuqK2WKI3IvZ7YiT22qkBqK..+hIFffX6u3Jy+B
+ 77pR6YcATtpMHW/AaOx+OX9l80dIRsRZKMDxYQ==
+ -----END CERTIFICATE-----
+ private_key: |
+ -----BEGIN PRIVATE KEY-----
+ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIB..DL45vSN+ZZinAu
+ L25W0+cz1Oi69AKkI7d9nbFics2ay5+7o+4rKqf3en2R4MSx..vy+iDlOmATib5O
+ x8TN5pJ9AgMBAAECggEADDLM8tHXXUoUFihzv+BUwff8p8Yc..cXFcSes+xTd5li
+ po8lNsx/v2pQx4ByBkuaYLZGIEXOWS6gkp44xhIXgQKBgQD4..7862u5HLbmhrV3
+ vs8nC69b3QKBgQDacCD8d8JpwPbg8t2VjXM3UvdmgAaLUfU7..DWV+W3jqzmDOoN
+ zWVgPbPNj0UmzvLDbgxLoxe77wjn2BHsAJVAfJ9VeQKBgGqF..gYO+wHR8lJUoa5
+ ZEe8Upy2oBtvND/0dnwO2ym2FGsBJN0Gr4NKdG5vkzLsthKk..Rm0ikwEUOUZQKE
+ K8J5yEVeo9K2v3wggtq8fYn6
+ -----END PRIVATE KEY-----
+ validate_certs: false
+
+ - name: Create a HTTP Gateway Endpoint with HA Group Binding
+ netapp.storagegrid.na_sg_grid_gateway:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ displayName: "App Endpoint 1"
+ port: 10501
+ secure: false
+ enable_ipv4: True
+ enable_ipv6: True
+ default_service_type: "s3"
+ binding_mode: ha-groups
+ ha_groups: site1_ha_group
+ validate_certs: false
+
+ - name: Create a HTTP Gateway Endpoint with Node Interface Binding
+ netapp.storagegrid.na_sg_grid_gateway:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ displayName: "App Endpoint 2"
+ port: 10502
+ secure: false
+ enable_ipv4: True
+ enable_ipv6: True
+ default_service_type: "s3"
+ binding_mode: node-interfaces
+ node_interfaecs:
+ - node: SITE1_ADM1
+ interface: eth2
+ - node: SITE2_ADM1
+ interface: eth2
+ validate_certs: false
+
+ - name: Delete Gateway Endpoint
+ netapp.storagegrid.na_sg_grid_gateway:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ displayName: "App Endpoint 2"
+ port: 10502
+ default_service_type: "s3"
+ validate_certs: false
+"""
+
+RETURN = """
+resp:
+ description: Returns information about the StorageGRID Load Balancer Endpoint.
+ returned: success
+ type: dict
+ sample: {
+ "id": "ffffffff-ffff-ffff-ffff-ffffffffffff",
+ "displayName": "ansibletest-secure",
+ "enableIPv4": True,
+ "enableIPv6": True,
+ "port": 10443,
+ "secure": True,
+ "accountId": "0",
+ "defaultServiceType": "s3",
+ "certSource": "plaintext",
+ "plaintextCertData": {
+ "serverCertificateEncoded": "-----BEGIN CERTIFICATE-----MIIC6DCCAdACCQC7l4WukhKD0zANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGE...-----END CERTIFICATE-----",
+ "caBundleEncoded": "-----BEGIN CERTIFICATE-----MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELM...-----END CERTIFICATE-----",
+ "metadata": {...}
+ }
+ }
+"""
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class SgGridGateway:
+ """
+ Create, modify and delete Gateway entries for StorageGRID
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ # Arguments for Creating Gateway Port
+ state=dict(required=False, type="str", choices=["present", "absent"], default="present"),
+ gateway_id=dict(required=False, type="str"),
+ display_name=dict(required=False, type="str"),
+ port=dict(required=True, type="int"),
+ secure=dict(required=False, type="bool", default=True),
+ enable_ipv4=dict(required=False, type="bool", default=True),
+ enable_ipv6=dict(required=False, type="bool", default=True),
+ binding_mode=dict(
+ required=False, type="str", choices=["global", "ha-groups", "node-interfaces"], default="global"
+ ),
+ ha_groups=dict(required=False, type="list", elements="str"),
+ node_interfaces=dict(
+ required=False,
+ type="list",
+ elements="dict",
+ options=dict(
+ node=dict(required=False, type="str"),
+ interface=dict(required=False, type="str"),
+ ),
+ ),
+ # Arguments for setting Gateway Virtual Server
+ default_service_type=dict(required=False, type="str", choices=["s3", "swift"], default="s3"),
+ server_certificate=dict(required=False, type="str"),
+ ca_bundle=dict(required=False, type="str"),
+ private_key=dict(required=False, type="str", no_log=True),
+ )
+ )
+
+ parameter_map_gateway = {
+ "gateway_id": "id",
+ "display_name": "displayName",
+ "port": "port",
+ "secure": "secure",
+ "enable_ipv4": "enableIPv4",
+ "enable_ipv6": "enableIPv6",
+ }
+ parameter_map_server = {
+ "server_certificate": "serverCertificateEncoded",
+ "ca_bundle": "caBundleEncoded",
+ "private_key": "privateKeyEncoded",
+ }
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ required_if=[("state", "present", ["display_name"])],
+ supports_check_mode=True,
+ )
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+ # Get API version
+ self.rest_api.get_sg_product_version()
+
+ # Checking for the parameters passed and create new parameters list
+
+ # Parameters for creating a new gateway port configuration
+ self.data_gateway = {}
+ self.data_gateway["accountId"] = "0"
+
+ for k in parameter_map_gateway.keys():
+ if self.parameters.get(k) is not None:
+ self.data_gateway[parameter_map_gateway[k]] = self.parameters[k]
+
+ # Parameters for setting a gateway virtual server configuration for a gateway port
+ self.data_server = {}
+ self.data_server["defaultServiceType"] = self.parameters["default_service_type"]
+
+ if self.parameters["secure"]:
+ self.data_server["plaintextCertData"] = {}
+ self.data_server["certSource"] = "plaintext"
+
+ for k in parameter_map_server.keys():
+ if self.parameters.get(k) is not None:
+ self.data_server["plaintextCertData"][parameter_map_server[k]] = self.parameters[k]
+
+ if self.parameters["binding_mode"] != "global":
+ self.rest_api.fail_if_not_sg_minimum_version("non-global binding mode", 11, 5)
+
+ if self.parameters["binding_mode"] == "ha-groups":
+ self.data_gateway["pinTargets"] = {}
+ self.data_gateway["pinTargets"]["haGroups"] = self.build_ha_group_list()
+ self.data_gateway["pinTargets"]["nodeInterfaces"] = []
+
+ elif self.parameters["binding_mode"] == "node-interfaces":
+ self.data_gateway["pinTargets"] = {}
+ self.data_gateway["pinTargets"]["nodeInterfaces"] = self.build_node_interface_list()
+ self.data_gateway["pinTargets"]["haGroups"] = []
+
+ else:
+ self.data_gateway["pinTargets"] = {}
+ self.data_gateway["pinTargets"]["haGroups"] = []
+ self.data_gateway["pinTargets"]["nodeInterfaces"] = []
+
+ def build_ha_group_list(self):
+ ha_group_ids = []
+
+ api = "api/v3/private/ha-groups"
+ ha_groups, error = self.rest_api.get(api)
+ if error:
+ self.module.fail_json(msg=error)
+
+ for param in self.parameters["ha_groups"]:
+ ha_group = next(
+ (item for item in ha_groups["data"] if (item["name"] == param or item["id"] == param)), None
+ )
+ if ha_group is not None:
+ ha_group_ids.append(ha_group["id"])
+ else:
+ self.module.fail_json(msg="HA Group '%s' is invalid" % param)
+
+ return ha_group_ids
+
+ def build_node_interface_list(self):
+ node_interfaces = []
+
+ api = "api/v3/grid/node-health"
+ nodes, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ for node_interface in self.parameters["node_interfaces"]:
+ node_dict = {}
+ node = next((item for item in nodes["data"] if item["name"] == node_interface["node"]), None)
+ if node is not None:
+ node_dict["nodeId"] = node["id"]
+ node_dict["interface"] = node_interface["interface"]
+ node_interfaces.append(node_dict)
+ else:
+ self.module.fail_json(msg="Node '%s' is invalid" % node_interface["node"])
+
+ return node_interfaces
+
+ def get_grid_gateway_config(self, gateway_id):
+ api = "api/v3/private/gateway-configs/%s" % gateway_id
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ gateway = response["data"]
+ gateway_config = self.get_grid_gateway_server_config(gateway["id"])
+
+ return gateway, gateway_config
+
+ def get_grid_gateway_server_config(self, gateway_id):
+ api = "api/v3/private/gateway-configs/%s/server-config" % gateway_id
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def get_grid_gateway_ports(self, target_port):
+
+ configured_ports = []
+ gateway = {}
+ gateway_config = {}
+
+ api = "api/v3/private/gateway-configs"
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ grid_gateway_ports = response["data"]
+
+ # Get only a list of used ports
+ configured_ports = [data["port"] for data in grid_gateway_ports]
+
+ for index, port in enumerate(configured_ports):
+ # if port already exists then get gateway ID and get the gateway port server configs
+ if target_port == port and grid_gateway_ports[index]["displayName"] == self.parameters["display_name"]:
+ gateway = grid_gateway_ports[index]
+ gateway_config = self.get_grid_gateway_server_config(gateway["id"])
+ break
+
+ return gateway, gateway_config
+
+ def create_grid_gateway(self):
+ api = "api/v3/private/gateway-configs"
+ response, error = self.rest_api.post(api, self.data_gateway)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def delete_grid_gateway(self, gateway_id):
+ api = "api/v3/private/gateway-configs/" + gateway_id
+ self.data = None
+ response, error = self.rest_api.delete(api, self.data)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ def update_grid_gateway(self, gateway_id):
+ api = "api/v3/private/gateway-configs/%s" % gateway_id
+ response, error = self.rest_api.put(api, self.data_gateway)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def update_grid_gateway_server(self, gateway_id):
+ api = "api/v3/private/gateway-configs/%s/server-config" % gateway_id
+ response, error = self.rest_api.put(api, self.data_server)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def apply(self):
+ gateway = None
+ gateway_config = None
+
+ update_gateway = False
+ update_gateway_server = False
+
+ if self.parameters.get("gateway_id"):
+ gateway, gateway_config = self.get_grid_gateway_config(self.parameters["gateway_id"])
+
+ else:
+ # Get list of all gateway port configurations
+ gateway, gateway_config = self.get_grid_gateway_ports(self.data_gateway["port"])
+
+ cd_action = self.na_helper.get_cd_action(gateway.get("id"), self.parameters)
+
+ if cd_action is None and self.parameters["state"] == "present":
+ update = False
+
+ if self.data_server.get("plaintextCertData"):
+ if self.data_server["plaintextCertData"].get("privateKeyEncoded") is not None:
+ update = True
+ self.module.warn("This module is not idempotent when private_key is present.")
+
+ if gateway_config.get("plaintextCertData"):
+ # If certificate private key supplied, update
+ if gateway_config["plaintextCertData"].get("metadata"):
+ # remove metadata because we can't compare that
+ del gateway_config["plaintextCertData"]["metadata"]
+
+ # compare current and desired state
+ # gateway config cannot be modified until StorageGRID 11.5
+ if self.rest_api.meets_sg_minimum_version(11, 5):
+ update_gateway = self.na_helper.get_modified_attributes(gateway, self.data_gateway)
+ update_gateway_server = self.na_helper.get_modified_attributes(gateway_config, self.data_server)
+
+ if update:
+ self.na_helper.changed = True
+
+ result_message = ""
+ resp_data = {}
+
+ if self.na_helper.changed and not self.module.check_mode:
+ if cd_action == "delete":
+ self.delete_grid_gateway(gateway["id"])
+ result_message = "Load Balancer Gateway Port Deleted"
+
+ elif cd_action == "create":
+ resp_data = self.create_grid_gateway()
+ gateway["id"] = resp_data["id"]
+ resp_data_server = self.update_grid_gateway_server(gateway["id"])
+ resp_data.update(resp_data_server)
+ result_message = "Load Balancer Gateway Port Created"
+
+ else:
+ resp_data = gateway
+ if update_gateway:
+ resp_data = self.update_grid_gateway(gateway["id"])
+ resp_data.update(gateway_config)
+
+ if update_gateway_server:
+ resp_data_server = self.update_grid_gateway_server(gateway["id"])
+ resp_data.update(resp_data_server)
+ result_message = "Load Balancer Gateway Port Updated"
+
+ self.module.exit_json(changed=self.na_helper.changed, msg=result_message, resp=resp_data)
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_grid_gateway = SgGridGateway()
+ na_sg_grid_gateway.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_group.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_group.py
new file mode 100644
index 000000000..60592c609
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_group.py
@@ -0,0 +1,341 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage Grid Groups"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+module: na_sg_grid_group
+short_description: NetApp StorageGRID manage groups.
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '20.6.0'
+author: NetApp Ansible Team (@joshedmonds) <ng-ansibleteam@netapp.com>
+description:
+- Create, Update, Delete Administration Groups within NetApp StorageGRID.
+options:
+ state:
+ description:
+ - Whether the specified group should exist or not.
+ type: str
+ choices: ['present', 'absent']
+ default: present
+ display_name:
+ description:
+ - Name of the group.
+ - Required for create operation
+ type: str
+ unique_name:
+ description:
+ - Unique Name for the group. Must begin with C(group/) or C(federated-group/)
+ - Required for create, modify or delete operation.
+ type: str
+ required: true
+ management_policy:
+ description:
+ - Management access controls granted to the group within the tenancy.
+ type: dict
+ suboptions:
+ alarm_acknowledgement:
+ description:
+ - Group members can have permission to acknowledge alarms.
+ required: false
+ type: bool
+ other_grid_configuration:
+ description:
+ - Need to investigate.
+ required: false
+ type: bool
+ grid_topology_page_configuration:
+ description:
+ - Users in this group will have permissions to change grid topology.
+ required: false
+ type: bool
+ tenant_accounts:
+ description:
+ - Users in this group will have permissions to manage tenant accounts.
+ required: false
+ type: bool
+ change_tenant_root_password:
+ description:
+ - Users in this group will have permissions to change tenant password.
+ required: false
+ type: bool
+ maintenance:
+ description:
+ - Users in this group will have permissions to run maintenance tasks on StorageGRID.
+ required: false
+ type: bool
+ metrics_query:
+ description:
+ - Users in this group will have permissions to query metrics on StorageGRID.
+ required: false
+ type: bool
+ activate_features:
+ description:
+ - Users in this group will have permissions to reactivate features.
+ required: false
+ type: bool
+ ilm:
+ description:
+ - Users in this group will have permissions to manage ILM rules on StorageGRID.
+ required: false
+ type: bool
+ object_metadata:
+ description:
+ - Users in this group will have permissions to manage object metadata.
+ required: false
+ type: bool
+ root_access:
+ description:
+ - Users in this group will have root access.
+ required: false
+ type: bool
+"""
+
+EXAMPLES = """
+ - name: create a StorageGRID group
+ netapp.storagegrid.na_sg_grid_group:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ display_name: ansiblegroup100
+ unique_name: group/ansiblegroup100
+ management_policy:
+ tenant_accounts: true
+ maintenance: true
+ root_access: false
+"""
+
+RETURN = """
+resp:
+ description: Returns information about the StorageGRID group attributes.
+ returned: success
+ type: dict
+ sample: {
+ "displayName": "Example Group",
+ "policies": {
+ "management": {
+ "alarmAcknowledgment": true,
+ "manageAlerts": true,
+ "otherGridConfiguration": true,
+ "gridTopologyPageConfiguration": true,
+ "tenantAccounts": true,
+ "changeTenantRootPassword": true,
+ "maintenance": true,
+ "metricsQuery": true,
+ "activateFeatures": false,
+ "ilm": true,
+ "objectMetadata": true,
+ "storageAdmin": true,
+ "rootAccess": true
+ }
+ },
+ "uniqueName": "group/examplegroup",
+ "accountId": "12345678901234567890",
+ "id": "00000000-0000-0000-0000-000000000000",
+ "federated": false,
+ "groupURN": "urn:sgws:identity::12345678901234567890:group/examplegroup"
+ }
+"""
+
+import json
+import re
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class SgGridGroup(object):
+ """
+ Create, modify and delete StorageGRID Grid-administration Group
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ state=dict(required=False, type="str", choices=["present", "absent"], default="present"),
+ display_name=dict(required=False, type="str"),
+ unique_name=dict(required=True, type="str"),
+ management_policy=dict(
+ required=False,
+ type="dict",
+ options=dict(
+ alarm_acknowledgement=dict(required=False, type="bool"),
+ other_grid_configuration=dict(required=False, type="bool"),
+ grid_topology_page_configuration=dict(required=False, type="bool"),
+ tenant_accounts=dict(required=False, type="bool"),
+ change_tenant_root_password=dict(required=False, type="bool"),
+ maintenance=dict(required=False, type="bool"),
+ metrics_query=dict(required=False, type="bool"),
+ activate_features=dict(required=False, type="bool"),
+ ilm=dict(required=False, type="bool"),
+ object_metadata=dict(required=False, type="bool"),
+ root_access=dict(required=False, type="bool"),
+ ),
+ ),
+ )
+ )
+ parameter_map = {
+ "alarm_acknowledgement": "alarmAcknowledgement",
+ "other_grid_configuration": "otherGridConfiguration",
+ "grid_topology_page_configuration": "gridTopologyPageConfiguration",
+ "tenant_accounts": "tenantAccounts",
+ "change_tenant_root_password": "changeTenantRootPassword",
+ "maintenance": "maintenance",
+ "metrics_query": "metricsQuery",
+ "activate_features": "activateFeatures",
+ "ilm": "ilm",
+ "object_metadata": "objectMetadata",
+ "root_access": "rootAccess",
+ }
+ self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True,)
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+ # Checking for the parameters passed and create new parameters list
+ self.data = {}
+ self.data["displayName"] = self.parameters.get("display_name")
+ self.data["uniqueName"] = self.parameters["unique_name"]
+ # Only add the parameter if value is True, as JSON response does not include non-true objects
+ self.data["policies"] = {}
+
+ if self.parameters.get("management_policy"):
+ self.data["policies"] = {
+ "management": dict(
+ (parameter_map[k], v) for (k, v) in self.parameters["management_policy"].items() if v
+ )
+ }
+ if not self.data["policies"].get("management"):
+ self.data["policies"]["management"] = None
+
+ self.re_local_group = re.compile("^group/")
+ self.re_fed_group = re.compile("^federated-group/")
+
+ if (
+ self.re_local_group.match(self.parameters["unique_name"]) is None
+ and self.re_fed_group.match(self.parameters["unique_name"]) is None
+ ):
+ self.module.fail_json(msg="unique_name must begin with 'group/' or 'federated-group/'")
+
+ def get_grid_group(self, unique_name):
+ # Use the unique name to check if the group exists
+ api = "api/v3/grid/groups/%s" % unique_name
+ response, error = self.rest_api.get(api)
+
+ if error:
+ if response["code"] != 404:
+ self.module.fail_json(msg=error)
+ else:
+ return response["data"]
+ return None
+
+ def create_grid_group(self):
+ api = "api/v3/grid/groups"
+
+ response, error = self.rest_api.post(api, self.data)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def delete_grid_group(self, group_id):
+ api = "api/v3/grid/groups/" + group_id
+
+ self.data = None
+ response, error = self.rest_api.delete(api, self.data)
+ if error:
+ self.module.fail_json(msg=error)
+
+ def update_grid_group(self, group_id):
+ api = "api/v3/grid/groups/" + group_id
+
+ response, error = self.rest_api.put(api, self.data)
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def apply(self):
+ """
+ Perform pre-checks, call functions and exit
+ """
+ grid_group = self.get_grid_group(self.parameters["unique_name"])
+
+ cd_action = self.na_helper.get_cd_action(grid_group, self.parameters)
+
+ if cd_action is None and self.parameters["state"] == "present":
+ # let's see if we need to update parameters
+ update = False
+
+ if self.parameters.get("management_policy"):
+ if (
+ grid_group.get("policies") is None
+ or grid_group.get("policies", {}).get("management") != self.data["policies"]["management"]
+ ):
+ update = True
+
+ if update:
+ self.na_helper.changed = True
+ result_message = ""
+ resp_data = grid_group
+ if self.na_helper.changed:
+ if self.module.check_mode:
+ pass
+ else:
+ if cd_action == "delete":
+ self.delete_grid_group(grid_group["id"])
+ result_message = "Grid Group deleted"
+
+ elif cd_action == "create":
+ resp_data = self.create_grid_group()
+ result_message = "Grid Group created"
+
+ else:
+ # for a federated group, the displayName parameter needs to be specified
+ # and must match the existing displayName
+ if self.re_fed_group.match(self.parameters["unique_name"]):
+ self.data["displayName"] = grid_group["displayName"]
+
+ resp_data = self.update_grid_group(grid_group["id"])
+ result_message = "Grid Group updated"
+
+ self.module.exit_json(changed=self.na_helper.changed, msg=result_message, resp=resp_data)
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_grid_group = SgGridGroup()
+ na_sg_grid_group.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_ha_group.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_ha_group.py
new file mode 100644
index 000000000..c99719c6d
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_ha_group.py
@@ -0,0 +1,334 @@
+#!/usr/bin/python
+
+# (c) 2022, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage HA Groups"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+module: na_sg_grid_ha_group
+short_description: Manage high availability (HA) group configuration on StorageGRID.
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '21.10.0'
+author: NetApp Ansible Team (@joshedmonds) <ng-ansibleteam@netapp.com>
+description:
+- Create, Update, Delete HA Groups on NetApp StorageGRID.
+options:
+ state:
+ description:
+ - Whether the specified HA Group should exist.
+ type: str
+ choices: ['present', 'absent']
+ default: present
+ name:
+ description:
+ - Name of the HA Group.
+ type: str
+ ha_group_id:
+ description:
+ - HA Group ID.
+ - May be used for modify or delete operation.
+ type: str
+ description:
+ description:
+ - Description of the HA Group.
+ type: str
+ gateway_cidr:
+ description:
+ - CIDR for the gateway IP and VIP subnet.
+ type: str
+ virtual_ips:
+ description:
+ - A list of virtual IP addresses.
+ type: list
+ elements: str
+ interfaces:
+ description:
+ - A set of StorageGRID node interface pairs.
+ - The primary interface is specified first, followed by the other interface pairs in failover order.
+ type: list
+ elements: dict
+ suboptions:
+ node:
+ description:
+ - Name of the StorageGRID node.
+ type: str
+ interface:
+ description:
+ - The interface to bind to. eth0 corresponds to the Grid Network, eth1 to the Admin Network, and eth2 to the Client Network.
+ type: str
+"""
+
+EXAMPLES = """
+ - name: create HA Group
+ netapp.storagegrid.na_sg_grid_ha_group:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ name: Site1-HA-Group
+ description: "Site 1 HA Group"
+ gateway_cidr: 192.168.50.1/24
+ virtual_ips: 192.168.50.5
+ interfaces:
+ - node: SITE1-ADM1
+ interface: eth2
+ - node: SITE1-G1
+ interface: eth2
+
+ - name: add VIP to HA Group
+ netapp.storagegrid.na_sg_grid_ha_group:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ name: Site1-HA-Group
+ description: "Site 1 HA Group"
+ gateway_cidr: 192.168.50.1/24
+ virtual_ips: 192.168.50.5,192.168.50.6
+ interfaces:
+ - node: SITE1-ADM1
+ interface: eth2
+ - node: SITE1-G1
+ interface: eth2
+
+ - name: rename HA Group
+ netapp.storagegrid.na_sg_grid_ha_group:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ ha_group_id: 00000000-0000-0000-0000-000000000000
+ name: Site1-HA-Group-New-Name
+ description: "Site 1 HA Group"
+ gateway_cidr: 192.168.50.1/24
+ virtual_ips: 192.168.50.5
+ interfaces:
+ - node: SITE1-ADM1
+ interface: eth2
+ - node: SITE1-G1
+ interface: eth2
+
+ - name: delete HA Group
+ netapp.storagegrid.na_sg_grid_ha_group:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: absent
+ name: Site1-HA-Group
+"""
+
+RETURN = """
+resp:
+ description: Returns information about the StorageGRID HA Group.
+ returned: success
+ type: dict
+ sample: {
+ "description": "Site 1 HA Group",
+ "gatewayCidr": "192.168.50.1/24",
+ "id": "bb386f30-805d-4fec-a2c5-85790b460db0",
+ "interfaces": [
+ {
+ "interface": "eth2",
+ "nodeId": "0b1866ed-d6e7-41b4-815f-bf867348b76b"
+ },
+ {
+ "interface": "eth2",
+ "nodeId": "7bb5bf05-a04c-4344-8abd-08c5c4048666"
+ }
+ ],
+ "name": "Site1-HA-Group",
+ "virtualIps": [
+ "192.168.50.5",
+ "192.168.50.6"
+ ]
+ }
+"""
+
+import json
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class SgGridHaGroup:
+ """
+ Create, modify and delete HA Group configurations for StorageGRID
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ state=dict(required=False, type="str", choices=["present", "absent"], default="present"),
+ name=dict(required=False, type="str"),
+ ha_group_id=dict(required=False, type="str"),
+ description=dict(required=False, type="str"),
+ gateway_cidr=dict(required=False, type="str"),
+ virtual_ips=dict(required=False, type="list", elements="str"),
+ interfaces=dict(
+ required=False,
+ type="list",
+ elements="dict",
+ options=dict(
+ node=dict(required=False, type="str"),
+ interface=dict(required=False, type="str"),
+ ),
+ ),
+ )
+ )
+
+ parameter_map = {
+ "name": "name",
+ "description": "description",
+ "gateway_cidr": "gatewayCidr",
+ "virtual_ips": "virtualIps",
+ }
+
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ required_if=[("state", "present", ["name", "gateway_cidr", "virtual_ips", "interfaces"])],
+ required_one_of=[("name", "ha_group_id")],
+ )
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+ # Checking for the parameters passed and create new parameters list
+ self.data = {}
+
+ if self.parameters["state"] == "present":
+ for k in parameter_map.keys():
+ if self.parameters.get(k) is not None:
+ self.data[parameter_map[k]] = self.parameters[k]
+
+ if self.parameters.get("interfaces") is not None:
+ self.data["interfaces"] = self.build_node_interface_list()
+
+ def build_node_interface_list(self):
+ node_interfaces = []
+
+ api = "api/v3/grid/node-health"
+ nodes, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ for node_interface in self.parameters["interfaces"]:
+ node_dict = {}
+ node = next((item for item in nodes["data"] if item["name"] == node_interface["node"]), None)
+ if node is not None:
+ node_dict["nodeId"] = node["id"]
+ node_dict["interface"] = node_interface["interface"]
+ node_interfaces.append(node_dict)
+ else:
+ self.module.fail_json(msg="Node '%s' is invalid" % node_interface["node"])
+
+ return node_interfaces
+
+ def get_ha_group_id(self):
+ # Check if HA Group exists
+ # Return HA Group info if found, or None
+ api = "api/v3/private/ha-groups"
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return next((item["id"] for item in response.get("data") if item["name"] == self.parameters["name"]), None)
+
+ def get_ha_group(self, ha_group_id):
+ api = "api/v3/private/ha-groups/%s" % ha_group_id
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def create_ha_group(self):
+ api = "api/v3/private/ha-groups"
+ response, error = self.rest_api.post(api, self.data)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def delete_ha_group(self, ha_group_id):
+ api = "api/v3/private/ha-groups/%s" % ha_group_id
+ dummy, error = self.rest_api.delete(api, self.data)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ def update_ha_group(self, ha_group_id):
+ api = "api/v3/private/ha-groups/%s" % ha_group_id
+ response, error = self.rest_api.put(api, self.data)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def apply(self):
+ """
+ Perform pre-checks, call functions and exit
+ """
+
+ ha_group = None
+
+ if self.parameters.get("ha_group_id"):
+ ha_group = self.get_ha_group(self.parameters["ha_group_id"])
+ else:
+ ha_group_id = self.get_ha_group_id()
+ if ha_group_id:
+ ha_group = self.get_ha_group(ha_group_id)
+
+ cd_action = self.na_helper.get_cd_action(ha_group, self.parameters)
+
+ if cd_action is None and self.parameters["state"] == "present":
+ # let's see if we need to update parameters
+ modify = self.na_helper.get_modified_attributes(ha_group, self.data)
+
+ result_message = ""
+ resp_data = {}
+
+ if self.na_helper.changed and not self.module.check_mode:
+ if cd_action == "delete":
+ self.delete_ha_group(ha_group["id"])
+ result_message = "HA Group deleted"
+ elif cd_action == "create":
+ resp_data = self.create_ha_group()
+ result_message = "HA Group created"
+ elif modify:
+ resp_data = self.update_ha_group(ha_group["id"])
+ result_message = "HA Group updated"
+
+ self.module.exit_json(changed=self.na_helper.changed, msg=result_message, resp=resp_data)
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_grid_ha_group = SgGridHaGroup()
+ na_sg_grid_ha_group.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_identity_federation.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_identity_federation.py
new file mode 100644
index 000000000..729cf4545
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_identity_federation.py
@@ -0,0 +1,335 @@
+#!/usr/bin/python
+
+# (c) 2021, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage Grid Identity Federation"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+module: na_sg_grid_identity_federation
+short_description: NetApp StorageGRID manage Grid identity federation.
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '21.6.0'
+author: NetApp Ansible Team (@joshedmonds) <ng-ansibleteam@netapp.com>
+description:
+- Configure Grid Identity Federation within NetApp StorageGRID.
+- If module is run with I(check_mode), a connectivity test will be performed using the supplied values without changing the configuration.
+- This module is idempotent if I(password) is not specified.
+options:
+ state:
+ description:
+ - Whether identity federation should be enabled or not.
+ type: str
+ choices: ['present', 'absent']
+ default: present
+ username:
+ description:
+ - The username to bind to the LDAP server.
+ type: str
+ password:
+ description:
+ - The password associated with the username.
+ type: str
+ hostname:
+ description:
+ - The hostname or IP address of the LDAP server.
+ type: str
+ port:
+ description:
+ - The port used to connect to the LDAP server. Typically 389 for LDAP, or 636 for LDAPS.
+ type: int
+ base_group_dn:
+ description:
+ - The Distinguished Name of the LDAP subtree to search for groups.
+ type: str
+ base_user_dn:
+ description:
+ - The Distinguished Name of the LDAP subtree to search for users.
+ type: str
+ ldap_service_type:
+ description:
+ - The type of LDAP server.
+ choices: ['Active Directory', 'OpenLDAP', 'Other']
+ type: str
+ type:
+ description:
+ - The type of identity source.
+ - Default is C(ldap).
+ type: str
+ default: ldap
+ ldap_user_id_attribute:
+ description:
+ - The LDAP attribute which contains the unique user name of a user.
+ - Should be configured if I(ldap_service_type=Other).
+ type: str
+ ldap_user_uuid_attribute:
+ description:
+ - The LDAP attribute which contains the permanent unique identity of a user.
+ - Should be configured if I(ldap_service_type=Other).
+ type: str
+ ldap_group_id_attribute:
+ description:
+ - The LDAP attribute which contains the group for a user.
+ - Should be configured if I(ldap_service_type=Other).
+ type: str
+ ldap_group_uuid_attribute:
+ description:
+ - The LDAP attribute which contains the group's permanent unique identity.
+ - Should be configured if I(ldap_service_type=Other).
+ type: str
+ tls:
+ description:
+ - Whether Transport Layer Security is used to connect to the LDAP server.
+ choices: ['STARTTLS', 'LDAPS', 'Disabled']
+ type: str
+ default: STARTTLS
+ ca_cert:
+ description:
+ - Custom certificate used to connect to the LDAP server.
+ - If a custom certificate is not supplied, the operating system CA certificate will be used.
+ type: str
+"""
+
+EXAMPLES = """
+ - name: test identity federation configuration
+ netapp.storagegrid.na_sg_grid_identity_federation:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ ldap_service_type: "Active Directory"
+ hostname: "ad.example.com"
+ port: 389
+ username: "binduser"
+ password: "bindpass"
+ base_group_dn: "DC=example,DC=com"
+ base_user_dn: "DC=example,DC=com"
+ tls: "Disabled"
+ check_mode: yes
+
+ - name: configure identity federation with AD and TLS
+ netapp.storagegrid.na_sg_grid_identity_federation:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ ldap_service_type: "Active Directory"
+ hostname: "ad.example.com"
+ port: 636
+ username: "binduser"
+ password: "bindpass"
+ base_group_dn: "DC=example,DC=com"
+ base_user_dn: "DC=example,DC=com"
+ tls: "LDAPS"
+ ca_cert: |
+ -----BEGIN CERTIFICATE-----
+ MIIC+jCCAeICCQDmn9Gow08LTzANBgkqhkiG9w0BAQsFADA/..swCQYDVQQGEwJV
+ bXBsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB..JFzNIXQEGnsgjV
+ JGU4giuvOLOZ8Q3gyuUbkSUQDjmjpMR8PliwJ6iW2Ity89Dv..dl1TaIYI/ansyZ
+ Uxk4YXeN6kUkrDtNxCg1McALzXVAfxMTtj2SFlLxne4Z6rX2..UyftQrfM13F1vY
+ gK8dBPz+l+X/Uozo/xNm7gxe68p9le9/pcULst1CQn5/sPqq..kgWcSvlKUItu82
+ lq3B2169rovdIaNdcvaQjMPhrDGo5rvLfMN35U3Hgbz41PL5..x2BcUE6/0ab5T4
+ qKBxKa3t9twj+zpUqOzyL0PFfCE+SK5fEXAS1ow4eAcLN+eB..gR/PuvGAyIPCtE
+ 1+X4GrECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAFpO+04Ra..FMJPH6dBmzfb7l
+ k04BWTvSlur6HiQdXY+oFQMJZzyI7MQ8v9HBIzS0ZAzYWLp4..VZhHmRxnrWyxVs
+ u783V5YfQH2L4QnBDoiDefgxyfDs2PcoF5C+X9CGXmPqzst2..y/6tdOVJzdiA==
+ -----END CERTIFICATE-----
+"""
+
+RETURN = """
+resp:
+ description: Returns information about the StorageGRID management identity source configuration.
+ returned: success
+ type: dict
+ sample: {
+ "id": "00000000-0000-0000-0000-000000000000",
+ "disable": false,
+ "hostname": "10.1.2.3",
+ "port": 389,
+ "username": "MYDOMAIN\\\\Administrator",
+ "password": "********",
+ "baseGroupDn": "DC=example,DC=com",
+ "baseUserDn": "DC=example,DC=com",
+ "ldapServiceType": "Active Directory",
+ "type": "ldap",
+ "disableTLS": false,
+ "enableLDAPS": false,
+ "caCert": "-----BEGIN CERTIFICATE----- abcdefghijkl123456780ABCDEFGHIJKL 123456/7890ABCDEFabcdefghijklABCD -----END CERTIFICATE-----\n"
+ }
+"""
+
+import json
+import re
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class SgGridIdentityFederation:
+ """
+ Configure and modify StorageGRID Grid Identity Federation
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ state=dict(required=False, type="str", choices=["present", "absent"], default="present"),
+ username=dict(required=False, type="str"),
+ password=dict(required=False, type="str", no_log=True),
+ hostname=dict(required=False, type="str"),
+ port=dict(required=False, type="int"),
+ base_group_dn=dict(required=False, type="str"),
+ base_user_dn=dict(required=False, type="str"),
+ ldap_service_type=dict(required=False, type="str", choices=["OpenLDAP", "Active Directory", "Other"]),
+ type=dict(required=False, type="str", default="ldap"),
+ ldap_user_id_attribute=dict(required=False, type="str"),
+ ldap_user_uuid_attribute=dict(required=False, type="str"),
+ ldap_group_id_attribute=dict(required=False, type="str"),
+ ldap_group_uuid_attribute=dict(required=False, type="str"),
+ tls=dict(required=False, type="str", choices=["STARTTLS", "LDAPS", "Disabled"], default="STARTTLS"),
+ ca_cert=dict(required=False, type="str"),
+ ),
+ )
+
+ parameter_map = {
+ "username": "username",
+ "password": "password",
+ "hostname": "hostname",
+ "port": "port",
+ "base_group_dn": "baseGroupDn",
+ "base_user_dn": "baseUserDn",
+ "ldap_service_type": "ldapServiceType",
+ "ldap_user_id_attribute": "ldapUserIdAttribute",
+ "ldap_user_uuid_attribute": "ldapUserUUIDAttribute",
+ "ldap_group_id_attribute": "ldapGroupIdAttribute",
+ "ldap_group_uuid_attribute": "ldapGroupUUIDAttribute",
+ "ca_cert": "caCert",
+ }
+ self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True,)
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+ # Checking for the parameters passed and create new parameters list
+ self.data = {}
+
+ if self.parameters["state"] == "present":
+ self.data["disable"] = False
+
+ for k in parameter_map.keys():
+ if self.parameters.get(k) is not None:
+ self.data[parameter_map[k]] = self.parameters[k]
+
+ if self.parameters.get("tls") == "STARTTLS":
+ self.data["disableTLS"] = False
+ self.data["enableLDAPS"] = False
+ elif self.parameters.get("tls") == "LDAPS":
+ self.data["disableTLS"] = False
+ self.data["enableLDAPS"] = True
+ else:
+ self.data["disableTLS"] = True
+ self.data["enableLDAPS"] = False
+
+ def get_grid_identity_source(self):
+ api = "api/v3/grid/identity-source"
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+ else:
+ return response["data"]
+ return None
+
+ def update_identity_federation(self, test=False):
+ api = "api/v3/grid/identity-source"
+
+ params = {}
+
+ if test:
+ params["test"] = True
+
+ response, error = self.rest_api.put(api, self.data, params=params)
+ if error:
+ self.module.fail_json(msg=error, payload=self.data)
+
+ if response is not None:
+ return response["data"]
+ else:
+ return None
+
+ def apply(self):
+ """
+ Perform pre-checks, call functions and exit
+ """
+ grid_identity_source = self.get_grid_identity_source()
+
+ cd_action = self.na_helper.get_cd_action(grid_identity_source, self.parameters)
+
+ if cd_action is None and self.parameters["state"] == "present":
+ # let's see if we need to update parameters
+ update = False
+
+ for k in (i for i in self.data.keys() if i != "password"):
+ if self.data[k] != grid_identity_source.get(k):
+ update = True
+ break
+
+ # if a password has been specified we need to update it
+ if self.data.get("password") and self.parameters["state"] == "present":
+ update = True
+ self.module.warn("Password attribute has been specified. Task is not idempotent.")
+
+ if update:
+ self.na_helper.changed = True
+
+ if cd_action == "delete":
+ # if identity federation is already in a disable state
+ if grid_identity_source.get("disable"):
+ self.na_helper.changed = False
+
+ result_message = ""
+ resp_data = grid_identity_source
+
+ if self.na_helper.changed and not self.module.check_mode:
+ if cd_action == "delete":
+ self.data = dict(disable=True)
+ resp_data = self.update_identity_federation()
+ result_message = "Grid identity federation disabled"
+ else:
+ resp_data = self.update_identity_federation()
+ result_message = "Grid identity federation updated"
+
+ if self.module.check_mode:
+ self.update_identity_federation(test=True)
+ # if no error, connection test successful
+ self.module.exit_json(changed=self.na_helper.changed, msg="Connection test successful")
+
+ self.module.exit_json(changed=self.na_helper.changed, msg=result_message, resp=resp_data)
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_grid_identity_federation = SgGridIdentityFederation()
+ na_sg_grid_identity_federation.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_info.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_info.py
new file mode 100644
index 000000000..b14f88a22
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_info.py
@@ -0,0 +1,405 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" NetApp StorageGRID Grid Info using REST APIs """
+
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = """
+module: na_sg_grid_info
+author: NetApp Ansible Team (@jasonl4) <ng-ansibleteam@netapp.com>
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+short_description: NetApp StorageGRID Grid information gatherer.
+description:
+ - This module allows you to gather various information about StorageGRID Grid configuration.
+version_added: 20.11.0
+
+options:
+ gather_subset:
+ type: list
+ elements: str
+ description:
+ - When supplied, this argument will restrict the information collected to a given subset.
+ - Either the info name or the REST API can be given.
+ - Possible values for this argument include
+ - C(grid_accounts_info) or C(grid/accounts)
+ - C(grid_alarms_info) or C(grid/alarms)
+ - C(grid_audit_info) or C(grid/audit)
+ - C(grid_compliance_global_info) or C(grid/compliance-global)
+ - C(grid_config_info) or C(grid/config)
+ - C(grid_config_management_info) or C(grid/config/management)
+ - C(grid_config_product_version_info) or C(grid/config/product-version)
+ - C(grid_deactivated_features_info) or C(grid/deactivated-features)
+ - C(grid_dns_servers_info) or C(grid/dns-servers)
+ - C(grid_domain_names_info) or C(grid/domain-names)
+ - C(grid_ec_profiles_info) or C(grid/ec-profiles)
+ - C(grid_expansion_info) or C(grid/expansion)
+ - C(grid_expansion_nodes_info) or C(grid/expansion/nodes)
+ - C(grid_expansion_sites_info) or C(grid/expansion/sites)
+ - C(grid_grid_networks_info) or C(grid/grid-networks)
+ - C(grid_groups_info) or C(grid/groups)
+ - C(grid_health_info) or C(grid/health)
+ - C(grid_health_topology_info) or C(grid/health/topology)
+ - C(grid_identity_source_info) or C(grid/identity-source)
+ - C(grid_ilm_criteria_info) or C(grid/ilm-criteria)
+ - C(grid_ilm_policies_info) or C(grid/ilm-policies)
+ - C(grid_ilm_rules_info) or C(grid/ilm-rules)
+ - C(grid_license_info) or C(grid/license)
+ - C(grid_management_certificate_info) or C(grid/management-certificate)
+ - C(grid_ntp_servers_info) or C(grid/ntp-servers)
+ - C(grid_recovery_available_nodes_info) or C(grid/recovery/available-nodes)
+ - C(grid_recovery_info) or C(grid/recovery)
+ - C(grid_regions_info) or C(grid/regions)
+ - C(grid_schemes_info) or C(grid/schemes)
+ - C(grid_snmp_info) or C(grid/snmp)
+ - C(grid_storage_api_certificate_info) or C(grid/storage-api-certificate)
+ - C(grid_untrusted_client_network_info) or C(grid/untrusted-client-network)
+ - C(grid_users_info) or C(grid/users)
+ - C(grid_users_root_info) or C(grid/users/root)
+ - C(versions_info) or C(versions)
+ - Can specify a list of values to include a larger subset.
+ default: all
+ parameters:
+ description:
+ - Allows for any rest option to be passed in.
+ type: dict
+"""
+
+EXAMPLES = """
+- name: Gather StorageGRID Grid info
+ netapp.storagegrid.na_sg_grid_info:
+ api_url: "https://1.2.3.4/"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ register: sg_grid_info
+
+- name: Gather StorageGRID Grid info for grid/accounts and grid/config subsets
+ netapp.storagegrid.na_sg_grid_info:
+ api_url: "https://1.2.3.4/"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ gather_subset:
+ - grid_accounts_info
+ - grid/config
+ register: sg_grid_info
+
+- name: Gather StorageGRID Grid info for all subsets
+ netapp.storagegrid.na_sg_grid_info:
+ api_url: "https://1.2.3.4/"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ gather_subset:
+ - all
+ register: sg_grid_info
+
+- name: Gather StorageGRID Grid info for grid/accounts and grid/users subsets, limit to 5 results for each subset
+ netapp.storagegrid.na_sg_grid_info:
+ api_url: "https://1.2.3.4/"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ gather_subset:
+ - grid/accounts
+ - grid/users
+ parameters:
+ limit: 5
+ register: sg_grid_info
+"""
+
+RETURN = """
+sg_info:
+ description: Returns various information about the StorageGRID Grid configuration.
+ returned: always
+ type: dict
+ sample: {
+ "grid/accounts": {...},
+ "grid/alarms": {...},
+ "grid/audit": {...},
+ "grid/compliance-global": {...},
+ "grid/config": {...},
+ "grid/config/management": {...},
+ "grid/config/product-version": {...},
+ "grid/deactivated-features": {...},
+ "grid/dns-servers": {...},
+ "grid/domain-names": {...},
+ "grid/ec-profiles": {...},
+ "grid/expansion": {...},
+ "grid/expansion/nodes": {...},
+ "grid/expansion/sites": {...},
+ "grid/networks": {...},
+ "grid/groups": {...},
+ "grid/health": {...},
+ "grid/health/topology": {...},
+ "grid/identity-source": {...},
+ "grid/ilm-criteria": {...},
+ "grid/ilm-policies": {...},
+ "grid/ilm-rules": {...},
+ "grid/license": {...},
+ "grid/management-certificate": {...},
+ "grid/ntp-servers": {...},
+ "grid/recovery/available-nodes": {...},
+ "grid/recovery": {...},
+ "grid/regions": {...},
+ "grid/schemes": {...},
+ "grid/snmp": {...},
+ "grid/storage-api-certificate": {...},
+ "grid/untrusted-client-network": {...},
+ "grid/users": {...},
+ "grid/users/root": {...},
+ "grid/versions": {...}
+ }
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class NetAppSgGatherInfo(object):
+ """ Class with gather info methods """
+
+ def __init__(self):
+ """
+ Parse arguments, setup variables, check parameters and ensure
+ request module is installed.
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(dict(
+ gather_subset=dict(default=['all'], type='list', elements='str', required=False),
+ parameters=dict(type='dict', required=False)
+ ))
+
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ supports_check_mode=True
+ )
+
+ # set up variables
+ self.na_helper = NetAppModule()
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ self.rest_api = SGRestAPI(self.module)
+
+ def get_subset_info(self, gather_subset_info):
+ """
+ Gather StorageGRID information for the given subset using REST APIs
+ Input for REST APIs call : (api, data)
+ return gathered_sg_info
+ """
+
+ api = gather_subset_info['api_call']
+ data = {}
+ # allow for passing in any additional rest api parameters
+ if self.parameters.get('parameters'):
+ for each in self.parameters['parameters']:
+ data[each] = self.parameters['parameters'][each]
+
+ gathered_sg_info, error = self.rest_api.get(api, data)
+
+ if error:
+ self.module.fail_json(msg=error)
+ else:
+ return gathered_sg_info
+
+ return None
+
+ def convert_subsets(self):
+ """ Convert an info to the REST API """
+ info_to_rest_mapping = {
+ 'grid_accounts_info': 'grid/accounts',
+ 'grid_alarms_info': 'grid/alarms',
+ 'grid_audit_info': 'grid/audit',
+ 'grid_compliance_global_info': 'grid/compliance-global',
+ 'grid_config_info': 'grid/config',
+ 'grid_config_management_info': 'grid/config/management',
+ 'grid_config_product_version_info': 'grid/config/product-version',
+ 'grid_deactivated_features_info': 'grid/deactivated-features',
+ 'grid_dns_servers_info': 'grid/dns-servers',
+ 'grid_domain_names_info': 'grid/domain-names',
+ 'grid_ec_profiles_info': 'grid/ec-profiles',
+ 'grid_expansion_info': 'grid/expansion',
+ 'grid_expansion_nodes_info': 'grid/expansion/nodes',
+ 'grid_expansion_sites_info': 'grid/expansion/sites',
+ 'grid_grid_networks_info': 'grid/grid-networks',
+ 'grid_groups_info': 'grid/groups',
+ 'grid_health_info': 'grid/health',
+ 'grid_health_topology_info': 'grid/health/topology',
+ 'grid_identity_source_info': 'grid/identity-source',
+ 'grid_ilm_criteria_info': 'grid/ilm-criteria',
+ 'grid_ilm_policies_info': 'grid/ilm-policies',
+ 'grid_ilm_rules_info': 'grid/ilm-rules',
+ 'grid_license_info': 'grid/license',
+ 'grid_management_certificate_info': 'grid/management-certificate',
+ 'grid_ntp_servers_info': 'grid/ntp-servers',
+ 'grid_recovery_available_nodes_info': 'grid/recovery/available-nodes',
+ 'grid_recovery_info': 'grid/recovery',
+ 'grid_regions_info': 'grid/regions',
+ 'grid_schemes_info': 'grid/schemes',
+ 'grid_snmp_info': 'grid/snmp',
+ 'grid_storage_api_certificate_info': 'grid/storage-api-certificate',
+ 'grid_untrusted_client_network_info': 'grid/untrusted-client-network',
+ 'grid_users_info': 'grid/users',
+ 'grid_users_root_info': 'grid/users/root',
+ 'versions_info': 'versions',
+ }
+ # Add rest API names as there info version, also make sure we don't add a duplicate
+ subsets = []
+ for subset in self.parameters['gather_subset']:
+ if subset in info_to_rest_mapping:
+ if info_to_rest_mapping[subset] not in subsets:
+ subsets.append(info_to_rest_mapping[subset])
+ else:
+ if subset not in subsets:
+ subsets.append(subset)
+ return subsets
+
+ def apply(self):
+ """ Perform pre-checks, call functions and exit """
+
+ result_message = dict()
+
+ # Defining gather_subset and appropriate api_call
+ get_sg_subset_info = {
+ 'grid/accounts': {
+ 'api_call': 'api/v3/grid/accounts',
+ },
+ 'grid/alarms': {
+ 'api_call': 'api/v3/grid/alarms',
+ },
+ 'grid/audit': {
+ 'api_call': 'api/v3/grid/audit',
+ },
+ 'grid/compliance-global': {
+ 'api_call': 'api/v3/grid/compliance-global',
+ },
+ 'grid/config': {
+ 'api_call': 'api/v3/grid/config',
+ },
+ 'grid/config/management': {
+ 'api_call': 'api/v3/grid/config/management',
+ },
+ 'grid/config/product-version': {
+ 'api_call': 'api/v3/grid/config/product-version',
+ },
+ 'grid/deactivated-features': {
+ 'api_call': 'api/v3/grid/deactivated-features',
+ },
+ 'grid/dns-servers': {
+ 'api_call': 'api/v3/grid/dns-servers',
+ },
+ 'grid/domain-names': {
+ 'api_call': 'api/v3/grid/domain-names',
+ },
+ 'grid/ec-profiles': {
+ 'api_call': 'api/v3/grid/ec-profiles',
+ },
+ 'grid/expansion': {
+ 'api_call': 'api/v3/grid/expansion',
+ },
+ 'grid/expansion/nodes': {
+ 'api_call': 'api/v3/grid/expansion/nodes',
+ },
+ 'grid/expansion/sites': {
+ 'api_call': 'api/v3/grid/expansion/sites',
+ },
+ 'grid/grid-networks': {
+ 'api_call': 'api/v3/grid/grid-networks',
+ },
+ 'grid/groups': {
+ 'api_call': 'api/v3/grid/groups',
+ },
+ 'grid/health': {
+ 'api_call': 'api/v3/grid/health',
+ },
+ 'grid/health/topology': {
+ 'api_call': 'api/v3/grid/health/topology',
+ },
+ 'grid/identity-source': {
+ 'api_call': 'api/v3/grid/identity-source',
+ },
+ 'grid/ilm-criteria': {
+ 'api_call': 'api/v3/grid/ilm-criteria',
+ },
+ 'grid/ilm-policies': {
+ 'api_call': 'api/v3/grid/ilm-policies',
+ },
+ 'grid/ilm-rules': {
+ 'api_call': 'api/v3/grid/ilm-rules',
+ },
+ 'grid/license': {
+ 'api_call': 'api/v3/grid/license',
+ },
+ 'grid/management-certificate': {
+ 'api_call': 'api/v3/grid/management-certificate',
+ },
+ 'grid/ntp-servers': {
+ 'api_call': 'api/v3/grid/ntp-servers',
+ },
+ 'grid/recovery/available-nodes': {
+ 'api_call': 'api/v3/grid/recovery/available-nodes',
+ },
+ 'grid/recovery': {
+ 'api_call': 'api/v3/grid/recovery',
+ },
+ 'grid/regions': {
+ 'api_call': 'api/v3/grid/regions',
+ },
+ 'grid/schemes': {
+ 'api_call': 'api/v3/grid/schemes',
+ },
+ 'grid/snmp': {
+ 'api_call': 'api/v3/grid/snmp',
+ },
+ 'grid/storage-api-certificate': {
+ 'api_call': 'api/v3/grid/storage-api-certificate',
+ },
+ 'grid/untrusted-client-network': {
+ 'api_call': 'api/v3/grid/untrusted-client-network',
+ },
+ 'grid/users': {
+ 'api_call': 'api/v3/grid/users',
+ },
+ 'grid/users/root': {
+ 'api_call': 'api/v3/grid/users/root',
+ },
+ 'versions': {
+ 'api_call': 'api/v3/versions',
+ },
+ }
+
+ if 'all' in self.parameters['gather_subset']:
+ # If all in subset list, get the information of all subsets
+ self.parameters['gather_subset'] = sorted(get_sg_subset_info.keys())
+
+ converted_subsets = self.convert_subsets()
+
+ for subset in converted_subsets:
+ try:
+ # Verify whether the supported subset passed
+ specified_subset = get_sg_subset_info[subset]
+ except KeyError:
+ self.module.fail_json(msg="Specified subset %s not found, supported subsets are %s" %
+ (subset, list(get_sg_subset_info.keys())))
+
+ result_message[subset] = self.get_subset_info(specified_subset)
+
+ self.module.exit_json(changed='False', sg_info=result_message)
+
+
+def main():
+ """ Main function """
+ obj = NetAppSgGatherInfo()
+ obj.apply()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_ntp.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_ntp.py
new file mode 100644
index 000000000..0c22ba2c1
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_ntp.py
@@ -0,0 +1,173 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage Grid NTP Servers"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+module: na_sg_grid_ntp
+short_description: NetApp StorageGRID manage external NTP servers for the grid.
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '20.6.0'
+author: NetApp Ansible Team (@jkandati) <ng-sg-ansibleteam@netapp.com>
+description:
+- Update NTP server on NetApp StorageGRID.
+options:
+ state:
+ description:
+ - Whether the specified user should exist or not.
+ type: str
+ choices: ['present']
+ default: present
+ ntp_servers:
+ description:
+ - List of comma separated NTP server address.
+ type: list
+ elements: str
+ required: true
+ passphrase:
+ description:
+ - passphrase for GRID.
+ type: str
+ required: true
+"""
+
+EXAMPLES = """
+ - name: update NTP servers
+ netapp.storagegrid.na_sg_grid_ntp:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ passphrase: "{{ grid_pass }}"
+ ntp_servers: "x.x.x.x,xx.x.xx.x"
+"""
+
+RETURN = """
+resp:
+ description: Returns information about the configured NTP servers.
+ returned: success
+ type: list
+ elements: str
+ sample: ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4"]
+"""
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class SgGridNtp(object):
+ """
+ Create, modify and delete NTP entries for StorageGRID
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ state=dict(required=False, type="str", choices=["present"], default="present"),
+ ntp_servers=dict(required=True, type="list", elements="str"),
+ passphrase=dict(required=True, type="str", no_log=True),
+ )
+ )
+
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ # required_if=[("state", "present", ["state", "name", "protocol"])],
+ supports_check_mode=True,
+ )
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+ # Checking for the parameters passed and create new parameters list
+ self.data = self.parameters["ntp_servers"]
+ self.passphrase = self.parameters["passphrase"]
+ self.ntp_input = {"passphrase": self.passphrase, "servers": self.data}
+
+ def get_grid_ntp(self):
+ # Check if tenant account exists
+ # Return tenant account info if found, or None
+ api = "api/v3/grid/ntp-servers"
+
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def update_grid_ntp(self):
+ api = "api/v3/grid/ntp-servers/update"
+
+ response, error = self.rest_api.post(api, self.ntp_input)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def apply(self):
+ """
+ Perform pre-checks, call functions and exit
+ """
+ grid_ntp = self.get_grid_ntp()
+
+ cd_action = self.na_helper.get_cd_action(grid_ntp, self.parameters["ntp_servers"])
+
+ if cd_action is None and self.parameters["state"] == "present":
+ # let's see if we need to update parameters
+ update = False
+
+ ntp_diff = [i for i in self.data + grid_ntp if i not in self.data or i not in grid_ntp]
+ if ntp_diff:
+ update = True
+
+ if update:
+ self.na_helper.changed = True
+
+ result_message = ""
+ resp_data = grid_ntp
+ if self.na_helper.changed:
+ if self.module.check_mode:
+ pass
+ else:
+ resp_data = self.update_grid_ntp()
+ result_message = "Grid NTP updated"
+
+ self.module.exit_json(changed=self.na_helper.changed, msg=result_message, resp=resp_data)
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_grid_ntp = SgGridNtp()
+ na_sg_grid_ntp.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_regions.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_regions.py
new file mode 100644
index 000000000..58179cf03
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_regions.py
@@ -0,0 +1,163 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage Grid Regions"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+module: na_sg_grid_regions
+short_description: NetApp StorageGRID manage Regions for the grid.
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '20.6.0'
+author: NetApp Ansible Team (@joshedmonds) <ng-ansibleteam@netapp.com>
+description:
+- Create, Update, Delete Users within a NetApp StorageGRID tenant.
+options:
+ state:
+ description:
+ - Whether the specified user should exist or not.
+ type: str
+ choices: ['present']
+ default: present
+ regions:
+ description:
+ - List of regions
+ required: true
+ type: list
+ elements: str
+"""
+
+EXAMPLES = """
+ - name: update Regions
+ netapp.storagegrid.na_sg_grid_regions:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ regions: "us-east-1"
+"""
+
+RETURN = """
+resp:
+ description: Returns information about the configured regions.
+ returned: success
+ type: list
+ elements: str
+ sample: ["us-east-1", "us-central-1"]
+"""
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class SgGridRegions(object):
+ """
+ Create, modify and delete Regions for StorageGRID
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ state=dict(required=False, type="str", choices=["present"], default="present"),
+ regions=dict(required=True, type="list", elements="str"),
+ )
+ )
+
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ # required_if=[("state", "present", ["state", "name", "protocol"])],
+ supports_check_mode=True,
+ )
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+ # Checking for the parameters passed and create new parameters list
+ self.data = self.parameters["regions"]
+
+ def get_grid_regions(self):
+ # Check if tenant account exists
+ # Return tenant account info if found, or None
+ api = "api/v3/grid/regions"
+
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def update_grid_regions(self):
+ api = "api/v3/grid/regions"
+
+ response, error = self.rest_api.put(api, self.data)
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def apply(self):
+ """
+ Perform pre-checks, call functions and exit
+ """
+ grid_regions = self.get_grid_regions()
+
+ cd_action = self.na_helper.get_cd_action(grid_regions, self.parameters["regions"])
+
+ if cd_action is None and self.parameters["state"] == "present":
+ # let's see if we need to update parameters
+ update = False
+
+ regions_diff = [i for i in self.data + grid_regions if i not in self.data or i not in grid_regions]
+ if regions_diff:
+ update = True
+
+ if update:
+ self.na_helper.changed = True
+
+ result_message = ""
+ resp_data = grid_regions
+ if self.na_helper.changed:
+ if self.module.check_mode:
+ pass
+ else:
+ resp_data = self.update_grid_regions()
+ result_message = "Grid Regions updated"
+
+ self.module.exit_json(changed=self.na_helper.changed, msg=result_message, resp=resp_data)
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_grid_regions = SgGridRegions()
+ na_sg_grid_regions.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_traffic_classes.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_traffic_classes.py
new file mode 100644
index 000000000..9901a3e00
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_traffic_classes.py
@@ -0,0 +1,375 @@
+#!/usr/bin/python
+
+# (c) 2022, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage Traffic Classification Policies"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+module: na_sg_grid_traffic_classes
+short_description: Manage Traffic Classification Policy configuration on StorageGRID.
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '21.10.0'
+author: NetApp Ansible Team (@joshedmonds) <ng-ansibleteam@netapp.com>
+description:
+- Create, Update, Delete Traffic Classification Policies on NetApp StorageGRID.
+options:
+ state:
+ description:
+ - Whether the specified Traffic Classification Policy should exist.
+ type: str
+ choices: ['present', 'absent']
+ default: present
+ name:
+ description:
+ - Name of the Traffic Classification Policy.
+ type: str
+ policy_id:
+ description:
+ - Traffic Classification Policy ID.
+ - May be used for modify or delete operation.
+ type: str
+ description:
+ description:
+ - Description of the Traffic Classification Policy.
+ type: str
+ matchers:
+ description:
+ - A set of parameters to match.
+ - The traffic class will match requests where any of these matchers match.
+ type: list
+ elements: dict
+ suboptions:
+ type:
+ description:
+ - The attribute of the request to match.
+ - C(bucket) - The S3 bucket (or Swift container) being accessed.
+ - C(bucket-regex) - A regular expression to evaluate against the S3 bucket (or Swift container) being accessed.
+ - C(cidr) - Matches if the client request source IP is in the specified IPv4 CIDR (RFC4632).
+ - C(tenant) - Matches if the S3 bucket (or Swift container) is owned by the tenant account with this ID.
+ choices: ['bucket', 'bucket-regex', 'cidr', 'endpoint', 'tenant']
+ type: str
+ required: true
+ inverse:
+ description:
+ - If I(true), entities that match the value are excluded.
+ type: bool
+ default: false
+ members:
+ description:
+ - A list of members to match on.
+ type: list
+ elements: str
+ required: true
+ limits:
+ description:
+ - Optional limits to impose on client requests matched by this traffic class.
+ - Only one of each limit type can be specified.
+ type: list
+ elements: dict
+ suboptions:
+ type:
+ description:
+ - The type of limit to apply.
+ - C(aggregateBandwidthIn) - The maximum combined upload bandwidth in bytes/second of all concurrent requests that match this policy.
+ - C(aggregateBandwidthOut) - The maximum combined download bandwidth in bytes/second of all concurrent requests that match this policy.
+ - C(concurrentReadRequests) - The maximum number of download requests that can be in progress at the same time.
+ - C(concurrentWriteRequests) - The maximum number of upload requests that can be in progress at the same time.
+ - C(readRequestRate) - The maximum number of download requests that can be started each second.
+ - C(writeRequestRate) - The maximum number of download requests that can be started each second.
+ - C(perRequestBandwidthIn) - The maximum upload bandwidth in bytes/second allowed for each request that matches this policy.
+ - C(perRequestBandwidthOut) - The maximum download bandwidth in bytes/second allowed for each request that matches this policy.
+ choices: [
+ 'aggregateBandwidthIn',
+ 'aggregateBandwidthOut',
+ 'concurrentReadRequests',
+ 'concurrentWriteRequests',
+ 'readRequestRate',
+ 'writeRequestRate',
+ 'perRequestBandwidthIn',
+ 'perRequestBandwidthOut'
+ ]
+ type: str
+ required: true
+ value:
+ description:
+ - The limit to apply.
+ - Limit values are type specific.
+ type: int
+ required: true
+"""
+
+EXAMPLES = """
+ - name: create Traffic Classification Policy with bandwidth limit on buckets
+ netapp.storagegrid.na_sg_grid_traffic_classes:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ name: Traffic-Policy1
+ matchers:
+ - type: bucket
+ members: bucket1,anotherbucket
+ limits:
+ - type: aggregateBandwidthOut
+ value: 100000000
+
+ - name: create Traffic Classification Policy with bandwidth limits except for specific tenant account
+ netapp.storagegrid.na_sg_grid_traffic_classes:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ name: Fabricpool-Policy
+ description: "Limit all to 500MB/s except FabricPool tenant"
+ matchers:
+ - type: tenant
+ inverse: True
+ members: 12345678901234567890
+ limits:
+ - type: aggregateBandwidthIn
+ value: 50000000
+ - type: aggregateBandwidthOut
+ value: 50000000
+
+ - name: rename Traffic Classification Policy
+ netapp.storagegrid.na_sg_grid_traffic_classes:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ policy_id: 00000000-0000-0000-0000-000000000000
+ name: Traffic-Policy1-New-Name
+ matchers:
+ - type: bucket
+ members: bucket1,anotherbucket
+ limits:
+ - type: aggregateBandwidthOut
+ value: 100000000
+
+ - name: delete Traffic Classification Policy
+ netapp.storagegrid.na_sg_grid_traffic_classes:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: absent
+ name: Traffic-Policy1
+"""
+
+RETURN = """
+resp:
+ description: Returns information about the StorageGRID Traffic Classification Policy.
+ returned: success
+ type: dict
+ sample: {
+ "id": "6b2946e6-7fed-40d0-9262-8e922580aba7",
+ "name": "Traffic-Policy1",
+ "description": "Traffic Classification Policy 1",
+ "matchers": [
+ {
+ "type": "cidr",
+ "inverse": False,
+ "members": [
+ "192.168.50.0/24"
+ ]
+ },
+ {
+ "type": "bucket",
+ "inverse": False,
+ "members": [
+ "mybucket1",
+ "mybucket2"
+ ]
+ },
+ ],
+ "limits": [
+ {
+ "type": "aggregateBandwidthOut",
+ "value": 100000000
+ }
+ ],
+ }
+"""
+
+import json
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class SgGridTrafficClasses:
+ """
+ Create, modify and delete Traffic Classification Policies for StorageGRID
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ state=dict(required=False, type="str", choices=["present", "absent"], default="present"),
+ name=dict(required=False, type="str"),
+ policy_id=dict(required=False, type="str"),
+ description=dict(required=False, type="str"),
+ matchers=dict(
+ required=False,
+ type="list",
+ elements="dict",
+ options=dict(
+ type=dict(
+ required=True,
+ type="str",
+ choices=["bucket", "bucket-regex", "cidr", "endpoint", "tenant"],
+ ),
+ inverse=dict(required=False, type="bool", default="false"),
+ members=dict(required=True, type="list", elements="str"),
+ ),
+ ),
+ limits=dict(
+ required=False,
+ type="list",
+ elements="dict",
+ options=dict(
+ type=dict(
+ required=True,
+ type="str",
+ choices=[
+ "aggregateBandwidthIn",
+ "aggregateBandwidthOut",
+ "concurrentReadRequests",
+ "concurrentWriteRequests",
+ "readRequestRate",
+ "writeRequestRate",
+ "perRequestBandwidthIn",
+ "perRequestBandwidthOut",
+ ],
+ ),
+ value=dict(required=True, type="int"),
+ ),
+ ),
+ )
+ )
+
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ required_if=[("state", "present", ["name"])],
+ required_one_of=[("name", "policy_id")],
+ )
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+ # Checking for the parameters passed and create new parameters list
+ self.data = {}
+
+ if self.parameters["state"] == "present":
+ for k in ["name", "description", "matchers", "limits"]:
+ if self.parameters.get(k) is not None:
+ self.data[k] = self.parameters[k]
+
+ def get_traffic_class_policy_id(self):
+ # Check if Traffic Classification Policy exists
+ # Return policy ID if found, or None
+ api = "api/v3/grid/traffic-classes/policies"
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return next((item["id"] for item in response.get("data") if item["name"] == self.parameters["name"]), None)
+
+ def get_traffic_class_policy(self, policy_id):
+ api = "api/v3/grid/traffic-classes/policies/%s" % policy_id
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def create_traffic_class_policy(self):
+ api = "api/v3/grid/traffic-classes/policies"
+ # self.module.fail_json(msg=self.data)
+ response, error = self.rest_api.post(api, self.data)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def delete_traffic_class_policy(self, policy_id):
+ api = "api/v3/grid/traffic-classes/policies/%s" % policy_id
+ dummy, error = self.rest_api.delete(api, self.data)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ def update_traffic_class_policy(self, policy_id):
+ api = "api/v3/grid/traffic-classes/policies/%s" % policy_id
+ response, error = self.rest_api.put(api, self.data)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def apply(self):
+ """
+ Perform pre-checks, call functions and exit
+ """
+
+ traffic_class_policy = None
+
+ if self.parameters.get("policy_id"):
+ traffic_class_policy = self.get_traffic_class_policy(self.parameters["policy_id"])
+ else:
+ policy_id = self.get_traffic_class_policy_id()
+ if policy_id:
+ traffic_class_policy = self.get_traffic_class_policy(policy_id)
+
+ cd_action = self.na_helper.get_cd_action(traffic_class_policy, self.parameters)
+
+ if cd_action is None and self.parameters["state"] == "present":
+ # let's see if we need to update parameters
+ modify = self.na_helper.get_modified_attributes(traffic_class_policy, self.data)
+
+ result_message = ""
+ resp_data = {}
+
+ if self.na_helper.changed and not self.module.check_mode:
+ if cd_action == "delete":
+ self.delete_traffic_class_policy(traffic_class_policy["id"])
+ result_message = "Traffic Classification Policy deleted"
+ elif cd_action == "create":
+ resp_data = self.create_traffic_class_policy()
+ result_message = "Traffic Classification Policy created"
+ elif modify:
+ resp_data = self.update_traffic_class_policy(traffic_class_policy["id"])
+ result_message = "Traffic Classification Policy updated"
+
+ self.module.exit_json(changed=self.na_helper.changed, msg=result_message, resp=resp_data)
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_grid_traffic_classes = SgGridTrafficClasses()
+ na_sg_grid_traffic_classes.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_user.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_user.py
new file mode 100644
index 000000000..521d4f566
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_grid_user.py
@@ -0,0 +1,316 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage Grid-administration Users"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+module: na_sg_grid_user
+short_description: NetApp StorageGRID manage users.
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '20.6.0'
+author: NetApp Ansible Team (@joshedmonds) <ng-ansibleteam@netapp.com>
+description:
+- Create, Update, Delete Administrative Users within NetApp StorageGRID.
+options:
+ state:
+ description:
+ - Whether the specified user should exist or not.
+ type: str
+ choices: ['present', 'absent']
+ default: present
+ full_name:
+ description:
+ - Full Name of the user.
+ - Required for create operation
+ type: str
+ unique_name:
+ description:
+ - Unique Name for the user. Must begin with C(user/) or C(federated-user/)
+ - Required for create, modify or delete operation.
+ type: str
+ required: true
+ member_of:
+ description:
+ - List of C(unique_groups) that the user is a member of.
+ type: list
+ elements: str
+ password:
+ description:
+ - Set a password for a local user. Does not apply to federated users.
+ - Requires root privilege.
+ required: false
+ type: str
+ update_password:
+ description:
+ - Choose when to update the password.
+ - When set to C(always), the password will always be updated.
+ - When set to C(on_create), the password will only be set upon a new user creation.
+ default: on_create
+ choices:
+ - on_create
+ - always
+ type: str
+ disable:
+ description:
+ - Disable the user from signing in. Does not apply to federated users.
+ type: bool
+"""
+
+EXAMPLES = """
+ - name: create a user
+ netapp.storagegrid.na_sg_grid_user:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ full_name: ansibleuser100
+ unique_name: user/ansibleuser100
+ member_of: "group/ansiblegroup100"
+ disable: false
+
+"""
+
+RETURN = """
+resp:
+ description: Returns information about the StorageGRID Grid user.
+ returned: always
+ type: dict
+ sample: {
+ "fullName": "Example User",
+ "memberOf": ["00000000-0000-0000-0000-000000000000"],
+ "disable": false,
+ "uniqueName": "user/Example",
+ "accountId": "0",
+ "id": "00000000-0000-0000-0000-000000000000",
+ "federated": false,
+ "userURN": "urn:sgws:identity::0:user/Example"
+ }
+"""
+
+import json
+import re
+
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class SgGridUser(object):
+ """
+ Create, modify and delete user within a StorageGRID Tenant Account
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ state=dict(required=False, type="str", choices=["present", "absent"], default="present"),
+ full_name=dict(required=False, type="str"),
+ unique_name=dict(required=True, type="str"),
+ member_of=dict(required=False, type="list", elements="str"),
+ disable=dict(required=False, type="bool"),
+ password=dict(required=False, type="str", no_log=True),
+ update_password=dict(default="on_create", choices=["on_create", "always"]),
+ )
+ )
+
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ required_if=[("state", "present", ["full_name", "unique_name"])],
+ supports_check_mode=True,
+ )
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+ # Checking for the parameters passed and create new parameters list
+ self.data = {}
+ self.data["memberOf"] = []
+ if self.parameters.get("full_name"):
+ self.data["fullName"] = self.parameters["full_name"]
+ if self.parameters.get("unique_name"):
+ self.data["uniqueName"] = self.parameters["unique_name"]
+
+ if self.parameters.get("disable") is not None:
+ self.data["disable"] = self.parameters["disable"]
+
+ re_local_user = re.compile("^user/")
+ re_fed_user = re.compile("^federated-user/")
+
+ if (
+ re_local_user.match(self.parameters["unique_name"]) is None
+ and re_fed_user.match(self.parameters["unique_name"]) is None
+ ):
+ self.module.fail_json(msg="unique_name must begin with 'user/' or 'federated-user/'")
+
+ self.pw_change = {}
+ if self.parameters.get("password") is not None:
+ if re_fed_user.match(self.parameters["unique_name"]):
+ self.module.fail_json(msg="password cannot be set for a federated user")
+ self.pw_change["password"] = self.parameters["password"]
+
+ def get_grid_groups(self):
+ # Get list of admin groups
+ # Retrun mapping of uniqueName to ids if found, or None
+ api = "api/v3/grid/groups?limit=350"
+
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ if response["data"]:
+ name_to_id_map = dict(zip([i["uniqueName"] for i in response["data"]], [j["id"] for j in response["data"]]))
+ return name_to_id_map
+
+ return None
+
+ def get_grid_user(self, unique_name):
+ # Use the unique name to check if the user exists
+ api = "api/v3/grid/users/%s" % unique_name
+ response, error = self.rest_api.get(api)
+
+ if error:
+ if response["code"] != 404:
+ self.module.fail_json(msg=error["text"])
+ else:
+ return response["data"]
+ return None
+
+ def create_grid_user(self):
+ api = "api/v3/grid/users"
+
+ response, error = self.rest_api.post(api, self.data)
+
+ if error:
+ self.module.fail_json(msg=error["text"])
+
+ return response["data"]
+
+ def delete_grid_user(self, user_id):
+ api = "api/v3/grid/users/" + user_id
+
+ self.data = None
+ response, error = self.rest_api.delete(api, self.data)
+ if error:
+ self.module.fail_json(msg=error)
+
+ def update_grid_user(self, user_id):
+ api = "api/v3/grid/users/" + user_id
+
+ response, error = self.rest_api.put(api, self.data)
+ if error:
+ self.module.fail_json(msg=error["text"])
+
+ return response["data"]
+
+ def set_grid_user_password(self, unique_name):
+ api = "api/v3/grid/users/%s/change-password" % unique_name
+ response, error = self.rest_api.post(api, self.pw_change)
+
+ if error:
+ self.module.fail_json(msg=error["text"])
+
+ def apply(self):
+ """
+ Perform pre-checks, call functions and exit
+ """
+ grid_user = self.get_grid_user(self.parameters["unique_name"])
+
+ if self.parameters.get("member_of"):
+ grid_groups = self.get_grid_groups()
+ try:
+ self.data["memberOf"] = [grid_groups[x] for x in self.parameters["member_of"]]
+ except KeyError as e:
+ self.module.fail_json(msg="Invalid unique_group supplied: '%s' not found" % e.args[0])
+
+ cd_action = self.na_helper.get_cd_action(grid_user, self.parameters)
+
+ if cd_action is None and self.parameters["state"] == "present":
+ # let's see if we need to update parameters
+ update = False
+
+ if grid_user["memberOf"] is None:
+ member_of_diff = []
+ else:
+ member_of_diff = [
+ i
+ for i in self.data["memberOf"] + grid_user["memberOf"]
+ if i not in self.data["memberOf"] or i not in grid_user["memberOf"]
+ ]
+ if member_of_diff:
+ update = True
+
+ if self.parameters.get("disable") is not None and self.parameters["disable"] != grid_user.get("disable"):
+ update = True
+
+ if update:
+ self.na_helper.changed = True
+ result_message = ""
+ resp_data = grid_user
+ if self.na_helper.changed:
+ if self.module.check_mode:
+ pass
+ else:
+ if cd_action == "delete":
+ self.delete_grid_user(grid_user["id"])
+ result_message = "Grid User deleted"
+
+ elif cd_action == "create":
+ resp_data = self.create_grid_user()
+ result_message = "Grid User created"
+
+ else:
+ resp_data = self.update_grid_user(grid_user["id"])
+ result_message = "Grid User updated"
+
+ # If a password has been set
+ if self.pw_change:
+ if self.module.check_mode:
+ pass
+ else:
+ # Only update the password if update_password is always, or a create activity has occurred
+ if cd_action == "create" or self.parameters["update_password"] == "always":
+ self.set_grid_user_password(self.parameters["unique_name"])
+ self.na_helper.changed = True
+
+ results = [result_message, "Grid User password updated"]
+ result_message = "; ".join(filter(None, results))
+
+ self.module.exit_json(changed=self.na_helper.changed, msg=result_message, resp=resp_data)
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_grid_user = SgGridUser()
+ na_sg_grid_user.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_container.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_container.py
new file mode 100644
index 000000000..da9663184
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_container.py
@@ -0,0 +1,352 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage Buckets"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+module: na_sg_org_container
+short_description: Manage buckets on StorageGRID.
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '20.6.0'
+author: NetApp Ansible Team (@joshedmonds) <ng-ansibleteam@netapp.com>
+description:
+- Create S3 buckets on NetApp StorageGRID.
+options:
+ state:
+ description:
+ - Whether the specified bucket should exist or not.
+ type: str
+ choices: ['present', 'absent']
+ default: present
+ name:
+ description:
+ - Name of the bucket.
+ required: true
+ type: str
+ region:
+ description:
+ - Set a region for the bucket.
+ type: str
+ compliance:
+ description:
+ - Configure compliance settings for an S3 bucket.
+ - Cannot be specified along with I(s3_object_lock_enabled).
+ type: dict
+ suboptions:
+ auto_delete:
+ description:
+ - If enabled, objects will be deleted automatically when its retention period expires, unless the bucket is under a legal hold.
+ type: bool
+ legal_hold:
+ description:
+ - If enabled, objects in this bucket cannot be deleted, even if their retention period has expired.
+ type: bool
+ retention_period_minutes:
+ description:
+ - specify the length of the retention period for objects added to this bucket, in minutes.
+ type: int
+ s3_object_lock_enabled:
+ description:
+ - Enable S3 Object Lock on the bucket.
+ - S3 Object Lock requires StorageGRID 11.5 or greater.
+ type: bool
+ version_added: '21.9.0'
+ bucket_versioning_enabled:
+ description:
+ - Enable versioning on the bucket.
+ - This API requires StorageGRID 11.6 or greater.
+ type: bool
+ version_added: '21.11.0'
+"""
+
+EXAMPLES = """
+ - name: create a s3 bucket
+ netapp.storagegrid.na_sg_org_container:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ name: ansiblebucket1
+
+ - name: delete a s3 bucket
+ netapp.storagegrid.na_sg_org_container:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: absent
+ name: ansiblebucket1
+
+ - name: create a s3 bucket with Object Lock
+ netapp.storagegrid.na_sg_org_container:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ name: objectlock-bucket1
+ s3_object_lock_enabled: true
+
+ - name: create a s3 bucket with versioning enabled
+ netapp.storagegrid.na_sg_org_container:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ name: ansiblebucket1
+ bucket_versioning_enabled: true
+"""
+
+RETURN = """
+resp:
+ description: Returns information about the StorageGRID bucket.
+ returned: always
+ type: dict
+ sample: {
+ "name": "example-bucket",
+ "creationTime": "2021-01-01T00:00:00.000Z",
+ "region": "us-east-1",
+ "compliance": {
+ "autoDelete": false,
+ "legalHold": false,
+ "retentionPeriodMinutes": 2629800
+ },
+ "s3ObjectLock": {
+ "enabled": false
+ }
+ }
+"""
+
+import json
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class SgOrgContainer(object):
+ """
+ Create, modify and delete StorageGRID Tenant Account
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ state=dict(required=False, type="str", choices=["present", "absent"], default="present"),
+ name=dict(required=True, type="str"),
+ region=dict(required=False, type="str"),
+ compliance=dict(
+ required=False,
+ type="dict",
+ options=dict(
+ auto_delete=dict(required=False, type="bool"),
+ legal_hold=dict(required=False, type="bool"),
+ retention_period_minutes=dict(required=False, type="int"),
+ ),
+ ),
+ s3_object_lock_enabled=dict(required=False, type="bool"),
+ bucket_versioning_enabled=dict(required=False, type="bool"),
+ )
+ )
+ parameter_map = {
+ "auto_delete": "autoDelete",
+ "legal_hold": "legalHold",
+ "retention_period_minutes": "retentionPeriodMinutes",
+ }
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ mutually_exclusive=[("compliance", "s3_object_lock_enabled")],
+ supports_check_mode=True,
+ )
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+ # Get API version
+ self.rest_api.get_sg_product_version(api_root="org")
+
+ # Checking for the parameters passed and create new parameters list
+
+ self.data_versioning = {}
+ self.data_versioning["versioningSuspended"] = True
+
+ self.data = {}
+ self.data["name"] = self.parameters["name"]
+ self.data["region"] = self.parameters.get("region")
+ if self.parameters.get("compliance"):
+ self.data["compliance"] = dict(
+ (parameter_map[k], v) for (k, v) in self.parameters["compliance"].items() if v is not None
+ )
+
+ if self.parameters.get("s3_object_lock_enabled") is not None:
+ self.rest_api.fail_if_not_sg_minimum_version("S3 Object Lock", 11, 5)
+ self.data["s3ObjectLock"] = dict(enabled=self.parameters["s3_object_lock_enabled"])
+
+ if self.parameters.get("bucket_versioning_enabled") is not None:
+ self.rest_api.fail_if_not_sg_minimum_version("Bucket versioning configuration", 11, 6)
+ self.data_versioning["versioningEnabled"] = self.parameters["bucket_versioning_enabled"]
+ if self.data_versioning["versioningEnabled"]:
+ self.data_versioning["versioningSuspended"] = False
+
+ def get_org_container(self):
+ # Check if bucket/container exists
+ # Return info if found, or None
+
+ params = {"include": "compliance,region"}
+ response, error = self.rest_api.get("api/v3/org/containers", params=params)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ for container in response["data"]:
+ if container["name"] == self.parameters["name"]:
+ return container
+
+ return None
+
+ def create_org_container(self):
+ api = "api/v3/org/containers"
+
+ response, error = self.rest_api.post(api, self.data)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def get_org_container_versioning(self):
+ api = "api/v3/org/containers/%s/versioning" % self.parameters["name"]
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def update_org_container_versioning(self):
+ api = "api/v3/org/containers/%s/versioning" % self.parameters["name"]
+
+ response, error = self.rest_api.put(api, self.data_versioning)
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def fail_if_global_object_lock_disabled(self):
+ api = "api/v3/org/compliance-global"
+
+ response, error = self.rest_api.get(api)
+ if error:
+ self.module.fail_json(msg=error)
+
+ if not response["data"]["complianceEnabled"]:
+ self.module.fail_json(msg="Error: Global S3 Object Lock setting is not enabled.")
+
+ def update_org_container_compliance(self):
+ api = "api/v3/org/containers/%s/compliance" % self.parameters["name"]
+
+ response, error = self.rest_api.put(api, self.data["compliance"])
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def delete_org_container(self):
+ api = "api/v3/org/containers/%s" % self.parameters["name"]
+
+ response, error = self.rest_api.delete(api, None)
+ if error:
+ self.module.fail_json(msg=error["text"])
+
+ def apply(self):
+ """
+ Perform pre-checks, call functions and exit
+ """
+ versioning_config = None
+ update_versioning = False
+
+ org_container = self.get_org_container()
+
+ if org_container and self.parameters.get("bucket_versioning_enabled") is not None:
+ versioning_config = self.get_org_container_versioning()
+
+ cd_action = self.na_helper.get_cd_action(org_container, self.parameters)
+
+ if cd_action is None and self.parameters["state"] == "present":
+ # let's see if we need to update parameters
+ update_compliance = False
+
+ if self.parameters.get("compliance") and org_container.get("compliance") != self.data["compliance"]:
+ update_compliance = True
+ self.na_helper.changed = True
+
+ if (
+ versioning_config
+ and versioning_config["versioningEnabled"] != self.data_versioning["versioningEnabled"]
+ ):
+ update_versioning = True
+ self.na_helper.changed = True
+
+ result_message = ""
+ resp_data = org_container
+ if self.na_helper.changed:
+ if self.module.check_mode:
+ pass
+ else:
+ if cd_action == "delete":
+ self.delete_org_container()
+ resp_data = None
+ result_message = "Org Container deleted"
+
+ elif cd_action == "create":
+ if self.parameters.get("s3_object_lock_enabled"): # if it is set and true
+ self.fail_if_global_object_lock_disabled()
+
+ resp_data = self.create_org_container()
+
+ if self.parameters.get("bucket_versioning_enabled") is not None:
+ self.update_org_container_versioning()
+ result_message = "Org Container created"
+
+ else:
+ if update_compliance:
+ resp_data = self.update_org_container_compliance()
+ if update_versioning:
+ self.update_org_container_versioning()
+ result_message = "Org Container updated"
+
+ self.module.exit_json(changed=self.na_helper.changed, msg=result_message, resp=resp_data)
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_org_container = SgOrgContainer()
+ na_sg_org_container.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_group.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_group.py
new file mode 100644
index 000000000..d13a7559a
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_group.py
@@ -0,0 +1,301 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage tenant Groups"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+module: na_sg_org_group
+short_description: NetApp StorageGRID manage groups within a tenancy.
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '20.6.0'
+author: NetApp Ansible Team (@joshedmonds) <ng-ansibleteam@netapp.com>
+description:
+- Create, Update, Delete Groups within NetApp StorageGRID tenant.
+options:
+ state:
+ description:
+ - Whether the specified group should exist or not.
+ type: str
+ choices: ['present', 'absent']
+ default: present
+ unique_name:
+ description:
+ - Unique Name for the group. Must begin with C(group/) or C(federated-group/).
+ - Required for create, modify or delete operation.
+ type: str
+ required: true
+ display_name:
+ description:
+ - Name of the group.
+ - Required for create operation.
+ type: str
+ management_policy:
+ description:
+ - Management access controls granted to the group within the tenancy.
+ type: dict
+ suboptions:
+ manage_all_containers:
+ description:
+ - Allows users to manage the settings for all S3 buckets in the tenant account, regardless of S3 bucket or group policies.
+ type: bool
+ manage_endpoints:
+ description:
+ - Allows users to use the Tenant Manager or the Tenant Management API to create or edit endpoints.
+ - Endpoints are used as the destination for StorageGRID platform services.
+ type: bool
+ manage_own_s3_credentials:
+ description:
+ - Allows users to create and remove their own S3 access keys.
+ - Users who do not have this permission do not see the S3 > My Credentials menu option.
+ type: bool
+ root_access:
+ description:
+ - Provides full access to the Tenant Manager and the Tenant Management API.
+ type: bool
+ s3_policy:
+ description:
+ - StorageGRID S3 Group Policy.
+ default: ""
+ type: json
+"""
+
+EXAMPLES = """
+ - name: create a group
+ netapp.storagegrid.na_sg_org_group:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ display_name: ansiblegroup1
+ unique_name: group/ansiblegroup1
+ management_policy:
+ manage_all_containers: true
+ manage_endpoints: true
+ manage_own_s3_credentials: false
+ root_access: false
+ s3_policy: {"Statement":[{"Effect":"Deny","Action":"s3:*","Resource":"arn:aws:s3:::*"}]}
+"""
+
+RETURN = """
+resp:
+ description: Returns information about the StorageGRID tenant group attributes.
+ returned: success
+ type: dict
+ sample: {
+ "displayName": "Example Group",
+ "policies": {
+ "management": {
+ "manageAllContainers": true,
+ "manageEndpoints": true,
+ "manageOwnS3Credentials": true,
+ "rootAccess": true
+ },
+ "s3": {...},
+ "swift": {...}
+ },
+ "uniqueName": "group/examplegroup",
+ "accountId": "12345678901234567890",
+ "id": "00000000-0000-0000-0000-000000000000",
+ "federated": false,
+ "groupURN": "urn:sgws:identity::12345678901234567890:group/examplegroup"
+ }
+"""
+
+import json
+import re
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class SgOrgGroup(object):
+ """
+ Create, modify and delete StorageGRID Tenant Account
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ state=dict(required=False, type="str", choices=["present", "absent"], default="present"),
+ display_name=dict(required=False, type="str"),
+ unique_name=dict(required=True, type="str"),
+ management_policy=dict(
+ required=False,
+ type="dict",
+ options=dict(
+ manage_all_containers=dict(required=False, type="bool"),
+ manage_endpoints=dict(required=False, type="bool"),
+ manage_own_s3_credentials=dict(required=False, type="bool"),
+ root_access=dict(required=False, type="bool"),
+ ),
+ ),
+ s3_policy=dict(required=False, type="json"),
+ )
+ )
+ parameter_map = {
+ "manage_all_containers": "manageAllContainers",
+ "manage_endpoints": "manageEndpoints",
+ "manage_own_s3_credentials": "manageOwnS3Credentials",
+ "root_access": "rootAccess",
+ }
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ # required_if=[("state", "present", ["display_name"])],
+ supports_check_mode=True,
+ )
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+ # Checking for the parameters passed and create new parameters list
+ self.data = {}
+ self.data["displayName"] = self.parameters.get("display_name")
+ self.data["uniqueName"] = self.parameters["unique_name"]
+ # Only add the parameter if value is True, as JSON response does not include non-true objects
+ self.data["policies"] = {}
+
+ if self.parameters.get("management_policy"):
+ self.data["policies"] = {
+ "management": dict(
+ (parameter_map[k], v) for (k, v) in self.parameters["management_policy"].items() if v
+ )
+ }
+ if not self.data["policies"].get("management"):
+ self.data["policies"]["management"] = None
+
+ if self.parameters.get("s3_policy"):
+ try:
+ self.data["policies"]["s3"] = json.loads(self.parameters["s3_policy"])
+ except ValueError:
+ self.module.fail_json(msg="Failed to decode s3_policy. Invalid JSON.")
+
+ self.re_local_group = re.compile("^group/")
+ self.re_fed_group = re.compile("^federated-group/")
+
+ if (
+ self.re_local_group.match(self.parameters["unique_name"]) is None
+ and self.re_fed_group.match(self.parameters["unique_name"]) is None
+ ):
+ self.module.fail_json(msg="unique_name must begin with 'group/' or 'federated-group/'")
+
+ def get_org_group(self, unique_name):
+ # Use the unique name to check if the group exists
+ api = "api/v3/org/groups/%s" % unique_name
+ response, error = self.rest_api.get(api)
+
+ if error:
+ if response["code"] != 404:
+ self.module.fail_json(msg=error)
+ else:
+ return response["data"]
+ return None
+
+ def create_org_group(self):
+ api = "api/v3/org/groups"
+
+ response, error = self.rest_api.post(api, self.data)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def delete_org_group(self, group_id):
+ api = "api/v3/org/groups/" + group_id
+
+ self.data = None
+ response, error = self.rest_api.delete(api, self.data)
+ if error:
+ self.module.fail_json(msg=error)
+
+ def update_org_group(self, group_id):
+ api = "api/v3/org/groups/" + group_id
+
+ response, error = self.rest_api.put(api, self.data)
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def apply(self):
+ """
+ Perform pre-checks, call functions and exit
+ """
+ org_group = self.get_org_group(self.parameters["unique_name"])
+
+ cd_action = self.na_helper.get_cd_action(org_group, self.parameters)
+
+ if cd_action is None and self.parameters["state"] == "present":
+ # let's see if we need to update parameters
+ update = False
+
+ if self.parameters.get("management_policy"):
+ if org_group.get("policies") is None or org_group.get("policies", {}).get("management") != self.data["policies"]["management"]:
+ update = True
+ if self.parameters.get("s3_policy"):
+ if org_group.get("policies") is None or org_group.get("policies", {}).get("s3") != self.data["policies"]["s3"]:
+ update = True
+
+ if update:
+ self.na_helper.changed = True
+ result_message = ""
+ resp_data = org_group
+ if self.na_helper.changed:
+ if self.module.check_mode:
+ pass
+ else:
+ if cd_action == "delete":
+ self.delete_org_group(org_group["id"])
+ result_message = "Org Group deleted"
+
+ elif cd_action == "create":
+ resp_data = self.create_org_group()
+ result_message = "Org Group created"
+
+ else:
+ # for a federated group, the displayName parameter needs to be specified
+ # and must match the existing displayName
+ if self.re_fed_group.match(self.parameters["unique_name"]):
+ self.data["displayName"] = org_group["displayName"]
+
+ resp_data = self.update_org_group(org_group["id"])
+ result_message = "Org Group updated"
+
+ self.module.exit_json(changed=self.na_helper.changed, msg=result_message, resp=resp_data)
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_org_group = SgOrgGroup()
+ na_sg_org_group.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_identity_federation.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_identity_federation.py
new file mode 100644
index 000000000..4b6811cd6
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_identity_federation.py
@@ -0,0 +1,335 @@
+#!/usr/bin/python
+
+# (c) 2021, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage Tenant Identity Federation"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+module: na_sg_org_identity_federation
+short_description: NetApp StorageGRID manage Tenant identity federation.
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '21.6.0'
+author: NetApp Ansible Team (@joshedmonds) <ng-ansibleteam@netapp.com>
+description:
+- Configure Tenant Identity Federation within NetApp StorageGRID.
+- If module is run with C(check_mode), a connectivity test will be performed using the supplied values without changing the configuration.
+- This module is idempotent if I(password) is not specified.
+options:
+ state:
+ description:
+ - Whether identity federation should be enabled or not.
+ type: str
+ choices: ['present', 'absent']
+ default: present
+ username:
+ description:
+ - The username to bind to the LDAP server.
+ type: str
+ password:
+ description:
+ - The password associated with the username.
+ type: str
+ hostname:
+ description:
+ - The hostname or IP address of the LDAP server.
+ type: str
+ port:
+ description:
+ - The port used to connect to the LDAP server. Typically 389 for LDAP, or 636 for LDAPS.
+ type: int
+ base_group_dn:
+ description:
+ - The Distinguished Name of the LDAP subtree to search for groups.
+ type: str
+ base_user_dn:
+ description:
+ - The Distinguished Name of the LDAP subtree to search for users.
+ type: str
+ ldap_service_type:
+ description:
+ - The type of LDAP server.
+ choices: ['Active Directory', 'OpenLDAP', 'Other']
+ type: str
+ type:
+ description:
+ - The type of identity source.
+ - Default is 'ldap'.
+ type: str
+ default: ldap
+ ldap_user_id_attribute:
+ description:
+ - The LDAP attribute which contains the unique user name of a user.
+ - Should be configured if I(ldap_service_type=Other).
+ type: str
+ ldap_user_uuid_attribute:
+ description:
+ - The LDAP attribute which contains the permanent unique identity of a user.
+ - Should be configured if I(ldap_service_type=Other).
+ type: str
+ ldap_group_id_attribute:
+ description:
+ - The LDAP attribute which contains the group for a user.
+ - Should be configured if I(ldap_service_type=Other).
+ type: str
+ ldap_group_uuid_attribute:
+ description:
+ - The LDAP attribute which contains the group's permanent unique identity.
+ - Should be configured if I(ldap_service_type=Other).
+ type: str
+ tls:
+ description:
+ - Whether Transport Layer Security is used to connect to the LDAP server.
+ choices: ['STARTTLS', 'LDAPS', 'Disabled']
+ type: str
+ default: STARTTLS
+ ca_cert:
+ description:
+ - Custom certificate used to connect to the LDAP server.
+ - If a custom certificate is not supplied, the operating system CA certificate will be used.
+ type: str
+"""
+
+EXAMPLES = """
+ - name: test identity federation configuration
+ netapp.storagegrid.na_sg_org_identity_federation:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ ldap_service_type: "Active Directory"
+ hostname: "ad.example.com"
+ port: 389
+ username: "binduser"
+ password: "bindpass"
+ base_group_dn: "DC=example,DC=com"
+ base_user_dn: "DC=example,DC=com"
+ tls: "Disabled"
+ check_mode: yes
+
+ - name: configure identity federation with AD and TLS
+ netapp.storagegrid.na_sg_org_identity_federation:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ ldap_service_type: "Active Directory"
+ hostname: "ad.example.com"
+ port: 636,
+ username: "binduser"
+ password: "bindpass"
+ base_group_dn: "DC=example,DC=com"
+ base_user_dn: "DC=example,DC=com"
+ tls: "LDAPS"
+ ca_cert: |
+ -----BEGIN CERTIFICATE-----
+ MIIC+jCCAeICCQDmn9Gow08LTzANBgkqhkiG9w0BAQsFADA/..swCQYDVQQGEwJV
+ bXBsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB..JFzNIXQEGnsgjV
+ JGU4giuvOLOZ8Q3gyuUbkSUQDjmjpMR8PliwJ6iW2Ity89Dv..dl1TaIYI/ansyZ
+ Uxk4YXeN6kUkrDtNxCg1McALzXVAfxMTtj2SFlLxne4Z6rX2..UyftQrfM13F1vY
+ gK8dBPz+l+X/Uozo/xNm7gxe68p9le9/pcULst1CQn5/sPqq..kgWcSvlKUItu82
+ lq3B2169rovdIaNdcvaQjMPhrDGo5rvLfMN35U3Hgbz41PL5..x2BcUE6/0ab5T4
+ qKBxKa3t9twj+zpUqOzyL0PFfCE+SK5fEXAS1ow4eAcLN+eB..gR/PuvGAyIPCtE
+ 1+X4GrECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAFpO+04Ra..FMJPH6dBmzfb7l
+ k04BWTvSlur6HiQdXY+oFQMJZzyI7MQ8v9HBIzS0ZAzYWLp4..VZhHmRxnrWyxVs
+ u783V5YfQH2L4QnBDoiDefgxyfDs2PcoF5C+X9CGXmPqzst2..y/6tdOVJzdiA==
+ -----END CERTIFICATE-----
+"""
+
+RETURN = """
+resp:
+ description: Returns information about the StorageGRID tenant account identity source configuration.
+ returned: success
+ type: dict
+ sample: {
+ "id": "00000000-0000-0000-0000-000000000000",
+ "disable": false,
+ "hostname": "10.1.2.3",
+ "port": 389,
+ "username": "MYDOMAIN\\\\Administrator",
+ "password": "********",
+ "baseGroupDn": "DC=example,DC=com",
+ "baseUserDn": "DC=example,DC=com",
+ "ldapServiceType": "Active Directory",
+ "type": "ldap",
+ "disableTLS": false,
+ "enableLDAPS": false,
+ "caCert": "-----BEGIN CERTIFICATE----- abcdefghijkl123456780ABCDEFGHIJKL 123456/7890ABCDEFabcdefghijklABCD -----END CERTIFICATE-----\n"
+ }
+"""
+
+import json
+import re
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class SgOrgIdentityFederation:
+ """
+ Configure and modify StorageGRID Tenant Identity Federation
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ state=dict(required=False, type="str", choices=["present", "absent"], default="present"),
+ username=dict(required=False, type="str"),
+ password=dict(required=False, type="str", no_log=True),
+ hostname=dict(required=False, type="str"),
+ port=dict(required=False, type="int"),
+ base_group_dn=dict(required=False, type="str"),
+ base_user_dn=dict(required=False, type="str"),
+ ldap_service_type=dict(required=False, type="str", choices=["OpenLDAP", "Active Directory", "Other"]),
+ type=dict(required=False, type="str", default="ldap"),
+ ldap_user_id_attribute=dict(required=False, type="str"),
+ ldap_user_uuid_attribute=dict(required=False, type="str"),
+ ldap_group_id_attribute=dict(required=False, type="str"),
+ ldap_group_uuid_attribute=dict(required=False, type="str"),
+ tls=dict(required=False, type="str", choices=["STARTTLS", "LDAPS", "Disabled"], default="STARTTLS"),
+ ca_cert=dict(required=False, type="str"),
+ ),
+ )
+
+ parameter_map = {
+ "username": "username",
+ "password": "password",
+ "hostname": "hostname",
+ "port": "port",
+ "base_group_dn": "baseGroupDn",
+ "base_user_dn": "baseUserDn",
+ "ldap_service_type": "ldapServiceType",
+ "ldap_user_id_attribute": "ldapUserIdAttribute",
+ "ldap_user_uuid_attribute": "ldapUserUUIDAttribute",
+ "ldap_group_id_attribute": "ldapGroupIdAttribute",
+ "ldap_group_uuid_attribute": "ldapGroupUUIDAttribute",
+ "ca_cert": "caCert",
+ }
+ self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=True,)
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+ # Checking for the parameters passed and create new parameters list
+ self.data = {}
+
+ if self.parameters["state"] == "present":
+ self.data["disable"] = False
+
+ for k in parameter_map.keys():
+ if self.parameters.get(k) is not None:
+ self.data[parameter_map[k]] = self.parameters[k]
+
+ if self.parameters.get("tls") == "STARTTLS":
+ self.data["disableTLS"] = False
+ self.data["enableLDAPS"] = False
+ elif self.parameters.get("tls") == "LDAPS":
+ self.data["disableTLS"] = False
+ self.data["enableLDAPS"] = True
+ else:
+ self.data["disableTLS"] = True
+ self.data["enableLDAPS"] = False
+
+ def get_org_identity_source(self):
+ api = "api/v3/org/identity-source"
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+ else:
+ return response["data"]
+ return None
+
+ def update_identity_federation(self, test=False):
+ api = "api/v3/org/identity-source"
+
+ params = {}
+
+ if test:
+ params["test"] = True
+
+ response, error = self.rest_api.put(api, self.data, params=params)
+ if error:
+ self.module.fail_json(msg=error, payload=self.data)
+
+ if response is not None:
+ return response["data"]
+ else:
+ return None
+
+ def apply(self):
+ """
+ Perform pre-checks, call functions and exit
+ """
+ org_identity_source = self.get_org_identity_source()
+
+ cd_action = self.na_helper.get_cd_action(org_identity_source, self.parameters)
+
+ if cd_action is None and self.parameters["state"] == "present":
+ # let's see if we need to update parameters
+ update = False
+
+ for k in (i for i in self.data.keys() if i != "password"):
+ if self.data[k] != org_identity_source.get(k):
+ update = True
+ break
+
+ # if a password has been specified we need to update it
+ if self.data.get("password") and self.parameters["state"] == "present":
+ update = True
+ self.module.warn("Password attribute has been specified. Task is not idempotent.")
+
+ if update:
+ self.na_helper.changed = True
+
+ if cd_action == "delete":
+ # if identity federation is already in a disable state
+ if org_identity_source.get("disable"):
+ self.na_helper.changed = False
+
+ result_message = ""
+ resp_data = org_identity_source
+
+ if self.na_helper.changed and not self.module.check_mode:
+ if cd_action == "delete":
+ self.data = dict(disable=True)
+ resp_data = self.update_identity_federation()
+ result_message = "Tenant identity federation disabled"
+ else:
+ resp_data = self.update_identity_federation()
+ result_message = "Tenant identity federation updated"
+
+ if self.module.check_mode:
+ self.update_identity_federation(test=True)
+ # if no error, connection test successful
+ self.module.exit_json(changed=self.na_helper.changed, msg="Connection test successful")
+
+ self.module.exit_json(changed=self.na_helper.changed, msg=result_message, resp=resp_data)
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_org_identity_federation = SgOrgIdentityFederation()
+ na_sg_org_identity_federation.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_info.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_info.py
new file mode 100644
index 000000000..b2d3c4e48
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_info.py
@@ -0,0 +1,279 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" NetApp StorageGRID Org Info using REST APIs """
+
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = """
+module: na_sg_org_info
+author: NetApp Ansible Team (@jasonl4) <ng-ansibleteam@netapp.com>
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+short_description: NetApp StorageGRID Org information gatherer.
+description:
+ - This module allows you to gather various information about StorageGRID Org configuration.
+version_added: 20.11.0
+
+options:
+ gather_subset:
+ type: list
+ elements: str
+ description:
+ - When supplied, this argument will restrict the information collected to a given subset.
+ - Either the info name or the Rest API can be given.
+ - Possible values for this argument include
+ - C(org_compliance_global_info) or C(org/compliance-global)
+ - C(org_config_info) or C(org/config)
+ - C(org_config_product_version_info) or C(org/config/product-version)
+ - C(org_containers_info) or C(org/containers)
+ - C(org_deactivated_features_info) or C(org/deactivated-features)
+ - C(org_endpoints_info) or C(org/endpoints)
+ - C(org_groups_info) or C(org/groups)
+ - C(org_identity_source_info) or C(org/identity-source)
+ - C(org_regions_info) or C(org/regions)
+ - C(org_users_current_user_s3_access_keys_info) or C(org/users/current-user/s3-access-keys)
+ - C(org_usage_info) or C(org/usage)
+ - C(org_users_info) or C(org/users)
+ - C(org_users_root_info) or C(org/users/root)
+ - C(versions_info) or C(versions)
+ - Can specify a list of values to include a larger subset.
+ default: "all"
+ parameters:
+ description:
+ - Allows for any rest option to be passed in.
+ type: dict
+"""
+
+EXAMPLES = """
+- name: Gather StorageGRID Org info
+ netapp.storagegrid.na_sg_org_info:
+ api_url: "https://1.2.3.4/"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ register: sg_org_info
+
+- name: Gather StorageGRID Org info for org/containers and org/config subsets
+ netapp.storagegrid.na_sg_org_info:
+ api_url: "https://1.2.3.4/"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ gather_subset:
+ - org_containers_info
+ - org/config
+ register: sg_org_info
+
+- name: Gather StorageGRID Org info for all subsets
+ netapp.storagegrid.na_sg_org_info:
+ api_url: "https://1.2.3.4/"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ gather_subset:
+ - all
+ register: sg_org_info
+
+- name: Gather StorageGRID Org info for org/containers and org/users subsets, limit to 5 results for each subset
+ netapp.storagegrid.na_sg_org_info:
+ api_url: "https://1.2.3.4/"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ gather_subset:
+ - org/containers
+ - org/users
+ parameters:
+ limit: 5
+ register: sg_org_info
+"""
+
+RETURN = """
+sg_info:
+ description: Returns various information about the StorageGRID Grid configuration.
+ returned: always
+ type: dict
+ sample: {
+ "org/compliance-global": {...},
+ "org/config": {...},
+ "org/config/product-version": {...},
+ "org/containers": {...},
+ "org/deactivated-features": {...},
+ "org/endpoints": {...},
+ "org/groups": {...},
+ "org/identity-source": {...},
+ "org/regions": {...},
+ "org/users/current-user/s3-access-keys": {...},
+ "org/usage": {...},
+ "org/users": {...},
+ "org/users/root": {...},
+ "org/versions": {...}
+ }
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class NetAppSgGatherInfo(object):
+ """ Class with gather info methods """
+
+ def __init__(self):
+ """
+ Parse arguments, setup variables, check parameters and ensure
+ request module is installed.
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(dict(
+ gather_subset=dict(default=['all'], type='list', elements='str', required=False),
+ parameters=dict(type='dict', required=False)
+ ))
+
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ supports_check_mode=True
+ )
+
+ # set up variables
+ self.na_helper = NetAppModule()
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ self.rest_api = SGRestAPI(self.module)
+
+ def get_subset_info(self, gather_subset_info):
+ """
+ Gather StorageGRID information for the given subset using REST APIs
+ Input for REST APIs call : (api, data)
+ return gathered_sg_info
+ """
+
+ api = gather_subset_info['api_call']
+ data = {}
+ # allow for passing in any additional rest api parameters
+ if self.parameters.get('parameters'):
+ for each in self.parameters['parameters']:
+ data[each] = self.parameters['parameters'][each]
+
+ gathered_sg_info, error = self.rest_api.get(api, data)
+
+ if error:
+ self.module.fail_json(msg=error)
+ else:
+ return gathered_sg_info
+
+ return None
+
+ def convert_subsets(self):
+ """ Convert an info to the REST API """
+ info_to_rest_mapping = {
+ 'org_compliance_global_info': 'org/compliance-global',
+ 'org_config_info': 'org/config',
+ 'org_config_product_version_info': 'org/config/product-version',
+ 'org_containers_info': 'org/containers',
+ 'org_deactivated_features_info': 'org/deactivated-features',
+ 'org_endpoints_info': 'org/endpoints',
+ 'org_groups_info': 'org/groups',
+ 'org_identity_source_info': 'org/identity-source',
+ 'org_regions_info': 'org/regions',
+ 'org_users_current_user_s3_access_keys_info': 'org/users/current-user/s3-access-keys',
+ 'org_usage_info': 'org/usage',
+ 'org_users_info': 'org/users',
+ 'org_users_root_info': 'org/users/root',
+ 'versions_info': 'versions'
+ }
+ # Add rest API names as there info version, also make sure we don't add a duplicate
+ subsets = []
+ for subset in self.parameters['gather_subset']:
+ if subset in info_to_rest_mapping:
+ if info_to_rest_mapping[subset] not in subsets:
+ subsets.append(info_to_rest_mapping[subset])
+ else:
+ if subset not in subsets:
+ subsets.append(subset)
+ return subsets
+
+ def apply(self):
+ """ Perform pre-checks, call functions and exit """
+
+ result_message = dict()
+
+ # Defining gather_subset and appropriate api_call
+ get_sg_subset_info = {
+ 'org/compliance-global': {
+ 'api_call': 'api/v3/org/compliance-global',
+ },
+ 'org/config': {
+ 'api_call': 'api/v3/org/config',
+ },
+ 'org/config/product-version': {
+ 'api_call': 'api/v3/org/config/product-version',
+ },
+ 'org/containers': {
+ 'api_call': 'api/v3/org/containers',
+ },
+ 'org/deactivated-features': {
+ 'api_call': 'api/v3/org/deactivated-features',
+ },
+ 'org/endpoints': {
+ 'api_call': 'api/v3/org/endpoints',
+ },
+ 'org/groups': {
+ 'api_call': 'api/v3/org/groups',
+ },
+ 'org/identity-source': {
+ 'api_call': 'api/v3/org/identity-source',
+ },
+ 'org/regions': {
+ 'api_call': 'api/v3/org/regions',
+ },
+ 'org/users/current-user/s3-access-keys': {
+ 'api_call': 'api/v3/org/users/current-user/s3-access-keys',
+ },
+ 'org/usage': {
+ 'api_call': 'api/v3/org/usage',
+ },
+ 'org/users': {
+ 'api_call': 'api/v3/org/users',
+ },
+ 'org/users/root': {
+ 'api_call': 'api/v3/org/users/root',
+ },
+ 'versions': {
+ 'api_call': 'api/v3/versions',
+ },
+ }
+
+ if 'all' in self.parameters['gather_subset']:
+ # If all in subset list, get the information of all subsets
+ self.parameters['gather_subset'] = sorted(get_sg_subset_info.keys())
+
+ converted_subsets = self.convert_subsets()
+
+ for subset in converted_subsets:
+ try:
+ # Verify whether the supported subset passed
+ specified_subset = get_sg_subset_info[subset]
+ except KeyError:
+ self.module.fail_json(msg="Specified subset %s not found, supported subsets are %s" %
+ (subset, list(get_sg_subset_info.keys())))
+
+ result_message[subset] = self.get_subset_info(specified_subset)
+
+ self.module.exit_json(changed='False', sg_info=result_message)
+
+
+def main():
+ """ Main function """
+ obj = NetAppSgGatherInfo()
+ obj.apply()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_user.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_user.py
new file mode 100644
index 000000000..455ffa345
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_user.py
@@ -0,0 +1,335 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage Tenant Users"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+module: na_sg_org_user
+short_description: NetApp StorageGRID manage users within a tenancy.
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '20.6.0'
+author: NetApp Ansible Team (@joshedmonds) <ng-ansibleteam@netapp.com>
+description:
+- Create, Update, Delete Users within a NetApp StorageGRID tenant.
+options:
+ state:
+ description:
+ - Whether the specified user should exist or not.
+ type: str
+ choices: ['present', 'absent']
+ default: present
+ full_name:
+ description:
+ - Full Name of the user.
+ - Required for create operation
+ type: str
+ unique_name:
+ description:
+ - Unique Name for the user. Must begin with C(user/) or C(federated-user/).
+ - Required for create, modify or delete operation.
+ type: str
+ required: true
+ member_of:
+ description:
+ - List of unique_groups that the user is a member of.
+ type: list
+ elements: str
+ password:
+ description:
+ - Set a password for a local user. Does not apply to federated users.
+ - Requires root privilege.
+ required: false
+ type: str
+ update_password:
+ description:
+ - Choose when to update the password.
+ - When set to C(always), the password will always be updated.
+ - When set to C(on_create), the password will only be set upon a new user creation.
+ default: on_create
+ choices:
+ - on_create
+ - always
+ type: str
+ disable:
+ description:
+ - Disable the user from signing in. Does not apply to federated users.
+ type: bool
+"""
+
+EXAMPLES = """
+ - name: create a tenant user
+ netapp.storagegrid.na_sg_org_user:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ full_name: ansibleuser1
+ unique_name: user/ansibleuser1
+ member_of: "group/ansiblegroup1"
+ disable: false
+
+"""
+
+RETURN = """
+resp:
+ description: Returns information about the StorageGRID tenant user.
+ returned: always
+ type: dict
+ sample: {
+ "fullName": "Example User",
+ "memberOf": ["00000000-0000-0000-0000-000000000000"],
+ "disable": false,
+ "uniqueName": "user/Example",
+ "accountId": "0",
+ "id": "00000000-0000-0000-0000-000000000000",
+ "federated": false,
+ "userURN": "urn:sgws:identity::0:user/Example"
+ }
+"""
+
+import json
+import re
+
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import (
+ NetAppModule,
+)
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class SgOrgUser(object):
+ """
+ Create, modify and delete user within a StorageGRID Tenant Account
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ state=dict(required=False, type="str", choices=["present", "absent"], default="present"),
+ full_name=dict(required=False, type="str"),
+ unique_name=dict(required=True, type="str"),
+ member_of=dict(required=False, type="list", elements="str"),
+ disable=dict(required=False, type="bool"),
+ password=dict(required=False, type="str", no_log=True),
+ update_password=dict(
+ default="on_create", choices=["on_create", "always"]
+ ),
+ )
+ )
+
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ required_if=[("state", "present", ["full_name", "unique_name"])],
+ supports_check_mode=True,
+ )
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+ # Checking for the parameters passed and create new parameters list
+ self.data = {}
+ self.data["memberOf"] = []
+ if self.parameters.get("full_name"):
+ self.data["fullName"] = self.parameters["full_name"]
+ if self.parameters.get("unique_name"):
+ self.data["uniqueName"] = self.parameters["unique_name"]
+
+ if self.parameters.get("disable") is not None:
+ self.data["disable"] = self.parameters["disable"]
+
+ re_local_user = re.compile("^user/")
+ re_fed_user = re.compile("^federated-user/")
+
+ if (
+ re_local_user.match(self.parameters["unique_name"]) is None
+ and re_fed_user.match(self.parameters["unique_name"]) is None
+ ):
+ self.module.fail_json(
+ msg="unique_name must begin with 'user/' or 'federated-user/'"
+ )
+
+ self.pw_change = {}
+ if self.parameters.get("password") is not None:
+ if re_fed_user.match(self.parameters["unique_name"]):
+ self.module.fail_json(msg="password cannot be set for a federated user")
+ self.pw_change["password"] = self.parameters["password"]
+
+ def get_org_groups(self):
+ # Get list of groups
+ # Retrun mapping of uniqueName to ids if found, or None
+ api = "api/v3/org/groups?limit=350"
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ if response["data"]:
+ name_to_id_map = dict(
+ zip(
+ [i["uniqueName"] for i in response["data"]],
+ [j["id"] for j in response["data"]],
+ )
+ )
+ return name_to_id_map
+
+ return None
+
+ def get_org_user(self, unique_name):
+ # Use the unique name to check if the user exists
+ api = "api/v3/org/users/%s" % unique_name
+ response, error = self.rest_api.get(api)
+
+ if error:
+ if response["code"] != 404:
+ self.module.fail_json(msg=error)
+ else:
+ return response["data"]
+ return None
+
+ def create_org_user(self):
+ api = "api/v3/org/users"
+
+ response, error = self.rest_api.post(api, self.data)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def delete_org_user(self, user_id):
+ api = "api/v3/org/users/" + user_id
+
+ self.data = None
+ response, error = self.rest_api.delete(api, self.data)
+ if error:
+ self.module.fail_json(msg=error)
+
+ def update_org_user(self, user_id):
+ api = "api/v3/org/users/" + user_id
+
+ response, error = self.rest_api.put(api, self.data)
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def set_org_user_password(self, unique_name):
+ api = "api/v3/org/users/%s/change-password" % unique_name
+ response, error = self.rest_api.post(api, self.pw_change)
+
+ if error:
+ self.module.fail_json(msg=error["text"])
+
+ def apply(self):
+ """
+ Perform pre-checks, call functions and exit
+ """
+ org_user = self.get_org_user(self.parameters["unique_name"])
+
+ if self.parameters.get("member_of"):
+ org_groups = self.get_org_groups()
+ try:
+ self.data["memberOf"] = [
+ org_groups[x] for x in self.parameters["member_of"]
+ ]
+ except KeyError as e:
+ self.module.fail_json(
+ msg="Invalid unique_group supplied: '%s' not found" % e.args[0]
+ )
+
+ cd_action = self.na_helper.get_cd_action(org_user, self.parameters)
+
+ if cd_action is None and self.parameters["state"] == "present":
+ # let's see if we need to update parameters
+ update = False
+
+ if org_user["memberOf"] is None:
+ member_of_diff = []
+ else:
+ member_of_diff = [
+ i
+ for i in self.data["memberOf"] + org_user["memberOf"]
+ if i not in self.data["memberOf"] or i not in org_user["memberOf"]
+ ]
+ if member_of_diff:
+ update = True
+
+ if self.parameters.get("disable") is not None and self.parameters[
+ "disable"
+ ] != org_user.get("disable"):
+ update = True
+
+ if update:
+ self.na_helper.changed = True
+
+ result_message = ""
+ resp_data = org_user
+ if self.na_helper.changed:
+ if self.module.check_mode:
+ pass
+ else:
+ if cd_action == "delete":
+ self.delete_org_user(org_user["id"])
+ result_message = "Org User deleted"
+
+ elif cd_action == "create":
+ resp_data = self.create_org_user()
+ result_message = "Org User created"
+
+ else:
+ resp_data = self.update_org_user(org_user["id"])
+ result_message = "Org User updated"
+
+ # If a password has been set
+ if self.pw_change:
+ if self.module.check_mode:
+ pass
+ else:
+ # Only update the password if update_password is always, or a create activity has occurred
+ if cd_action == "create" or self.parameters["update_password"] == "always":
+ self.set_org_user_password(self.parameters["unique_name"])
+ self.na_helper.changed = True
+
+ results = [result_message, "Org User password updated"]
+ result_message = "; ".join(filter(None, results))
+
+ self.module.exit_json(
+ changed=self.na_helper.changed, msg=result_message, resp=resp_data
+ )
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_org_user = SgOrgUser()
+ na_sg_org_user.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_user_s3_key.py b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_user_s3_key.py
new file mode 100644
index 000000000..0de396eb7
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/plugins/modules/na_sg_org_user_s3_key.py
@@ -0,0 +1,210 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""NetApp StorageGRID - Manage User S3 keys"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+module: na_sg_org_user_s3_key
+short_description: Creates NetApp StorageGRID User S3 keys.
+extends_documentation_fragment:
+ - netapp.storagegrid.netapp.sg
+version_added: '20.6.0'
+author: NetApp Ansible Team (@joshedmonds) <ng-ansibleteam@netapp.com>
+description:
+- Create, Delete Users S3 keys on NetApp StorageGRID.
+options:
+ state:
+ description:
+ - Whether the specified account should exist or not.
+ type: str
+ choices: ['present', 'absent']
+ default: present
+ unique_user_name:
+ description:
+ - Unique user name owning the S3 Key.
+ required: true
+ type: str
+ expires:
+ description:
+ - Date-Time string for the key to expire.
+ type: str
+ access_key:
+ description:
+ - Access Key or S3 credential pair identifier.
+ - Required for delete operation.
+ type: str
+"""
+
+EXAMPLES = """
+ - name: create a s3 key
+ netapp.storagegrid.na_sg_org_user_s3_key:
+ api_url: "https://<storagegrid-endpoint-url>"
+ auth_token: "storagegrid-auth-token"
+ validate_certs: false
+ state: present
+ unique_user_name: user/ansibleuser1
+"""
+
+RETURN = """
+resp:
+ description: Returns information about an S3 access key for the user.
+ returned: always
+ type: dict
+ sample: {
+ "id": "abcABC_01234-0123456789abcABCabc0123456789==",
+ "accountId": 12345678901234567000,
+ "displayName": "****************AB12",
+ "userURN": "urn:sgws:identity::12345678901234567000:root",
+ "userUUID": "00000000-0000-0000-0000-000000000000",
+ "expires": "2020-09-04T00:00:00.000Z"
+ }
+"""
+
+import json
+
+import ansible_collections.netapp.storagegrid.plugins.module_utils.netapp as netapp_utils
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp_module import NetAppModule
+from ansible_collections.netapp.storagegrid.plugins.module_utils.netapp import SGRestAPI
+
+
+class SgOrgUserS3Key(object):
+ """
+ Create, modify and delete StorageGRID Tenant Account
+ """
+
+ def __init__(self):
+ """
+ Parse arguments, setup state variables,
+ check parameters and ensure request module is installed
+ """
+ self.argument_spec = netapp_utils.na_storagegrid_host_argument_spec()
+ self.argument_spec.update(
+ dict(
+ state=dict(required=False, type="str", choices=["present", "absent"], default="present"),
+ unique_user_name=dict(required=True, type="str"),
+ expires=dict(required=False, type="str"),
+ access_key=dict(required=False, type="str", no_log=False),
+ )
+ )
+
+ self.module = AnsibleModule(
+ argument_spec=self.argument_spec,
+ required_if=[("state", "absent", ["access_key"])],
+ supports_check_mode=False,
+ )
+
+ self.na_helper = NetAppModule()
+
+ # set up state variables
+ self.parameters = self.na_helper.set_parameters(self.module.params)
+ # Calling generic SG rest_api class
+ self.rest_api = SGRestAPI(self.module)
+ # Checking for the parameters passed and create new parameters list
+ self.data = {}
+ self.data["expires"] = self.parameters.get("expires")
+
+ def get_org_user_id(self, unique_name):
+ # Use the unique name to check if the user exists
+ api = "api/v3/org/users/%s" % unique_name
+ response, error = self.rest_api.get(api)
+
+ if error:
+ if response["code"] != 404:
+ self.module.fail_json(msg=error)
+ else:
+ return response["data"]["id"]
+ return None
+
+ def get_org_user_s3_key(self, user_id, access_key):
+ # Use the unique name to check if the user exists
+ api = "api/v3/org/users/current-user/s3-access-keys/%s" % access_key
+
+ if user_id:
+ api = "api/v3/org/users/%s/s3-access-keys/%s" % (user_id, access_key,)
+
+ response, error = self.rest_api.get(api)
+
+ if error:
+ self.module.fail_json(msg=error)
+ else:
+ return response["data"]
+ return None
+
+ def create_org_user_s3_key(self, user_id):
+ api = "api/v3/org/users/current-user/s3-access-keys"
+
+ if user_id:
+ api = "api/v3/org/users/%s/s3-access-keys" % user_id
+
+ response, error = self.rest_api.post(api, self.data)
+
+ if error:
+ self.module.fail_json(msg=error)
+
+ return response["data"]
+
+ def delete_org_user_s3_key(self, user_id, access_key):
+ api = "api/v3/org/users/current-user/s3-access-keys"
+
+ if user_id:
+ api = "api/v3/org/users/%s/s3-access-keys/%s" % (user_id, access_key,)
+
+ self.data = None
+ response, error = self.rest_api.delete(api, self.data)
+ if error:
+ self.module.fail_json(msg=error)
+
+ def apply(self):
+ """
+ Perform pre-checks, call functions and exit
+ """
+ result_message = ""
+ resp_data = {}
+ user_id = None
+
+ if self.parameters.get("unique_user_name"):
+ user_id = self.get_org_user_id(self.parameters["unique_user_name"])
+
+ if self.parameters["state"] == "present":
+ org_user_s3_key = None
+ if self.parameters.get("access_key"):
+ org_user_s3_key = self.get_org_user_s3_key(user_id, self.parameters["access_key"])
+ resp_data = org_user_s3_key
+
+ if not org_user_s3_key: # create
+ resp_data = self.create_org_user_s3_key(user_id)
+ self.na_helper.changed = True
+
+ if self.parameters["state"] == "absent":
+ self.delete_org_user_s3_key(user_id, self.parameters["access_key"])
+ self.na_helper.changed = True
+ result_message = "Org User S3 key deleted"
+
+ self.module.exit_json(changed=self.na_helper.changed, msg=result_message, resp=resp_data)
+
+
+def main():
+ """
+ Main function
+ """
+ na_sg_org_user_s3_key = SgOrgUserS3Key()
+ na_sg_org_user_s3_key.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/netapp/storagegrid/requirements.txt b/ansible_collections/netapp/storagegrid/requirements.txt
new file mode 100644
index 000000000..f2293605c
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/requirements.txt
@@ -0,0 +1 @@
+requests
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/compat/__init__.py b/ansible_collections/netapp/storagegrid/tests/unit/compat/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/compat/__init__.py
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/compat/builtins.py b/ansible_collections/netapp/storagegrid/tests/unit/compat/builtins.py
new file mode 100644
index 000000000..bfc8adfbe
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/compat/builtins.py
@@ -0,0 +1,34 @@
+# (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible 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.
+#
+# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+#
+# Compat for python2.7
+#
+
+# One unittest needs to import builtins via __import__() so we need to have
+# the string that represents it
+try:
+ import __builtin__
+except ImportError:
+ BUILTINS = "builtins"
+else:
+ BUILTINS = "__builtin__"
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/compat/mock.py b/ansible_collections/netapp/storagegrid/tests/unit/compat/mock.py
new file mode 100644
index 000000000..ce13d07cb
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/compat/mock.py
@@ -0,0 +1,125 @@
+# (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible 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.
+#
+# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+"""
+Compat module for Python3.x's unittest.mock module
+"""
+import sys
+
+# Python 2.7
+
+# Note: Could use the pypi mock library on python3.x as well as python2.x. It
+# is the same as the python3 stdlib mock library
+
+try:
+ # Allow wildcard import because we really do want to import all of mock's
+ # symbols into this compat shim
+ # pylint: disable=wildcard-import,unused-wildcard-import
+ from unittest.mock import *
+except ImportError:
+ # Python 2
+ # pylint: disable=wildcard-import,unused-wildcard-import
+ try:
+ from mock import *
+ except ImportError:
+ print("You need the mock library installed on python2.x to run tests")
+
+
+# Prior to 3.4.4, mock_open cannot handle binary read_data
+if sys.version_info >= (3,) and sys.version_info < (3, 4, 4):
+ file_spec = None
+
+ def _iterate_read_data(read_data):
+ # Helper for mock_open:
+ # Retrieve lines from read_data via a generator so that separate calls to
+ # readline, read, and readlines are properly interleaved
+ sep = b"\n" if isinstance(read_data, bytes) else "\n"
+ data_as_list = [l + sep for l in read_data.split(sep)]
+
+ if data_as_list[-1] == sep:
+ # If the last line ended in a newline, the list comprehension will have an
+ # extra entry that's just a newline. Remove this.
+ data_as_list = data_as_list[:-1]
+ else:
+ # If there wasn't an extra newline by itself, then the file being
+ # emulated doesn't have a newline to end the last line remove the
+ # newline that our naive format() added
+ data_as_list[-1] = data_as_list[-1][:-1]
+
+ for line in data_as_list:
+ yield line
+
+ def mock_open(mock=None, read_data=""):
+ """
+ A helper function to create a mock to replace the use of `open`. It works
+ for `open` called directly or used as a context manager.
+ The `mock` argument is the mock object to configure. If `None` (the
+ default) then a `MagicMock` will be created for you, with the API limited
+ to methods or attributes available on standard file handles.
+ `read_data` is a string for the `read` methoddline`, and `readlines` of the
+ file handle to return. This is an empty string by default.
+ """
+
+ def _readlines_side_effect(*args, **kwargs):
+ if handle.readlines.return_value is not None:
+ return handle.readlines.return_value
+ return list(_data)
+
+ def _read_side_effect(*args, **kwargs):
+ if handle.read.return_value is not None:
+ return handle.read.return_value
+ return type(read_data)().join(_data)
+
+ def _readline_side_effect():
+ if handle.readline.return_value is not None:
+ while True:
+ yield handle.readline.return_value
+ for line in _data:
+ yield line
+
+ global file_spec
+ if file_spec is None:
+ import _io
+
+ file_spec = list(
+ set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO)))
+ )
+
+ if mock is None:
+ mock = MagicMock(name="open", spec=open)
+
+ handle = MagicMock(spec=file_spec)
+ handle.__enter__.return_value = handle
+
+ _data = _iterate_read_data(read_data)
+
+ handle.write.return_value = None
+ handle.read.return_value = None
+ handle.readline.return_value = None
+ handle.readlines.return_value = None
+
+ handle.read.side_effect = _read_side_effect
+ handle.readline.side_effect = _readline_side_effect()
+ handle.readlines.side_effect = _readlines_side_effect
+
+ mock.return_value = handle
+ return mock
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/compat/unittest.py b/ansible_collections/netapp/storagegrid/tests/unit/compat/unittest.py
new file mode 100644
index 000000000..73a20cf8c
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/compat/unittest.py
@@ -0,0 +1,44 @@
+# (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible 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.
+#
+# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+'''
+Compat module for Python2.7's unittest module
+'''
+
+import sys
+
+import pytest
+
+# Allow wildcard import because we really do want to import all of
+# unittests's symbols into this compat shim
+# pylint: disable=wildcard-import,unused-wildcard-import
+if sys.version_info < (2, 7):
+ try:
+ # Need unittest2 on python2.6
+ from unittest2 import *
+ except ImportError:
+ print('You need unittest2 installed on python2.6.x to run tests')
+
+ class TestCase:
+ """ skip everything """
+ pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as unittest2 may not be available')
+else:
+ from unittest import *
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_account.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_account.py
new file mode 100644
index 000000000..e96697381
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_account.py
@@ -0,0 +1,380 @@
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID Tenant Ansible module: na_sg_grid_account"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+try:
+ from requests import Response
+except ImportError:
+ if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available')
+ else:
+ raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_grid_account import (
+ SgGridAccount as grid_account_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "empty_good": ({"data": []}, None),
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "delete_good": ({"code": 204}, None),
+ "pw_change_good": ({"code": 204}, None),
+ "grid_accounts": (
+ {
+ "data": [
+ {
+ "name": "TestTenantAccount",
+ "capabilities": ["management", "s3"],
+ "policy": {
+ "useAccountIdentitySource": True,
+ "allowPlatformServices": False,
+ "quotaObjectBytes": None,
+ },
+ "id": "12345678901234567890",
+ }
+ ]
+ },
+ None,
+ ),
+ "grid_account_record": (
+ {
+ "data": {
+ "name": "TestTenantAccount",
+ "capabilities": ["management", "s3"],
+ "policy": {
+ "useAccountIdentitySource": True,
+ "allowPlatformServices": False,
+ "quotaObjectBytes": None,
+ },
+ "id": "12345678901234567890",
+ }
+ },
+ None,
+ ),
+ "grid_account_record_with_quota": (
+ {
+ "data": {
+ "name": "TestTenantAccount",
+ "capabilities": ["management", "s3"],
+ "policy": {
+ "useAccountIdentitySource": True,
+ "allowPlatformServices": False,
+ "quotaObjectBytes": 10737418240,
+ },
+ "id": "12345678901234567890",
+ }
+ },
+ None,
+ ),
+ "grid_account_record_update_quota": (
+ {
+ "data": {
+ "name": "TestTenantAccount",
+ "capabilities": ["management", "s3"],
+ "policy": {
+ "useAccountIdentitySource": True,
+ "allowPlatformServices": False,
+ "quotaObjectBytes": 21474836480,
+ },
+ "id": "12345678901234567890",
+ }
+ },
+ None,
+ ),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """ a group of related Unit Tests """
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(
+ basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json
+ )
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ "name": "TestTenantAccount",
+ "protocol": "s3",
+ "management": True,
+ "use_own_identity_source": True,
+ "allow_platform_services": False,
+ "password": "abc123",
+ "quota_size": 0,
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "state": "present",
+ "name": "TestTenantAccount",
+ "protocol": "s3",
+ "management": True,
+ "use_own_identity_source": True,
+ "allow_platform_services": False,
+ "password": "abc123",
+ "quota_size": 0,
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_create_na_sg_grid_account(self):
+ return dict(
+ {
+ "state": "present",
+ "name": "TestTenantAccount",
+ "protocol": "s3",
+ "management": True,
+ "use_own_identity_source": True,
+ "allow_platform_services": False,
+ "password": "abc123",
+ "quota_size": 0,
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_delete_na_sg_grid_account(self):
+ return dict(
+ {
+ "state": "absent",
+ "name": "TestTenantAccount",
+ "protocol": "s3",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def test_module_fail_when_required_args_missing(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ grid_account_module()
+ print(
+ "Info: test_module_fail_when_required_args_missing: %s"
+ % exc.value.args[0]["msg"]
+ )
+
+ def test_module_fail_when_required_args_present(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ grid_account_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print(
+ "Info: test_module_fail_when_required_args_present: %s"
+ % exc.value.args[0]["msg"]
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_create_na_sg_grid_account_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_grid_account())
+ my_obj = grid_account_module()
+ mock_request.side_effect = [
+ SRR["empty_good"], # get
+ SRR["grid_accounts"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_create_na_sg_tenant_account_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_idempotent_create_na_sg_grid_account_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_grid_account())
+ my_obj = grid_account_module()
+ mock_request.side_effect = [
+ SRR["grid_accounts"], # get id
+ SRR["grid_account_record"], # get account
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_idempotent_create_na_sg_tenant_account_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert not exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_update_na_sg_grid_account_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_account()
+ args["quota_size"] = 10
+ set_module_args(args)
+ my_obj = grid_account_module()
+ mock_request.side_effect = [
+ SRR["grid_accounts"], # get
+ SRR["grid_account_record"], # get
+ SRR["grid_account_record_with_quota"], # put
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_update_na_sg_tenant_account_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_update_na_sg_grid_account_quota_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_account()
+ args["quota_size"] = 20480
+ args["quota_size_unit"] = "mb"
+ set_module_args(args)
+ my_obj = grid_account_module()
+ mock_request.side_effect = [
+ SRR["grid_accounts"], # get
+ SRR["grid_account_record_with_quota"], # get
+ SRR["grid_account_record_update_quota"], # put
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_create_na_sg_tenant_account_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ # update Tenant Account and set pass
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_update_na_sg_grid_account_and_set_password_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_account()
+ args["quota_size"] = 20480
+ args["quota_size_unit"] = "mb"
+ args["update_password"] = "always"
+
+ set_module_args(args)
+ my_obj = grid_account_module()
+ mock_request.side_effect = [
+ SRR["grid_accounts"], # get
+ SRR["grid_account_record_with_quota"], # get
+ SRR["grid_account_record_update_quota"], # put
+ SRR["pw_change_good"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_update_na_sg_grid_account_and_set_password_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ # set pass only
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_set_na_sg_grid_account_root_password_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_account()
+ args["update_password"] = "always"
+
+ set_module_args(args)
+ my_obj = grid_account_module()
+ mock_request.side_effect = [
+ SRR["grid_accounts"], # get id
+ SRR["grid_account_record"], # get account
+ SRR["pw_change_good"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_set_na_sg_grid_account_root_password_pass: %s" % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_delete_na_sg_grid_account_pass(self, mock_request):
+ set_module_args(self.set_args_delete_na_sg_grid_account())
+ my_obj = grid_account_module()
+ mock_request.side_effect = [
+ SRR["grid_accounts"], # get
+ SRR["grid_account_record"], # get
+ SRR["delete_good"], # delete
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_create_na_sg_tenant_account_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_certificate.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_certificate.py
new file mode 100644
index 000000000..74974abff
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_certificate.py
@@ -0,0 +1,342 @@
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID Grid Certificate Ansible module: na_sg_grid_certificate"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+try:
+ from requests import Response
+except ImportError:
+ if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available')
+ else:
+ raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_grid_certificate import (
+ SgGridCertificate as grid_certificate_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "empty_good": ({"data": []}, None),
+ "not_found": ({"status": "error", "code": 404, "data": {}}, {"key": "error.404"},),
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "delete_good": (None, None),
+ "update_good": (None, None),
+ "cert_unset": ({"data": {"serverCertificateEncoded": None, "caBundleEncoded": None}}, None),
+ "storage_api_cert": (
+ {
+ "data": {
+ "serverCertificateEncoded": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIICyDCCAbACCQCgFntI3q7iADANBgkqhkiG9w0BAQsFADAmMQswCQYDVQQGEwJV\n"
+ "UzEXMBUGA1UEAwwOczMuZXhhbXBsZS5jb20wHhcNMjEwNDI5MDQ1NTM1WhcNMjIw\n"
+ "NDI5MDQ1NTM1WjAmMQswCQYDVQQGEwJVUzEXMBUGA1UEAwwOczMuZXhhbXBsZS5j\n"
+ "b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0LMcJUdWmTtxi7U7B\n"
+ "yldDRfyCD9W+QJ1Ygm7E9iFwvkThUCV5q+DIcgSfogoSKaQuHaImLXMZn36QC22n\n"
+ "+Ah2EGrQiggyny3wDzuWf5/Qg7ogqQRqiespBFLlV4RGCREHK0y5uq8mzpIaQ8l8\n"
+ "STa7nLS7BIc6rD15BJaNWZpDVHIzhljlnhfnqwio/ZfP++lAjk4/j8pPGPEEI5Fe\n"
+ "WxhOtQjr7xTHeJxKHp2VKiLEvFxniL3qk4uJ3k5fJ7IqALUEPWH92brFp2IkObUA\n"
+ "EGsZYB4KFV7asBVhGuspYNzUQ6NqWbEUmtTjKEXcb1TA8RK+Pc2TotOrQ2E7Z+rU\n"
+ "gl2fAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAD5PW1WI7GCfxLQjaitnXpD1MR2O\n"
+ "6b5csymPYwRejMsSswd8egjs+vO2pbF9TptLjqGliE9XUoI+mWpuMzzd75F0jcjq\n"
+ "1DhlINgAmjUJEAg0RAqce0Kn8xQF+SofMtkOH+nZm3Q9nbTJKr1H5m2TnCq3v5TH\n"
+ "Qo0ASf0LLGgrwUtT0IghdSttYLS89dJprZ6c5wK7qeBzxfdHxxjiaSnvByL2Ryn5\n"
+ "cec9lptYKoRY42hWvkQv9Wkr3DDoyNA3xPdZJr0Hpf8/mSPnt9r/AR8E32xi0SXp\n"
+ "hOMTDgMicbK82ycxz0yW88gm6yhrChlJrWaEsVGod3FU+lbMAnagYZ/Vwp8=\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "caBundleEncoded": None,
+ }
+ },
+ None,
+ ),
+ "storage_api_cert_update": (
+ {
+ "data": {
+ "serverCertificateEncoded": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIICzjCCAbYCCQDZVi1OT89SAjANBgkqhkiG9w0BAQsFADApMQswCQYDVQQGEwJV\n"
+ "UzEaMBgGA1UEAwwRczMubmV3ZXhhbXBsZS5jb20wHhcNMjEwNDI5MDQ1NzIxWhcN\n"
+ "MjIwNDI5MDQ1NzIxWjApMQswCQYDVQQGEwJVUzEaMBgGA1UEAwwRczMubmV3ZXhh\n"
+ "bXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmg37q2sjZ\n"
+ "k+HsXtai3PSMtGUiqij04JtG9ahMqIejuxy5sDCWnigh//NjdK+wPYc2VfYd6KFA\n"
+ "Uk9rP84M7sqdqGzIzmyEu7INyCnlbxcXlST6UZDsZnVU7Gk2GvUzk2OoO5N+G0oI\n"
+ "Lfc/3eKTx9j9BguOaWUy+ni+Te8j6EwK6HolGRBjLYqf1SYFBzaoVpy7pmzaFZ4R\n"
+ "10jFSxHbotIZ+kR8pPE5jGkP8OjOfrpbhEgmffpeq2MSCMRuhRtRiVp4ULwkMTRN\n"
+ "tFj89mu1gl9T3lYM/LO1SmBv3il0mNmrTL+99UJ4s2eL0zr/uHAVYJcVqFgWP7X8\n"
+ "WnOk+d86b0TXAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFmGV3IOuNYeM3LQxls+\n"
+ "/CNHznvIqvoiJOWq0S7LFy1eO7PVzCl3l/fDKjGMt2lGXeU89YKdFVPqsainNEFT\n"
+ "cNEWlezVut+/CWQpBXujyBqPLkYbzyGsakMImDb+MrSkBO5MCjlt38vppm5a97fB\n"
+ "9o/wM31e+N6gJLiHWs0XB9TK6bY9CvcutcGUOH/oxH1TEBgrJ3SoS7/HmZJSaCQA\n"
+ "hjZappzuEpGVXT8YDlb67PzUoE2rDWjdSFRXCk/0U6VR0xNgnN1WtfHaypU71DrB\n"
+ "zxbDaOIZoDp5G4OgjkFxoCoSWLant+LsqEwclIbCFgEvJPE8855UThelTHmIfivP\n"
+ "veI=\n-----END CERTIFICATE-----\n"
+ ),
+ "caBundleEncoded": None,
+ }
+ },
+ None,
+ ),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """ a group of related Unit Tests """
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json)
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ "server_certificate": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIICyDCCAbACCQCgFntI3q7iADANBgkqhkiG9w0BAQsFADAmMQswCQYDVQQGEwJV\n"
+ "UzEXMBUGA1UEAwwOczMuZXhhbXBsZS5jb20wHhcNMjEwNDI5MDQ1NTM1WhcNMjIw\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "private_key": (
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQD0LMcJUdWmTtxi\n"
+ "7U7ByldDRfyCD9W+QJ1Ygm7E9iFwvkThUCV5q+DIcgSfogoSKaQuHaImLXMZn36Q\n"
+ "C22n+Ah2EGrQiggyny3wDzuWf5/Qg7ogqQRqiespBFLlV4RGCREHK0y5uq8mzpIa\n"
+ "-----END PRIVATE KEY-----\n"
+ ),
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "state": "present",
+ "type": "storage-api",
+ "server_certificate": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIICyDCCAbACCQCgFntI3q7iADANBgkqhkiG9w0BAQsFADAmMQswCQYDVQQGEwJV\n"
+ "UzEXMBUGA1UEAwwOczMuZXhhbXBsZS5jb20wHhcNMjEwNDI5MDQ1NTM1WhcNMjIw\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "private_key": (
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQD0LMcJUdWmTtxi\n"
+ "7U7ByldDRfyCD9W+QJ1Ygm7E9iFwvkThUCV5q+DIcgSfogoSKaQuHaImLXMZn36Q\n"
+ "C22n+Ah2EGrQiggyny3wDzuWf5/Qg7ogqQRqiespBFLlV4RGCREHK0y5uq8mzpIa\n"
+ "-----END PRIVATE KEY-----\n"
+ ),
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_set_na_sg_grid_storage_api_certificate(self):
+ return dict(
+ {
+ "state": "present",
+ "type": "storage-api",
+ "server_certificate": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIICyDCCAbACCQCgFntI3q7iADANBgkqhkiG9w0BAQsFADAmMQswCQYDVQQGEwJV\n"
+ "UzEXMBUGA1UEAwwOczMuZXhhbXBsZS5jb20wHhcNMjEwNDI5MDQ1NTM1WhcNMjIw\n"
+ "NDI5MDQ1NTM1WjAmMQswCQYDVQQGEwJVUzEXMBUGA1UEAwwOczMuZXhhbXBsZS5j\n"
+ "b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0LMcJUdWmTtxi7U7B\n"
+ "yldDRfyCD9W+QJ1Ygm7E9iFwvkThUCV5q+DIcgSfogoSKaQuHaImLXMZn36QC22n\n"
+ "+Ah2EGrQiggyny3wDzuWf5/Qg7ogqQRqiespBFLlV4RGCREHK0y5uq8mzpIaQ8l8\n"
+ "STa7nLS7BIc6rD15BJaNWZpDVHIzhljlnhfnqwio/ZfP++lAjk4/j8pPGPEEI5Fe\n"
+ "WxhOtQjr7xTHeJxKHp2VKiLEvFxniL3qk4uJ3k5fJ7IqALUEPWH92brFp2IkObUA\n"
+ "EGsZYB4KFV7asBVhGuspYNzUQ6NqWbEUmtTjKEXcb1TA8RK+Pc2TotOrQ2E7Z+rU\n"
+ "gl2fAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAD5PW1WI7GCfxLQjaitnXpD1MR2O\n"
+ "6b5csymPYwRejMsSswd8egjs+vO2pbF9TptLjqGliE9XUoI+mWpuMzzd75F0jcjq\n"
+ "1DhlINgAmjUJEAg0RAqce0Kn8xQF+SofMtkOH+nZm3Q9nbTJKr1H5m2TnCq3v5TH\n"
+ "Qo0ASf0LLGgrwUtT0IghdSttYLS89dJprZ6c5wK7qeBzxfdHxxjiaSnvByL2Ryn5\n"
+ "cec9lptYKoRY42hWvkQv9Wkr3DDoyNA3xPdZJr0Hpf8/mSPnt9r/AR8E32xi0SXp\n"
+ "hOMTDgMicbK82ycxz0yW88gm6yhrChlJrWaEsVGod3FU+lbMAnagYZ/Vwp8=\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "private_key": (
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQD0LMcJUdWmTtxi\n"
+ "7U7ByldDRfyCD9W+QJ1Ygm7E9iFwvkThUCV5q+DIcgSfogoSKaQuHaImLXMZn36Q\n"
+ "C22n+Ah2EGrQiggyny3wDzuWf5/Qg7ogqQRqiespBFLlV4RGCREHK0y5uq8mzpIa\n"
+ "Q8l8STa7nLS7BIc6rD15BJaNWZpDVHIzhljlnhfnqwio/ZfP++lAjk4/j8pPGPEE\n"
+ "I5FeWxhOtQjr7xTHeJxKHp2VKiLEvFxniL3qk4uJ3k5fJ7IqALUEPWH92brFp2Ik\n"
+ "ObUAEGsZYB4KFV7asBVhGuspYNzUQ6NqWbEUmtTjKEXcb1TA8RK+Pc2TotOrQ2E7\n"
+ "Z+rUgl2fAgMBAAECggEAAwSSqTDTvSx4WNiqAocnsPMqfckIUUOnLjLef5yzKRuQ\n"
+ "6l/9NpXDP3b5S6fLDBJrrw46tNIW/BgWjl01y7+rCxqE13L9SvLgtHjbua52ITOf\n"
+ "l0u/fDmcKHOfOqpsPhlaloYYeqsuAwLGl4CC+wBEpuj26uDRcw4x7E78NV8IIxDf\n"
+ "8kUNPQXI9ox6P3isXrFkMncDfKLWOYJ5fF5zCoVZai/SS8z3FhGjAXlMkay48RX4\n"
+ "4vuP7TNLZ2O2pAk2aVs54tQyBn9MOxIzOg3/ZFLiKZR4pY6H5sm+bT263TdvN+A4\n"
+ "C8kwML5HnsCjVkTzJ/3dYc9SeUOuqvJI332GCQ9YcQKBgQD8Ev2qhS61kZ3WGO6G\n"
+ "DRkZ6tDyt5vCuzWQ8uAAXcAerFDWN6XtDPfXq2UVcWnoCQOUpnjslCb/NJgCetLh\n"
+ "mOPeJGRWyMly+YuYb4/rnbwSbUs28PO4D9B/f5YQBnBjGDLL/i2+wnXg3WZTVogf\n"
+ "WfdKziOHGSxmWd6JinI+4UkpiwKBgQD3+krkFORTsUAlTgeIy8+QzXSuclwNygcX\n"
+ "HAe0F96hSYHBC7+1n7nzC1lwcbkU3jLIt3A90Uwew4nr5GCu4sSVwDeWrqP2I9WH\n"
+ "4w0zeaFPC1QKfKGBtsIf/89pDz/7iGlcKWlEg+56VVIJn7qC2lO8qbeUCoglsSwC\n"
+ "vr2Qld5WvQKBgQCHM2xpHHv8GPlOTxsIPVg8RW0C8iYSITVO5GXu7FnSWdwVuc0+\n"
+ "QtlgDObvxF/oe4U3Ir7zLVdpRH1Pvy8Cn22AxYYn4hPiniQYg6Xu2zB3tbVE56Hh\n"
+ "FGJhMD59o+Z90AnWziMdENIG5NkwU9Y48pknvz7hBEiDMSqiHObAATerlwKBgQCP\n"
+ "5LhCY3Ees3MCcqXilkmqv93eQFP0WHAG0+gQc+1m7+2QJI4pCTdwtfw/SG5akpkr\n"
+ "aW6DIIkoLNVCgbIsqT/jmbdoA4z3DlIg2PrXDNQytuMcdreNOoyo3trvHr9E6SIi\n"
+ "LZF9BYWDjTDejsY+mgwPJPh2uinInWdpbF85oA11jQKBgQCc6U2fSwpPQowOaat/\n"
+ "pY5bDCKxhfwrKk3Ecye5HfhbBZ0pu6Oneiq6cNhQC0X69iFn6ogTFx5qqyMQrWH0\n"
+ "L+kQRkyYFLnebCzUA8364lieRzc3cN+xQEn+jX8z7eDZ8JsvVnKdc6lTjPTwN1Fj\n"
+ "FZtaH2L1IEiA8ZZapMb/MNNozg==\n"
+ "-----END PRIVATE KEY-----\n"
+ ),
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_delete_na_sg_grid_storage_api_certificate(self):
+ return dict(
+ {
+ "state": "absent",
+ "type": "storage-api",
+ "server_certificate": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIICyDCCAbACCQCgFntI3q7iADANBgkqhkiG9w0BAQsFADAmMQswCQYDVQQGEwJV\n"
+ "UzEXMBUGA1UEAwwOczMuZXhhbXBsZS5jb20wHhcNMjEwNDI5MDQ1NTM1WhcNMjIw\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "private_key": (
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQD0LMcJUdWmTtxi\n"
+ "7U7ByldDRfyCD9W+QJ1Ygm7E9iFwvkThUCV5q+DIcgSfogoSKaQuHaImLXMZn36Q\n"
+ "C22n+Ah2EGrQiggyny3wDzuWf5/Qg7ogqQRqiespBFLlV4RGCREHK0y5uq8mzpIa\n"
+ "-----END PRIVATE KEY-----\n"
+ ),
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def test_module_fail_when_required_args_missing(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ grid_certificate_module()
+ print("Info: test_module_fail_when_required_args_missing: %s" % exc.value.args[0]["msg"])
+
+ def test_module_pass_when_required_args_present(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ grid_certificate_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print("Info: test_module_pass_when_required_args_present: %s" % exc.value.args[0]["msg"])
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_set_na_sg_grid_storage_api_certificate_pass(self, mock_request):
+ set_module_args(self.set_args_set_na_sg_grid_storage_api_certificate())
+ my_obj = grid_certificate_module()
+ mock_request.side_effect = [
+ SRR["cert_unset"], # get
+ SRR["update_good"], # post
+ SRR["storage_api_cert"], # get
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_set_na_sg_grid_storage_api_certificate_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_idempotent_set_na_sg_grid_storage_api_certificate_pass(self, mock_request):
+ set_module_args(self.set_args_set_na_sg_grid_storage_api_certificate())
+ my_obj = grid_certificate_module()
+ mock_request.side_effect = [
+ SRR["storage_api_cert"], # get
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_idempotent_set_na_sg_grid_storage_api_certificate_pass: %s" % repr(exc.value.args[0]))
+ assert not exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_update_na_sg_grid_storage_api_certificate_pass(self, mock_request):
+ args = self.set_args_set_na_sg_grid_storage_api_certificate()
+ args["server_certificate"] = ""
+ args["private_key"] = ""
+
+ set_module_args(args)
+ my_obj = grid_certificate_module()
+ mock_request.side_effect = [
+ SRR["storage_api_cert"], # get
+ SRR["update_good"], # put
+ SRR["storage_api_cert_update"], # get
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_update_na_sg_grid_storage_api_certificate_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_delete_na_sg_storage_api_certificate_pass(self, mock_request):
+ set_module_args(self.set_args_delete_na_sg_grid_storage_api_certificate())
+ my_obj = grid_certificate_module()
+ mock_request.side_effect = [
+ SRR["storage_api_cert"], # get
+ SRR["delete_good"], # delete
+ SRR["cert_unset"], # get
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_delete_na_sg_storage_api_certificate_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_client_certificate.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_client_certificate.py
new file mode 100644
index 000000000..d21f9da9c
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_client_certificate.py
@@ -0,0 +1,347 @@
+# (c) 2022, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID Grid HA Group Ansible module: na_sg_grid_client_certificate"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+
+try:
+ from requests import Response
+except ImportError:
+ if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip("Skipping Unit Tests on 2.6 as requests is not available")
+ else:
+ raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_grid_client_certificate import (
+ SgGridClientCertificate as grid_client_certificate_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "empty_good": ({"data": []}, None),
+ "not_found": (
+ {"status": "error", "code": 404, "data": {}},
+ {"key": "error.404"},
+ ),
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "delete_good": (None, None),
+ "update_good": (None, None),
+ "version_114": ({"data": {"productVersion": "11.4.0-20200721.1338.d3969b3"}}, None),
+ "version_116": ({"data": {"productVersion": "11.6.0-20211120.0301.850531e"}}, None),
+ "client_cert_record": (
+ {
+ "data": {
+ "id": "841ee2c7-3144-4c3c-8709-335462c5b05d",
+ "displayName": "testcert1",
+ "publicKey": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIEOzCCAyOgAwIBAgIIFuVL2ktGT0MwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UE\n"
+ "BhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlTdW5ueXZhbGUxFDASBgNVBAoM\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "allowPrometheus": True,
+ "expiryDate": "2024-01-01T00:00:00.000Z",
+ }
+ },
+ None,
+ ),
+ "client_cert_record_updated": (
+ {
+ "data": {
+ "id": "841ee2c7-3144-4c3c-8709-335462c5b05d",
+ "displayName": "testcert1",
+ "publicKey": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIICrDCCAZSgAwIBAgIUM3IQEKIypqPrXmoA/KmELXfFAz8wDQYJKoZIhvcNAQEL\n"
+ "BQAwADAeFw0yMjA5MDUyMzI3MTVaFw0yNDA5MDQyMzI3MTVaMAAwggEiMA0GCSqG\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "allowPrometheus": True,
+ "expiryDate": "2024-01-01T00:00:00.000Z",
+ }
+ },
+ None,
+ ),
+ "client_cert_record_rename": (
+ {
+ "data": {
+ "id": "841ee2c7-3144-4c3c-8709-335462c5b05d",
+ "displayName": "testcert1-rename",
+ "publicKey": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIEOzCCAyOgAwIBAgIIFuVL2ktGT0MwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UE\n"
+ "BhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlTdW5ueXZhbGUxFDASBgNVBAoM\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "allowPrometheus": True,
+ "expiryDate": "2024-01-01T00:00:00.000Z",
+ }
+ },
+ None,
+ ),
+ "client_certificates": (
+ {
+ "data": [
+ {
+ "id": "841ee2c7-3144-4c3c-8709-335462c5b05d",
+ "displayName": "testcert1",
+ "publicKey": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIEOzCCAyOgAwIBAgIIFuVL2ktGT0MwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UE\n"
+ "BhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlTdW5ueXZhbGUxFDASBgNVBAoM\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "allowPrometheus": True,
+ "expiryDate": "2024-01-01T00:00:00.000Z",
+ },
+ {
+ "id": "869e1792-5505-42f1-a1fc-57a04e56f644",
+ "displayName": "testcert2",
+ "publicKey": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIC9DCCAdygAwIBAgIUD7y+AyrSqRjQdYVflLJ9aTIJu3wwDQYJKoZIhvcNAQEL\n"
+ "BQAwFTETMBEGA1UEAwwKUHJvbWV0aGV1czAeFw0yMjA4MjQxMjQxNDhaFw0yNDA4\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "allowPrometheus": True,
+ "expiryDate": "2024-01-01T00:00:00.000Z",
+ },
+ ]
+ },
+ None,
+ ),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """a group of related Unit Tests"""
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json)
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ "allow_prometheus": True,
+ "public_key": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIEOzCCAyOgAwIBAgIIFuVL2ktGT0MwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UE\n"
+ "BhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlTdW5ueXZhbGUxFDASBgNVBAoM\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "state": "present",
+ "display_name": "testcert1",
+ "allow_prometheus": True,
+ "public_key": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIEOzCCAyOgAwIBAgIIFuVL2ktGT0MwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UE\n"
+ "BhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlTdW5ueXZhbGUxFDASBgNVBAoM\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_create_na_sg_grid_client_certificate(self):
+ return dict(
+ {
+ "state": "present",
+ "display_name": "testcert1",
+ "allow_prometheus": True,
+ "public_key": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIEOzCCAyOgAwIBAgIIFuVL2ktGT0MwDQYJKoZIhvcNAQELBQAwbzELMAkGA1UE\n"
+ "BhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlTdW5ueXZhbGUxFDASBgNVBAoM\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_delete_na_sg_grid_client_certificate(self):
+ return dict(
+ {
+ "state": "absent",
+ "display_name": "testcert1",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_module_fail_when_required_args_missing(self, mock_request):
+ """required arguments are reported as errors"""
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ grid_client_certificate_module()
+ print("Info: test_module_fail_when_required_args_missing: %s" % exc.value.args[0]["msg"])
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_module_pass_when_required_args_present(self, mock_request):
+ """required arguments are reported as errors"""
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ grid_client_certificate_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print("Info: test_module_pass_when_required_args_present: %s" % exc.value.args[0]["msg"])
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_create_na_sg_grid_client_certificate_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_grid_client_certificate())
+ mock_request.side_effect = [
+ SRR["empty_good"], # get
+ SRR["client_cert_record"], # post
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_client_certificate_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_create_na_sg_grid_client_certificate_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_idempotent_create_na_sg_grid_client_certificate_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_client_certificate()
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["client_certificates"], # get
+ SRR["client_cert_record"], # get
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_client_certificate_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_idempotent_create_na_sg_grid_client_certificate_pass: %s" % repr(exc.value.args[0]))
+ assert not exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_update_na_sg_grid_client_certificate_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_client_certificate()
+ args["public_key"] = (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIICrDCCAZSgAwIBAgIUM3IQEKIypqPrXmoA/KmELXfFAz8wDQYJKoZIhvcNAQEL\n"
+ "BQAwADAeFw0yMjA5MDUyMzI3MTVaFw0yNDA5MDQyMzI3MTVaMAAwggEiMA0GCSqG\n"
+ "-----END CERTIFICATE-----\n",
+ )
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["client_certificates"], # get
+ SRR["client_cert_record"], # get
+ SRR["client_cert_record_updated"], # put
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_client_certificate_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_update_na_sg_grid_client_certificate_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_rename_na_sg_grid_client_certificate_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_client_certificate()
+ args["certificate_id"] = "841ee2c7-3144-4c3c-8709-335462c5b05d"
+ args["display_name"] = "testcert1-rename"
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["client_cert_record"], # get
+ SRR["client_cert_record_rename"], # put
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_client_certificate_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_rename_na_sg_grid_client_certificate_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_delete_na_sg_grid_client_certificate_pass(self, mock_request):
+ args = self.set_args_delete_na_sg_grid_client_certificate()
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["client_certificates"], # get
+ SRR["client_cert_record"], # get
+ SRR["delete_good"], # delete
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_client_certificate_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_delete_na_sg_grid_client_certificate_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_update_na_sg_grid_client_certificate_bad_certificate_id_fail(self, mock_request):
+ args = self.set_args_create_na_sg_grid_client_certificate()
+ args["certificate_id"] = "ffffffff-ffff-aaaa-aaaa-000000000000"
+ args["display_name"] = "Bad ID"
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["not_found"], # get
+ ]
+ with pytest.raises(AnsibleFailJson) as exc:
+ my_obj = grid_client_certificate_module()
+ my_obj.apply()
+ print("Info: test_update_na_sg_grid_client_certificate_bad_certificate_id_fail: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["failed"]
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_dns.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_dns.py
new file mode 100644
index 000000000..42abde9c8
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_dns.py
@@ -0,0 +1,241 @@
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID DNS Ansible module: na_sg_grid_dns"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+try:
+ from requests import Response
+except ImportError:
+ if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available')
+ else:
+ raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_grid_dns import (
+ SgGridDns as grid_dns_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "empty_good": ({"data": []}, None),
+ "not_found": (
+ {"status": "error", "code": 404, "data": {}},
+ {"key": "error.404"},
+ ),
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "delete_good": ({"code": 204}, None),
+ "no_dns_servers": ({"data": []}, None,),
+ "dns_servers": ({"data": ["10.11.12.5", "10.11.12.6"]}, None,),
+ "add_dns_servers": (
+ {"data": ["10.11.12.5", "10.11.12.6", "10.11.12.7"]},
+ None,
+ ),
+ "remove_dns_servers": ({"data": ["10.11.12.5"]}, None,),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """ a group of related Unit Tests """
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(
+ basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json
+ )
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ "dns_servers": "10.11.12.8",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "state": "present",
+ "dns_servers": "10.11.12.8",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_set_na_sg_grid_dns_servers(self):
+ return dict(
+ {
+ "state": "present",
+ "dns_servers": "10.11.12.5,10.11.12.6",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_add_na_sg_grid_dns_server(self):
+ return dict(
+ {
+ "state": "present",
+ "dns_servers": "10.11.12.5,10.11.12.6,10.11.12.7",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_remove_na_sg_grid_dns_server(self):
+ return dict(
+ {
+ "state": "present",
+ "dns_servers": "10.11.12.5",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def test_module_fail_when_required_args_missing(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ grid_dns_module()
+ print(
+ "Info: test_module_fail_when_required_args_missing: %s"
+ % exc.value.args[0]["msg"]
+ )
+
+ def test_module_fail_when_required_args_present(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ grid_dns_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print(
+ "Info: test_module_fail_when_required_args_present: %s"
+ % exc.value.args[0]["msg"]
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_set_na_sg_grid_dns_servers_pass(self, mock_request):
+ set_module_args(self.set_args_set_na_sg_grid_dns_servers())
+ my_obj = grid_dns_module()
+ mock_request.side_effect = [
+ SRR["no_dns_servers"], # get
+ SRR["dns_servers"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_set_na_sg_grid_dns_servers_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_idempotent_set_na_sg_grid_dns_servers_pass(self, mock_request):
+ set_module_args(self.set_args_set_na_sg_grid_dns_servers())
+ my_obj = grid_dns_module()
+ mock_request.side_effect = [
+ SRR["dns_servers"], # get
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_idempotent_set_na_sg_grid_dns_servers_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert not exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_add_na_sg_grid_dns_servers_pass(self, mock_request):
+ set_module_args(self.set_args_add_na_sg_grid_dns_server())
+ my_obj = grid_dns_module()
+ mock_request.side_effect = [
+ SRR["dns_servers"], # get
+ SRR["add_dns_servers"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_add_na_sg_grid_dns_servers_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_remove_na_sg_grid_dns_servers_pass(self, mock_request):
+ set_module_args(self.set_args_remove_na_sg_grid_dns_server())
+ my_obj = grid_dns_module()
+ mock_request.side_effect = [
+ SRR["dns_servers"], # get
+ SRR["remove_dns_servers"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_remove_na_sg_grid_dns_servers_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_gateway.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_gateway.py
new file mode 100644
index 000000000..0a5a7e386
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_gateway.py
@@ -0,0 +1,693 @@
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID Grid Load Balancer Endpoint Ansible module: na_sg_grid_gateway"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+
+# try:
+# from requests import Response
+# except ImportError:
+# if sys.version_info < (2, 7):
+# pytestmark = pytest.mark.skip("Skipping Unit Tests on 2.6 as requests is not available")
+# else:
+# raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_grid_gateway import (
+ SgGridGateway as grid_gateway_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "empty_good": ({"data": []}, None),
+ "not_found": (
+ {"status": "error", "code": 404, "data": {}},
+ {"key": "error.404"},
+ ),
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "delete_good": (None, None),
+ "update_good": (None, None),
+ "version_114": ({"data": {"productVersion": "11.4.0-20200721.1338.d3969b3"}}, None),
+ "version_116": ({"data": {"productVersion": "11.6.0-20211120.0301.850531e"}}, None),
+ "gateway_record": (
+ {
+ "data": {
+ "id": "e777d415-057f-4d37-9b0c-6d132d872ea0",
+ "displayName": "ansibletest-secure",
+ "enableIPv4": True,
+ "enableIPv6": True,
+ "port": 10443,
+ "secure": True,
+ "accountId": "0",
+ }
+ },
+ None,
+ ),
+ "gateway_record_ha_group_binding": (
+ {
+ "data": {
+ "id": "e777d415-057f-4d37-9b0c-6d132d872ea0",
+ "displayName": "ansibletest-secure",
+ "enableIPv4": True,
+ "enableIPv6": True,
+ "port": 10443,
+ "secure": True,
+ "accountId": "0",
+ "pinTargets": {"haGroups": ["c08e6dca-038d-4a05-9499-6fbd1e6a4c3e"], "nodeInterfaces": []},
+ }
+ },
+ None,
+ ),
+ "gateway_record_node_interface_binding": (
+ {
+ "data": {
+ "id": "e777d415-057f-4d37-9b0c-6d132d872ea0",
+ "displayName": "ansibletest-secure",
+ "enableIPv4": True,
+ "enableIPv6": True,
+ "port": 10443,
+ "secure": True,
+ "accountId": "0",
+ "pinTargets": {
+ "haGroups": [],
+ "nodeInterfaces": [
+ {"interface": "eth2", "nodeId": "0b1866ed-d6e7-41b4-815f-bf867348b76b"},
+ {"interface": "eth2", "nodeId": "970ad050-b68b-4aae-a94d-aef73f3095c4"},
+ ],
+ },
+ }
+ },
+ None,
+ ),
+ "gateway_record_rename": (
+ {
+ "data": {
+ "id": "e777d415-057f-4d37-9b0c-6d132d872ea0",
+ "displayName": "ansibletest-rename",
+ "enableIPv4": True,
+ "enableIPv6": True,
+ "port": 10443,
+ "secure": True,
+ "accountId": "0",
+ "pinTargets": {"haGroups": ["c08e6dca-038d-4a05-9499-6fbd1e6a4c3e"], "nodeInterfaces": []},
+ }
+ },
+ None,
+ ),
+ "ha_groups": (
+ {
+ "data": [
+ {
+ "id": "c08e6dca-038d-4a05-9499-6fbd1e6a4c3e",
+ "name": "site1_primary",
+ "description": "test ha group",
+ "virtualIps": ["10.193.174.117"],
+ "interfaces": [
+ {
+ "nodeId": "0b1866ed-d6e7-41b4-815f-bf867348b76b",
+ "nodeName": "SITE1-ADM1",
+ "interface": "eth2",
+ "preferredMaster": True,
+ },
+ {
+ "nodeId": "970ad050-b68b-4aae-a94d-aef73f3095c4",
+ "nodeName": "SITE2-ADM1",
+ "interface": "eth2",
+ },
+ ],
+ "gatewayCidr": "192.168.14.1/24",
+ },
+ {
+ "id": "da9ac524-9a16-4be0-9d6e-ec9b22218e75",
+ "name": "site1_gw",
+ "description": "another test ha group",
+ "virtualIps": ["10.193.204.200"],
+ "interfaces": [
+ {
+ "nodeId": "7bb5bf05-a04c-4344-8abd-08c5c4048666",
+ "nodeName": "SITE1-GW1",
+ "interface": "eth0",
+ "preferredMaster": True,
+ },
+ ],
+ "gatewayCidr": "192.168.14.1/24",
+ }
+ ]
+ },
+ None,
+ ),
+ "node_health": (
+ {
+ "data": [
+ {
+ "id": "0b1866ed-d6e7-41b4-815f-bf867348b76b",
+ "isPrimaryAdmin": True,
+ "name": "SITE1-ADM1",
+ "siteId": "ae56d06d-bd83-46bd-adce-77146b1d94bd",
+ "siteName": "SITE1",
+ "severity": "normal",
+ "state": "connected",
+ "type": "adminNode",
+ },
+ {
+ "id": "970ad050-b68b-4aae-a94d-aef73f3095c4",
+ "isPrimaryAdmin": False,
+ "name": "SITE2-ADM1",
+ "siteId": "7c24002e-5157-43e9-83e5-02db9b265b02",
+ "siteName": "SITE2",
+ "severity": "normal",
+ "state": "connected",
+ "type": "adminNode",
+ },
+ ]
+ },
+ None,
+ ),
+ "present_gateways": (
+ {
+ "data": [
+ {
+ "id": "e777d415-057f-4d37-9b0c-6d132d872ea0",
+ "displayName": "ansibletest-secure",
+ "enableIPv4": True,
+ "enableIPv6": True,
+ "port": 10443,
+ "secure": True,
+ "accountId": "0",
+ }
+ ]
+ },
+ None,
+ ),
+ "present_gateways_with_binding": (
+ {
+ "data": [
+ {
+ "id": "e777d415-057f-4d37-9b0c-6d132d872ea0",
+ "displayName": "ansibletest-secure",
+ "enableIPv4": True,
+ "enableIPv6": True,
+ "port": 10443,
+ "secure": True,
+ "accountId": "0",
+ "pinTargets": {"haGroups": [], "nodeInterfaces": []},
+ }
+ ]
+ },
+ None,
+ ),
+ "server_config": (
+ {
+ "data": {
+ "defaultServiceType": "s3",
+ "certSource": "plaintext",
+ "plaintextCertData": {
+ "serverCertificateEncoded": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIICyDCCAbACCQCgFntI3q7iADANBgkqhkiG9w0BAQsFADAmMQswCQYDVQQGEwJV\n"
+ "UzEXMBUGA1UEAwwOczMuZXhhbXBsZS5jb20wHhcNMjEwNDI5MDQ1NTM1WhcNMjIw\n"
+ "NDI5MDQ1NTM1WjAmMQswCQYDVQQGEwJVUzEXMBUGA1UEAwwOczMuZXhhbXBsZS5j\n"
+ "b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0LMcJUdWmTtxi7U7B\n"
+ "yldDRfyCD9W+QJ1Ygm7E9iFwvkThUCV5q+DIcgSfogoSKaQuHaImLXMZn36QC22n\n"
+ "+Ah2EGrQiggyny3wDzuWf5/Qg7ogqQRqiespBFLlV4RGCREHK0y5uq8mzpIaQ8l8\n"
+ "STa7nLS7BIc6rD15BJaNWZpDVHIzhljlnhfnqwio/ZfP++lAjk4/j8pPGPEEI5Fe\n"
+ "WxhOtQjr7xTHeJxKHp2VKiLEvFxniL3qk4uJ3k5fJ7IqALUEPWH92brFp2IkObUA\n"
+ "EGsZYB4KFV7asBVhGuspYNzUQ6NqWbEUmtTjKEXcb1TA8RK+Pc2TotOrQ2E7Z+rU\n"
+ "gl2fAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAD5PW1WI7GCfxLQjaitnXpD1MR2O\n"
+ "6b5csymPYwRejMsSswd8egjs+vO2pbF9TptLjqGliE9XUoI+mWpuMzzd75F0jcjq\n"
+ "1DhlINgAmjUJEAg0RAqce0Kn8xQF+SofMtkOH+nZm3Q9nbTJKr1H5m2TnCq3v5TH\n"
+ "Qo0ASf0LLGgrwUtT0IghdSttYLS89dJprZ6c5wK7qeBzxfdHxxjiaSnvByL2Ryn5\n"
+ "cec9lptYKoRY42hWvkQv9Wkr3DDoyNA3xPdZJr0Hpf8/mSPnt9r/AR8E32xi0SXp\n"
+ "hOMTDgMicbK82ycxz0yW88gm6yhrChlJrWaEsVGod3FU+lbMAnagYZ/Vwp8=\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "caBundleEncoded": None,
+ "metadata": {
+ "serverCertificateDetails": {
+ "subject": "/CN=test",
+ "issuer": "/CN=test",
+ "serialNumber": "32:6F:20:EB:0E:90:60:7E:07:8F:6E:CC:02:2D:7C:37:3D:AB:42:7E",
+ "notBefore": "2021-09-27T12:39:17.000Z",
+ "notAfter": "2023-09-27T12:39:17.000Z",
+ "fingerPrints": {
+ "SHA-1": "A4:F9:74:BE:E8:A2:46:C2:E1:23:DE:8F:A8:1B:F1:C4:91:51:C5:56",
+ "SHA-256": "7B:65:7F:CD:35:8F:33:1C:C8:2D:F0:C1:9F:58:2F:2B:3B:78:44:95:4E:23:8C:1B:2B:91:6C:94:B0:71:64:E8",
+ },
+ "subjectAltNames": ["DNS:*.test.com"],
+ }
+ },
+ },
+ }
+ },
+ None,
+ ),
+ "server_config_cert_update": (
+ {
+ "data": {
+ "defaultServiceType": "s3",
+ "certSource": "plaintext",
+ "plaintextCertData": {
+ "serverCertificateEncoded": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIICzjCCAbYCCQDZVi1OT89SAjANBgkqhkiG9w0BAQsFADApMQswCQYDVQQGEwJV\n"
+ "UzEaMBgGA1UEAwwRczMubmV3ZXhhbXBsZS5jb20wHhcNMjEwNDI5MDQ1NzIxWhcN\n"
+ "MjIwNDI5MDQ1NzIxWjApMQswCQYDVQQGEwJVUzEaMBgGA1UEAwwRczMubmV3ZXhh\n"
+ "bXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmg37q2sjZ\n"
+ "k+HsXtai3PSMtGUiqij04JtG9ahMqIejuxy5sDCWnigh//NjdK+wPYc2VfYd6KFA\n"
+ "Uk9rP84M7sqdqGzIzmyEu7INyCnlbxcXlST6UZDsZnVU7Gk2GvUzk2OoO5N+G0oI\n"
+ "Lfc/3eKTx9j9BguOaWUy+ni+Te8j6EwK6HolGRBjLYqf1SYFBzaoVpy7pmzaFZ4R\n"
+ "10jFSxHbotIZ+kR8pPE5jGkP8OjOfrpbhEgmffpeq2MSCMRuhRtRiVp4ULwkMTRN\n"
+ "tFj89mu1gl9T3lYM/LO1SmBv3il0mNmrTL+99UJ4s2eL0zr/uHAVYJcVqFgWP7X8\n"
+ "WnOk+d86b0TXAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFmGV3IOuNYeM3LQxls+\n"
+ "/CNHznvIqvoiJOWq0S7LFy1eO7PVzCl3l/fDKjGMt2lGXeU89YKdFVPqsainNEFT\n"
+ "cNEWlezVut+/CWQpBXujyBqPLkYbzyGsakMImDb+MrSkBO5MCjlt38vppm5a97fB\n"
+ "9o/wM31e+N6gJLiHWs0XB9TK6bY9CvcutcGUOH/oxH1TEBgrJ3SoS7/HmZJSaCQA\n"
+ "hjZappzuEpGVXT8YDlb67PzUoE2rDWjdSFRXCk/0U6VR0xNgnN1WtfHaypU71DrB\n"
+ "zxbDaOIZoDp5G4OgjkFxoCoSWLant+LsqEwclIbCFgEvJPE8855UThelTHmIfivP\n"
+ "veI=\n-----END CERTIFICATE-----\n"
+ ),
+ "caBundleEncoded": None,
+ "metadata": {
+ "serverCertificateDetails": {
+ "subject": "/CN=test",
+ "issuer": "/CN=test",
+ "serialNumber": "32:6F:20:EB:0E:90:60:7E:07:8F:6E:CC:02:2D:7C:37:3D:AB:42:7E",
+ "notBefore": "2021-09-27T12:39:17.000Z",
+ "notAfter": "2023-09-27T12:39:17.000Z",
+ "fingerPrints": {
+ "SHA-1": "F2:C2:6F:A8:45:DA:86:09:91:F5:04:B0:25:43:B7:FC:FA:C1:43:F8",
+ "SHA-256": "99:3E:21:1A:03:25:69:C8:0A:D5:FE:E3:FB:6E:51:03:BD:A7:0E:88:6B:53:06:04:92:3B:34:17:68:43:F7:2F",
+ },
+ "subjectAltNames": ["DNS:*.test.com"],
+ }
+ },
+ },
+ }
+ },
+ None,
+ ),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """a group of related Unit Tests"""
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json)
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ "display_name": "ansibletest-secure",
+ "default_service_type": "s3",
+ "server_certificate": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIICyDCCAbACCQCgFntI3q7iADANBgkqhkiG9w0BAQsFADAmMQswCQYDVQQGEwJV\n"
+ "UzEXMBUGA1UEAwwOczMuZXhhbXBsZS5jb20wHhcNMjEwNDI5MDQ1NTM1WhcNMjIw\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "private_key": (
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQD0LMcJUdWmTtxi\n"
+ "7U7ByldDRfyCD9W+QJ1Ygm7E9iFwvkThUCV5q+DIcgSfogoSKaQuHaImLXMZn36Q\n"
+ "C22n+Ah2EGrQiggyny3wDzuWf5/Qg7ogqQRqiespBFLlV4RGCREHK0y5uq8mzpIa\n"
+ "-----END PRIVATE KEY-----\n"
+ ),
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "state": "present",
+ "display_name": "ansibletest-secure",
+ "default_service_type": "s3",
+ "port": 10443,
+ "server_certificate": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIICyDCCAbACCQCgFntI3q7iADANBgkqhkiG9w0BAQsFADAmMQswCQYDVQQGEwJV\n"
+ "UzEXMBUGA1UEAwwOczMuZXhhbXBsZS5jb20wHhcNMjEwNDI5MDQ1NTM1WhcNMjIw\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "private_key": (
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQD0LMcJUdWmTtxi\n"
+ "7U7ByldDRfyCD9W+QJ1Ygm7E9iFwvkThUCV5q+DIcgSfogoSKaQuHaImLXMZn36Q\n"
+ "C22n+Ah2EGrQiggyny3wDzuWf5/Qg7ogqQRqiespBFLlV4RGCREHK0y5uq8mzpIa\n"
+ "-----END PRIVATE KEY-----\n"
+ ),
+ "api_url": "https://gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_create_na_sg_grid_gateway_port(self):
+ return dict(
+ {
+ "state": "present",
+ "display_name": "ansibletest-secure",
+ "default_service_type": "s3",
+ "port": 10443,
+ "server_certificate": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIICyDCCAbACCQCgFntI3q7iADANBgkqhkiG9w0BAQsFADAmMQswCQYDVQQGEwJV\n"
+ "UzEXMBUGA1UEAwwOczMuZXhhbXBsZS5jb20wHhcNMjEwNDI5MDQ1NTM1WhcNMjIw\n"
+ "NDI5MDQ1NTM1WjAmMQswCQYDVQQGEwJVUzEXMBUGA1UEAwwOczMuZXhhbXBsZS5j\n"
+ "b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0LMcJUdWmTtxi7U7B\n"
+ "yldDRfyCD9W+QJ1Ygm7E9iFwvkThUCV5q+DIcgSfogoSKaQuHaImLXMZn36QC22n\n"
+ "+Ah2EGrQiggyny3wDzuWf5/Qg7ogqQRqiespBFLlV4RGCREHK0y5uq8mzpIaQ8l8\n"
+ "STa7nLS7BIc6rD15BJaNWZpDVHIzhljlnhfnqwio/ZfP++lAjk4/j8pPGPEEI5Fe\n"
+ "WxhOtQjr7xTHeJxKHp2VKiLEvFxniL3qk4uJ3k5fJ7IqALUEPWH92brFp2IkObUA\n"
+ "EGsZYB4KFV7asBVhGuspYNzUQ6NqWbEUmtTjKEXcb1TA8RK+Pc2TotOrQ2E7Z+rU\n"
+ "gl2fAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAD5PW1WI7GCfxLQjaitnXpD1MR2O\n"
+ "6b5csymPYwRejMsSswd8egjs+vO2pbF9TptLjqGliE9XUoI+mWpuMzzd75F0jcjq\n"
+ "1DhlINgAmjUJEAg0RAqce0Kn8xQF+SofMtkOH+nZm3Q9nbTJKr1H5m2TnCq3v5TH\n"
+ "Qo0ASf0LLGgrwUtT0IghdSttYLS89dJprZ6c5wK7qeBzxfdHxxjiaSnvByL2Ryn5\n"
+ "cec9lptYKoRY42hWvkQv9Wkr3DDoyNA3xPdZJr0Hpf8/mSPnt9r/AR8E32xi0SXp\n"
+ "hOMTDgMicbK82ycxz0yW88gm6yhrChlJrWaEsVGod3FU+lbMAnagYZ/Vwp8=\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "private_key": (
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQD0LMcJUdWmTtxi\n"
+ "7U7ByldDRfyCD9W+QJ1Ygm7E9iFwvkThUCV5q+DIcgSfogoSKaQuHaImLXMZn36Q\n"
+ "C22n+Ah2EGrQiggyny3wDzuWf5/Qg7ogqQRqiespBFLlV4RGCREHK0y5uq8mzpIa\n"
+ "Q8l8STa7nLS7BIc6rD15BJaNWZpDVHIzhljlnhfnqwio/ZfP++lAjk4/j8pPGPEE\n"
+ "I5FeWxhOtQjr7xTHeJxKHp2VKiLEvFxniL3qk4uJ3k5fJ7IqALUEPWH92brFp2Ik\n"
+ "ObUAEGsZYB4KFV7asBVhGuspYNzUQ6NqWbEUmtTjKEXcb1TA8RK+Pc2TotOrQ2E7\n"
+ "Z+rUgl2fAgMBAAECggEAAwSSqTDTvSx4WNiqAocnsPMqfckIUUOnLjLef5yzKRuQ\n"
+ "6l/9NpXDP3b5S6fLDBJrrw46tNIW/BgWjl01y7+rCxqE13L9SvLgtHjbua52ITOf\n"
+ "l0u/fDmcKHOfOqpsPhlaloYYeqsuAwLGl4CC+wBEpuj26uDRcw4x7E78NV8IIxDf\n"
+ "8kUNPQXI9ox6P3isXrFkMncDfKLWOYJ5fF5zCoVZai/SS8z3FhGjAXlMkay48RX4\n"
+ "4vuP7TNLZ2O2pAk2aVs54tQyBn9MOxIzOg3/ZFLiKZR4pY6H5sm+bT263TdvN+A4\n"
+ "C8kwML5HnsCjVkTzJ/3dYc9SeUOuqvJI332GCQ9YcQKBgQD8Ev2qhS61kZ3WGO6G\n"
+ "DRkZ6tDyt5vCuzWQ8uAAXcAerFDWN6XtDPfXq2UVcWnoCQOUpnjslCb/NJgCetLh\n"
+ "mOPeJGRWyMly+YuYb4/rnbwSbUs28PO4D9B/f5YQBnBjGDLL/i2+wnXg3WZTVogf\n"
+ "WfdKziOHGSxmWd6JinI+4UkpiwKBgQD3+krkFORTsUAlTgeIy8+QzXSuclwNygcX\n"
+ "HAe0F96hSYHBC7+1n7nzC1lwcbkU3jLIt3A90Uwew4nr5GCu4sSVwDeWrqP2I9WH\n"
+ "4w0zeaFPC1QKfKGBtsIf/89pDz/7iGlcKWlEg+56VVIJn7qC2lO8qbeUCoglsSwC\n"
+ "vr2Qld5WvQKBgQCHM2xpHHv8GPlOTxsIPVg8RW0C8iYSITVO5GXu7FnSWdwVuc0+\n"
+ "QtlgDObvxF/oe4U3Ir7zLVdpRH1Pvy8Cn22AxYYn4hPiniQYg6Xu2zB3tbVE56Hh\n"
+ "FGJhMD59o+Z90AnWziMdENIG5NkwU9Y48pknvz7hBEiDMSqiHObAATerlwKBgQCP\n"
+ "5LhCY3Ees3MCcqXilkmqv93eQFP0WHAG0+gQc+1m7+2QJI4pCTdwtfw/SG5akpkr\n"
+ "aW6DIIkoLNVCgbIsqT/jmbdoA4z3DlIg2PrXDNQytuMcdreNOoyo3trvHr9E6SIi\n"
+ "LZF9BYWDjTDejsY+mgwPJPh2uinInWdpbF85oA11jQKBgQCc6U2fSwpPQowOaat/\n"
+ "pY5bDCKxhfwrKk3Ecye5HfhbBZ0pu6Oneiq6cNhQC0X69iFn6ogTFx5qqyMQrWH0\n"
+ "L+kQRkyYFLnebCzUA8364lieRzc3cN+xQEn+jX8z7eDZ8JsvVnKdc6lTjPTwN1Fj\n"
+ "FZtaH2L1IEiA8ZZapMb/MNNozg==\n"
+ "-----END PRIVATE KEY-----\n"
+ ),
+ "api_url": "https://gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_delete_na_sg_grid_gateway_port(self):
+ return dict(
+ {
+ "state": "absent",
+ "display_name": "ansibletest-secure",
+ "default_service_type": "s3",
+ "port": 10443,
+ "server_certificate": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIICyDCCAbACCQCgFntI3q7iADANBgkqhkiG9w0BAQsFADAmMQswCQYDVQQGEwJV\n"
+ "UzEXMBUGA1UEAwwOczMuZXhhbXBsZS5jb20wHhcNMjEwNDI5MDQ1NTM1WhcNMjIw\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "private_key": (
+ "-----BEGIN PRIVATE KEY-----\n"
+ "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQD0LMcJUdWmTtxi\n"
+ "7U7ByldDRfyCD9W+QJ1Ygm7E9iFwvkThUCV5q+DIcgSfogoSKaQuHaImLXMZn36Q\n"
+ "C22n+Ah2EGrQiggyny3wDzuWf5/Qg7ogqQRqiespBFLlV4RGCREHK0y5uq8mzpIa\n"
+ "-----END PRIVATE KEY-----\n"
+ ),
+ "api_url": "https://gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_module_fail_when_required_args_missing(self, mock_request):
+ """required arguments are reported as errors"""
+ mock_request.side_effect = [
+ SRR["version_114"],
+ ]
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ grid_gateway_module()
+ print("Info: test_module_fail_when_required_args_missing: %s" % exc.value.args[0]["msg"])
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_module_pass_when_required_args_present(self, mock_request):
+ """required arguments are reported as errors"""
+ mock_request.side_effect = [
+ SRR["version_114"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ grid_gateway_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print("Info: test_module_pass_when_required_args_present: %s" % exc.value.args[0]["msg"])
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_create_na_sg_grid_gateway_port_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_grid_gateway_port())
+ mock_request.side_effect = [
+ SRR["version_114"], # get
+ SRR["empty_good"], # get
+ SRR["gateway_record"], # post
+ SRR["server_config"], # post
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_gateway_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_create_na_sg_grid_gateway_port_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_idempotent_create_na_sg_grid_gateway_port_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_gateway_port()
+ del args["private_key"]
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["version_114"], # get
+ SRR["present_gateways"], # get
+ SRR["server_config"], # get
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_gateway_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_idempotent_create_na_sg_grid_gateway_port_pass: %s" % repr(exc.value.args[0]))
+ assert not exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_update_na_sg_grid_gateway_certificate_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_gateway_port()
+ args["server_certificate"] = "-----BEGIN CERTIFICATE-----\nABCDEFGABCD\n-----END CERTIFICATE-----\n"
+ args["private_key"] = "-----BEGIN PRIVATE KEY-----\nABCDEFGABCD\n-----END PRIVATE KEY-----\n"
+
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["version_114"], # get
+ SRR["present_gateways"], # get
+ SRR["server_config"], # get
+ SRR["server_config_cert_update"], # put
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_gateway_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_update_na_sg_grid_gateway_certificate_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_delete_na_sg_grid_gateway_port_pass(self, mock_request):
+ set_module_args(self.set_args_delete_na_sg_grid_gateway_port())
+ mock_request.side_effect = [
+ SRR["version_114"], # get
+ SRR["present_gateways"], # get
+ SRR["server_config"], # get
+ SRR["delete_good"], # delete
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_gateway_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_delete_na_sg_grid_gateway_port_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_module_fail_minimum_version_not_met(self, mock_request):
+ args = self.set_args_create_na_sg_grid_gateway_port()
+ args["binding_mode"] = "ha-groups"
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["version_114"], # get
+ ]
+ with pytest.raises(AnsibleFailJson) as exc:
+ grid_gateway_module()
+ print("Info: test_module_fail_minimum_version_not_met: %s" % exc.value.args[0]["msg"])
+
+ # test create with ha groups
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_create_na_sg_grid_gateway_port_with_ha_group_binding_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_gateway_port()
+ args["binding_mode"] = "ha-groups"
+ args["ha_groups"] = ["site1_primary", "da9ac524-9a16-4be0-9d6e-ec9b22218e75"]
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["version_116"], # get
+ SRR["ha_groups"], # get
+ SRR["empty_good"], # get
+ SRR["gateway_record_ha_group_binding"], # post
+ SRR["server_config"], # post
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_gateway_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_create_na_sg_grid_gateway_port_with_ha_group_binding_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ # test create with bad ha group ID
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_create_na_sg_grid_gateway_port_with_bad_ha_group_binding_fail(self, mock_request):
+ mock_request.side_effect = [
+ SRR["version_116"], # get
+ SRR["ha_groups"], # get
+ ]
+ with pytest.raises(AnsibleFailJson) as exc:
+ args = self.set_args_create_na_sg_grid_gateway_port()
+ args["binding_mode"] = "ha-groups"
+ args["ha_groups"] = ["fffac524-9a16-4be0-9d6e-ec9b22218e75"]
+ set_module_args(args)
+ grid_gateway_module()
+ print("Info: test_create_na_sg_grid_gateway_port_with_bad_ha_group_binding_fail: %s" % repr(exc.value.args[0]))
+
+ # test create with node interfaces
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_create_na_sg_grid_gateway_port_with_node_interface_binding_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_gateway_port()
+ args["binding_mode"] = "node-interfaces"
+ args["node_interfaces"] = [
+ {"node": "SITE1-ADM1", "interface": "eth2"},
+ {"node": "SITE2-ADM1", "interface": "eth2"},
+ ]
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["version_116"], # get
+ SRR["node_health"], # get
+ SRR["empty_good"], # get
+ SRR["gateway_record_node_interface_binding"], # post
+ SRR["server_config"], # post
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_gateway_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_create_na_sg_grid_gateway_port_with_node_interface_binding_pass: %s" % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ # test change from global to ha groups
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_update_na_sg_grid_gateway_binding_to_ha_groups_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_gateway_port()
+ args["binding_mode"] = "ha-groups"
+ args["ha_groups"] = "site1_primary"
+ args["server_certificate"] = "-----BEGIN CERTIFICATE-----\nABCDEFGABCD\n-----END CERTIFICATE-----\n"
+ args["private_key"] = "-----BEGIN PRIVATE KEY-----\nABCDEFGABCD\n-----END PRIVATE KEY-----\n"
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["version_116"], # get
+ SRR["ha_groups"], # get
+ SRR["present_gateways_with_binding"], # get
+ SRR["server_config"], # get
+ SRR["gateway_record_ha_group_binding"], # put
+ SRR["server_config_cert_update"], # put
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_gateway_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_update_na_sg_grid_gateway_binding_to_ha_groups_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ # test rename by supplying gateway_id
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_update_na_sg_grid_gateway_rename_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_gateway_port()
+ args["gateway_id"] = "e777d415-057f-4d37-9b0c-6d132d872ea0"
+ args["binding_mode"] = "ha-groups"
+ args["ha_groups"] = "site1_primary"
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["version_116"], # get
+ SRR["ha_groups"], # get
+ SRR["gateway_record_ha_group_binding"], # get
+ SRR["server_config"], # get
+ SRR["gateway_record_rename"], # put
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_gateway_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_update_na_sg_grid_gateway_rename_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_group.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_group.py
new file mode 100644
index 000000000..fd9fdf15c
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_group.py
@@ -0,0 +1,317 @@
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID Grid Group Ansible module: na_sg_grid_group"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+try:
+ from requests import Response
+except ImportError:
+ if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available')
+ else:
+ raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_grid_group import (
+ SgGridGroup as grid_group_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "empty_good": ({"data": []}, None),
+ "not_found": (
+ {"status": "error", "code": 404, "data": {}},
+ {"key": "error.404"},
+ ),
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "delete_good": ({"code": 204}, None),
+ "grid_groups": (
+ {
+ "data": [
+ {
+ "displayName": "TestGridGroup",
+ "uniqueName": "group/testgridgroup",
+ "policies": {
+ "management": {
+ "tenantAccounts": True,
+ "metricsQuery": True,
+ "maintenance": True,
+ },
+ },
+ "id": "00000000-0000-0000-0000-000000000000",
+ "federated": False,
+ "groupURN": "urn:sgws:identity::12345678901234567890:group/testgridgroup",
+ }
+ ]
+ },
+ None,
+ ),
+ "grid_group_record": (
+ {
+ "data": {
+ "displayName": "TestGridGroup",
+ "uniqueName": "group/testgridgroup",
+ "policies": {
+ "management": {
+ "tenantAccounts": True,
+ "metricsQuery": True,
+ "maintenance": True,
+ },
+ },
+ "id": "00000000-0000-0000-0000-000000000000",
+ "federated": False,
+ "groupURN": "urn:sgws:identity::12345678901234567890:group/testgridgroup",
+ }
+ },
+ None,
+ ),
+ "grid_group_record_update": (
+ {
+ "data": {
+ "displayName": "TestGridGroup",
+ "uniqueName": "group/testgridgroup",
+ "policies": {
+ "management": {
+ "tenantAccounts": True,
+ "metricsQuery": False,
+ "maintenance": True,
+ "ilm": True,
+ },
+ },
+ "id": "00000000-0000-0000-0000-000000000000",
+ "federated": False,
+ "groupURN": "urn:sgws:identity::12345678901234567890:group/testgridgroup",
+ }
+ },
+ None,
+ ),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """ a group of related Unit Tests """
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(
+ basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json
+ )
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ "display_name": "TestGroup",
+ "management_policy": {
+ "maintenance": True,
+ "ilm": True,
+ "root_access": False,
+ },
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "state": "present",
+ "display_name": "TestGroup",
+ "unique_name": "group/testgroup",
+ "management_policy": {
+ "maintenance": True,
+ "ilm": True,
+ "root_access": False,
+ },
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_create_na_sg_grid_group(self):
+ return dict(
+ {
+ "state": "present",
+ "display_name": "TestGridGroup",
+ "unique_name": "group/testgridgroup",
+ "management_policy": {
+ "tenant_accounts": True,
+ "metrics_query": True,
+ "maintenance": True,
+ },
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_delete_na_sg_grid_group(self):
+ return dict(
+ {
+ "state": "absent",
+ "unique_name": "group/testgridgroup",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def test_module_fail_when_required_args_missing(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ grid_group_module()
+ print(
+ "Info: test_module_fail_when_required_args_missing: %s"
+ % exc.value.args[0]["msg"]
+ )
+
+ def test_module_fail_when_required_args_present(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ grid_group_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print(
+ "Info: test_module_fail_when_required_args_present: %s"
+ % exc.value.args[0]["msg"]
+ )
+ assert exc.value.args[0]["changed"]
+
+ def test_module_fail_with_bad_unique_name(self):
+ """ error returned if unique_name doesn't start with group or federated_group """
+ with pytest.raises(AnsibleFailJson) as exc:
+ args = self.set_default_args_pass_check()
+ args["unique_name"] = "noprefixgroup"
+ set_module_args(args)
+ grid_group_module()
+ print(
+ "Info: test_module_fail_with_bad_unique_name: %s"
+ % exc.value.args[0]["msg"]
+ )
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_create_na_sg_grid_group_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_grid_group())
+ my_obj = grid_group_module()
+ mock_request.side_effect = [
+ SRR["not_found"], # get
+ SRR["grid_group_record"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_create_na_sg_grid_group_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_idempotent_create_na_sg_grid_group_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_grid_group())
+ my_obj = grid_group_module()
+ mock_request.side_effect = [
+ SRR["grid_group_record"], # get
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_idempotent_create_na_sg_grid_group_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert not exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_update_na_sg_grid_group_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_group()
+ args["management_policy"]["tenant_accounts"] = True
+ args["management_policy"]["metrics_query"] = False
+ args["management_policy"]["ilm"] = False
+
+ set_module_args(args)
+ my_obj = grid_group_module()
+ mock_request.side_effect = [
+ SRR["grid_group_record"], # get
+ SRR["grid_group_record_update"], # put
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_update_na_sg_grid_group_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_delete_na_sg_grid_group_pass(self, mock_request):
+ set_module_args(self.set_args_delete_na_sg_grid_group())
+ my_obj = grid_group_module()
+ mock_request.side_effect = [
+ SRR["grid_group_record"], # get
+ SRR["delete_good"], # delete
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_delete_na_sg_grid_group_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_ha_group.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_ha_group.py
new file mode 100644
index 000000000..fbc8fd0ce
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_ha_group.py
@@ -0,0 +1,408 @@
+# (c) 2022, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID Grid HA Group Ansible module: na_sg_grid_ha_group"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+
+try:
+ from requests import Response
+except ImportError:
+ if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip("Skipping Unit Tests on 2.6 as requests is not available")
+ else:
+ raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_grid_ha_group import (
+ SgGridHaGroup as grid_ha_group_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "empty_good": ({"data": []}, None),
+ "not_found": (
+ {"status": "error", "code": 404, "data": {}},
+ {"key": "error.404"},
+ ),
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "delete_good": (None, None),
+ "update_good": (None, None),
+ "version_114": ({"data": {"productVersion": "11.4.0-20200721.1338.d3969b3"}}, None),
+ "version_116": ({"data": {"productVersion": "11.6.0-20211120.0301.850531e"}}, None),
+ "ha_group_record": (
+ {
+ "data": {
+ "id": "fbe724da-c941-439b-bb61-a536f6211ca9",
+ "name": "ansible-ha-group",
+ "description": None,
+ "virtualIps": ["192.168.50.5"],
+ "interfaces": [
+ {"nodeId": "0b1866ed-d6e7-41b4-815f-bf867348b76b", "interface": "ens256"},
+ {"nodeId": "7bb5bf05-a04c-4344-8abd-08c5c4048666", "interface": "ens256"},
+ ],
+ "gatewayCidr": "192.168.50.1/24",
+ }
+ },
+ None,
+ ),
+ "ha_group_record_twovip": (
+ {
+ "data": {
+ "id": "fbe724da-c941-439b-bb61-a536f6211ca9",
+ "name": "ansible-ha-group",
+ "description": "2 VIP HA Group",
+ "virtualIps": ["192.168.50.5", "192.168.50.6"],
+ "interfaces": [
+ {"nodeId": "0b1866ed-d6e7-41b4-815f-bf867348b76b", "interface": "ens256"},
+ {"nodeId": "7bb5bf05-a04c-4344-8abd-08c5c4048666", "interface": "ens256"},
+ ],
+ "gatewayCidr": "192.168.50.1/24",
+ }
+ },
+ None,
+ ),
+ "ha_group_record_rename": (
+ {
+ "data": {
+ "id": "fbe724da-c941-439b-bb61-a536f6211ca9",
+ "name": "ansible-ha-group-rename",
+ "description": None,
+ "virtualIps": ["192.168.50.5"],
+ "interfaces": [
+ {"nodeId": "0b1866ed-d6e7-41b4-815f-bf867348b76b", "interface": "ens256"},
+ {"nodeId": "7bb5bf05-a04c-4344-8abd-08c5c4048666", "interface": "ens256"},
+ ],
+ "gatewayCidr": "192.168.50.1/24",
+ }
+ },
+ None,
+ ),
+ "ha_groups": (
+ {
+ "data": [
+ {
+ "id": "c08e6dca-038d-4a05-9499-6fbd1e6a4c3e",
+ "name": "site1_primary",
+ "description": "test ha group",
+ "virtualIps": ["10.193.174.117"],
+ "interfaces": [
+ {
+ "nodeId": "0b1866ed-d6e7-41b4-815f-bf867348b76b",
+ "nodeName": "SITE1-ADM1",
+ "interface": "eth2",
+ "preferredMaster": True,
+ },
+ {
+ "nodeId": "970ad050-b68b-4aae-a94d-aef73f3095c4",
+ "nodeName": "SITE2-ADM1",
+ "interface": "eth2",
+ },
+ ],
+ "gatewayCidr": "192.168.14.1/24",
+ },
+ {
+ "id": "fbe724da-c941-439b-bb61-a536f6211ca9",
+ "name": "ansible-ha-group",
+ "description": None,
+ "virtualIps": ["192.168.50.5"],
+ "interfaces": [
+ {"nodeId": "0b1866ed-d6e7-41b4-815f-bf867348b76b", "interface": "ens256"},
+ {"nodeId": "7bb5bf05-a04c-4344-8abd-08c5c4048666", "interface": "ens256"},
+ ],
+ "gatewayCidr": "192.168.50.1/24",
+ },
+ ]
+ },
+ None,
+ ),
+ "node_health": (
+ {
+ "data": [
+ {
+ "id": "0b1866ed-d6e7-41b4-815f-bf867348b76b",
+ "isPrimaryAdmin": True,
+ "name": "SITE1-ADM1",
+ "siteId": "ae56d06d-bd83-46bd-adce-77146b1d94bd",
+ "siteName": "SITE1",
+ "severity": "normal",
+ "state": "connected",
+ "type": "adminNode",
+ },
+ {
+ "id": "7bb5bf05-a04c-4344-8abd-08c5c4048666",
+ "isPrimaryAdmin": None,
+ "name": "SITE1-G1",
+ "siteId": "ae56d06d-bd83-46bd-adce-77146b1d94bd",
+ "siteName": "SITE1",
+ "severity": "normal",
+ "state": "connected",
+ "type": "apiGatewayNode",
+ },
+ {
+ "id": "970ad050-b68b-4aae-a94d-aef73f3095c4",
+ "isPrimaryAdmin": False,
+ "name": "SITE2-ADM1",
+ "siteId": "7c24002e-5157-43e9-83e5-02db9b265b02",
+ "siteName": "SITE2",
+ "severity": "normal",
+ "state": "connected",
+ "type": "adminNode",
+ },
+ ]
+ },
+ None,
+ ),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """a group of related Unit Tests"""
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json)
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ "gateway_cidr": "192.168.50.1/24",
+ "virtual_ips": "192.168.50.5",
+ "interfaces": [
+ {"node": "SITE1-ADM1", "interface": "ens256"},
+ {"node": "SITE1-G1", "interface": "ens256"},
+ ],
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "state": "present",
+ "name": "ansible-test-ha-group",
+ "gateway_cidr": "192.168.50.1/24",
+ "virtual_ips": "192.168.50.5",
+ "interfaces": [
+ {"node": "SITE1-ADM1", "interface": "ens256"},
+ {"node": "SITE1-G1", "interface": "ens256"},
+ ],
+ "api_url": "https://gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_create_na_sg_grid_ha_group(self):
+ return dict(
+ {
+ "state": "present",
+ "name": "ansible-ha-group",
+ "gateway_cidr": "192.168.50.1/24",
+ "virtual_ips": "192.168.50.5",
+ "interfaces": [
+ {"node": "SITE1-ADM1", "interface": "ens256"},
+ {"node": "SITE1-G1", "interface": "ens256"},
+ ],
+ "api_url": "https://gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_delete_na_sg_grid_ha_group(self):
+ return dict(
+ {
+ "state": "absent",
+ "name": "ansible-ha-group",
+ "gateway_cidr": "192.168.50.1/24",
+ "virtual_ips": "192.168.50.5",
+ "interfaces": [
+ {"node": "SITE1-ADM1", "interface": "ens256"},
+ {"node": "SITE1-G1", "interface": "ens256"},
+ ],
+ "api_url": "https://gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_module_fail_when_required_args_missing(self, mock_request):
+ """required arguments are reported as errors"""
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ grid_ha_group_module()
+ print("Info: test_module_fail_when_required_args_missing: %s" % exc.value.args[0]["msg"])
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_module_pass_when_required_args_present(self, mock_request):
+ """required arguments are reported as errors"""
+ mock_request.side_effect = [
+ SRR["node_health"], # get
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ grid_ha_group_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print("Info: test_module_pass_when_required_args_present: %s" % exc.value.args[0]["msg"])
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_create_na_sg_grid_ha_group_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_grid_ha_group())
+ mock_request.side_effect = [
+ SRR["node_health"], # get
+ SRR["empty_good"], # get
+ SRR["ha_group_record"], # post
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_ha_group_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_create_na_sg_grid_ha_group_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_idempotent_create_na_sg_grid_ha_group_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_ha_group()
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["node_health"], # get
+ SRR["ha_groups"], # get
+ SRR["ha_group_record"], # get
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_ha_group_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_idempotent_create_na_sg_grid_ha_group_pass: %s" % repr(exc.value.args[0]))
+ assert not exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_update_na_sg_grid_ha_group_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_ha_group()
+ args["description"] = "2 VIP HA Group"
+ args["virtual_ips"] = ["192.168.50.5", "192.168.50.6"]
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["node_health"], # get
+ SRR["ha_groups"], # get
+ SRR["ha_group_record"], # get
+ SRR["ha_group_record_twovip"], # post
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_ha_group_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_update_na_sg_grid_ha_group_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_rename_na_sg_grid_ha_group_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_ha_group()
+ args["ha_group_id"] = "fbe724da-c941-439b-bb61-a536f6211ca9"
+ args["name"] = "ansible-ha-group-rename"
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["node_health"], # get
+ SRR["ha_group_record"], # get
+ SRR["ha_group_record_rename"], # post
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_ha_group_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_rename_na_sg_grid_ha_group_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_delete_na_sg_grid_ha_group_pass(self, mock_request):
+ args = self.set_args_delete_na_sg_grid_ha_group()
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["ha_groups"], # get
+ SRR["ha_group_record"], # get
+ SRR["delete_good"], # delete
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_ha_group_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_delete_na_sg_grid_ha_group_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_create_na_sg_grid_ha_group_bad_node_fail(self, mock_request):
+ args = self.set_args_create_na_sg_grid_ha_group()
+ args["interfaces"] = [{"node": "FakeNode", "interface": "eth0"}]
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["node_health"], # get
+ ]
+ with pytest.raises(AnsibleFailJson) as exc:
+ grid_ha_group_module()
+ print("Info: test_create_na_sg_grid_ha_group_bad_node_fail: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["failed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_update_na_sg_grid_ha_group_bad_ha_group_id_fail(self, mock_request):
+ args = self.set_args_create_na_sg_grid_ha_group()
+ args["ha_group_id"] = "ffffffff-ffff-aaaa-aaaa-000000000000"
+ args["virtual_ips"] = "192.168.50.10"
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["node_health"], # get
+ SRR["not_found"], # get
+ ]
+ with pytest.raises(AnsibleFailJson) as exc:
+ my_obj = grid_ha_group_module()
+ my_obj.apply()
+ print("Info: test_create_na_sg_grid_ha_group_bad_node_fail: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["failed"]
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_identity_federation.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_identity_federation.py
new file mode 100644
index 000000000..058fc609e
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_identity_federation.py
@@ -0,0 +1,354 @@
+# (c) 2021, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID Grid Identity Federation Ansible module: na_sg_grid_identity_federation"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+try:
+ from requests import Response
+except ImportError:
+ if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available')
+ else:
+ raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_grid_identity_federation import (
+ SgGridIdentityFederation as grid_identity_federation_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "check_mode_good": (None, None),
+ "identity_federation_unset": (
+ {
+ "data": {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "disable": True,
+ "type": "",
+ "ldapServiceType": "",
+ "hostname": "",
+ "port": 0,
+ "username": "",
+ "password": None,
+ "baseGroupDn": "",
+ "baseUserDn": "",
+ "disableTLS": False,
+ "enableLDAPS": False,
+ "caCert": "",
+ }
+ },
+ None,
+ ),
+ "identity_federation": (
+ {
+ "data": {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "disable": False,
+ "type": "ldap",
+ "ldapServiceType": "Active Directory",
+ "hostname": "ad.example.com",
+ "port": 389,
+ "username": "binduser",
+ "password": "********",
+ "baseGroupDn": "DC=example,DC=com",
+ "baseUserDn": "DC=example,DC=com",
+ "disableTLS": True,
+ "enableLDAPS": False,
+ "caCert": "",
+ }
+ },
+ None,
+ ),
+ "identity_federation_tls": (
+ {
+ "data": {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "disable": False,
+ "type": "ldap",
+ "ldapServiceType": "Active Directory",
+ "hostname": "ad.example.com",
+ "port": 636,
+ "username": "binduser",
+ "password": "********",
+ "baseGroupDn": "DC=example,DC=com",
+ "baseUserDn": "DC=example,DC=com",
+ "disableTLS": False,
+ "enableLDAPS": True,
+ "caCert": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIF+DCCBOCgAwIBAgITRwAAAAIg5KzMrJo+kQAAAAAAAjANBgkqhkiG9w0BAQUF\n"
+ "ADBlMRIwEAYKCZImiZPyLGQBGRYCYXUxFjAUBgoJkiaJk/IsZAEZFgZuZXRhcHAx\n"
+ "FjAUBgoJkiaJk/IsZAEZFgZhdXNuZ3MxHzAdBgNVBAMTFmF1c25ncy1NRUxOR1NE\n"
+ "QzAxLUNBLTEwHhcNMjEwMjExMDkzMTIwWhcNMjMwMjExMDk0MTIwWjAAMIIBIjAN\n"
+ "BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt2xPi4FS4Uc37KrDLEXXUoc4lhhT\n"
+ "uQmMnLc0PYZCIpzYOaosFIeGqco3woiC7wSZJ2whKE4RDcxxgE+azuGiSWVjIxIL\n"
+ "AimmcDhFid/T3KRN5jmkjBzUKuPBYzZBFih8iU9056rqgN7eMKQYjRwPeV0+AeiB\n"
+ "irw46OgkwVQu3shEUtXxZPP2Mb6Md23+4vSmcElUcW28Opt2q/M5fs7DNomG3eaG\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ }
+ },
+ None,
+ ),
+ "identity_federation_disable": (
+ {
+ "data": {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "disable": True,
+ "type": "ldap",
+ "ldapServiceType": "Active Directory",
+ "hostname": "ad.example.com",
+ "port": 389,
+ "username": "binduser",
+ "password": "********",
+ "baseGroupDn": "DC=example,DC=com",
+ "baseUserDn": "DC=example,DC=com",
+ "disableTLS": True,
+ "enableLDAPS": False,
+ "caCert": "",
+ }
+ },
+ None,
+ ),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """ a group of related Unit Tests """
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json)
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ "ldap_service_type": "Active Directory",
+ "hostname": "ad.example.com",
+ "port": 389,
+ "username": "binduser",
+ "password": "bindpass",
+ "base_group_dn": "DC=example,DC=com",
+ "base_user_dn": "DC=example,DC=com",
+ "tls": "Disabled",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "ldap_service_type": "Active Directory",
+ "hostname": "ad.example.com",
+ "port": 389,
+ "username": "binduser",
+ "password": "bindpass",
+ "base_group_dn": "DC=example,DC=com",
+ "base_user_dn": "DC=example,DC=com",
+ "tls": "Disabled",
+ "state": "present",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_set_na_sg_grid_identity_federation(self):
+ return dict(
+ {
+ "ldap_service_type": "Active Directory",
+ "hostname": "ad.example.com",
+ "port": 389,
+ "username": "binduser",
+ "password": "bindpass",
+ "base_group_dn": "DC=example,DC=com",
+ "base_user_dn": "DC=example,DC=com",
+ "tls": "Disabled",
+ "state": "present",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_set_na_sg_grid_identity_federation_tls(self):
+ return dict(
+ {
+ "ldap_service_type": "Active Directory",
+ "hostname": "ad.example.com",
+ "port": 636,
+ "username": "binduser",
+ "password": "bindpass",
+ "base_group_dn": "DC=example,DC=com",
+ "base_user_dn": "DC=example,DC=com",
+ "tls": "LDAPS",
+ "ca_cert": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIF+DCCBOCgAwIBAgITRwAAAAIg5KzMrJo+kQAAAAAAAjANBgkqhkiG9w0BAQUF\n"
+ "ADBlMRIwEAYKCZImiZPyLGQBGRYCYXUxFjAUBgoJkiaJk/IsZAEZFgZuZXRhcHAx\n"
+ "FjAUBgoJkiaJk/IsZAEZFgZhdXNuZ3MxHzAdBgNVBAMTFmF1c25ncy1NRUxOR1NE\n"
+ "QzAxLUNBLTEwHhcNMjEwMjExMDkzMTIwWhcNMjMwMjExMDk0MTIwWjAAMIIBIjAN\n"
+ "BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt2xPi4FS4Uc37KrDLEXXUoc4lhhT\n"
+ "uQmMnLc0PYZCIpzYOaosFIeGqco3woiC7wSZJ2whKE4RDcxxgE+azuGiSWVjIxIL\n"
+ "AimmcDhFid/T3KRN5jmkjBzUKuPBYzZBFih8iU9056rqgN7eMKQYjRwPeV0+AeiB\n"
+ "irw46OgkwVQu3shEUtXxZPP2Mb6Md23+4vSmcElUcW28Opt2q/M5fs7DNomG3eaG\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "state": "present",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_remove_na_sg_grid_identity_federation(self):
+ return dict(
+ {
+ "ldap_service_type": "Active Directory",
+ "hostname": "ad.example.com",
+ "state": "absent",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def test_module_fail_when_required_args_missing(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ grid_identity_federation_module()
+ print("Info: test_module_fail_when_required_args_missing: %s" % exc.value.args[0]["msg"])
+
+ def test_module_fail_when_required_args_present(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ grid_identity_federation_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print("Info: test_module_fail_when_required_args_present: %s" % exc.value.args[0]["msg"])
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_set_na_sg_grid_identity_federation_pass(self, mock_request):
+ set_module_args(self.set_args_set_na_sg_grid_identity_federation())
+ my_obj = grid_identity_federation_module()
+ mock_request.side_effect = [
+ SRR["identity_federation_unset"], # get
+ SRR["identity_federation"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_set_na_sg_grid_identity_federation_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_idempotent_set_na_sg_grid_identity_federation_pass(self, mock_request):
+ args = self.set_args_set_na_sg_grid_identity_federation()
+ # remove password
+ del args["password"]
+ set_module_args(args)
+ my_obj = grid_identity_federation_module()
+ mock_request.side_effect = [
+ SRR["identity_federation"], # get
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_idempotent_set_na_sg_grid_identity_federation_pass: %s" % repr(exc.value.args[0]))
+ assert not exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_set_na_sg_grid_identity_federation_tls_pass(self, mock_request):
+ set_module_args(self.set_args_set_na_sg_grid_identity_federation_tls())
+ my_obj = grid_identity_federation_module()
+ mock_request.side_effect = [
+ SRR["identity_federation_unset"], # get
+ SRR["identity_federation_tls"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_set_na_sg_grid_identity_federation_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_remove_na_sg_grid_identity_federation_pass(self, mock_request):
+ set_module_args(self.set_args_remove_na_sg_grid_identity_federation())
+ my_obj = grid_identity_federation_module()
+ mock_request.side_effect = [
+ SRR["identity_federation"], # get
+ SRR["identity_federation_disable"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_remove_na_sg_grid_identity_federation_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ # test check mode
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_check_mode_na_sg_grid_identity_federation_pass(self, mock_request):
+ set_module_args(self.set_args_set_na_sg_grid_identity_federation())
+ my_obj = grid_identity_federation_module()
+ my_obj.module.check_mode = True
+ mock_request.side_effect = [
+ SRR["identity_federation_unset"], # get
+ SRR["check_mode_good"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_check_mode_na_sg_grid_identity_federation_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+ assert exc.value.args[0]["msg"] == "Connection test successful"
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_info.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_info.py
new file mode 100644
index 000000000..2de26109b
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_info.py
@@ -0,0 +1,362 @@
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+''' Unit Tests NetApp StorageGRID Grid Ansible module: na_sg_grid_info '''
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+import json
+import pytest
+
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import patch
+
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_grid_info \
+ import NetAppSgGatherInfo as sg_grid_info_module
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ 'empty_good': ({'data': []}, None),
+ 'end_of_sequence': (None, 'Unexpected call to send_request'),
+ 'generic_error': (None, 'Expected error'),
+ 'grid_accounts': (
+ {
+ 'data': [
+ {
+ 'name': 'TestTenantAccount1',
+ 'capabilities': ['management', 's3'],
+ 'policy': {
+ 'useAccountIdentitySource': True,
+ 'allowPlatformServices': False,
+ 'quotaObjectBytes': None,
+ },
+ 'id': '12345678901234567891',
+ },
+ {
+ 'name': 'TestTenantAccount2',
+ 'capabilities': ['management', 's3'],
+ 'policy': {
+ 'useAccountIdentitySource': True,
+ 'allowPlatformServices': False,
+ 'quotaObjectBytes': None,
+ },
+ 'id': '12345678901234567892',
+ },
+ {
+ 'name': 'TestTenantAccount3',
+ 'capabilities': ['management', 's3'],
+ 'policy': {
+ 'useAccountIdentitySource': True,
+ 'allowPlatformServices': False,
+ 'quotaObjectBytes': None,
+ },
+ 'id': '12345678901234567893',
+ },
+ ]
+ },
+ None,
+ ),
+ 'grid_alarms': ({'data': []}, None),
+ 'grid_audit': ({'data': {}}, None),
+ 'grid_compliance_global': ({'data': {}}, None),
+ 'grid_config': ({'data': {}}, None),
+ 'grid_config_management': ({'data': {}}, None),
+ 'grid_config_product_version': ({'data': {}}, None),
+ 'grid_deactivated_features': ({'data': {}}, None),
+ 'grid_dns_servers': ({'data': []}, None),
+ 'grid_domain_names': ({'data': []}, None),
+ 'grid_ec_profiles': ({'data': []}, None),
+ 'grid_expansion': ({'data': {}}, None),
+ 'grid_expansion_nodes': ({'data': []}, None),
+ 'grid_expansion_sites': ({'data': []}, None),
+ 'grid_grid_networks': ({'data': []}, None),
+ 'grid_groups': ({'data': []}, None),
+ 'grid_health': ({'data': {}}, None),
+ 'grid_health_topology': ({'data': {}}, None),
+ 'grid_identity_source': ({'data': {}}, None),
+ 'grid_ilm_criteria': ({'data': []}, None),
+ 'grid_ilm_policies': ({'data': []}, None),
+ 'grid_ilm_rules': ({'data': []}, None),
+ 'grid_license': ({'data': []}, None),
+ 'grid_management_certificate': ({'data': {}}, None),
+ 'grid_ntp_servers': ({'data': []}, None),
+ 'grid_recovery': ({'data': {}}, None),
+ 'grid_recovery_available_nodes': ({'data': []}, None),
+ 'grid_regions': ({'data': []}, None),
+ 'grid_schemes': ({'data': []}, None),
+ 'grid_snmp': ({'data': {}}, None),
+ 'grid_storage_api_certificate': ({'data': {}}, None),
+ 'grid_untrusted_client_network': ({'data': {}}, None),
+ 'grid_users': (
+ {
+ 'data': [
+ {
+ 'accountId': '0',
+ 'disable': False,
+ 'federated': False,
+ 'fullName': 'Root',
+ 'id': '00000000-0000-0000-0000-000000000000',
+ 'memberOf': None,
+ 'uniqueName': 'root',
+ 'userURN': 'urn:sgws:identity::0:root'
+ },
+ ]
+ },
+ None
+ ),
+ 'grid_users_root': (
+ {
+ 'data': {
+ 'accountId': '0',
+ 'disable': False,
+ 'federated': False,
+ 'fullName': 'Root',
+ 'id': '00000000-0000-0000-0000-000000000000',
+ 'memberOf': None,
+ 'uniqueName': 'root',
+ 'userURN': 'urn:sgws:identity::0:root'
+ },
+ },
+ None
+ ),
+ 'versions': ({'data': [2, 3]}, None),
+}
+
+
+def set_module_args(args):
+ ''' Prepare arguments so that they will be picked up during module creation '''
+ args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ ''' Exception class to be raised by module.exit_json and caught by the test case '''
+ pass
+
+
+class AnsibleFailJson(Exception):
+ ''' Exception class to be raised by module.fail_json and caught by the test case '''
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ ''' Function to patch over exit_json; package return data into an exception '''
+ if 'changed' not in kwargs:
+ kwargs['changed'] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ ''' Function to patch over fail_json; package return data into an exception '''
+ kwargs['failed'] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ ''' A group of related Unit Tests '''
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(basic.AnsibleModule,
+ exit_json=exit_json,
+ fail_json=fail_json)
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ 'api_url': 'sgmi.example.com',
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ 'api_url': 'sgmi.example.com',
+ 'auth_token': '01234567-5678-9abc-78de-9fgabc123def',
+ }
+ )
+
+ def set_default_optional_args_pass_check(self):
+ return dict(
+ {
+ 'api_url': 'sgmi.example.com',
+ 'auth_token': '01234567-5678-9abc-78de-9fgabc123def',
+ 'validate_certs': False,
+ 'gather_subset': ['all'],
+ 'parameters': {'limit': 5},
+ }
+ )
+
+ def set_args_run_sg_gather_facts_for_all_info(self):
+ return dict({
+ 'api_url': 'sgmi.example.com',
+ 'auth_token': '01234567-5678-9abc-78de-9fgabc123def',
+ 'validate_certs': False,
+ })
+
+ def set_args_run_sg_gather_facts_for_grid_accounts_info(self):
+ return dict({
+ 'api_url': 'sgmi.example.com',
+ 'auth_token': '01234567-5678-9abc-78de-9fgabc123def',
+ 'validate_certs': False,
+ 'gather_subset': ['grid_accounts_info'],
+ })
+
+ def set_args_run_sg_gather_facts_for_grid_accounts_and_grid_users_root_info(self):
+ return dict({
+ 'api_url': 'sgmi.example.com',
+ 'auth_token': '01234567-5678-9abc-78de-9fgabc123def',
+ 'validate_certs': False,
+ 'gather_subset': ['grid_accounts_info', 'grid/users/root'],
+ })
+
+ def test_module_fail_when_required_args_missing(self):
+ ''' required arguments are reported as errors '''
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ sg_grid_info_module()
+ print(
+ 'Info: test_module_fail_when_required_args_missing: %s'
+ % exc.value.args[0]['msg']
+ )
+
+ def test_module_pass_when_required_args_present(self):
+ ''' required arguments are reported as errors '''
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ sg_grid_info_module()
+ exit_json(changed=True, msg='Induced arguments check')
+ print(
+ 'Info: test_module_pass_when_required_args_present: %s'
+ % exc.value.args[0]['msg']
+ )
+ assert exc.value.args[0]['changed']
+
+ def test_module_pass_when_optional_args_present(self):
+ ''' Optional arguments are reported as pass '''
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_optional_args_pass_check())
+ sg_grid_info_module()
+ exit_json(changed=True, msg='Induced arguments check')
+ print(
+ 'Info: test_module_pass_when_optional_args_present: %s'
+ % exc.value.args[0]['msg']
+ )
+ assert exc.value.args[0]['changed']
+
+ @patch('ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request')
+ def test_run_sg_gather_facts_for_all_info_pass(self, mock_request):
+ set_module_args(self.set_args_run_sg_gather_facts_for_all_info())
+ my_obj = sg_grid_info_module()
+ gather_subset = [
+ 'grid/accounts',
+ 'grid/alarms',
+ 'grid/audit',
+ 'grid/compliance-global',
+ 'grid/config',
+ 'grid/config/management',
+ 'grid/config/product-version',
+ 'grid/deactivated-features',
+ 'grid/dns-servers',
+ 'grid/domain-names',
+ 'grid/ec-profiles',
+ 'grid/expansion',
+ 'grid/expansion/nodes',
+ 'grid/expansion/sites',
+ 'grid/grid-networks',
+ 'grid/groups',
+ 'grid/health',
+ 'grid/health/topology',
+ 'grid/identity-source',
+ 'grid/ilm-criteria',
+ 'grid/ilm-policies',
+ 'grid/ilm-rules',
+ 'grid/license',
+ 'grid/management-certificate',
+ 'grid/ntp-servers',
+ 'grid/recovery/available-nodes',
+ 'grid/recovery',
+ 'grid/regions',
+ 'grid/schemes',
+ 'grid/snmp',
+ 'grid/storage-api-certificate',
+ 'grid/untrusted-client-network',
+ 'grid/users',
+ 'grid/users/root',
+ 'versions',
+ ]
+ mock_request.side_effect = [
+ SRR['grid_accounts'],
+ SRR['grid_alarms'],
+ SRR['grid_audit'],
+ SRR['grid_compliance_global'],
+ SRR['grid_config'],
+ SRR['grid_config_management'],
+ SRR['grid_config_product_version'],
+ SRR['grid_deactivated_features'],
+ SRR['grid_dns_servers'],
+ SRR['grid_domain_names'],
+ SRR['grid_ec_profiles'],
+ SRR['grid_expansion'],
+ SRR['grid_expansion_nodes'],
+ SRR['grid_expansion_sites'],
+ SRR['grid_grid_networks'],
+ SRR['grid_groups'],
+ SRR['grid_health'],
+ SRR['grid_health_topology'],
+ SRR['grid_identity_source'],
+ SRR['grid_ilm_criteria'],
+ SRR['grid_ilm_policies'],
+ SRR['grid_ilm_rules'],
+ SRR['grid_license'],
+ SRR['grid_management_certificate'],
+ SRR['grid_ntp_servers'],
+ SRR['grid_recovery_available_nodes'],
+ SRR['grid_recovery'],
+ SRR['grid_regions'],
+ SRR['grid_schemes'],
+ SRR['grid_snmp'],
+ SRR['grid_storage_api_certificate'],
+ SRR['grid_untrusted_client_network'],
+ SRR['grid_users'],
+ SRR['grid_users_root'],
+ SRR['versions'],
+ SRR['end_of_sequence'],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print('Info: test_run_sg_gather_facts_for_all_info_pass: %s' % repr(exc.value.args))
+ assert set(exc.value.args[0]['sg_info']) == set(gather_subset)
+
+ @patch('ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request')
+ def test_run_sg_gather_facts_for_grid_accounts_info_pass(self, mock_request):
+ set_module_args(self.set_args_run_sg_gather_facts_for_grid_accounts_info())
+ my_obj = sg_grid_info_module()
+ gather_subset = ['grid/accounts']
+ mock_request.side_effect = [
+ SRR['grid_accounts'],
+ SRR['end_of_sequence'],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print('Info: test_run_sg_gather_facts_for_grid_accounts_info_pass: %s' % repr(exc.value.args))
+ assert set(exc.value.args[0]['sg_info']) == set(gather_subset)
+
+ @patch('ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request')
+ def test_run_sg_gather_facts_for_grid_accounts_and_grid_users_root_info_pass(self, mock_request):
+ set_module_args(self.set_args_run_sg_gather_facts_for_grid_accounts_and_grid_users_root_info())
+ my_obj = sg_grid_info_module()
+ gather_subset = ['grid/accounts', 'grid/users/root']
+ mock_request.side_effect = [
+ SRR['grid_accounts'],
+ SRR['grid_users_root'],
+ SRR['end_of_sequence'],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print('Info: test_run_sg_gather_facts_for_grid_accounts_and_grid_users_root_info_pass: %s' % repr(exc.value.args))
+ assert set(exc.value.args[0]['sg_info']) == set(gather_subset)
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_ntp.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_ntp.py
new file mode 100644
index 000000000..eed83d49b
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_ntp.py
@@ -0,0 +1,257 @@
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID NTP Ansible module: na_sg_grid_ntp"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+try:
+ from requests import Response
+except ImportError:
+ if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available')
+ else:
+ raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_grid_ntp import (
+ SgGridNtp as grid_ntp_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "empty_good": ({"data": []}, None),
+ "not_found": (
+ {"status": "error", "code": 404, "data": {}},
+ {"key": "error.404"},
+ ),
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "delete_good": ({"code": 204}, None),
+ "ntp_servers": ({"data": ["123.12.3.123", "123.1.23.123"]}, None,),
+ "update_ntp_servers": ({"data": ["123.12.3.123", "12.3.12.3"]}, None,),
+ "add_ntp_servers": (
+ {"data": ["123.12.3.123", "123.1.23.123", "12.3.12.3"]},
+ None,
+ ),
+ "remove_ntp_servers": ({"data": ["123.12.3.123"]}, None,),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """ a group of related Unit Tests """
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(
+ basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json
+ )
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ "ntp_servers": "123.12.3.123,123.1.23.123",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "state": "present",
+ "passphrase": "secretstring",
+ "ntp_servers": "123.12.3.123,123.1.23.123",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "state": "present",
+ "passphrase": "secretstring",
+ "ntp_servers": "123.12.3.123,123.1.23.123",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_set_na_sg_grid_ntp_servers(self):
+ return dict(
+ {
+ "state": "present",
+ "passphrase": "secretstring",
+ "ntp_servers": "123.12.3.123,12.3.12.3",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_add_na_sg_grid_ntp_servers(self):
+ return dict(
+ {
+ "state": "present",
+ "passphrase": "secretstring",
+ "ntp_servers": "123.12.3.123,123.1.23.123,12.3.12.3",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_remove_na_sg_grid_ntp_server(self):
+ return dict(
+ {
+ "state": "present",
+ "passphrase": "secretstring",
+ "ntp_servers": "123.12.3.123",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def test_module_fail_when_required_args_missing(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ grid_ntp_module()
+ print(
+ "Info: test_module_fail_when_required_args_missing: %s"
+ % exc.value.args[0]["msg"]
+ )
+
+ def test_module_fail_when_required_args_present(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ grid_ntp_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print(
+ "Info: test_module_fail_when_required_args_present: %s"
+ % exc.value.args[0]["msg"]
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_set_na_sg_grid_ntp_servers_pass(self, mock_request):
+ set_module_args(self.set_args_set_na_sg_grid_ntp_servers())
+ my_obj = grid_ntp_module()
+ mock_request.side_effect = [
+ SRR["ntp_servers"], # get
+ SRR["update_ntp_servers"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_set_na_sg_grid_ntp_servers_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_idempotent_set_na_sg_grid_ntp_servers_pass(self, mock_request):
+ set_module_args(self.set_args_set_na_sg_grid_ntp_servers())
+ my_obj = grid_ntp_module()
+ mock_request.side_effect = [
+ SRR["update_ntp_servers"], # get
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_idempotent_set_na_sg_grid_ntp_servers_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert not exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_add_na_sg_grid_ntp_servers_pass(self, mock_request):
+ set_module_args(self.set_args_add_na_sg_grid_ntp_servers())
+ my_obj = grid_ntp_module()
+ mock_request.side_effect = [
+ SRR["ntp_servers"], # get
+ SRR["add_ntp_servers"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_add_na_sg_grid_ntp_servers_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_remove_na_sg_grid_ntp_servers_pass(self, mock_request):
+ set_module_args(self.set_args_remove_na_sg_grid_ntp_server())
+ my_obj = grid_ntp_module()
+ mock_request.side_effect = [
+ SRR["ntp_servers"], # get
+ SRR["remove_ntp_servers"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_remove_na_sg_grid_ntp_servers_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_regions.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_regions.py
new file mode 100644
index 000000000..585ba3f45
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_regions.py
@@ -0,0 +1,206 @@
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID Regions Ansible module: na_sg_grid_regions"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+try:
+ from requests import Response
+except ImportError:
+ if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available')
+ else:
+ raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_grid_regions import (
+ SgGridRegions as grid_regions_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "empty_good": ({"data": []}, None),
+ "not_found": (
+ {"status": "error", "code": 404, "data": {}},
+ {"key": "error.404"},
+ ),
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "delete_good": ({"code": 204}, None),
+ "default_regions": ({"data": ["us-east-1"]}, None,),
+ "regions": ({"data": ["us-east-1", "us-west-1"]}, None,),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """ a group of related Unit Tests """
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(
+ basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json
+ )
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ "regions": "us-east-1,us-west-1",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "state": "present",
+ "regions": "us-east-1,us-west-1",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_set_na_sg_grid_regions(self):
+ return dict(
+ {
+ "state": "present",
+ "regions": "us-east-1,us-west-1",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_remove_na_sg_grid_regions(self):
+ return dict(
+ {
+ "state": "present",
+ "regions": "us-east-1",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def test_module_fail_when_required_args_missing(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ grid_regions_module()
+ print(
+ "Info: test_module_fail_when_required_args_missing: %s"
+ % exc.value.args[0]["msg"]
+ )
+
+ def test_module_fail_when_required_args_present(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ grid_regions_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print(
+ "Info: test_module_fail_when_required_args_present: %s"
+ % exc.value.args[0]["msg"]
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_set_na_sg_grid_regions_pass(self, mock_request):
+ set_module_args(self.set_args_set_na_sg_grid_regions())
+ my_obj = grid_regions_module()
+ mock_request.side_effect = [
+ SRR["default_regions"], # get
+ SRR["regions"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_set_na_sg_grid_regions_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_idempotent_set_na_sg_grid_regions_pass(self, mock_request):
+ set_module_args(self.set_args_set_na_sg_grid_regions())
+ my_obj = grid_regions_module()
+ mock_request.side_effect = [
+ SRR["regions"], # get
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_idempotent_set_na_sg_grid_regions_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert not exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_remove_na_sg_grid_regions_pass(self, mock_request):
+ set_module_args(self.set_args_remove_na_sg_grid_regions())
+ my_obj = grid_regions_module()
+ mock_request.side_effect = [
+ SRR["regions"], # get
+ SRR["default_regions"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_remove_na_sg_grid_regions_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_traffic_classes.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_traffic_classes.py
new file mode 100644
index 000000000..42fce0e3b
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_traffic_classes.py
@@ -0,0 +1,355 @@
+# (c) 2022, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID Grid HA Group Ansible module: na_sg_grid_traffic_classes"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+
+try:
+ from requests import Response
+except ImportError:
+ if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip("Skipping Unit Tests on 2.6 as requests is not available")
+ else:
+ raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_grid_traffic_classes import (
+ SgGridTrafficClasses as grid_traffic_classes_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "empty_good": ({"data": []}, None),
+ "not_found": (
+ {"status": "error", "code": 404, "data": {}},
+ {"key": "error.404"},
+ ),
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "delete_good": (None, None),
+ "update_good": (None, None),
+ "version_114": ({"data": {"productVersion": "11.4.0-20200721.1338.d3969b3"}}, None),
+ "version_116": ({"data": {"productVersion": "11.6.0-20211120.0301.850531e"}}, None),
+ "traffic_class_record": (
+ {
+ "data": {
+ "id": "6b2946e6-7fed-40d0-9262-8e922580aba7",
+ "name": "ansible-test-traffic-class",
+ "description": "Ansible Test",
+ "matchers": [
+ {"type": "cidr", "inverse": False, "members": ["192.168.50.0/24"]},
+ {"type": "bucket", "inverse": False, "members": ["ansible-test1", "ansible-test2"]},
+ ],
+ "limits": [],
+ }
+ },
+ None,
+ ),
+ "traffic_class_record_updated": (
+ {
+ "data": {
+ "id": "6b2946e6-7fed-40d0-9262-8e922580aba7",
+ "name": "ansible-test-traffic-class",
+ "description": "Ansible Test",
+ "matchers": [
+ {"type": "cidr", "inverse": False, "members": ["192.168.50.0/24"]},
+ {"type": "bucket", "inverse": False, "members": ["ansible-test1", "ansible-test2"]},
+ ],
+ "limits": [{"type": "aggregateBandwidthIn", "value": 888888}],
+ }
+ },
+ None,
+ ),
+ "traffic_class_record_rename": (
+ {
+ "data": {
+ "id": "6b2946e6-7fed-40d0-9262-8e922580aba7",
+ "name": "ansible-test-traffic-class-rename",
+ "description": "Ansible Test",
+ "matchers": [
+ {"type": "cidr", "inverse": False, "members": ["192.168.50.0/24"]},
+ {"type": "bucket", "inverse": False, "members": ["ansible-test1", "ansible-test2"]},
+ ],
+ "limits": [],
+ }
+ },
+ None,
+ ),
+ "traffic_classes": (
+ {
+ "data": [
+ {
+ "id": "6b2946e6-7fed-40d0-9262-8e922580aba7",
+ "name": "ansible-test-traffic-class",
+ "description": "Ansible Test",
+ },
+ {
+ "id": "531e6be1-e9b1-4010-bb79-03437c7c13d2",
+ "name": "policy-test1",
+ "description": "First test policy",
+ },
+ ]
+ },
+ None,
+ ),
+ "node_health": (
+ {
+ "data": [
+ {
+ "id": "0b1866ed-d6e7-41b4-815f-bf867348b76b",
+ "isPrimaryAdmin": True,
+ "name": "SITE1-ADM1",
+ "siteId": "ae56d06d-bd83-46bd-adce-77146b1d94bd",
+ "siteName": "SITE1",
+ "severity": "normal",
+ "state": "connected",
+ "type": "adminNode",
+ },
+ {
+ "id": "7bb5bf05-a04c-4344-8abd-08c5c4048666",
+ "isPrimaryAdmin": None,
+ "name": "SITE1-G1",
+ "siteId": "ae56d06d-bd83-46bd-adce-77146b1d94bd",
+ "siteName": "SITE1",
+ "severity": "normal",
+ "state": "connected",
+ "type": "apiGatewayNode",
+ },
+ {
+ "id": "970ad050-b68b-4aae-a94d-aef73f3095c4",
+ "isPrimaryAdmin": False,
+ "name": "SITE2-ADM1",
+ "siteId": "7c24002e-5157-43e9-83e5-02db9b265b02",
+ "siteName": "SITE2",
+ "severity": "normal",
+ "state": "connected",
+ "type": "adminNode",
+ },
+ ]
+ },
+ None,
+ ),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """a group of related Unit Tests"""
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json)
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ "matchers": [
+ {"type": "bucket", "members": ["ansible-test1", "ansible-test2"]},
+ {"type": "cidr", "members": ["192.168.50.0/24"]},
+ ],
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "state": "present",
+ "name": "ansible-test-traffic-class",
+ "matchers": [
+ {"type": "bucket", "members": ["ansible-test1", "ansible-test2"]},
+ {"type": "cidr", "members": ["192.168.50.0/24"]},
+ ],
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_create_na_sg_grid_traffic_class(self):
+ return dict(
+ {
+ "state": "present",
+ "name": "ansible-test-traffic-class",
+ "description": "Ansible Test",
+ "matchers": [
+ {"type": "bucket", "members": ["ansible-test1", "ansible-test2"]},
+ {"type": "cidr", "members": ["192.168.50.0/24"]},
+ ],
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_delete_na_sg_grid_traffic_class(self):
+ return dict(
+ {
+ "state": "absent",
+ "name": "ansible-test-traffic-class",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_module_fail_when_required_args_missing(self, mock_request):
+ """required arguments are reported as errors"""
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ grid_traffic_classes_module()
+ print("Info: test_module_fail_when_required_args_missing: %s" % exc.value.args[0]["msg"])
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_module_pass_when_required_args_present(self, mock_request):
+ """required arguments are reported as errors"""
+ mock_request.side_effect = [
+ SRR["node_health"], # get
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ grid_traffic_classes_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print("Info: test_module_pass_when_required_args_present: %s" % exc.value.args[0]["msg"])
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_create_na_sg_grid_traffic_class_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_grid_traffic_class())
+ mock_request.side_effect = [
+ SRR["empty_good"], # get
+ SRR["traffic_class_record"], # post
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_traffic_classes_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_create_na_sg_grid_traffic_class_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_idempotent_create_na_sg_grid_traffic_class_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_traffic_class()
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["traffic_classes"], # get
+ SRR["traffic_class_record"], # get
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_traffic_classes_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_idempotent_create_na_sg_grid_traffic_class_pass: %s" % repr(exc.value.args[0]))
+ assert not exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_update_na_sg_grid_traffic_class_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_traffic_class()
+ args["description"] = "Ansible Test with Limit"
+ args["limits"] = [{"type": "aggregateBandwidthIn", "value": 888888}]
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["traffic_classes"], # get
+ SRR["traffic_class_record"], # get
+ SRR["traffic_class_record_updated"], # put
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_traffic_classes_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_update_na_sg_grid_traffic_class_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_rename_na_sg_grid_traffic_class_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_traffic_class()
+ args["policy_id"] = "6b2946e6-7fed-40d0-9262-8e922580aba7"
+ args["name"] = "ansible-test-traffic-class-rename"
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["traffic_class_record"], # get
+ SRR["traffic_class_record_rename"], # put
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_traffic_classes_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_rename_na_sg_grid_traffic_class_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_delete_na_sg_grid_traffic_class_pass(self, mock_request):
+ args = self.set_args_delete_na_sg_grid_traffic_class()
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["traffic_classes"], # get
+ SRR["traffic_class_record"], # get
+ SRR["delete_good"], # delete
+ SRR["end_of_sequence"],
+ ]
+ my_obj = grid_traffic_classes_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_delete_na_sg_grid_traffic_class_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_update_na_sg_grid_traffic_class_bad_policy_id_fail(self, mock_request):
+ args = self.set_args_create_na_sg_grid_traffic_class()
+ args["policy_id"] = "ffffffff-ffff-aaaa-aaaa-000000000000"
+ args["description"] = "Bad ID"
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["not_found"], # get
+ ]
+ with pytest.raises(AnsibleFailJson) as exc:
+ my_obj = grid_traffic_classes_module()
+ my_obj.apply()
+ print("Info: test_update_na_sg_grid_traffic_class_bad_policy_id_fail: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["failed"]
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_user.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_user.py
new file mode 100644
index 000000000..c8ec38c09
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_grid_user.py
@@ -0,0 +1,476 @@
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID Grid User Ansible module: na_sg_grid_user"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+try:
+ from requests import Response
+except ImportError:
+ if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available')
+ else:
+ raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_grid_user import (
+ SgGridUser as grid_user_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "empty_good": ({"data": []}, None),
+ "not_found": ({"status": "error", "code": 404, "data": {}}, {"key": "error.404"},),
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "delete_good": ({"code": 204}, None),
+ "pw_change_good": ({"code": 204}, None),
+ "grid_groups": (
+ {
+ "data": [
+ {
+ "displayName": "TestGridGroup1",
+ "uniqueName": "group/testgridgroup1",
+ "accountId": "12345678901234567890",
+ "id": "12345678-abcd-1234-abcd-1234567890ab",
+ "federated": False,
+ "groupURN": "urn:sgws:identity::12345678901234567890:group/testgridgroup1",
+ },
+ {
+ "displayName": "TestGridGroup2",
+ "uniqueName": "group/testgridgroup2",
+ "accountId": "12345678901234567890",
+ "id": "87654321-abcd-1234-cdef-1234567890ab",
+ "federated": False,
+ "groupURN": "urn:sgws:identity::12345678901234567890:group/testgridgroup2",
+ },
+ ]
+ },
+ None,
+ ),
+ "grid_users": (
+ {
+ "data": [
+ {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "accountId": "12345678901234567890",
+ "fullName": "testgriduser",
+ "uniqueName": "user/ansible-sg-adm-user1",
+ "userURN": "urn:sgws:identity::12345678901234567890:user/testgriduser",
+ "federated": False,
+ "memberOf": ["12345678-abcd-1234-abcd-1234567890ab"],
+ "disable": False,
+ }
+ ]
+ },
+ None,
+ ),
+ "grid_user_record_no_group": (
+ {
+ "data": {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "accountId": "12345678901234567890",
+ "fullName": "testgriduser",
+ "uniqueName": "user/ansible-sg-adm-user1",
+ "userURN": "urn:sgws:identity::12345678901234567890:user/testgriduser",
+ "federated": False,
+ "disable": False,
+ }
+ },
+ None,
+ ),
+ "grid_user_record": (
+ {
+ "data": {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "accountId": "12345678901234567890",
+ "fullName": "testgriduser",
+ "uniqueName": "user/ansible-sg-adm-user1",
+ "userURN": "urn:sgws:identity::12345678901234567890:user/testgriduser",
+ "federated": False,
+ "memberOf": ["12345678-abcd-1234-abcd-1234567890ab"],
+ "disable": False,
+ }
+ },
+ None,
+ ),
+ "grid_user_record_update": (
+ {
+ "data": {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "accountId": "12345678901234567890",
+ "fullName": "testgriduser",
+ "uniqueName": "user/ansible-sg-adm-user1",
+ "userURN": "urn:sgws:identity::12345678901234567890:user/testgriduser",
+ "federated": False,
+ "memberOf": [
+ "12345678-abcd-1234-abcd-1234567890ab",
+ "87654321-abcd-1234-cdef-1234567890ab",
+ ],
+ "disable": False,
+ }
+ },
+ None,
+ ),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """ a group of related Unit Tests """
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(
+ basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json
+ )
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ "full_name": "TestUser",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "state": "present",
+ "full_name": "TestUser",
+ "unique_name": "user/testuser",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_create_na_sg_grid_user_no_group(self):
+ return dict(
+ {
+ "state": "present",
+ "full_name": "TestUser",
+ "unique_name": "user/testuser",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_create_na_sg_grid_user(self):
+ return dict(
+ {
+ "state": "present",
+ "full_name": "TestUser",
+ "unique_name": "user/testuser",
+ "member_of": ["group/testgridgroup1"],
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_delete_na_sg_grid_user(self):
+ return dict(
+ {
+ "state": "absent",
+ "unique_name": "user/testuser",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def test_module_fail_when_required_args_missing(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ grid_user_module()
+ print(
+ "Info: test_module_fail_when_required_args_missing: %s"
+ % exc.value.args[0]["msg"]
+ )
+
+ def test_module_fail_when_required_args_present(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ grid_user_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print(
+ "Info: test_module_fail_when_required_args_present: %s"
+ % exc.value.args[0]["msg"]
+ )
+ assert exc.value.args[0]["changed"]
+
+ def test_module_fail_with_bad_unique_name(self):
+ """ error returned if unique_name doesn't start with user or federated_user """
+ with pytest.raises(AnsibleFailJson) as exc:
+ args = self.set_default_args_pass_check()
+ args["unique_name"] = "noprefixuser"
+ set_module_args(args)
+ grid_user_module()
+ print(
+ "Info: test_module_fail_with_bad_unique_name: %s" % exc.value.args[0]["msg"]
+ )
+
+ def set_args_create_na_sg_grid_user_with_password(self):
+ return dict(
+ {
+ "state": "present",
+ "full_name": "TestUser",
+ "unique_name": "user/testuser",
+ "member_of": ["group/testgridgroup1"],
+ "password": "netapp123",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_create_na_sg_grid_user_no_group_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_grid_user_no_group())
+ my_obj = grid_user_module()
+ mock_request.side_effect = [
+ SRR["not_found"], # get
+ SRR["grid_user_record_no_group"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_create_na_sg_grid_user_no_group_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_create_na_sg_grid_user_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_grid_user())
+ my_obj = grid_user_module()
+ mock_request.side_effect = [
+ SRR["not_found"], # get
+ SRR["grid_groups"], # get
+ SRR["grid_user_record"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_create_na_sg_grid_user_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_idempotent_create_na_sg_grid_user_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_grid_user())
+ my_obj = grid_user_module()
+ mock_request.side_effect = [
+ SRR["grid_user_record"], # get
+ SRR["grid_groups"], # get
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_idempotent_create_na_sg_grid_user_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert not exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_update_na_sg_grid_user_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_user()
+ args["member_of"] = ["group/testgridgroup1", "group/testgridgroup2"]
+
+ set_module_args(args)
+ my_obj = grid_user_module()
+ mock_request.side_effect = [
+ SRR["grid_user_record"], # get
+ SRR["grid_groups"], # get
+ SRR["grid_user_record_update"], # put
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_update_na_sg_grid_user_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_delete_na_sg_grid_user_pass(self, mock_request):
+ set_module_args(self.set_args_delete_na_sg_grid_user())
+ my_obj = grid_user_module()
+ mock_request.side_effect = [
+ SRR["grid_user_record"], # get
+ SRR["grid_groups"], # get
+ SRR["delete_good"], # delete
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_delete_na_sg_grid_user_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ # create user and set pass
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_create_na_sg_grid_user_and_set_password_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_grid_user_with_password())
+ my_obj = grid_user_module()
+ mock_request.side_effect = [
+ SRR["not_found"], # get
+ SRR["grid_groups"], # get
+ SRR["grid_user_record"], # post
+ SRR["pw_change_good"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_create_na_sg_grid_user_and_set_password_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ # Idempotent user with password defined
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_idempotent_create_na_sg_grid_user_and_set_password_pass(
+ self, mock_request
+ ):
+ set_module_args(self.set_args_create_na_sg_grid_user_with_password())
+ my_obj = grid_user_module()
+ mock_request.side_effect = [
+ SRR["grid_user_record"], # get
+ SRR["grid_groups"], # get
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_idempotent_create_na_sg_grid_user_and_set_password_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert not exc.value.args[0]["changed"]
+
+ # update user and set pass
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_update_na_sg_grid_user_and_set_password_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_user_with_password()
+ args["member_of"] = ["group/testgridgroup1", "group/testgridgroup2"]
+ args["update_password"] = "always"
+
+ set_module_args(args)
+ my_obj = grid_user_module()
+ mock_request.side_effect = [
+ SRR["grid_user_record"], # get
+ SRR["grid_groups"], # get
+ SRR["grid_user_record_update"], # put
+ SRR["pw_change_good"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_update_na_sg_grid_user_and_set_password_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ # set pass only
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_set_na_sg_grid_user_password_pass(self, mock_request):
+ args = self.set_args_create_na_sg_grid_user_with_password()
+ args["update_password"] = "always"
+
+ set_module_args(args)
+ my_obj = grid_user_module()
+ mock_request.side_effect = [
+ SRR["grid_user_record"], # get
+ SRR["grid_groups"], # get
+ SRR["pw_change_good"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_set_na_sg_grid_user_password_pass: %s" % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ # attempt to set password on federated user
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_fail_set_federated_user_password(self, mock_request):
+ with pytest.raises(AnsibleFailJson) as exc:
+ args = self.set_args_create_na_sg_grid_user_with_password()
+ args["unique_name"] = "federated-user/abc123"
+ args["update_password"] = "always"
+ set_module_args(args)
+ grid_user_module()
+ print(
+ "Info: test_fail_set_federated_user_password: %s" % repr(exc.value.args[0])
+ )
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_container.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_container.py
new file mode 100644
index 000000000..21c49a556
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_container.py
@@ -0,0 +1,348 @@
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID Org Container Ansible module: na_sg_org_container"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+
+try:
+ from requests import Response
+except ImportError:
+ if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip("Skipping Unit Tests on 2.6 as requests is not available")
+ else:
+ raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_org_container import (
+ SgOrgContainer as org_container_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "empty_good": ({"data": []}, None),
+ "not_found": (
+ {"status": "error", "code": 404, "data": {}},
+ {"key": "error.404"},
+ ),
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "delete_good": (None, None),
+ "version_114": ({"data": {"productVersion": "11.4.0-20200721.1338.d3969b3"}}, None),
+ "version_116": ({"data": {"productVersion": "11.6.0-20211120.0301.850531e"}}, None),
+ "global_compliance_disabled": (
+ {
+ "data": {
+ "complianceEnabled": False,
+ }
+ },
+ None,
+ ),
+ "global_compliance_enabled": (
+ {
+ "data": {
+ "complianceEnabled": True,
+ }
+ },
+ None,
+ ),
+ "org_containers": (
+ {"data": [{"name": "testbucket", "creationTime": "2020-02-04T12:43:50.777Z", "region": "us-east-1"}]},
+ None,
+ ),
+ "org_container_record": (
+ {"data": {"name": "testbucket", "creationTime": "2020-02-04T12:43:50.777Z", "region": "us-east-1"}},
+ None,
+ ),
+ "org_container_objectlock_record": (
+ {
+ "data": {
+ "name": "testbucket",
+ "creationTime": "2020-02-04T12:43:50.777Z",
+ "region": "us-east-1",
+ "s3ObjectLock": {"enabled": True},
+ }
+ },
+ None,
+ ),
+ "org_container_record_update": (
+ {
+ "data": {
+ "name": "testbucket",
+ "creationTime": "2020-02-04T12:43:50.777Z",
+ "region": "us-east-1",
+ "compliance": {"autoDelete": False, "legalHold": False},
+ }
+ },
+ None,
+ ),
+ "org_container_versioning_disabled": ({"data": {"versioningEnabled": False, "versioningSuspended": False}}, None),
+ "org_container_versioning_enabled": ({"data": {"versioningEnabled": True, "versioningSuspended": False}}, None),
+ "org_container_versioning_suspended": ({"data": {"versioningEnabled": False, "versioningSuspended": True}}, None),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """a group of related Unit Tests"""
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json)
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {"name": "testbucket", "auth_token": "01234567-5678-9abc-78de-9fgabc123def", "validate_certs": False}
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "state": "present",
+ "name": "testbucket",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_create_na_sg_org_container(self):
+ return dict(
+ {
+ "state": "present",
+ "name": "testbucket",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_delete_na_sg_org_container(self):
+ return dict(
+ {
+ "state": "absent",
+ "name": "testbucket",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_module_fail_when_required_args_missing(self, mock_request):
+ """required arguments are reported as errors"""
+ mock_request.side_effect = [
+ SRR["version_114"],
+ ]
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ org_container_module()
+ print("Info: test_module_fail_when_required_args_missing: %s" % exc.value.args[0]["msg"])
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_module_fail_when_required_args_present(self, mock_request):
+ """required arguments are reported as errors"""
+ mock_request.side_effect = [
+ SRR["version_114"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ org_container_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print("Info: test_module_fail_when_required_args_present: %s" % exc.value.args[0]["msg"])
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_create_na_sg_org_container_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_org_container())
+ mock_request.side_effect = [
+ SRR["version_114"],
+ SRR["empty_good"], # get
+ SRR["org_container_record"], # post
+ SRR["end_of_sequence"],
+ ]
+ my_obj = org_container_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_create_na_sg_org_container_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_idempotent_create_na_sg_org_container_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_org_container())
+ mock_request.side_effect = [
+ SRR["version_114"],
+ SRR["org_containers"], # get
+ SRR["end_of_sequence"],
+ ]
+ my_obj = org_container_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_idempotent_create_na_sg_org_container_pass: %s" % repr(exc.value.args[0]))
+ assert not exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_update_na_sg_org_container_pass(self, mock_request):
+ args = self.set_args_create_na_sg_org_container()
+ args["compliance"] = {"auto_delete": False, "legal_hold": False}
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["version_114"],
+ SRR["org_containers"], # get
+ SRR["org_container_record_update"], # put
+ SRR["end_of_sequence"],
+ ]
+ my_obj = org_container_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_update_na_sg_org_container_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_delete_na_sg_org_container_pass(self, mock_request):
+ set_module_args(self.set_args_delete_na_sg_org_container())
+ mock_request.side_effect = [
+ SRR["version_114"],
+ SRR["org_containers"], # get
+ SRR["delete_good"], # delete
+ SRR["end_of_sequence"],
+ ]
+ my_obj = org_container_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_delete_na_sg_org_container_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_module_fail_minimum_version_not_met_object_lock(self, mock_request):
+ args = self.set_args_create_na_sg_org_container()
+ args["s3_object_lock_enabled"] = True
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["version_114"], # get
+ ]
+ with pytest.raises(AnsibleFailJson) as exc:
+ org_container_module()
+ print("Info: test_module_fail_minimum_version_not_met_object_lock: %s" % exc.value.args[0]["msg"])
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_create_na_sg_org_container_objectlock_global_compliance_fail(self, mock_request):
+ args = self.set_args_create_na_sg_org_container()
+ args["s3_object_lock_enabled"] = True
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["version_116"],
+ SRR["empty_good"], # get
+ SRR["global_compliance_disabled"], # get
+ ]
+ my_obj = org_container_module()
+ with pytest.raises(AnsibleFailJson) as exc:
+ my_obj.apply()
+ print("Info: test_create_na_sg_org_container_objectlock_global_compliance_fail: %s" % repr(exc.value.args[0]))
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_create_na_sg_org_container_objectlock_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_org_container())
+ mock_request.side_effect = [
+ SRR["version_116"],
+ SRR["empty_good"], # get
+ SRR["global_compliance_enabled"], # get
+ SRR["org_container_objectlock_record"], # post
+ SRR["end_of_sequence"],
+ ]
+ my_obj = org_container_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_create_na_sg_org_container_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_module_fail_minimum_version_not_met_versioning(self, mock_request):
+ args = self.set_args_create_na_sg_org_container()
+ args["bucket_versioning_enabled"] = True
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["version_114"], # get
+ ]
+ with pytest.raises(AnsibleFailJson) as exc:
+ org_container_module()
+ print("Info: test_module_fail_minimum_version_not_met_versioning: %s" % exc.value.args[0]["msg"])
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_create_na_sg_org_container_with_versioning_pass(self, mock_request):
+ args = self.set_args_create_na_sg_org_container()
+ args["bucket_versioning_enabled"] = True
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["version_116"],
+ SRR["empty_good"], # get
+ SRR["org_container_record"], # post
+ SRR["org_container_versioning_enabled"], # post
+ SRR["end_of_sequence"],
+ ]
+ my_obj = org_container_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_create_na_sg_org_container_with_versioning_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_update_na_sg_org_container_enable_versioning_pass(self, mock_request):
+ args = self.set_args_create_na_sg_org_container()
+ args["bucket_versioning_enabled"] = True
+ set_module_args(args)
+ mock_request.side_effect = [
+ SRR["version_116"],
+ SRR["org_containers"], # get
+ SRR["org_container_versioning_disabled"], # get
+ SRR["org_container_versioning_enabled"], # put
+ SRR["end_of_sequence"],
+ ]
+ my_obj = org_container_module()
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_update_na_sg_org_container_enable_versioning_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_group.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_group.py
new file mode 100644
index 000000000..c229130c2
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_group.py
@@ -0,0 +1,403 @@
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID Org Group Ansible module: na_sg_org_group"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+try:
+ from requests import Response
+except ImportError:
+ if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available')
+ else:
+ raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_org_group import (
+ SgOrgGroup as org_group_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "empty_good": ({"data": []}, None),
+ "not_found": (
+ {"status": "error", "code": 404, "data": {}},
+ {"key": "error.404"},
+ ),
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "delete_good": ({"code": 204}, None),
+ "org_groups": (
+ {
+ "data": [
+ {
+ "displayName": "TestOrgGroup",
+ "uniqueName": "group/testorggroup",
+ "policies": {
+ "management": {
+ "manageAllContainers": True,
+ "manageEndpoints": True,
+ "manageOwnS3Credentials": True,
+ },
+ "s3": {
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": "s3:*",
+ "Resource": "arn:aws:s3:::*",
+ }
+ ]
+ },
+ },
+ "accountId": "12345678901234567890",
+ "id": "00000000-0000-0000-0000-000000000000",
+ "federated": False,
+ "groupURN": "urn:sgws:identity::12345678901234567890:group/testorggroup",
+ }
+ ]
+ },
+ None,
+ ),
+ "org_group_record": (
+ {
+ "data": {
+ "displayName": "TestOrgGroup",
+ "uniqueName": "group/testorggroup",
+ "policies": {
+ "management": {
+ "manageAllContainers": True,
+ "manageEndpoints": True,
+ "manageOwnS3Credentials": True,
+ },
+ "s3": {
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": "s3:*",
+ "Resource": "arn:aws:s3:::*",
+ }
+ ]
+ },
+ },
+ "accountId": "12345678901234567890",
+ "id": "00000000-0000-0000-0000-000000000000",
+ "federated": False,
+ "groupURN": "urn:sgws:identity::12345678901234567890:group/testorggroup",
+ }
+ },
+ None,
+ ),
+ "org_group_record_update": (
+ {
+ "data": {
+ "displayName": "TestOrgGroup",
+ "uniqueName": "group/testorggroup",
+ "policies": {
+ "management": {
+ "manageAllContainers": True,
+ "manageEndpoints": True,
+ "manageOwnS3Credentials": True,
+ # "rootAccess": False,
+ },
+ "s3": {
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": "s3:*",
+ "Resource": "arn:aws:s3:::mybucket/*",
+ }
+ ]
+ },
+ },
+ "accountId": "12345678901234567890",
+ "id": "00000000-0000-0000-0000-000000000000",
+ "federated": False,
+ "groupURN": "urn:sgws:identity::12345678901234567890:group/testorggroup",
+ }
+ },
+ None,
+ ),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """ a group of related Unit Tests """
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(
+ basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json
+ )
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ "display_name": "TestGroup",
+ "management_policy": {
+ "manage_all_containers": True,
+ "manage_endpoints": True,
+ "manage_own_s3_credentials": True,
+ "root_access": False,
+ },
+ "s3_policy": {
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": "s3:*",
+ "Resource": "arn:aws:s3:::*",
+ }
+ ]
+ },
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "state": "present",
+ "display_name": "TestGroup",
+ "unique_name": "group/testgroup",
+ "management_policy": {
+ "manage_all_containers": True,
+ "manage_endpoints": True,
+ "manage_own_s3_credentials": True,
+ "root_access": False,
+ },
+ "s3_policy": {
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": "s3:*",
+ "Resource": "arn:aws:s3:::*",
+ }
+ ]
+ },
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_create_na_sg_org_group(self):
+ return dict(
+ {
+ "state": "present",
+ "display_name": "TestOrgGroup",
+ "unique_name": "group/testorggroup",
+ "management_policy": {
+ "manage_all_containers": True,
+ "manage_endpoints": True,
+ "manage_own_s3_credentials": True,
+ "root_access": False,
+ },
+ "s3_policy": {
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": "s3:*",
+ "Resource": "arn:aws:s3:::*",
+ }
+ ]
+ },
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_delete_na_sg_org_group(self):
+ return dict(
+ {
+ "state": "absent",
+ # "display_name": "TestOrgGroup",
+ "unique_name": "group/testorggroup",
+ # "management_policy": {
+ # "manage_all_containers": True,
+ # "manage_endpoints": True,
+ # "manage_own_s3_credentials": True,
+ # "root_access": False,
+ # },
+ # "s3_policy": {
+ # "Statement": [
+ # {
+ # "Effect": "Allow",
+ # "Action": "s3:*",
+ # "Resource": "arn:aws:s3:::*",
+ # }
+ # ]
+ # },
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def test_module_fail_when_required_args_missing(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ org_group_module()
+ print(
+ "Info: test_module_fail_when_required_args_missing: %s"
+ % exc.value.args[0]["msg"]
+ )
+
+ def test_module_fail_when_required_args_present(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ org_group_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print(
+ "Info: test_module_fail_when_required_args_present: %s"
+ % exc.value.args[0]["msg"]
+ )
+ assert exc.value.args[0]["changed"]
+
+ def test_module_fail_with_bad_unique_name(self):
+ """ error returned if unique_name doesn't start with group or federated_group """
+ with pytest.raises(AnsibleFailJson) as exc:
+ args = self.set_default_args_pass_check()
+ args["unique_name"] = "noprefixgroup"
+ set_module_args(args)
+ org_group_module()
+ print(
+ "Info: test_module_fail_with_bad_unique_name: %s"
+ % exc.value.args[0]["msg"]
+ )
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_create_na_sg_org_group_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_org_group())
+ my_obj = org_group_module()
+ mock_request.side_effect = [
+ SRR["not_found"], # get
+ SRR["org_group_record"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_create_na_sg_org_group_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_idempotent_create_na_sg_org_group_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_org_group())
+ my_obj = org_group_module()
+ mock_request.side_effect = [
+ SRR["org_group_record"], # get
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_idempotent_create_na_sg_org_group_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert not exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_update_na_sg_org_group_pass(self, mock_request):
+ args = self.set_args_create_na_sg_org_group()
+ args["s3_policy"] = (
+ {
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": "s3:*",
+ "Resource": "arn:aws:s3:::mybucket/*",
+ }
+ ]
+ },
+ )
+
+ args["management_policy"]["manage_endpoints"] = False
+
+ set_module_args(args)
+ my_obj = org_group_module()
+ mock_request.side_effect = [
+ SRR["org_group_record"], # get
+ SRR["org_group_record_update"], # put
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_update_na_sg_org_group_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_delete_na_sg_org_group_pass(self, mock_request):
+ set_module_args(self.set_args_delete_na_sg_org_group())
+ my_obj = org_group_module()
+ mock_request.side_effect = [
+ SRR["org_group_record"], # get
+ SRR["delete_good"], # delete
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_delete_na_sg_org_group_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_identity_federation.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_identity_federation.py
new file mode 100644
index 000000000..b02259005
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_identity_federation.py
@@ -0,0 +1,354 @@
+# (c) 2021, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID Tenant Identity Federation Ansible module: na_sg_org_identity_federation"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+try:
+ from requests import Response
+except ImportError:
+ if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available')
+ else:
+ raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_org_identity_federation import (
+ SgOrgIdentityFederation as org_identity_federation_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "check_mode_good": (None, None),
+ "identity_federation_unset": (
+ {
+ "data": {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "disable": True,
+ "type": "",
+ "ldapServiceType": "",
+ "hostname": "",
+ "port": 0,
+ "username": "",
+ "password": None,
+ "baseGroupDn": "",
+ "baseUserDn": "",
+ "disableTLS": False,
+ "enableLDAPS": False,
+ "caCert": "",
+ }
+ },
+ None,
+ ),
+ "identity_federation": (
+ {
+ "data": {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "disable": False,
+ "type": "ldap",
+ "ldapServiceType": "Active Directory",
+ "hostname": "ad.example.com",
+ "port": 389,
+ "username": "binduser",
+ "password": "********",
+ "baseGroupDn": "DC=example,DC=com",
+ "baseUserDn": "DC=example,DC=com",
+ "disableTLS": True,
+ "enableLDAPS": False,
+ "caCert": "",
+ }
+ },
+ None,
+ ),
+ "identity_federation_tls": (
+ {
+ "data": {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "disable": False,
+ "type": "ldap",
+ "ldapServiceType": "Active Directory",
+ "hostname": "ad.example.com",
+ "port": 636,
+ "username": "binduser",
+ "password": "********",
+ "baseGroupDn": "DC=example,DC=com",
+ "baseUserDn": "DC=example,DC=com",
+ "disableTLS": False,
+ "enableLDAPS": True,
+ "caCert": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIF+DCCBOCgAwIBAgITRwAAAAIg5KzMrJo+kQAAAAAAAjANBgkqhkiG9w0BAQUF\n"
+ "ADBlMRIwEAYKCZImiZPyLGQBGRYCYXUxFjAUBgoJkiaJk/IsZAEZFgZuZXRhcHAx\n"
+ "FjAUBgoJkiaJk/IsZAEZFgZhdXNuZ3MxHzAdBgNVBAMTFmF1c25ncy1NRUxOR1NE\n"
+ "QzAxLUNBLTEwHhcNMjEwMjExMDkzMTIwWhcNMjMwMjExMDk0MTIwWjAAMIIBIjAN\n"
+ "BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt2xPi4FS4Uc37KrDLEXXUoc4lhhT\n"
+ "uQmMnLc0PYZCIpzYOaosFIeGqco3woiC7wSZJ2whKE4RDcxxgE+azuGiSWVjIxIL\n"
+ "AimmcDhFid/T3KRN5jmkjBzUKuPBYzZBFih8iU9056rqgN7eMKQYjRwPeV0+AeiB\n"
+ "irw46OgkwVQu3shEUtXxZPP2Mb6Md23+4vSmcElUcW28Opt2q/M5fs7DNomG3eaG\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ }
+ },
+ None,
+ ),
+ "identity_federation_disable": (
+ {
+ "data": {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "disable": True,
+ "type": "ldap",
+ "ldapServiceType": "Active Directory",
+ "hostname": "ad.example.com",
+ "port": 389,
+ "username": "binduser",
+ "password": "********",
+ "baseGroupDn": "DC=example,DC=com",
+ "baseUserDn": "DC=example,DC=com",
+ "disableTLS": True,
+ "enableLDAPS": False,
+ "caCert": "",
+ }
+ },
+ None,
+ ),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """ a group of related Unit Tests """
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json)
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ "ldap_service_type": "Active Directory",
+ "hostname": "ad.example.com",
+ "port": 389,
+ "username": "binduser",
+ "password": "bindpass",
+ "base_group_dn": "DC=example,DC=com",
+ "base_user_dn": "DC=example,DC=com",
+ "tls": "Disabled",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "ldap_service_type": "Active Directory",
+ "hostname": "ad.example.com",
+ "port": 389,
+ "username": "binduser",
+ "password": "bindpass",
+ "base_group_dn": "DC=example,DC=com",
+ "base_user_dn": "DC=example,DC=com",
+ "tls": "Disabled",
+ "state": "present",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_set_na_sg_org_identity_federation(self):
+ return dict(
+ {
+ "ldap_service_type": "Active Directory",
+ "hostname": "ad.example.com",
+ "port": 389,
+ "username": "binduser",
+ "password": "bindpass",
+ "base_group_dn": "DC=example,DC=com",
+ "base_user_dn": "DC=example,DC=com",
+ "tls": "Disabled",
+ "state": "present",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_set_na_sg_org_identity_federation_tls(self):
+ return dict(
+ {
+ "ldap_service_type": "Active Directory",
+ "hostname": "ad.example.com",
+ "port": 636,
+ "username": "binduser",
+ "password": "bindpass",
+ "base_group_dn": "DC=example,DC=com",
+ "base_user_dn": "DC=example,DC=com",
+ "tls": "LDAPS",
+ "ca_cert": (
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIF+DCCBOCgAwIBAgITRwAAAAIg5KzMrJo+kQAAAAAAAjANBgkqhkiG9w0BAQUF\n"
+ "ADBlMRIwEAYKCZImiZPyLGQBGRYCYXUxFjAUBgoJkiaJk/IsZAEZFgZuZXRhcHAx\n"
+ "FjAUBgoJkiaJk/IsZAEZFgZhdXNuZ3MxHzAdBgNVBAMTFmF1c25ncy1NRUxOR1NE\n"
+ "QzAxLUNBLTEwHhcNMjEwMjExMDkzMTIwWhcNMjMwMjExMDk0MTIwWjAAMIIBIjAN\n"
+ "BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt2xPi4FS4Uc37KrDLEXXUoc4lhhT\n"
+ "uQmMnLc0PYZCIpzYOaosFIeGqco3woiC7wSZJ2whKE4RDcxxgE+azuGiSWVjIxIL\n"
+ "AimmcDhFid/T3KRN5jmkjBzUKuPBYzZBFih8iU9056rqgN7eMKQYjRwPeV0+AeiB\n"
+ "irw46OgkwVQu3shEUtXxZPP2Mb6Md23+4vSmcElUcW28Opt2q/M5fs7DNomG3eaG\n"
+ "-----END CERTIFICATE-----\n"
+ ),
+ "state": "present",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_remove_na_sg_org_identity_federation(self):
+ return dict(
+ {
+ "ldap_service_type": "Active Directory",
+ "hostname": "ad.example.com",
+ "state": "absent",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def test_module_fail_when_required_args_missing(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ org_identity_federation_module()
+ print("Info: test_module_fail_when_required_args_missing: %s" % exc.value.args[0]["msg"])
+
+ def test_module_fail_when_required_args_present(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ org_identity_federation_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print("Info: test_module_fail_when_required_args_present: %s" % exc.value.args[0]["msg"])
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_set_na_sg_org_identity_federation_pass(self, mock_request):
+ set_module_args(self.set_args_set_na_sg_org_identity_federation())
+ my_obj = org_identity_federation_module()
+ mock_request.side_effect = [
+ SRR["identity_federation_unset"], # get
+ SRR["identity_federation"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_set_na_sg_org_identity_federation_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_idempotent_set_na_sg_org_identity_federation_pass(self, mock_request):
+ args = self.set_args_set_na_sg_org_identity_federation()
+ # remove password
+ del args["password"]
+ set_module_args(args)
+ my_obj = org_identity_federation_module()
+ mock_request.side_effect = [
+ SRR["identity_federation"], # get
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_idempotent_set_na_sg_org_identity_federation_pass: %s" % repr(exc.value.args[0]))
+ assert not exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_set_na_sg_org_identity_federation_tls_pass(self, mock_request):
+ set_module_args(self.set_args_set_na_sg_org_identity_federation_tls())
+ my_obj = org_identity_federation_module()
+ mock_request.side_effect = [
+ SRR["identity_federation_unset"], # get
+ SRR["identity_federation_tls"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_set_na_sg_org_identity_federation_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_remove_na_sg_org_identity_federation_pass(self, mock_request):
+ set_module_args(self.set_args_remove_na_sg_org_identity_federation())
+ my_obj = org_identity_federation_module()
+ mock_request.side_effect = [
+ SRR["identity_federation"], # get
+ SRR["identity_federation_disable"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_remove_na_sg_org_identity_federation_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ # test check mode
+
+ @patch("ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request")
+ def test_check_mode_na_sg_org_identity_federation_pass(self, mock_request):
+ set_module_args(self.set_args_set_na_sg_org_identity_federation())
+ my_obj = org_identity_federation_module()
+ my_obj.module.check_mode = True
+ mock_request.side_effect = [
+ SRR["identity_federation_unset"], # get
+ SRR["check_mode_good"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_check_mode_na_sg_org_identity_federation_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+ assert exc.value.args[0]["msg"] == "Connection test successful"
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_info.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_info.py
new file mode 100644
index 000000000..e24c7cd46
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_info.py
@@ -0,0 +1,263 @@
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+''' Unit Tests NetApp StorageGRID Org Ansible module: na_sg_org_info '''
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+import json
+import pytest
+
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import patch
+
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_org_info \
+ import NetAppSgGatherInfo as sg_org_info_module
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ 'empty_good': ({'data': []}, None),
+ 'end_of_sequence': (None, 'Unexpected call to send_request'),
+ 'generic_error': (None, 'Expected error'),
+ 'org_compliance_global': ({'data': {}}, None),
+ 'org_config': ({'data': {}}, None),
+ 'org_config_product_version': ({'data': {}}, None),
+ 'org_containers': ({'data': {}}, None),
+ 'org_deactivated_features': ({'data': {}}, None),
+ 'org_endpoints': ({'data': []}, None),
+ 'org_groups': ({'data': []}, None),
+ 'org_identity_source': ({'data': {}}, None),
+ 'org_regions': ({'data': []}, None),
+ 'org_users_current_user_s3_access_keys': ({'data': []}, None),
+ 'org_usage': ({'data': {}}, None),
+ 'org_users': (
+ {
+ 'data': [
+ {
+ 'accountId': '99846664116007910793',
+ 'disable': False,
+ 'federated': False,
+ 'fullName': 'Root',
+ 'id': '00000000-0000-0000-0000-000000000000',
+ 'memberOf': None,
+ 'uniqueName': 'root',
+ 'userURN': 'urn:sgws:identity::99846664116007910793:root'
+ },
+ ]
+ },
+ None
+ ),
+ 'org_users_root': (
+ {
+ 'data': {
+ 'accountId': '99846664116007910793',
+ 'disable': False,
+ 'federated': False,
+ 'fullName': 'Root',
+ 'id': '00000000-0000-0000-0000-000000000000',
+ 'memberOf': None,
+ 'uniqueName': 'root',
+ 'userURN': 'urn:sgws:identity::99846664116007910793:root'
+ },
+ },
+ None
+ ),
+ 'versions': ({'data': [2, 3]}, None),
+}
+
+
+def set_module_args(args):
+ ''' Prepare arguments so that they will be picked up during module creation '''
+ args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ ''' Exception class to be raised by module.exit_json and caught by the test case '''
+ pass
+
+
+class AnsibleFailJson(Exception):
+ ''' Exception class to be raised by module.fail_json and caught by the test case '''
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ ''' Function to patch over exit_json; package return data into an exception '''
+ if 'changed' not in kwargs:
+ kwargs['changed'] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ ''' Function to patch over fail_json; package return data into an exception '''
+ kwargs['failed'] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ ''' A group of related Unit Tests '''
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(basic.AnsibleModule,
+ exit_json=exit_json,
+ fail_json=fail_json)
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ 'api_url': 'sgmi.example.com',
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ 'api_url': 'sgmi.example.com',
+ 'auth_token': '01234567-5678-9abc-78de-9fgabc123def',
+ }
+ )
+
+ def set_default_optional_args_pass_check(self):
+ return dict(
+ {
+ 'api_url': 'sgmi.example.com',
+ 'auth_token': '01234567-5678-9abc-78de-9fgabc123def',
+ 'validate_certs': False,
+ 'gather_subset': ['all'],
+ 'parameters': {'limit': 5},
+ }
+ )
+
+ def set_args_run_sg_gather_facts_for_all_info(self):
+ return dict({
+ 'api_url': 'sgmi.example.com',
+ 'auth_token': '01234567-5678-9abc-78de-9fgabc123def',
+ 'validate_certs': False,
+ })
+
+ def set_args_run_sg_gather_facts_for_org_users_info(self):
+ return dict({
+ 'api_url': 'sgmi.example.com',
+ 'auth_token': '01234567-5678-9abc-78de-9fgabc123def',
+ 'validate_certs': False,
+ 'gather_subset': ['org_users_info'],
+ })
+
+ def set_args_run_sg_gather_facts_for_org_users_and_org_users_root_info(self):
+ return dict({
+ 'api_url': 'sgmi.example.com',
+ 'auth_token': '01234567-5678-9abc-78de-9fgabc123def',
+ 'validate_certs': False,
+ 'gather_subset': ['org_users_info', 'org/users/root'],
+ })
+
+ def test_module_fail_when_required_args_missing(self):
+ ''' required arguments are reported as errors '''
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ sg_org_info_module()
+ print(
+ 'Info: test_module_fail_when_required_args_missing: %s'
+ % exc.value.args[0]['msg']
+ )
+
+ def test_module_pass_when_required_args_present(self):
+ ''' required arguments are reported as errors '''
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ sg_org_info_module()
+ exit_json(changed=True, msg='Induced arguments check')
+ print(
+ 'Info: test_module_pass_when_required_args_present: %s'
+ % exc.value.args[0]['msg']
+ )
+ assert exc.value.args[0]['changed']
+
+ def test_module_pass_when_optional_args_present(self):
+ ''' Optional arguments are reported as pass '''
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_optional_args_pass_check())
+ sg_org_info_module()
+ exit_json(changed=True, msg='Induced arguments check')
+ print(
+ 'Info: test_module_pass_when_optional_args_present: %s'
+ % exc.value.args[0]['msg']
+ )
+ assert exc.value.args[0]['changed']
+
+ @patch('ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request')
+ def test_run_sg_gather_facts_for_all_info_pass(self, mock_request):
+ set_module_args(self.set_args_run_sg_gather_facts_for_all_info())
+ my_obj = sg_org_info_module()
+ gather_subset = [
+ 'org/compliance-global',
+ 'org/config',
+ 'org/config/product-version',
+ 'org/containers',
+ 'org/deactivated-features',
+ 'org/endpoints',
+ 'org/groups',
+ 'org/identity-source',
+ 'org/regions',
+ 'org/users/current-user/s3-access-keys',
+ 'org/usage',
+ 'org/users',
+ 'org/users/root',
+ 'versions',
+ ]
+ mock_request.side_effect = [
+ SRR['org_compliance_global'],
+ SRR['org_config'],
+ SRR['org_config_product_version'],
+ SRR['org_containers'],
+ SRR['org_deactivated_features'],
+ SRR['org_endpoints'],
+ SRR['org_groups'],
+ SRR['org_identity_source'],
+ SRR['org_regions'],
+ SRR['org_users_current_user_s3_access_keys'],
+ SRR['org_usage'],
+ SRR['org_users'],
+ SRR['org_users_root'],
+ SRR['versions'],
+ SRR['end_of_sequence'],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print('Info: test_run_sg_gather_facts_for_all_info_pass: %s' % repr(exc.value.args))
+ assert set(exc.value.args[0]['sg_info']) == set(gather_subset)
+
+ @patch('ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request')
+ def test_run_sg_gather_facts_for_org_users_info_pass(self, mock_request):
+ set_module_args(self.set_args_run_sg_gather_facts_for_org_users_info())
+ my_obj = sg_org_info_module()
+ gather_subset = ['org/users']
+ mock_request.side_effect = [
+ SRR['org_users'],
+ SRR['end_of_sequence'],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print('Info: test_run_sg_gather_facts_for_org_users_info_pass: %s' % repr(exc.value.args))
+ assert set(exc.value.args[0]['sg_info']) == set(gather_subset)
+
+ @patch('ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request')
+ def test_run_sg_gather_facts_for_org_users_and_org_users_root_info_pass(self, mock_request):
+ set_module_args(self.set_args_run_sg_gather_facts_for_org_users_and_org_users_root_info())
+ my_obj = sg_org_info_module()
+ gather_subset = ['org/users', 'org/users/root']
+ mock_request.side_effect = [
+ SRR['org_users'],
+ SRR['org_users_root'],
+ SRR['end_of_sequence'],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print('Info: test_run_sg_gather_facts_for_org_users_and_org_users_root_info_pass: %s' % repr(exc.value.args))
+ assert set(exc.value.args[0]['sg_info']) == set(gather_subset)
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_user.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_user.py
new file mode 100644
index 000000000..8fcec6734
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_user.py
@@ -0,0 +1,476 @@
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID Org Group Ansible module: na_sg_org_user"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+try:
+ from requests import Response
+except ImportError:
+ if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available')
+ else:
+ raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_org_user import (
+ SgOrgUser as org_user_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "empty_good": ({"data": []}, None),
+ "not_found": ({"status": "error", "code": 404, "data": {}}, {"key": "error.404"},),
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "delete_good": ({"code": 204}, None),
+ "pw_change_good": ({"code": 204}, None),
+ "org_groups": (
+ {
+ "data": [
+ {
+ "displayName": "TestOrgGroup1",
+ "uniqueName": "group/testorggroup1",
+ "accountId": "12345678901234567890",
+ "id": "12345678-abcd-1234-abcd-1234567890ab",
+ "federated": False,
+ "groupURN": "urn:sgws:identity::12345678901234567890:group/testorggroup1",
+ },
+ {
+ "displayName": "TestOrgGroup2",
+ "uniqueName": "group/testorggroup2",
+ "accountId": "12345678901234567890",
+ "id": "87654321-abcd-1234-cdef-1234567890ab",
+ "federated": False,
+ "groupURN": "urn:sgws:identity::12345678901234567890:group/testorggroup2",
+ },
+ ]
+ },
+ None,
+ ),
+ "org_users": (
+ {
+ "data": [
+ {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "accountId": "12345678901234567890",
+ "fullName": "testorguser",
+ "uniqueName": "user/ansible-sg-demo-user1",
+ "userURN": "urn:sgws:identity::12345678901234567890:user/testorguser",
+ "federated": False,
+ "memberOf": ["12345678-abcd-1234-abcd-1234567890ab"],
+ "disable": False,
+ }
+ ]
+ },
+ None,
+ ),
+ "org_user_record_no_group": (
+ {
+ "data": {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "accountId": "12345678901234567890",
+ "fullName": "testorguser",
+ "uniqueName": "user/ansible-sg-demo-user1",
+ "userURN": "urn:sgws:identity::12345678901234567890:user/testorguser",
+ "federated": False,
+ "disable": False,
+ }
+ },
+ None,
+ ),
+ "org_user_record": (
+ {
+ "data": {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "accountId": "12345678901234567890",
+ "fullName": "testorguser",
+ "uniqueName": "user/ansible-sg-demo-user1",
+ "userURN": "urn:sgws:identity::12345678901234567890:user/testorguser",
+ "federated": False,
+ "memberOf": ["12345678-abcd-1234-abcd-1234567890ab"],
+ "disable": False,
+ }
+ },
+ None,
+ ),
+ "org_user_record_update": (
+ {
+ "data": {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "accountId": "12345678901234567890",
+ "fullName": "testorguser",
+ "uniqueName": "user/ansible-sg-demo-user1",
+ "userURN": "urn:sgws:identity::12345678901234567890:user/testorguser",
+ "federated": False,
+ "memberOf": [
+ "12345678-abcd-1234-abcd-1234567890ab",
+ "87654321-abcd-1234-cdef-1234567890ab",
+ ],
+ "disable": False,
+ }
+ },
+ None,
+ ),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """ a group of related Unit Tests """
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(
+ basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json
+ )
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ "full_name": "TestUser",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "state": "present",
+ "full_name": "TestUser",
+ "unique_name": "user/testuser",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_create_na_sg_org_user_no_group(self):
+ return dict(
+ {
+ "state": "present",
+ "full_name": "TestUser",
+ "unique_name": "user/testuser",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_create_na_sg_org_user(self):
+ return dict(
+ {
+ "state": "present",
+ "full_name": "TestUser",
+ "unique_name": "user/testuser",
+ "member_of": ["group/testorggroup1"],
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_delete_na_sg_org_user(self):
+ return dict(
+ {
+ "state": "absent",
+ # "full_name": "TestUser",
+ "unique_name": "user/testuser",
+ # "member_of": ["group/testorggroup1"],
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def test_module_fail_when_required_args_missing(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ org_user_module()
+ print(
+ "Info: test_module_fail_when_required_args_missing: %s"
+ % exc.value.args[0]["msg"]
+ )
+
+ def test_module_fail_when_required_args_present(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ org_user_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print(
+ "Info: test_module_fail_when_required_args_present: %s"
+ % exc.value.args[0]["msg"]
+ )
+ assert exc.value.args[0]["changed"]
+
+ def test_module_fail_with_bad_unique_name(self):
+ """ error returned if unique_name doesn't start with user or federated_user """
+ with pytest.raises(AnsibleFailJson) as exc:
+ args = self.set_default_args_pass_check()
+ args["unique_name"] = "noprefixuser"
+ set_module_args(args)
+ org_user_module()
+ print(
+ "Info: test_module_fail_with_bad_unique_name: %s" % exc.value.args[0]["msg"]
+ )
+
+ def set_args_create_na_sg_org_user_with_password(self):
+ return dict(
+ {
+ "state": "present",
+ "full_name": "TestUser",
+ "unique_name": "user/testuser",
+ "member_of": ["group/testorggroup1"],
+ "password": "netapp123",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_create_na_sg_org_user_no_group_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_org_user_no_group())
+ my_obj = org_user_module()
+ mock_request.side_effect = [
+ SRR["not_found"], # get
+ SRR["org_user_record_no_group"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_create_na_sg_org_user_no_group_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_create_na_sg_org_user_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_org_user())
+ my_obj = org_user_module()
+ mock_request.side_effect = [
+ SRR["not_found"], # get
+ SRR["org_groups"], # get
+ SRR["org_user_record"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_create_na_sg_org_user_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_idempotent_create_na_sg_org_user_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_org_user())
+ my_obj = org_user_module()
+ mock_request.side_effect = [
+ SRR["org_user_record"], # get
+ SRR["org_groups"], # get
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_idempotent_create_na_sg_org_user_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert not exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_update_na_sg_org_user_pass(self, mock_request):
+ args = self.set_args_create_na_sg_org_user()
+ args["member_of"] = ["group/testorggroup1", "group/testorggroup2"]
+
+ set_module_args(args)
+ my_obj = org_user_module()
+ mock_request.side_effect = [
+ SRR["org_user_record"], # get
+ SRR["org_groups"], # get
+ SRR["org_user_record_update"], # put
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_update_na_sg_org_user_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_delete_na_sg_org_user_pass(self, mock_request):
+ set_module_args(self.set_args_delete_na_sg_org_user())
+ my_obj = org_user_module()
+ mock_request.side_effect = [
+ SRR["org_user_record"], # get
+ SRR["org_groups"], # get
+ SRR["delete_good"], # delete
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print("Info: test_delete_na_sg_org_user_pass: %s" % repr(exc.value.args[0]))
+ assert exc.value.args[0]["changed"]
+
+ # create user and set pass
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_create_na_sg_org_user_and_set_password_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_org_user_with_password())
+ my_obj = org_user_module()
+ mock_request.side_effect = [
+ SRR["not_found"], # get
+ SRR["org_groups"], # get
+ SRR["org_user_record"], # post
+ SRR["pw_change_good"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_create_na_sg_org_user_and_set_password_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ # Idempotent user with password defined
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_idempotent_create_na_sg_org_user_and_set_password_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_org_user_with_password())
+ my_obj = org_user_module()
+ mock_request.side_effect = [
+ SRR["org_user_record"], # get
+ SRR["org_groups"], # get
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_idempotent_create_na_sg_org_user_and_set_password_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert not exc.value.args[0]["changed"]
+
+ # update user and set pass
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_update_na_sg_org_user_and_set_password_pass(self, mock_request):
+ args = self.set_args_create_na_sg_org_user_with_password()
+ args["member_of"] = ["group/testorggroup1", "group/testorggroup2"]
+ args["update_password"] = "always"
+
+ set_module_args(args)
+ my_obj = org_user_module()
+ mock_request.side_effect = [
+ SRR["org_user_record"], # get
+ SRR["org_groups"], # get
+ SRR["org_user_record_update"], # put
+ SRR["pw_change_good"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_update_na_sg_org_user_and_set_password_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ # set pass only
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_set_na_sg_org_user_password_pass(self, mock_request):
+ args = self.set_args_create_na_sg_org_user_with_password()
+ args["update_password"] = "always"
+
+ set_module_args(args)
+ my_obj = org_user_module()
+ mock_request.side_effect = [
+ SRR["org_user_record"], # get
+ SRR["org_groups"], # get
+ SRR["pw_change_good"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_set_na_sg_org_user_password_pass: %s" % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ # attempt to set password on federated user
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_fail_set_federated_user_password(self, mock_request):
+ with pytest.raises(AnsibleFailJson) as exc:
+ args = self.set_args_create_na_sg_org_user_with_password()
+ args["unique_name"] = "federated-user/abc123"
+ args["update_password"] = "always"
+ set_module_args(args)
+ org_user_module()
+ print(
+ "Info: test_fail_set_federated_user_password: %s" % repr(exc.value.args[0])
+ )
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_user_s3_key.py b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_user_s3_key.py
new file mode 100644
index 000000000..53696bdbf
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/plugins/modules/test_na_sg_org_user_s3_key.py
@@ -0,0 +1,238 @@
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+""" unit tests NetApp StorageGRID Org Group Ansible module: na_sg_org_user_s3_key"""
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+import json
+import pytest
+import sys
+try:
+ from requests import Response
+except ImportError:
+ if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not be available')
+ else:
+ raise
+
+from ansible_collections.netapp.storagegrid.tests.unit.compat import unittest
+from ansible_collections.netapp.storagegrid.tests.unit.compat.mock import (
+ patch,
+ Mock,
+)
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible_collections.netapp.storagegrid.plugins.modules.na_sg_org_user_s3_key import (
+ SgOrgUserS3Key as org_s3_key_module,
+)
+
+# REST API canned responses when mocking send_request
+SRR = {
+ # common responses
+ "empty_good": ({"data": []}, None),
+ "not_found": (
+ {"status": "error", "code": 404, "data": {}},
+ {"key": "error.404"},
+ ),
+ "end_of_sequence": (None, "Unexpected call to send_request"),
+ "generic_error": (None, "Expected error"),
+ "delete_good": ({"code": 204}, None),
+ "org_user_record": (
+ {
+ "data": {
+ "id": "09876543-abcd-4321-abcd-0987654321ab",
+ "accountId": "12345678901234567890",
+ "fullName": "testorguser",
+ "uniqueName": "user/testorguser",
+ "userURN": "urn:sgws:identity::12345678901234567890:user/testorguser",
+ "federated": False,
+ "memberOf": ["12345678-abcd-1234-abcd-1234567890ab"],
+ "disable": False,
+ }
+ },
+ None,
+ ),
+ "org_s3_key": (
+ {
+ "data": {
+ "id": "abcABC_01234-0123456789abcABCabc0123456789==",
+ "accountId": 12345678901234567000,
+ "displayName": "****************AB12",
+ "userURN": "urn:sgws:identity::12345678901234567890:root",
+ "userUUID": "09876543-abcd-4321-abcd-0987654321ab",
+ "expires": "2020-09-04T00:00:00.000Z",
+ "accessKey": "ABCDEFabcd1234567890",
+ "secretAccessKey": "abcABC+123456789012345678901234567890123",
+ }
+ },
+ None,
+ ),
+}
+
+
+def set_module_args(args):
+ """prepare arguments so that they will be picked up during module creation"""
+ args = json.dumps({"ANSIBLE_MODULE_ARGS": args})
+ basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access
+
+
+class AnsibleExitJson(Exception):
+ """Exception class to be raised by module.exit_json and caught by the test case"""
+
+ pass
+
+
+class AnsibleFailJson(Exception):
+ """Exception class to be raised by module.fail_json and caught by the test case"""
+
+ pass
+
+
+def exit_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over exit_json; package return data into an exception"""
+ if "changed" not in kwargs:
+ kwargs["changed"] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs): # pylint: disable=unused-argument
+ """function to patch over fail_json; package return data into an exception"""
+ kwargs["failed"] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class TestMyModule(unittest.TestCase):
+ """ a group of related Unit Tests """
+
+ def setUp(self):
+ self.mock_module_helper = patch.multiple(
+ basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json
+ )
+ self.mock_module_helper.start()
+ self.addCleanup(self.mock_module_helper.stop)
+
+ def set_default_args_fail_check(self):
+ return dict(
+ {
+ "unique_user_name": "user/testorguser",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_default_args_pass_check(self):
+ return dict(
+ {
+ "state": "present",
+ "unique_user_name": "user/testorguser",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_create_na_sg_org_user_s3_keys(self):
+ return dict(
+ {
+ "state": "present",
+ "unique_user_name": "user/testorguser",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def set_args_delete_na_sg_org_user_s3_keys(self):
+ return dict(
+ {
+ "state": "absent",
+ "unique_user_name": "user/testorguser",
+ "access_key": "ABCDEFabcd1234567890",
+ "api_url": "gmi.example.com",
+ "auth_token": "01234567-5678-9abc-78de-9fgabc123def",
+ "validate_certs": False,
+ }
+ )
+
+ def test_module_fail_when_required_args_missing(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleFailJson) as exc:
+ set_module_args(self.set_default_args_fail_check())
+ org_s3_key_module()
+ print(
+ "Info: test_module_fail_when_required_args_missing: %s"
+ % exc.value.args[0]["msg"]
+ )
+
+ def test_module_fail_when_required_args_present(self):
+ """ required arguments are reported as errors """
+ with pytest.raises(AnsibleExitJson) as exc:
+ set_module_args(self.set_default_args_pass_check())
+ org_s3_key_module()
+ exit_json(changed=True, msg="Induced arguments check")
+ print(
+ "Info: test_module_fail_when_required_args_present: %s"
+ % exc.value.args[0]["msg"]
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_create_na_sg_org_user_s3_key_pass(self, mock_request):
+ set_module_args(self.set_args_create_na_sg_org_user_s3_keys())
+ my_obj = org_s3_key_module()
+ mock_request.side_effect = [
+ SRR["org_user_record"], # get
+ SRR["org_s3_key"], # post
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_create_na_sg_org_user_s3_key_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_idempotent_create_na_sg_org_user_s3_key_pass(self, mock_request):
+ args = self.set_args_create_na_sg_org_user_s3_keys()
+ args["access_key"] = "ABCDEFabcd1234567890"
+ set_module_args(args)
+ my_obj = org_s3_key_module()
+ mock_request.side_effect = [
+ SRR["org_user_record"], # get
+ SRR["org_s3_key"], # get
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_idempotent_create_na_sg_org_user_s3_key_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert not exc.value.args[0]["changed"]
+
+ @patch(
+ "ansible_collections.netapp.storagegrid.plugins.module_utils.netapp.SGRestAPI.send_request"
+ )
+ def test_delete_na_sg_org_user_s3_keys_pass(self, mock_request):
+ set_module_args(self.set_args_delete_na_sg_org_user_s3_keys())
+ my_obj = org_s3_key_module()
+ mock_request.side_effect = [
+ SRR["org_s3_key"], # get
+ SRR["delete_good"], # delete
+ SRR["end_of_sequence"],
+ ]
+ with pytest.raises(AnsibleExitJson) as exc:
+ my_obj.apply()
+ print(
+ "Info: test_delete_na_sg_org_user_s3_keys_pass: %s"
+ % repr(exc.value.args[0])
+ )
+ assert exc.value.args[0]["changed"]
diff --git a/ansible_collections/netapp/storagegrid/tests/unit/requirements.txt b/ansible_collections/netapp/storagegrid/tests/unit/requirements.txt
new file mode 100644
index 000000000..b754473a9
--- /dev/null
+++ b/ansible_collections/netapp/storagegrid/tests/unit/requirements.txt
@@ -0,0 +1 @@
+requests ; python_version >= '2.7'