diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:22 +0000 |
commit | 38b7c80217c4e72b1d8988eb1e60bb6e77334114 (patch) | |
tree | 356e9fd3762877d07cde52d21e77070aeff7e789 /ansible_collections/netapp/ontap | |
parent | Adding upstream version 7.7.0+dfsg. (diff) | |
download | ansible-38b7c80217c4e72b1d8988eb1e60bb6e77334114.tar.xz ansible-38b7c80217c4e72b1d8988eb1e60bb6e77334114.zip |
Adding upstream version 9.4.0+dfsg.upstream/9.4.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
118 files changed, 5115 insertions, 520 deletions
diff --git a/ansible_collections/netapp/ontap/.github/ISSUE_TEMPLATE/bug_report.yml b/ansible_collections/netapp/ontap/.github/ISSUE_TEMPLATE/bug_report.yml index e67eb0f94..0a216967d 100644 --- a/ansible_collections/netapp/ontap/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/ansible_collections/netapp/ontap/.github/ISSUE_TEMPLATE/bug_report.yml @@ -20,12 +20,12 @@ body: **Tip:** If you are seeking community support, please consider - [Join our Slack community][ML||IRC]. + [Join our Discord community][ML||IRC]. [ML||IRC]: - https://join.slack.com/t/netapppub/shared_invite/zt-njcjx2sh-1VR2mEDvPcJAmPutOnP~mg + https://discord.gg/NetApp [issue search]: ../search?q=is%3Aissue&type=issues diff --git a/ansible_collections/netapp/ontap/.github/workflows/main.yml b/ansible_collections/netapp/ontap/.github/workflows/main.yml index 94353d961..b2b7e389d 100644 --- a/ansible_collections/netapp/ontap/.github/workflows/main.yml +++ b/ansible_collections/netapp/ontap/.github/workflows/main.yml @@ -20,6 +20,8 @@ jobs: - stable-2.12 - stable-2.13 - stable-2.14 + - stable-2.15 + - stable-2.16 - devel steps: diff --git a/ansible_collections/netapp/ontap/CHANGELOG.rst b/ansible_collections/netapp/ontap/CHANGELOG.rst index 0a5b2167a..9a796e0ab 100644 --- a/ansible_collections/netapp/ontap/CHANGELOG.rst +++ b/ansible_collections/netapp/ontap/CHANGELOG.rst @@ -5,6 +5,111 @@ NetApp ONTAP Collection Release Notes .. contents:: Topics +v22.10.0 +======== + +Minor Changes +------------- + +- na_ontap_cifs_server - new option `is_multichannel_enabled` added in REST, requires ONTAP 9.10 or later. +- na_ontap_export_policy_rule - added `actions` and `modify` in module output. +- na_ontap_file_security_permissions_acl - added `actions` and `modify` in module output. +- na_ontap_igroup_initiator - added `actions` in module output. +- na_ontap_lun_map - added `actions` in module output. +- na_ontap_lun_map_reporting_nodes - added `actions` in module output. +- na_ontap_name_mappings - added `actions` and `modify` in module output. +- na_ontap_node - added `modify` in module output. +- na_ontap_rest_info - added warning message if given subset doesn't support option `owning_resource`. +- na_ontap_storage_auto_giveback - added information on modified attributes in module output. +- na_ontap_vscan_scanner_pool - added REST support to Vscan Scanner Pools Configuration module, requires ONTAP 9.6 or later. + +Bugfixes +-------- + +- na_ontap_igroup_initiator - fixed issue with idempotency. + +v22.9.0 +======= + +Minor Changes +------------- + +- na_ontap_cifs_server - new option `lm_compatibility_level` added in REST, requires ONTAP 9.8 or later. +- na_ontap_cluster - new option `certificate.uuid` added in REST, requires ONTAP 9.10 or later. +- na_ontap_cluster_peer - added REST only support for modifying remote intercluster addresses in cluster peer relation. +- na_ontap_ems_destination - new options `syslog`, `port`, `transport`, `message_format`, `timestamp_format_override` and `hostname_format_override` added in REST, requires ONTAP 9.12.1 or later. +- na_ontap_s3_services - create, modify S3 service returns `s3_service_info` in module output. +- na_ontap_snapmirror - updated resync and resume operation for synchronous snapmirror relationship in REST. + +Bugfixes +-------- + +- na_ontap_nfs - fix error with `windows` in REST for ONTAP 9.10 or earlier. +- na_ontap_security_certificates - fix error with ontap_info returned in module output in REST. +- na_ontap_snapshot_policy - fix issue with modifying snapshot policy in REST. +- na_ontap_volume - modified `type` to be case insensitive in REST. + +New Modules +----------- + +- netapp.ontap.na_ontap_cifs_unix_symlink_mapping - NetApp ONTAP module to manage UNIX symbolic link mapping for CIFS clients. +- netapp.ontap.na_ontap_cli_timeout - NetApp ONTAP module to set the CLI inactivity timeout value. +- netapp.ontap.na_ontap_snmp_config - NetApp ONTAP module to modify SNMP configuration. + +v22.8.3 +======= + +Bugfixes +-------- + +- na_ontap_ems_destination - fix field error with `certificate.name` for ONTAP 9.11.1 or later in REST. +- na_ontap_vserver_peer - fix issue with peering multiple clusters with same vserver name in REST. + +v22.8.1 +======= + +Bugfixes +-------- + +- na_ontap_dns - fix keyerror for uuid when DNS is set to vserver in REST. +- na_ontap_volume - fix invalid field error with 'space.snapshot.autodelete' in REST. + +v22.8.0 +======= + +Minor Changes +------------- + +- na_ontap_broadcast_domain - changed documentation for ipspace as it is required while using REST. +- na_ontap_cg_snapshot - added REST support to the cg snapshot module, requires ONTAP 9.10.1 or later. +- na_ontap_cifs_server - new option `default_site` added in REST, requires ONTAP 9.13.1 or later. +- na_ontap_ems_destination - new option ``certificate``, ``ca`` added. +- na_ontap_kerberos_realm - add REST support for `admin_server_ip`, `admin_server_port`, `pw_server_ip`, `pw_server_port` and `clock_skew` from ONTAP 9.13.1 or later +- na_ontap_lun - new option `qtree_name` added in REST. +- na_ontap_net_ifgrp - return `name` and other details of a newly created interface group in module output in REST. +- na_ontap_qos_policy_group - added new REST only options `expected_iops_allocation` and `peak_iops_allocation`, requires ONTAP 9.10.1 or later. +- na_ontap_rest_info - new option `hal_linking` added to enable or disable HAL links. +- na_ontap_restit - returns changed as False for GET method. +- na_ontap_snmp - added REST support for snmpv3 user. +- na_ontap_user - Added warning message when password is not changed. +- na_ontap_volume - added REST support for `atime_update` requires ONTAP 9.8 or later, `snapdir_access` and `snapshot_auto_delete` requires ONTAP 9.13.1 or later. +- na_ontap_volume - added new REST only options `vol_nearly_full_threshold_percent` and `vol_full_threshold_percent`, requires ONTAP 9.9 or later. + +Bugfixes +-------- + +- na_ontap_dns - fix DNS not working with Cluster mode. +- na_ontap_ems_filter - fix delete operation not idempotent for filter. +- na_ontap_ems_filter - fix modify operation to add rule in existing filter. +- na_ontap_login_messages - fix idempotency issue in Cluster scope in REST. +- na_ontap_nfs - fix `default_user` under `windows` not getting modified if not set previously in REST. +- na_ontap_svm - fix REST version warning for `ndmp` under `services`. + +New Modules +----------- + +- netapp.ontap.na_ontap_ems_config - NetApp ONTAP module to modify EMS configuration. + v22.7.0 ======= diff --git a/ansible_collections/netapp/ontap/FILES.json b/ansible_collections/netapp/ontap/FILES.json index 6d2b6162a..dde171372 100644 --- a/ansible_collections/netapp/ontap/FILES.json +++ b/ansible_collections/netapp/ontap/FILES.json @@ -109,7 +109,7 @@ "name": "plugins/module_utils/netapp.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ec34efdb000ccbfa336b4f9f09229dae445132884bca6ab83903e20273e620d9", + "chksum_sha256": "6daa1820439f22b17459fe302f0d4bab7d9b55abe9a16954ba75a90a242bd01e", "format": 1 }, { @@ -228,7 +228,7 @@ "name": "plugins/modules/na_ontap_lun_map_reporting_nodes.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "c242cdda8ed0d6909df6850414f5b669d0c0f6bfa5f734c0d67d0190918db78a", + "chksum_sha256": "1eda5ac3890de22d0b01ad52cca1e4ebebed292d1522484320e5988aebaee6ac", "format": 1 }, { @@ -319,7 +319,7 @@ "name": "plugins/modules/na_ontap_dns.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "4fe57a0b4047c049393d27cda4e811ef882a14f6d31a1d0281fab65f68e98f23", + "chksum_sha256": "6ea5685b153844b586517029a15901465b587a5e8c460647e8b4672135c5fe14", "format": 1 }, { @@ -340,7 +340,7 @@ "name": "plugins/modules/na_ontap_volume.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "3ee1b2a761551734cc35fefca8ab1ae77da4030a43de2681a2665c30ed4866e2", + "chksum_sha256": "3c7a5ae9b747fbc66bc02ef0725b23880c3fdb121770c5f46f1964f699bb62f7", "format": 1 }, { @@ -382,7 +382,7 @@ "name": "plugins/modules/na_ontap_restit.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "e9c29e8a5dac84b2dda82f8456541055f0c2507c7d3ba160bb9f656603e2012f", + "chksum_sha256": "3b962cd935d284e56fbaee7b33e299078363e0ba6091e698b8941844e92fb2c7", "format": 1 }, { @@ -473,14 +473,14 @@ "name": "plugins/modules/na_ontap_lun_map.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "715e1b085b5efca021341bd7acfe60dbe6f1b1b6aa085cb1fda43a273b7137c3", + "chksum_sha256": "7db77cccba69ec51ce6a3ae0fea4f9b6f176b59183f0d1a51ab5c26f1b0e19ca", "format": 1 }, { "name": "plugins/modules/na_ontap_user.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "734c38050413783ac404c333cffa25012eb236e80e58c2fe12377f3833df8ab8", + "chksum_sha256": "327a7de29eddb2604a792a299757d6e72078db5410b1731fd570e0cdf59ebcbf", "format": 1 }, { @@ -494,7 +494,7 @@ "name": "plugins/modules/na_ontap_security_certificates.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "86b25e1b63997f6b23ed639afc080e09dd8990314b4a7aa55878d5aab77cfe4c", + "chksum_sha256": "57fb9f829e8c75ad7ac50be2274372806c5d43410f354d0d5da3bc0c45a8dbe0", "format": 1 }, { @@ -540,6 +540,13 @@ "format": 1 }, { + "name": "plugins/modules/na_ontap_cifs_unix_symlink_mapping.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ab6f70acbe8e7d14b1259d6dd26e11956a57518dc70282e91878e47f465ed7a4", + "format": 1 + }, + { "name": "plugins/modules/na_ontap_s3_users.py", "ftype": "file", "chksum_type": "sha256", @@ -564,7 +571,7 @@ "name": "plugins/modules/na_ontap_snapmirror.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "9514d382448a13c71a3ef577a9947937261f53c4f3d26249fad0e6c169244c9e", + "chksum_sha256": "ec388f54cdb2cbcf61799dce69ee71faaed10b9092208dda884863a06f48afdc", "format": 1 }, { @@ -599,7 +606,14 @@ "name": "plugins/modules/na_ontap_snapshot_policy.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "d950f392c6da9173d4a222357e2f36417739c9881a7592a70ef3b9d69c28429c", + "chksum_sha256": "135b0efb8be0c082d51d6d955fbf39ca27e49be3faf80513cf59517ee7fa9ac4", + "format": 1 + }, + { + "name": "plugins/modules/na_ontap_ems_config.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "678459ebfcbc2c441fd3bd2cada52550ffb65ba97ce6eb3f738ee65f8a393179", "format": 1 }, { @@ -648,7 +662,7 @@ "name": "plugins/modules/na_ontap_lun.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "5d44070a28af9662c623e174e9699de994fda2ce6b8626bcd6d3e61e6867a705", + "chksum_sha256": "17260d405983a1ff389ce146d8f30082a5ea67cf5f7fb23bf13cd7280fbfe5df", "format": 1 }, { @@ -711,7 +725,7 @@ "name": "plugins/modules/na_ontap_cifs_server.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "4287377fb2c9f65556b9dbf64ff6ec467f81fc5d2cc82ebb4c2c16ade1dc1de2", + "chksum_sha256": "5ea181714e685520b6682a5aa46520bb475b62ab1edc5cef967743c51685da67", "format": 1 }, { @@ -725,7 +739,7 @@ "name": "plugins/modules/na_ontap_info.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "4a1b8844f0d0f5438c159b44b7d11c6d8e3f2394aa0f6e033b7757b7b9a061b2", + "chksum_sha256": "49fc9dbbbc53dae9daa8af45b3e212357e0caee59cb82f04100404f0d745668b", "format": 1 }, { @@ -753,7 +767,7 @@ "name": "plugins/modules/na_ontap_service_policy.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "8ac05dab425cafd7de913f81014eb10d856b5b99612bb063cdd7ca4e5a0d063d", + "chksum_sha256": "809f043bcbf561f0adcb74920313a3953dff31ac2eab74548abbae25681c741c", "format": 1 }, { @@ -774,14 +788,14 @@ "name": "plugins/modules/na_ontap_login_messages.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "bb1da6df48e95a15f43e615f7d36a230f7540adc96aead7dc0d132682956fba1", + "chksum_sha256": "2e5a603d0202b5e29710c6a52de5f449135aaa43ebb7d7cb275ca2c0a066c123", "format": 1 }, { "name": "plugins/modules/na_ontap_file_security_permissions_acl.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ee2a8e5945e09845ad851ba4448083d9d6751f0f4ae98ff04372b15052c36ce7", + "chksum_sha256": "1aab00d75da338d0c475f412a76bc3e34f960a1f54a89665a4ff67ec882d30e1", "format": 1 }, { @@ -802,7 +816,7 @@ "name": "plugins/modules/na_ontap_s3_services.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "eb9b2ecd506650c6277c7db50399776c44de92a479085ded69e4d4be45fbb06b", + "chksum_sha256": "44a50f6d21125d571a8f115c9a4c15009c9a24aac7035da149ddc81443b6976d", "format": 1 }, { @@ -823,21 +837,21 @@ "name": "plugins/modules/na_ontap_storage_auto_giveback.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "24dffb7ebb9eacb949cd7dfbc9cb9ec35da18faa3176a239f1214da8282b66ee", + "chksum_sha256": "1ed26bbf1bfb4454ced206908e5256ad431093e0ea71919d5afb284d2937f3fb", "format": 1 }, { "name": "plugins/modules/na_ontap_name_mappings.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "83bf5f628ba9cdd88ba87d7af2344d97a290272b44a0acd1a658e46c86a6ce19", + "chksum_sha256": "e243d773c2cbc10d52e286e35771629416678be9daf28cd0fdc870870d5b620b", "format": 1 }, { "name": "plugins/modules/na_ontap_qos_policy_group.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "0b68a80bfac9ff1437326a1ac17bf296988ba6cf356d178970414623f608a9a1", + "chksum_sha256": "df651e1f1304ed4d27c17c9ac1332f5820a221419ba0239c3d7cb120fabef8f8", "format": 1 }, { @@ -851,7 +865,7 @@ "name": "plugins/modules/na_ontap_node.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "67b359d9c72f3768342f5ec483cf2d201e0b7c4cb02815d5f5e2e886d1411adc", + "chksum_sha256": "44e0b1c41de40c78af4f7b2b16bac92cb7e5214555524423750e10a69d4a37d6", "format": 1 }, { @@ -879,7 +893,7 @@ "name": "plugins/modules/na_ontap_cluster_peer.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "439ea7093e16deca7b055bb8b4577ee49fe1579c56560261a9a618a853756d37", + "chksum_sha256": "baffb6efea12760dbd074bd3604889b2c79dbf5255bc64628b5f3061c1cdbff1", "format": 1 }, { @@ -893,14 +907,14 @@ "name": "plugins/modules/na_ontap_nfs.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "c3f57bd199e8bb2bf6fc88de81540f4def926757d083abfafcefc3737885c674", + "chksum_sha256": "054ccb00eb7802c43a4e3c903f6d322b3fcac3cfbd0537c41a4edbdf783f8e57", "format": 1 }, { "name": "plugins/modules/na_ontap_igroup_initiator.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "5e0219793ff679105e7cdd12fb92ed998d7d0286fa4616881c35b48c20f93ca3", + "chksum_sha256": "b6e659c9d95211c61b000b1e0865cbeb478d2ffc2c365299a2716f63d0656fd7", "format": 1 }, { @@ -914,7 +928,7 @@ "name": "plugins/modules/na_ontap_broadcast_domain.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "6249740f48db9713cce0f3e639284a9cd58f589d13188161218fa36710589850", + "chksum_sha256": "068b01b68cb7b3de8c1c9c6a17082fed04d70ae6a4d3299854c08af1589631dc", "format": 1 }, { @@ -991,14 +1005,14 @@ "name": "plugins/modules/na_ontap_svm.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "5807c3522331a413d68c8c6a1852065ea500b4890a48e65708765431440edea1", + "chksum_sha256": "ffe2ffea83f07e00d9e5564a5699bb30700557f46a601fb364ff296b8af41db1", "format": 1 }, { "name": "plugins/modules/na_ontap_cg_snapshot.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "50138c1b5feda5cac7f79cfba886bb22837ee33c851f5e9975d3951df3c8927d", + "chksum_sha256": "a3d1673902619fa6191db5336c65fc32ad3fdc4154c694b2f9fe34357382a882", "format": 1 }, { @@ -1026,7 +1040,7 @@ "name": "plugins/modules/na_ontap_vscan_scanner_pool.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "3852ac303caf42cea6b6ff90f851fa78b51d400690edf3acc833ade94c63f365", + "chksum_sha256": "7d923fe9f87d5f90a0aa2155b16622974003b41e03457ed2718661aed84d66a9", "format": 1 }, { @@ -1037,10 +1051,17 @@ "format": 1 }, { + "name": "plugins/modules/na_ontap_snmp_config.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "34a3fc900f046f84fcfd9d4b669e454709a6b37cfeb07bcb98a025812a510e25", + "format": 1 + }, + { "name": "plugins/modules/na_ontap_cluster.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "45e9d6197ea7263e1a211b50cf63cffe0826e098d4b262c37b8d47eb2814f05f", + "chksum_sha256": "011dd065b2341c49bd7d07ab424462ea1fbbe309b3b408e7e0a20b07eac79d06", "format": 1 }, { @@ -1082,7 +1103,7 @@ "name": "plugins/modules/na_ontap_rest_info.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "2856157fe4f1c49354d226279e8b48e2307e85ab7c4aa2fb06d74406f4e6ff17", + "chksum_sha256": "830827828d6f15dc305f41df23f1fa48caf4cf9628fe9e60786b8fc82db69b29", "format": 1 }, { @@ -1110,7 +1131,7 @@ "name": "plugins/modules/na_ontap_export_policy_rule.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "6268758ca9e6bf19ecc2ec77874a5de0c52deeeb47a278b4cf59a10aa999333f", + "chksum_sha256": "b81f861d458e94935077bc7c8afd0ffdef57e4ff3869067ebd1434fe5011b00d", "format": 1 }, { @@ -1131,7 +1152,7 @@ "name": "plugins/modules/na_ontap_net_ifgrp.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "700b1c21b2fceb7004f7e21525df82b9bf75e5a0b0b09b743b880ba7ae9c88ac", + "chksum_sha256": "b1e80c8c376745d83c3b86ba490ea5138855b67924421ef1872108683c5daac1", "format": 1 }, { @@ -1142,6 +1163,13 @@ "format": 1 }, { + "name": "plugins/modules/na_ontap_cli_timeout.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "85e78370f6dbfe3f964300540582692b371721f00d061d027dca3f7c96bbe90f", + "format": 1 + }, + { "name": "plugins/modules/na_ontap_software_update.py", "ftype": "file", "chksum_type": "sha256", @@ -1152,7 +1180,7 @@ "name": "plugins/modules/na_ontap_kerberos_realm.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "80e39927bb3588e6d457eb6fe30d7693ebd174c5984bc04f289a317ce727bd19", + "chksum_sha256": "a71dac9c8eeabd098fdacdc6a72be8099085c2ce6bae671c623b7eb6274ccec8", "format": 1 }, { @@ -1173,7 +1201,7 @@ "name": "plugins/modules/na_ontap_ems_filter.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ab8386552e4c7a9b42cc436fb452086767d10405fbb4b1110e12b88f1d6a71d1", + "chksum_sha256": "1c792058420a84d67a0cc136de4a5a07fbd9ec8157eeb62d61a947b20522c70b", "format": 1 }, { @@ -1187,7 +1215,7 @@ "name": "plugins/modules/na_ontap_vserver_peer.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "47ec3d718b88fbe09abfc49a2d7c4896a11d8b1d2158ce1e55d636221f70f7d7", + "chksum_sha256": "2fc0315ff8bfd66b89822d4f75f779f1688df45eb5964bea58f263b48ace327a", "format": 1 }, { @@ -1208,7 +1236,7 @@ "name": "plugins/modules/na_ontap_snmp.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "bbb6b7ded15cacef91b478955e289e1dd3baa6a9723e7605872eaaf61d0fc56d", + "chksum_sha256": "7c09ad3d3be2a1de5e7dcc2668379dba75ab41d3295e7b1c34523de2749eb1aa", "format": 1 }, { @@ -1229,7 +1257,7 @@ "name": "plugins/modules/na_ontap_ems_destination.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "f1388814e0a0917e90b444c4e80339b90be71b0114af3c501312741cd7155b54", + "chksum_sha256": "5c02f2471aa67ec959883f5b57f6a83b5165eda45e17eedd25cde48860df6009", "format": 1 }, { @@ -1534,10 +1562,17 @@ "format": 1 }, { + "name": "tests/unit/plugins/modules/test_na_ontap_vscan_scanner_pool_rest.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "359f4dc18fcee6815f9d5bfcac47377d540e3206e1187e40537b1a4d95ac9f58", + "format": 1 + }, + { "name": "tests/unit/plugins/modules/test_na_ontap_rest_info.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "72fa0742c19761dd2be646cdbff2b537eb8659eb4be46a13082cd422c649288d", + "chksum_sha256": "23dcb44c70edf87aa7ba6ef5369be372a055dd28ded864fabb766e2175501350", "format": 1 }, { @@ -1558,7 +1593,7 @@ "name": "tests/unit/plugins/modules/test_na_ontap_ems_destination.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "a70b09add9149c9ad2c74f344c0f5bb7c8e2806c2989bb722e74395637e6e20c", + "chksum_sha256": "17c949a72cc7a5ccb0a10962caf81ca1b70c9b6492c9e799856b8fadc493c995", "format": 1 }, { @@ -1572,14 +1607,14 @@ "name": "tests/unit/plugins/modules/test_na_ontap_snapshot_policy_rest.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "28d0650df1636d66970ee053297cdb23a3bfc6477870c7a439c21f1c28e30572", + "chksum_sha256": "fe8ec853b68a9f132691799cd55be43f53635096868114ec86048f5ba713fa16", "format": 1 }, { "name": "tests/unit/plugins/modules/test_na_ontap_net_ifgrp.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "fdf9b1ee67ec49c2df619de50d4e2fe1ca0068792020e1abd6a3cc5101f1f4aa", + "chksum_sha256": "67fc692e2c26ab9213b3773525e0d37e899417231bb0325ce5c8dfa7e2ec6abe", "format": 1 }, { @@ -1618,6 +1653,13 @@ "format": 1 }, { + "name": "tests/unit/plugins/modules/test_na_ontap_cg_snapshot_rest.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d47ce7404e7ec493b6cae1240fc514af3d1da071dbf03e1b7b1e0cd6ac6a6cd9", + "format": 1 + }, + { "name": "tests/unit/plugins/modules/test_na_ontap_fpolicy_ext_engine.py", "ftype": "file", "chksum_type": "sha256", @@ -1663,14 +1705,14 @@ "name": "tests/unit/plugins/modules/test_na_ontap_cluster.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "c882a166465a3b80cf6edf4b333646a12a7fd78e5045ea59702d1c4458ecd6fa", + "chksum_sha256": "cc4f5c9ab06a5ab402e9c988f5ef28c21e2dcad7a1feae07bb098353086b39c7", "format": 1 }, { "name": "tests/unit/plugins/modules/test_na_ontap_svm.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "68d2fd805bd1adfbf387c926f4a7023644130be7cbd9b27a89cfa4b59456521c", + "chksum_sha256": "25e28f82eb025fd5b454fd61743a59e3964c09eab5edb0ccebfa31014f4b1da2", "format": 1 }, { @@ -1761,7 +1803,7 @@ "name": "tests/unit/plugins/modules/test_na_ontap_qos_policy_group.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "637d6977a9ad1258222b8201dc194e1ee712f81899fa7ed77376a2c3d524f759", + "chksum_sha256": "2f42e2bffa4d33b13f0aeb6b0024dcbd9a4b1ae4b1e16741d91c9808601bbcfd", "format": 1 }, { @@ -1789,7 +1831,7 @@ "name": "tests/unit/plugins/modules/test_na_ontap_cifs_server.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "20b53adc4d0ae871cd6c7431fb766bb7464d3210b79ec318dc44c9dcc3abe42e", + "chksum_sha256": "45279434fa84da11484d1f411788c68197321f105ae274916c92b448a4a27755", "format": 1 }, { @@ -1803,7 +1845,7 @@ "name": "tests/unit/plugins/modules/test_na_ontap_s3_services.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "eac0a91c90b250a0e5d4093d22e8837767199c4de718dc70854d94302ba85284", + "chksum_sha256": "e6511d83a915acf2aca33475322538f0916a188781d323f2031eecf63ad6bbaa", "format": 1 }, { @@ -1936,14 +1978,14 @@ "name": "tests/unit/plugins/modules/test_na_ontap_snapmirror.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "0f9a591c29a0560042a34c108dadd408dad7e053020f3fb79b08570fa0b28304", + "chksum_sha256": "f684c553b8e1fd7d00d6c1b11064077a8905f687293658b7af2b73bd013a9ff6", "format": 1 }, { "name": "tests/unit/plugins/modules/test_na_ontap_cluster_peer.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "23f42d9f923f12a2ce87fe0982d85ec2e6c663c174f6964bd89bc821db014e39", + "chksum_sha256": "20e0f925e07256a69fa8582075887048e84d8134eb27b5fade6fdb40893fb333", "format": 1 }, { @@ -2055,7 +2097,7 @@ "name": "tests/unit/plugins/modules/test_na_ontap_lun_rest.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ce572f17103f660e31dfda9bc5ece970aa6cf4a4f349e080ea5c8552d1dded9b", + "chksum_sha256": "7624b91ca479b285e4004fd6fd466e47e465419651e3bfc56b4fce046a267ca6", "format": 1 }, { @@ -2195,14 +2237,14 @@ "name": "tests/unit/plugins/modules/test_na_ontap_restit.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "338d1436f4e001717dee8e2a7276c6f8d061c4ce5a56aa2bdac670da41642e8a", + "chksum_sha256": "7e6dc7ac01bfeb65d57d57ea0e3e0ecaea1af52b6930b28a6574c6f3fa9bfbaf", "format": 1 }, { "name": "tests/unit/plugins/modules/test_na_ontap_volume.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "23c30dbaacad0f9ea68d368fd88b0459ca4b7ea458fec5c4592c945aa97ebfbb", + "chksum_sha256": "8bb5fd6d6597cae989691f5c3de7683728ba99bc9409247e65afe248d1d26ae9", "format": 1 }, { @@ -2220,6 +2262,13 @@ "format": 1 }, { + "name": "tests/unit/plugins/modules/test_na_ontap_cli_timeout_rest.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8c771796f7882ca87a496067a6ec08fe423b739f875112148535482ee7b80d5a", + "format": 1 + }, + { "name": "tests/unit/plugins/modules/test_na_ontap_unix_user.py", "ftype": "file", "chksum_type": "sha256", @@ -2251,7 +2300,7 @@ "name": "tests/unit/plugins/modules/test_na_ontap_kerberos_realm.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "45a10a5f869386b7a112d2d10da5bcfa9679537714fad526e170fbe883befb06", + "chksum_sha256": "194266b02394b5e2fb9dc8af45cdde6fedc4b31fa9f46229ce368a869f02f55a", "format": 1 }, { @@ -2314,7 +2363,7 @@ "name": "tests/unit/plugins/modules/test_na_ontap_security_certificates.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "cc1820aeab587ca1f867c83ec5cbbffdbe04e949434b42cb70736e21008c9638", + "chksum_sha256": "e773a4de1c948991b07d74ffb7d4a4db6d2eaa4cbfca2e5c5fb53699507dde14", "format": 1 }, { @@ -2405,7 +2454,7 @@ "name": "tests/unit/plugins/modules/test_na_ontap_info.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "260c1c6a7eab1af937ba0364f015209ce0a510782e8534f94a2e01c8dcdeac2e", + "chksum_sha256": "f28b5b1d03e66d1128b0c00bd29f19fba1ce2a9e5760417536d532c2ca50bf76", "format": 1 }, { @@ -2440,7 +2489,7 @@ "name": "tests/unit/plugins/modules/test_na_ontap_ems_filter.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "a6bc41fcd86c0af36511a2a59f3565fe8d3bffb6c6acd0308a2096d1bfcbc8ff", + "chksum_sha256": "d104d1c4b8e0a072d06d45f32b2acddbcb1c0f0fe108dce504e85fe4b04dbaf2", "format": 1 }, { @@ -2468,7 +2517,7 @@ "name": "tests/unit/plugins/modules/test_na_ontap_volume_rest.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "6703a6c2f9c4a9f23a33f04a03692d8ade5f6401c6eff2a71f22106a2379485a", + "chksum_sha256": "12fc54ccef4180bf9755e61c0396f0436aa251c321cb4742f0e7729c0165d795", "format": 1 }, { @@ -2489,7 +2538,7 @@ "name": "tests/unit/plugins/modules/test_na_ontap_snmp.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "2745af5002bd12d7f4fe61c270684cce7345e251aca858ac4307ed867febccce", + "chksum_sha256": "6cf653c7019017da57bade6b514480d4643fe83c76cf47f9a939262c74b742ba", "format": 1 }, { @@ -2605,6 +2654,13 @@ "format": 1 }, { + "name": "tests/unit/plugins/modules/test_na_ontap_ems_config_rest.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8fa4366c96d911631e396a2c67d3615bb9bbae4fe6cbe803036886bdddffb04c", + "format": 1 + }, + { "name": "tests/unit/plugins/modules/test_na_ontap_efficiency_policy.py", "ftype": "file", "chksum_type": "sha256", @@ -2612,6 +2668,13 @@ "format": 1 }, { + "name": "tests/unit/plugins/modules/test_na_ontap_cifs_unix_symlink_mapping_rest.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b14380254416e50cb587ac3bf0a78cd7c0e02c3f6066c2d358917212a65b78aa", + "format": 1 + }, + { "name": "tests/unit/plugins/modules/test_na_ontap_nvme_subsystem_rest.py", "ftype": "file", "chksum_type": "sha256", @@ -2619,6 +2682,13 @@ "format": 1 }, { + "name": "tests/unit/plugins/modules/test_na_ontap_snmp_config_rest.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4f63c18b52b5f76ea166bd535f60b059dcba23281635f364d44498f5b29d1205", + "format": 1 + }, + { "name": "tests/unit/plugins/modules/test_na_ontap_security_ipsec_config.py", "ftype": "file", "chksum_type": "sha256", @@ -2685,7 +2755,7 @@ "name": "tests/unit/plugins/modules/test_na_ontap_dns.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "8d9a65c36d2f55ca4cf0f36774515f5dda2c1c1e697a9d3e5ce640710da29fb7", + "chksum_sha256": "bb8275c1ae29ba059dd5897cb47b174da8b59f8a5acf64b241061f219bc46bbe", "format": 1 }, { @@ -2811,7 +2881,7 @@ "name": "meta/runtime.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "1038889fbba0e97c200632faf2549ec6194fc13ffb08af2402cbf52e3b6e6419", + "chksum_sha256": "f8d72f0da49c6fab626aa540d1571b47ee1846ac66613f549476a280d7b1336f", "format": 1 }, { @@ -3511,7 +3581,7 @@ "name": "roles/na_ontap_vserver_create/README.md", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "1df8af3622b0b165fbf5b40544e44bd9cd5cbed5ac3703cfdf75bd82780f4696", + "chksum_sha256": "8ca3472b8eb89ba0488c9db5483a424b73d18644cd9da6d866be9989d3243d40", "format": 1 }, { @@ -3844,6 +3914,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6486.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8501087711c8d0a717eae19f38765219b99347282b61ac3e2edb624dccf04455", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-6193.yaml", "ftype": "file", "chksum_type": "sha256", @@ -3970,6 +4047,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6664.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8cbcbe2a91705f20c30c285ba2095a54bbafe084a71c1af755fa8dde80524a6f", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-3870.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4019,6 +4103,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6330.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "201ca10262d5eab95c3fc97f99262e9a57e583abd5f7937d4542d04b7eed99a3", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-5137.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4040,6 +4131,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6520.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "335a825f4ffff4e206ff19a22cf93cff29b62b8c7aa72857ac04821ba908cc0d", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-3667.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4257,6 +4355,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6331.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5e348511a61968bcb2c4058bf1222cc5c73a502a7cf82014469901aa3996cf87", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-4863.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4264,6 +4369,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6389.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ec9c6bf3a6512a79921920419b89e0069040d3c21ccba337aa793c09d5a2bdd8", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-5819.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4390,6 +4502,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6487.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "40544537955d5d121a0ff78e06622def58f8ab13b9aee5704eda13390ebcc44c", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-6192.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4488,6 +4607,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6413.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b3b104e725a081a83482888394ef8434e868fe83a42380b007bc848755e1d879", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-3241.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4495,6 +4621,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6556.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6cc7a08f69b9c2911c0c82a0e78c6ff76065eada33086b772525d87143139eca", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-3754.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4621,6 +4754,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6320.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "adab447f548a1ce10c9ff2fd87843922b59defe7b050e27e3de9c783391fe319", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-5367.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4705,6 +4845,20 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6658.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "94c46027fed496fcc075c2cfad38544afbd1c9267505820e38ca805653eb7830", + "format": 1 + }, + { + "name": "changelogs/fragments/DEVOPS-6463.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "aff00f447b5affc307da06fa4dcdabb88809c22991a37619f7f81d0903ee3fc8", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-5536.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4712,6 +4866,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-5920.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "de68834e39c654fb7465a20dd205422d18f181a9e180add39e92bd0ae10e5616", + "format": 1 + }, + { "name": "changelogs/fragments/20.5.0.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4796,6 +4957,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6286.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3f2ed4d85e7ad1b07891d340937dde28cd349352035618936e9dc80bc5d95e74", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-6005.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4831,6 +4999,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6681.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a2e99c55ceb7e3a9cd2fb477fc558cbe562bcea7d43b1dffd070209fb8cd7b14", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-3483.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4880,6 +5055,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6551.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "24db9206f4b74acf6aaa5f7974bae86f09f5c593732e79292cc99a0c4d3dbd45", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-4612.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4908,6 +5090,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6341.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9f81356bf55d57304b88dbed04f042aa6ca98a478d45912688fb415321f5b306", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-4393.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4936,6 +5125,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6438.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9d4d2898f9c8b4d302a839a09d2eb4b22daa79f3d0830d84e821c94314aaaba6", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-3952.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4978,6 +5174,13 @@ "format": 1 }, { + "name": "changelogs/fragments/GITHUB-174.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "da11debbbd55d35ad98c2d91791c39cb59b4de0608bfe0c86fdeaed4bf77a38e", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-4501.yaml", "ftype": "file", "chksum_type": "sha256", @@ -4999,6 +5202,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-5828.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "42f9f947f7fbc8da5d70d7e6e8be912cead1fa4a62a6700a02cb70d752a6e4e0", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-5594.yaml", "ftype": "file", "chksum_type": "sha256", @@ -5006,6 +5216,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6584.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d744ee25e6289ef5a971c20a4c4565a61d4cc1c088ee383132d2032169e75e31", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-4804.yaml", "ftype": "file", "chksum_type": "sha256", @@ -5062,6 +5279,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6481.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8c892112bc0b8d175b908bc766a6941bdf53a9c7b010bcd810e003eb9ec97a80", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-3400.yaml", "ftype": "file", "chksum_type": "sha256", @@ -5097,6 +5321,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6291.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "49832d5b326a49b2aa2d9fe56a98fee74611bf016841a34649950fd1a59687c4", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-5844.yaml", "ftype": "file", "chksum_type": "sha256", @@ -5153,6 +5384,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6395.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9d8701abe85f3af6c8cd41c29ce66a757649aca0a64b15f97f1aec85b48e81e5", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-5413.yaml", "ftype": "file", "chksum_type": "sha256", @@ -5160,6 +5398,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6680.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7cad34c82bfe8ac487c7e4211e0c2bb2acabb3eea4f8a14f0899a99423f39ff9", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-3178.yaml", "ftype": "file", "chksum_type": "sha256", @@ -5307,6 +5552,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6527.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f7ac99e8373f04d01ad44515766e01bb2ed13ce522690e8a3fc75866b121f86e", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-3230.yaml", "ftype": "file", "chksum_type": "sha256", @@ -5468,6 +5720,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6524.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f6653f1f9107e7a4721e6330daa02e3b79378fea7e03e644358bae328c4e0617", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-5426.yaml", "ftype": "file", "chksum_type": "sha256", @@ -5608,6 +5867,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6528.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d7c1b5e6f6cb4cc915546a255b948647fccbdd78cec968e51fc6a35d48ef2ace", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-5304.yaml", "ftype": "file", "chksum_type": "sha256", @@ -5853,6 +6119,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6529.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "22a6c32bc547a62313297446b9b11efa3ae6e52ea9be36b4812af57265eb8853", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-5190.yaml", "ftype": "file", "chksum_type": "sha256", @@ -5881,6 +6154,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6495.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "73571800ca68c39cac5dda9bb4e8158fd08e878d6bded4f764264430e6604c61", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-3443.yaml", "ftype": "file", "chksum_type": "sha256", @@ -5965,6 +6245,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6374.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9f844402377378d6aefbbe2156ad70fc80b5ef0c7c7757b07779094315447e6c", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-4349.yaml", "ftype": "file", "chksum_type": "sha256", @@ -6021,6 +6308,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6525.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a9de53d77edc2ab90ec35de395d790b32df1b7017e9421ba976b8e50336da63f", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-4774.yaml", "ftype": "file", "chksum_type": "sha256", @@ -6091,6 +6385,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6646.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "21f63e47a2639f35a8e18feb70723d56da0877bd61b307517396ab08191ff00e", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-5986.yaml", "ftype": "file", "chksum_type": "sha256", @@ -6231,6 +6532,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6386.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9b8c6632ed25f15a3801442efba11713262c649d33b6168f79e21977033d7fdf", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-5380.yaml", "ftype": "file", "chksum_type": "sha256", @@ -6273,6 +6581,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6282.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0c029a1405ca1cd34ac7be9a6575a210c516082eaaf68cdbe6ba9684fcacf29e", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-4745.yaml", "ftype": "file", "chksum_type": "sha256", @@ -6406,6 +6721,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6488.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "eee19d3fbc9bde48ac0ee2093b4409f5f3777b3df319533a33c6be3bc3fff42d", + "format": 1 + }, + { "name": "changelogs/fragments/20.8.0.yaml", "ftype": "file", "chksum_type": "sha256", @@ -6511,6 +6833,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6519.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "83316031d7862cc7ae638f327b70e0ca8dcf989e3edb14b93183d61033efa79a", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-5270.yaml", "ftype": "file", "chksum_type": "sha256", @@ -6532,6 +6861,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-5509.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e04b3b08214c3e5b2c6a86aabe982ec1b4982f41d5d5eefc8b0ca31ff7d4a70a", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-4573.yaml", "ftype": "file", "chksum_type": "sha256", @@ -6553,6 +6889,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6667.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d442b6c0e1bad182b4bca72950581ea0d54c9f04e1a13f1879904a647e4925c9", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-5948.yaml", "ftype": "file", "chksum_type": "sha256", @@ -6588,6 +6931,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6671.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "86e8491366c89fb30b62b71a615b901ef8b640d4710165a82fe7e705168ee3bf", + "format": 1 + }, + { "name": "changelogs/fragments/20.6.0.yaml", "ftype": "file", "chksum_type": "sha256", @@ -6679,6 +7029,13 @@ "format": 1 }, { + "name": "changelogs/fragments/DEVOPS-6309.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "47434bb4ef9be1fc80a9ca0880b6cfe17adaaa00af912c44aac704f9f4173cc6", + "format": 1 + }, + { "name": "changelogs/fragments/DEVOPS-4764.yaml", "ftype": "file", "chksum_type": "sha256", @@ -6934,14 +7291,14 @@ "name": "changelogs/changelog.yaml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "36885965bfb781446612c16dc008aa38596609693958a42b92e4fb6b760f1a95", + "chksum_sha256": "4c5611a9741bbfaaa7bc6fde286865b864f1c3c183327902ad3a5558b98903a0", "format": 1 }, { "name": "README.md", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "8bc903a9f9e960e83709868d4e500ec990596a01b84de2ac5a2541df22497f8f", + "chksum_sha256": "0453a86d933c48e94a50eb91635f95a6b1b53be67f320647c7ff5e00234f6e4a", "format": 1 }, { @@ -6983,7 +7340,7 @@ "name": ".github/workflows/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "a63778d527b7f382ebb71cde6c15af7fd2c08200b52166eb965e2e41ad107128", + "chksum_sha256": "81717f13c4cc656d1cb9ed378b8b61f0da71b488679a11bfb36b3b52251ce37a", "format": 1 }, { @@ -7004,14 +7361,14 @@ "name": ".github/ISSUE_TEMPLATE/bug_report.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "0a52c539892ce8413167fb8a94b7c299efe4727087fa0478b76c0c92c1f18cce", + "chksum_sha256": "073ca54b3b4421d1f071a39ecdea38cc3fbf8b9501edce99ca15d1247b249982", "format": 1 }, { "name": "CHANGELOG.rst", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "50916c09a55828383e593285e59044d240349554c2a54eed9ccbe367d97b7d6a", + "chksum_sha256": "edab2dd435af86cb61fc7a7f112d3136ae05a9a2340e4bd7bff546764beb0f2f", "format": 1 } ], diff --git a/ansible_collections/netapp/ontap/MANIFEST.json b/ansible_collections/netapp/ontap/MANIFEST.json index 333653f31..c066583ca 100644 --- a/ansible_collections/netapp/ontap/MANIFEST.json +++ b/ansible_collections/netapp/ontap/MANIFEST.json @@ -2,7 +2,7 @@ "collection_info": { "namespace": "netapp", "name": "ontap", - "version": "22.7.0", + "version": "22.10.0", "authors": [ "NetApp Ansible Team <ng-ansibleteam@netapp.com>" ], @@ -25,7 +25,7 @@ "name": "FILES.json", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "5fcf72da57259f9f93e9dd46099622ff04e193cef08c9aacedb13253ce404b31", + "chksum_sha256": "3b1ef023a88302b05998107de07d042c2efd877b22839f39bfd038585e6a7e35", "format": 1 }, "format": 1 diff --git a/ansible_collections/netapp/ontap/README.md b/ansible_collections/netapp/ontap/README.md index f086b09e9..cfad4cbcf 100644 --- a/ansible_collections/netapp/ontap/README.md +++ b/ansible_collections/netapp/ontap/README.md @@ -24,7 +24,7 @@ collections: - netapp.ontap ``` # Requirements -- ansible version >= 2.9 +- ansible version >= 7.0 - requests >= 2.20 - netapp-lib version >= 2018.11.13 @@ -56,13 +56,95 @@ return values differently than ZAPI you will need to update your playbooks to wo ### Deprecated Modules The following modules do not have REST equivalent APIs. They will stop working on any ONTAP release after CY22-Q4 release. - - na_ontap_cg_snapshot - na_ontap_file_directory_policy - na_ontap_svm_options - na_ontap_quota_policy # Release Notes +## 22.10.0 + +### Minor Changes + - na_ontap_rest_info - added warning message if given subset doesn't support option `owning_resource`. + - na_ontap_export_policy_rule - added `actions` and `modify` in module output. + - na_ontap_file_security_permissions_acl - added `actions` and `modify` in module output. + - na_ontap_igroup_initiator - added `actions` in module output. + - na_ontap_name_mappings - added `actions` and `modify` in module output. + - na_ontap_node - added `modify` in module output. + - na_ontap_lun_map - added `actions` in module output. + - na_ontap_lun_map_reporting_nodes - added `actions` in module output. + - na_ontap_storage_auto_giveback - added information on modifed attributes in module output. + - na_ontap_vscan_scanner_pool - added REST support to Vscan Scanner Pools Configuration module, requires ONTAP 9.6 or later. + - na_ontap_cifs_server - new option `is_multichannel_enabled` added in REST, requires ONTAP 9.10 or later. + +### Bug Fixes + - na_ontap_igroup_initiator - fixed issue with idempotency. + +## 22.9.0 + +### New Options + - na_ontap_cifs_server - new option `lm_compatibility_level` added in REST, requires ONTAP 9.8 or later. + - na_ontap_cluster - new option `certificate.uuid` added in REST, requires ONTAP 9.10 or later. + - na_ontap_ems_destination - new options `syslog`, `port`, `transport`, `message_format`, `timestamp_format_override` and `hostname_format_override` added in REST, requires ONTAP 9.12.1 or later. + +### Minor Changes + - na_ontap_s3_services - create, modify S3 service returns `s3_service_info` in module output. + - na_ontap_nfs - fix error with `windows` in REST for ONTAP 9.10 or earlier. + - na_ontap_cluster_peer - added REST only support for modifying remote intercluster addresses in cluster peer relation. + - na_ontap_snapmirror - updated resync and resume operation for synchronous snapmirror relationship in REST. + +### Bug Fixes + - na_ontap_snapshot_policy - fix issue with modifying snapshot policy in REST. + - na_ontap_volume - modified `type` to be case insensitive in REST. + - na_ontap_security_certificates - fix error with ontap_info returned in module output in REST. + +### New Modules + - na_ontap_snmp_config - REST only support for modifying SNMP configuration, requires ONTAP 9.7 or later. + - na_ontap_cli_timeout - REST only support for setting CLI inactivity timeout value, requires ONTAP 9.6 or later. + - na_ontap_cifs_unix_symlink_mapping - REST only support for managing UNIX symbolic link mapping for CIFS clients, requires ONTAP 9.6 or later. + +## 22.8.3 + +### Bug Fixes + - na_ontap_vserver_peer - fix issue with peering multiple clusters with same vserver name in REST. + - na_ontap_ems_destination - fix field error with `certificate.name` for ONTAP 9.11.1 or later in REST. + - na_ontap_snmp - fix for getting error when `authentication_method` set to default with ZAPI. + +## 22.8.1 + +### Bug Fixes + - na_ontap_dns - fix keyerror for uuid when DNS is set to vserver in REST. + - na_ontap_volume - fix invalid field error with 'space.snapshot.autodelete' in REST. + +## 22.8.0 + +### New Options + - na_ontap_lun - new option `qtree_name` added in REST. + - na_ontap_rest_info - new option `hal_linking` added. + - na_ontap_cifs_server - new option `default_site` added in REST, requires ONTAP 9.13.1 or later. + - na_ontap_ems_destination - new option `certificate`, `ca` added. + - na_ontap_volume - added new REST only options `vol_nearly_full_threshold_percent` and `vol_full_threshold_percent`, requires ONTAP 9.9 or later. + - na_ontap_qos_policy_group - added new REST only options `expected_iops_allocation` and `peak_iops_allocation`, requires ONTAP 9.10.1 or later. + +### Minor Changes + - na_ontap_user - added warning message when password is not changed. + - na_ontap_restit - returns changed as False for GET method. + - na_ontap_kerberos_realm - added REST support for `admin_server_ip`, `admin_server_port`, `pw_server_ip`, `pw_server_port` and `clock_skew`, requires ONTAP 9.13.1 or later. + - na_ontap_volume - added REST support for `atime_update` requires ONTAP 9.8 or later, `snapdir_access` and `snapshot_auto_delete` requires ONTAP 9.13.1 or later. + - na_ontap_net_ifgrp - return `name` and other details of a newly created interface group in module output in REST. + - na_ontap_cg_snapshot - added REST support to the cg snapshot module, requires ONTAP 9.10.1 or later. + - na_ontap_snmp - added REST support for `snmpv3` user. + +### Bug Fixes + - na_ontap_nfs - fix `default_user` under `windows` not getting modified, if not set previously, in REST. + - na_ontap_dns - fix DNS not working with Cluster mode. + - na_ontap_ems_filter - fix modify operation to add rule in existing filter. + - na_ontap_svm - fix REST version warning for `ndmp` under `services`. + - na_ontap_login_messages - fix idempotency issue in Cluster scope in REST. + +### New Modules + - na_ontap_ems_config - REST only support for modifying EMS configuration, requires ONTAP 9.6 or later. + ## 22.7.0 ### New Options diff --git a/ansible_collections/netapp/ontap/changelogs/changelog.yaml b/ansible_collections/netapp/ontap/changelogs/changelog.yaml index d850c7337..8d0eb138e 100644 --- a/ansible_collections/netapp/ontap/changelogs/changelog.yaml +++ b/ansible_collections/netapp/ontap/changelogs/changelog.yaml @@ -2735,6 +2735,37 @@ releases: name: na_ontap_security_ipsec_policy namespace: '' release_date: '2022-12-07' + 22.10.0: + changes: + bugfixes: + - na_ontap_igroup_initiator - fixed issue with idempotency. + minor_changes: + - na_ontap_cifs_server - new option `is_multichannel_enabled` added in REST, + requires ONTAP 9.10 or later. + - na_ontap_export_policy_rule - added `actions` and `modify` in module output. + - na_ontap_file_security_permissions_acl - added `actions` and `modify` in module + output. + - na_ontap_igroup_initiator - added `actions` in module output. + - na_ontap_lun_map - added `actions` in module output. + - na_ontap_lun_map_reporting_nodes - added `actions` in module output. + - na_ontap_name_mappings - added `actions` and `modify` in module output. + - na_ontap_node - added `modify` in module output. + - na_ontap_rest_info - added warning message if given subset doesn't support + option `owning_resource`. + - na_ontap_storage_auto_giveback - added information on modified attributes + in module output. + - na_ontap_vscan_scanner_pool - added REST support to Vscan Scanner Pools Configuration + module, requires ONTAP 9.6 or later. + fragments: + - DEVOPS-6584.yaml + - DEVOPS-6646.yaml + - DEVOPS-6658.yaml + - DEVOPS-6664.yaml + - DEVOPS-6667.yaml + - DEVOPS-6671.yaml + - DEVOPS-6680.yaml + - DEVOPS-6681.yaml + release_date: '2024-02-06' 22.2.0: changes: bugfixes: @@ -3033,3 +3064,129 @@ releases: name: na_ontap_active_directory_domain_controllers namespace: '' release_date: '2023-06-09' + 22.8.0: + changes: + bugfixes: + - na_ontap_dns - fix DNS not working with Cluster mode. + - na_ontap_ems_filter - fix delete operation not idempotent for filter. + - na_ontap_ems_filter - fix modify operation to add rule in existing filter. + - na_ontap_login_messages - fix idempotency issue in Cluster scope in REST. + - na_ontap_nfs - fix `default_user` under `windows` not getting modified if + not set previously in REST. + - na_ontap_svm - fix REST version warning for `ndmp` under `services`. + minor_changes: + - na_ontap_broadcast_domain - changed documentation for ipspace as it is required + while using REST. + - na_ontap_cg_snapshot - added REST support to the cg snapshot module, requires + ONTAP 9.10.1 or later. + - na_ontap_cifs_server - new option `default_site` added in REST, requires ONTAP + 9.13.1 or later. + - na_ontap_ems_destination - new option ``certificate``, ``ca`` added. + - na_ontap_kerberos_realm - add REST support for `admin_server_ip`, `admin_server_port`, + `pw_server_ip`, `pw_server_port` and `clock_skew` from ONTAP 9.13.1 or later + - na_ontap_lun - new option `qtree_name` added in REST. + - na_ontap_net_ifgrp - return `name` and other details of a newly created interface + group in module output in REST. + - na_ontap_qos_policy_group - added new REST only options `expected_iops_allocation` + and `peak_iops_allocation`, requires ONTAP 9.10.1 or later. + - na_ontap_rest_info - new option `hal_linking` added to enable or disable HAL + links. + - na_ontap_restit - returns changed as False for GET method. + - na_ontap_snmp - added REST support for snmpv3 user. + - na_ontap_user - Added warning message when password is not changed. + - na_ontap_volume - added REST support for `atime_update` requires ONTAP 9.8 + or later, `snapdir_access` and `snapshot_auto_delete` requires ONTAP 9.13.1 + or later. + - na_ontap_volume - added new REST only options `vol_nearly_full_threshold_percent` + and `vol_full_threshold_percent`, requires ONTAP 9.9 or later. + fragments: + - DEVOPS-5509.yaml + - DEVOPS-5828.yaml + - DEVOPS-6286.yaml + - DEVOPS-6291.yaml + - DEVOPS-6309.yaml + - DEVOPS-6320.yaml + - DEVOPS-6330.yaml + - DEVOPS-6331.yaml + - DEVOPS-6341.yaml + - DEVOPS-6374.yaml + - DEVOPS-6386.yaml + - DEVOPS-6395.yaml + - DEVOPS-6413.yaml + - DEVOPS-6438.yaml + - DEVOPS-6463.yaml + - DEVOPS-6481.yaml + - DEVOPS-6486.yaml + - DEVOPS-6488.yaml + - DEVOPS-6495.yaml + - GITHUB-174.yaml + modules: + - description: NetApp ONTAP module to modify EMS configuration. + name: na_ontap_ems_config + namespace: '' + release_date: '2023-11-01' + 22.8.1: + changes: + bugfixes: + - na_ontap_dns - fix keyerror for uuid when DNS is set to vserver in REST. + - na_ontap_volume - fix invalid field error with 'space.snapshot.autodelete' + in REST. + fragments: + - DEVOPS-6519.yaml + - DEVOPS-6520.yaml + release_date: '2023-11-07' + 22.8.3: + changes: + bugfixes: + - na_ontap_ems_destination - fix field error with `certificate.name` for ONTAP + 9.11.1 or later in REST. + - na_ontap_vserver_peer - fix issue with peering multiple clusters with same + vserver name in REST. + fragments: + - DEVOPS-6527.yaml + - DEVOPS-6528.yaml + release_date: '2023-11-16' + 22.9.0: + changes: + bugfixes: + - na_ontap_nfs - fix error with `windows` in REST for ONTAP 9.10 or earlier. + - na_ontap_security_certificates - fix error with ontap_info returned in module + output in REST. + - na_ontap_snapshot_policy - fix issue with modifying snapshot policy in REST. + - na_ontap_volume - modified `type` to be case insensitive in REST. + minor_changes: + - na_ontap_cifs_server - new option `lm_compatibility_level` added in REST, + requires ONTAP 9.8 or later. + - na_ontap_cluster - new option `certificate.uuid` added in REST, requires ONTAP + 9.10 or later. + - na_ontap_cluster_peer - added REST only support for modifying remote intercluster + addresses in cluster peer relation. + - na_ontap_ems_destination - new options `syslog`, `port`, `transport`, `message_format`, + `timestamp_format_override` and `hostname_format_override` added in REST, + requires ONTAP 9.12.1 or later. + - na_ontap_s3_services - create, modify S3 service returns `s3_service_info` + in module output. + - na_ontap_snapmirror - updated resync and resume operation for synchronous + snapmirror relationship in REST. + fragments: + - DEVOPS-5920.yaml + - DEVOPS-6282.yaml + - DEVOPS-6389.yaml + - DEVOPS-6487.yaml + - DEVOPS-6524.yaml + - DEVOPS-6525.yaml + - DEVOPS-6529.yaml + - DEVOPS-6551.yaml + - DEVOPS-6556.yaml + modules: + - description: NetApp ONTAP module to manage UNIX symbolic link mapping for CIFS + clients. + name: na_ontap_cifs_unix_symlink_mapping + namespace: '' + - description: NetApp ONTAP module to set the CLI inactivity timeout value. + name: na_ontap_cli_timeout + namespace: '' + - description: NetApp ONTAP module to modify SNMP configuration. + name: na_ontap_snmp_config + namespace: '' + release_date: '2024-01-03' diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-5509.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-5509.yaml new file mode 100644 index 000000000..4e3c20584 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-5509.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_volume - added new REST only options `vol_nearly_full_threshold_percent` and `vol_full_threshold_percent`, requires ONTAP 9.9 or later.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-5828.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-5828.yaml new file mode 100644 index 000000000..c06bfa58b --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-5828.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_cg_snapshot - added REST support to the cg snapshot module, requires ONTAP 9.10.1 or later.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-5920.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-5920.yaml new file mode 100644 index 000000000..ef47ace18 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-5920.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_cluster_peer - added REST only support for modifying remote intercluster addresses in cluster peer relation.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6282.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6282.yaml new file mode 100644 index 000000000..319ec4d92 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6282.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_snapmirror - updated resync and resume operation for synchronous snapmirror relationship in REST.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6286.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6286.yaml new file mode 100644 index 000000000..74fc7e76a --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6286.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_user - Added warning message when password is not changed. diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6291.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6291.yaml new file mode 100644 index 000000000..b21fe8dbc --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6291.yaml @@ -0,0 +1,2 @@ +bugfixes: + - na_ontap_ems_filter - fix modify operation to add rule in existing filter.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6309.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6309.yaml new file mode 100644 index 000000000..963655759 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6309.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_restit - returns changed as False for GET method.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6320.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6320.yaml new file mode 100644 index 000000000..a0ec3ad6f --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6320.yaml @@ -0,0 +1,2 @@ +bugfixes: + - na_ontap_nfs - fix `default_user` under `windows` not getting modified if not set previously in REST.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6330.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6330.yaml new file mode 100644 index 000000000..9d2d9e0f5 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6330.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_lun - new option `qtree_name` added in REST.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6331.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6331.yaml new file mode 100644 index 000000000..18e06be95 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6331.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_cifs_server - new option `default_site` added in REST, requires ONTAP 9.13.1 or later.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6341.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6341.yaml new file mode 100644 index 000000000..821f89652 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6341.yaml @@ -0,0 +1,2 @@ +bugfixes: + - na_ontap_dns - fix DNS not working with Cluster mode.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6374.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6374.yaml new file mode 100644 index 000000000..ef79605d5 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6374.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_kerberos_realm - add REST support for `admin_server_ip`, `admin_server_port`, `pw_server_ip`, `pw_server_port` and `clock_skew` from ONTAP 9.13.1 or later
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6386.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6386.yaml new file mode 100644 index 000000000..408039214 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6386.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_rest_info - new option `hal_linking` added to enable or disable HAL links.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6389.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6389.yaml new file mode 100644 index 000000000..785e06a49 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6389.yaml @@ -0,0 +1,2 @@ +bugfixes: + - na_ontap_snapshot_policy - fix issue with modifying snapshot policy in REST.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6395.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6395.yaml new file mode 100644 index 000000000..79fb05b5b --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6395.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_volume - added REST support for `atime_update` requires ONTAP 9.8 or later, `snapdir_access` and `snapshot_auto_delete` requires ONTAP 9.13.1 or later.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6413.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6413.yaml new file mode 100644 index 000000000..2a7e8b162 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6413.yaml @@ -0,0 +1,5 @@ +minor_changes: + - na_ontap_snmp - added REST support for snmpv3 user. + +bugfixes: + - na_ontap_snmp - fix for getting error when `authentication_method` set to default with ZAPI.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6438.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6438.yaml new file mode 100644 index 000000000..91a8562ff --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6438.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_net_ifgrp - return `name` and other details of a newly created interface group in module output in REST.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6463.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6463.yaml new file mode 100644 index 000000000..fb997b30f --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6463.yaml @@ -0,0 +1,2 @@ +bugfixes: + - na_ontap_ems_filter - fix delete operation not idempotent for filter.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6481.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6481.yaml new file mode 100644 index 000000000..231a5a903 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6481.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_broadcast_domain - changed documentation for ipspace as it is required while using REST.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6486.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6486.yaml new file mode 100644 index 000000000..a44334ea3 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6486.yaml @@ -0,0 +1,2 @@ +bugfixes: + - na_ontap_svm - fix REST version warning for `ndmp` under `services`.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6487.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6487.yaml new file mode 100644 index 000000000..894e1bad1 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6487.yaml @@ -0,0 +1,2 @@ +bugfixes: + - na_ontap_volume - modified `type` to be case insensitive in REST.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6488.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6488.yaml new file mode 100644 index 000000000..2060d5133 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6488.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_qos_policy_group - added new REST only options `expected_iops_allocation` and `peak_iops_allocation`, requires ONTAP 9.10.1 or later.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6495.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6495.yaml new file mode 100644 index 000000000..c5797f855 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6495.yaml @@ -0,0 +1,2 @@ +bugfixes: + - na_ontap_login_messages - fix idempotency issue in Cluster scope in REST.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6519.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6519.yaml new file mode 100644 index 000000000..f3e4c19bd --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6519.yaml @@ -0,0 +1,2 @@ +bugfixes: + - na_ontap_volume - fix invalid field error with 'space.snapshot.autodelete' in REST.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6520.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6520.yaml new file mode 100644 index 000000000..ca91b16a9 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6520.yaml @@ -0,0 +1,2 @@ +bugfixes: + - na_ontap_dns - fix keyerror for uuid when DNS is set to vserver in REST.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6524.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6524.yaml new file mode 100644 index 000000000..2d96a6530 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6524.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_ems_destination - new options `syslog`, `port`, `transport`, `message_format`, `timestamp_format_override` and `hostname_format_override` added in REST, requires ONTAP 9.12.1 or later.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6525.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6525.yaml new file mode 100644 index 000000000..0c1cc0337 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6525.yaml @@ -0,0 +1,5 @@ +minor_changes: + - na_ontap_cluster - new option `certificate.uuid` added in REST, requires ONTAP 9.10 or later. + +bugfixes: + - na_ontap_security_certificates - fix error with ontap_info returned in module output in REST.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6527.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6527.yaml new file mode 100644 index 000000000..53d0b496b --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6527.yaml @@ -0,0 +1,2 @@ +bugfixes: + - na_ontap_vserver_peer - fix issue with peering multiple clusters with same vserver name in REST.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6528.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6528.yaml new file mode 100644 index 000000000..6edc14f1d --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6528.yaml @@ -0,0 +1,2 @@ +bugfixes: + - na_ontap_ems_destination - fix field error with `certificate.name` for ONTAP 9.11.1 or later in REST.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6529.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6529.yaml new file mode 100644 index 000000000..4b276e8fa --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6529.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_cifs_server - new option `lm_compatibility_level` added in REST, requires ONTAP 9.8 or later.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6551.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6551.yaml new file mode 100644 index 000000000..f31cce5a9 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6551.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_s3_services - create, modify S3 service returns `s3_service_info` in module output.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6556.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6556.yaml new file mode 100644 index 000000000..c82759a81 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6556.yaml @@ -0,0 +1,2 @@ +bugfixes: + - na_ontap_nfs - fix error with `windows` in REST for ONTAP 9.10 or earlier.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6584.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6584.yaml new file mode 100644 index 000000000..a2cbcac5c --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6584.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_rest_info - added warning message if given subset doesn't support option `owning_resource`.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6646.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6646.yaml new file mode 100644 index 000000000..2f2103812 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6646.yaml @@ -0,0 +1,6 @@ +minor_changes: + - na_ontap_export_policy_rule - added `actions` and `modify` in module output. + - na_ontap_file_security_permissions_acl - added `actions` and `modify` in module output. + - na_ontap_igroup_initiator - added `actions` in module output. + - na_ontap_name_mappings - added `actions` and `modify` in module output. + - na_ontap_node - added `modify` in module output.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6658.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6658.yaml new file mode 100644 index 000000000..b90e520f7 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6658.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_lun_map - added `actions` in module output.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6664.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6664.yaml new file mode 100644 index 000000000..65f8ca7a7 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6664.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_lun_map_reporting_nodes - added `actions` in module output.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6667.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6667.yaml new file mode 100644 index 000000000..2e3a10b17 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6667.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_storage_auto_giveback - added information on modified attributes in module output.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6671.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6671.yaml new file mode 100644 index 000000000..5a13d9199 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6671.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_vscan_scanner_pool - added REST support to Vscan Scanner Pools Configuration module, requires ONTAP 9.6 or later.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6680.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6680.yaml new file mode 100644 index 000000000..18dd7f28c --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6680.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_cifs_server - new option `is_multichannel_enabled` added in REST, requires ONTAP 9.10 or later.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6681.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6681.yaml new file mode 100644 index 000000000..867dd6aba --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/DEVOPS-6681.yaml @@ -0,0 +1,2 @@ +bugfixes: + - na_ontap_igroup_initiator - fixed issue with idempotency.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/changelogs/fragments/GITHUB-174.yaml b/ansible_collections/netapp/ontap/changelogs/fragments/GITHUB-174.yaml new file mode 100644 index 000000000..f26c149d0 --- /dev/null +++ b/ansible_collections/netapp/ontap/changelogs/fragments/GITHUB-174.yaml @@ -0,0 +1,2 @@ +minor_changes: + - na_ontap_ems_destination - new option ``certificate``, ``ca`` added.
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/meta/runtime.yml b/ansible_collections/netapp/ontap/meta/runtime.yml index 49dcabb60..4d8208129 100644 --- a/ansible_collections/netapp/ontap/meta/runtime.yml +++ b/ansible_collections/netapp/ontap/meta/runtime.yml @@ -1,5 +1,5 @@ --- -requires_ansible: ">=2.9.10" +requires_ansible: ">=2.14" action_groups: netapp_ontap: - na_ontap_active_directory_domain_controllers @@ -19,6 +19,8 @@ action_groups: - na_ontap_cifs_local_user_set_password - na_ontap_cifs - na_ontap_cifs_server + - na_ontap_cifs_unix_symlink_mapping + - na_ontap_cli_timeout - na_ontap_cluster_ha - na_ontap_cluster_peer - na_ontap_cluster @@ -29,6 +31,7 @@ action_groups: - na_ontap_dns - na_ontap_domain_tunnel - na_ontap_efficiency_policy + - na_ontap_ems_config - na_ontap_ems_destination - na_ontap_ems_filter - na_ontap_export_policy @@ -123,6 +126,7 @@ action_groups: - na_ontap_snapshot_policy - na_ontap_snapshot - na_ontap_snmp + - na_ontap_snmp_config - na_ontap_snmp_traphosts - na_ontap_software_update - na_ontap_ssh_command diff --git a/ansible_collections/netapp/ontap/plugins/module_utils/netapp.py b/ansible_collections/netapp/ontap/plugins/module_utils/netapp.py index 28d9428a2..f41139423 100644 --- a/ansible_collections/netapp/ontap/plugins/module_utils/netapp.py +++ b/ansible_collections/netapp/ontap/plugins/module_utils/netapp.py @@ -48,7 +48,7 @@ try: except ImportError: ANSIBLE_VERSION = 'unknown' -COLLECTION_VERSION = "22.7.0" +COLLECTION_VERSION = "22.10.0" CLIENT_APP_VERSION = "%s/%s" % ("%s", COLLECTION_VERSION) IMPORT_EXCEPTION = None diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_broadcast_domain.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_broadcast_domain.py index ef74d1705..11c762d7c 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_broadcast_domain.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_broadcast_domain.py @@ -47,6 +47,7 @@ options: - Specify the required ipspace for the broadcast domain. - With ZAPI, a domain ipspace cannot be modified after the domain has been created. - With REST, a domain ipspace can be modified. + - This option is required while using REST. type: str from_ipspace: description: diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cg_snapshot.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cg_snapshot.py index 313bf223e..20bca605d 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cg_snapshot.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cg_snapshot.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2018-2019, NetApp, Inc +# (c) 2018-2023, NetApp, Inc # 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 @@ -14,17 +14,17 @@ DOCUMENTATION = ''' short_description: NetApp ONTAP manage consistency group snapshot author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com> description: - - Create consistency group snapshot for ONTAP volumes. - - This module only supports ZAPI and is deprecated. - - The final version of ONTAP to support ZAPI is 9.12.1. + - Create or delete consistency group snapshot for ONTAP volumes. extends_documentation_fragment: - - netapp.ontap.netapp.na_ontap_zapi + - netapp.ontap.netapp.na_ontap module: na_ontap_cg_snapshot options: state: description: - - If you want to create a snapshot. + - Specifies whether to create or delete the snapshot. + - Choice 'absent' is valid only with REST. default: present + choices: ['present', 'absent'] type: str vserver: required: true @@ -32,11 +32,19 @@ options: description: - Name of the vserver. volumes: - required: true + required: false type: list elements: str description: - A list of volumes in this filer that is part of this CG operation. + - Required with ZAPI. + consistency_group: + required: false + type: str + description: + - Name of the consistency group for which snapshot needs to be created or deleted. + - Valid only with REST. + version_added: 22.8.0 snapshot: required: true type: str @@ -45,6 +53,7 @@ options: timeout: description: - Timeout selector. + - Not supported with REST. choices: ['urgent', 'medium', 'relaxed'] type: str default: medium @@ -52,8 +61,18 @@ options: description: - A human readable SnapMirror label to be attached with the consistency group snapshot copies. type: str + comment: + description: + - Comment for the snapshot copy. + - Only supported with REST. + type: str + version_added: 22.8.0 version_added: 2.7.0 +notes: + - REST support requires ONTAP 9.10 or later. + - Delete operation is supported only with REST. + ''' EXAMPLES = """ @@ -66,6 +85,40 @@ EXAMPLES = """ username: "{{ netapp username }}" password: "{{ netapp password }}" hostname: "{{ netapp hostname }}" + + - name: Create CG snapshot using CG name - REST + na_ontap_cg_snapshot: + state: present + vserver: vserver_name + snapshot: snapshot_name + consistency_group: cg_name + snapmirror_label: sm_label + username: "{{ netapp username }}" + password: "{{ netapp password }}" + hostname: "{{ netapp hostname }}" + + - name: Create CG snapshot using volumes - REST + na_ontap_cg_snapshot: + state: present + vserver: vserver_name + snapshot: snapshot_name + volumes: + - vol1 + - vol2 + snapmirror_label: sm_label + username: "{{ netapp username }}" + password: "{{ netapp password }}" + hostname: "{{ netapp hostname }}" + + - name: Delete CG snapshot - REST + na_ontap_cg_snapshot: + state: absent + vserver: vserver_name + snapshot: snapshot_name + consistency_group: cg_name + username: "{{ netapp username }}" + password: "{{ netapp password }}" + hostname: "{{ netapp hostname }}" """ RETURN = """ @@ -77,55 +130,56 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_native import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule - -HAS_NETAPP_LIB = netapp_utils.has_netapp_lib() +from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic class NetAppONTAPCGSnapshot(object): """ - Methods to create CG snapshots + Methods to create or delete CG snapshots """ def __init__(self): - self.argument_spec = netapp_utils.na_ontap_zapi_only_spec() + self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec.update(dict( - state=dict(required=False, type='str', default='present'), + state=dict(required=False, choices=['present', 'absent'], default='present'), vserver=dict(required=True, type='str'), - volumes=dict(required=True, type='list', elements='str'), + volumes=dict(required=False, type='list', elements='str'), snapshot=dict(required=True, type='str'), timeout=dict(required=False, type='str', choices=[ 'urgent', 'medium', 'relaxed'], default='medium'), - snapmirror_label=dict(required=False, type='str') + snapmirror_label=dict(required=False, type='str'), + consistency_group=dict(required=False, type='str'), + comment=dict(required=False, type='str'), )) self.module = AnsibleModule( argument_spec=self.argument_spec, - supports_check_mode=False + supports_check_mode=False, + mutually_exclusive=[ + ['consistency_group', 'volumes']] ) - parameters = self.module.params - - # set up variables - self.state = parameters['state'] - self.vserver = parameters['vserver'] - self.volumes = parameters['volumes'] - self.snapshot = parameters['snapshot'] - self.timeout = parameters['timeout'] - self.snapmirror_label = parameters['snapmirror_label'] - self.cgid = None - NetAppModule().module_deprecated(self.module) - if HAS_NETAPP_LIB is False: - self.module.fail_json( - msg="the python NetApp-Lib module is required") + self.na_helper = NetAppModule() + self.parameters = self.na_helper.set_parameters(self.module.params) + self.rest_api = netapp_utils.OntapRestAPI(self.module) + self.use_rest = self.rest_api.is_rest() + + if self.use_rest: + if not self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 10, 1): + self.module.fail_json(msg='REST requires ONTAP 9.10.1 or later for /application/consistency-groups APIs.') + self.cg_uuid = None else: - self.server = netapp_utils.setup_na_ontap_zapi( - module=self.module, vserver=self.vserver) + self.cgid = None + if not netapp_utils.has_netapp_lib(): + self.module.fail_json(msg=netapp_utils.netapp_lib_is_required()) + self.zapi_errors() + self.server = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=self.parameters['vserver']) def does_snapshot_exist(self, volume): """ This is duplicated from na_ontap_snapshot Checks to see if a snapshot exists or not - :return: Return True if a snapshot exists, false if it dosn't + :return: Return True if a snapshot exists, false if it dosen't """ # TODO: Remove this method and import snapshot module and # call get after re-factoring __init__ across all the modules @@ -141,9 +195,9 @@ class NetAppONTAPCGSnapshot(object): # compose query query = netapp_utils.zapi.NaElement("query") snapshot_info_obj = netapp_utils.zapi.NaElement("snapshot-info") - snapshot_info_obj.add_new_child("name", self.snapshot) + snapshot_info_obj.add_new_child("name", self.parameters['snapshot']) snapshot_info_obj.add_new_child("volume", volume) - snapshot_info_obj.add_new_child("vserver", self.vserver) + snapshot_info_obj.add_new_child("vserver", self.parameters['vserver']) query.add_child_elem(snapshot_info_obj) snapshot_obj.add_child_elem(query) result = self.server.invoke_successfully(snapshot_obj, True) @@ -164,7 +218,7 @@ class NetAppONTAPCGSnapshot(object): if self.cgid is not None: self.cg_commit() else: - self.module.fail_json(msg="Error fetching CG ID for CG commit %s" % self.snapshot, + self.module.fail_json(msg="Error fetching CG ID for CG commit %s" % self.parameters['snapshot'], exception=traceback.format_exc()) return started @@ -174,19 +228,19 @@ class NetAppONTAPCGSnapshot(object): """ snapshot_started = False cgstart = netapp_utils.zapi.NaElement("cg-start") - cgstart.add_new_child("snapshot", self.snapshot) - cgstart.add_new_child("timeout", self.timeout) + cgstart.add_new_child("snapshot", self.parameters['snapshot']) + cgstart.add_new_child("timeout", self.parameters['timeout']) volume_list = netapp_utils.zapi.NaElement("volumes") cgstart.add_child_elem(volume_list) - for vol in self.volumes: + for vol in self.parameters['volumes']: snapshot_exists = self.does_snapshot_exist(vol) if snapshot_exists is None: snapshot_started = True volume_list.add_new_child("volume-name", vol) if snapshot_started: - if self.snapmirror_label: + if self.parameters.get('snapmirror_label') is not None: cgstart.add_new_child("snapmirror-label", - self.snapmirror_label) + self.parameters['snapmirror_label']) try: cgresult = self.server.invoke_successfully( cgstart, enable_tunneling=True) @@ -194,7 +248,7 @@ class NetAppONTAPCGSnapshot(object): self.cgid = cgresult['cg-id'] except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg="Error creating CG snapshot %s: %s" % - (self.snapshot, to_native(error)), + (self.parameters['snapshot'], to_native(error)), exception=traceback.format_exc()) return snapshot_started @@ -209,18 +263,126 @@ class NetAppONTAPCGSnapshot(object): enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg="Error committing CG snapshot %s: %s" % - (self.snapshot, to_native(error)), + (self.parameters['snapshot'], to_native(error)), + exception=traceback.format_exc()) + + def zapi_errors(self): + unsupported_zapi_properties = ['consistency_group', 'comment'] + used_unsupported_zapi_properties = [option for option in unsupported_zapi_properties if option in self.parameters] + if used_unsupported_zapi_properties: + self.module.fail_json(msg="Error: %s options supported only with REST." % " ,".join(used_unsupported_zapi_properties)) + if self.parameters.get('volumes') is None: + self.module.fail_json(msg="Error: 'volumes' option is mandatory while using ZAPI.") + if self.parameters.get('state') == 'absent': + self.module.fail_json(msg="Deletion of consistency group snapshot is not supported with ZAPI.") + + def get_cg_rest(self): + """ + Retrieve consistency group with the given CG name or list of volumes + """ + api = '/application/consistency-groups' + query = { + 'svm.name': self.parameters['vserver'], + 'fields': 'svm.uuid,name,uuid,' + } + + if self.parameters.get('consistency_group') is not None: + query['name'] = self.parameters['consistency_group'] + record, error = rest_generic.get_one_record(self.rest_api, api, query) + if error: + self.module.fail_json(msg='Error searching for consistency group %s: %s' % (self.parameters['consistency_group'], to_native(error)), + exception=traceback.format_exc()) + if record: + self.cg_uuid = record.get('uuid') + + if self.parameters.get('volumes') is not None: + query['fields'] += 'volumes.name,' + records, error = rest_generic.get_0_or_more_records(self.rest_api, api, query) + if error: + self.module.fail_json(msg='Error searching for consistency group having volumes %s: %s' % (self.parameters['volumes'], to_native(error)), + exception=traceback.format_exc()) + if records: + for record in records: + if record.get('volumes') is not None: + cg_volumes = [vol_item['name'] for vol_item in record['volumes']] + if cg_volumes == self.parameters['volumes']: + self.cg_uuid = record.get('uuid') + break + return None + + def get_cg_snapshot_rest(self): + """ + Retrieve CG snapshots using fetched CG uuid + """ + self.get_cg_rest() + if self.cg_uuid is None: + if self.parameters.get('consistency_group') is not None: + self.module.fail_json(msg="Consistency group named '%s' not found" % self.parameters.get('consistency_group')) + if self.parameters.get('volumes') is not None: + self.module.fail_json(msg="Consistency group having volumes '%s' not found" % self.parameters.get('volumes')) + + api = '/application/consistency-groups/%s/snapshots' % self.cg_uuid + query = {'name': self.parameters['snapshot'], + 'fields': 'name,' + 'uuid,' + 'consistency_group,' + 'snapmirror_label,' + 'comment,'} + record, error = rest_generic.get_one_record(self.rest_api, api, query) + if error: + self.module.fail_json(msg='Error searching for consistency group snapshot %s: %s' % (self.parameters['snapshot'], to_native(error)), + exception=traceback.format_exc()) + if record: + return { + 'snapshot': record.get('name'), + 'snapshot_uuid': record.get('uuid'), + 'consistency_group': self.na_helper.safe_get(record, ['consistency_group', 'name']), + 'snapmirror_label': record.get('snapmirror_label'), + 'comment': record.get('comment'), + } + return None + + def create_cg_snapshot_rest(self): + """Create CG snapshot""" + api = '/application/consistency-groups/%s/snapshots' % self.cg_uuid + body = {'name': self.parameters['snapshot']} + if self.parameters.get('snapmirror_label'): + body['snapmirror_label'] = self.parameters['snapmirror_label'] + if self.parameters.get('comment'): + body['comment'] = self.parameters['comment'] + dummy, error = rest_generic.post_async(self.rest_api, api, body) + if error: + self.module.fail_json(msg='Error creating consistency group snapshot %s: %s' % (self.parameters['snapshot'], to_native(error)), + exception=traceback.format_exc()) + + def delete_cg_snapshot_rest(self, current): + """Delete CG snapshot""" + api = '/application/consistency-groups/%s/snapshots' % self.cg_uuid + dummy, error = rest_generic.delete_async(self.rest_api, api, current['snapshot_uuid']) + if error: + self.module.fail_json(msg='Error deleting consistency group snapshot %s: %s' % (self.parameters['snapshot'], to_native(error)), exception=traceback.format_exc()) def apply(self): - '''Applies action from playbook''' - if not self.module.check_mode: - changed = self.cgcreate() - self.module.exit_json(changed=changed) + """Applies action from playbook""" + if not self.use_rest: + if not self.module.check_mode: + changed = self.cgcreate() + self.module.exit_json(changed=changed) + current = self.get_cg_snapshot_rest() + cd_action = self.na_helper.get_cd_action(current, self.parameters) + + if self.na_helper.changed and not self.module.check_mode: + if cd_action == 'create': + self.create_cg_snapshot_rest() + elif cd_action == 'delete': + self.delete_cg_snapshot_rest(current) + result = netapp_utils.generate_result(self.na_helper.changed, cd_action) + self.module.exit_json(**result) def main(): - '''Execute action from playbook''' + """Execute action from playbook""" cg_obj = NetAppONTAPCGSnapshot() cg_obj.apply() diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cifs_server.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cifs_server.py index 8a65dd6c5..08a69c1f7 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cifs_server.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cifs_server.py @@ -1,7 +1,7 @@ #!/usr/bin/python """ this is cifs_server module - (c) 2018-2022, NetApp, Inc + (c) 2018-2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) """ @@ -70,6 +70,13 @@ options: version_added: 2.7.0 type: str + default_site: + description: + - Specifies the site within the Active Directory domain to associate with the CIFS server if Data ONTAP cannot determine an appropriate site. + - Only supported with REST and requires ontap version 9.13.1 or later. + version_added: 22.8.0 + type: str + force: type: bool description: @@ -175,6 +182,21 @@ options: type: str version_added: 21.20.0 + lm_compatibility_level: + description: + - Specifies CIFS server minimum security level, also known as the LMCompatibilityLevel. + - Only supported with REST and requires ontap version 9.8 or later. Use na_ontap_vserver_cifs_security with ZAPI. + choices: ['lm_ntlm_ntlmv2_krb', 'ntlm_ntlmv2_krb', 'ntlmv2_krb', 'krb'] + type: str + version_added: 22.9.0 + + is_multichannel_enabled: + description: + - Specifies whether the CIFS server supports Multichannel or not. + - Only supported with REST and requires ontap version 9.10 or later. + type: bool + version_added: 22.10.0 + ''' EXAMPLES = ''' @@ -239,16 +261,16 @@ EXAMPLES = ''' name: data2 vserver: svm1 service_state: stopped - encrypt_dc_connection: True, - smb_encryption: True, - kdc_encryption: True, - smb_signing: True, - aes_netlogon_enabled: True, - ldap_referral_enabled: True, - session_security: seal, - try_ldap_channel_binding: False, - use_ldaps: True, - use_start_tls": True + encrypt_dc_connection: True + smb_encryption: True + kdc_encryption: True + smb_signing: True + aes_netlogon_enabled: True + ldap_referral_enabled: True + session_security: seal + try_ldap_channel_binding: False + use_ldaps: True + use_start_tls: True restrict_anonymous: no_access domain: "{{ id_domain }}" admin_user_name: "{{ domain_login }}" @@ -289,6 +311,7 @@ class NetAppOntapcifsServer: admin_user_name=dict(required=False, type='str'), admin_password=dict(required=False, type='str', no_log=True), ou=dict(required=False, type='str'), + default_site=dict(required=False, type='str'), force=dict(required=False, type='bool'), vserver=dict(required=True, type='str'), from_name=dict(required=False, type='str'), @@ -300,9 +323,11 @@ class NetAppOntapcifsServer: aes_netlogon_enabled=dict(required=False, type='bool'), ldap_referral_enabled=dict(required=False, type='bool'), session_security=dict(required=False, type='str', choices=['none', 'sign', 'seal']), + lm_compatibility_level=dict(required=False, type='str', choices=['lm_ntlm_ntlmv2_krb', 'ntlm_ntlmv2_krb', 'ntlmv2_krb', 'krb']), try_ldap_channel_binding=dict(required=False, type='bool'), use_ldaps=dict(required=False, type='bool'), - use_start_tls=dict(required=False, type='bool') + use_start_tls=dict(required=False, type='bool'), + is_multichannel_enabled=dict(required=False, type='bool'), )) self.module = AnsibleModule( @@ -318,15 +343,16 @@ class NetAppOntapcifsServer: # Set up Rest API self.rest_api = OntapRestAPI(self.module) unsupported_rest_properties = ['workgroup'] - partially_supported_rest_properties = [['encrypt_dc_connection', (9, 8)], ['aes_netlogon_enabled', (9, 10, 1)], ['ldap_referral_enabled', (9, 10, 1)], - ['session_security', (9, 10, 1)], ['try_ldap_channel_binding', (9, 10, 1)], ['use_ldaps', (9, 10, 1)], - ['use_start_tls', (9, 10, 1)], ['force', (9, 11)]] + partially_supported_rest_properties = [['encrypt_dc_connection', (9, 8)], ['lm_compatibility_level', (9, 8)], + ['aes_netlogon_enabled', (9, 10, 1)], ['ldap_referral_enabled', (9, 10, 1)], ['session_security', (9, 10, 1)], + ['try_ldap_channel_binding', (9, 10, 1)], ['use_ldaps', (9, 10, 1)], ['use_start_tls', (9, 10, 1)], + ['is_multichannel_enabled', (9, 10, 1)], ['force', (9, 11)], ['default_site', (9, 13, 1)]] self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, unsupported_rest_properties, partially_supported_rest_properties) if not self.use_rest: unsupported_zapi_properties = ['smb_signing', 'encrypt_dc_connection', 'kdc_encryption', 'smb_encryption', 'restrict_anonymous', 'aes_netlogon_enabled', 'ldap_referral_enabled', 'try_ldap_channel_binding', 'session_security', - 'use_ldaps', 'use_start_tls', 'from_name'] + 'lm_compatibility_level', 'use_ldaps', 'use_start_tls', 'from_name', 'default_site', 'is_multichannel_enabled'] used_unsupported_zapi_properties = [option for option in unsupported_zapi_properties if option in self.parameters] if used_unsupported_zapi_properties: self.module.fail_json(msg="Error: %s options supported only with REST." % " ,".join(used_unsupported_zapi_properties)) @@ -460,7 +486,9 @@ class NetAppOntapcifsServer: query['name'] = from_name or self.parameters['cifs_server_name'] api = 'protocols/cifs/services' if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 8): - query['fields'] += 'security.encrypt_dc_connection,' + security_option_9_8 = ('security.encrypt_dc_connection,' + 'security.lm_compatibility_level,') + query['fields'] += security_option_9_8 if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 10, 1): security_option_9_10 = ('security.use_ldaps,' @@ -470,6 +498,10 @@ class NetAppOntapcifsServer: 'security.ldap_referral_enabled,' 'security.aes_netlogon_enabled,') query['fields'] += security_option_9_10 + + if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 10, 1): + service_option_9_10 = ('options.multichannel,') + query['fields'] += service_option_9_10 record, error = rest_generic.get_one_record(self.rest_api, api, query) if error: self.module.fail_json(msg="Error on fetching cifs: %s" % error) @@ -486,10 +518,12 @@ class NetAppOntapcifsServer: 'aes_netlogon_enabled': self.na_helper.safe_get(record, ['security', 'aes_netlogon_enabled']), 'ldap_referral_enabled': self.na_helper.safe_get(record, ['security', 'ldap_referral_enabled']), 'session_security': self.na_helper.safe_get(record, ['security', 'session_security']), + 'lm_compatibility_level': self.na_helper.safe_get(record, ['security', 'lm_compatibility_level']), 'try_ldap_channel_binding': self.na_helper.safe_get(record, ['security', 'try_ldap_channel_binding']), 'use_ldaps': self.na_helper.safe_get(record, ['security', 'use_ldaps']), 'use_start_tls': self.na_helper.safe_get(record, ['security', 'use_start_tls']), - 'restrict_anonymous': self.na_helper.safe_get(record, ['security', 'restrict_anonymous']) + 'restrict_anonymous': self.na_helper.safe_get(record, ['security', 'restrict_anonymous']), + 'is_multichannel_enabled': self.na_helper.safe_get(record, ['options', 'multichannel']), } return record @@ -503,17 +537,20 @@ class NetAppOntapcifsServer: ad_domain['organizational_unit'] = self.parameters['ou'] if 'domain' in self.parameters: ad_domain['fqdn'] = self.parameters['domain'] + if 'default_site' in self.parameters: + ad_domain['default_site'] = self.parameters['default_site'] return ad_domain def create_modify_body_rest(self, params=None): """ Function to define body for create and modify cifs server """ - body, query, security = {}, {}, {} + body, query, security, service_options = {}, {}, {}, {} if params is None: params = self.parameters security_options = ['smb_signing', 'encrypt_dc_connection', 'kdc_encryption', 'smb_encryption', 'restrict_anonymous', - 'aes_netlogon_enabled', 'ldap_referral_enabled', 'try_ldap_channel_binding', 'session_security', 'use_ldaps', 'use_start_tls'] + 'aes_netlogon_enabled', 'ldap_referral_enabled', 'try_ldap_channel_binding', 'session_security', + 'lm_compatibility_level', 'use_ldaps', 'use_start_tls'] ad_domain = self.build_ad_domain() if ad_domain: body['ad_domain'] = ad_domain @@ -524,6 +561,14 @@ class NetAppOntapcifsServer: security[key] = params[key] if security: body['security'] = security + # for parameters having different key names in REST API and module inputs + for key, option in [ + ('multichannel', 'is_multichannel_enabled'), + ]: + if option in params: + service_options.update({key: params[option]}) + if service_options: + body['options'] = service_options if 'vserver' in params: body['svm.name'] = params['vserver'] if 'cifs_server_name' in params: diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cifs_unix_symlink_mapping.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cifs_unix_symlink_mapping.py new file mode 100644 index 000000000..f8f1bfacb --- /dev/null +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cifs_unix_symlink_mapping.py @@ -0,0 +1,289 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: NetApp, Inc +# 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 + +DOCUMENTATION = """ +module: na_ontap_cifs_unix_symlink_mapping +short_description: NetApp ONTAP module to manage UNIX symbolic link mapping for CIFS clients. +extends_documentation_fragment: + - netapp.ontap.netapp.na_ontap +version_added: '22.9.0' +author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com> +description: + - Create/ modify/ delete a UNIX symbolic link mapping for a CIFS client. +options: + state: + description: + - Whether the specified symlink mapping should exist or not. + choices: ['present', 'absent'] + type: str + default: present + + vserver: + description: + - Name of the vserver to use. + type: str + required: true + + unix_path: + description: + - Specifies the UNIX path prefix to be matched for the mapping. + - It must begin and end with a forward slash (/). + type: str + required: true + + share_name: + description: + - Specifies the CIFS share name on the destination CIFS server to which the UNIX symbolic link is pointing. + type: str + + cifs_server: + description: + - Specifies the destination CIFS server (DNS name, IP address, or NetBIOS name). + - This field is mandatory if the locality of the symbolic link is 'widelink'. + type: str + + cifs_path: + description: + - Specifies the CIFS path on the destination to which the symbolic link maps. + - Note that this value is specified by using a UNIX-style path. It must begin and end with a forward slash (/). + type: str + + locality: + description: + - Specifies whether the CIFS symbolic link is a local link or wide link. The default setting is local. + - The following values are supported + local - Local symbolic link maps only to the same CIFS share. + widelink - Wide symbolic link maps to any CIFS share on the network. + type: str + choices: ['local', 'widelink'] + default: 'local' + + home_directory: + description: + - Specify if the destination share is a home directory. The default value is false. + type: bool + default: False + +notes: + - Only supported with REST and requires ONTAP 9.6 or later. + +""" + +EXAMPLES = """ + - name: Create a UNIX symlink mapping for CIFS share + netapp.ontap.na_ontap_cifs_unix_symlink_mapping: + state: present + vserver: "{{ svm }}" + unix_path: "/example1/" + share_name: "share1" + cifs_path: "/path1/test_dir/" + cifs_server: "CIFS" + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + https: true + validate_certs: "{{ validate_certs }}" + + - name: Update a specific UNIX symlink mapping for a SVM + netapp.ontap.na_ontap_cifs_unix_symlink_mapping: + state: present + vserver: "{{ svm }}" + unix_path: "/example1/" + share_name: "share2" + cifs_path: "/path2/test_dir/" + cifs_server: "CIFS" + locality: "widelink" + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + https: true + validate_certs: "{{ validate_certs }}" + + - name: Remove a specific UNIX symlink mapping for a SVM + netapp.ontap.na_ontap_cifs_unix_symlink_mapping: + state: absent + vserver: "{{ svm }}" + unix_path: "/example1/" + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + https: true + validate_certs: "{{ validate_certs }}" + +""" + +RETURN = """ +""" + +import traceback +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule +from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic + + +class NetAppOntapCifsUnixSymlink: + def __init__(self): + self.argument_spec = netapp_utils.na_ontap_host_argument_spec() + self.argument_spec.update(dict( + state=dict(required=False, type='str', choices=['present', 'absent'], default='present'), + vserver=dict(required=True, type='str'), + unix_path=dict(required=True, type='str'), + share_name=dict(required=False, type='str'), + cifs_path=dict(required=False, type='str'), + cifs_server=dict(required=False, type='str'), + locality=dict(required=False, type='str', choices=['local', 'widelink'], default='local'), + home_directory=dict(required=False, type='bool', default=False) + )) + self.module = AnsibleModule( + argument_spec=self.argument_spec, + required_if=[ + ('state', 'present', ['share_name', 'cifs_path']), + ('locality', 'widelink', ['cifs_server']), + ], + supports_check_mode=True + ) + self.svm_uuid = None + self.na_helper = NetAppModule(self.module) + self.parameters = self.na_helper.check_and_set_parameters(self.module) + self.rest_api = netapp_utils.OntapRestAPI(self.module) + self.rest_api.fail_if_not_rest_minimum_version('na_ontap_cifs_unix_symlink_mapping:', 9, 6) + + @staticmethod + def validate_path(path): + if not path.startswith('/'): + path = "/%s" % path + if not path.endswith('/'): + path = "%s/" % path + return path + + @staticmethod + def encode_path(path): + return path.replace('/', '%2F') + + def get_symlink_mapping_rest(self): + """ + Retrieves a specific UNIX symbolink mapping for a SVM + """ + api = 'protocols/cifs/unix-symlink-mapping' + query = {'svm.name': self.parameters.get('vserver'), + 'unix_path': self.parameters['unix_path'], + 'fields': 'svm.uuid,' + 'unix_path,' + 'target.share,' + 'target.path,'} + if self.parameters.get('cifs_server') is not None: + query['fields'] += 'target.server,' + if self.parameters.get('locality') is not None: + query['fields'] += 'target.locality,' + if self.parameters.get('home_directory') is not None: + query['fields'] += 'target.home_directory,' + + record, error = rest_generic.get_one_record(self.rest_api, api, query) + if error: + self.module.fail_json(msg='Error while fetching cifs unix symlink mapping: %s' % to_native(error), + exception=traceback.format_exc()) + if record: + self.svm_uuid = self.na_helper.safe_get(record, ['svm', 'uuid']) + return self.format_record(record) + return None + + def format_record(self, record): + return { + 'unix_path': record.get('unix_path'), + 'share_name': self.na_helper.safe_get(record, ['target', 'share']), + 'cifs_path': self.na_helper.safe_get(record, ['target', 'path']), + 'cifs_server': self.na_helper.safe_get(record, ['target', 'server']), + 'locality': self.na_helper.safe_get(record, ['target', 'locality']), + 'home_directory': self.na_helper.safe_get(record, ['target', 'home_directory']) + } + + def create_symlink_mapping_rest(self): + """ + Creates a UNIX symbolink mapping for CIFS share + """ + api = 'protocols/cifs/unix-symlink-mapping' + body = { + 'svm.name': self.parameters['vserver'], + 'unix_path': self.parameters['unix_path'], + 'target': { + 'share': self.parameters['share_name'], + 'path': self.parameters['cifs_path'] + } + } + if 'cifs_server' in self.parameters: + body['target.server'] = self.parameters['cifs_server'] + if 'locality' in self.parameters: + body['target.locality'] = self.parameters['locality'] + if 'home_directory' in self.parameters: + body['target.home_directory'] = self.parameters['home_directory'] + + dummy, error = rest_generic.post_async(self.rest_api, api, body) + if error is not None: + self.module.fail_json(msg='Error while creating cifs unix symlink mapping: %s' % to_native(error), + exception=traceback.format_exc()) + + def modify_symlink_mapping_rest(self, modify): + """ + Updates a specific UNIX symbolink mapping for a SVM + """ + api = 'protocols/cifs/unix-symlink-mapping/%s/%s' % (self.svm_uuid, self.encode_path(self.parameters['unix_path'])) + body = {'target': {}} + for key, option in [ + ('share', 'share_name'), + ('path', 'cifs_path'), + ('server', 'cifs_server'), + ('locality', 'locality'), + ('home_directory', 'home_directory'), + ]: + if modify.get(option) is not None: + body['target'][key] = modify[option] + + dummy, error = rest_generic.patch_async(self.rest_api, api, uuid_or_name=None, body=body) + if error: + self.module.fail_json(msg='Error while modifying cifs unix symlink mapping: %s.' % to_native(error), + exception=traceback.format_exc()) + + def delete_symlink_mapping_rest(self): + """ + Removes a specific UNIX symbolink mapping for a SVM + """ + api = 'protocols/cifs/unix-symlink-mapping/%s/%s' % (self.svm_uuid, self.encode_path(self.parameters['unix_path'])) + dummy, error = rest_generic.delete_async(self.rest_api, api, uuid=None) + if error is not None: + self.module.fail_json(msg='Error while deleting cifs unix symlink mapping: %s' % to_native(error)) + + def apply(self): + # validate leading and trailing forward slashes in unix_path & cifs_path + for option in ['unix_path', 'cifs_path']: + if self.parameters.get(option) is not None: + self.parameters[option] = self.validate_path(self.parameters[option]) + + current = self.get_symlink_mapping_rest() + cd_action = self.na_helper.get_cd_action(current, self.parameters) + modify = self.na_helper.get_modified_attributes(current, self.parameters) + if self.na_helper.changed and not self.module.check_mode: + if cd_action == 'create': + self.create_symlink_mapping_rest() + elif cd_action == 'delete': + self.delete_symlink_mapping_rest() + elif modify: + self.modify_symlink_mapping_rest(modify) + result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify) + self.module.exit_json(**result) + + +def main(): + symlink_mapping = NetAppOntapCifsUnixSymlink() + symlink_mapping.apply() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cli_timeout.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cli_timeout.py new file mode 100644 index 000000000..02ff00a32 --- /dev/null +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cli_timeout.py @@ -0,0 +1,123 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: NetApp, Inc +# 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 + +DOCUMENTATION = """ +module: na_ontap_cli_timeout +short_description: NetApp ONTAP module to set the CLI inactivity timeout value. +extends_documentation_fragment: + - netapp.ontap.netapp.na_ontap +version_added: '22.9.0' +author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com> +description: + - Modify the timeout value for CLI sessions. +options: + state: + description: + - Modify timeout value, only present is supported. + choices: ['present'] + type: str + default: present + timeout: + description: + - Specifies the timeout value, in minutes. + - To prevent CLI sessions from timing out, specify a value of 0 (zero). + type: int + required: true + +notes: + - Only supported with REST and requires ONTAP 9.6 or later. +""" + +EXAMPLES = """ + - name: Modify the timeout value for CLI sessions to be 15 minutes + netapp.ontap.na_ontap_cli_timeout: + state: present + timeout: 15 + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + https: true + validate_certs: "{{ validate_certs }}" + + - name: Prevent CLI sessions from timing out + netapp.ontap.na_ontap_cli_timeout: + state: present + timeout: 0 + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + https: true + validate_certs: "{{ validate_certs }}" +""" + +RETURN = """ +""" + +import traceback +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule +from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic + + +class NetAppOntapCliTimeout: + def __init__(self): + self.argument_spec = netapp_utils.na_ontap_host_argument_spec() + self.argument_spec.update(dict( + state=dict(required=False, type='str', choices=['present'], default='present'), + timeout=dict(required=True, type='int') + )) + self.module = AnsibleModule( + argument_spec=self.argument_spec, + supports_check_mode=True + ) + self.na_helper = NetAppModule(self.module) + self.parameters = self.na_helper.check_and_set_parameters(self.module) + self.rest_api = netapp_utils.OntapRestAPI(self.module) + self.rest_api.fail_if_not_rest_minimum_version('na_ontap_cli_timeout:', 9, 6) + + def get_timeout_value_rest(self): + """ Get CLI inactivity timeout value """ + fields = 'timeout' + api = 'private/cli/system/timeout' + record, error = rest_generic.get_one_record(self.rest_api, api, query=None, fields=fields) + if error: + self.module.fail_json(msg="Error fetching CLI sessions timeout value: %s" % to_native(error), + exception=traceback.format_exc()) + if record: + return { + 'timeout': record.get('timeout') + } + return None + + def modify_timeout_value_rest(self, modify): + """ Modify CLI inactivity timeout value """ + api = 'private/cli/system/timeout' + dummy, error = rest_generic.patch_async(self.rest_api, api, uuid_or_name=None, body=modify) + if error: + self.module.fail_json(msg='Error modifying CLI sessions timeout value: %s.' % to_native(error), + exception=traceback.format_exc()) + + def apply(self): + current = self.get_timeout_value_rest() + modify = self.na_helper.get_modified_attributes(current, self.parameters) + if self.na_helper.changed and not self.module.check_mode: + self.modify_timeout_value_rest(modify) + result = netapp_utils.generate_result(self.na_helper.changed, modify=modify) + self.module.exit_json(**result) + + +def main(): + cli_timeout = NetAppOntapCliTimeout() + cli_timeout.apply() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster.py index fb0f507fc..bac9fd261 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2017-2022, NetApp, Inc +# (c) 2017-2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ''' @@ -94,7 +94,17 @@ options: - A system-specific or other term not associated with a geographic region or GMT - "full list of supported alias can be found here: https://library.netapp.com/ecmdocs/ECMP1155590/html/GUID-D3B8A525-67A2-4BEE-99DB-EF52D6744B5F.html" - Only supported by REST - + certificate: + description: + - Certificate used by cluster and node management interfaces for TLS connection requests. + - Only supported with REST and requires ONTAP 9.10 or later. + type: dict + version_added: 22.9.0 + suboptions: + uuid: + type: str + description: + - Certificate UUID. notes: - supports REST and ZAPI ''' @@ -108,6 +118,7 @@ EXAMPLES = """ hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" + - name: Add node to cluster (Join cluster) netapp.ontap.na_ontap_cluster: state: present @@ -115,6 +126,7 @@ EXAMPLES = """ hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" + - name: Add node to cluster (Join cluster) netapp.ontap.na_ontap_cluster: state: present @@ -123,6 +135,7 @@ EXAMPLES = """ hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" + - name: Create a 2 node cluster in one call netapp.ontap.na_ontap_cluster: state: present @@ -131,6 +144,7 @@ EXAMPLES = """ hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" + - name: Remove node from cluster netapp.ontap.na_ontap_cluster: state: absent @@ -138,6 +152,7 @@ EXAMPLES = """ hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" + - name: Remove node from cluster netapp.ontap.na_ontap_cluster: state: absent @@ -145,6 +160,7 @@ EXAMPLES = """ hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" + - name: modify cluster netapp.ontap.na_ontap_cluster: state: present @@ -154,6 +170,19 @@ EXAMPLES = """ hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" + + - name: updating the cluster-wide web services configuration + netapp.ontap.na_ontap_cluster: + state: present + cluster_contact: testing + cluster_location: testing + certificate: + uuid: 7f2f332c-933e-11ee-ab1c-005056b397ff + cluster_name: "{{ netapp_cluster}}" + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + """ RETURN = """ @@ -182,6 +211,9 @@ class NetAppONTAPCluster: cluster_ip_address=dict(required=False, type='str'), cluster_location=dict(required=False, type='str'), cluster_contact=dict(required=False, type='str'), + certificate=dict(required=False, type='dict', options=dict( + uuid=dict(required=False, type='str') + )), force=dict(required=False, type='bool', default=False), single_node_cluster=dict(required=False, type='bool'), node_name=dict(required=False, type='str'), @@ -202,6 +234,10 @@ class NetAppONTAPCluster: # cached, so that we don't call the REST API more than once self.node_records = None + self.rest_api = OntapRestAPI(self.module) + partially_supported_rest_properties = [['certificate', (9, 10, 1)]] + self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, None, partially_supported_rest_properties) + if self.parameters['state'] == 'absent' and self.parameters.get('node_name') is not None and self.parameters.get('cluster_ip_address') is not None: msg = 'when state is "absent", parameters are mutually exclusive: cluster_ip_address|node_name' self.module.fail_json(msg=msg) @@ -209,14 +245,14 @@ class NetAppONTAPCluster: if self.parameters.get('node_name') is not None and '-' in self.parameters.get('node_name'): self.warnings.append('ONTAP ZAPI converts "-" to "_", node_name: %s may be changed or not matched' % self.parameters.get('node_name')) - self.rest_api = OntapRestAPI(self.module) - self.use_rest = self.rest_api.is_rest() if self.use_rest and self.parameters['state'] == 'absent' and not self.rest_api.meets_rest_minimum_version(True, 9, 7, 0): self.module.warn('switching back to ZAPI as DELETE is not supported on 9.6') self.use_rest = False if not self.use_rest: if self.na_helper.safe_get(self.parameters, ['timezone', 'name']): self.module.fail_json(msg='Timezone is only supported with REST') + if self.na_helper.safe_get(self.parameters, ['certificate', 'uuid']): + self.module.fail_json(msg='Certificate is only supported with REST') if not netapp_utils.has_netapp_lib(): self.module.fail_json(msg="the python NetApp-Lib module is required") self.server = netapp_utils.setup_na_ontap_zapi(module=self.module) @@ -235,12 +271,17 @@ class NetAppONTAPCluster: self.module.fail_json(msg='Error fetching cluster identity info: %s' % to_native(error), exception=traceback.format_exc()) if record: - return { + cluster_info = { 'cluster_contact': record.get('contact'), 'cluster_location': record.get('location'), 'cluster_name': record.get('name'), 'timezone': self.na_helper.safe_get(record, ['timezone']) } + if self.parameters.get('certificate') is not None: + web_service_record = self.get_web_services() + cluster_info.update(web_service_record) + if cluster_info: + return cluster_info return None def get_cluster_identity(self, ignore_error=True): @@ -526,6 +567,27 @@ class NetAppONTAPCluster: exception=traceback.format_exc()) return uuid, from_node + def get_web_services(self): + record, error = rest_generic.get_one_record(self.rest_api, 'cluster/web', fields='certificate') + if error: + self.module.fail_json(msg='Error fetching cluster web service config: %s' % to_native(error), + exception=traceback.format_exc()) + if record: + return record + return None + + def modify_web_services(self): + body = { + 'certificate': { + 'uuid': self.parameters['certificate']['uuid'] + } + } + dummy, error = rest_generic.patch_async(self.rest_api, 'cluster/web', None, body) + if error: + self.module.fail_json(msg='Error modifying cluster web service config for %s: %s' + % (self.parameters['cluster_name'], to_native(error)), + exception=traceback.format_exc()) + def remove_node_rest(self): """ Remove a node from an existing cluster @@ -570,10 +632,12 @@ class NetAppONTAPCluster: """ Modifies the cluster identity """ + if 'certificate' in modify: + self.modify_web_services() body = self.create_cluster_body(modify) dummy, error = rest_generic.patch_async(self.rest_api, 'cluster', None, body) if error: - self.module.fail_json(msg='Error modifying cluster idetity details %s: %s' + self.module.fail_json(msg='Error modifying cluster identity details %s: %s' % (self.parameters['cluster_name'], to_native(error)), exception=traceback.format_exc()) diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster_peer.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster_peer.py index 820001cc4..92c2c419d 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster_peer.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_cluster_peer.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2018-2022, NetApp, Inc +# (c) 2018-2023, NetApp, Inc # 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 @@ -9,6 +9,7 @@ DOCUMENTATION = ''' author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com> description: - Create/Delete cluster peer relations on ONTAP + - Modify remote intercluster addresses in cluster peer relation on ONTAP extends_documentation_fragment: - netapp.ontap.netapp.na_ontap - netapp.ontap.netapp.na_ontap_peer @@ -46,12 +47,13 @@ options: type: str source_cluster_name: description: - - The name of the source cluster name in the peer relation to be deleted. + - The name of the source cluster name in the peer relation to be modified or deleted. + - Required for deleting peer relation and for modifying source_intercluster_lifs. type: str dest_cluster_name: description: - - The name of the destination cluster name in the peer relation to be deleted. - - Required for delete + - The name of the destination cluster name in the peer relation to be modified or deleted. + - Required for deleting peer relation and for modifying dest_intercluster_lifs. type: str dest_hostname: description: @@ -86,6 +88,9 @@ options: version_added: '20.5.0' short_description: NetApp ONTAP Manage Cluster peering version_added: 2.7.0 + +notes: + - Modify remote intercluster addresses operation is supported only with REST. ''' EXAMPLES = """ @@ -129,6 +134,18 @@ EXAMPLES = """ key_filepath: "{{ key_filepath }}" encryption_protocol_proposed: tls_psk + - name: Modify cluster peer - destination intercluster addresses + netapp.ontap.na_ontap_cluster_peer: + state: present + source_intercluster_lifs: 1.2.3.4,1.2.3.5 + dest_intercluster_lifs: 1.2.3.8 + dest_cluster_name: test-dest-cluster + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + peer_options: + hostname: "{{ dest_netapp_hostname }}" + """ RETURN = """ @@ -279,7 +296,7 @@ class NetAppONTAPClusterPeer: # if peer-lifs not present in parameters, use peer_cluster to filter desired cluster peer in current. if self.parameters.get(peer_lifs) is not None: peer_addresses_exist = set(self.parameters[peer_lifs]) == set(record['remote']['ip_addresses']) - else: + if self.parameters.get(peer_cluster) is not None: peer_cluster_exist = self.parameters[peer_cluster] == record['remote']['name'] if peer_addresses_exist or peer_cluster_exist: cluster_info['cluster_name'] = record['remote']['name'] @@ -379,23 +396,56 @@ class NetAppONTAPClusterPeer: for record in response['records']: self.generated_passphrase = record['authentication']['passphrase'] + def cluster_peer_modify_rest(self, cluster, uuid, modified_peer_addresses): + api = 'cluster/peers' + body = {'remote.ip_addresses': modified_peer_addresses} + server = self.rest_api if cluster == 'source' else self.dst_rest_api + dummy, error = rest_generic.patch_async(server, api, uuid, body) + if error: + self.module.fail_json(msg=error) + def apply(self): """ Apply action to cluster peer :return: None """ + modify = {} source = self.cluster_peer_get('source') destination = self.cluster_peer_get('destination') source_action = self.na_helper.get_cd_action(source, self.parameters) destination_action = self.na_helper.get_cd_action(destination, self.parameters) self.na_helper.changed = False + # create only if expected cluster peer relation is not present on both source and destination clusters # will error out with appropriate message if peer relationship already exists on either cluster - if source_action == 'create' or destination_action == 'create': + if source_action == 'create' and destination_action == 'create': if not self.module.check_mode: self.cluster_peer_create('source') self.cluster_peer_create('destination') self.na_helper.changed = True + # check and modify IP addresses of the logical interfaces used in peer relation + # on either source or destination cluster + elif self.use_rest and (source_action is None or destination_action is None): + source_changed, destination_changed = False, False + if source_action is None: + if destination_action == 'create' and self.parameters.get('source_cluster_name') is None: + self.module.fail_json(msg='Following option is missing: source_cluster_name') + if not self.module.check_mode: + if source and (source.get('peer-addresses') != self.parameters.get('dest_intercluster_lifs')): + source_changed = True + uuid = source['uuid'] + self.cluster_peer_modify_rest('source', uuid, self.parameters['dest_intercluster_lifs']) + modify['dest_intercluster_lifs'] = self.parameters['dest_intercluster_lifs'] + if destination_action is None: + if source_action == 'create' and self.parameters.get('dest_cluster_name') is None: + self.module.fail_json(msg='Following option is missing: dest_cluster_name') + if not self.module.check_mode: + if destination and (destination.get('peer-addresses') != self.parameters.get('source_intercluster_lifs')): + destination_changed = True + uuid = destination['uuid'] + self.cluster_peer_modify_rest('destination', uuid, self.parameters['source_intercluster_lifs']) + modify['source_intercluster_lifs'] = self.parameters['source_intercluster_lifs'] + self.na_helper.changed = source_changed | destination_changed # delete peer relation in cluster where relation is present else: if source_action == 'delete': @@ -409,8 +459,8 @@ class NetAppONTAPClusterPeer: self.cluster_peer_delete('destination', uuid) self.na_helper.changed = True - result = netapp_utils.generate_result(self.na_helper.changed, extra_responses={'source_action': source_action, - 'destination_action': destination_action}) + result = netapp_utils.generate_result(self.na_helper.changed, modify=modify, extra_responses={'source_action': source_action, + 'destination_action': destination_action}) self.module.exit_json(**result) @@ -419,8 +469,8 @@ def main(): Execute action :return: None """ - community_obj = NetAppONTAPClusterPeer() - community_obj.apply() + cluster_peer_obj = NetAppONTAPClusterPeer() + cluster_peer_obj.apply() if __name__ == '__main__': diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_dns.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_dns.py index 67d23cffd..3c46b0084 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_dns.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_dns.py @@ -259,10 +259,14 @@ class NetAppOntapDns: if error: self.module.fail_json(msg="Error getting DNS service: %s" % error) if record: + if params.get('scope') == 'cluster': + uuid = record.get('uuid') + else: + uuid = self.na_helper.safe_get(record, ['svm', 'uuid']) return { - 'domains': record['domains'], - 'nameservers': record['servers'], - 'uuid': record['svm']['uuid'] + 'domains': record.get('domains'), + 'nameservers': record.get('servers'), + 'uuid': uuid } if self.parameters.get('vserver') and not self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 9, 1): # There is a chance we are working at the cluster level diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_config.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_config.py new file mode 100644 index 000000000..30a80e574 --- /dev/null +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_config.py @@ -0,0 +1,186 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: NetApp, Inc +# 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 + +DOCUMENTATION = """ +module: na_ontap_ems_config +short_description: NetApp ONTAP module to modify EMS configuration. +extends_documentation_fragment: + - netapp.ontap.netapp.na_ontap +version_added: '22.8.0' +author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com> +description: + - Configure event notification and logging for the cluster. +options: + state: + description: + - modify EMS configuration, only present is supported. + choices: ['present'] + type: str + default: present + mail_from: + description: + - The email address that the event notification system uses as the "From" address for email notifications. + type: str + required: false + mail_server: + description: + - The name or IP address of the SMTP server that the event notification system uses to send email notification of events. + type: str + required: false + proxy_url: + description: + - HTTP or HTTPS proxy server URL used by rest-api type EMS notification destinations if your organization uses a proxy. + type: str + required: false + proxy_user: + description: + - User name for the HTTP or HTTPS proxy server if authentication is required. + type: str + required: false + proxy_password: + description: + - Password for HTTP or HTTPS proxy. + type: str + required: false + pubsub_enabled: + description: + - Indicates whether or not events are published to the Publish/Subscribe messaging broker. + - Requires ONTAP 9.10 or later. + type: bool + required: false + +notes: + - Only supported with REST and requires ONTAP 9.6 or later. + - Module is not idempotent when proxy_password is set. +""" + +EXAMPLES = """ + - name: Modify EMS mail config + netapp.ontap.na_ontap_ems_config: + state: present + mail_from: administrator@mycompany.com + mail_server: mail.mycompany.com + pubsub_enabled: true + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + https: true + validate_certs: "{{ validate_certs }}" + + - name: Modify EMS proxy config + netapp.ontap.na_ontap_ems_config: + state: present + proxy_url: http://proxy.example.com:8080 + pubsub_enabled: true + proxy_user: admin + proxy_password: password + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + https: true + validate_certs: "{{ validate_certs }}" +""" + +RETURN = """ +""" + +import traceback +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule +from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic + + +class NetAppOntapEmsConfig: + def __init__(self): + self.argument_spec = netapp_utils.na_ontap_host_argument_spec() + self.argument_spec.update(dict( + state=dict(required=False, type='str', choices=['present'], default='present'), + mail_from=dict(required=False, type='str'), + mail_server=dict(required=False, type='str'), + proxy_url=dict(required=False, type='str'), + proxy_user=dict(required=False, type='str'), + proxy_password=dict(required=False, type='str', no_log=True), + pubsub_enabled=dict(required=False, type='bool') + )) + self.module = AnsibleModule( + argument_spec=self.argument_spec, + supports_check_mode=False + ) + self.uuid = None + self.na_helper = NetAppModule(self.module) + self.parameters = self.na_helper.check_and_set_parameters(self.module) + self.rest_api = netapp_utils.OntapRestAPI(self.module) + self.rest_api.fail_if_not_rest_minimum_version('na_ontap_ems_config:', 9, 6) + self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, None, [['pubsub_enabled', (9, 10, 1)]]) + + def get_ems_config_rest(self): + """Get EMS config details""" + fields = 'mail_from,mail_server,proxy_url,proxy_user' + if 'pubsub_enabled' in self.parameters and self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 10, 1): + fields += ',pubsub_enabled' + record, error = rest_generic.get_one_record(self.rest_api, 'support/ems', None, fields) + if error: + self.module.fail_json(msg="Error fetching EMS config: %s" % to_native(error), exception=traceback.format_exc()) + if record: + return { + 'mail_from': record.get('mail_from'), + 'mail_server': record.get('mail_server'), + 'proxy_url': record.get('proxy_url'), + 'proxy_user': record.get('proxy_user'), + 'pubsub_enabled': record.get('pubsub_enabled') + } + return None + + def modify_ems_config_rest(self, modify): + """Modify EMS config""" + dummy, error = rest_generic.patch_async(self.rest_api, 'support/ems', None, modify) + if error: + self.module.fail_json(msg='Error modifying EMS config: %s.' % to_native(error), exception=traceback.format_exc()) + + def check_proxy_url(self, current): + # GET return the proxy url, if configured, along with port number + # based on the existing config, append port numnber to input url to + # maintain idempotency while modifying config + port = None + if current.get('proxy_url') is not None: + # strip trailing '/' and extract the port no + port = current['proxy_url'].rstrip('/').split(':')[-1] + pos = self.parameters['proxy_url'].rstrip('/').rfind(':') + if self.parameters['proxy_url'][pos + 1] == '/': + # port is not mentioned in input proxy URL + # if port is present in current url configured then add to the input url + if port is not None and port != '': + self.parameters['proxy_url'] = "%s:%s" % (self.parameters['proxy_url'].rstrip('/'), port) + + def apply(self): + current = self.get_ems_config_rest() + if self.parameters.get('proxy_url') not in [None, '']: + self.check_proxy_url(current) + modify = self.na_helper.get_modified_attributes(current, self.parameters) + + password_changed = False + if self.parameters.get('proxy_password') not in [None, '']: + modify['proxy_password'] = self.parameters['proxy_password'] + self.module.warn('Module is not idempotent when proxy_password is set.') + password_changed = True + if (self.na_helper.changed or password_changed) and not self.module.check_mode: + self.modify_ems_config_rest(modify) + result = netapp_utils.generate_result(changed=self.na_helper.changed | password_changed, modify=modify) + self.module.exit_json(**result) + + +def main(): + ems_config = NetAppOntapEmsConfig() + ems_config.apply() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_destination.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_destination.py index 76ddfa31b..599c86c74 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_destination.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_destination.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2022, NetApp, Inc +# (c) 2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ''' @@ -18,7 +18,7 @@ extends_documentation_fragment: version_added: 21.23.0 author: Bartosz Bielawski (@bielawb) <bartek.bielawski@live.com> description: - - Configure EMS destination. Currently certificate authentication for REST is not supported. + - Configure EMS destination. options: state: description: @@ -48,6 +48,57 @@ options: required: true type: list elements: str + certificate: + description: + - Name of the certificate + required: false + type: str + version_added: 22.8.0 + ca: + description: + - Name of the CA certificate + required: false + type: str + version_added: 22.8.0 + syslog: + description: + - The parameter is specified when the EMS destination type is C(syslog). + required: false + version_added: 22.9.0 + type: dict + suboptions: + transport: + choices: [udp_unencrypted, tcp_unencrypted, tcp_encrypted] + description: + - Syslog Transport Protocol. + type: str + default: 'udp_unencrypted' + timestamp_format_override: + choices: [no_override, rfc_3164, iso_8601_local_time, iso_8601_utc] + description: + - Syslog Timestamp Format Override. + type: str + default: 'no_override' + hostname_format_override: + choices: [no_override, fqdn, hostname_only] + description: + - Syslog Hostname Format Override. + type: str + default: 'no_override' + message_format: + choices: [legacy_netapp, rfc_5424] + description: + - Syslog Message Format. + type: str + default: 'legacy_netapp' + port: + description: + - Syslog Port. + type: int + default: 514 +notes: + - Supports check_mode. + - This module only supports REST. ''' EXAMPLES = """ @@ -62,6 +113,38 @@ EXAMPLES = """ username: "{{username}}" password: "{{password}}" + - name: Configure REST EMS destination with a certificate + netapp.ontap.na_ontap_ems_destination: + state: present + name: rest + type: rest_api + filters: ['important_events'] + destination: http://my.rest.api/address + certificate: my_cert + ca: my_cert_ca + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + + - name: Configure REST EMS destination with type syslog + netapp.ontap.na_ontap_ems_destination: + state: present + name: syslog_destination + type: syslog + filters: ['important_events'] + destination: http://my.rest.api/address + certificate: my_cert + ca: my_cert_ca + syslog: + transport: udp_unencrypted + port: 514 + message_format: legacy_netapp + hostname_format_override: no_override + timestamp_format_override: no_override + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + - name: Remove email EMS destination netapp.ontap.na_ontap_ems_destination: state: absent @@ -91,17 +174,31 @@ class NetAppOntapEmsDestination: state=dict(required=False, type='str', choices=['present', 'absent'], default='present'), name=dict(required=True, type='str'), type=dict(required=True, type='str', choices=['email', 'syslog', 'rest_api']), + syslog=dict(required=False, type='dict', + options=dict( + transport=dict(required=False, type='str', choices=['udp_unencrypted', 'tcp_unencrypted', 'tcp_encrypted'], + default='udp_unencrypted'), + port=dict(required=False, type='int', default=514), + message_format=dict(required=False, type='str', choices=['legacy_netapp', 'rfc_5424'], default='legacy_netapp'), + timestamp_format_override=dict(required=False, type='str', + choices=['no_override', 'rfc_3164', 'iso_8601_local_time', 'iso_8601_utc'], default='no_override'), + hostname_format_override=dict(required=False, type='str', choices=['no_override', 'fqdn', 'hostname_only'], default='no_override') + )), destination=dict(required=True, type='str'), - filters=dict(required=True, type='list', elements='str') + filters=dict(required=True, type='list', elements='str'), + certificate=dict(required=False, type='str'), + ca=dict(required=False, type='str'), )) self.module = AnsibleModule( argument_spec=self.argument_spec, + required_together=[('certificate', 'ca')], supports_check_mode=True ) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) self.rest_api = netapp_utils.OntapRestAPI(self.module) - self.use_rest = self.rest_api.is_rest() + partially_supported_rest_properties = [['certificate', (9, 11, 1)], ['syslog', (9, 12, 1)]] + self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, partially_supported_rest_properties=partially_supported_rest_properties) if not self.use_rest: self.module.fail_json(msg='na_ontap_ems_destination is only supported with REST API') @@ -116,8 +213,20 @@ class NetAppOntapEmsDestination: def get_ems_destination(self, name): api = 'support/ems/destinations' - fields = 'name,type,destination,filters.name' - query = dict(name=name, fields=fields) + query = {'name': name, + 'fields': 'type,' + 'destination,' + 'filters.name,' + 'certificate.ca,'} + if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 11, 1): + query['fields'] += 'certificate.name,' + if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 12, 1): + syslog_option_9_12 = ('syslog.transport,' + 'syslog.port,' + 'syslog.format.message,' + 'syslog.format.timestamp_override,' + 'syslog.format.hostname_override,') + query['fields'] += syslog_option_9_12 record, error = rest_generic.get_one_record(self.rest_api, api, query) self.fail_on_error(error, 'fetching EMS destination for %s' % name) if record: @@ -125,8 +234,18 @@ class NetAppOntapEmsDestination: 'name': self.na_helper.safe_get(record, ['name']), 'type': self.na_helper.safe_get(record, ['type']), 'destination': self.na_helper.safe_get(record, ['destination']), - 'filters': None + 'filters': None, + 'certificate': self.na_helper.safe_get(record, ['certificate', 'name']), + 'ca': self.na_helper.safe_get(record, ['certificate', 'ca']), } + if record.get('syslog') is not None: + current['syslog'] = { + 'port': self.na_helper.safe_get(record, ['syslog', 'port']), + 'transport': self.na_helper.safe_get(record, ['syslog', 'transport']), + 'timestamp_format_override': self.na_helper.safe_get(record, ['syslog', 'format', 'timestamp_override']), + 'hostname_format_override': self.na_helper.safe_get(record, ['syslog', 'format', 'hostname_override']), + 'message_format': self.na_helper.safe_get(record, ['syslog', 'format', 'message']), + } # 9.9.0 and earlier versions returns rest-api, convert it to rest_api. if current['type'] and '-' in current['type']: current['type'] = current['type'].replace('-', '_') @@ -135,6 +254,24 @@ class NetAppOntapEmsDestination: return current return None + def get_certificate_serial(self, cert_name): + """Retrieve the serial of a certificate""" + api = 'security/certificates' + query = { + 'scope': "cluster", + 'type': "client", + 'name': cert_name + } + fields = 'serial_number' + record, error = rest_generic.get_one_record(self.rest_api, api, query, fields) + if error: + self.module.fail_json(msg='Error retrieving certificates: %s' % error) + + if not record: + self.module.fail_json(msg='Error certificate not found: %s.' + % (self.parameters['certificate'])) + return record['serial_number'] + def create_ems_destination(self): api = 'support/ems/destinations' name = self.parameters['name'] @@ -144,6 +281,25 @@ class NetAppOntapEmsDestination: 'destination': self.parameters['destination'], 'filters': self.generate_filters_list(self.parameters['filters']) } + + if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 11, 1): + if self.parameters.get('certificate') and self.parameters.get('ca') is not None: + body['certificate'] = { + 'serial_number': self.get_certificate_serial(self.parameters['certificate']), + 'ca': self.parameters['ca'], + } + if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 12, 1): + if self.parameters.get('syslog') is not None: + body['syslog'] = {} + for key, option in [ + ('syslog.port', 'port'), + ('syslog.transport', 'transport'), + ('syslog.format.message', 'message_format'), + ('syslog.format.timestamp_override', 'timestamp_format_override'), + ('syslog.format.hostname_override', 'hostname_format_override') + ]: + if self.parameters['syslog'].get(option) is not None: + body[key] = self.parameters['syslog'][option] dummy, error = rest_generic.post_async(self.rest_api, api, body) self.fail_on_error(error, 'creating EMS destinations for %s' % name) @@ -159,9 +315,25 @@ class NetAppOntapEmsDestination: self.create_ems_destination() else: body = {} + if any(item in modify for item in ['certificate', 'ca']): + body['certificate'] = {} for option in modify: if option == 'filters': body[option] = self.generate_filters_list(modify[option]) + elif option == 'certificate': + body[option]['serial_number'] = self.get_certificate_serial(modify[option]) + elif option == 'ca': + body['certificate']['ca'] = modify[option] + elif option == 'syslog': + for key, option in [ + ('syslog.port', 'port'), + ('syslog.transport', 'transport'), + ('syslog.format.message', 'message_format'), + ('syslog.format.timestamp_override', 'timestamp_format_override'), + ('syslog.format.hostname_override', 'hostname_format_override') + ]: + if option in modify['syslog']: + body[key] = modify['syslog'][option] else: body[option] = modify[option] if body: @@ -170,10 +342,9 @@ class NetAppOntapEmsDestination: self.fail_on_error(error, 'modifying EMS destination for %s' % name) def apply(self): - name = None - modify = None - current = self.get_ems_destination(self.parameters['name']) name = self.parameters['name'] + modify = None + current = self.get_ems_destination(name) cd_action = self.na_helper.get_cd_action(current, self.parameters) if cd_action is None and self.parameters['state'] == 'present': modify = self.na_helper.get_modified_attributes(current, self.parameters) diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_filter.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_filter.py index bdd3a73c3..d6ea223d9 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_filter.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_ems_filter.py @@ -166,67 +166,90 @@ class NetAppOntapEMSFilters: self.module.fail_json(msg='Error deleting EMS filter %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) - def modify_ems_filter(self): - # only variable other than name is rules, so if we hit this we know rules has been changed + def modify_ems_filter(self, desired_rules): + post_api = 'support/ems/filters/%s/rules' % self.parameters['name'] api = 'support/ems/filters' - body = {'rules': self.na_helper.filter_out_none_entries(self.parameters['rules'])} - dummy, error = rest_generic.patch_async(self.rest_api, api, self.parameters['name'], body) - if error: - self.module.fail_json(msg='Error modifying EMS filter %s: %s' % (self.parameters['name'], to_native(error)), - exception=traceback.format_exc()) + if desired_rules['patch_rules'] != []: + patch_body = {'rules': desired_rules['patch_rules']} + dummy, error = rest_generic.patch_async(self.rest_api, api, self.parameters['name'], patch_body) + if error: + self.module.fail_json(msg='Error modifying EMS filter %s: %s' % (self.parameters['name'], to_native(error)), + exception=traceback.format_exc()) + if desired_rules['post_rules'] != []: + for rule in desired_rules['post_rules']: + dummy, error = rest_generic.post_async(self.rest_api, post_api, rule) + if error: + self.module.fail_json(msg='Error modifying EMS filter %s: %s' % (self.parameters['name'], to_native(error)), + exception=traceback.format_exc()) + + def desired_ems_rules(self, current_rules): + # Modify current filter to remove auto added rule of type exclude, from testing it always appears to be the last element + current_rules['rules'] = current_rules['rules'][:-1] + if self.parameters.get('rules'): + input_rules = self.na_helper.filter_out_none_entries(self.parameters['rules']) + for i in range(len(input_rules)): + input_rules[i]['message_criteria']['severities'] = input_rules[i]['message_criteria']['severities'].lower() + matched_idx = [] + patch_rules = [] + post_rules = [] + for rule_dict in current_rules['rules']: + for i in range(len(input_rules)): + if input_rules[i]['index'] == rule_dict['index']: + matched_idx.append(int(input_rules[i]['index'])) + patch_rules.append(input_rules[i]) + break + else: + rule = {'index': rule_dict['index']} + rule['type'] = rule_dict.get('type') + if 'message_criteria' in rule_dict: + rule['message_criteria'] = {} + rule['message_criteria']['severities'] = rule_dict.get('message_criteria').get('severities') + rule['message_criteria']['name_pattern'] = rule_dict.get('message_criteria').get('name_pattern') + patch_rules.append(rule) + for i in range(len(input_rules)): + if int(input_rules[i]['index']) not in matched_idx: + post_rules.append(input_rules[i]) + desired_rules = {'patch_rules': patch_rules, 'post_rules': post_rules} + return desired_rules + return None - def find_modify(self, current): - # The normal modify will not work for 2 reasons - # First ems filter will add a new rule at the end that excludes everything that there isn't a rule for - # Second Options that are not given are returned as '*' in rest + def find_modify(self, current, desired_rules): if not current: return False - # Modify Current to remove auto added rule, from testing it always appears to be the last element - if current.get('rules'): - current['rules'].pop() - # Next check if both have no rules - if current.get('rules') is None and self.parameters.get('rules') is None: + # Next check if either one has no rules + if current.get('rules') is None or desired_rules is None: return False + modify = False + merge_rules = desired_rules['patch_rules'] + desired_rules['post_rules'] # Next let check if rules is the same size if not we need to modify - if len(current.get('rules')) != len(self.parameters.get('rules')): + if len(current.get('rules')) != len(merge_rules): return True - # Next let put the current rules in a dictionary by rule number - current_rules = self.dic_of_rules(current) - # Now we need to compare each field to see if there is a match - modify = False - for rule in self.parameters['rules']: - # allow modify if a desired rule index may not exist in current rules. - # when testing found only index 1, 2 are allowed, if try to set index other than this, let REST throw error. - if current_rules.get(rule['index']) is None: - modify = True - break - # Check if types are the same - if rule['type'].lower() != current_rules[rule['index']]['type'].lower(): - modify = True - break - if rule.get('message_criteria'): - if rule['message_criteria'].get('severities') and rule['message_criteria']['severities'].lower() != \ - current_rules[rule['index']]['message_criteria']['severities'].lower(): - modify = True - break - if rule['message_criteria'].get('name_pattern') and rule['message_criteria']['name_pattern'] != \ - current_rules[rule['index']]['message_criteria']['name_pattern']: - modify = True - break - return modify + for i in range(len(current['rules'])): + # compare each field to see if there is a mismatch + if current['rules'][i]['index'] != merge_rules[i]['index'] or current['rules'][i]['type'] != merge_rules[i]['type']: + return True + else: + # adding default values for fields under message_criteria + if merge_rules[i].get('message_criteria') is None: + merge_rules[i]['message_criteria'] = {'severities': '*', 'name_pattern': '*'} + elif merge_rules[i]['message_criteria'].get('severities') is None: + merge_rules[i]['message_criteria']['severities'] = '*' + elif merge_rules[i]['message_criteria'].get('name_pattern') is None: + merge_rules[i]['message_criteria']['name_pattern'] = '*' - def dic_of_rules(self, current): - rules = {} - for rule in current['rules']: - rules[rule['index']] = rule - return rules + if current['rules'][i].get('message_criteria').get('name_pattern') != merge_rules[i].get('message_criteria').get('name_pattern'): + return True + if current['rules'][i].get('message_criteria').get('severities') != merge_rules[i].get('message_criteria').get('severities'): + return True + return modify def apply(self): current = self.get_ems_filter() cd_action, modify = None, False cd_action = self.na_helper.get_cd_action(current, self.parameters) - if cd_action is None: - modify = self.find_modify(current) + if cd_action is None and self.parameters['state'] == 'present': + desired_rules = self.desired_ems_rules(current) + modify = self.find_modify(current, desired_rules) if modify: self.na_helper.changed = True if self.na_helper.changed and not self.module.check_mode: @@ -235,7 +258,7 @@ class NetAppOntapEMSFilters: if cd_action == 'delete': self.delete_ems_filter() if modify: - self.modify_ems_filter() + self.modify_ems_filter(desired_rules) result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify) self.module.exit_json(**result) diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_export_policy_rule.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_export_policy_rule.py index 8b9414074..660ef6825 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_export_policy_rule.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_export_policy_rule.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2018-2022, NetApp, Inc +# (c) 2018-2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ''' @@ -169,6 +169,7 @@ EXAMPLES = """ state: present name: default123 rule_index: 100 + vserver: ci_dev client_match: 0.0.0.0/0 anonymous_user_id: 65521 ro_rule: ntlm @@ -732,7 +733,8 @@ class NetAppontapExportRule: elif modify: self.modify_export_policy_rule(modify, current['rule_index']) - self.module.exit_json(changed=self.na_helper.changed) + result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify) + self.module.exit_json(**result) def main(): diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_file_security_permissions_acl.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_file_security_permissions_acl.py index 277986466..92514d994 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_file_security_permissions_acl.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_file_security_permissions_acl.py @@ -209,7 +209,7 @@ EXAMPLES = """ path: "{{ file_mount_path }}" validate_changes: warn access: access_allow - # Note, wihout quotes, use a single backslash in AD user names + # Note, without quotes, use a single backslash in AD user names # with quotes, it needs to be escaped as a double backslash # user: "ANSIBLE_CIFS\\user1" # we can't show an example with a single backslash as this is a python file, but it works in YAML. @@ -466,7 +466,8 @@ class NetAppOntapFileSecurityPermissionsACL: if modify: self.modify_file_security_permissions_acl() self.validate_changes(cd_action, modify) - self.module.exit_json(changed=self.na_helper.changed) + result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify) + self.module.exit_json(**result) def validate_changes(self, cd_action, modify): if self.parameters['validate_changes'] == 'ignore': diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_igroup_initiator.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_igroup_initiator.py index 7280eb181..18d25f4dd 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_igroup_initiator.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_igroup_initiator.py @@ -1,7 +1,7 @@ #!/usr/bin/python ''' This is an Ansible module for ONTAP, to manage initiators in an Igroup - (c) 2019-2022, NetApp, Inc + (c) 2019-2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ''' @@ -192,7 +192,7 @@ class NetAppOntapIgroupInitiator(object): dummy, error = rest_generic.post_async(self.rest_api, api, body) else: query = {'allow_delete_while_mapped': self.parameters['force_remove']} - dummy, error = rest_generic.delete_async(self.rest_api, api, initiator_name, query) + dummy, error = rest_generic.delete_async(self.rest_api, api, initiator_name.lower(), query) if error: self.module.fail_json(msg="Error modifying igroup initiator %s: %s" % (initiator_name, error)) @@ -201,7 +201,7 @@ class NetAppOntapIgroupInitiator(object): for initiator in self.parameters['names']: present = None initiator = self.na_helper.sanitize_wwn(initiator) - if initiator in initiators: + if initiator.lower() in initiators: present = True cd_action = self.na_helper.get_cd_action(present, self.parameters) if self.na_helper.changed and not self.module.check_mode: @@ -209,7 +209,8 @@ class NetAppOntapIgroupInitiator(object): self.modify_initiator(initiator, 'igroup-add') elif cd_action == 'delete': self.modify_initiator(initiator, 'igroup-remove') - self.module.exit_json(changed=self.na_helper.changed) + result = netapp_utils.generate_result(self.na_helper.changed, cd_action) + self.module.exit_json(**result) def main(): diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_info.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_info.py index 6591cc9cd..f9060ffc9 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_info.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_info.py @@ -213,7 +213,7 @@ options: - missing_vserver_api_error - most likely the API is available at cluster level but not vserver level. - rpc_error - some queries are failing because the node cannot reach another node in the cluster. - key_error - a query is failing because the returned data does not contain an expected key. - - for key errors, make sure to report this in Slack. It may be a change in a new ONTAP version. + - for key errors, make sure to report this in Discord. It may be a change in a new ONTAP version. - other_error - anything not in the above list. - always will continue on any error, never will fail on any error, they cannot be used with any other keyword. type: list diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_kerberos_realm.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_kerberos_realm.py index 9cb4c346b..27362f220 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_kerberos_realm.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_kerberos_realm.py @@ -1,7 +1,7 @@ #!/usr/bin/python ''' (c) 2019, Red Hat, Inc -(c) 2019-2022, NetApp, Inc +(c) 2019-2023, NetApp, Inc GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ''' @@ -65,7 +65,7 @@ options: description: - The clock skew in minutes is the tolerance for accepting tickets with time stamps that do not exactly match the host's system clock. - The default for this parameter is '5' minutes. - - This option is not supported with REST. + - Supported from ONTAP 9.13.1 in REST. type: str comment: @@ -77,14 +77,14 @@ options: description: - IP address of the host where the Kerberos administration daemon is running. This is usually the master KDC. - If this parameter is omitted, the address specified in kdc_ip is used. - - This option is not supported with REST. + - Supported from ONTAP 9.13.1 in REST. type: str admin_server_port: description: - The TCP port on the Kerberos administration server where the Kerberos administration service is running. - The default for this parmater is '749'. - - This option is not supported with REST. + - Supported from ONTAP 9.13.1 in REST. type: str pw_server_ip: @@ -92,14 +92,14 @@ options: - IP address of the host where the Kerberos password-changing server is running. - Typically, this is the same as the host indicated in the adminserver-ip. - If this parameter is omitted, the IP address in kdc-ip is used. - - This option is not supported with REST. + - Supported from ONTAP 9.13.1 in REST. type: str pw_server_port: description: - The TCP port on the Kerberos password-changing server where the Kerberos password-changing service is running. - The default for this parameter is '464'. - - This option is not supported with REST. + - Supported from ONTAP 9.13.1 in REST. type: str ad_server_ip: @@ -145,6 +145,21 @@ EXAMPLES = ''' username: "{{ netapp_username }}" password: "{{ netapp_password }}" + - name: Create kerberos realm other kdc vendor - REST + netapp.ontap.na_ontap_kerberos_realm: + state: present + realm: 'EXAMPLE.COM' + vserver: 'vserver1' + kdc_ip: '1.2.3.4' + kdc_vendor: 'other' + pw_server_ip: '0.0.0.0' + pw_server_port: '5' + admin_server_ip: '1.2.3.4' + admin_server_port: '2' + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + ''' RETURN = ''' @@ -195,8 +210,9 @@ class NetAppOntapKerberosRealm: self.parameters = self.na_helper.set_parameters(self.module.params) # Set up Rest API self.rest_api = netapp_utils.OntapRestAPI(self.module) - unsupported_rest_properties = ['admin_server_ip', 'admin_server_port', 'clock_skew', 'pw_server_ip', 'pw_server_port'] - self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, unsupported_rest_properties) + partially_supported_rest_properties = [['admin_server_ip', (9, 13, 1)], ['admin_server_port', (9, 13, 1)], ['clock_skew', (9, 13, 1)], + ['pw_server_ip', (9, 13, 1)], ['pw_server_port', (9, 13, 1)]] + self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, None, partially_supported_rest_properties) self.svm_uuid = None if not self.use_rest: @@ -350,7 +366,7 @@ class NetAppOntapKerberosRealm: params = { 'name': self.parameters['realm'], 'svm.name': self.parameters['vserver'], - 'fields': 'kdc,ad_server,svm,comment' + 'fields': 'kdc,ad_server,svm,comment,password_server,admin_server,clock_skew' } record, error = rest_generic.get_one_record(self.rest_api, api, params) if error: @@ -363,7 +379,12 @@ class NetAppOntapKerberosRealm: 'kdc_vendor': self.na_helper.safe_get(record, ['kdc', 'vendor']), 'ad_server_ip': self.na_helper.safe_get(record, ['ad_server', 'address']), 'ad_server_name': self.na_helper.safe_get(record, ['ad_server', 'name']), - 'comment': self.na_helper.safe_get(record, ['comment']) + 'comment': self.na_helper.safe_get(record, ['comment']), + 'pw_server_ip': self.na_helper.safe_get(record, ['password_server', 'address']), + 'pw_server_port': str(self.na_helper.safe_get(record, ['password_server', 'port'])), + 'admin_server_ip': self.na_helper.safe_get(record, ['admin_server', 'address']), + 'admin_server_port': str(self.na_helper.safe_get(record, ['admin_server', 'port'])), + 'clock_skew': str(self.na_helper.safe_get(record, ['clock_skew'])) } return None @@ -379,9 +400,20 @@ class NetAppOntapKerberosRealm: body['kdc.port'] = self.parameters['kdc_port'] if self.parameters.get('comment'): body['comment'] = self.parameters['comment'] - if self.parameters['kdc_vendor'] == 'microsoft': + if self.parameters.get('ad_server_ip'): body['ad_server.address'] = self.parameters['ad_server_ip'] + if self.parameters.get('ad_server_name'): body['ad_server.name'] = self.parameters['ad_server_name'] + if self.parameters.get('admin_server_port'): + body['admin_server.port'] = self.parameters['admin_server_port'] + if self.parameters.get('pw_server_port'): + body['password_server.port'] = self.parameters['pw_server_port'] + if self.parameters.get('clock_skew'): + body['clock_skew'] = self.parameters['clock_skew'] + if self.parameters.get('admin_server_ip'): + body['admin_server.address'] = self.parameters['admin_server_ip'] + if self.parameters.get('pw_server_ip'): + body['password_server.address'] = self.parameters['pw_server_ip'] dummy, error = rest_generic.post_async(self.rest_api, api, body) if error: self.module.fail_json(msg='Error creating Kerberos Realm configuration %s: %s' % (self.parameters['realm'], to_native(error))) @@ -401,6 +433,16 @@ class NetAppOntapKerberosRealm: body['ad_server.address'] = modify['ad_server_ip'] if modify.get('ad_server_name'): body['ad_server.name'] = modify['ad_server_name'] + if modify.get('admin_server_ip'): + body['admin_server.address'] = modify['admin_server_ip'] + if modify.get('admin_server_port'): + body['admin_server.port'] = modify['admin_server_port'] + if modify.get('pw_server_ip'): + body['password_server.address'] = modify['pw_server_ip'] + if modify.get('pw_server_port'): + body['password_server.port'] = modify['pw_server_port'] + if modify.get('clock_skew'): + body['clock_skew'] = modify['clock_skew'] dummy, error = rest_generic.patch_async(self.rest_api, api, self.parameters['realm'], body) if error: self.module.fail_json(msg='Error modifying Kerberos Realm %s: %s' % (self.parameters['realm'], to_native(error))) @@ -416,7 +458,6 @@ class NetAppOntapKerberosRealm: current = self.get_krbrealm() cd_action = self.na_helper.get_cd_action(current, self.parameters) modify = self.na_helper.get_modified_attributes(current, self.parameters) - if self.na_helper.changed and not self.module.check_mode: if cd_action == 'create': self.create_krbrealm() diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_login_messages.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_login_messages.py index 099cea8b9..49ad2080a 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_login_messages.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_login_messages.py @@ -154,12 +154,15 @@ class NetAppOntapLoginMessages: } def form_current(self, record): + show_cluster_motd = True + if record and record.get('show_cluster_message') is not None: + show_cluster_motd = record.get('show_cluster_message') return_result = { 'banner': '', 'motd_message': '', # we need the SVM UUID to add banner or motd if they are not present 'uuid': record['uuid'] if record else self.get_svm_uuid(self.parameters.get('vserver')), - 'show_cluster_motd': record.get('show_cluster_message') if record else None + 'show_cluster_motd': show_cluster_motd } # by default REST adds a trailing \n if no trailing \n set in desired message/banner. # rstip \n only when desired message/banner does not have trailing \n to preserve idempotency. diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun.py index c0fb796f7..03ea4f592 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2017-2022, NetApp, Inc +# (c) 2017-2023, NetApp, Inc # 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 @@ -48,6 +48,14 @@ options: - Not allowed if san_application_template is present. type: str + qtree_name: + description: + - Specifies the name of the Qtree that contains the new LUN. + - Not allowed if san_application_template is present. + - Only supported with REST. + version_added: 22.8.0 + type: str + size: description: - The size of the LUN in C(size_unit). @@ -336,6 +344,7 @@ class NetAppOntapLUN: force_remove=dict(required=False, type='bool', default=False), force_remove_fenced=dict(type='bool'), flexvol_name=dict(type='str'), + qtree_name=dict(type='str'), vserver=dict(required=True, type='str'), os_type=dict(required=False, type='str', aliases=['ostype']), qos_policy_group=dict(required=False, type='str'), @@ -418,6 +427,8 @@ class NetAppOntapLUN: if use_application_template: if self.parameters.get('flexvol_name') is not None: self.module.fail_json(msg="'flexvol_name' option is not supported when san_application_template is present") + if self.parameters.get('qtree_name') is not None: + self.module.fail_json(msg="'qtree_name' option is not supported when san_application_template is present") name = self.na_helper.safe_get(self.parameters, ['san_application_template', 'name'], allow_sparse_dict=False) rest_app = RestApplication(self.rest_api, self.parameters['vserver'], name) elif self.parameters.get('flexvol_name') is None: @@ -914,6 +925,8 @@ class NetAppOntapLUN: query['name'] = lun_path else: query['location.volume.name'] = self.parameters['flexvol_name'] + if self.parameters.get('qtree_name') is not None: + query['location.qtree.name'] = self.parameters['qtree_name'] record, error = rest_generic.get_0_or_more_records(self.rest_api, api, query) if error: if lun_path is not None: @@ -956,6 +969,8 @@ class NetAppOntapLUN: } if self.parameters.get('flexvol_name') is not None: body['location.volume.name'] = self.parameters['flexvol_name'] + if self.parameters.get('qtree_name') is not None: + body['location.qtree.name'] = self.parameters['qtree_name'] if self.parameters.get('os_type') is not None: body['os_type'] = self.parameters['os_type'] if self.parameters.get('size') is not None: @@ -978,7 +993,9 @@ class NetAppOntapLUN: If the name start with a slash we will assume it a path and use it as the name """ if not self.parameters['name'].startswith('/') and self.parameters.get('flexvol_name') is not None: - # if it dosn't start with a slash and we have a flexvol name we will use it to build the path + # if it dosn't start with a slash we will use flexvol name and/or qtree name to build the path + if self.parameters.get('qtree_name') is not None: + return '/vol/%s/%s/%s' % (self.parameters['flexvol_name'], self.parameters['qtree_name'], self.parameters['name']) return '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['name']) return self.parameters['name'] diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map.py index 5bdbc17c8..67acb4b38 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map.py @@ -2,7 +2,7 @@ """ this is lun mapping module - (c) 2018-2022, NetApp, Inc + (c) 2018-2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) """ @@ -146,9 +146,7 @@ class NetAppOntapLUNMap: ], supports_check_mode=True ) - self.result = dict( - changed=False, - ) + self.lun_info = dict() self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) @@ -337,19 +335,19 @@ class NetAppOntapLUNMap: if modify: self.module.fail_json(msg="Modification of lun_map not allowed") if self.parameters['state'] == 'present' and lun_details: - self.result.update(lun_details) - self.result['changed'] = self.na_helper.changed + self.lun_info.update(lun_details) if self.na_helper.changed and not self.module.check_mode: if cd_action == 'create': self.create_lun_map() if cd_action == 'delete': self.delete_lun_map() - self.module.exit_json(**self.result) + result = netapp_utils.generate_result(self.na_helper.changed, cd_action, extra_responses=self.lun_info) + self.module.exit_json(**result) def main(): - v = NetAppOntapLUNMap() - v.apply() + lun_mapping = NetAppOntapLUNMap() + lun_mapping.apply() if __name__ == '__main__': diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map_reporting_nodes.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map_reporting_nodes.py index 607c8c430..5763a3d66 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map_reporting_nodes.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_lun_map_reporting_nodes.py @@ -1,7 +1,7 @@ #!/usr/bin/python """ - (c) 2018-2022, NetApp, Inc + (c) 2018-2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) """ @@ -62,9 +62,9 @@ notes: EXAMPLES = """ - name: Create Lun Map reporting nodes netapp.ontap.na_ontap_lun_map_reporting_nodes: - hostname: 172.21.121.82 - username: admin - password: netapp1! + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" https: true validate_certs: false vserver: vs1 @@ -75,9 +75,9 @@ EXAMPLES = """ - name: Delete Lun Map reporting nodes netapp.ontap.na_ontap_lun_map_reporting_nodes: - hostname: 172.21.121.82 - username: admin - password: netapp1! + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" https: true validate_certs: false vserver: vs1 @@ -248,21 +248,27 @@ class NetAppOntapLUNMapReportingNodes: else: nodes_to_add = list() nodes_to_delete = [node for node in self.parameters['nodes'] if node in reporting_nodes] + cd_action = None changed = len(nodes_to_add) > 0 or len(nodes_to_delete) > 0 if changed and not self.module.check_mode: if nodes_to_add: + cd_action = 'add_node' if self.use_rest: for node in nodes_to_add: self.add_lun_map_reporting_nodes_rest(node) else: self.add_lun_map_reporting_nodes(nodes_to_add) if nodes_to_delete: + cd_action = 'remove_node' if self.use_rest: for node in nodes_to_delete: self.remove_lun_map_reporting_nodes_rest(node) else: self.remove_lun_map_reporting_nodes(nodes_to_delete) - self.module.exit_json(changed=changed, reporting_nodes=reporting_nodes, nodes_to_add=nodes_to_add, nodes_to_delete=nodes_to_delete) + result = netapp_utils.generate_result(changed, cd_action, extra_responses={'reporting_nodes': reporting_nodes, + 'nodes_to_add': nodes_to_add, + 'nodes_to_delete': nodes_to_delete}) + self.module.exit_json(**result) def main(): diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_name_mappings.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_name_mappings.py index 3aa4f2df5..f6afb1546 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_name_mappings.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_name_mappings.py @@ -273,7 +273,11 @@ class NetAppOntapNameMappings: self.delete_name_mappings_rest() elif modify or reindex: self.modify_name_mappings_rest(modify, reindex) - self.module.exit_json(changed=self.na_helper.changed) + if reindex: + modify['new_index'] = self.parameters.get('index') + modify['from_index'] = self.parameters['from_index'] + result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify) + self.module.exit_json(**result) def main(): diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_net_ifgrp.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_net_ifgrp.py index 6ba4083e5..45035ed64 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_net_ifgrp.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_net_ifgrp.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2018-2021, NetApp, Inc +# (c) 2018-2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ''' @@ -295,7 +295,7 @@ class NetAppOntapIfGrp: } return return_value - def get_if_grp_rest(self, ports, allow_partial_match): + def get_if_grp_rest(self, ports, allow_partial_match, force=False): api = 'network/ethernet/ports' query = { 'type': 'lag', @@ -303,7 +303,7 @@ class NetAppOntapIfGrp: } fields = 'name,node,uuid,broadcast_domain,lag' error = None - if not self.current_records: + if not self.current_records or force: self.current_records, error = rest_generic.get_0_or_more_records(self.rest_api, api, query, fields) if error: self.module.fail_json(msg=error) @@ -342,6 +342,7 @@ class NetAppOntapIfGrp: current = { 'node': record['node']['name'], 'uuid': record['uuid'], + 'name': record['name'], 'ports': current_port_list } if record.get('broadcast_domain'): @@ -496,6 +497,7 @@ class NetAppOntapIfGrp: def apply(self): # for a LAG, rename is equivalent to adding/removing ports from an existing LAG. current, exact_match, modify, rename = None, True, None, None + response = None if not self.use_rest: current = self.get_if_grp() elif self.use_rest: @@ -523,6 +525,9 @@ class NetAppOntapIfGrp: uuid = current['uuid'] if current and self.use_rest else None if cd_action == 'create': self.create_if_grp() + # While using REST, fetch the name of the created LAG and return as response in result + if self.use_rest: + response, exact_match = self.get_if_grp_rest(self.parameters.get('ports'), allow_partial_match=True, force=True) elif cd_action == 'delete': self.delete_if_grp(uuid) elif modify: @@ -530,7 +535,7 @@ class NetAppOntapIfGrp: self.modify_ports_rest(modify, uuid) else: self.modify_ports(current_ports['ports']) - result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify) + result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify, response=response) self.module.exit_json(**result) diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_nfs.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_nfs.py index a1315df1b..9c4c5911f 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_nfs.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_nfs.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2018-2022, NetApp, Inc +# (c) 2018-2023, NetApp, Inc # 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 @@ -555,6 +555,9 @@ class NetAppONTAPNFS: if error: self.module.fail_json(msg='Error getting nfs services for SVM %s: %s' % (self.parameters['vserver'], to_native(error)), exception=traceback.format_exc()) + if self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 11, 0): + if record and 'default_user' not in record.get('windows'): + record['windows']['default_user'] = None return self.format_get_nfs_service_rest(record) if record else record def format_get_nfs_service_rest(self, record): diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_node.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_node.py index ced6f44be..f88a8827b 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_node.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_node.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2018-2019, NetApp, Inc +# (c) 2018-2023, NetApp, Inc # 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 @@ -83,6 +83,7 @@ import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_ut from ansible_collections.netapp.ontap.plugins.module_utils.netapp import OntapRestAPI from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule import ansible_collections.netapp.ontap.plugins.module_utils.rest_response_helpers as rrh +import copy HAS_NETAPP_LIB = netapp_utils.has_netapp_lib() @@ -218,7 +219,7 @@ class NetAppOntapNode(object): def apply(self): from_exists = None - modify = None + modify, modify_dict = None, None uuid = None current = self.get_node(self.parameters['name']) if current is None and 'from_name' in self.parameters: @@ -235,8 +236,10 @@ class NetAppOntapNode(object): allowed_options = ['name', 'location'] if not self.use_rest: allowed_options.append('asset_tag') - if modify and any(x not in allowed_options for x in modify): - self.module.fail_json(msg='Too many modified attributes found: %s, allowed: %s' % (modify, allowed_options)) + if modify: + if any(x not in allowed_options for x in modify): + self.module.fail_json(msg='Too many modified attributes found: %s, allowed: %s' % (modify, allowed_options)) + modify_dict = copy.deepcopy(modify) if current is None and from_exists is None: msg = 'from_name: %s' % self.parameters.get('from_name') if 'from_name' in self.parameters \ else 'name: %s' % self.parameters['name'] @@ -249,8 +252,8 @@ class NetAppOntapNode(object): modify.pop('name') if modify: self.modify_node(modify, uuid) - - self.module.exit_json(changed=self.na_helper.changed) + result = netapp_utils.generate_result(self.na_helper.changed, modify=modify_dict) + self.module.exit_json(**result) def main(): diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_qos_policy_group.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_qos_policy_group.py index 8628efd46..b092848ac 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_qos_policy_group.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_qos_policy_group.py @@ -145,6 +145,22 @@ options: required: false choices: ['any', '4k', '8k', '16k', '32k', '64k', '128k'] version_added: 22.6.0 + expected_iops_allocation: + description: + - Specifies the size to be used to calculate expected IOPS per TB. + - Supported only with REST; requires ONTAP 9.10.1 or later. + type: str + required: false + choices: ['used_space', 'allocated_space'] + version_added: 22.8.0 + peak_iops_allocation: + description: + - Specifies the size to be used to calculate peak IOPS per TB. + - Supported only with REST; requires ONTAP 9.10.1 or later. + type: str + required: false + choices: ['used_space', 'allocated_space'] + version_added: 22.8.0 ''' EXAMPLES = """ @@ -224,6 +240,18 @@ EXAMPLES = """ expected_iops: 200 peak_iops: 500 + - name: modify adaptive qos policy group in REST. + netapp.ontap.na_ontap_qos_policy_group: + state: present + name: adaptive_policy + vserver: policy_vserver + hostname: 10.193.78.30 + username: admin + password: netapp1! + use_rest: always + adaptive_qos_options: + expected_iops_allocation: used_space + peak_iops_allocation: allocated_space """ RETURN = """ @@ -268,7 +296,9 @@ class NetAppOntapQosPolicyGroup: absolute_min_iops=dict(required=True, type='int'), expected_iops=dict(required=True, type='int'), peak_iops=dict(required=True, type='int'), - block_size=dict(required=False, type='str', choices=['any', '4k', '8k', '16k', '32k', '64k', '128k']) + block_size=dict(required=False, type='str', choices=['any', '4k', '8k', '16k', '32k', '64k', '128k']), + expected_iops_allocation=dict(required=False, type='str', choices=['used_space', 'allocated_space']), + peak_iops_allocation=dict(required=False, type='str', choices=['used_space', 'allocated_space']) )) )) @@ -297,9 +327,11 @@ class NetAppOntapQosPolicyGroup: if not self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 8) and \ self.na_helper.safe_get(self.parameters, ['fixed_qos_options', 'min_throughput_mbps']): self.module.fail_json(msg="Minimum version of ONTAP for 'fixed_qos_options.min_throughput_mbps' is (9, 8, 0)") + + ontap_9_10_adaptive_options = ['block_size', 'expected_iops_allocation', 'peak_iops_allocation'] if not self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 10, 1) and \ - self.na_helper.safe_get(self.parameters, ['adaptive_qos_options', 'block_size']): - self.module.fail_json(msg="Minimum version of ONTAP for 'adaptive_qos_options.block_size' is (9, 10, 1)") + any(self.na_helper.safe_get(self.parameters, ['adaptive_qos_options', option]) for option in ontap_9_10_adaptive_options): + self.module.fail_json(msg='Error: %s' % self.rest_api.options_require_ontap_version(ontap_9_10_adaptive_options, version='9.10.1')) self.uuid = None if not self.use_rest: @@ -383,7 +415,8 @@ class NetAppOntapQosPolicyGroup: if 'adaptive' in record: current['adaptive_qos_options'] = {} - for adaptive_qos_option in ['absolute_min_iops', 'expected_iops', 'peak_iops', 'block_size']: + for adaptive_qos_option in ['absolute_min_iops', 'expected_iops', 'peak_iops', 'block_size', + 'expected_iops_allocation', 'peak_iops_allocation']: current['adaptive_qos_options'][adaptive_qos_option] = record['adaptive'].get(adaptive_qos_option) return current @@ -479,6 +512,11 @@ class NetAppOntapQosPolicyGroup: if 'fixed_qos_options' in modify: body['fixed'] = modify['fixed_qos_options'] else: + if 'block_size' not in self.na_helper.safe_get(modify, ['adaptive_qos_options']) and \ + self.na_helper.safe_get(self.parameters, ['adaptive_qos_options', 'block_size']) is None: + # if block_size is not to be modified then remove it from the params + # to avoid error with block_size option during modification of other adaptive qos options + del self.parameters['adaptive_qos_options']['block_size'] body['adaptive'] = self.parameters['adaptive_qos_options'] dummy, error = rest_generic.patch_async(self.rest_api, api, self.uuid, body) if error: diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_rest_info.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_rest_info.py index b1b5b6dae..029cf40d0 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_rest_info.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_rest_info.py @@ -293,7 +293,7 @@ options: version_added: '21.9.0' owning_resource: description: - - Some resources cannot be accessed directly. You need to select them based on the owner or parent. For instance, volume for a snaphot. + - Some resources cannot be accessed directly. You need to select them based on the owner or parent. For instance, volume for a snapshot. - The following subsets require an owning resource, and the following suboptions when uuid is not present. - <storage/volumes/snapshots> B(volume_name) is the volume name, B(svm_name) is the owning vserver name for the volume. - <protocols/nfs/export-policies/rules> B(policy_name) is the name of the policy, B(svm_name) is the owning vserver name for the policy, @@ -310,6 +310,11 @@ options: type: list elements: str version_added: '21.23.0' + hal_linking: + description: + - if false, HAL-encoded links are disabled in the REST calls. + default: true + type: bool ''' EXAMPLES = ''' @@ -488,6 +493,7 @@ class NetAppONTAPGatherInfo(object): use_python_keys=dict(type='bool', default=False), owning_resource=dict(type='dict', required=False), ignore_api_errors=dict(type='list', elements='str', required=False), + hal_linking=dict(required=False, type='bool', default=True), )) self.module = AnsibleModule( @@ -532,7 +538,9 @@ class NetAppONTAPGatherInfo(object): for each in self.parameters['parameters']: data[each] = self.parameters['parameters'][each] - gathered_ontap_info, error = self.rest_api.get(api, data) + accept_header = 'application/hal+json' if self.parameters.get('hal_linking') else 'application/json' + headers = self.rest_api.build_headers(accept=accept_header) + gathered_ontap_info, error = self.rest_api.get(api, data, headers=headers) if not error: return gathered_ontap_info @@ -1091,6 +1099,8 @@ class NetAppONTAPGatherInfo(object): def add_uuid_subsets(self, get_ontap_subset_info): params = self.parameters.get('owning_resource') + owning_resource_supported_subsets = ['storage/volumes/snapshots', 'protocols/nfs/export-policies/rules', + 'protocols/vscan/on-access-policies', 'protocols/vscan/on-demand-policies', 'protocols/vscan/scanner-pools'] if 'gather_subset' in self.parameters: if 'storage/volumes/snapshots' in self.parameters['gather_subset']: self.check_error_values('storage/volumes/snapshots', params, ['volume_name', 'svm_name']) @@ -1112,6 +1122,9 @@ class NetAppONTAPGatherInfo(object): self.add_vserver_owning_resource('protocols/vscan/on-demand-policies', params, 'protocols/vscan/%s/on-demand-policies', get_ontap_subset_info) if 'protocols/vscan/scanner-pools' in self.parameters['gather_subset']: self.add_vserver_owning_resource('protocols/vscan/scanner-pools', params, 'protocols/vscan/%s/scanner-pools', get_ontap_subset_info) + owning_resource_warning = any(subset not in owning_resource_supported_subsets for subset in self.parameters['gather_subset']) + if owning_resource_warning and params is not None: + self.module.warn("Kindly refer to Ansible documentation to check the subsets that support option 'owning_resource'.") return get_ontap_subset_info def add_vserver_owning_resource(self, subset, params, api, get_ontap_subset_info): diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_restit.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_restit.py index 7bfd63b71..fd56307f3 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_restit.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_restit.py @@ -1,6 +1,6 @@ #!/usr/bin/python ''' -# (c) 2020, NetApp, Inc +# (c) 2020-2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ''' @@ -372,13 +372,15 @@ class NetAppONTAPRestAPI(object): def apply(self): ''' calls the api and returns json output ''' + changed_status = False if self.method.upper() == 'GET' else True + if self.module.check_mode: status_code, response = None, {'check_mode': 'would run %s %s' % (self.method, self.api)} elif self.wait_for_completion: status_code, response = self.run_api_async() else: status_code, response = self.run_api() - self.module.exit_json(changed=True, status_code=status_code, response=response) + self.module.exit_json(changed=changed_status, status_code=status_code, response=response) def main(): diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_s3_services.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_s3_services.py index ff5feb722..e8d8ed994 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_s3_services.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_s3_services.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2018-2022, NetApp, Inc +# (c) 2018-2023, NetApp, Inc # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -86,6 +86,19 @@ EXAMPLES = """ RETURN = """ +s3_service_info: + description: Returns S3 service response. + returned: on creation or modification of S3 service + type: dict + sample: '{ + "s3_service_info": { + "name": "Service1", + "enabled": false, + "certificate_name": "testSVM_177966509ABA4EC6", + "users": [{"name": "root"}, {"name": "user1", "access_key": "IWE711019OW02ZB3WH6Q"}], + "svm": {"name": "testSVM", "uuid": "39c2a5a0-35e2-11ee-b8da-005056b37403"}} + } + }' """ import traceback @@ -116,21 +129,19 @@ class NetAppOntapS3Services: self.svm_uuid = None self.na_helper = NetAppModule(self.module) self.parameters = self.na_helper.check_and_set_parameters(self.module) - self.rest_api = OntapRestAPI(self.module) - partially_supported_rest_properties = [] # TODO: Remove if there nothing here - self.use_rest = self.rest_api.is_rest(partially_supported_rest_properties=partially_supported_rest_properties, - parameters=self.parameters) - + self.use_rest = self.rest_api.is_rest() self.rest_api.fail_if_not_rest_minimum_version('na_ontap_s3_services', 9, 8) - def get_s3_service(self): + def get_s3_service(self, extra_field=False): api = 'protocols/s3/services' fields = ','.join(('name', 'enabled', 'svm.uuid', 'comment', 'certificate.name')) + if extra_field: + fields += ',users' params = { 'name': self.parameters['name'], @@ -192,25 +203,48 @@ class NetAppOntapS3Services: self.svm_uuid = record['svm']['uuid'] return record + def parse_response(self, response): + if response is not None: + users_info = [] + options = ['name', 'access_key', 'secret_key'] + for user_info in response.get('users'): + info = {} + for option in options: + if user_info.get(option) is not None: + info[option] = user_info.get(option) + users_info.append(info) + return { + 'name': response.get('name'), + 'enabled': response.get('enabled'), + 'certificate_name': response.get('certificate_name'), + 'users': users_info, + 'svm': {'name': self.na_helper.safe_get(response, ['svm', 'name']), + 'uuid': self.na_helper.safe_get(response, ['svm', 'uuid'])} + } + return None + def apply(self): current = self.get_s3_service() - cd_action, modify = None, None + cd_action, modify, response = None, None, None cd_action = self.na_helper.get_cd_action(current, self.parameters) if cd_action is None: modify = self.na_helper.get_modified_attributes(current, self.parameters) if self.na_helper.changed and not self.module.check_mode: if cd_action == 'create': self.create_s3_service() + response = self.get_s3_service(True) if cd_action == 'delete': self.delete_s3_service() if modify: self.modify_s3_service(modify) - result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify) + response = self.get_s3_service(True) + message = self.parse_response(response) + result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify, extra_responses={'s3_service_info': message}) self.module.exit_json(**result) def main(): - '''Apply volume operations from playbook''' + '''Apply S3 service operations from playbook''' obj = NetAppOntapS3Services() obj.apply() diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_security_certificates.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_security_certificates.py index c7131fe5e..e6fc74e92 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_security_certificates.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_security_certificates.py @@ -378,8 +378,11 @@ class NetAppOntapSecurityCertificates: for key in required_keys + optional_keys: if self.parameters.get(key) is not None: body[key] = self.parameters[key] + params = { + "return_records": "true" + } api = "security/certificates" - message, error = self.rest_api.post(api, body) + message, error = self.rest_api.post(api, body, params) if error: if self.parameters.get('svm') is None and error.get('target') == 'uuid': error['target'] = 'cluster' @@ -399,7 +402,10 @@ class NetAppOntapSecurityCertificates: for key in optional_keys: if self.parameters.get(key) is not None: body[key] = self.parameters[key] - message, error = self.rest_api.post(api, body) + params = { + "return_records": "true" + } + message, error = self.rest_api.post(api, body, params) if error: self.module.fail_json(msg="Error signing certificate: %s" % error) return message diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_service_policy.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_service_policy.py index f2969f720..395bbb695 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_service_policy.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_service_policy.py @@ -64,13 +64,14 @@ options: choices: ['cluster', 'svm'] known_services: description: - - List of known services in 9.11.1 + - List of known services in 9.12.1 - An error is raised if any service in C(services) is not in this list or C(new_services). - Modify this list to restrict the services you want to support if needed. default: [cluster_core, intercluster_core, management_core, management_autosupport, management_bgp, management_ems, management_https, management_http, management_ssh, management_portmap, data_core, data_nfs, data_cifs, data_flexcache, data_iscsi, data_s3_server, data_dns_server, data_fpolicy_client, management_ntp_client, management_dns_client, management_ad_client, management_ldap_client, management_nis_client, - management_snmp_server, management_rsh_server, management_telnet_server, management_ntp_server, data_nvme_tcp, backup_ndmp_control] + management_snmp_server, management_rsh_server, management_telnet_server, management_ntp_server, data_nvme_tcp, backup_ndmp_control, + management_log_forwarding] type: list elements: str version_added: 22.0.0 @@ -184,7 +185,7 @@ class NetAppOntapServicePolicy: 'data_flexcache', 'data_iscsi', 'data_s3_server', 'data_dns_server', 'data_fpolicy_client', 'management_ntp_client', 'management_dns_client', 'management_ad_client', 'management_ldap_client', 'management_nis_client', 'management_snmp_server', 'management_rsh_server', 'management_telnet_server', 'management_ntp_server', - 'data_nvme_tcp', 'backup_ndmp_control']), + 'data_nvme_tcp', 'backup_ndmp_control', 'management_log_forwarding']), additional_services=dict(type='list', elements='str') )) diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapmirror.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapmirror.py index 26254e03b..e53358041 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapmirror.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapmirror.py @@ -595,6 +595,7 @@ class NetAppONTAPSnapmirror(object): self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) + self.policy_type = None self.new_style = False # when deleting, ignore previous errors, but report them if delete fails self.previous_errors = [] @@ -1051,7 +1052,8 @@ class NetAppONTAPSnapmirror(object): resync SnapMirror based on relationship state """ if self.use_rest: - self.snapmirror_mod_init_resync_break_quiesce_resume_rest(state="snapmirrored") + state = 'in_sync' if self.policy_type == 'sync' else 'snapmirrored' + self.snapmirror_mod_init_resync_break_quiesce_resume_rest(state=state) else: options = {'destination-location': self.parameters['destination_path']} snapmirror_resync = netapp_utils.zapi.NaElement.create_node_with_children('snapmirror-resync', **options) @@ -1067,7 +1069,8 @@ class NetAppONTAPSnapmirror(object): resume SnapMirror based on relationship state """ if self.use_rest: - return self.snapmirror_mod_init_resync_break_quiesce_resume_rest(state="snapmirrored") + state = 'in_sync' if self.policy_type == 'sync' else 'snapmirrored' + return self.snapmirror_mod_init_resync_break_quiesce_resume_rest(state=state) options = {'destination-location': self.parameters['destination_path']} snapmirror_resume = netapp_utils.zapi.NaElement.create_node_with_children('snapmirror-resume', **options) @@ -1482,7 +1485,7 @@ class NetAppONTAPSnapmirror(object): destination = self.parameters['destination_path'] api = 'snapmirror/relationships' - fields = 'uuid,state,transfer.state,transfer.uuid,policy.name,unhealthy_reason.message,healthy,source' + fields = 'uuid,state,transfer.state,transfer.uuid,policy.name,policy.type,unhealthy_reason.message,healthy,source' if 'schedule' in self.parameters: fields += ',transfer_schedule' options = {'destination.path': destination, 'fields': fields} @@ -1499,9 +1502,10 @@ class NetAppONTAPSnapmirror(object): snap_info['status'] = self.na_helper.safe_get(record, ['transfer', 'state']) self.parameters['current_transfer_status'] = self.na_helper.safe_get(record, ['transfer', 'state']) snap_info['policy'] = self.na_helper.safe_get(record, ['policy', 'name']) + self.policy_type = self.na_helper.safe_get(record, ['policy', 'type']) # REST API supports only Extended Data Protection (XDP) SnapMirror relationship snap_info['relationship_type'] = 'extended_data_protection' - # initilized to avoid name keyerror + # initialized to avoid name keyerror snap_info['current_transfer_type'] = "" snap_info['max_transfer_rate'] = "" if 'unhealthy_reason' in record: @@ -1741,8 +1745,8 @@ class NetAppONTAPSnapmirror(object): def main(): """Execute action""" - community_obj = NetAppONTAPSnapmirror() - community_obj.apply() + snapmirror_obj = NetAppONTAPSnapmirror() + snapmirror_obj.apply() if __name__ == '__main__': diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapshot_policy.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapshot_policy.py index 1d271657a..ee1a63be5 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapshot_policy.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snapshot_policy.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2018-2022, NetApp, Inc +# (c) 2018-2023, NetApp, Inc # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -611,6 +611,7 @@ class NetAppOntapSnapshotPolicy(object): api = 'storage/snapshot-policies/%s/schedules' % current['uuid'] schedule_info = self.get_snapshot_schedule_rest(current) delete_schedules, modify_schedules, add_schedules = [], [], [] + retain_schedules_count = 0 if 'snapmirror_label' in self.parameters: snapmirror_labels = self.parameters['snapmirror_label'] @@ -629,6 +630,8 @@ class NetAppOntapSnapshotPolicy(object): schedule_name = self.safe_strip(schedule_name) if schedule_name not in [item.strip() for item in self.parameters['schedule']]: delete_schedules.append(schedule_uuid) + else: + retain_schedules_count += 1 # Identify schedules to be modified or added for schedule_name, count, snapmirror_label, prefix in zip(self.parameters['schedule'], self.parameters['count'], snapmirror_labels, prefixes): @@ -668,9 +671,11 @@ class NetAppOntapSnapshotPolicy(object): body['prefix'] = prefix add_schedules.append(body) - # Delete N-1 schedules no longer required. Must leave 1 schedule in policy + # Delete N schedules no longer required if there is at least 1 schedule is to be retained + # Otherwise, delete N-1 schedules no longer required as policy must have at least 1 schedule # at any one time. Delete last one afterwards. - while len(delete_schedules) > 1: + count = 0 if retain_schedules_count > 0 else 1 + while len(delete_schedules) > count: schedule_uuid = delete_schedules.pop() record, error = rest_generic.delete_async(self.rest_api, api, schedule_uuid) if error is not None: diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snmp.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snmp.py index c1f278e0d..acde02da2 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snmp.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snmp.py @@ -3,7 +3,7 @@ create SNMP module to add/delete/modify SNMP user """ -# (c) 2018-2021, NetApp, Inc +# (c) 2018-2023, NetApp, Inc # 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 @@ -12,7 +12,7 @@ __metaclass__ = type DOCUMENTATION = ''' author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com> description: - - "Create/Delete SNMP community" + - Create/Delete SNMP user. extends_documentation_fragment: - netapp.ontap.netapp.na_ontap module: na_ontap_snmp @@ -20,21 +20,60 @@ options: access_control: choices: ['ro'] description: - - "Access control for the community. The only supported value is 'ro' (read-only). Ignored with REST" + - Access control for the community. The only supported value is 'ro' (read-only). + - Ignored with REST. default: 'ro' type: str - community_name: + snmp_username: description: - - "The name of the SNMP community to manage." + - The name of the SNMP user to manage. required: true type: str + version_added: 22.8.0 state: choices: ['present', 'absent'] description: - - "Whether the specified SNMP community should exist or not." + - Whether the specified SNMP user should exist or not. default: 'present' type: str -short_description: NetApp ONTAP SNMP community + authentication_method: + choices: ['community', 'usm', 'both'] + description: + - Authentication method for SNMP user. + - Only supported with REST. The default value is community. + type: str + version_added: 22.8.0 + snmpv3: + description: + - Specify only when C(authentication_method) is either C(usm) or C(both). + - This option defines the SNMPv3 credentials for an SNMPv3 user or also called usm user. + - Only supported with REST. + type: dict + version_added: 22.8.0 + suboptions: + authentication_password: + description: + - Authentication protocol password. + type: str + required: true + authentication_protocol: + choices: ['none', 'md5', 'sha', 'sha2_256'] + description: + - Authentication protocol for SNMPv3. + default: 'none' + type: str + privacy_password: + description: + - Privacy protocol password. + type: str + required: true + privacy_protocol: + choices: ['none', 'des', 'aes128'] + description: + - Privacy protocol for SNMPv3. + default: 'none' + type: str +short_description: NetApp ONTAP SNMP user version_added: 2.6.0 ''' @@ -42,34 +81,60 @@ EXAMPLES = """ - name: Create SNMP community (ZAPI only) netapp.ontap.na_ontap_snmp: state: present - community_name: communityName + snmp_username: communityName access_control: 'ro' use_rest: never hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" + - name: Create SNMP community (snmpv1 or snmpv2) (REST only) netapp.ontap.na_ontap_snmp: state: present - community_name: communityName + snmp_username: communityName use_rest: always hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" + - name: Create SNMP user (snmpv3) (REST only) + netapp.ontap.na_ontap_snmp: + state: present + snmp_username: username + use_rest: always + authentication_method: usm + snmpv3: + authentication_protocol: sha + authentication_password: humTdumt*@t0nAwa21 + privacy_protocol: aes128 + privacy_password: p@**GOandCLCt*300 + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + - name: Delete SNMP community (ZAPI only) netapp.ontap.na_ontap_snmp: state: absent - community_name: communityName + snmp_username: communityName access_control: 'ro' use_rest: never hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" + - name: Delete SNMP community (snmpv1 or snmpv2) (REST only) netapp.ontap.na_ontap_snmp: state: absent - community_name: communityName + snmp_username: communityName + use_rest: always + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + + - name: Delete SNMP user (snmpv3) (REST only) + netapp.ontap.na_ontap_snmp: + state: absent + snmp_username: username use_rest: always hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" @@ -96,8 +161,17 @@ class NetAppONTAPSnmp(object): self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec.update(dict( state=dict(required=False, type='str', choices=['present', 'absent'], default='present'), - community_name=dict(required=True, type='str'), + snmp_username=dict(required=True, type='str'), access_control=dict(required=False, type='str', choices=['ro'], default='ro'), + authentication_method=dict(required=False, type='str', choices=['community', 'usm', 'both']), + snmpv3=dict(required=False, type='dict', + options=dict( + authentication_password=dict(required=True, type='str', no_log=True), + privacy_protocol=dict(required=False, type='str', choices=['none', 'des', 'aes128'], default='none'), + authentication_protocol=dict(required=False, type='str', choices=['none', 'md5', 'sha', 'sha2_256'], default='none'), + privacy_password=dict(required=True, type='str', no_log=True), + ) + ) )) self.module = AnsibleModule( @@ -113,18 +187,29 @@ class NetAppONTAPSnmp(object): self.rest_api = OntapRestAPI(self.module) self.use_rest = self.rest_api.is_rest() + self.unsupported_zapi_properties = ['authentication_method', 'snmpv3', 'authentication_protocol', 'authentication_password', 'privacy_protocol', + 'privacy_password'] + + if self.use_rest: + if self.parameters.get('authentication_method') == 'community' and 'snmpv3' in self.parameters: + self.module.fail_json("SNMPv3 user can be created when 'authentication_method' is either 'usm' or 'both'") + if not self.use_rest: if HAS_NETAPP_LIB is False: self.module.fail_json(msg="the python NetApp-Lib module is required") - else: - self.server = netapp_utils.setup_na_ontap_zapi(module=self.module) + + for unsupported_zapi_property in self.unsupported_zapi_properties: + if self.parameters.get(unsupported_zapi_property) is not None: + msg = "Error: %s option is not supported with ZAPI. It can only be used with REST." % unsupported_zapi_property + self.module.fail_json(msg=msg) + self.server = netapp_utils.setup_na_ontap_zapi(module=self.module) def invoke_snmp_community(self, zapi): """ Invoke zapi - add/delete take the same NaElement structure """ snmp_community = netapp_utils.zapi.NaElement.create_node_with_children( - zapi, **{'community': self.parameters['community_name'], + zapi, **{'community': self.parameters['snmp_username'], 'access-control': self.parameters['access_control']}) try: self.server.invoke_successfully(snmp_community, enable_tunneling=True) @@ -135,7 +220,7 @@ class NetAppONTAPSnmp(object): action = 'deleting' else: action = 'unexpected' - self.module.fail_json(msg='Error %s community %s: %s' % (action, self.parameters['community_name'], to_native(error)), + self.module.fail_json(msg='Error %s community %s: %s' % (action, self.parameters['snmp_username'], to_native(error)), exception=traceback.format_exc()) def get_snmp(self): @@ -151,19 +236,19 @@ class NetAppONTAPSnmp(object): self.module.fail_json(msg=to_native(error), exception=traceback.format_exc()) if result.get_child_by_name('communities') is not None: for snmp_entry in result.get_child_by_name('communities').get_children(): - community_name = snmp_entry.get_child_content('community') - if community_name == self.parameters['community_name']: + snmp_username = snmp_entry.get_child_content('community') + if snmp_username == self.parameters['snmp_username']: return { - 'community_name': snmp_entry.get_child_content('community'), + 'snmp_username': snmp_entry.get_child_content('community'), 'access_control': snmp_entry.get_child_content('access-control'), } return None def get_snmp_rest(self): # There can be SNMPv1, SNMPv2 (called community) or - # SNMPv3 local or SNMPv3 remote (called users) + # SNMPv3 (called usm users) api = 'support/snmp/users' - params = {'name': self.parameters['community_name'], + params = {'name': self.parameters['snmp_username'], 'fields': 'name,engine_id'} message, error = self.rest_api.get(api, params) record, error = rrh.check_for_0_or_1_records(api, message, error) @@ -171,56 +256,56 @@ class NetAppONTAPSnmp(object): self.module.fail_json(msg=error) if record: # access control does not exist in rest - return dict(community_name=record['name'], engine_id=record['engine_id'], access_control='ro') + return dict(snmp_username=record['name'], engine_id=record['engine_id'], access_control='ro') return None - def add_snmp_community(self): + def add_snmp_user(self): """ - Adds a SNMP community + Add a SNMP user """ if self.use_rest: - self.add_snmp_community_rest() + self.add_snmp_rest() else: self.invoke_snmp_community('snmp-community-add') - def add_snmp_community_rest(self): + def add_snmp_rest(self): api = 'support/snmp/users' - params = {'name': self.parameters['community_name'], - 'authentication_method': 'community'} - message, error = self.rest_api.post(api, params) + self.parameters['authentication_method'] = self.parameters.get('authentication_method', 'community') + body = { + 'name': self.parameters['snmp_username'], + 'authentication_method': self.parameters['authentication_method'] + } + if self.parameters.get('authentication_method') == 'usm' or self.parameters.get('authentication_method') == 'both': + if self.parameters.get('snmpv3'): + body['snmpv3'] = self.parameters['snmpv3'] + message, error = self.rest_api.post(api, body) if error: self.module.fail_json(msg=error) - def delete_snmp_community(self, current=None): + def delete_snmp_user(self, current=None): """ - Delete a SNMP community + Delete a SNMP user """ if self.use_rest: - self.delete_snmp_community_rest(current) + self.delete_snmp_rest(current) else: self.invoke_snmp_community('snmp-community-delete') - def delete_snmp_community_rest(self, current): - api = 'support/snmp/users/' + current['engine_id'] + '/' + self.parameters["community_name"] + def delete_snmp_rest(self, current): + api = 'support/snmp/users/' + current['engine_id'] + '/' + self.parameters["snmp_username"] dummy, error = self.rest_api.delete(api) if error: self.module.fail_json(msg=error) def apply(self): - """ - Apply action to SNMP community - This module is not idempotent: - Add doesn't fail the playbook if user is trying - to add an already existing snmp community - """ - # TODO: This module should of been called snmp_community has it only deals with community and not snmp + # TODO: This module should have been called snmp_community as it only deals with community and not snmp current = self.get_snmp() cd_action = self.na_helper.get_cd_action(current, self.parameters) if self.na_helper.changed and not self.module.check_mode: if cd_action == 'create': - self.add_snmp_community() + self.add_snmp_user() elif cd_action == 'delete': - self.delete_snmp_community(current) + self.delete_snmp_user(current) result = netapp_utils.generate_result(self.na_helper.changed, cd_action) self.module.exit_json(**result) diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snmp_config.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snmp_config.py new file mode 100644 index 000000000..83fed1655 --- /dev/null +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_snmp_config.py @@ -0,0 +1,142 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: NetApp, Inc +# 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 + +DOCUMENTATION = """ +module: na_ontap_snmp_config +short_description: NetApp ONTAP module to modify SNMP configuration. +extends_documentation_fragment: + - netapp.ontap.netapp.na_ontap +version_added: '22.9.0' +author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com> +description: + - Modify cluster wide SNMP configuration. + - Enable or disable SNMP on a cluster. +options: + state: + description: + - Modify SNMP configuration, only present is supported. + choices: ['present'] + type: str + default: present + enabled: + description: + - Specifies whether to enable or disable SNMP. + type: bool + required: false + auth_traps_enabled: + description: + - Specifies whether to enable or disable SNMP authentication traps. + type: bool + required: false + traps_enabled: + description: + - Specifies whether to enable or disable SNMP traps. + - Requires ONTAP 9.10.1 or later. + type: bool + required: false + +notes: + - Only supported with REST and requires ONTAP 9.7 or later. +""" + +EXAMPLES = """ + - name: Disable SNMP on cluster + netapp.ontap.na_ontap_snmp_config: + state: present + enabled: false + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + https: true + validate_certs: "{{ validate_certs }}" + + - name: Modify SNMP configuration + netapp.ontap.na_ontap_snmp_config: + state: present + auth_traps_enabled: true + traps_enabled: true + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + https: true + validate_certs: "{{ validate_certs }}" +""" + +RETURN = """ +""" + +import traceback +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule +from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic + + +class NetAppOntapSNMPConfig: + def __init__(self): + self.argument_spec = netapp_utils.na_ontap_host_argument_spec() + self.argument_spec.update(dict( + state=dict(required=False, type='str', choices=['present'], default='present'), + enabled=dict(required=False, type='bool'), + auth_traps_enabled=dict(required=False, type='bool'), + traps_enabled=dict(required=False, type='bool') + )) + self.module = AnsibleModule( + argument_spec=self.argument_spec, + supports_check_mode=True + ) + self.uuid = None + self.na_helper = NetAppModule(self.module) + self.parameters = self.na_helper.check_and_set_parameters(self.module) + self.rest_api = netapp_utils.OntapRestAPI(self.module) + self.rest_api.fail_if_not_rest_minimum_version('na_ontap_snmp_config:', 9, 7) + self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, None, [['traps_enabled', (9, 10, 1)]]) + + def get_snmp_config_rest(self): + """Retrieve cluster wide SNMP configuration""" + fields = 'enabled' + if self.parameters.get('auth_traps_enabled') is not None: + fields += ',auth_traps_enabled' + if 'traps_enabled' in self.parameters and self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 10, 1): + fields += ',traps_enabled' + record, error = rest_generic.get_one_record(self.rest_api, 'support/snmp', None, fields) + if error: + self.module.fail_json(msg="Error fetching SNMP configuration: %s" % to_native(error), exception=traceback.format_exc()) + if record: + return { + 'enabled': record.get('enabled'), + 'auth_traps_enabled': record.get('auth_traps_enabled'), + 'traps_enabled': record.get('traps_enabled') + } + return None + + def modify_snmp_config_rest(self, modify): + """Update cluster wide SNMP configuration""" + dummy, error = rest_generic.patch_async(self.rest_api, 'support/snmp', None, modify) + if error: + self.module.fail_json(msg='Error modifying SNMP configuration: %s.' % to_native(error), exception=traceback.format_exc()) + + def apply(self): + current = self.get_snmp_config_rest() + modify = self.na_helper.get_modified_attributes(current, self.parameters) + + if self.na_helper.changed and not self.module.check_mode: + self.modify_snmp_config_rest(modify) + result = netapp_utils.generate_result(changed=self.na_helper.changed, modify=modify) + self.module.exit_json(**result) + + +def main(): + snmp_config = NetAppOntapSNMPConfig() + snmp_config.apply() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_storage_auto_giveback.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_storage_auto_giveback.py index 4446371d1..298b07c4b 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_storage_auto_giveback.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_storage_auto_giveback.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2021, NetApp, Inc +# (c) 2021-2023, NetApp, Inc # 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 @@ -128,8 +128,8 @@ class NetAppOntapStorageAutoGiveback(object): if error is None and records is not None: return_value = { 'name': message['records'][0]['node'], - 'auto_giveback_enabled': message['records'][0]['auto_giveback'], - 'auto_giveback_after_panic_enabled': message['records'][0]['auto_giveback_after_panic'] + 'auto_giveback_enabled': message['records'][0].get('auto_giveback'), + 'auto_giveback_after_panic_enabled': message['records'][0].get('auto_giveback_after_panic') } if error: @@ -228,12 +228,13 @@ class NetAppOntapStorageAutoGiveback(object): def apply(self): current = self.get_storage_auto_giveback() - self.na_helper.get_modified_attributes(current, self.parameters) + modify = self.na_helper.get_modified_attributes(current, self.parameters) if self.na_helper.changed: if not self.module.check_mode: self.modify_storage_auto_giveback() - self.module.exit_json(changed=self.na_helper.changed) + result = netapp_utils.generate_result(self.na_helper.changed, modify=modify) + self.module.exit_json(**result) def main(): diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_svm.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_svm.py index 9d5fc6c66..28a10252c 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_svm.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_svm.py @@ -170,7 +170,7 @@ options: description: - If this is set to true, an SVM administrator can manage the NDMP service - If it is false, only the cluster administrator can manage the service. - - Requires ONTAP 9.7 or later. + - Requires ONTAP 9.10.1 or later. type: bool version_added: 21.24.0 aggr_list: @@ -469,9 +469,9 @@ class NetAppOntapSVM(): ] if errors: self.module.fail_json(msg='Error - %s' % ' '.join(errors)) - if use_rest and self.parameters.get('services') and not self.parameters.get('allowed_protocols') and self.parameters['services'].get('ndmp')\ - and not self.rest_api.meets_rest_minimum_version(use_rest, 9, 7): - self.module.fail_json(msg=self.rest_api.options_require_ontap_version('ndmp', '9.7', use_rest=use_rest)) + if use_rest and self.parameters.get('services') and not self.parameters.get('allowed_protocols'): + if self.parameters['services'].get('ndmp') and not self.rest_api.meets_rest_minimum_version(use_rest, 9, 10, 1): + self.module.fail_json(msg=self.rest_api.options_require_ontap_version('ndmp', '9.10.1', use_rest=use_rest)) if self.parameters.get('services') and not use_rest: self.module.fail_json(msg=self.rest_api.options_require_ontap_version('services', use_rest=use_rest)) if self.parameters.get('web'): diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_user.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_user.py index 7fada8ac6..e95ab492a 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_user.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_user.py @@ -654,7 +654,8 @@ class NetAppOntapUser: error = self.patch_account(owner_uuid, username, body) if error: if 'message' in error and self.is_repeated_password(error['message']): - # if the password is reused, assume idempotency + # if the password is reused, assume idempotency but show a warning + self.module.warn('Password was not changed: %s' % error['message']) return False self.module.fail_json(msg='Error while updating user password: %s' % error) return True diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_volume.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_volume.py index 7ca007c29..8f5d827e7 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_volume.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_volume.py @@ -4,17 +4,11 @@ # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -''' -na_ontap_volume -''' - from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = ''' - module: na_ontap_volume - short_description: NetApp ONTAP manage volumes. extends_documentation_fragment: - netapp.ontap.netapp.na_ontap @@ -307,6 +301,7 @@ options: - This is an advanced option, the default is False. - Enable the visible '.snapshot' directory that is normally present at system internal mount points. - This value also turns on access to all other '.snapshot' directories in the volume. + - This option is supported in REST for ONTAP 9.13.1 or later with ONTAP collection version 22.8.0 or later. type: bool version_added: 2.8.0 @@ -318,9 +313,28 @@ options: since it prevents writes to the inode file for the volume from contending with reads from other files. - This field should be used carefully. - That is, use this field when you know in advance that the correct access time for inodes will not be needed for files on that volume. + - This option is supported in REST for ONTAP 9.8 or later with ONTAP collection version 22.8.0 or later. type: bool version_added: 2.8.0 + vol_nearly_full_threshold_percent: + description: + - Specifies the percentage at which the volume is considered nearly full, and above which an EMS warning will be generated. + - The default value is 95%. The maximum value for this option is 99%. + - Setting this threshold to 0 disables the volume nearly full space alerts. + - Supported only with in REST for ONTAP 9.9 or later. + type: int + version_added: 22.8.0 + + vol_full_threshold_percent: + description: + - Specifies the percentage at which the volume is considered full, and above which a critical EMS error will be generated. + - The default value is 98%. The maximum value for this option is 100%. + - Setting this threshold to 0 disables the volume full space alerts. + - Supported only with in REST for ONTAP 9.9 or later. + type: int + version_added: 22.8.0 + wait_for_completion: description: - Set this parameter to 'true' for synchronous execution during create (wait until volume status is online) @@ -346,7 +360,7 @@ options: description: - Volume move and encryption operations might take longer time to complete. - With C(wait_for_completion) set, module will wait for time set in this option for volume move and encryption to complete. - - If time exipres, module exit and the operation may still running. + - If time exipres, module exit and the operation may still be running. - Default is set to 10 minutes. default: 600 type: int @@ -459,6 +473,7 @@ options: - A dictionary for the auto delete options and values. - Supported options include 'state', 'commitment', 'trigger', 'target_free_space', 'delete_order', 'defer_delete', 'prefix', 'destroy_list'. + - All the above mentioned options except 'destroy_list' are supported in REST for ONTAP 9.13.1 or later with ONTAP collection version 22.8.0 or later. - Option 'state' determines if the snapshot autodelete is currently enabled for the volume. Possible values are 'on' and 'off'. - Option 'commitment' determines the snapshots which snapshot autodelete is allowed to delete to get back space. Possible values are 'try', 'disrupt' and 'destroy'. @@ -880,6 +895,33 @@ EXAMPLES = """ retention: default: "{{ 60 | netapp.ontap.iso8601_duration_from_seconds }}" + - name: Create volume with snapshot-auto-delete options - REST + netapp.ontap.na_ontap_volume: + state: present + name: test_vol + aggregate_name: "{{ aggr }}" + size: 20 + size_unit: mb + snapshot_auto_delete: + state: 'on' + trigger: volume + delete_order: "oldest_first" + defer_delete: "user_created" + commitment: "try" + target_free_space: 30 + prefix: "my_prefix" + wait_for_completion: true + + - name: Modify volume - REST + netapp.ontap.na_ontap_volume: + state: present + name: test_vol + aggregate_name: "{{ aggr }}" + snapdir_access: false + snapshot_auto_delete: + state: 'on' + target_free_space: 25 + """ RETURN = """ @@ -929,6 +971,8 @@ class NetAppOntapVolume: aggr_list_multiplier=dict(required=False, type='int'), snapdir_access=dict(required=False, type='bool'), atime_update=dict(required=False, type='bool'), + vol_nearly_full_threshold_percent=dict(required=False, type='int'), + vol_full_threshold_percent=dict(required=False, type='int'), auto_provision_as=dict(choices=['flexgroup'], required=False, type='str'), wait_for_completion=dict(required=False, type='bool', default=False), time_out=dict(required=False, type='int', default=180), @@ -1021,19 +1065,20 @@ class NetAppOntapVolume: netapp_utils.POW2_BYTE_MAP[self.parameters['size_unit']] self.validate_snapshot_auto_delete() self.rest_api = netapp_utils.OntapRestAPI(self.module) - unsupported_rest_properties = ['atime_update', - 'cutover_action', + unsupported_rest_properties = ['cutover_action', 'encrypt-destination', 'force_restore', 'nvfail_enabled', 'preserve_lun_ids', - 'snapdir_access', - 'snapshot_auto_delete', + 'destroy_list', 'space_slo', 'vserver_dr_protection'] - partially_supported_rest_properties = [['efficiency_policy', (9, 7)], ['tiering_minimum_cooling_days', (9, 8)], ['analytics', (9, 8)], - ['tags', (9, 13, 1)]] - self.unsupported_zapi_properties = ['sizing_method', 'logical_space_enforcement', 'logical_space_reporting', 'snaplock', 'analytics', 'tags'] + partially_supported_rest_properties = [['efficiency_policy', (9, 7)], ['tiering_minimum_cooling_days', (9, 8)], + ['analytics', (9, 8)], ['atime_update', (9, 8)], + ['vol_nearly_full_threshold_percent', (9, 9)], ['vol_full_threshold_percent', (9, 9)], + ['tags', (9, 13, 1)], ['snapdir_access', (9, 13, 1)], ['snapshot_auto_delete', (9, 13, 1)]] + self.unsupported_zapi_properties = ['sizing_method', 'logical_space_enforcement', 'logical_space_reporting', 'snaplock', + 'analytics', 'tags', 'vol_nearly_full_threshold_percent', 'vol_full_threshold_percent'] self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, unsupported_rest_properties, partially_supported_rest_properties) if not self.use_rest: @@ -1313,6 +1358,25 @@ class NetAppOntapVolume: self.na_helper.fail_on_error(error) return response + def wait_for_volume_online(self, sleep_time=10): + # round off time_out + retries = (self.parameters['time_out'] + 5) // 10 + is_online = None + errors = [] + while not is_online and retries > 0: + try: + current = self.get_volume() + is_online = None if current is None else current['is_online'] + except KeyError as err: + # get_volume may receive incomplete data as the volume is being created + errors.append(repr(err)) + if not is_online: + time.sleep(sleep_time) + retries -= 1 + if not is_online: + errors.append("Timeout after %s seconds" % self.parameters['time_out']) + self.module.fail_json(msg='Error waiting for volume %s to come online: %s' % (self.parameters['name'], str(errors))) + def create_volume(self): '''Create ONTAP volume''' if self.rest_app: @@ -1333,24 +1397,7 @@ class NetAppOntapVolume: exception=traceback.format_exc()) if self.parameters.get('wait_for_completion'): - # round off time_out - retries = (self.parameters['time_out'] + 5) // 10 - is_online = None - errors = [] - while not is_online and retries > 0: - try: - current = self.get_volume() - is_online = None if current is None else current['is_online'] - except KeyError as err: - # get_volume may receive incomplete data as the volume is being created - errors.append(repr(err)) - if not is_online: - time.sleep(10) - retries -= 1 - if not is_online: - errors.append("Timeout after %s seconds" % self.parameters['time_out']) - self.module.fail_json(msg='Error waiting for volume %s to come online: %s' - % (self.parameters['name'], str(errors))) + self.wait_for_volume_online() return None def create_volume_async(self): @@ -1948,12 +1995,13 @@ class NetAppOntapVolume: 'snapshot_policy', 'percent_snapshot_space', 'snapdir_access', 'atime_update', 'volume_security_style', 'nvfail_enabled', 'space_slo', 'qos_policy_group', 'qos_adaptive_policy_group', 'vserver_dr_protection', 'comment', 'logical_space_enforcement', 'logical_space_reporting', 'tiering_minimum_cooling_days', - 'snaplock', 'max_files', 'analytics', 'tags']: + 'snaplock', 'max_files', 'analytics', 'tags', 'snapshot_auto_delete', 'vol_nearly_full_threshold_percent', + 'vol_full_threshold_percent']: self.volume_modify_attributes(modify) break if 'snapshot_auto_delete' in attributes and not self.use_rest: - # Rest doesn't support any snapshot_auto_delete option other than is_autodelete_enabled. For now i've completely - # disabled this in rest + # Rest didn't support snapshot_auto_delete prior to ONTAP 9.13.1; for supported ONTAP versions, + # modification for this parameter is handled by calling volume_modify_attributes function. self.set_snapshot_auto_delete() # don't mount or unmount when offline if modify.get('junction_path'): @@ -2265,6 +2313,8 @@ class NetAppOntapVolume: auto_delete_info = current.pop('snapshot_auto_delete', None) # ignore small changes in volume size or inode maximum by adjusting self.parameters['size'] or self.parameters['max_files'] self.adjust_sizes(current, after_create) + if 'type' in self.parameters: + self.parameters['type'] = self.parameters['type'].lower() modify = self.na_helper.get_modified_attributes(current, self.parameters) if modify is not None and 'type' in modify: msg = "Error: volume type was not set properly at creation time." if after_create else \ @@ -2393,6 +2443,16 @@ class NetAppOntapVolume: params['fields'] += 'analytics,' if self.parameters.get('tags'): params['fields'] += '_tags,' + if self.parameters.get('atime_update') is not None: + params['fields'] += 'access_time_enabled,' + if self.parameters.get('snapdir_access') is not None: + params['fields'] += 'snapshot_directory_access_enabled,' + if self.parameters.get('snapshot_auto_delete') is not None: + params['fields'] += 'space.snapshot.autodelete,' + if self.parameters.get('vol_nearly_full_threshold_percent') is not None: + params['fields'] += 'space.nearly_full_threshold_percent,' + if self.parameters.get('vol_full_threshold_percent') is not None: + params['fields'] += 'space.full_threshold_percent,' record, error = rest_generic.get_one_record(self.rest_api, api, params) if error: @@ -2432,6 +2492,8 @@ class NetAppOntapVolume: if error: self.module.fail_json(msg='Error creating volume %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) + if self.parameters.get('wait_for_completion'): + self.wait_for_volume_online(sleep_time=5) def create_volume_body_rest(self): body = { @@ -2464,7 +2526,7 @@ class NetAppOntapVolume: if self.parameters.get('comment') is not None: body['comment'] = self.parameters['comment'] if self.parameters.get('type') is not None: - body['type'] = self.parameters['type'] + body['type'] = self.parameters['type'].lower() if self.parameters.get('percent_snapshot_space') is not None: body['space.snapshot.reserve_percent'] = self.parameters['percent_snapshot_space'] if self.parameters.get('language') is not None: @@ -2514,6 +2576,13 @@ class NetAppOntapVolume: def bool_to_online(item): return 'online' if item else 'offline' + @staticmethod + def enabled_to_bool(item, reverse=False): + """ convertes on/off to true/false or vice versa """ + if reverse: + return 'on' if item else 'off' + return True if item == 'on' else False + def modify_volume_body_rest(self, params): body = {} for key, option, transform in [ @@ -2533,7 +2602,11 @@ class NetAppOntapVolume: ('space.logical_space.reporting', 'logical_space_reporting', None), ('tiering.min_cooling_days', 'tiering_minimum_cooling_days', None), ('state', 'is_online', self.bool_to_online), - ('_tags', 'tags', None) + ('_tags', 'tags', None), + ('snapshot_directory_access_enabled', 'snapdir_access', None), + ('access_time_enabled', 'atime_update', None), + ('space.nearly_full_threshold_percent', 'vol_nearly_full_threshold_percent', None), + ('space.full_threshold_percent', 'vol_full_threshold_percent', None), ]: value = self.parameters.get(option) if value is not None and transform: @@ -2557,6 +2630,22 @@ class NetAppOntapVolume: sl_dict.pop('type', None) if sl_dict: body['snaplock'] = sl_dict + + if params and params.get('snapshot_auto_delete') is not None: + for key, option, transform in [ + ('space.snapshot.autodelete.trigger', 'trigger', None), + ('space.snapshot.autodelete.target_free_space', 'target_free_space', None), + ('space.snapshot.autodelete.delete_order', 'delete_order', None), + ('space.snapshot.autodelete.commitment', 'commitment', None), + ('space.snapshot.autodelete.defer_delete', 'defer_delete', None), + ('space.snapshot.autodelete.prefix', 'prefix', None), + ('space.snapshot.autodelete.enabled', 'state', self.enabled_to_bool), + ]: + if params and params['snapshot_auto_delete'].get(option) is not None: + if transform: + body[key] = transform(self.parameters['snapshot_auto_delete'][option]) + else: + body[key] = self.parameters['snapshot_auto_delete'][option] return body def change_volume_state_rest(self): @@ -2683,6 +2772,10 @@ class NetAppOntapVolume: self.na_helper.safe_get(self.parameters, ['nas_application_template', 'flexcache', 'dr_cache']) is not None: self.module.fail_json(msg='Error: %s' % self.rest_api.options_require_ontap_version('flexcache: dr_cache', version='9.9')) + if 'snapshot_auto_delete' in self.parameters: + if 'destroy_list' in self.parameters['snapshot_auto_delete']: + self.module.fail_json(msg="snapshot_auto_delete option 'destroy_list' is currently not supported with REST.") + def format_get_volume_rest(self, record): is_online = record.get('state') == 'online' # TODO FIX THIS!!!! ZAPI would only return a single aggr, REST can return more than 1. @@ -2696,6 +2789,10 @@ class NetAppOntapVolume: # if analytics.state is initializing it will be ON once completed. state = self.na_helper.safe_get(record, ['analytics', 'state']) analytics = 'on' if state == 'initializing' else state + auto_delete_info = self.na_helper.safe_get(record, ['space', 'snapshot', 'autodelete']) + if auto_delete_info is not None: + auto_delete_info['state'] = self.enabled_to_bool(self.na_helper.safe_get(record, ['space', 'snapshot', 'autodelete', 'enabled']), reverse=True) + del auto_delete_info['enabled'] return { 'tags': record.get('_tags', []), 'name': record.get('name', None), @@ -2732,7 +2829,12 @@ class NetAppOntapVolume: 'tiering_minimum_cooling_days': self.na_helper.safe_get(record, ['tiering', 'min_cooling_days']), 'snaplock': self.na_helper.safe_get(record, ['snaplock']), 'max_files': self.na_helper.safe_get(record, ['files', 'maximum']), - + # The default setting for access_time_enabled and snapshot_directory_access_enabled is true + 'atime_update': record.get('access_time_enabled', True), + 'snapdir_access': record.get('snapshot_directory_access_enabled', True), + 'snapshot_auto_delete': auto_delete_info, + 'vol_nearly_full_threshold_percent': self.na_helper.safe_get(record, ['space', 'nearly_full_threshold_percent']), + 'vol_full_threshold_percent': self.na_helper.safe_get(record, ['space', 'full_threshold_percent']), } def is_fabricpool(self, name, aggregate_uuid): @@ -2868,6 +2970,8 @@ class NetAppOntapVolume: # if we create using ZAPI and modify only options are set (snapdir_access or atime_update), we need to run a modify. # The modify also takes care of efficiency (sis) parameters and snapshot_auto_delete. # If we create using REST application, some options are not available, we may need to run a modify. + # If we create using REST and modify only options are set (snapdir_access or atime_update or snapshot_auto_delete), we need to run a modify. + # For modify only options to be set after creation wait_for_completion needs to be set. # volume should be online for modify. current = self.get_volume() if current: diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_vscan_scanner_pool.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_vscan_scanner_pool.py index 20e480637..831ae7253 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_vscan_scanner_pool.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_vscan_scanner_pool.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2018-2019, NetApp, Inc +# (c) 2018-2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ''' @@ -108,6 +108,8 @@ from ansible.module_utils._text import to_native import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils from ansible_collections.netapp.ontap.plugins.module_utils.netapp import OntapRestAPI from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule +from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic +from ansible_collections.netapp.ontap.plugins.module_utils import rest_vserver HAS_NETAPP_LIB = netapp_utils.has_netapp_lib() @@ -129,13 +131,18 @@ class NetAppOntapVscanScannerPool(object): argument_spec=self.argument_spec, supports_check_mode=True ) + self.svm_uuid = None self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) self.rest_api = OntapRestAPI(self.module) - if HAS_NETAPP_LIB is False: - self.module.fail_json(msg="the python NetApp-Lib module is required") - else: + self.use_rest = self.rest_api.is_rest() + if self.use_rest and not self.rest_api.meets_rest_minimum_version(self.use_rest, 9, 6): + msg = 'REST requires ONTAP 9.6 or later for /protocols/vscan/{{svm.uuid}}/scanner-pools APIs' + self.use_rest = self.na_helper.fall_back_to_zapi(self.module, msg, self.parameters) + if not self.use_rest: + if HAS_NETAPP_LIB is False: + self.module.fail_json(msg=netapp_utils.netapp_lib_is_required()) self.server = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=self.parameters['vserver']) def create_scanner_pool(self): @@ -143,6 +150,8 @@ class NetAppOntapVscanScannerPool(object): Create a Vscan Scanner Pool :return: nothing """ + if self.use_rest: + return self.create_scanner_pool_rest() scanner_pool_obj = netapp_utils.zapi.NaElement('vscan-scanner-pool-create') if self.parameters['hostnames']: string_obj = netapp_utils.zapi.NaElement('hostnames') @@ -182,37 +191,36 @@ class NetAppOntapVscanScannerPool(object): Check to see if a scanner pool exist or not :return: True if it exist, False if it does not """ - return_value = None if self.use_rest: - pass - else: - scanner_pool_obj = netapp_utils.zapi.NaElement('vscan-scanner-pool-get-iter') - scanner_pool_info = netapp_utils.zapi.NaElement('vscan-scanner-pool-info') - scanner_pool_info.add_new_child('scanner-pool', self.parameters['scanner_pool']) - scanner_pool_info.add_new_child('vserver', self.parameters['vserver']) - query = netapp_utils.zapi.NaElement('query') - query.add_child_elem(scanner_pool_info) - scanner_pool_obj.add_child_elem(query) - try: - result = self.server.invoke_successfully(scanner_pool_obj, True) - except netapp_utils.zapi.NaApiError as error: - self.module.fail_json(msg='Error searching for Vscan Scanner Pool %s: %s' % - (self.parameters['scanner_pool'], to_native(error)), exception=traceback.format_exc()) - if result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1: - if result.get_child_by_name('attributes-list').get_child_by_name('vscan-scanner-pool-info').get_child_content( - 'scanner-pool') == self.parameters['scanner_pool']: - scanner_pool_obj = result.get_child_by_name('attributes-list').get_child_by_name('vscan-scanner-pool-info') - hostname = [host.get_content() for host in - scanner_pool_obj.get_child_by_name('hostnames').get_children()] - privileged_users = [user.get_content() for user in - scanner_pool_obj.get_child_by_name('privileged-users').get_children()] - return_value = { - 'hostnames': hostname, - 'enable': scanner_pool_obj.get_child_content('is-currently-active'), - 'privileged_users': privileged_users, - 'scanner_pool': scanner_pool_obj.get_child_content('scanner-pool'), - 'scanner_policy': scanner_pool_obj.get_child_content('scanner-policy') - } + return self.get_scanner_pool_rest() + return_value = None + scanner_pool_obj = netapp_utils.zapi.NaElement('vscan-scanner-pool-get-iter') + scanner_pool_info = netapp_utils.zapi.NaElement('vscan-scanner-pool-info') + scanner_pool_info.add_new_child('scanner-pool', self.parameters['scanner_pool']) + scanner_pool_info.add_new_child('vserver', self.parameters['vserver']) + query = netapp_utils.zapi.NaElement('query') + query.add_child_elem(scanner_pool_info) + scanner_pool_obj.add_child_elem(query) + try: + result = self.server.invoke_successfully(scanner_pool_obj, True) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error searching for Vscan Scanner Pool %s: %s' % + (self.parameters['scanner_pool'], to_native(error)), exception=traceback.format_exc()) + if result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1: + if result.get_child_by_name('attributes-list').get_child_by_name('vscan-scanner-pool-info').get_child_content( + 'scanner-pool') == self.parameters['scanner_pool']: + scanner_pool_obj = result.get_child_by_name('attributes-list').get_child_by_name('vscan-scanner-pool-info') + hostname = [host.get_content() for host in + scanner_pool_obj.get_child_by_name('hostnames').get_children()] + privileged_users = [user.get_content() for user in + scanner_pool_obj.get_child_by_name('privileged-users').get_children()] + return_value = { + 'hostnames': hostname, + 'enable': scanner_pool_obj.get_child_content('is-currently-active'), + 'privileged_users': privileged_users, + 'scanner_pool': scanner_pool_obj.get_child_content('scanner-pool'), + 'scanner_policy': scanner_pool_obj.get_child_content('scanner-policy') + } return return_value def delete_scanner_pool(self): @@ -220,6 +228,8 @@ class NetAppOntapVscanScannerPool(object): Delete a Scanner pool :return: nothing """ + if self.use_rest: + return self.delete_scanner_pool_rest() scanner_pool_obj = netapp_utils.zapi.NaElement('vscan-scanner-pool-delete') scanner_pool_obj.add_new_child('scanner-pool', self.parameters['scanner_pool']) try: @@ -234,6 +244,8 @@ class NetAppOntapVscanScannerPool(object): Modify a scanner pool :return: nothing """ + if self.use_rest: + return self.modify_scanner_pool_rest(modify) vscan_pool_modify = netapp_utils.zapi.NaElement('vscan-scanner-pool-modify') vscan_pool_modify.add_new_child('scanner-pool', self.parameters['scanner_pool']) for key in modify: @@ -261,26 +273,117 @@ class NetAppOntapVscanScannerPool(object): def attribute_to_name(attribute): return str.replace(attribute, '_', '-') + def get_svm_uuid(self): + """ + Get a vserver's uuid + :return: nothing + """ + record, error = rest_vserver.get_vserver_uuid(self.rest_api, self.parameters['vserver']) + if error is not None: + self.module.fail_json(msg="Error fetching vserver %s: %s" % (self.parameters['vserver'], to_native(error)), + exception=traceback.format_exc()) + if record is None: + self.module.fail_json(msg="Error fetching vserver %s. Please make sure vserver name is correct." + % self.parameters['vserver'], exception=traceback.format_exc()) + self.svm_uuid = record + + def get_scanner_pool_rest(self): + """ + Check to see if a scanner pool exist or not using REST + :return: record if it exist, None if it does not + """ + self.get_svm_uuid() + api = 'protocols/vscan/%s/scanner-pools' % self.svm_uuid + query = {'name': self.parameters.get('scanner_pool'), + 'fields': 'servers,' + 'privileged_users,'} + if self.parameters.get('scanner_policy') is not None: + query['fields'] += 'role,' + + record, error = rest_generic.get_one_record(self.rest_api, api, query) + if error: + self.module.fail_json(msg='Error searching for Vscan Scanner Pool %s: %s' % + (self.parameters['scanner_pool'], to_native(error)), + exception=traceback.format_exc()) + if record: + return { + 'scanner_pool': record.get('name'), + 'hostnames': record.get('servers'), + 'privileged_users': record.get('privileged_users'), + 'scanner_policy': record.get('role'), + } + return None + + def create_scanner_pool_rest(self): + """ + Create a Vscan Scanner Pool using REST + :return: nothing + """ + api = 'protocols/vscan/%s/scanner-pools' % self.svm_uuid + body = { + 'name': self.parameters['scanner_pool'], + 'servers': self.parameters['hostnames'], + 'privileged_users': self.parameters['privileged_users'], + } + if 'scanner_policy' in self.parameters: + body['role'] = self.parameters['scanner_policy'] + + dummy, error = rest_generic.post_async(self.rest_api, api, body) + if error is not None: + self.module.fail_json(msg='Error creating Vscan Scanner Pool %s: %s' % + (self.parameters['scanner_pool'], to_native(error)), + exception=traceback.format_exc()) + + def delete_scanner_pool_rest(self): + """ + Delete a Scanner pool using REST + :return: nothing + """ + api = 'protocols/vscan/%s/scanner-pools/%s' % (self.svm_uuid, self.parameters['scanner_pool']) + dummy, error = rest_generic.delete_async(self.rest_api, api, uuid=None) + if error is not None: + self.module.fail_json(msg='Error deleting Vscan Scanner Pool %s: %s' % + (self.parameters['scanner_pool'], to_native(error)), + exception=traceback.format_exc()) + + def modify_scanner_pool_rest(self, modify): + """ + Modify a scanner pool using REST + :return: nothing + """ + api = 'protocols/vscan/%s/scanner-pools/%s' % (self.svm_uuid, self.parameters['scanner_pool']) + body = {} + for key, option in [ + ('servers', 'hostnames'), + ('privileged_users', 'privileged_users'), + ('role', 'scanner_policy'), + ]: + if modify.get(option) is not None: + body[key] = modify[option] + + dummy, error = rest_generic.patch_async(self.rest_api, api, uuid_or_name=None, body=body) + if error: + self.module.fail_json(msg='Error modifying Vscan Scanner Pool %s: %s.' % + (self.parameters['scanner_pool'], to_native(error)), + exception=traceback.format_exc()) + def apply(self): - scanner_pool_obj = self.get_scanner_pool() - cd_action = self.na_helper.get_cd_action(scanner_pool_obj, self.parameters) + current = self.get_scanner_pool() + cd_action = self.na_helper.get_cd_action(current, self.parameters) modify = None if self.parameters['state'] == 'present' and cd_action is None: - modify = self.na_helper.get_modified_attributes(scanner_pool_obj, self.parameters) - if self.na_helper.changed: - if self.module.check_mode: - pass - else: - if cd_action == 'create': - self.create_scanner_pool() - if self.parameters.get('scanner_policy') is not None: - self.apply_policy() - elif cd_action == 'delete': - self.delete_scanner_pool() - elif modify: - self.modify_scanner_pool(modify) - if self.parameters.get('scanner_policy') is not None: - self.apply_policy() + modify = self.na_helper.get_modified_attributes(current, self.parameters) + if self.na_helper.changed and not self.module.check_mode: + if cd_action == 'create': + self.create_scanner_pool() + if not self.use_rest and self.parameters.get('scanner_policy') is not None: + self.apply_policy() + elif cd_action == 'delete': + self.delete_scanner_pool() + elif modify: + self.modify_scanner_pool(modify) + if not self.use_rest and self.parameters.get('scanner_policy') is not None: + self.apply_policy() result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify) self.module.exit_json(**result) @@ -289,8 +392,8 @@ def main(): """ Execute action from playbook """ - command = NetAppOntapVscanScannerPool() - command.apply() + scanner_pool = NetAppOntapVscanScannerPool() + scanner_pool.apply() if __name__ == '__main__': diff --git a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_vserver_peer.py b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_vserver_peer.py index 3c34ccf08..5f7c7260d 100644 --- a/ansible_collections/netapp/ontap/plugins/modules/na_ontap_vserver_peer.py +++ b/ansible_collections/netapp/ontap/plugins/modules/na_ontap_vserver_peer.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2018-2022, NetApp, Inc +# (c) 2018-2023, NetApp, Inc # 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 @@ -185,7 +185,9 @@ class NetAppONTAPVserverPeer: self.dst_rest_api = OntapRestAPI(self.module, host_options=self.parameters['peer_options']) self.dst_use_rest = self.dst_rest_api.is_rest() self.use_rest = bool(self.src_use_rest and self.dst_use_rest) - if not self.use_rest: + if self.use_rest: + self.peer_relation_uuid = None + else: if not netapp_utils.has_netapp_lib(): self.module.fail_json(msg=netapp_utils.netapp_lib_is_required()) self.server = netapp_utils.setup_na_ontap_zapi(module=self.module) @@ -365,6 +367,10 @@ class NetAppONTAPVserverPeer: vserver, remote_vserver = self.get_local_and_peer_vserver(target) restapi = self.rest_api if target == 'source' else self.dst_rest_api options = {'svm.name': vserver, 'peer.svm.name': remote_vserver, 'fields': 'name,svm.name,peer.svm.name,state,uuid'} + # peer cluster may have multiple peer relationships + # filter by the created relationship uuid + if target == 'peer' and self.peer_relation_uuid is not None: + options['uuid'] = self.peer_relation_uuid record, error = rest_generic.get_one_record(restapi, api, options) if error: self.module.fail_json(msg='Error fetching vserver peer %s: %s' % (self.parameters['vserver'], error)) @@ -407,16 +413,19 @@ class NetAppONTAPVserverPeer: Create a vserver peer using rest """ api = 'svm/peers' - params = { + query = {'return_records': 'true'} + body = { 'svm.name': self.parameters['vserver'], 'peer.cluster.name': self.parameters['peer_cluster'], 'peer.svm.name': self.parameters['peer_vserver'], 'applications': self.parameters['applications'] } if 'local_name_for_peer' in self.parameters: - params['name'] = self.parameters['local_name_for_peer'] - dummy, error = rest_generic.post_async(self.rest_api, api, params) + body['name'] = self.parameters['local_name_for_peer'] + record, error = rest_generic.post_async(self.rest_api, api, body, query) self.check_and_report_rest_error(error, 'creating', self.parameters['vserver']) + if record.get('records') is not None: + self.peer_relation_uuid = record['records'][0].get('uuid') def apply(self): """ diff --git a/ansible_collections/netapp/ontap/roles/na_ontap_vserver_create/README.md b/ansible_collections/netapp/ontap/roles/na_ontap_vserver_create/README.md index e146107d2..ec8e055b9 100644 --- a/ansible_collections/netapp/ontap/roles/na_ontap_vserver_create/README.md +++ b/ansible_collections/netapp/ontap/roles/na_ontap_vserver_create/README.md @@ -72,7 +72,7 @@ Example Playbook prompt: domain admin password (enter if skipped) vars_files: - globals.yml - roles + roles: - na_ontap_vserver_create ``` I use a globals file to hold my variables. diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cg_snapshot_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cg_snapshot_rest.py new file mode 100644 index 000000000..52264b43e --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cg_snapshot_rest.py @@ -0,0 +1,331 @@ +# (c) 2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_cg_snapshot while using REST """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible, \ + create_and_apply, create_module +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cg_snapshot \ + import NetAppONTAPCGSnapshot as my_module # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always', + 'vserver': 'ansibleSVM' +} + + +# REST API canned responses when mocking send_request. +# The rest_factory provides default responses shared across testcases. +SRR = rest_responses({ + # module specific responses + 'cg_info_by_cg_name': (200, {"records": [ + { + "uuid": "af37131d-5dd2-11ee-b8da-005056b37403", + "name": "cg1", + "svm": { + "uuid": "39c2a5a0-35e2-11ee-b8da-005056b37403", + "name": "ansibleSVM" + } + } + ], + "num_records": 1 + }, None), + 'cg_info_by_volumes': (200, {"records": [ + { + "uuid": "af37131d-5dd2-11ee-b8da-005056b37403", + "name": "cg1", + "svm": { + "uuid": "39c2a5a0-35e2-11ee-b8da-005056b37403", + "name": "ansibleSVM" + }, + "volumes": [ + { + "name": "vol1" + }, + { + "name": "vol2" + } + ] + } + ], + "num_records": 1 + }, None), + 'cg_snapshot_info': (200, {"records": [ + { + "consistency_group": { + "name": "cg1" + }, + "uuid": "695a3306-6361-11ee-b8da-005056b37403", + "name": "snapshot1", + "comment": "dummy comment", + "snapmirror_label": "sm_label1" + } + ], + "num_records": 1 + }, None), +}) + + +cg_uuid = SRR['cg_info_by_cg_name'][1]['records'][0]['uuid'] +snapshot_uuid = SRR['cg_snapshot_info'][1]['records'][0]['uuid'] + + +def test_rest_successful_create_snapshot_given_consistency_group(): + '''Test successful rest create snapshot given consistency_group''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/application/consistency-groups', SRR['cg_info_by_cg_name']), # retrieve CG, given consistency_group + ('GET', '/application/consistency-groups/%s/snapshots' % (cg_uuid), SRR['empty_records']), # retrieve snapshots for the CG + ('POST', '/application/consistency-groups/%s/snapshots' % (cg_uuid), SRR['success']), # create CG snapshot + ]) + module_args = { + 'state': 'present', + 'consistency_group': 'cg1', + 'snapshot': 'snap1', + 'snapmirror_label': 'sm_label1', + 'comment': 'dummy comment' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successful_create_snapshot_idempotency(): + '''Test successful rest create snapshot idempotency''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/application/consistency-groups', SRR['cg_info_by_cg_name']), # retrieve CG, given consistency_group + ('GET', '/application/consistency-groups/%s/snapshots' % (cg_uuid), SRR['cg_snapshot_info']), # retrieve snapshots for the CG + ]) + module_args = { + 'state': 'present', + 'consistency_group': 'cg1', + 'snapshot': 'snap1', + 'snapmirror_label': 'sm_label1', + 'comment': 'dummy comment' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] is False + + +def test_rest_successful_create_snapshot_given_volumes(): + '''Test successful rest create snapshot given volumes''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/application/consistency-groups', SRR['cg_info_by_volumes']), # retrieve CG, given volumes + ('GET', '/application/consistency-groups/%s/snapshots' % (cg_uuid), SRR['empty_records']), # retrieve snapshots for the CG + ('POST', '/application/consistency-groups/%s/snapshots' % (cg_uuid), SRR['success']), # create CG snapshot + ]) + module_args = { + 'state': 'present', + 'volumes': ['vol1', 'vol2'], + 'snapshot': 'snap1', + 'snapmirror_label': 'sm_label1', + 'comment': 'dummy comment' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_error_create_snapshot(): + '''Test error rest create snapshot''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/application/consistency-groups', SRR['cg_info_by_cg_name']), # retrieve CG, given consistency_group + ('GET', '/application/consistency-groups/%s/snapshots' % (cg_uuid), SRR['empty_records']), # retrieve snapshots for the CG + ('POST', '/application/consistency-groups/%s/snapshots' % (cg_uuid), SRR['generic_error']), + ]) + module_args = { + 'state': 'present', + 'consistency_group': 'cg1', + 'snapshot': 'snap1', + 'snapmirror_label': 'sm_label1', + 'comment': 'dummy comment' + } + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'Error creating consistency group snapshot' in error + + +def test_rest_successful_delete_snapshot_given_consistency_group(): + '''Test successful rest delete snapshot given consistency_group''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/application/consistency-groups', SRR['cg_info_by_cg_name']), # retrieve CG, given consistency_group + ('GET', '/application/consistency-groups/%s/snapshots' % (cg_uuid), SRR['cg_snapshot_info']), # retrieve snapshots for the CG + ('DELETE', '/application/consistency-groups/%s/snapshots/%s' % (cg_uuid, snapshot_uuid), SRR['success']), # delete CG snapshot + ]) + module_args = { + 'state': 'absent', + 'consistency_group': 'cg1', + 'snapshot': 'snap1', + 'snapmirror_label': 'sm_label1', + 'comment': 'dummy comment' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successful_delete_snapshot_idempotency(): + '''Test successful rest delete snapshot idempotency''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/application/consistency-groups', SRR['cg_info_by_cg_name']), # retrieve CG, given consistency_group + ('GET', '/application/consistency-groups/%s/snapshots' % (cg_uuid), SRR['empty_records']), # retrieve snapshots for the CG + ]) + module_args = { + 'state': 'absent', + 'consistency_group': 'cg1', + 'snapshot': 'snap1', + 'snapmirror_label': 'sm_label1', + 'comment': 'dummy comment' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] is False + + +def test_rest_successful_delete_snapshot_given_volumes(): + '''Test successful rest delete snapshot given volumes''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/application/consistency-groups', SRR['cg_info_by_volumes']), # retrieve CG, given volumes + ('GET', '/application/consistency-groups/%s/snapshots' % (cg_uuid), SRR['cg_snapshot_info']), # retrieve snapshots for the CG + ('DELETE', '/application/consistency-groups/%s/snapshots/%s' % (cg_uuid, snapshot_uuid), SRR['success']), # delete CG snapshot + ]) + module_args = { + 'state': 'absent', + 'volumes': ['vol1', 'vol2'], + 'snapshot': 'snap1', + 'snapmirror_label': 'sm_label1', + 'comment': 'dummy comment' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_error_delete_snapshot(): + '''Test error rest delete snapshot''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/application/consistency-groups', SRR['cg_info_by_cg_name']), # retrieve CG, given consistency_group + ('GET', '/application/consistency-groups/%s/snapshots' % (cg_uuid), SRR['cg_snapshot_info']), # retrieve snapshots for the CG + ('DELETE', '/application/consistency-groups/%s/snapshots/%s' % (cg_uuid, snapshot_uuid), SRR['generic_error']), + ]) + module_args = { + 'state': 'absent', + 'consistency_group': 'cg1', + 'snapshot': 'snap1', + 'snapmirror_label': 'sm_label1', + 'comment': 'dummy comment' + } + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'Error deleting consistency group snapshot' in error + + +def test_error_ontap_version(): + ''' Test module supported from 9.10 ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ]) + module_args = { + 'state': 'present', + 'consistency_group': 'cg1', + 'snapshot': 'snap1' + } + error = create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'requires ONTAP 9.10.1 or later' in error + + +def test_rest_error_mutually_exclusive_params(): + '''Test error rest mutually exclusive parameters''' + register_responses([ + ]) + module_args = { + 'state': 'present', + 'consistency_group': 'cg1', + 'volumes': ['vol1', 'vol2'], + 'snapshot': 'snap1', + 'snapmirror_label': 'sm_label1', + 'comment': 'dummy comment' + } + error = create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert "parameters are mutually exclusive: consistency_group|volumes" in error + + +def test_rest_error_cg_not_found_given_consistency_group(): + '''Test error rest consistency group not found, given consistency_group''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/application/consistency-groups', SRR['empty_records']), # retrieve CG, given consistency_group + ]) + module_args = { + 'state': 'present', + 'consistency_group': 'cg1', + 'snapshot': 'snap1', + 'snapmirror_label': 'sm_label1', + 'comment': 'dummy comment' + } + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert "Consistency group named 'cg1' not found" in error + + +def test_rest_error_cg_not_found_given_volumes(): + '''Test error rest consistency group not found, given volumes''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/application/consistency-groups', SRR['empty_records']), # retrieve CG, given volumes + ]) + module_args = { + 'state': 'present', + 'volumes': ['vol3'], + 'snapshot': 'snap1', + 'snapmirror_label': 'sm_label1', + 'comment': 'dummy comment' + } + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert "Consistency group having volumes '['vol3']' not found" in error + + +def test_rest_error_retrieve_cg(): + '''Test error rest retrieve consistency group''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/application/consistency-groups', SRR['generic_error']), # retrieve CG, given consistency_group + ]) + module_args = { + 'state': 'absent', + 'consistency_group': 'cg1', + 'snapshot': 'snap1', + 'snapmirror_label': 'sm_label1', + 'comment': 'dummy comment' + } + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'Error searching for consistency group' in error + + +def test_rest_error_retrieve_cg_snapshot(): + '''Test error rest retrieve consistency group snapshot''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/application/consistency-groups', SRR['cg_info_by_cg_name']), # retrieve CG, given consistency_group + ('GET', '/application/consistency-groups/%s/snapshots' % (cg_uuid), SRR['generic_error']), # retrieve snapshots for the CG + ]) + module_args = { + 'state': 'absent', + 'consistency_group': 'cg1', + 'snapshot': 'snap1', + 'snapmirror_label': 'sm_label1', + 'comment': 'dummy comment' + } + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'Error searching for consistency group snapshot' in error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_server.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_server.py index 820c33d17..1ec919657 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_server.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_server.py @@ -1,4 +1,4 @@ -# (c) 2018-2022, NetApp, Inc +# (c) 2018-2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ''' unit tests ONTAP Ansible module: na_ontap_cifs_server ''' @@ -36,6 +36,7 @@ SRR = rest_responses({ "enabled": True, "security": { "encrypt_dc_connection": False, + "lm_compatibility_level": "lm_ntlm_ntlmv2_krb", "smb_encryption": False, "kdc_encryption": False, "smb_signing": False, @@ -47,6 +48,9 @@ SRR = rest_responses({ "use_ldaps": False, "use_start_tls": False }, + "options": { + "multichannel": True + }, "target": { "name": "20:05:00:50:56:b3:0c:fa" }, @@ -68,6 +72,7 @@ SRR = rest_responses({ "enabled": False, "security": { "encrypt_dc_connection": False, + "lm_compatibility_level": "lm_ntlm_ntlmv2_krb", "smb_encryption": False, "kdc_encryption": False, "smb_signing": False, @@ -79,8 +84,11 @@ SRR = rest_responses({ "use_ldaps": False, "use_start_tls": False }, + "options": { + "multichannel": True + }, "target": { - "nam,e": "20:05:00:50:56:b3:0c:fa" + "name": "20:05:00:50:56:b3:0c:fa" }, "name": "cifs_server_name" } @@ -100,6 +108,7 @@ SRR = rest_responses({ "enabled": True, "security": { "encrypt_dc_connection": False, + "lm_compatibility_level": "lm_ntlm_ntlmv2_krb", "smb_encryption": False, "kdc_encryption": False, "smb_signing": False, @@ -457,6 +466,19 @@ def test_rest_successful_create_with_domain(): assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] +def test_rest_successful_create_with_default_site(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_13_1']), + ('GET', 'protocols/cifs/services', SRR['empty_records']), + ('POST', 'protocols/cifs/services', SRR['empty_good']), + ]) + module_args = { + 'default_site': 'default_site' + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + def test_rest_successful_create_with_security(): '''Test successful rest create''' register_responses([ @@ -469,11 +491,48 @@ def test_rest_successful_create_with_security(): 'smb_signing': True, 'kdc_encryption': True, 'encrypt_dc_connection': True, - 'restrict_anonymous': 'no_enumeration' + 'restrict_anonymous': 'no_enumeration', + 'lm_compatibility_level': 'lm_ntlm_ntlmv2_krb' } assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] +def test_rest_version_error_with_security_options_9_8(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ]) + module_args = { + 'use_rest': 'always', + 'lm_compatibility_level': 'ntlm_ntlmv2_krb', + } + error = create_module(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Minimum version of ONTAP for lm_compatibility_level is (9, 8)' in error + + +def test_rest_version_error_with_service_options_9_10(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ]) + module_args = { + 'use_rest': 'always', + 'is_multichannel_enabled': False + } + error = create_module(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Minimum version of ONTAP for is_multichannel_enabled is (9, 10, 1)' in error + + +def test_rest_version_error_with_default_site(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_1']) + ]) + module_args = { + 'use_rest': 'always', + 'default_site': 'default_site', + } + error = create_module(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Minimum version of ONTAP for default_site is (9, 13, 1)' in error + + def test_rest_version_error_with_security_encryption(): register_responses([ ('GET', 'cluster', SRR['is_rest_96']) @@ -625,7 +684,7 @@ def test_rest_negative_security_options_modify(): def test_rest_successful_security_options_modify(): - '''Test successful rest enable''' + '''Test successful rest security options modify''' register_responses([ ('GET', 'cluster', SRR['is_rest_9_10_1']), ('GET', 'protocols/cifs/services', SRR['cifs_record_disabled']), @@ -635,12 +694,26 @@ def test_rest_successful_security_options_modify(): "aes_netlogon_enabled": True, "ldap_referral_enabled": True, "session_security": "seal", + "lm_compatibility_level": "ntlm_ntlmv2_krb", "try_ldap_channel_binding": False, "use_ldaps": True } assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] +def test_rest_successful_service_options_modify(): + '''Test successful rest service options modify''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/services', SRR['cifs_record_disabled']), + ('PATCH', 'protocols/cifs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ]) + module_args = { + "is_multichannel_enabled": False + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + def test_rest_successful_rename_cifs(): '''Test successful rest rename''' register_responses([ diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_unix_symlink_mapping_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_unix_symlink_mapping_rest.py new file mode 100644 index 000000000..28e62f6f0 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_unix_symlink_mapping_rest.py @@ -0,0 +1,252 @@ +# Copyright: NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_cifs_unix_symlink_mapping """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible, \ + create_and_apply, create_module, call_main, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cifs_unix_symlink_mapping \ + import NetAppOntapCifsUnixSymlink as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip( + 'Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always' +} + + +# REST API canned responses when mocking send_request. +# The rest_factory provides default responses shared across testcases. +SRR = rest_responses({ + # module specific responses + 'symlink_mapping': (200, {"records": [ + { + "svm": { + "uuid": "a7d278fb-2d2d-11ee-b8da-005056b37403", + "name": "ansibleSVM" + }, + "unix_path": "/example1/", + "target": { + "share": "share1", + "path": "/path1/test_dir/", + "server": "CIFS", + "locality": "local", + "home_directory": False + } + } + ] + }, None), +}) + + +svm_uuid = 'a7d278fb-2d2d-11ee-b8da-005056b37403' +unix_path = '/example1/' +unix_path_encoded = unix_path.replace('/', '%2F') + + +def test_successful_create(): + ''' Test successful rest create ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/cifs/unix-symlink-mapping', SRR['empty_records']), + ('POST', 'protocols/cifs/unix-symlink-mapping', SRR['empty_good']), + ]) + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'unix_path': '/example1/', + 'share_name': 'share1', + 'cifs_path': '/path1/test_dir/' + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_successful_create_idempotency(): + ''' Test successful rest create idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/cifs/unix-symlink-mapping', SRR['symlink_mapping']), + ]) + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'unix_path': '/example1/', + 'share_name': 'share1', + 'cifs_path': '/path1/test_dir/' + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] is False + + +def test_successful_delete(): + ''' Test successful rest delete ''' + unix_path_encoded = '%2Fexample1%2F' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/cifs/unix-symlink-mapping', SRR['symlink_mapping']), + ('DELETE', 'protocols/cifs/unix-symlink-mapping/%s/%s' % (svm_uuid, unix_path_encoded), SRR['success']), + ]) + args = { + 'state': 'absent', + 'vserver': 'ansibleSVM', + 'unix_path': '/example1/' + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_successful_delete_idempotency(): + ''' Test successful rest delete idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/cifs/unix-symlink-mapping', SRR['empty_records']), + ]) + args = { + 'state': 'absent', + 'vserver': 'ansibleSVM', + 'unix_path': '/example1/' + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] is False + + +def test_successful_modify(): + ''' Test successful rest modify ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/cifs/unix-symlink-mapping', SRR['symlink_mapping']), + ('PATCH', 'protocols/cifs/unix-symlink-mapping/%s/%s' % (svm_uuid, unix_path_encoded), SRR['success']), + ]) + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'unix_path': '/example1/', + 'share_name': 'share2', + 'cifs_path': '/path2/test_dir/' + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_error_get(): + ''' Test error rest get ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/cifs/unix-symlink-mapping', SRR['generic_error']), + ]), + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'unix_path': '/example1/', + 'share_name': 'share1', + 'cifs_path': '/path1/test_dir/' + } + error = create_and_apply(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Error while fetching cifs unix symlink mapping' in error + + +def test_error_create(): + ''' Test error rest create ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/cifs/unix-symlink-mapping', SRR['empty_records']), + ('POST', 'protocols/cifs/unix-symlink-mapping', SRR['generic_error']), + ]), + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'unix_path': '/example1/', + 'share_name': 'share1', + 'cifs_path': '/path1/test_dir/' + } + error = create_and_apply(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Error while creating cifs unix symlink mapping' in error + + +def test_error_modify(): + ''' Test error rest modify ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/cifs/unix-symlink-mapping', SRR['symlink_mapping']), + ('PATCH', 'protocols/cifs/unix-symlink-mapping/%s/%s' % (svm_uuid, unix_path_encoded), SRR['generic_error']), + ]) + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'unix_path': '/example1/', + 'share_name': 'share2', + 'cifs_path': '/path2/test_dir/' + } + error = create_and_apply(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Error while modifying cifs unix symlink mapping' in error + + +def test_error_delete(): + ''' Test error rest delete ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/cifs/unix-symlink-mapping', SRR['symlink_mapping']), + ('DELETE', 'protocols/cifs/unix-symlink-mapping/%s/%s' % (svm_uuid, unix_path_encoded), SRR['generic_error']), + ]) + args = { + 'state': 'absent', + 'vserver': 'ansibleSVM', + 'unix_path': '/example1/' + } + error = create_and_apply(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Error while deleting cifs unix symlink mapping' in error + + +def test_error_ontap96(): + ''' Test error module supported from 9.6 ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ]) + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'unix_path': 'example1', + 'share_name': 'share1', + 'cifs_path': '/path1/test_dir/' + } + assert 'requires ONTAP 9.6.0 or later' in call_main(my_main, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_missing_options_state_present(): + ''' Test error missing options with state=present ''' + register_responses([]) + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'unix_path': '/example1/' + } + error = create_module(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'state is present but all of the following are missing: share_name, cifs_path' in error + + +def test_missing_options_locality_widelink(): + ''' Test error missing cifs_server with locality=widelink ''' + register_responses([]) + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'unix_path': '/example1/', + 'share_name': 'share1', + 'cifs_path': '/path1/test_dir/', + 'locality': 'widelink' + } + error = create_module(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'locality is widelink but all of the following are missing: cifs_server' in error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cli_timeout_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cli_timeout_rest.py new file mode 100644 index 000000000..86ec7640e --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cli_timeout_rest.py @@ -0,0 +1,104 @@ +# Copyright: NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_cli_timeout """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible, \ + create_and_apply, create_module, call_main, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cli_timeout \ + import NetAppOntapCliTimeout as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip( + 'Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always', + 'state': 'present' +} + + +# REST API canned responses when mocking send_request. +# The rest_factory provides default responses shared across testcases. +SRR = rest_responses({ + # module specific responses + 'cli_timeout': (200, { + 'timeout': 30 + }, None), +}) + + +def test_successful_modify(): + ''' Test successful modify timeout value ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'private/cli/system/timeout', SRR['cli_timeout']), # get timeout value + ('PATCH', 'private/cli/system/timeout', SRR['success']), # modify timeout value + ]) + args = { + 'timeout': 0 + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_successful_modify_idempotency(): + ''' Test successful modify timeout value idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'private/cli/system/timeout', SRR['cli_timeout']), # get timeout value + ]) + args = { + 'timeout': 30 + } + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_all_methods_catch_exception(): + ''' Test exception in get/modify timeout value ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + # GET/PATCH error + ('GET', 'private/cli/system/timeout', SRR['generic_error']), + ('PATCH', 'private/cli/system/timeout', SRR['generic_error']) + ]) + args = { + 'timeout': 15 + } + cli_timeout = create_module(my_module, DEFAULT_ARGS, args) + error = 'Error fetching CLI sessions timeout value' + assert error in expect_and_capture_ansible_exception(cli_timeout.get_timeout_value_rest, 'fail')['msg'] + error = 'Error modifying CLI sessions timeout value' + assert error in expect_and_capture_ansible_exception(cli_timeout.modify_timeout_value_rest, 'fail', args)['msg'] + + +def test_missing_options(): + ''' Test error missing required option timeout ''' + register_responses([]) + error = create_module(my_module, DEFAULT_ARGS, fail=True)['msg'] + assert 'missing required arguments: timeout' in error + + +def test_error_ontap96(): + ''' Test error module supported from 9.6 ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ]) + args = { + 'timeout': 15 + } + assert 'requires ONTAP 9.6.0 or later' in call_main(my_main, DEFAULT_ARGS, args, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster.py index 89fe069a3..bfd5ca4ef 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster.py @@ -404,11 +404,13 @@ SRR = { 'is_rest_95': (200, dict(version=dict(generation=9, major=5, minor=0, full='dummy_9_5_0')), None), 'is_rest_96': (200, dict(version=dict(generation=9, major=6, minor=0, full='dummy_9_6_0')), None), 'is_rest_97': (200, dict(version=dict(generation=9, major=7, minor=0, full='dummy_9_7_0')), None), + 'is_rest_910': (200, dict(version=dict(generation=9, major=10, minor=1, full='dummy_9_10_1')), None), 'is_zapi': (400, {}, "Unreachable"), 'empty_good': ({}, None, None), 'zero_record': (200, {'records': []}, None), 'precluster': (500, None, {'message': 'are available in precluster.'}), 'cluster_identity': (200, {'location': 'Oz', 'name': 'abc'}, None), + 'cluster_web_service': (200, {'certificate': {'uuid': 'abcd12'}}, None), 'nodes': (200, {'records': [ {'name': 'node2', 'uuid': 'uuid2', 'cluster_interfaces': [{'ip': {'address': '10.10.10.2'}}]} ]}, None), @@ -471,6 +473,28 @@ def test_rest_create_timezone(mock_request, patch_ansible): @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_modify_certificate(mock_request, patch_ansible): + ''' modify cluster certificate ''' + args = dict(set_default_args()) + args['certificate'] = {'uuid': 'abcd123'} + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_910'], + SRR['cluster_identity'], # get /cluster + SRR['cluster_web_service'], # get /cluster/web + SRR['empty_good'], # patch /cluster + SRR['empty_good'], # patch /cluster/web + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print(mock_request.mock_calls) + assert exc.value.args[0]['changed'] is True + assert len(mock_request.mock_calls) == 5 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') def test_rest_create_single(mock_request, patch_ansible): ''' create cluster ''' args = dict(set_default_args()) diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster_peer.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster_peer.py index 7551a619e..190169cee 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster_peer.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster_peer.py @@ -267,6 +267,38 @@ def test_delete_idempotency_rest(): assert create_and_apply(my_module, DEFAULT_ARGS, module_args) +def test_successful_modify_rest(): + ''' Test successful modify ''' + module_args = DEFAULT_ARGS + module_args['dest_intercluster_lifs'] = ['10.193.179.58'] + module_args['source_intercluster_lifs'] = ['10.193.179.181'] + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster/peers', SRR['cluster_peer_src']), + ('GET', 'cluster/peers', SRR['cluster_peer_dst']), + ('PATCH', 'cluster/peers/1fg98aba-2aa6-11ec-b7be-005fgvb366e1', SRR['empty_good']), + ('PATCH', 'cluster/peers/1e698aba-2aa6-11ec-b7be-005056b366e1', SRR['empty_good']) + ]) + assert create_and_apply(my_module, module_args) + + +def test_modify_idempotency_rest(): + ''' Test successful modify idempotency ''' + module_args = DEFAULT_ARGS + module_args['dest_intercluster_lifs'] = ['10.193.179.58'] + module_args['source_intercluster_lifs'] = ['10.193.179.181'] + SRR['cluster_peer_src'][1]['records'][0]['remote']['ip_addresses'] = ['10.193.179.58'] + SRR['cluster_peer_dst'][1]['records'][0]['remote']['ip_addresses'] = ['10.193.179.181'] + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster/peers', SRR['cluster_peer_src']), + ('GET', 'cluster/peers', SRR['cluster_peer_dst']) + ]) + assert create_and_apply(my_module, module_args) + + def test_error_get_cluster_peer_rest(): ''' Test get error ''' register_responses([ @@ -303,3 +335,19 @@ def test_error_create_cluster_peer_rest(): ]) error = create_and_apply(my_module, DEFAULT_ARGS, fail=True)['msg'] assert 'calling: cluster/peers: got Expected error.' == error + + +def test_error_modify_cluster_peer_rest(): + ''' Test modify error ''' + module_args = DEFAULT_ARGS + module_args['dest_intercluster_lifs'] = ['10.193.179.59'] + module_args['source_intercluster_lifs'] = ['10.193.179.180'] + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster/peers', SRR['cluster_peer_src']), + ('GET', 'cluster/peers', SRR['cluster_peer_dst']), + ('PATCH', 'cluster/peers/1fg98aba-2aa6-11ec-b7be-005fgvb366e1', SRR['generic_error']), + ]) + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'calling: cluster/peers/1fg98aba-2aa6-11ec-b7be-005fgvb366e1: got Expected error.' == error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_dns.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_dns.py index c592f5c88..bb88ee5fc 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_dns.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_dns.py @@ -33,6 +33,7 @@ SRR = rest_responses({ 'end_of_sequence': (500, None, "Unexpected call to send_request"), 'generic_error': (400, None, "Expected error"), 'dns_record': (200, {"records": [{"domains": ['test.com'], + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7", "servers": ['0.0.0.0'], "svm": {"name": "svm1", "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7"}}]}, None), 'cluster_data': (200, {"dns_domains": ['test.com'], diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_config_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_config_rest.py new file mode 100644 index 000000000..71efac389 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_config_rest.py @@ -0,0 +1,107 @@ +# Copyright: NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_ems_config """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible, \ + create_and_apply, create_module, call_main, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_ems_config \ + import NetAppOntapEmsConfig as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip( + 'Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always', + 'state': 'present' +} + + +# REST API canned responses when mocking send_request. +# The rest_factory provides default responses shared across testcases. +SRR = rest_responses({ + # module specific responses + 'ems_config': (200, { + 'mail_from': 'administrator@mycompany.com', + 'mail_server': 'mail.mycompany.com', + 'pubsub_enabled': True + }, None), + 'ems_config_modified': (200, { + 'mail_from': 'admin@mycompany.com', + 'mail_server': 'mail.mycompany.com', + 'proxy_url': 'http://proxyserver.mycompany.com', + 'proxy_user': 'proxy_user', + 'pubsub_enabled': False + }, None), +}) + + +def test_successful_modify(): + ''' Test successful rest modify ems config with idempotency check''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems', SRR['ems_config']), # get ems config + ('PATCH', 'support/ems', SRR['success']), # modify ems config + + ('GET', 'cluster', SRR['is_rest_9_10_1']), # get ems config + ('GET', 'support/ems', SRR['ems_config_modified']), # modify ems config + ]) + args = { + 'mail_from': 'admin@mycompany.com', + 'mail_server': 'mail.mycompany.com', + 'pubsub_enabled': 'false' + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_all_methods_catch_exception(): + ''' Test exception in get/modify ems config ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + # GET/PATCH error + ('GET', 'support/ems', SRR['generic_error']), + ('PATCH', 'support/ems', SRR['generic_error']) + ]) + modify_args = { + 'pubsub_enabled': 'false' + } + ems_config = create_module(my_module, DEFAULT_ARGS) + assert 'Error fetching EMS config' in expect_and_capture_ansible_exception(ems_config.get_ems_config_rest, 'fail')['msg'] + assert 'Error modifying EMS config' in expect_and_capture_ansible_exception(ems_config.modify_ems_config_rest, 'fail', modify_args)['msg'] + + +def test_error_ontap96(): + ''' Test module supported from 9.6 ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ]) + assert 'requires ONTAP 9.6.0 or later' in call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] + + +def test_version_error_with_pubsub_enabled(): + ''' Test version error for pubsub_enabled ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']) + ]) + args = { + 'pubsub_enabled': 'false' + } + error = create_module(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Minimum version of ONTAP for pubsub_enabled is (9, 10, 1)' in error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_destination.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_destination.py index ca951ba58..dacc2cb97 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_destination.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_destination.py @@ -48,7 +48,50 @@ SRR = rest_responses({ "destination": "https://test.destination" }], "num_records": 1 - }, None) + }, None), + 'certificate_record_1': (200, + {'records': [{"name": "cert_1", + "uuid": "cert_uuid_1", + "serial_number": "cert_serial"}]}, None), + 'ems_destination_with_cert': (200, { + "records": [ + { + "name": "test", + "type": "rest-api", + "destination": "https://test.destination", + "certificate": { + "ca": "cert_ca", + "name": "cert1" + }, + "filters": [ + { + "name": "test-filter" + } + ] + }], + "num_records": 1 + }, None), + 'ems_destination_type_syslog': (200, { + "records": [ + { + "name": "test", + "type": "syslog", + "destination": "https://test.destination", + "filters": [ + { + "name": "test-filter" + } + ], + "syslog": { + "port": 514, + "transport": "udp_unencrypted", + "message_format": "legacy_netapp", + "timestamp_format_override": "no_override", + "hostname_format_override": "no_override" + } + }], + "num_records": 1 + }, None), }) DEFAULT_ARGS = { @@ -90,6 +133,40 @@ def test_create_ems_destination(): assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] +def test_create_ems_destination_with_cert(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'support/ems/destinations', SRR['empty_records']), + ('GET', 'security/certificates', SRR['certificate_record_1']), + ('POST', 'support/ems/destinations', SRR['empty_good']) + ]) + module_args = { + 'name': 'test', + 'type': 'rest_api', + 'destination': 'https://test.destination', + 'filters': ['test-filter'], + 'certificate': 'cert1', + 'ca': 'cert_ca', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_create_ems_destination_with_cert_unsupported_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ]) + module_args = { + 'name': 'test', + 'type': 'rest_api', + 'destination': 'https://test.destination', + 'filters': ['test-filter'], + 'certificate': 'cert1', + 'ca': 'cert_ca', + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'na_ontap_ems_destination is only supported with REST API' == error + + def test_create_ems_destination_error(): register_responses([ ('GET', 'cluster', SRR['is_rest_9_10_1']), @@ -224,3 +301,161 @@ def test_empty_modify_skips_patch(): module_args = {'name': 'test', 'type': 'rest_api', 'destination': 'https://test.destination', 'filters': ['test-filter']} my_obj = create_module(my_module, DEFAULT_ARGS, module_args) my_obj.modify_ems_destination('test', {}) + + +def test_module_error_missing_required_together_param(): + module_args = { + 'name': 'test', + 'type': 'rest_api', + 'destination': 'https://test.destination', + 'filters': ['test-filter'], + 'certificate': 'cert1', + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'parameters are required together: certificate, ca' == error + + +def test_module_error_cert_not_found(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'support/ems/destinations', SRR['ems_destination']), + ('GET', 'security/certificates', SRR['empty_records']), + ]) + module_args = { + 'name': 'test', + 'type': 'rest_api', + 'destination': 'https://test.destination', + 'filters': ['test-filter'], + 'certificate': 'cert1', + 'ca': 'my_cert_ca', + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'Error certificate not found: cert1.' == error + + +def test_module_error_rest_get_cert(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'support/ems/destinations', SRR['ems_destination']), + ('GET', 'security/certificates', SRR['generic_error']), + ]) + module_args = { + 'name': 'test', + 'type': 'rest_api', + 'destination': 'https://test.destination', + 'filters': ['test-filter'], + 'certificate': 'cert1', + 'ca': 'my_cert_ca', + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'Error retrieving certificates: calling: security/certificates: got Expected error.' == error + + +def test_modify_ems_cert(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'support/ems/destinations', SRR['ems_destination']), + ('GET', 'security/certificates', SRR['certificate_record_1']), + ('PATCH', 'support/ems/destinations/test', SRR['empty_good']) + ]) + module_args = { + 'name': 'test', + 'type': 'rest_api', + 'destination': 'https://test.destination', + 'filters': ['test-filter'], + 'certificate': 'cert1', + 'ca': 'cert_ca', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_ems_destination_with_type_syslog(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'support/ems/destinations', SRR['empty_records']), + ('POST', 'support/ems/destinations', SRR['empty_good']) + ]) + module_args = { + 'name': 'test', + 'type': 'syslog', + 'destination': 'https://test.destination', + 'filters': ['test-filter'], + 'syslog': { + 'port': 514, + 'transport': 'udp_unencrypted', + 'message_format': 'legacy_netapp', + 'timestamp_format_override': 'no_override', + 'hostname_format_override': 'no_override' + } + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_ontap_9_12_1(): + ''' syslog option supported from 9.12.1 ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']) + ]) + module_args = { + 'name': 'test', + 'type': 'syslog', + 'use_rest': 'always', + 'destination': 'https://test.destination', + 'filters': ['test-filter'], + 'syslog': { + 'port': 514, + 'transport': 'udp_unencrypted', + 'message_format': 'legacy_netapp', + 'timestamp_format_override': 'no_override', + 'hostname_format_override': 'no_override' + } + } + assert 'Error: Minimum version of ONTAP for syslog is (9, 12, 1). Current version: (9, 10, 1).' in call_main(my_main, DEFAULT_ARGS, + module_args, fail=True)['msg'] + + +def test_create_ems_destination_with_type_syslog_and_add_certs(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'support/ems/destinations', SRR['empty_records']), + ('GET', 'security/certificates', SRR['certificate_record_1']), + ('POST', 'support/ems/destinations', SRR['empty_good']) + ]) + module_args = { + 'name': 'test', + 'type': 'syslog', + 'destination': 'https://test.destination', + 'filters': ['test-filter'], + 'certificate': 'cert1', + 'ca': 'cert_ca', + 'syslog': { + 'port': 514, + 'transport': 'udp_unencrypted', + 'message_format': 'legacy_netapp', + 'timestamp_format_override': 'no_override', + 'hostname_format_override': 'no_override' + } + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_ems_destination_with_type_syslog(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'support/ems/destinations', SRR['ems_destination_type_syslog']), + ('PATCH', 'support/ems/destinations/test', SRR['empty_good']) + ]) + module_args = { + 'name': 'test', + 'type': 'syslog', + 'destination': 'https://test.destination', + 'filters': ['test-filter'], + 'syslog': { + 'port': 614, + 'transport': 'tcp_unencrypted', + 'message_format': 'rfc_5424', + 'timestamp_format_override': 'no_override', + 'hostname_format_override': 'fqdn' + } + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_filter.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_filter.py index f7f0a1feb..8223944c9 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_filter.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_filter.py @@ -25,6 +25,18 @@ if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') SRR = rest_responses({ + 'default_ems_filter': (200, { + "name": "snmp-traphost", + "rules": [{ + "index": "1", + "type": "exclude", + "message_criteria": { + "severities": "*", + "name_pattern": "*", + "snmp_trap_types": "*", + } + }] + }, None), 'ems_filter': (200, { "name": "snmp-traphost", "rules": [{ @@ -44,7 +56,8 @@ SRR = rest_responses({ } }] }, None), - 'ems_filter_2_riles': (200, { + 'post_empty_good': (201, {}, None), + 'ems_filter_2_rules': (200, { "name": "snmp-traphost", "rules": [{ "index": "1", @@ -70,6 +83,39 @@ SRR = rest_responses({ } }] }, None), + 'ems_filter_3_rules': (200, { + "name": "snmp-traphost", + "rules": [{ + "index": "1", + "type": "include", + "message_criteria": { + "severities": "error", + "name_pattern": "*", + } + }, { + "index": "2", + "type": "include", + "message_criteria": { + "severities": "alert", + "name_pattern": "callhome.*", + } + }, { + "index": "3", + "type": "include", + "message_criteria": { + "severities": "emergency", + "name_pattern": "callhome.*", + } + }, { + "index": "4", + "type": "exclude", + "message_criteria": { + "severities": "*", + "name_pattern": "*", + "snmp_trap_types": "*", + } + }] + }, None), 'ems_filter_no_rules': (200, { "name": "snmp-traphost", }, None) @@ -94,17 +140,19 @@ DEFAULT_RULE = [{ DEFAULT_RULE_2_RULES = [{ "index": "1", - "type": "include", + "type": "exclude", "message_criteria": { "severities": "error,informational", "name_pattern": "callhome.*", - }}, { + } +}, { "index": "2", - "type": "include", + "type": "exclude", "message_criteria": { - "severities": "alert", - "name_pattern": "callhome.*", - }}] + "severities": "*", + "name_pattern": "*", + } +}] DEFAULT_RULE_MODIFY_TYPE_2_RULES = [{ "index": "1", @@ -126,7 +174,7 @@ DEFAULT_RULE_MODIFY_SEVERITIES_2_RULES = [{ "index": "1", "type": "include", "message_criteria": { - "severities": "informational", + "severities": "notice", "name_pattern": "callhome.*", } }, { @@ -150,6 +198,29 @@ DEFAULT_RULE_MODIFY_NAME_PATTERN_2_RULES = [{ "type": "include", "message_criteria": { "severities": "alert", + "name_pattern": "*", + } +}] + +DEFAULT_RULE_MODIFY_SEVERITIES_3_RULES = [{ + "index": "1", + "type": "include", + "message_criteria": { + "severities": "error, informational", + "name_pattern": "*", + } +}, { + "index": "2", + "type": "include", + "message_criteria": { + "severities": "alert", + "name_pattern": "callhome.*", + } +}, { + "index": "3", + "type": "include", + "message_criteria": { + "severities": "emergency", "name_pattern": "callhome.*", } }] @@ -241,28 +312,32 @@ def test_delete_ems_filter_error(): def test_modify_ems_filter_add_rule(): register_responses([ ('GET', 'cluster', SRR['is_rest_9_10_1']), - ('GET', 'support/ems/filters', SRR['ems_filter']), - ('PATCH', 'support/ems/filters/snmp-traphost', SRR['empty_good']) + ('GET', 'support/ems/filters', SRR['default_ems_filter']), + ('POST', 'support/ems/filters/snmp-traphost/rules', SRR['post_empty_good']), ]) - module_args = {'rules': DEFAULT_RULE_2_RULES} + module_args = {'rules': DEFAULT_RULE} assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] def test_modify_ems_filter_change_type(): register_responses([ ('GET', 'cluster', SRR['is_rest_9_10_1']), - ('GET', 'support/ems/filters', SRR['ems_filter_2_riles']), - ('PATCH', 'support/ems/filters/snmp-traphost', SRR['empty_good']) + ('GET', 'support/ems/filters', SRR['ems_filter']), + ('PATCH', 'support/ems/filters/snmp-traphost', SRR['empty_good']), + ('POST', 'support/ems/filters/snmp-traphost/rules', SRR['post_empty_good']), + ('POST', 'support/ems/filters/snmp-traphost/rules', SRR['post_empty_good']) ]) - module_args = {'rules': DEFAULT_RULE_MODIFY_TYPE_2_RULES} + module_args = {'rules': DEFAULT_RULE_2_RULES} assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] def test_modify_ems_filter_change_severities(): register_responses([ ('GET', 'cluster', SRR['is_rest_9_10_1']), - ('GET', 'support/ems/filters', SRR['ems_filter_2_riles']), - ('PATCH', 'support/ems/filters/snmp-traphost', SRR['empty_good']) + ('GET', 'support/ems/filters', SRR['ems_filter_2_rules']), + ('PATCH', 'support/ems/filters/snmp-traphost', SRR['empty_good']), + ('POST', 'support/ems/filters/snmp-traphost/rules', SRR['post_empty_good']), + ('POST', 'support/ems/filters/snmp-traphost/rules', SRR['post_empty_good']) ]) module_args = {'rules': DEFAULT_RULE_MODIFY_SEVERITIES_2_RULES} assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] @@ -271,21 +346,38 @@ def test_modify_ems_filter_change_severities(): def test_modify_ems_filter_change_name_pattern(): register_responses([ ('GET', 'cluster', SRR['is_rest_9_10_1']), - ('GET', 'support/ems/filters', SRR['ems_filter_2_riles']), - ('PATCH', 'support/ems/filters/snmp-traphost', SRR['empty_good']) + ('GET', 'support/ems/filters', SRR['ems_filter_2_rules']), + ('PATCH', 'support/ems/filters/snmp-traphost', SRR['empty_good']), + ('POST', 'support/ems/filters/snmp-traphost/rules', SRR['post_empty_good']), + ('POST', 'support/ems/filters/snmp-traphost/rules', SRR['post_empty_good']) ]) module_args = {'rules': DEFAULT_RULE_MODIFY_NAME_PATTERN_2_RULES} assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] +def test_modify_ems_filter_add_rule_and_change_severities(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/filters', SRR['ems_filter_2_rules']), + ('PATCH', 'support/ems/filters/snmp-traphost', SRR['empty_good']), + ('POST', 'support/ems/filters/snmp-traphost/rules', SRR['post_empty_good']), + ('POST', 'support/ems/filters/snmp-traphost/rules', SRR['post_empty_good']), + ('POST', 'support/ems/filters/snmp-traphost/rules', SRR['post_empty_good']) + ]) + module_args = {'rules': DEFAULT_RULE_MODIFY_SEVERITIES_3_RULES} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + def test_modify_ems_filter_error(): register_responses([ ('GET', 'cluster', SRR['is_rest_9_10_1']), - ('PATCH', 'support/ems/filters/snmp-traphost', SRR['generic_error']) + ('PATCH', 'support/ems/filters/snmp-traphost', SRR['generic_error']), ]) my_obj = create_module(my_module, DEFAULT_ARGS) - my_obj.parameters['rules'] = DEFAULT_RULE_2_RULES - error = expect_and_capture_ansible_exception(my_obj.modify_ems_filter, 'fail')['msg'] + patch_rules = [{'index': 1, 'type': 'include', 'message_criteria': {'severities': 'error', 'name_pattern': '*'}}] + post_rules = [{'index': 2, 'type': 'include', 'message_criteria': {'severities': 'notice', 'name_pattern': '*'}}] + desired_rules = {'patch_rules': patch_rules, 'post_rules': post_rules} + error = expect_and_capture_ansible_exception(my_obj.modify_ems_filter, 'fail', desired_rules)['msg'] print('Info: %s' % error) assert 'Error modifying EMS filter snmp-traphost: calling: support/ems/filters/snmp-traphost: got Expected error.' == error @@ -293,7 +385,7 @@ def test_modify_ems_filter_error(): def test_modify_ems_filter_no_rules(): register_responses([ ('GET', 'cluster', SRR['is_rest_9_10_1']), - ('GET', 'support/ems/filters', SRR['ems_filter_no_rules']), + ('GET', 'support/ems/filters', SRR['default_ems_filter']), ]) assert not create_and_apply(my_module, DEFAULT_ARGS, {})['changed'] @@ -302,7 +394,8 @@ def test_modify_star_test(): register_responses([ ('GET', 'cluster', SRR['is_rest_9_10_1']), ('GET', 'support/ems/filters', SRR['ems_filter']), - ('PATCH', 'support/ems/filters/snmp-traphost', SRR['empty_good']) + ('PATCH', 'support/ems/filters/snmp-traphost', SRR['empty_good']), + ('POST', 'support/ems/filters/snmp-traphost/rules', SRR['post_empty_good']) ]) module_args = {'rules': DEFAULT_RULE_STARS} assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_info.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_info.py index 18c35c910..c3c845b16 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_info.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_info.py @@ -7,6 +7,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type import pytest +import sys from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils @@ -21,6 +22,9 @@ from ansible_collections.netapp.ontap.plugins.modules.na_ontap_info import conve if not netapp_utils.has_netapp_lib(): pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') +if sys.version_info < (2, 8): + pytestmark = pytest.mark.skip('Skipping Unit Tests on python 2') + DEFAULT_ARGS = { 'hostname': 'hostname', diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_kerberos_realm.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_kerberos_realm.py index 30f577d4c..c1db9d26a 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_kerberos_realm.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_kerberos_realm.py @@ -1,4 +1,4 @@ -# (c) 2018-2022, NetApp, Inc +# (c) 2018-2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ''' unit test for ONTAP Kerberos Realm module ''' @@ -36,7 +36,8 @@ DEFAULT_ARGS = { 'realm': 'NETAPP.COM', 'vserver': 'vserver1', 'kdc_ip': '192.168.0.1', - 'kdc_vendor': 'other' + 'kdc_vendor': 'other', + } kerberos_info = { @@ -78,6 +79,15 @@ SRR = rest_responses({ "ip": "10.193.115.116", "port": 88 }, + "admin_server": { + "address": "10.193.115.116", + "port": 126 + }, + "password_server": { + "address": "1.2.3.4", + "port": 0 + }, + "clock_skew": 10, "comment": "mohan", "ad_server": { "name": "netapp", @@ -157,7 +167,7 @@ def test_if_all_methods_catch_exception(): ('kerberos-realm-create', ZRR['error']), ('kerberos-realm-modify', ZRR['error']), ('kerberos-realm-delete', ZRR['error']), - ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['is_rest_9_13_1']), ('GET', 'protocols/nfs/kerberos/realms', SRR['generic_error']), ('POST', 'protocols/nfs/kerberos/realms', SRR['generic_error']), ('PATCH', 'protocols/nfs/kerberos/realms/89368b07/NETAPP.COM', SRR['generic_error']), @@ -180,7 +190,7 @@ def test_if_all_methods_catch_exception(): def test_successfully_create_realm_rest(): ''' Test successfully create realm ''' register_responses([ - ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['is_rest_9_13_1']), ('GET', 'protocols/nfs/kerberos/realms', SRR['empty_records']), ('POST', 'protocols/nfs/kerberos/realms', SRR['success']), ]) @@ -191,11 +201,11 @@ def test_successfully_modify_realm_rest(): ''' Test modify realm successful for modifying kdc_ip. ''' register_responses([ # modify ip. - ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['is_rest_9_13_1']), ('GET', 'protocols/nfs/kerberos/realms', SRR['kerberos_info']), ('PATCH', 'protocols/nfs/kerberos/realms/89368b07/NETAPP.COM', SRR['success']), # modify port. - ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['is_rest_9_13_1']), ('GET', 'protocols/nfs/kerberos/realms', SRR['kerberos_info']), ('PATCH', 'protocols/nfs/kerberos/realms/89368b07/NETAPP.COM', SRR['success']), ]) @@ -206,8 +216,20 @@ def test_successfully_modify_realm_rest(): def test_successfully_delete_realm_rest(): ''' Test successfully delete realm ''' register_responses([ - ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['is_rest_9_13_1']), ('GET', 'protocols/nfs/kerberos/realms', SRR['kerberos_info']), ('DELETE', 'protocols/nfs/kerberos/realms/89368b07/NETAPP.COM', SRR['success']) ]) assert create_and_apply(my_module, DEFAULT_ARGS, {'use_rest': 'always', 'state': 'absent'}) + + +def test_error_with_params_supported_before_9_13_1_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ]) + args = {'use_rest': 'always', 'admin_server_ip': '10.193.115.116', 'admin_server_port': 126, 'clock_skew': 10, 'pw_server_ip': '1.2.3.4', + 'pw_server_port': 0} + error = create_module(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + unsupported_param_before_9_13_1 = ['admin_server_ip', 'admin_server_port', 'clock_skew', 'pw_server_ip', 'pw_server_port'] + msg = 'Error: Minimum version of ONTAP for ' + ' is (9, 13, 1).\nMinimum version of ONTAP for '.join(unsupported_param_before_9_13_1) + ' is (9, 13, 1).' + assert msg in error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_rest.py index fd65062d0..36d07815f 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_rest.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_rest.py @@ -1,4 +1,4 @@ -# (c) 2022, NetApp, Inc +# (c) 2022-2023, NetApp, Inc # 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) @@ -36,6 +36,10 @@ SRR = rest_responses({ "name": "volume1", "uuid": "028baa66-41bd-11e9-81d5-00a0986138f7" }, + "qtree": { + "name": "qtree1", + "id": 1, + }, }, "name": "/vol/volume1/qtree1/lun1", "space": { @@ -77,6 +81,10 @@ SRR = rest_responses({ "name": "volume1", "uuid": "028baa66-41bd-11e9-81d5-00a0986138f7" }, + "qtree": { + "name": "qtree1", + "id": 1, + }, }, "name": "/vol/volume1/qtree1/lun1", "space": { @@ -114,6 +122,10 @@ SRR = rest_responses({ "name": "volume2", "uuid": "028baa66-41bd-11e9-81d5-00a0986138f3" }, + "qtree": { + "name": "qtree1", + "id": 1, + }, }, "name": "/vol/volume1/qtree1/lun2", "space": { diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_ifgrp.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_ifgrp.py index 7e3e58783..011d53083 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_ifgrp.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_ifgrp.py @@ -1,4 +1,4 @@ -# (c) 2018, NetApp, Inc +# (c) 2018-2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ''' unit test template for ONTAP Ansible module ''' @@ -629,6 +629,7 @@ def test_create_ifgrp_port(mock_request, patch_ansible): SRR['is_rest_9_8'], # get version SRR['ifgrp_record_create'], # get SRR['empty_good'], # create + SRR['ifgrp_record_create'], # get details of created lag SRR['end_of_sequence'] ] my_obj = ifgrp_module() diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_qos_policy_group.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_qos_policy_group.py index c14b13151..44388fd54 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_qos_policy_group.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_qos_policy_group.py @@ -1,7 +1,7 @@ # (c) 2018-2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -''' unit test template for ONTAP Ansible module ''' +''' unit test cases for ONTAP Ansible module: na_ontap_qos_policy_group ''' from __future__ import (absolute_import, division, print_function) __metaclass__ = type @@ -349,6 +349,10 @@ def test_successful_create_adaptive_rest(): ('GET', 'cluster', SRR['is_rest_9_10_1']), ('GET', 'storage/qos/policies', SRR['empty_records']), ('POST', 'storage/qos/policies', SRR['success']), + # with expected and peak IOPS per TB + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/qos/policies', SRR['empty_records']), + ('POST', 'storage/qos/policies', SRR['success']), ]) DEFAULT_ARGS_COPY = DEFAULT_ARGS_REST.copy() del DEFAULT_ARGS_COPY['fixed_qos_options'] @@ -358,9 +362,14 @@ def test_successful_create_adaptive_rest(): "peak_iops": 500 } assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS_COPY)['changed'] + DEFAULT_ARGS_COPY['adaptive_qos_options']['block_size'] = '4k' assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS_COPY)['changed'] + DEFAULT_ARGS_COPY['adaptive_qos_options']['expected_iops_allocation'] = 'used_space' + DEFAULT_ARGS_COPY['adaptive_qos_options']['peak_iops_allocation'] = 'allocated_space' + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS_COPY)['changed'] + def test_partially_supported_option_rest(): ''' Test delete error ''' @@ -370,16 +379,19 @@ def test_partially_supported_option_rest(): ]) error = create_module(qos_policy_group_module, DEFAULT_ARGS_REST, fail=True)['msg'] assert "Minimum version of ONTAP for 'fixed_qos_options.min_throughput_mbps' is (9, 8, 0)" in error + DEFAULT_ARGS_COPY = DEFAULT_ARGS_REST.copy() del DEFAULT_ARGS_COPY['fixed_qos_options'] DEFAULT_ARGS_COPY['adaptive_qos_options'] = { "absolute_min_iops": 100, "expected_iops": 200, "peak_iops": 500, - "block_size": "4k" + "block_size": "4k", + "expected_iops_allocation": "used_space", + "peak_iops_allocation": "allocated_space" } error = create_module(qos_policy_group_module, DEFAULT_ARGS_COPY, fail=True)['msg'] - assert "Minimum version of ONTAP for 'adaptive_qos_options.block_size' is (9, 10, 1)" in error + assert "using any of ['block_size', 'expected_iops_allocation', 'peak_iops_allocation'] requires ONTAP 9.10.1 or later and REST must be enabled" in error def test_error_create_adaptive_rest(): @@ -486,7 +498,9 @@ def test_successful_modify_adaptive_qos_options_rest(): 'expected_iops': 300, 'peak_iops': 600, 'absolute_min_iops': 200, - 'block_size': '4k' + 'block_size': '4k', + 'expected_iops_allocation': 'used_space', + 'peak_iops_allocation': 'allocated_space' } } assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS_REST_COPY, args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_rest_info.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_rest_info.py index bf678e3ac..08ea9f2bc 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_rest_info.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_rest_info.py @@ -1,4 +1,4 @@ -# (c) 2020-2022, NetApp, Inc +# (c) 2020-2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ''' Unit Tests NetApp ONTAP REST APIs Ansible module: na_ontap_rest_info ''' @@ -56,6 +56,12 @@ SRR = rest_responses({ 'records': [{'name': 'dummy_vol1'}, {'name': 'dummy_vol2'}], 'version': 'ontap_version'}, None), + 'get_subset_info_without_hal_links': (200, + {'num_records': 3, + 'records': [{'name': 'dummy_vol1'}, + {'name': 'dummy_vol2'}, + {'name': 'dummy_vol3'}], + 'version': 'ontap_version'}, None), 'metrocluster_post': (200, {'job': { 'uuid': 'fde79888-692a-11ea-80c2-005056b39fe7', @@ -617,6 +623,17 @@ def set_default_args(): }) +def set_args_run_ontap_gather_facts_disable_hal_links(): + return dict({ + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'https': True, + 'validate_certs': False, + 'hal_linking': False + }) + + def set_args_run_ontap_version_check(): return dict({ 'hostname': 'hostname', @@ -763,6 +780,19 @@ def test_version_warning_message(): 'your version of ONTAP cluster/metrocluster/diagnostics requires (9, 8), ') +def test_owning_resource_warning_message(): + gather_subset = ['cluster/nodes'] + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'storage/volumes', SRR['get_subset_info']), + ]) + extra_args = { + 'owning_resource': {'svm_name': 'testSVM'} + } + create_and_apply(ontap_rest_info_module, set_args_run_ontap_version_check(), extra_args) + assert_warning_was_raised("Kindly refer to Ansible documentation to check the subsets that support option 'owning_resource'.") + + # metrocluster/diagnostics doesn't call get_subset_info and has 3 api calls instead of 1 def test_run_metrocluster_pass(): gather_subset = ['cluster/metrocluster/diagnostics'] @@ -981,6 +1011,16 @@ def test_demo_subset(): assert 'cluster/nodes' in call_main(my_main, set_default_args(), {'gather_subset': 'demo'})['ontap_info'] +def test_demo_subset_without_hal_links(): + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'cluster/software', SRR['get_subset_info_without_hal_links']), + ('GET', 'svm/svms', SRR['get_subset_info_without_hal_links']), + ('GET', 'cluster/nodes', SRR['get_subset_info_without_hal_links']), + ]) + assert 'cluster/nodes' in call_main(my_main, set_args_run_ontap_gather_facts_disable_hal_links(), {'gather_subset': 'demo'})['ontap_info'] + + def test_subset_with_default_fields(): register_responses([ ('GET', 'cluster', SRR['validate_ontap_version_pass']), diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_restit.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_restit.py index 89289386a..6b15d9087 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_restit.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_restit.py @@ -97,7 +97,7 @@ def test_rest_run_default_get(mock_request, patch_ansible): my_obj = my_module() with pytest.raises(AnsibleExitJson) as exc: my_obj.apply() - assert exc.value.args[0]['changed'] is True + assert exc.value.args[0]['changed'] is False print(mock_request.mock_calls) assert len(mock_request.mock_calls) == 1 diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_services.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_services.py index fce59093a..4bbd43f82 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_services.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_services.py @@ -1,4 +1,4 @@ -# (c) 2022, NetApp, Inc +# (c) 2022-2023, NetApp, Inc # 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) @@ -97,7 +97,8 @@ def test_create_s3_service(): ('GET', 'cluster', SRR['is_rest_9_10_1']), ('GET', 'cluster', SRR['is_rest_9_10_1']), ('GET', 'protocols/s3/services', SRR['empty_records']), - ('POST', 'protocols/s3/services', SRR['empty_good']) + ('POST', 'protocols/s3/services', SRR['empty_good']), + ('GET', 'protocols/s3/services', SRR['s3_service']), ]) module_args = { 'enabled': True, @@ -107,6 +108,22 @@ def test_create_s3_service(): assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] +def test_create_s3_service_response(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/services', SRR['empty_records']), + ('POST', 'protocols/s3/services', SRR['empty_good']), + ('GET', 'protocols/s3/services', SRR['s3_service']), + ]) + module_args = { + 'enabled': True, + 'comment': 'this is a s3 service', + 'certificate_name': 'cert1', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['s3_service_info'] is not None + + def test_create_s3_service_error(): register_responses([ ('GET', 'cluster', SRR['is_rest_9_8_0']), @@ -152,7 +169,8 @@ def test_modify_s3_service(): ('GET', 'cluster', SRR['is_rest_9_10_1']), ('GET', 'cluster', SRR['is_rest_9_10_1']), ('GET', 'protocols/s3/services', SRR['s3_service']), - ('PATCH', 'protocols/s3/services/08c8a385-b1ac-11ec-bd2e-005056b3b297', SRR['empty_good']) + ('PATCH', 'protocols/s3/services/08c8a385-b1ac-11ec-bd2e-005056b3b297', SRR['empty_good']), + ('GET', 'protocols/s3/services', SRR['s3_service']), ]) module_args = {'comment': 'this is a modified s3 service', 'enabled': False, @@ -161,6 +179,21 @@ def test_modify_s3_service(): assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] +def test_modify_s3_service_response(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/services', SRR['s3_service']), + ('PATCH', 'protocols/s3/services/08c8a385-b1ac-11ec-bd2e-005056b3b297', SRR['empty_good']), + ('GET', 'protocols/s3/services', SRR['s3_service']), + ]) + module_args = {'comment': 'this is a modified s3 service', + 'enabled': False, + 'certificate_name': 'cert2', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['s3_service_info'] is not None + + def test_modify_s3_service_error(): register_responses([ ('GET', 'cluster', SRR['is_rest_9_8_0']), diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_certificates.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_certificates.py index 866dd3a58..636e826ab 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_certificates.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_certificates.py @@ -113,7 +113,7 @@ def test_rest_create_failed(mock_request): def test_rest_successful_create(mock_request): mock_request.side_effect = [ SRR['is_rest'], - SRR['get_uuid'], # validate data vserver exist. + SRR['get_uuid'], # validate data vserver exists. SRR['empty_records'], # get certificate -> not found SRR['empty_good'], SRR['end_of_sequence'] @@ -132,6 +132,28 @@ def test_rest_successful_create(mock_request): @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_check_module_output(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], # validate data vserver exists. + SRR['empty_records'], # get certificate -> not found + SRR['empty_good'], + SRR['end_of_sequence'] + ] + data = { + 'type': 'server', + 'vserver': 'abc', + 'common_name': 'cname' + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['ontap_info'] is not None + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') def test_rest_idempotent_create(mock_request): mock_request.side_effect = [ SRR['is_rest'], diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapmirror.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapmirror.py index 9ba179279..0227c4e44 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapmirror.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapmirror.py @@ -45,7 +45,7 @@ DEFAULT_ARGS = { } -def sm_rest_info(state, healthy, transfer_state=None, destination_path=DEFAULT_ARGS['destination_path']): +def sm_rest_info(state, healthy, transfer_state=None, policy_type=None, destination_path=DEFAULT_ARGS['destination_path']): record = { 'uuid': 'b5ee4571-5429-11ec-9779-005056b39a06', 'destination': { @@ -63,6 +63,8 @@ def sm_rest_info(state, healthy, transfer_state=None, destination_path=DEFAULT_A record['transfer']['uuid'] = 'xfer_uuid' if healthy is False: record['unhealthy_reason'] = 'this is why the relationship is not healthy.' + if policy_type: + record['policy']['type'] = policy_type record['transfer_schedule'] = {'name': 'abc'} return { @@ -110,9 +112,12 @@ SRR = rest_responses({ 'sm_get_uninitialized': (200, sm_rest_info('uninitialized', True), None), 'sm_get_uninitialized_xfering': (200, sm_rest_info('uninitialized', True, 'transferring'), None), 'sm_get_mirrored': (200, sm_rest_info('snapmirrored', True, 'success'), None), + 'sm_sync_get_mirrored': (200, sm_rest_info('in_sync', True, 'success', 'sync'), None), 'sm_get_restore': (200, sm_rest_info('snapmirrored', True, 'success', destination_path=DEFAULT_ARGS['source_path']), None), 'sm_get_paused': (200, sm_rest_info('paused', True, 'success'), None), + 'sm_sync_get_paused': (200, sm_rest_info('paused', True, 'success', 'sync'), None), 'sm_get_broken': (200, sm_rest_info('broken_off', True, 'success'), None), + 'sm_sync_get_broken': (200, sm_rest_info('broken_off', True, 'success', 'sync'), None), 'sm_get_data_transferring': (200, sm_rest_info('transferring', True, 'transferring'), None), 'sm_get_abort': (200, sm_rest_info('sm_get_abort', False, 'failed'), None), 'sm_get_resync': (200, { @@ -1181,12 +1186,28 @@ def test_rest_resync_when_state_is_broken(dont_sleep): assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] +@patch('time.sleep') +def test_rest_synchronous_sm_resync_when_state_is_broken(dont_sleep): + ''' resync when snapmirror state is broken and relationship_state active ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['sm_sync_get_broken']), # apply first sm_get with state broken_off + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm resync response + ('GET', 'snapmirror/relationships', SRR['sm_sync_get_mirrored']), # check for idle + ('GET', 'snapmirror/relationships', SRR['sm_sync_get_mirrored']), # check_health calls sm_get + ]) + module_args = { + "use_rest": "always", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + def test_rest_resume_when_state_quiesced(): ''' resync when snapmirror state is broken and relationship_state active ''' register_responses([ ('GET', 'cluster', SRR['is_rest_9_8_0']), ('GET', 'snapmirror/relationships', SRR['sm_get_paused']), # apply first sm_get with state quiesced - ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm resync response + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm resume response ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # sm update calls sm_get ('POST', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06/transfers', SRR['success']), # sm update response ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # check_health calls sm_get @@ -1197,6 +1218,22 @@ def test_rest_resume_when_state_quiesced(): assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] +def test_rest_synchronous_sm_resume_when_state_quiesced(): + ''' resync when snapmirror state is broken and relationship_state active ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['sm_sync_get_paused']), # apply first sm_get with state quiesced + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm resume response + ('GET', 'snapmirror/relationships', SRR['sm_sync_get_mirrored']), # sm update calls sm_get + # ('POST', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06/transfers', SRR['success']), # sm update response + ('GET', 'snapmirror/relationships', SRR['sm_sync_get_mirrored']), # check_health calls sm_get + ]) + module_args = { + "use_rest": "always", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + @patch('time.sleep') def test_rest_snapmirror_delete(dont_sleep): ''' snapmirror delete ''' diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapshot_policy_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapshot_policy_rest.py index b79507759..5533451af 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapshot_policy_rest.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapshot_policy_rest.py @@ -156,7 +156,7 @@ def test_module_error_ontap_version(): assert 'Error: REST requires ONTAP 9.8 or later for snapshot schedules.' == msg -def test_create_snapshot_polciy_rest(): +def test_create_snapshot_policy_rest(): ''' Test create with rest API''' register_responses([ ('GET', 'cluster', SRR['is_rest_9_9_0']), @@ -166,7 +166,7 @@ def test_create_snapshot_polciy_rest(): assert create_and_apply(my_module, ARGS_REST) -def test_create_snapshot_polciy_with_snapmirror_label_rest(): +def test_create_snapshot_policy_with_snapmirror_label_rest(): ''' Test create with rest API''' register_responses([ ('GET', 'cluster', SRR['is_rest_9_9_0']), @@ -179,7 +179,7 @@ def test_create_snapshot_polciy_with_snapmirror_label_rest(): assert create_and_apply(my_module, ARGS_REST, module_args) -def test_create_snapshot_polciy_with_prefix_rest(): +def test_create_snapshot_policy_with_prefix_rest(): ''' Test create with rest API''' register_responses([ ('GET', 'cluster', SRR['is_rest_9_9_0']), @@ -192,7 +192,7 @@ def test_create_snapshot_polciy_with_prefix_rest(): assert create_and_apply(my_module, ARGS_REST, module_args) -def test_error_create_snapshot_polciy_rest(): +def test_error_create_snapshot_policy_rest(): ''' Test error create with rest API''' register_responses([ ('GET', 'cluster', SRR['is_rest_9_9_0']), diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snmp.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snmp.py index 24d8c5da4..48cbc1107 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snmp.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snmp.py @@ -47,6 +47,7 @@ SRR = { 'zero_record': (200, dict(records=[], num_records=0), None), 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'server_error': (500, None, "Internal Server error"), 'generic_error': (400, None, "Expected error"), 'community_user_record': (200, { 'records': [{ @@ -56,11 +57,17 @@ SRR = { }], 'num_records': 1 }, None), - 'snmp_user_record': (200, { + 'snmp_usm_user_record': (200, { 'records': [{ "name": "snmpv3user3", "authentication_method": "usm", - 'engine_id': "80000315058e02057c0fb8e911bc9f005056bb942e" + 'engine_id': "80000315058e02057c0fb8e911bc9f005056bb942e", + 'snmpv3': { + 'privacy_protocol': 'aes128', + 'authentication_password': 'humTdumt*@t0nAwa11', + 'authentication_protocol': 'sha', + 'privacy_password': 'p@**GOandCLCt*200' + } }], 'num_records': 1 }, None), @@ -73,15 +80,15 @@ def test_module_fail_when_required_args_missing(patch_ansible): set_module_args(dict(hostname='')) my_module() print('Info: %s' % exc.value.args[0]['msg']) - msg = 'missing required arguments: community_name' + msg = 'missing required arguments: snmp_username' assert msg == exc.value.args[0]['msg'] @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') -def test_ensure_get_community_called(mock_request, patch_ansible): +def test_ensure_get_snmp_community_called(mock_request, patch_ansible): ''' test get''' args = dict(default_args()) - args['community_name'] = 'snmpv3user2' + args['snmp_username'] = 'snmpv3user2' set_module_args(args) mock_request.side_effect = [ SRR['is_rest_9_8'], # get version @@ -97,10 +104,29 @@ def test_ensure_get_community_called(mock_request, patch_ansible): @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') -def test_ensure_create_community_called(mock_request, patch_ansible): +def test_ensure_get_snmp_usm_called(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['snmp_username'] = 'snmpv3user3' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['snmp_usm_user_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_create_snmp_community_called(mock_request, patch_ansible): ''' test get''' args = dict(default_args()) - args['community_name'] = 'snmpv3user2' + args['snmp_username'] = 'snmpv3user2' set_module_args(args) mock_request.side_effect = [ SRR['is_rest_9_8'], # get version @@ -117,10 +143,48 @@ def test_ensure_create_community_called(mock_request, patch_ansible): @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') -def test_ensure_delete_community_called(mock_request, patch_ansible): +def test_ensure_create_snmp_usm_called(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['snmp_username'] = 'snmpv3user3' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['zero_record'], # get + SRR['empty_good'], # create + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +def test_fail_to_create_snmp_usm_without_snmpv3_passwords(patch_ansible): + ''' required arguments are reported as errors ''' + usm_record = { + 'snmp_username': 'usm19', + 'authentication_method': 'usm', + 'snmpv3': { + 'privacy_protocol': 'aes128', + 'authentication_protocol': 'sha' + } + } + with pytest.raises(AnsibleFailJson) as exc: + set_module_args(usm_record) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + msg = 'missing required arguments:' + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_delete_snmp_community_called(mock_request, patch_ansible): ''' test get''' args = dict(default_args()) - args['community_name'] = 'snmpv3user2' + args['snmp_username'] = 'snmpv3user2' args['state'] = 'absent' set_module_args(args) mock_request.side_effect = [ @@ -139,10 +203,52 @@ def test_ensure_delete_community_called(mock_request, patch_ansible): @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') -def test_ensure_delete_community_idempotent(mock_request, patch_ansible): +def test_ensure_delete_snmp_usm_called(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['snmp_username'] = 'snmpv3user3' + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['snmp_usm_user_record'], # get + SRR['snmp_usm_user_record'], + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_delete_snmp_community_idempotent(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['snmp_username'] = 'snmpv3user2' + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['zero_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_delete_snmp_usm_idempotent(mock_request, patch_ansible): ''' test get''' args = dict(default_args()) - args['community_name'] = 'snmpv3user2' + args['snmp_username'] = 'snmpv3user3' args['state'] = 'absent' set_module_args(args) mock_request.side_effect = [ diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snmp_config_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snmp_config_rest.py new file mode 100644 index 000000000..bf6236825 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snmp_config_rest.py @@ -0,0 +1,123 @@ +# Copyright: NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_snmp_config """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible, \ + create_and_apply, create_module, call_main, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_snmp_config \ + import NetAppOntapSNMPConfig as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip( + 'Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always', + 'state': 'present' +} + + +# REST API canned responses when mocking send_request. +# The rest_factory provides default responses shared across testcases. +SRR = rest_responses({ + # module specific responses + 'snmp_config': (200, { + 'auth_traps_enabled': False, + 'enabled': True, + 'traps_enabled': False, + }, None), + 'snmp_config_disabled': (200, { + 'enabled': False + }, None), + 'snmp_config_modified': (200, { + 'auth_traps_enabled': True, + 'traps_enabled': True + }, None), +}) + + +def test_successful_disable_snmp(): + ''' Test successful rest modify SNMP config with idempotency check''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'support/snmp', SRR['snmp_config']), # get SNMP config + ('PATCH', 'support/snmp', SRR['success']), # update SNMP config + + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'support/snmp', SRR['snmp_config_disabled']), # get SNMP config + ]) + args = { + 'enabled': 'false' + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_successful_modify_snmp_config(): + ''' Test successful rest modify SNMP config with idempotency check''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/snmp', SRR['snmp_config']), # get SNMP config + ('PATCH', 'support/snmp', SRR['success']), # update SNMP config + + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/snmp', SRR['snmp_config_modified']), # get SNMP config + ]) + args = { + 'auth_traps_enabled': True, + 'traps_enabled': True + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_all_methods_catch_exception(): + ''' Test exception in get/modify SNMP config ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + # GET/PATCH error + ('GET', 'support/snmp', SRR['generic_error']), + ('PATCH', 'support/snmp', SRR['generic_error']) + ]) + modify_args = { + 'enabled': 'false' + } + snmp_config = create_module(my_module, DEFAULT_ARGS) + assert 'Error fetching SNMP config' in expect_and_capture_ansible_exception(snmp_config.get_snmp_config_rest, 'fail')['msg'] + assert 'Error modifying SNMP config' in expect_and_capture_ansible_exception(snmp_config.modify_snmp_config_rest, 'fail', modify_args)['msg'] + + +def test_error_ontap97(): + ''' Test module supported from 9.7 ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ]) + assert 'requires ONTAP 9.7.0 or later' in call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] + + +def test_partially_supported_options_rest(): + ''' Test REST version error for parameters ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + args = { + 'traps_enabled': 'true' + } + error = create_module(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Minimum version of ONTAP for traps_enabled is (9, 10, 1)' in error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_svm.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_svm.py index d18d32a57..757eca2e9 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_svm.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_svm.py @@ -282,7 +282,7 @@ def test_init_error(): 'services': {'ndmp': {'allowed': True}}, } error = create_module(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] - assert error == 'using ndmp requires ONTAP 9.7 or later and REST must be enabled - ONTAP version: 9.6.0 - using REST.' + assert error == 'using ndmp requires ONTAP 9.10.1 or later and REST must be enabled - ONTAP version: 9.6.0 - using REST.' def test_successful_rename(): diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume.py index 3161ead04..aae0f49a3 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume.py @@ -1544,6 +1544,7 @@ def test_successful_modify_snapshot_auto_delete(get_volume): ''' Test successful modify unix permissions flexGroup ''' register_responses([ # One ZAPI call for each option! + ('ZAPI', 'volume-modify-iter', ZRR['success']), ('ZAPI', 'snapshot-autodelete-set-option', ZRR['success']), ('ZAPI', 'snapshot-autodelete-set-option', ZRR['success']), ('ZAPI', 'snapshot-autodelete-set-option', ZRR['success']), diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_rest.py index 47525beec..20e3ba0f7 100644 --- a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_rest.py +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_rest.py @@ -78,7 +78,16 @@ volume_info = { }, "size": 10737418240, "snapshot": { - "reserve_percent": 5 + "reserve_percent": 5, + "autodelete": { + "enabled": False, + "trigger": "volume", + "delete_order": "oldest_first", + "defer_delete": "user_created", + "commitment": "try", + "target_free_space": 20, + "prefix": "(not specified)" + } } }, "guarantee": { @@ -89,7 +98,9 @@ volume_info = { }, "analytics": { "state": "on" - } + }, + "access_time_enabled": True, + "snapshot_directory_access_enabled": True } volume_info_mount = copy.deepcopy(volume_info) @@ -114,6 +125,8 @@ SRR = rest_responses({ # common responses 'is_rest': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), 'is_rest_96': (200, dict(version=dict(generation=9, major=6, minor=0, full='dummy_9_6_0')), None), + 'is_rest_9_8_0': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy_9_8_0')), None), + 'is_rest_9_13_1': (200, dict(version=dict(generation=9, major=13, minor=1, full='dummy_9_13_1')), None), 'is_zapi': (400, {}, "Unreachable"), 'empty_good': (200, {}, None), 'no_record': (200, {'num_records': 0, 'records': []}, None), @@ -507,6 +520,100 @@ def test_rest_error_modify_attributes(): assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == msg +def test_rest_version_error_with_atime_update(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']) + ]) + module_args = { + 'atime_update': False + } + error = create_module(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] + print('error', error) + assert 'Minimum version of ONTAP for atime_update is (9, 8)' in error + + +def test_rest_version_error_with_snapdir_access(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_1']) + ]) + module_args = { + 'snapdir_access': False + } + error = create_module(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] + print('error', error) + assert 'Minimum version of ONTAP for snapdir_access is (9, 13, 1)' in error + + +def test_rest_version_error_with_snapshot_auto_delete(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_1']) + ]) + module_args = { + 'snapshot_auto_delete': {'state': 'on'} + } + error = create_module(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] + print('error', error) + assert 'Minimum version of ONTAP for snapshot_auto_delete is (9, 13, 1)' in error + + +def test_rest_version_error_with_vol_nearly_full_threshold_percent(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']) + ]) + module_args = { + 'vol_nearly_full_threshold_percent': 96 + } + error = create_module(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] + print('error', error) + assert 'Minimum version of ONTAP for vol_nearly_full_threshold_percent is (9, 9)' in error + + +def test_rest_successfully_modify_attributes_atime_update(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # Modify + ]) + module_args = { + 'atime_update': False, + } + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_successfully_modify_attributes_snapdir_access_and_snapshot_auto_delete(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_13_1']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # Modify + ]) + module_args = { + 'snapdir_access': False, + 'snapshot_auto_delete': { + 'state': 'on', + 'trigger': 'volume', + 'delete_order': 'oldest_first', + 'defer_delete': 'user_created', + 'commitment': 'try', + 'target_free_space': 25, + 'prefix': 'prefix1' + } + } + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_successfully_modify_vol_threshold_percent_params(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # Modify + ]) + module_args = { + 'vol_nearly_full_threshold_percent': 98, + 'vol_full_threshold_percent': 99 + } + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + def test_rest_successfully_create_volume(): register_responses([ ('GET', 'cluster', SRR['is_rest']), diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan_scanner_pool_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan_scanner_pool_rest.py new file mode 100644 index 000000000..2cdc97a59 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan_scanner_pool_rest.py @@ -0,0 +1,256 @@ +# Copyright: NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_vscan_scanner_pool """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible, \ + create_and_apply, create_module, call_main, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_vscan_scanner_pool \ + import NetAppOntapVscanScannerPool as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip( + 'Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always' +} + + +# REST API canned responses when mocking send_request. +# The rest_factory provides default responses shared across testcases. +SRR = rest_responses({ + # module specific responses + 'scanner_pool_info': (200, {"records": [ + { + "name": "Scanner1", + "servers": [ + "10.193.78.219", + "10.193.78.221" + ], + "privileged_users": [ + "cifs\\user1", + "cifs\\user2" + ], + "role": "primary" + } + ]}, None), + 'svm_uuid': (200, {"records": [ + { + 'uuid': '5ec77839-b9b9-11ee-8084-005056b3d69a' + } + ], "num_records": 1}, None) +}) + + +svm_uuid = '5ec77839-b9b9-11ee-8084-005056b3d69a' +scanner_pool_name = 'Scanner1' + + +def test_successful_create(): + ''' Test successful rest create ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/%s/scanner-pools' % (svm_uuid), SRR['empty_records']), + ('POST', 'protocols/vscan/%s/scanner-pools' % (svm_uuid), SRR['empty_good']), + ]) + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'hostnames': ['10.193.78.219', '10.193.78.221'], + 'scanner_policy': 'primary', + 'privileged_users': ['cifs\\user1', 'cifs\\user2'], + 'scanner_pool': 'Scanner1' + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_successful_create_idempotency(): + ''' Test successful rest create idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/%s/scanner-pools' % (svm_uuid), SRR['scanner_pool_info']), + ]) + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'hostnames': ['10.193.78.219', '10.193.78.221'], + 'scanner_policy': 'primary', + 'privileged_users': ['cifs\\user1', 'cifs\\user2'], + 'scanner_pool': 'Scanner1' + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] is False + + +def test_successful_delete(): + ''' Test successful rest delete ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/%s/scanner-pools' % (svm_uuid), SRR['scanner_pool_info']), + ('DELETE', 'protocols/vscan/%s/scanner-pools/%s' % (svm_uuid, scanner_pool_name), SRR['success']), + ]) + args = { + 'state': 'absent', + 'vserver': 'ansibleSVM', + 'scanner_pool': 'Scanner1' + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_successful_delete_idempotency(): + ''' Test successful rest delete idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/%s/scanner-pools' % (svm_uuid), SRR['empty_records']), + ]) + args = { + 'state': 'absent', + 'vserver': 'ansibleSVM', + 'scanner_pool': 'Scanner1' + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] is False + + +def test_successful_modify(): + ''' Test successful rest modify ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/%s/scanner-pools' % (svm_uuid), SRR['scanner_pool_info']), + ('PATCH', 'protocols/vscan/%s/scanner-pools/%s' % (svm_uuid, scanner_pool_name), SRR['success']), + ]) + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'hostnames': ['10.193.78.219'], + 'scanner_policy': 'idle', + 'privileged_users': ['cifs\\user1'], + 'scanner_pool': 'Scanner1' + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_error_get(): + ''' Test error rest get ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/%s/scanner-pools' % (svm_uuid), SRR['generic_error']), + ]), + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'hostnames': ['10.193.78.219', '10.193.78.221'], + 'scanner_policy': 'primary', + 'privileged_users': ['cifs\\user1', 'cifs\\user2'], + 'scanner_pool': 'Scanner1' + } + error = create_and_apply(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Error searching for Vscan Scanner Pool' in error + + +def test_error_create(): + ''' Test error rest create ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/%s/scanner-pools' % (svm_uuid), SRR['empty_records']), + ('POST', 'protocols/vscan/%s/scanner-pools' % (svm_uuid), SRR['generic_error']), + ]), + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'hostnames': ['10.193.78.219', '10.193.78.221'], + 'scanner_policy': 'primary', + 'privileged_users': ['cifs\\user1', 'cifs\\user2'], + 'scanner_pool': 'Scanner1' + } + error = create_and_apply(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Error creating Vscan Scanner Pool' in error + + +def test_error_modify(): + ''' Test error rest modify ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/%s/scanner-pools' % (svm_uuid), SRR['scanner_pool_info']), + ('PATCH', 'protocols/vscan/%s/scanner-pools/%s' % (svm_uuid, scanner_pool_name), SRR['generic_error']), + ]) + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'hostnames': ['10.193.78.219'], + 'scanner_policy': 'idle', + 'privileged_users': ['cifs\\user1'], + 'scanner_pool': 'Scanner1' + } + error = create_and_apply(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Error modifying Vscan Scanner Pool' in error + + +def test_error_delete(): + ''' Test error rest delete ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/%s/scanner-pools' % (svm_uuid), SRR['scanner_pool_info']), + ('DELETE', 'protocols/vscan/%s/scanner-pools/%s' % (svm_uuid, scanner_pool_name), SRR['generic_error']), + ]) + args = { + 'state': 'absent', + 'vserver': 'ansibleSVM', + 'scanner_pool': 'Scanner1' + } + error = create_and_apply(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Error deleting Vscan Scanner Pool' in error + + +def test_error_ontap96(): + ''' Test error module supported from 9.6 ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ]) + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'hostnames': ['10.193.78.219', '10.193.78.221'], + 'scanner_policy': 'primary', + 'privileged_users': ['cifs\\user1', 'cifs\\user2'], + 'scanner_pool': 'Scanner1' + } + msg = 'REST requires ONTAP 9.6 or later for /protocols/vscan/{{svm.uuid}}/scanner-pools APIs' + assert msg in call_main(my_main, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_missing_options_scanner_pool(): + ''' Test error missing option scanner_pool ''' + register_responses([]) + args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'hostnames': ['10.193.78.219', '10.193.78.221'], + 'scanner_policy': 'primary', + 'privileged_users': ['cifs\\user1', 'cifs\\user2'], + } + error = create_module(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'missing required arguments: scanner_pool' in error |