summaryrefslogtreecommitdiffstats
path: root/collections-debian-merged/ansible_collections/netapp_eseries
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-14 20:03:01 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-14 20:03:01 +0000
commita453ac31f3428614cceb99027f8efbdb9258a40b (patch)
treef61f87408f32a8511cbd91799f9cececb53e0374 /collections-debian-merged/ansible_collections/netapp_eseries
parentInitial commit. (diff)
downloadansible-a453ac31f3428614cceb99027f8efbdb9258a40b.tar.xz
ansible-a453ac31f3428614cceb99027f8efbdb9258a40b.zip
Adding upstream version 2.10.7+merged+base+2.10.8+dfsg.upstream/2.10.7+merged+base+2.10.8+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'collections-debian-merged/ansible_collections/netapp_eseries')
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/.gitignore1
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/.gitignore3
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/inspectionProfiles/profiles_settings.xml6
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/misc.xml4
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/modules.xml8
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/santricity.iml15
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/vcs.xml6
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/workspace.xml128
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/CHANGELOG.md60
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/CHANGELOG.rst37
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/COPYING674
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/FILES.json1825
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/Jenkinsfile40
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/MANIFEST.json35
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/README.md748
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/changelogs/.plugin-cache.yaml297
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/changelogs/changelog.yaml54
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/changelogs/config.yaml32
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/doc_fragments/netapp.py228
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/doc_fragments/santricity.py90
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_host.py105
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_host_detail.py102
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_lun_mapping.py141
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_storage_pool.py81
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_volume.py91
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/module_utils/netapp.py746
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/module_utils/santricity.py431
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_alerts.py253
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_alerts_syslog.py176
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_asup.py521
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_auditlog.py200
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_auth.py341
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_client_certificate.py232
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_discover.py313
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_drive_firmware.py209
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_facts.py918
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_firmware.py613
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_global.py367
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_host.py512
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_hostgroup.py279
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_ib_iser_interface.py250
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_iscsi_interface.py342
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_iscsi_target.py246
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_ldap.py391
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_lun_mapping.py247
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_mgmt_interface.py531
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_nvme_interface.py305
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_proxy_drive_firmware_upload.py150
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_proxy_firmware_upload.py149
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_proxy_systems.py579
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_storagepool.py926
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_syslog.py248
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_volume.py896
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_alerts.py286
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_amg.py268
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_amg_role.py244
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_amg_sync.py267
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_asup.py314
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_auditlog.py286
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_auth.py283
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_drive_firmware.py215
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_facts.py530
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_firmware.py488
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_flashcache.py442
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_global.py159
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_host.py544
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_hostgroup.py307
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_iscsi_interface.py407
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_iscsi_target.py297
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_ldap.py401
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_lun_mapping.py291
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_mgmt_interface.py723
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_snapshot_group.py376
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_snapshot_images.py257
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_snapshot_volume.py289
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_storage_system.py310
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_storagepool.py941
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_syslog.py286
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_volume.py868
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_volume_copy.py431
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/.travis.yml29
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/README.md138
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/defaults/main.yml44
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/meta/main.yml13
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/build_info.yml37
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/collect_facts/discovery.yml41
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/collect_facts/prefer_embedded.yml183
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/collect_facts/prefer_proxy.yml106
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/collect_facts/validate_system_api_url.yml23
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/main.yml17
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/proxy.yml48
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/proxy_security.yml128
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/README.md339
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/defaults/main.yml294
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/meta/main.yml19
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/initiator.yml109
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface.yml27
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface/ib_iser.yml29
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface/iscsi.yml54
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface/nvme_ib.yml29
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface/nvme_roce.yml41
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/lun_mapping.yml119
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/main.yml50
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/storage_pool_absent.yml22
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/storage_pool_present.yml23
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/volume.yml31
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/.travis.yml29
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/README.md273
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/defaults/main.yml195
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/meta/main.yml13
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/firmware.yml78
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/interface.yml117
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/logging.yml90
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/main.yml36
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/security.yml66
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/system.yml16
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/integration_config.yml32
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_alerts/tasks/main.yml117
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_alerts_syslog/tasks/main.yml112
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_asup/tasks/main.yml287
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_auditlog/tasks/main.yml220
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_auth/tasks/main.yml170
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_client_certificate/tasks/main.yml55
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_discover/tasks/main.yml64
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_drive_firmware/tasks/main.yml185
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_facts/tasks/main.yml19
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_firmware/tasks/firmware_legacy_tests.yml128
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_firmware/tasks/firmware_tests.yml320
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_firmware/tasks/main.yml2
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_global/tasks/main.yml185
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_host/tasks/main.yml189
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_hostgroup/tasks/main.yml137
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_ib_iser_interface/tasks/main.yml88
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_iscsi_interface/tasks/main.yml115
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_iscsi_target/tasks/main.yml81
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_ldap/tasks/main.yml104
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_lun_mapping/tasks/main.yml318
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_mgmt_interface/tasks/main.yml383
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_nvme_interface/tasks/ib.yml88
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_nvme_interface/tasks/main.yml2
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_nvme_interface/tasks/roce.yml105
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_proxy_drive_firmware_upload/tasks/main.yml65
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_proxy_firmware_upload/tasks/main.yml65
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_proxy_systems/tasks/main.yml160
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_storagepool/tasks/main.yml1038
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_syslog/tasks/main.yml127
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_volume/tasks/main.yml768
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_alerts.py194
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_alerts_syslog.py151
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_asup.py318
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_auditlog.py205
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_auth.py488
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_client_certificate.py373
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_discover.py168
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_drive_firmware.py212
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_facts.py470
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_firmware.py494
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_global.py494
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_host.py434
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_hostgroup.py140
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_ib_iser_interface.py159
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_iscsi_interface.py239
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_iscsi_target.py188
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_ldap.py371
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_lun_mapping.py196
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_mgmt_interface.py513
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_nvme_interface.py220
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_proxy_drive_firmware_upload.py137
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_proxy_firmware_upload.py136
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_proxy_systems.py497
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_storagepool.py715
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_syslog.py128
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_volume.py864
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/hubPreCheck.groovy8
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/hubScan.groovy13
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/hubScanDocker.groovy76
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/hubScanProject.groovy123
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/setupBlackduckBuildParameters.groovy16
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/setupBuildParameters.groovy3
-rw-r--r--collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/setupSynopsysDetect.groovy15
180 files changed, 44085 insertions, 0 deletions
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.gitignore b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.gitignore
new file mode 100644
index 00000000..0d20b648
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.gitignore
@@ -0,0 +1 @@
+*.pyc
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/.gitignore b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/.gitignore
new file mode 100644
index 00000000..26d33521
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/inspectionProfiles/profiles_settings.xml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 00000000..105ce2da
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+ <settings>
+ <option name="USE_PROJECT_PROFILE" value="false" />
+ <version value="1.0" />
+ </settings>
+</component> \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/misc.xml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/misc.xml
new file mode 100644
index 00000000..65531ca9
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/misc.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.6" project-jdk-type="Python SDK" />
+</project> \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/modules.xml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/modules.xml
new file mode 100644
index 00000000..6d9217c9
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/modules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectModuleManager">
+ <modules>
+ <module fileurl="file://$PROJECT_DIR$/.idea/santricity.iml" filepath="$PROJECT_DIR$/.idea/santricity.iml" />
+ </modules>
+ </component>
+</project> \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/santricity.iml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/santricity.iml
new file mode 100644
index 00000000..4be04a76
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/santricity.iml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+ <component name="NewModuleRootManager">
+ <content url="file://$MODULE_DIR$" />
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ </component>
+ <component name="PyDocumentationSettings">
+ <option name="format" value="PLAIN" />
+ <option name="myDocStringFormat" value="Plain" />
+ </component>
+ <component name="TestRunnerService">
+ <option name="PROJECT_TEST_RUNNER" value="Twisted Trial" />
+ </component>
+</module> \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/vcs.xml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/vcs.xml
new file mode 100644
index 00000000..94a25f7f
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="VcsDirectoryMappings">
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
+ </component>
+</project> \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/workspace.xml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/workspace.xml
new file mode 100644
index 00000000..9e16d530
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/.idea/workspace.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="BranchesTreeState">
+ <expand>
+ <path>
+ <item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
+ <item name="LOCAL_ROOT" type="e8cecc67:BranchNodeDescriptor" />
+ </path>
+ <path>
+ <item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
+ <item name="REMOTE_ROOT" type="e8cecc67:BranchNodeDescriptor" />
+ </path>
+ <path>
+ <item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
+ <item name="REMOTE_ROOT" type="e8cecc67:BranchNodeDescriptor" />
+ <item name="GROUP_NODE:github" type="e8cecc67:BranchNodeDescriptor" />
+ </path>
+ <path>
+ <item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
+ <item name="REMOTE_ROOT" type="e8cecc67:BranchNodeDescriptor" />
+ <item name="GROUP_NODE:origin" type="e8cecc67:BranchNodeDescriptor" />
+ </path>
+ </expand>
+ <select />
+ </component>
+ <component name="ChangeListManager">
+ <list default="true" id="21a53f3b-4354-4571-93f8-1c405d0fbe6d" name="Default Changelist" comment="">
+ <change afterPath="$PROJECT_DIR$/roles/nar_santricity_common/tasks/collect_facts/discovery.yml" afterDir="false" />
+ <change afterPath="$PROJECT_DIR$/roles/nar_santricity_common/tasks/collect_facts/prefer_embedded.yml" afterDir="false" />
+ <change afterPath="$PROJECT_DIR$/roles/nar_santricity_common/tasks/collect_facts/prefer_proxy.yml" afterDir="false" />
+ <change afterPath="$PROJECT_DIR$/roles/nar_santricity_common/tasks/collect_facts/validate_system_api_url.yml" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/doc_fragments/netapp.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/doc_fragments/netapp.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/doc_fragments/santricity.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/doc_fragments/santricity.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/lookup/santricity_host.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/lookup/santricity_host.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/lookup/santricity_host_detail.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/lookup/santricity_host_detail.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/lookup/santricity_lun_mapping.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/lookup/santricity_lun_mapping.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/lookup/santricity_storage_pool.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/lookup/santricity_storage_pool.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/lookup/santricity_volume.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/lookup/santricity_volume.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/na_santricity_alerts.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/na_santricity_alerts.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/na_santricity_asup.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/na_santricity_asup.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/na_santricity_auth.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/na_santricity_auth.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/na_santricity_discover.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/na_santricity_discover.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/na_santricity_facts.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/na_santricity_facts.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/na_santricity_firmware.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/na_santricity_firmware.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/na_santricity_global.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/na_santricity_global.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/na_santricity_host.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/na_santricity_host.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/na_santricity_hostgroup.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/na_santricity_hostgroup.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/na_santricity_ib_iser_interface.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/na_santricity_ib_iser_interface.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/na_santricity_iscsi_target.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/na_santricity_iscsi_target.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/na_santricity_lun_mapping.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/na_santricity_lun_mapping.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/na_santricity_mgmt_interface.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/na_santricity_mgmt_interface.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/na_santricity_proxy_systems.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/na_santricity_proxy_systems.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/na_santricity_volume.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/na_santricity_volume.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/netapp_e_alerts.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/netapp_e_alerts.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/netapp_e_host.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/netapp_e_host.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/netapp_e_iscsi_target.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/netapp_e_iscsi_target.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/netapp_e_mgmt_interface.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/netapp_e_mgmt_interface.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/netapp_e_snapshot_volume.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/netapp_e_snapshot_volume.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/plugins/modules/netapp_e_volume.py" beforeDir="false" afterPath="$PROJECT_DIR$/plugins/modules/netapp_e_volume.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/roles/nar_santricity_common/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/roles/nar_santricity_common/README.md" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/roles/nar_santricity_common/tasks/main.yml" beforeDir="false" afterPath="$PROJECT_DIR$/roles/nar_santricity_common/tasks/main.yml" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/roles/nar_santricity_common/tasks/proxy.yml" beforeDir="false" afterPath="$PROJECT_DIR$/roles/nar_santricity_common/tasks/proxy.yml" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/roles/nar_santricity_host/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/roles/nar_santricity_host/README.md" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/roles/nar_santricity_host/defaults/main.yml" beforeDir="false" afterPath="$PROJECT_DIR$/roles/nar_santricity_host/defaults/main.yml" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/roles/nar_santricity_host/tasks/initiator.yml" beforeDir="false" afterPath="$PROJECT_DIR$/roles/nar_santricity_host/tasks/initiator.yml" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/roles/nar_santricity_host/tasks/interface/nvme_roce.yml" beforeDir="false" afterPath="$PROJECT_DIR$/roles/nar_santricity_host/tasks/interface/nvme_roce.yml" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/roles/nar_santricity_host/tasks/lun_mapping.yml" beforeDir="false" afterPath="$PROJECT_DIR$/roles/nar_santricity_host/tasks/lun_mapping.yml" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/roles/nar_santricity_host/tasks/main.yml" beforeDir="false" afterPath="$PROJECT_DIR$/roles/nar_santricity_host/tasks/main.yml" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/roles/nar_santricity_host/tasks/storage_pool_present.yml" beforeDir="false" afterPath="$PROJECT_DIR$/roles/nar_santricity_host/tasks/storage_pool_present.yml" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/roles/nar_santricity_host/tasks/volume.yml" beforeDir="false" afterPath="$PROJECT_DIR$/roles/nar_santricity_host/tasks/volume.yml" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/roles/nar_santricity_management/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/roles/nar_santricity_management/README.md" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/roles/nar_santricity_management/tasks/main.yml" beforeDir="false" afterPath="$PROJECT_DIR$/roles/nar_santricity_management/tasks/main.yml" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/roles/nar_santricity_management/tasks/security.yml" beforeDir="false" afterPath="$PROJECT_DIR$/roles/nar_santricity_management/tasks/security.yml" afterDir="false" />
+ </list>
+ <option name="SHOW_DIALOG" value="false" />
+ <option name="HIGHLIGHT_CONFLICTS" value="true" />
+ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
+ <option name="LAST_RESOLUTION" value="IGNORE" />
+ </component>
+ <component name="Git.Settings">
+ <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
+ </component>
+ <component name="ProjectId" id="1ivHtNLFNo4aEog7rBplvbPg0ay" />
+ <component name="ProjectViewState">
+ <option name="hideEmptyMiddlePackages" value="true" />
+ <option name="showLibraryContents" value="true" />
+ </component>
+ <component name="PropertiesComponent">
+ <property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
+ <property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
+ <property name="last_opened_file_path" value="$PROJECT_DIR$" />
+ </component>
+ <component name="SvnConfiguration">
+ <configuration />
+ </component>
+ <component name="TaskManager">
+ <task active="true" id="Default" summary="Default task">
+ <changelist id="21a53f3b-4354-4571-93f8-1c405d0fbe6d" name="Default Changelist" comment="" />
+ <created>1602781359010</created>
+ <option name="number" value="Default" />
+ <option name="presentableId" value="Default" />
+ <updated>1602781359010</updated>
+ </task>
+ <servers />
+ </component>
+ <component name="Vcs.Log.Tabs.Properties">
+ <option name="TAB_STATES">
+ <map>
+ <entry key="MAIN">
+ <value>
+ <State />
+ </value>
+ </entry>
+ </map>
+ </option>
+ </component>
+ <component name="WindowStateProjectService">
+ <state x="262" y="51" key="MergeDialog" timestamp="1602781503332">
+ <screen x="0" y="0" width="2560" height="1440" />
+ </state>
+ <state x="262" y="51" key="MergeDialog/0.0.2560.1440@0.0.2560.1440" timestamp="1602781503332" />
+ <state x="801" y="213" width="977" height="774" key="MultipleFileMergeDialog" timestamp="1602781504721">
+ <screen x="0" y="0" width="2560" height="1440" />
+ </state>
+ <state x="801" y="213" width="977" height="774" key="MultipleFileMergeDialog/0.0.2560.1440@0.0.2560.1440" timestamp="1602781504721" />
+ </component>
+</project> \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/CHANGELOG.md b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/CHANGELOG.md
new file mode 100644
index 00000000..8635fd23
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/CHANGELOG.md
@@ -0,0 +1,60 @@
+## [1.0.1] - 2020-03-016
+### Added
+This release consists of all existing NetApp E-Series Ansible content modified to conform with Ansible collections.
+
+Initial Collection Content:
+ - Roles:
+ - nar_santricity_common: Discover NetApp E-Series storage systems and configures SANtricity Web Services Proxy.
+ - nar_santricity_host: Configure storage pools, volumes, hosts, host groups, and port interfaces. (nar_santricity_common is called at the beginning)
+ - nar_santricity_management: Manage storage system's name, management interfaces, alerts, syslog, auditlog, asup, ldap, certificates, drive firmware and
+ controller firmware. (nar_santricity_common is called at the beginning)
+ - Modules:
+ - na_santricity_alerts: Manage email alert notification settings
+ - na_santricity_alerts_syslog: Manage syslog servers receiving storage system alerts
+ - na_santricity_asup: Manage auto-support settings
+ - na_santricity_auditlog: Manage audit-log configuration
+ - na_santricity_auth: Set or update the password for a storage array
+ - na_santricity_client_certificate: Manage remote server certificates
+ - na_santricity_discover: Discover E-Series storage systems on a subnet
+ - na_santricity_drive_firmware: Manage drive firmware
+ - na_santricity_facts: Retrieve facts about NetApp E-Series storage arrays
+ - na_santricity_firmware: Manage firmware
+ - na_santricity_global: Manage global settings configuration
+ - na_santricity_host: Manage eseries hosts
+ - na_santricity_hostgroup: Manage array host groups
+ - na_santricity_iscsi_interface: Manage iSCSI interface configuration
+ - na_santricity_iscsi_target: Manage iSCSI target configuration
+ - na_santricity_ldap: Manage LDAP integration to use for authentication
+ - na_santricity_lun_mapping: Manage lun mappings
+ - na_santricity_mgmt_interface: Manage management interface configuration
+ - na_santricity_storage_system: Manage SANtricity web services proxy storage arrays
+ - na_santricity_storagepool: Manage volume groups and disk pools
+ - na_santricity_syslog: Manage syslog settings
+ - na_santricity_volume: Manage storage volumes
+ - Deprecated Modules:
+ - netapp_e_alerts: Manage email notification settings
+ - netapp_e_amg: Create, remove, and update asynchronous mirror groups
+ - netapp_e_amg_role: Update the role of a storage array within an Asynchronous Mirror Group (AMG)
+ - netapp_e_amg_sync: Conduct synchronization actions on asynchronous mirror groups
+ - netapp_e_asup: Manage auto-support settings
+ - netapp_e_auditlog: Manage audit-log configuration
+ - netapp_e_auth: Set or update the password for a storage array
+ - netapp_e_drive_firmware: Manage drive firmware
+ - netapp_e_facts: Retrieve facts about NetApp E-Series storage arrays
+ - netapp_e_firmware: Manage firmware
+ - netapp_e_flashcache: Manage SSD caches
+ - netapp_e_global: Manage global settings configuration
+ - netapp_e_hostgroup: Manage eseries hosts
+ - netapp_e_host: Manage array host groups
+ - netapp_e_iscsi_interface: Manage iSCSI interface configuration
+ - netapp_e_iscsi_target: Manage iSCSI target configuration
+ - netapp_e_ldap: Manage LDAP integration to use for authentication
+ - netapp_e_lun_mapping: Create, delete, or modify lun mappings
+ - netapp_e_mgmt_interface: Manage management interface configuration
+ - netapp_e_snapshot_group: Manage snapshot groups
+ - netapp_e_snapshot_images: Create and delete snapshot images
+ - netapp_e_snapshot_volume: Manage snapshot volumes
+ - netapp_e_storagepool: Manage volume groups and disk pools
+ - netapp_e_storage_system: Manage Web Services Proxy manage storage arrays
+ - netapp_e_syslog: Manage syslog settings
+ - netapp_e_volume_copy: Create volume copy pairs
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/CHANGELOG.rst b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/CHANGELOG.rst
new file mode 100644
index 00000000..6401cff6
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/CHANGELOG.rst
@@ -0,0 +1,37 @@
+=======================================
+Netapp_Eseries.Santricity Release Notes
+=======================================
+
+.. contents:: Topics
+
+
+v1.1.0
+======
+
+Release Summary
+---------------
+
+This release focused on providing volume details to through the netapp_volumes_by_initiators in the na_santricity_facts module, improving on the nar_santricity_common role storage system API information and resolving issues.
+
+Minor Changes
+-------------
+
+- Add functionality to remove all inventory configuration in the nar_santricity_host role. Set configuration.eseries_remove_all_configuration=True to remove all storage pool/volume configuration, host, hostgroup, and lun mapping configuration.
+- Add host_types, host_port_protocols, host_port_information, hostside_io_interface_protocols to netapp_volumes_by_initiators in the na_santricity_facts module.
+- Add storage pool information to the volume_by_initiator facts.
+- Add storage system not found exception to the common role's build_info task.
+- Add volume_metadata option to na_santricity_volume module, add volume_metadata information to the netapp_volumes_by_initiators dictionary in na_santricity_facts module, and update the nar_santricity_host role with the option.
+- Improve nar_santricity_common storage system api determinations; attempts to discover the storage system using the information provided in the inventory before attempting to search the subnet.
+- Increased the storage system discovery connection timeouts to 30 seconds to prevent systems from not being discovered over slow connections.
+- Minimize the facts gathered for the host initiators.
+- Update ib iser determination to account for changes in firmware 11.60.2.
+- Use existing Web Services Proxy storage system identifier when one is already created and one is not provided in the inventory.
+- Utilize eseries_iscsi_iqn before searching host for iqn in nar_santricity_host role.
+
+Bugfixes
+--------
+
+- Fix check_port_type method for ib iser when ib is the port type.
+- Fix examples in the netapp_e_mgmt_interface module.
+- Fix issue with changing host port name.
+- Fix na_santricity_lun_mapping unmapping issue; previously mapped volumes failed to be unmapped.
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/COPYING b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/COPYING
new file mode 100644
index 00000000..f288702d
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/COPYING
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/FILES.json b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/FILES.json
new file mode 100644
index 00000000..a5ee0021
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/FILES.json
@@ -0,0 +1,1825 @@
+{
+ "files": [
+ {
+ "format": 1,
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": ".",
+ "chksum_type": null
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "094c25031dcfe5c288666671bc2012866293b3797ea53c796af798bda4c32f01",
+ "name": ".gitignore",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": ".idea",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "a9e54fc4f08bc97d82c04bd86ff37d3af84ba9ef4ad87fcfcb438dab622e8aaa",
+ "name": ".idea/.gitignore",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": ".idea/inspectionProfiles",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "6172c5997eeb3cd19c9ca2b5b97d6e2983cdd1fa60b2460d7bbb74055edcaa46",
+ "name": ".idea/inspectionProfiles/profiles_settings.xml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "473096d4507a41e01441edeed8ba180d32b4bd2341ced01ac77a9c69041da969",
+ "name": ".idea/misc.xml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "9a7efaba28a37918be96d837aac83d2e5c8fdaaa96ef10e79ed883b8c0edb7a4",
+ "name": ".idea/modules.xml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "5e7dcb9c2be24c4fa009e79c426787e0bd2b24995bc04e7a3433e92cc5627848",
+ "name": ".idea/santricity.iml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "55768f1f84fb117f1d0fc383e180b5d0aa5e5e6584c8c4a2995c15b4237f0972",
+ "name": ".idea/vcs.xml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "673207ee19cbe2e31e9cc3bbdd550a3dabd7d6e8ba353c31cb963eb5196382c1",
+ "name": ".idea/workspace.xml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "9bd55ed3283670866dc4ab508802d8da56813558a778afaef544a7095c8d174c",
+ "name": "CHANGELOG.md",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "bc9a697f2d39b8b6cff79e5fb2d6ca4a7a2343e86b1ff58fe2f1b39dba52fbcf",
+ "name": "CHANGELOG.rst",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986",
+ "name": "COPYING",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "5b7a3c01f97a388420c05cff4dce2502b4c73a725b47c4be0aa3ed40b729f506",
+ "name": "Jenkinsfile",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "b5e2c3908b88993f2cbb8b8cd4db748a44c760a5f1eb829d88365be8412a9d9b",
+ "name": "README.md",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "changelogs",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "7630bdc73a2b65ce153cbe1a3ee0605f9bc0e1ee2b023d610a2f831e2555de34",
+ "name": "changelogs/.plugin-cache.yaml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "6752eff1ce020cb56d407d8d4e1739708b047f7fdf3cbb8d2e242a6f5e660229",
+ "name": "changelogs/changelog.yaml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "3aadb9449a8312a120a248967d3ced4763709e37698abd5bbea5bf6b2d0d7e13",
+ "name": "changelogs/config.yaml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "plugins",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "plugins/doc_fragments",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "30690bb17f81f249bcfd1f08152d4d566711021ca85ff0861702c1ff0bf815b1",
+ "name": "plugins/doc_fragments/netapp.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "9776843ca6967c09b7e60384a8f9ae766e4b291ae991eca5c288f018edd97dbb",
+ "name": "plugins/doc_fragments/santricity.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "plugins/lookup",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "eef5d1ec178056e91a92f007eb6daa49b6541cc596a6c5a7539be02a4896f841",
+ "name": "plugins/lookup/santricity_host.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "b9e18e05f61cb73121d5bfa4acd933a1e0dd81c71a4a002f3628e8c150e6a970",
+ "name": "plugins/lookup/santricity_host_detail.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "3964ce29875a46b6c3f623f07f2b418cab245aacc37941558ed255e99aeffd9a",
+ "name": "plugins/lookup/santricity_lun_mapping.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "e1f040b91b92a07887150d8fd9e51e13e886ff0a4062d1d338b72e9048ad8793",
+ "name": "plugins/lookup/santricity_storage_pool.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "6fbc40f7a8a1ff7f8fee92196785f9ce8826896b43c01f4e11e3f6693685995e",
+ "name": "plugins/lookup/santricity_volume.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "plugins/module_utils",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "ee54c8258cf42c09a7e88db777cf8bb0242280ffcb0b2e06c0c1b27942644246",
+ "name": "plugins/module_utils/netapp.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "e6ebea8beba26844ed5c6f620f3b0cf08e26e387250d7305d37c2a34b0933c95",
+ "name": "plugins/module_utils/santricity.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "plugins/modules",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "ca5d6defb67d7df905b244deb744e9dd7b2c82e205fe217d4b2aa282e776d74c",
+ "name": "plugins/modules/na_santricity_alerts.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "f8a81396af06bb497a85fb6c2caf0e4bab87f251316dad4b7f3b0619c0d8aa6e",
+ "name": "plugins/modules/na_santricity_alerts_syslog.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "5b8aebc75e128939ca4e9461c713b732346e4f3d30eb883215619f8000d08c80",
+ "name": "plugins/modules/na_santricity_asup.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "ab6c86b0c9105cfcf5743c3bf9ae67cca08b2b7c0d882e19ea11268282da1c0d",
+ "name": "plugins/modules/na_santricity_auditlog.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "be573603a985a7ffcab1a802109b2f8c90a3eb166e4b00f1b96a6503ad71cf75",
+ "name": "plugins/modules/na_santricity_auth.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "5237731221bdc4888bbfbf9ca33119d4b9857f3ab81a6860afa539247f3fc1f9",
+ "name": "plugins/modules/na_santricity_client_certificate.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "a322a213ada4c1c6e3ffb11cb86ac09c4970568bcf1897dea1f3dd33a40af23a",
+ "name": "plugins/modules/na_santricity_discover.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "786bef99041b59dc1bbb3709319ae2ec2630697e52012116a37aa11bd47f642f",
+ "name": "plugins/modules/na_santricity_drive_firmware.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "67492e17f7d55859379171b69ade5196a1fe002d4cc7a5dbdef01c6b585d1943",
+ "name": "plugins/modules/na_santricity_facts.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "c966e2729fb16867ad4b152640e710da110eb3d0f48655c5e7a0b9c610afd1dd",
+ "name": "plugins/modules/na_santricity_firmware.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "753f5955a94a80ea9d7ac49eb0439c299890d9b3a452aca52a23fe809cb1bc86",
+ "name": "plugins/modules/na_santricity_global.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "c4702e15a3ac67e4b68a84beafb3f140805e84aa7b59d21fb5dcafe97fe42d2b",
+ "name": "plugins/modules/na_santricity_host.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "d044e70f0a5fb572e74f0304c9742cf21a4dde2a253b5216f7173d2bc2261a17",
+ "name": "plugins/modules/na_santricity_hostgroup.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "46938ac21ab6eed604e504ccfcc233263632ab3b48c4103cbfc6ac5b4b98580f",
+ "name": "plugins/modules/na_santricity_ib_iser_interface.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "afc9d7fd625b79f205f1c635630aa3f02889ca9c1649dcf89189725fbb432534",
+ "name": "plugins/modules/na_santricity_iscsi_interface.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "7f633451b9669ab6582f0ecde28180cf9b8e01eca94d809dc6ee4c4b02241982",
+ "name": "plugins/modules/na_santricity_iscsi_target.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "1a8b64a1b21d14b1b781dca250c5531a617dccdb929307411797a6e002463522",
+ "name": "plugins/modules/na_santricity_ldap.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "fd0716ace016a9f6df416b312b2a0ea956c9e9d678ee439a1cc7f503a54ece4a",
+ "name": "plugins/modules/na_santricity_lun_mapping.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "a55ef258ca592b67a7a7b28bcd2bd90a23042c1d8a58dccaded6075a833830c6",
+ "name": "plugins/modules/na_santricity_mgmt_interface.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "ab149e051933c75c50d54c16b834fc4c72c41bacc07e52efeb7c573777adf775",
+ "name": "plugins/modules/na_santricity_nvme_interface.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "2f3b35caf8eec64d27d07f5a7d33d07eb84b2258ca69575f6f752656881bc826",
+ "name": "plugins/modules/na_santricity_proxy_drive_firmware_upload.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "9f8713206df951f245a0e35c35df154be0f30cc66e4e55a794548a54893cdbdb",
+ "name": "plugins/modules/na_santricity_proxy_firmware_upload.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "6af612769357c5153479323bd60e09d6f14bbf94d88a40bd957ac8223f175f1c",
+ "name": "plugins/modules/na_santricity_proxy_systems.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "0433855b62a234103e33e9106bb389a4a9dd460c0e3cd9f9a75d7b1b5723c513",
+ "name": "plugins/modules/na_santricity_storagepool.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "709c826f80fc0273fe836412df11ef06fd6cb9d12423d7659cdb928e80c51ab0",
+ "name": "plugins/modules/na_santricity_syslog.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "b136e8882ac9cfb5e74ea194fcd8ef5d369f22612d482ca22c5dc4a8fe16e1cc",
+ "name": "plugins/modules/na_santricity_volume.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "08c2159dde29a0e9d93eeb093d3dad8b02301912ee00305aeeca14f5a71469a1",
+ "name": "plugins/modules/netapp_e_alerts.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "cb66d4e13f9b36a7cbeee5b858f6968177b18dcef6d1d75e8e0f3388987520b9",
+ "name": "plugins/modules/netapp_e_amg.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "c43a87ad2d8c63201e8025e2a144845946a8855cfd0374838911308d3652d7cf",
+ "name": "plugins/modules/netapp_e_amg_role.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "a88c7ad0156ae75069b1a453f7d60e741c1de35500c888f142811eb393c9cb3a",
+ "name": "plugins/modules/netapp_e_amg_sync.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "4b50f232c688c6ac698a4ed681256e6f5590a88f8071b66906df61da76a7e002",
+ "name": "plugins/modules/netapp_e_asup.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "036a74f5d39604cb04b4eb48665485e9c2419f1e62889a34bdf6be7f83deef7d",
+ "name": "plugins/modules/netapp_e_auditlog.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "13987a41072dde6d6f187ede14c87904defab48c3fd7fbeda2790e53cdfe0594",
+ "name": "plugins/modules/netapp_e_auth.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "b0ce6a9d433adffe20a4a777ae6e473da95072e3da723b1426383deada085bb5",
+ "name": "plugins/modules/netapp_e_drive_firmware.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "3d5fc8d9dd8230df962c972bff06d261c8d6cf1cedd2eb3b8fe9fe881db99f9d",
+ "name": "plugins/modules/netapp_e_facts.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "b4dc8c105fc695981b1c3a85bd47ef9dc310ffd2eee9ac4e8d10bcbee2b30f9f",
+ "name": "plugins/modules/netapp_e_firmware.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "c16c7dcfdb0324510d29032a08f4b67509893ae717cf93affed3b183f826f8f1",
+ "name": "plugins/modules/netapp_e_flashcache.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "07a977d571ff46e74fb81017559ad8f6c4728da9f849703c90b11b2ef1a7bb6e",
+ "name": "plugins/modules/netapp_e_global.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "a9dea135fc49c2248a0492b995fba5f3d21b9cf0426da727752e88816d9267b3",
+ "name": "plugins/modules/netapp_e_host.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "2ca6f342ca6c0fb5e446c309aa7d4a677f59a4a2b8b21906bb8886d62dadc319",
+ "name": "plugins/modules/netapp_e_hostgroup.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "0ad785af26b09545e187f4cd672ee896cc94bca8a0a73d591d470a52dd5dba5c",
+ "name": "plugins/modules/netapp_e_iscsi_interface.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "99a33bbc5a9c63b7096e0de3d20eb619bf5e7c194d18ca0e8576d9393e7d0b5d",
+ "name": "plugins/modules/netapp_e_iscsi_target.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "d1d1ef7b578717e500990221238834ab0402e82eb46cbf6166c5603e436388ce",
+ "name": "plugins/modules/netapp_e_ldap.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "12ad1e184c0a38ec4429d72d0c8173c114b399a5822d04d082c14980d00f092e",
+ "name": "plugins/modules/netapp_e_lun_mapping.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "b49c27a1cc180cf20dabcfa342cff9c0a04b18e8e695b0a54146d73603efbba8",
+ "name": "plugins/modules/netapp_e_mgmt_interface.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "f6d47bfac5cf52cbc652f4bcb68179c9bed9f30363027fe35c5c28ff4d11a40c",
+ "name": "plugins/modules/netapp_e_snapshot_group.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "73874df191a3698c1e2ecdd3dda0256c3416a0bc274dd989b156d049b93154cb",
+ "name": "plugins/modules/netapp_e_snapshot_images.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "0cac16609ea088375b77878ac6b6c707a5e604d0e214f7202841449878a64051",
+ "name": "plugins/modules/netapp_e_snapshot_volume.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "b21a4263209e8e03b23268bd0513464d3a7c3df403f48b1ab063329498ecd479",
+ "name": "plugins/modules/netapp_e_storage_system.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "6151a66e8654d2bef10ef1bfa06b34c95cb9bafadd74d3d68d85882fa02b2b2d",
+ "name": "plugins/modules/netapp_e_storagepool.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "ee4a477da7ad80aece4f72332419baf3cb0b26c9e1007b62a32b44d36bf582d5",
+ "name": "plugins/modules/netapp_e_syslog.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "7936e0467a019baef57f4f5172290133fcabcbf6757c92882cb5fec26e791bb9",
+ "name": "plugins/modules/netapp_e_volume.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "1ca543b8dfcc630acab9aaa130a833f81ed00f5a9868dca7e535873363f157a1",
+ "name": "plugins/modules/netapp_e_volume_copy.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "roles",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "roles/nar_santricity_common",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "43e63a93716f15cdb102c201e4631be604b45910b8c749c78d89e3fc076c7c7e",
+ "name": "roles/nar_santricity_common/.travis.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "656eb0597979a834c962e1aed549ef3e8fae9d622c4c9826a757504e790233b4",
+ "name": "roles/nar_santricity_common/README.md",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "roles/nar_santricity_common/defaults",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "61b686fb0fb3088ca0d2ba4670dd7d86cad5fe51b8cccd829219a65d64459142",
+ "name": "roles/nar_santricity_common/defaults/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "roles/nar_santricity_common/meta",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "d0fe053407cc0e9f64645a6f40b4fa596a4799c1a29a6487add1c0f6890a4a5e",
+ "name": "roles/nar_santricity_common/meta/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "roles/nar_santricity_common/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "18b33aaa6a791d36384bab9e262941b36648c67a48c8cbb5908e943e58b277e8",
+ "name": "roles/nar_santricity_common/tasks/build_info.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "roles/nar_santricity_common/tasks/collect_facts",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "3264c8fbdaba719ea34800d64f89bbca9756c53ff5e2c2544840e54f7cfaef21",
+ "name": "roles/nar_santricity_common/tasks/collect_facts/discovery.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "9649d845ada521aa9ec3e427b44cbc89f983f0ed31f587ff952cf8dfe3b8e8e4",
+ "name": "roles/nar_santricity_common/tasks/collect_facts/prefer_embedded.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "399cdd9c96489d9793b4a456d64d8b7d5086fad2e1fe60e625fc4423904d680a",
+ "name": "roles/nar_santricity_common/tasks/collect_facts/prefer_proxy.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "1ab6007a7bc746709e438107d74168323dfff59a931e233ad126bef4078d277c",
+ "name": "roles/nar_santricity_common/tasks/collect_facts/validate_system_api_url.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "1284e21efded7946c14c3270e8b553f46d4f4ba6dc01ff928935a048c0cca804",
+ "name": "roles/nar_santricity_common/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "4c43af16a9b8ed930e85e5e35d9019d34c99190621d6917f734d7795970a81fd",
+ "name": "roles/nar_santricity_common/tasks/proxy.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "c4ccc742cec2e679b1872bdd41625561765583032701fa0010aa53c4913a5866",
+ "name": "roles/nar_santricity_common/tasks/proxy_security.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "roles/nar_santricity_host",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "6cda777bbb3916a31d24c9c037a965561129d297490bbe3afef2887b6605def6",
+ "name": "roles/nar_santricity_host/README.md",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "roles/nar_santricity_host/defaults",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "c55bc3cf73d9c72bab4f37184f3812d79d431d18997a9607e928dacff8668027",
+ "name": "roles/nar_santricity_host/defaults/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "roles/nar_santricity_host/meta",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "424ede1f4ba7aff8937e1a4d436d662d05ccc1d17f973296b703e16cf7e073fc",
+ "name": "roles/nar_santricity_host/meta/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "roles/nar_santricity_host/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "00ea2b645906e705ae40d3bb6f1d82fee7f519b4baf316fee9d3a33221f65413",
+ "name": "roles/nar_santricity_host/tasks/initiator.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "roles/nar_santricity_host/tasks/interface",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "ea4c2d181ef7b8344b8b21a8f464b8ac978ca4a6e2a899e4526368798889ae31",
+ "name": "roles/nar_santricity_host/tasks/interface/ib_iser.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "7f163b9453592260826698591e98375aedab38330e04f93b910cb1c8a193d3d0",
+ "name": "roles/nar_santricity_host/tasks/interface/iscsi.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "d7e71acafb62950c3ce13943eeff65b0a34823a0dec954e055d827c475853e63",
+ "name": "roles/nar_santricity_host/tasks/interface/nvme_ib.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "e62cf007cb3d748244eb89488e5e22156a8911746e311c251a4b219148c0f61c",
+ "name": "roles/nar_santricity_host/tasks/interface/nvme_roce.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "eedb7789077b544b5180d5e5fb465ab548dca3164827ffbda5a808515531fded",
+ "name": "roles/nar_santricity_host/tasks/interface.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "ed6368f09a2c3824aa3ad5db37e6dc1d7bf4c9fbcf0f7778b3a4aa7109211112",
+ "name": "roles/nar_santricity_host/tasks/lun_mapping.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "167790d3f40eed4cad6a7a3811c309c3bd0373984376ee54b04b3d42e584acec",
+ "name": "roles/nar_santricity_host/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "b70a0d8b6fe6ded98c0422e722f81eecf55c4f85cf2f6273e08b90381e498b65",
+ "name": "roles/nar_santricity_host/tasks/storage_pool_absent.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "7160e4566b95a1f893499df845d4fced6bdb74fc4b6198449fdd9ed79fd1ee2c",
+ "name": "roles/nar_santricity_host/tasks/storage_pool_present.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "412af5a1e821bae12df056ca2362298170dad895177d939069785c30ad75ca55",
+ "name": "roles/nar_santricity_host/tasks/volume.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "roles/nar_santricity_management",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "43e63a93716f15cdb102c201e4631be604b45910b8c749c78d89e3fc076c7c7e",
+ "name": "roles/nar_santricity_management/.travis.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "a854a3877950d04d9bada31b1df1b275f6f690fcedce27f2db89e14f721b2a23",
+ "name": "roles/nar_santricity_management/README.md",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "roles/nar_santricity_management/defaults",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "bfd068d3b524791c78281cb610d2b984013bd84bc558210c715b9911daf7f464",
+ "name": "roles/nar_santricity_management/defaults/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "roles/nar_santricity_management/meta",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "c800933f64f7c55e3274f57cc4a36e05ace7ae085a4a0b8f35c4f2105b013090",
+ "name": "roles/nar_santricity_management/meta/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "roles/nar_santricity_management/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "4401fa2a0065e89f9d9ed8bf3456fdd86a3b084b2d8896ee2e1b38f3a58ace03",
+ "name": "roles/nar_santricity_management/tasks/firmware.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "0981be97e5e4c55a90b29dc2936cde615a73bb3a03004f55e317869c74de979c",
+ "name": "roles/nar_santricity_management/tasks/interface.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "6149cafa7e70abb1fac93608b0eb50bd3d6ccafe11f1e1921db3f4c70a2cd24a",
+ "name": "roles/nar_santricity_management/tasks/logging.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "147b9b90f2a9140c1f7956cddeacc200ba68aa6e50403c9ed83e1a636cac8b36",
+ "name": "roles/nar_santricity_management/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "263435bf4635f3d53873186bb3186d9ff62ad68aa5af881855cfff243e058d4a",
+ "name": "roles/nar_santricity_management/tasks/security.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "add396f8a4bf47c33f179c1b41038138a4d1938c4f14a5310f8f6c1fe5b7afdf",
+ "name": "roles/nar_santricity_management/tasks/system.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "0b541ccd104dd15f35a8ca62befd4a44fc202c2b73b668367e85887315fce9d9",
+ "name": "tests/integration/integration_config.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_alerts",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_alerts/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "4daa4382bae8129ae1b46aa2676864c81c1d30f7a9bf2b4bd9b97c0d560b4b1b",
+ "name": "tests/integration/targets/na_santricity_alerts/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_alerts_syslog",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_alerts_syslog/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "d3e7f2f74372dd47e02890b6992b8d1f4b3ee141941c1118c45eac7ed04e6933",
+ "name": "tests/integration/targets/na_santricity_alerts_syslog/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_asup",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_asup/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "1f99c211c88b05c103c7f066a3fcca5d1eeafe15d4c6217562e7a2bcc5649f77",
+ "name": "tests/integration/targets/na_santricity_asup/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_auditlog",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_auditlog/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "62d89c9146f8060a0b6028b88a2ad55ca3598f0757733964f6ef6f77e5761a47",
+ "name": "tests/integration/targets/na_santricity_auditlog/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_auth",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_auth/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "361e5c8d61f22ee7388fd0a4672f770f4af4dce09bfb85c2008dc0b5d4ef05c1",
+ "name": "tests/integration/targets/na_santricity_auth/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_client_certificate",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_client_certificate/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "57044ad5d4cb4a6abfb10f57fdc3fd67a9945e3487f4797ff2720e37522fa09a",
+ "name": "tests/integration/targets/na_santricity_client_certificate/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_discover",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_discover/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "b2f4f4e50f6a6e8b82faca06a24d3a84fae705951a62508053b21554c11f26d1",
+ "name": "tests/integration/targets/na_santricity_discover/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_drive_firmware",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_drive_firmware/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "b53979118cf728240e2712524191638bfda8e38f7da9e2d623d4e8c0fe6bbf47",
+ "name": "tests/integration/targets/na_santricity_drive_firmware/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_facts",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_facts/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "3877ef532f0f4c143815989ff7420414949c31c0aeda6a55a19c8c4749da1505",
+ "name": "tests/integration/targets/na_santricity_facts/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_firmware",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_firmware/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "386b07dcf7bcc6e8c5fb6c6cc507d601f64f026bd4c6f89ea5bc9e18d65cbe37",
+ "name": "tests/integration/targets/na_santricity_firmware/tasks/firmware_legacy_tests.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "6ee8117ea3a5edda386eed46132760603aebe728fb44ab587c314a15ba26c4f3",
+ "name": "tests/integration/targets/na_santricity_firmware/tasks/firmware_tests.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "51412bc402190a1e357ddac089b000508d3c656c164eed77f1ba80e3a4076bda",
+ "name": "tests/integration/targets/na_santricity_firmware/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_global",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_global/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "faf59aa803396eecb56306e273a68db4f43acb45b6e363408ce55de53598d750",
+ "name": "tests/integration/targets/na_santricity_global/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_host",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_host/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "3f9a8df527c8a215aed0f500316b2a12ccdd063ddd2dfefbb84c3530f2868f7a",
+ "name": "tests/integration/targets/na_santricity_host/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_hostgroup",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_hostgroup/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "3a067b4a6c973dd830116420ecade22b3b18a8d687dbcae3a29fe90778f6a188",
+ "name": "tests/integration/targets/na_santricity_hostgroup/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_ib_iser_interface",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_ib_iser_interface/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "916951a38b85304c670cb622ee88450903aaf75406a2bb91799f2bbb81528fc0",
+ "name": "tests/integration/targets/na_santricity_ib_iser_interface/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_iscsi_interface",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_iscsi_interface/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "4099778174fac96f2a110faf9fc78c224f4c934de9c29641f1f9c90b7b9b1ed9",
+ "name": "tests/integration/targets/na_santricity_iscsi_interface/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_iscsi_target",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_iscsi_target/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "cfe211b818a701aa7df286165e7509ee5ccddd3b6c160bd606492d4a990918fb",
+ "name": "tests/integration/targets/na_santricity_iscsi_target/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_ldap",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_ldap/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "2d0f299652f0253593b245ca409d72008d832c218f4a70fdb5a43709209acc32",
+ "name": "tests/integration/targets/na_santricity_ldap/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_lun_mapping",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_lun_mapping/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "25fb026948ec8345d9ac774346d37b00aef88250b57a9d3d2248bc6952f843a7",
+ "name": "tests/integration/targets/na_santricity_lun_mapping/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_mgmt_interface",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_mgmt_interface/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "b6d4879a8d0e422ab24b8963c98c68ad9b3b8d89d950bc11c4cf9aebfdd32b89",
+ "name": "tests/integration/targets/na_santricity_mgmt_interface/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_nvme_interface",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_nvme_interface/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "61a29d8d1156bc1eb6569eaef22560df51bfa01c963290a0d9590edc6937191e",
+ "name": "tests/integration/targets/na_santricity_nvme_interface/tasks/ib.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "32c65df83cec2f34228c0d2098df2f667f60ce14d438b3587de5f7f048abc10f",
+ "name": "tests/integration/targets/na_santricity_nvme_interface/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "741543b96775ed57e939f053d8651429a3d29fccb8bd05c1c7791a4338c58cc6",
+ "name": "tests/integration/targets/na_santricity_nvme_interface/tasks/roce.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_proxy_drive_firmware_upload",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_proxy_drive_firmware_upload/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "0f12de74d007683eb3f6b4f3309709d328b9b48eec45692c3d09a2d8ef497ac1",
+ "name": "tests/integration/targets/na_santricity_proxy_drive_firmware_upload/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_proxy_firmware_upload",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_proxy_firmware_upload/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "82681851f1dad9d2cdc816a80ea2315785152ed6d46e54cfe5d5927160118279",
+ "name": "tests/integration/targets/na_santricity_proxy_firmware_upload/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_proxy_systems",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_proxy_systems/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "ae4770030714addb929c2facc94161dea7b4a74dd702b38a0a1373e42588da9d",
+ "name": "tests/integration/targets/na_santricity_proxy_systems/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_storagepool",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_storagepool/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "cb6262188f4495ed0475276ba256d76fb59b23d3df8ff87be742207ac64f0478",
+ "name": "tests/integration/targets/na_santricity_storagepool/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_syslog",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_syslog/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "56973d5b71207809d2231bee3a57a5ba357b7fa87883a298fbc8cec20ceeaeab",
+ "name": "tests/integration/targets/na_santricity_syslog/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_volume",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/integration/targets/na_santricity_volume/tasks",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "883075039717541225bdd12fc4d5244c2c33356f1673e1eb62bba8ad6e612539",
+ "name": "tests/integration/targets/na_santricity_volume/tasks/main.yml",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/unit",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "tests/unit/modules",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "a7a2ed86b5246879519082addbb67b21ea16888139b0d90015e1a5f7a7f78882",
+ "name": "tests/unit/modules/test_na_santricity_alerts.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "579d272823f833dd0eb8b0c9fa20d67ed4845b867323a5792a3f8009a9f28087",
+ "name": "tests/unit/modules/test_na_santricity_alerts_syslog.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "a952a3ecceac3b820d4508caa42049ed48235ec84fa63f51428c824b3ba42292",
+ "name": "tests/unit/modules/test_na_santricity_asup.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "4058ede1bab1d1387be48b87c3a1b0252f6f5c7bc41150bdb523e0814692fbf7",
+ "name": "tests/unit/modules/test_na_santricity_auditlog.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "b2a9dcda0f50e5e2ba027c90c0cdf7d5742d9789d6d222cf262d51afae7b704c",
+ "name": "tests/unit/modules/test_na_santricity_auth.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "5a85f8089f8dd5bff52e86387740e1b3e83f1e06afafee94487f15eef75e8697",
+ "name": "tests/unit/modules/test_na_santricity_client_certificate.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "96e6da7639799fb31f8b13d75831735d4cf2ba0905cc8c0613c3c58a864f1c11",
+ "name": "tests/unit/modules/test_na_santricity_discover.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "67defa8018bff4056c4c5624e97574741c76a2dde57d4be4cea0343576f5468f",
+ "name": "tests/unit/modules/test_na_santricity_drive_firmware.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "2d40f697e305ca63014f8d69bd1aa914f9146d7791be81693b9e239fff61d4b1",
+ "name": "tests/unit/modules/test_na_santricity_facts.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "d8748be343eaaea095feae0de2800fffb6ffdfe7f73ba9ea18cb21fded98a265",
+ "name": "tests/unit/modules/test_na_santricity_firmware.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "7966de3e35426ecc5b1875aefe37b03d78f86839df51eb603e16a070810a30a3",
+ "name": "tests/unit/modules/test_na_santricity_global.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "0fc025ae166e4f75c8cda22914b12e89c14ae3af15c6e414aaebc31b751a56ee",
+ "name": "tests/unit/modules/test_na_santricity_host.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "482ba9550dfe4c1021ca7ea1777a87b0bc40bed7608be3a3ef44677a1a5bb2da",
+ "name": "tests/unit/modules/test_na_santricity_hostgroup.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "a1a1a00e49f08222e1188020acaa6486ee10594bc4fc6cc6959daceac159ea7a",
+ "name": "tests/unit/modules/test_na_santricity_ib_iser_interface.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "9808f5cb427e0ac02932e1d026c863847023eb88e854c53a5cb774e57c2a26fa",
+ "name": "tests/unit/modules/test_na_santricity_iscsi_interface.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "5512b6015214a44fba880d41fe6118c8ea32443e0601a979a406b66bd71505b4",
+ "name": "tests/unit/modules/test_na_santricity_iscsi_target.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "c145ca85c3a79494c64d21e422f0d0c294a4eaa8b16b2b98ea1f7054556a37f9",
+ "name": "tests/unit/modules/test_na_santricity_ldap.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "681ad2861a1e7083dcffeeae9b5e7631ecdc925bb6353c77e1002fd018da252d",
+ "name": "tests/unit/modules/test_na_santricity_lun_mapping.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "529296bf01960cc1c18eb41eb53d5e1900aa8cba5f046d22c600029b8c90392e",
+ "name": "tests/unit/modules/test_na_santricity_mgmt_interface.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "91329b895c2a014398db31e06b3adf5974a2170cd9c21f5a5d4644f74171c037",
+ "name": "tests/unit/modules/test_na_santricity_nvme_interface.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "19fd540ade11b616ac3e8b2f1d7126bbeb4efc1062174300b5dd900ec18f68b4",
+ "name": "tests/unit/modules/test_na_santricity_proxy_drive_firmware_upload.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "a501a034d03112ac421515a9898cee92c94fc424523f495317ef1f5930a7eac8",
+ "name": "tests/unit/modules/test_na_santricity_proxy_firmware_upload.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "98b029bbba2aeab8a7f589b01fe18d2875c56b7f25b5061abf627572f6bf68c6",
+ "name": "tests/unit/modules/test_na_santricity_proxy_systems.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "68752e3cbbe6490630cec79684dc9868e1474daeed6dba6e0fc61a6547381b58",
+ "name": "tests/unit/modules/test_na_santricity_storagepool.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "64f46c3b1ed4401f6d8ca6f90bbd5a6911c1a0ec7fe5405d8fa4797efcc944d6",
+ "name": "tests/unit/modules/test_na_santricity_syslog.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "16bbde2e9503cb45324f258cd0ec97d939f4473dde118b45689e5b794ecef680",
+ "name": "tests/unit/modules/test_na_santricity_volume.py",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "dir",
+ "chksum_sha256": null,
+ "name": "vars",
+ "chksum_type": null,
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "1a0f2fd90b2cc895e4c16fc58066441d43ae0fb9156a9d86ebe19175092ac722",
+ "name": "vars/hubPreCheck.groovy",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "17ff631dacfa7affc325cf3a8dee4d0251ea83e950dca43c34e30fbf0a0fba68",
+ "name": "vars/hubScan.groovy",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "06344da5db89a6ee4abad8d4ab87e4649a4c4116739006124977e9f1c90453df",
+ "name": "vars/hubScanDocker.groovy",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "2e5d3dc4ad8fbe5202c7353e18940f28648c4af9727f5e9a245a0169691581be",
+ "name": "vars/hubScanProject.groovy",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "1b0a8bbf6c402e999f533b8317c536ab1a786055f88d12493dc1cb205c243f9c",
+ "name": "vars/setupBlackduckBuildParameters.groovy",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "2e104536de237b7f1d36638b956a6dfb5c9e813cd0760856574416b4dd4b99b5",
+ "name": "vars/setupBuildParameters.groovy",
+ "chksum_type": "sha256",
+ "format": 1
+ },
+ {
+ "ftype": "file",
+ "chksum_sha256": "63170d1242b258731ab477dbdb172a6bf5c454c33398d81bc9f65a6d0d16cef5",
+ "name": "vars/setupSynopsysDetect.groovy",
+ "chksum_type": "sha256",
+ "format": 1
+ }
+ ],
+ "format": 1
+} \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/Jenkinsfile b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/Jenkinsfile
new file mode 100644
index 00000000..cf3b864c
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/Jenkinsfile
@@ -0,0 +1,40 @@
+@Library("hub")_
+setupBlackduckBuildParameters()
+
+def hubProjectName = "ansible-santricity-collection" //"esg"
+def hubProjectVersion = "1.1"
+
+
+pipeline {
+ agent {
+ label "linux-docker"
+ }
+
+ options {
+ timestamps()
+ timeout(time: 60, unit: "MINUTES")
+ }
+
+ stages {
+ stage("Hub scan") {
+ // NOTE: Using the declarative 'agent { docker image ...}' tends to run on a different node. So don't use it here.
+ steps {
+ script {
+ docker.image("docker.netapp.com/mswbuild/openjdk8:8u181-8").inside {
+ hubScanProject(
+ "${hubProjectName}",
+ "${hubProjectVersion}",
+ productionScan: true
+ )
+ }
+ }
+ }
+ }
+ }
+
+ post {
+ always {
+ deleteDir()
+ }
+ }
+}
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/MANIFEST.json b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/MANIFEST.json
new file mode 100644
index 00000000..e3cfc4e1
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/MANIFEST.json
@@ -0,0 +1,35 @@
+{
+ "collection_info": {
+ "description": null,
+ "repository": "https://www.github.com/netapp-eseries/santricity",
+ "tags": [
+ "netapp",
+ "eseries",
+ "santricity"
+ ],
+ "dependencies": {},
+ "authors": [
+ "Nathan Swartz (@ndswartz)"
+ ],
+ "issues": "https://github.com/netappeseries/santricity/issues",
+ "name": "santricity",
+ "license": [
+ "GPL-3.0-only",
+ "BSD-3-Clause"
+ ],
+ "documentation": "https://www.netapp.com/us/media/tr-4574.pdf",
+ "namespace": "netapp_eseries",
+ "version": "1.1.0",
+ "readme": "README.md",
+ "license_file": null,
+ "homepage": "https://www.github.com/netapp-eseries/santricity"
+ },
+ "file_manifest_file": {
+ "format": 1,
+ "ftype": "file",
+ "chksum_sha256": "ca2c7afeb1c3a6912dbf689a560cf94a6d2e2d7daaceacbc980c6a7cfbfce18e",
+ "name": "FILES.json",
+ "chksum_type": "sha256"
+ },
+ "format": 1
+} \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/README.md b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/README.md
new file mode 100644
index 00000000..327a2faf
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/README.md
@@ -0,0 +1,748 @@
+NetApp E-Series SANtricity Collection
+=========
+ The SANtricity collection consist of the latest available versions of the NetApp E-Series SANtricity modules and roles.
+
+ This collection provides NetApp E-Series customers with a wide range of configuration options through the collection's modules. However, the real
+ benefit of using the SANtricity collection is found in the host and management roles. These roles provide ready-made, policy-based orchestration for
+ E-Series platforms based on predefined role variables.
+ Once the physical hardware has been installed, the SANtricity roles are capable of discovering the DHCP-assigned addresses, setting initial passwords
+ and management interfaces so your automation can do full deployments for you without logging directly into the devices. Yet that's just the beginning,
+ the management role will also ensure alerts, ASUP, logging, LDAP, and firmware are configured as expected; and the host role will setup host interfaces,
+ provision and map storage, and if your servers are defined in the inventory, correctly populate E-Series hosts and host groups automatically.
+
+ Roles:
+ - nar_santricity_common: Discover NetApp E-Series storage systems and configures SANtricity Web Services Proxy.
+ - nar_santricity_host: Configure storage pools, volumes, hosts, host groups, and port interfaces.
+ - nar_santricity_management: Manage storage system's name, management interfaces, alerts, syslog, auditlog, asup, ldap, certificates, drive firmware and controller firmware.
+
+ Modules:
+ - na_santricity_alerts: Manage email alert notification settings
+ - na_santricity_alerts_syslog: Manage syslog servers receiving storage system alerts
+ - na_santricity_asup: Manage auto-support settings
+ - na_santricity_auditlog: Manage audit-log configuration
+ - na_santricity_auth: Set or update the password for a storage array
+ - na_santricity_client_certificate: Manage remote server certificates
+ - na_santricity_discover: Discover E-Series storage systems on a subnet
+ - na_santricity_drive_firmware: Manage drive firmware
+ - na_santricity_facts: Retrieve facts about NetApp E-Series storage arrays
+ - na_santricity_firmware: Manage firmware
+ - na_santricity_global: Manage global settings configuration
+ - na_santricity_host: Manage eseries hosts
+ - na_santricity_hostgroup: Manage array host groups
+ - na_santricity_iscsi_interface: Manage iSCSI interface configuration
+ - na_santricity_iscsi_target: Manage iSCSI target configuration
+ - na_santricity_ldap: Manage LDAP integration to use for authentication
+ - na_santricity_lun_mapping: Manage lun mappings
+ - na_santricity_mgmt_interface: Manage management interface configuration
+ - na_santricity_storage_system: Manage SANtricity web services proxy storage arrays
+ - na_santricity_storagepool: Manage volume groups and disk pools
+ - na_santricity_syslog: Manage syslog settings
+ - na_santricity_volume: Manage storage volumes
+
+ Deprecated Modules:
+ - netapp_e_alerts: Manage email notification settings
+ - netapp_e_amg: Create, remove, and update asynchronous mirror groups
+ - netapp_e_amg_role: Update the role of a storage array within an Asynchronous Mirror Group (AMG)
+ - netapp_e_amg_sync: Conduct synchronization actions on asynchronous mirror groups
+ - netapp_e_asup: Manage auto-support settings
+ - netapp_e_auditlog: Manage audit-log configuration
+ - netapp_e_auth: Set or update the password for a storage array
+ - netapp_e_drive_firmware: Manage drive firmware
+ - netapp_e_facts: Retrieve facts about NetApp E-Series storage arrays
+ - netapp_e_firmware: Manage firmware
+ - netapp_e_flashcache: Manage SSD caches
+ - netapp_e_global: Manage global settings configuration
+ - netapp_e_hostgroup: Manage eseries hosts
+ - netapp_e_host: Manage array host groups
+ - netapp_e_iscsi_interface: Manage iSCSI interface configuration
+ - netapp_e_iscsi_target: Manage iSCSI target configuration
+ - netapp_e_ldap: Manage LDAP integration to use for authentication
+ - netapp_e_lun_mapping: Create, delete, or modify lun mappings
+ - netapp_e_mgmt_interface: Manage management interface configuration
+ - netapp_e_snapshot_group: Manage snapshot groups
+ - netapp_e_snapshot_images: Create and delete snapshot images
+ - netapp_e_snapshot_volume: Manage snapshot volumes
+ - netapp_e_storagepool: Manage volume groups and disk pools
+ - netapp_e_storage_system: Manage Web Services Proxy manage storage arrays
+ - netapp_e_syslog: Manage syslog settings
+ - netapp_e_volume_copy: Create volume copy pairs
+ - netapp_e_volume: Manage storage volumes (standard and thin)
+
+Requirements
+------------
+ - Ansible 2.9 or later
+ - NetApp E-Series E2800 platform or newer or NetApp E-Series SANtricity Web Services Proxy configured for older E-Series Storage arrays.
+
+Example Playbook
+----------------
+ - hosts: eseries_storage_systems
+ gather_facts: false
+ collections:
+ - netapp_eseries.santricity
+ tasks:
+ - name: Ensure proxy has been configured and storage systems have been discovered.
+ import_role:
+ name: nar_santricity_common
+ - name: Ensure all management related policies are enforced.
+ import_role:
+ name: nar_santricity_management
+ - name: Ensure all host related policies are enforced.
+ import_role:
+ name: nar_santricity_host
+
+Example Storage System Inventory File (Simple example without storage system discovery)
+-------------------------------------
+ eseries_system_api_url: https://192.168.1.200:8443/devmgr/v2/
+ eseries_system_password: admin_password
+ eseries_validate_certs: false
+
+ eseries_system_name: my_eseries_array
+
+ eseries_management_interfaces:
+ config_method: static
+ subnet_mask: 255.255.255.0
+ gateway: 192.168.1.1
+ dns_config_method: static
+ dns_address: 192.168.1.253
+ ntp_config_method: static
+ ntp_address: 192.168.1.200
+ controller_a:
+ - address: 192.168.1.100
+ - address: 192.168.1.101
+ controller_b:
+ - address: 192.168.1.102
+ - address: 192.168.1.103
+
+ eseries_initiator_protocol: fc
+
+ # Storage pool and volume configuration
+ eseries_storage_pool_configuration:
+ - name: vg_01
+ raid_level: raid6
+ criteria_drive_count: 10
+ volumes:
+ - name: metadata
+ host: servers
+ size: 4096
+ - name: vg_02
+ raid_level: raid5
+ criteria_drive_count: 10
+ volumes:
+ - name: storage_[1-3]
+ host: servers
+ size: 2
+ size_unit: tb
+ volume_metadata: # Used by netapp_eseries.host.mount role to format and mount volumes
+ format_type: xfs
+ mount_options1: "noatime,nodiratime,logbufs=8,logbsize=256k,largeio"
+ mount_options2: "inode64,swalloc,allocsize=131072k,nobarrier,_netdev"
+ mount_directory: "/data/beegfs/"
+
+Example Storage System Inventory File (Discover storage system)
+-------------------------------------
+**Note that this discovery method works for SANtricity versions 11.62 or later, otherwise it will only discover the systems with unset passwords.**
+
+ eseries_system_serial: "012345678901" # Be sure to quote if the serial is all numbers and begins with zero.
+ eseries_system_password: admin_password
+ eseries_subnet: 192.168.1.0/24
+
+ eseries_management_interfaces: # (Optional) Specifying static management interfaces can be used not only to discover the storage system but also to contact when valid.
+ config_method: static
+ controller_a:
+ - address: 192.168.1.100
+ controller_b:
+ - address: 192.168.1.101
+
+ (...) # Same as the previous examples starting with the eseries_validate_certs line
+
+Example Storage System Inventory File (Discover storage system with proxy)
+-------------------------------------
+ eseries_system_serial: "012345678901" # Be sure to quote if the serial is all numbers and begins with zero.
+ eseries_system_password: admin_password
+
+ eseries_proxy_api_url: https://192.168.1.100:8443/devmgr/v2/
+ eseries_proxy_api_password: admin_password
+ eseries_subnet: 192.168.1.0/24
+ eseries_prefer_embedded: false # Overrides the default behavior of using Web Services Proxy when eseries_proxy_api_url is defined. This will only effect
+ # storage systems that have Embedded Web Services.
+
+ (...) # Same as the previous examples starting with the eseries_validate_certs line
+
+Example Storage System Inventory File
+-------------------------------------
+ eseries_system_api_url: https://192.168.1.200:8443/devmgr/v2/
+ eseries_system_password: admin_password
+ eseries_validate_certs: false
+
+ eseries_system_name: my_eseries_array
+ eseries_system_cache_block_size: 128
+ eseries_system_cache_flush_threshold: 90
+ eseries_system_autoload_balance: enabled
+ eseries_system_host_connectivity_reporting: enabled
+ eseries_system_default_host_type: Linux DM-MP
+
+ eseries_management_interfaces:
+ config_method: static
+ subnet_mask: 255.255.255.0
+ gateway: 192.168.1.1
+ dns_config_method: static
+ dns_address: 192.168.1.253
+ dns_address_backup: 192.168.1.254
+ ssh: true
+ ntp_config_method: static
+ ntp_address: 192.168.1.200
+ ntp_address_backup: 192.168.1.201
+ controller_a:
+ - address: 192.168.1.100
+ - address: 192.168.1.101
+ controller_b:
+ - address: 192.168.1.102
+ - address: 192.168.1.103
+
+ eseries_ldap_state: present
+ eseries_ldap_bind_username:
+ eseries_ldap_bind_password:
+ eseries_ldap_server:
+ eseries_ldap_search_base:
+ eseries_ldap_role_mappings:
+ ".*":
+ - storage.admin
+ - storage.monitor
+ - support.admin
+ - security.admin
+
+ eseries_client_certificate_certificates:
+ - /path/to/client_certificate.crt
+
+ eseries_firmware_firmware: "/path/to/firmware.dlp"
+ eseries_firmware_nvsram: "/path/to/nvsram.dlp"
+ eseries_drive_firmware_firmware_list:
+ - "/path/to/drive_firmware.dlp"
+
+ eseries_asup_state: enabled
+ eseries_asup_active: true
+ eseries_asup_days: [sunday, saturday]
+ eseries_asup_start: 17
+ eseries_asup_end: 24
+ eseries_asup_validate: false
+ eseries_asup_method: email
+ eseries_asup_email:
+ server: smtp.example.com
+ sender: noreply@example.com
+
+ eseries_syslog_state: present
+ eseries_syslog_address: 192.168.1.150
+ eseries_syslog_protocol: udp
+ eseries_syslog_port: 514
+ eseries_alert_syslog_servers:
+ - "address": 192.168.1.150
+ "port": 514
+ eseries_initiator_protocol: iscsi
+
+ # Controller port definitions
+ eseries_controller_iscsi_port_config_method: static
+ eseries_controller_iscsi_port_subnet_mask: 255.255.255.0
+ eseries_controller_iscsi_port:
+ controller_a:
+ - address: 192.168.2.100
+ - address: 192.168.2.110
+ controller_b:
+ - address: 192.168.3.100
+ - address: 192.168.3.110
+
+ # Storage pool and volume configuration
+ eseries_storage_pool_configuration:
+ - name: pool[1-2]
+ raid_level: raid6
+ criteria_drive_count: 10
+ volumes:
+ - name: "[pool]_volume[A-C]"
+ host: server_group
+ size: 10
+ size_unit: tb
+
+Collection Variables
+--------------
+**Note that when values are specified below, they indicate the default value.**
+
+ eseries_subnet: # Network subnet to search for the storage system specified in CIDR form. Example: 192.168.1.0/24
+ eseries_template_api_url: # Template for the web services api url. Default: https://0.0.0.0:8443/devmgr/v2/
+ eseries_prefer_embedded: false # Overrides the default behavior of using Web Services Proxy when eseries_proxy_api_url is defined. This will only effect
+ # storage systems that have Embedded Web Services.
+ eseries_validate_certs: true # Indicates Whether SSL certificates should be verified. Used for both embedded and proxy. Choices: true, false
+
+ # Storage system specific variables
+ eseries_proxy_ssid: # Arbitrary string for the proxy to represent the storage system. eseries_system_serial will be used when not defined.
+ eseries_system_serial: # Storage system serial number. Be sure to quote if the serial is all numbers and begins with zero. (This is located on a
+ # label at the top-left towards the front on the device)
+ eseries_system_addresses: # Storage system management IP addresses. Only required when eseries_system_serial or eseries_system_api_url are not
+ # defined. When not specified, addresses will be populated with eseries_management_interfaces controller addresses.
+ eseries_system_api_url: # Url for the storage system's for embedded web services rest api. Example: https://192.168.10.100/devmgr/v2
+ eseries_system_username: admin # Username for the storage system's for embedded web services rest api
+ eseries_system_password: # Password for the storage system's for embedded web services rest api and when the admin password has not been set
+ # eseries_system_password will be used to set it.
+ eseries_system_tags: # Meta tags to associate with storage system when added to the proxy.
+
+ # Web Services Proxy specific variable
+ Note: eseries_proxy_* variables are required to discover storage systems prior to SANtricity OS version 11.60.2.
+ eseries_proxy_api_url: # Url for the storage system's for proxy web services rest api. Example: https://192.168.10.100/devmgr/v2
+ eseries_proxy_api_username: admin # Username for the storage system's for proxy web services rest api.
+ eseries_proxy_api_password: # Password for the storage system's for proxy web services rest api and when the admin password has not been set
+ # eseries_proxy_api_password will be used to set it.
+ eseries_proxy_monitor_password: # Proxy password for the monitor username
+ eseries_proxy_security_password: # Proxy password for the security username
+ eseries_proxy_storage_password: # Proxy password for the monitor username
+ eseries_proxy_support_password: # Proxy password for the support username
+ eseries_proxy_accept_certifications: # Force automatic acceptance of all storage system's certificate
+ eseries_proxy_default_system_tags: # Default meta tags to associate with all storage systems
+ eseries_proxy_default_password: # Default password to associate with all storage systems. This is overridden by eseries_system_password.
+
+ # LDAP configuration defaults
+ eseries_proxy_ldap_state: # Whether LDAP should be configured for the proxy`
+ eseries_proxy_ldap_identifier: # The user attributes that should be considered for the group to role mapping
+ eseries_proxy_ldap_user_attribute: # Attribute used to the provided username during authentication.
+ eseries_proxy_ldap_bind_username: # User account that will be used for querying the LDAP server.
+ eseries_proxy_ldap_bind_password: # Password for the bind user account
+ eseries_proxy_ldap_server: # LDAP server URL.
+ eseries_proxy_ldap_search_base: # Search base used for find user's group membership
+ eseries_proxy_ldap_role_mappings: # Dictionary of user groups, each containing the list of access roles.
+ # Role choices: storage.admin - allows users full read/writes access to storage objects and operations.
+ # storage.monitor - allows users read-only access to storage objects and operations.
+ # storage.admin - allows users access to hardware, diagnostic information, major event logs,
+ # and other critical support-related functionality, but not the sorage configuration.
+ # security.admin - allows users access to authentication/authorization configuration, as
+ # well as the audit log configuration, adn certification management.
+
+ # Global storage system information
+ eseries_system_name: # Name of the storage system.
+ eseries_system_cache_block_size: # Cache block size
+ eseries_system_cache_flush_threshold: # Unwritten data will be flushed when exceeds this threshold
+ eseries_system_autoload_balance: # Whether automatic load balancing should be enabled. Choices: enabled, disabled
+ eseries_system_host_connectivity_reporting: # Whether host connectivity reporting should be enabled. Choices: enabled, disabled
+ eseries_system_default_host_type: # Only required when using something other than Linux kernel 3.10 or later with DM-MP (Linux DM-MP),
+ # non-clustered Windows (Windows), or the storage system default host type is incorrect.
+ # Common host type definitions:
+ # - AIX MPIO: The Advanced Interactive Executive (AIX) OS and the native MPIO driver
+ # - AVT 4M: Silicon Graphics, Inc. (SGI) proprietary multipath driver
+ # - HP-UX: The HP-UX OS with native multipath driver
+ # - Linux ATTO: The Linux OS and the ATTO Technology, Inc. driver (must use ATTO FC HBAs)
+ # - Linux DM-MP: The Linux OS and the native DM-MP driver
+ # - Linux Pathmanager: The Linux OS and the SGI proprietary multipath drive
+ # - Mac: The Mac OS and the ATTO Technology, Inc. driver
+ # - ONTAP: FlexArray
+ # - Solaris 11 or later: The Solaris 11 or later OS and the native MPxIO driver
+ # - Solaris 10 or earlier: The Solaris 10 or earlier OS and the native MPxIO driver
+ # - SVC: IBM SAN Volume Controller
+ # - VMware: ESXi OS
+ # - Windows: Windows Server OS and Windows MPIO with a DSM driver
+ # - Windows Clustered: Clustered Windows Server OS and Windows MPIO with a DSM driver
+ # - Windows ATTO: Windows OS and the ATTO Technology, Inc. driver
+
+ # Role-based username passwords
+ eseries_system_monitor_password: # Storage system monitor username password
+ eseries_system_security_password: # Storage system security username password
+ eseries_system_storage_password: # Storage system storage username password
+ eseries_system_support_password: # Storage system support username password
+
+ # Storage management interface defaults
+ Note: eseries_management_* variables have the lowest priority and will be overwritten by those found in eseries_management_interfaces
+ eseries_management_config_method: # Default config method for all management interfaces. Choices: static, dhcp
+ eseries_management_subnet_mask: # Default subnet mask for all management interfaces
+ eseries_management_gateway_mask: # Default gateway for all management interfaces
+ eseries_management_dns_config_method: # Default DNS config method for all management interfaces
+ eseries_management_dns_address: # Default primary DNS address for all management interfaces
+ eseries_management_dns_address_backup: # Default backup DNS address for all management interfaces
+ eseries_management_ntp_config_method: # Default NTP config method for all management interfaces
+ eseries_management_ntp_address: # Default primary NTP address for all management interfaces
+ eseries_management_ntp_address_backup: # Default backup NTP address for all management interfaces
+ eseries_management_ssh: # Default SSH access for all management interfaces. Choices: true, false
+ eseries_management_interfaces:
+ config_method: # Config method for all management interfaces. Choices: static, dhcp
+ subnet_mask: # Subnet mask for all management interfaces
+ gateway_mask: # Gateway for all management interfaces
+ dns_config_method: # DNS config method for all management interfaces
+ dns_address: # Primary DNS address for all management interfaces
+ dns_address_backup: # Backup DNS address for all management interfaces
+ ntp_config_method: # NTP config method for all management interfaces
+ ntp_address: # Primary NTP address for all management interfaces
+ ntp_address_backup: # Backup NTP address for all management interfaces
+ ssh: # SSH access for all management interfaces. Choices: true, false
+ controller_a: # List of controller A ports
+ - address: # IPv4 address for controller A
+ config_method: # Config method for controller A. Choices: static, dhcp
+ subnet_mask: # Subnet mask for controller A
+ gateway: # Gateway for controller A
+ dns_config_method: # DNS config method for controller A
+ dns_address: # Primary DNS address for controller A
+ dns_address_backup: # Backup DNS address for controller A
+ ntp_config_method: # NTP config method for controller A
+ ntp_address: # Primary NTP address for controller A
+ ntp_address_backup: # Backup NTP address for controller A
+ ssh: # SSH access for controller A. Choices: true, false
+ controller_b: # List of controller B ports
+ - (...) # Same as for controller A but for controller B.
+
+ # Alerts configuration defaults
+ eseries_alerts_state: # Whether to enable storage system alerts. Choices: enabled, disabled
+ eseries_alerts_contact: # This allows owner to specify free-form contact information such as email or phone number.
+ eseries_alerts_recipients: # List containing e-mails that should be sent notifications when alerts are issued.
+ eseries_alerts_sender: # Sender email. This does not necessarily need to be a valid e-mail.
+ eseries_alerts_server: # Fully qualified domain name, IPv4 address, or IPv6 address of the mail server.
+ eseries_alerts_test: false # When changes are made to the storage system alert configuration a test e-mail will be sent. Choices: true, false
+ eseries_alert_syslog_servers: # List of dictionaries where each dictionary contains a syslog server entry. [{"address": <address>, "port": <port>}]
+ eseries_alert_syslog_test: false # Whether alerts syslog servers configuration test message should be sent. Choices: true, false
+
+ # LDAP configuration defaults
+ eseries_ldap_state: # Whether LDAP should be configured
+ eseries_ldap_identifier: # The user attributes that should be considered for the group to role mapping
+ eseries_ldap_user_attribute: # Attribute used to the provided username during authentication.
+ eseries_ldap_bind_username: # User account that will be used for querying the LDAP server.
+ eseries_ldap_bind_password: # Password for the bind user account
+ eseries_ldap_server: # LDAP server URL.
+ eseries_ldap_search_base: # Search base used for find user's group membership
+ eseries_ldap_role_mappings: # Dictionary of user groups, each containing the list of access roles.
+ # Role choices: storage.admin - allows users full read/writes access to storage objects and operations.
+ # storage.monitor - allows users read-only access to storage objects and operations.
+ # storage.admin - allows users access to hardware, diagnostic information, major event logs,
+ # and other critical support-related functionality, but not the sorage configuration.
+ # security.admin - allows users access to authentication/authorization configuration, as
+ # well as the audit log configuration, adn certification management.
+
+ # Drive firmware defaults
+ eseries_drive_firmware_firmware_list: # Local path list for drive firmware.
+ eseries_drive_firmware_wait_for_completion: # Forces drive firmware upgrades to wait for all associated tasks to complete. Choices: true, false
+ eseries_drive_firmware_ignore_inaccessible_drives: # Forces drive firmware upgrades to ignore any inaccessible drives. Choices: true, false
+ eseries_drive_firmware_upgrade_drives_online: # Forces drive firmware upgrades to be performed while I/Os are accepted. Choices: true, false
+
+ # Controller firmware defaults
+ eseries_firmware_nvsram: # Local path for NVSRAM file.
+ eseries_firmware_firmware: # Local path for controller firmware file.
+ eseries_firmware_wait_for_completion: # Forces controller firmware upgrade to wait until upgrade has completed before continuing. Choices: true, false
+ eseries_firmware_clear_mel_events: # Forces firmware upgrade to be attempted regardless of the health check results. Choices: true, false
+
+ # Auto-Support configuration defaults
+ eseries_asup_state: # Whether auto support (ASUP) should be enabled. Choices: enabled, disabled
+ eseries_asup_active: # Allows NetApp support personnel to request support data to resolve issues. Choices: true, false
+ eseries_asup_days: # List of days of the week. Choices: monday, tuesday, wednesday, thursday, friday, saturday, sunday
+ eseries_asup_start: # Hour of the day(s) to start ASUP bundle transmissions. Start time must be less than end time. Choices: 0-23
+ eseries_asup_end: # Hour of the day(s) to end ASUP bundle transmissions. Start time must be less than end time. Choices: 1-24
+ eseries_asup_method: # ASUP delivery method. Choices https, http, email (default: https)
+ eseries_asup_routing_type: # ASUP delivery routing type for https or http. Choices: direct, proxy, script (default: direct)
+ eseries_asup_proxy: # ASUP proxy delivery method information.
+ host: # ASUP proxy host IP address or FQDN. When eseries_asup_routing_type==proxy this must be specified.
+ port: # ASUP proxy host port. When eseries_asup_routing_type==proxy this must be specified.
+ script: # ASUP proxy host script.
+ eseries_asup_email: # ASUP email delivery configuration information
+ server: # ASUP email server
+ sender: # ASUP email sender
+ test_recipient: # ASUP configuration mail test recipient
+ eseries_maintenance_duration: # Duration in hours (1-72) the ASUP maintenance mode will be active
+ eseries_maintenance_emails: # List of email addresses for maintenance notifications
+ eseries_asup_validate: # Verify ASUP configuration prior to applying changes
+
+ # Audit-log configuration defaults
+ eseries_auditlog_enforce_policy: # Whether to make audit-log policy changes. Choices: true, false
+ eseries_auditlog_force: # Forces audit-log to delete log messages when fullness threshold has been exceeded. Choices: true, false
+ eseries_auditlog_full_policy: # Policy for what to do when record limit has been reached. Choices: overWrite, preventSystemAccess
+ eseries_auditlog_log_level: # Filters logs based on the specified level. Choices: all, writeOnly
+ eseries_auditlog_max_records: # Maximum number of audit-log messages retained. Choices: 100-50000.
+ eseries_auditlog_threshold: # Memory full percentage threshold that audit-log will start issuing warning messages. Choices: 60-90
+
+ # Syslog configuration defaults
+ eseries_syslog_state: # Whether syslog servers should be added or removed from storage system. Choices: present, absent
+ eseries_syslog_address: # Syslog server IPv4 address or fully qualified hostname.
+ eseries_syslog_test: # Whether a test messages should be sent to syslog server when added to the storage system. Choices: true, false
+ eseries_syslog_protocol: # Protocol to be used when transmitting log messages to syslog server. Choices: udp, tc, tls
+ eseries_syslog_port: # Port to be used when transmitting log messages to syslog server.
+ eseries_syslog_components: # List of components log to syslog server. Choices: auditLog, (others may become available)
+
+ # iSCSI target discovery specifications
+ Note: add the following to ansible-playbook command to update the chap secret: --extra-vars "eseries_target_chap_secret_update=True
+ eseries_iscsi_target_name: # iSCSI target name that will be seen by the initiator
+ eseries_iscsi_target_ping: True # Enables ICMP ping response from the configured iSCSI ports (boolean)
+ eseries_iscsi_target_unnamed_discovery: True # Whether the iSCSI target iqn should be returned when an initiator performs a discovery session.
+ eseries_iscsi_target_chap_secret: # iSCSI chap secret. When left blank, the chap secret will be removed from the storage system.
+ eseries_iscsi_target_chap_secret_update: False # DO NOT REMOVE! Since na_santricity_iscsi_target cannot compare the chap secret with the current and
+ # will always return changed=True, this flag is used to force the module to update the chap secret.
+ # Leave this value False and to add the --extra-vars "eseries_target_chap_secret_update=True".
+
+ # Controller iSCSI Interface Port Default Policy Specifications
+ eseries_controller_iscsi_port_state: enabled # Generally specifies whether a controller port definition should be applied Choices: enabled, disabled
+ eseries_controller_iscsi_port_config_method: dhcp # General port configuration method definition for both controllers. Choices: static, dhcp
+ eseries_controller_iscsi_port_gateway: # General port IPv4 gateway for both controllers.
+ eseries_controller_iscsi_port_subnet_mask: # General port IPv4 subnet mask for both controllers.
+ eseries_controller_iscsi_port_mtu: 9000 # General port maximum transfer units (MTU) for both controllers. Any value greater than 1500 (bytes).
+ eseries_controller_iscsi_port:
+ controller_a: # Controller A port definition. Ordered list of port definitions reading iSCSI ports left to right
+ - state: # Whether the port should be enabled. Choices: enabled, disabled
+ config_method: # Port configuration method Choices: static, dhcp
+ address: # Port IPv4 address
+ gateway: # Port IPv4 gateway
+ subnet_mask: # Port IPv4 subnet_mask
+ mtu: # Port IPv4 mtu
+ controller_b: # Controller B port definition.
+ - (...) # Same as controller A but for controller B
+
+ # Controller InfiniBand iSER Interface Channel
+ eseries_controller_ib_iser_port:
+ controller_a: # Ordered list of controller A channel address definition.
+ - # Port IPv4 address for channel 1
+ controller_b: # Ordered list of controller B channel address definition.
+ - (...) # Same as controller A but for controller B
+
+ # Controller NVMe over InfiniBand Interface Channel
+ eseries_controller_nvme_ib_port:
+ controller_a: # Ordered list of controller A channel address definition.
+ - # Port IPv4 address for channel 1
+ controller_b: # Ordered list of controller B channel address definition.
+ - (...) # Same as controller A but for controller B
+
+ # Controller NVMe RoCE Interface Port Default Policy Specifications
+ eseries_controller_nvme_roce_port_state: enabled # Specifies whether a controller port definition should be applied. Choices: enabled, disabled
+ eseries_controller_nvme_roce_port_config_method: dhcp # Port configuration method definition for both controllers. Choices: static, dhcp
+ eseries_controller_nvme_roce_port_gateway: # Port IPv4 gateway for both controllers.
+ eseries_controller_nvme_roce_port_subnet_mask: # Port IPv4 subnet mask for both controllers.
+ eseries_controller_nvme_roce_port_mtu: 9000 # Port maximum transfer units (MTU). Any value greater than 1500 (bytes).
+ eseries_controller_nvme_roce_port_speed: auto # Interface speed. Value must be a supported speed or auto to negotiate the speed with the port.
+ eseries_controller_nvme_roce_port:
+ controller_a: # Controller A port definition. List containing ports definitions.
+ - channel: # Channel of the port to modify. This will be a numerical value that represents the port; typically read
+ # left to right on the HIC.
+ state: # Whether the port should be enabled.
+ config_method: # Port configuration method Choices: static, dhcp
+ address: # Port IPv4 address
+ gateway: # Port IPv4 gateway
+ subnet_mask: # Port IPv4 subnet_mask
+ mtu: # Port IPv4 mtu
+ speed: # Port IPv4 speed
+ controller_b: # Controller B port definition.
+ - (...) # Same as controller A but for controller B
+
+ # Storage Pool Default Policy Specifications
+ eseries_storage_pool_state: present # Default storage pool state. Choices: present, absent
+ eseries_storage_pool_raid_level: raidDiskPool # Default volume raid level. Choices: raid0, raid1, raid5, raid6, raidDiskPool
+ eseries_storage_pool_secure_pool: false # Default for storage pool drive security. This flag will enable the security at rest feature. There
+ # must be sufficient FDE or FIPS security capable drives. Choices: true, false
+ eseries_storage_pool_criteria_drive_count: # Default storage pool drive count.
+ eseries_storage_pool_reserve_drive_count: # Default reserve drive count for drive reconstruction for storage pools using dynamic disk pool and
+ # the raid level must be set for raidDiskPool.
+ eseries_storage_pool_criteria_min_usable_capacity: # Default minimum required capacity for storage pools.
+ eseries_storage_pool_criteria_drive_type: # Default drive type for storage pools. Choices: hdd, ssd
+ eseries_storage_pool_criteria_size_unit: gb # Default unit size for all storage pool related sizing.
+ # Choices: bytes, b, kb, mb, gb, tb, pb, eb, zb, yb
+ eseries_storage_pool_criteria_drive_min_size: # Default minimum drive size for storage pools.
+ eseries_storage_pool_criteria_drive_require_da: # Default for whether storage pools are required to have data assurance (DA) compatible drives.
+ # Choices: true, false
+ eseries_storage_pool_criteria_drive_require_fde: # Default for whether storage pools are required to have drive security compatible drives.
+ # Choices: true, false
+ eseries_storage_pool_remove_volumes: # Default policy for deleting volumes prior to removing storage pools.
+ eseries_storage_pool_erase_secured_drives: # Default policy for erasing the content drives during create and delete storage pool operations.
+ # Choices: true, false
+
+ # Volume Default Policy Specifications
+ eseries_volume_state: present # Default volume state. Choices: present, absent
+ eseries_volume_size_unit: gb # Default unit size for all volume sizing options.
+ eseries_volume_size: # Default volume size or the presented size for thinly provisioned volumes.
+ eseries_volume_data_assurance_enabled: # Default for whether data assurance(DA) is required to be enabled.
+ eseries_volume_segment_size_kb: # Default segment size measured in kib.
+ eseries_volume_read_cache_enable: # Default for read caching which will cache all read requests.
+ eseries_volume_read_ahead_enable: # Default for read ahead caching; this is good for sequential workloads to cache subsequent blocks.
+ eseries_volume_write_cache_enable: # Default for write caching which will cache all writes.
+ eseries_volume_cache_without_batteries: # Default for allowing caching when batteries are not present.
+ eseries_volume_thin_provision: # Default for whether volumes should be thinly provisioned.
+ eseries_volume_thin_volume_repo_size: # Default for actually allocated space for thinly provisioned volumes.
+ eseries_volume_thin_volume_max_repo_size: # Default for the maximum allocated space allowed for thinly provisioned volumes.
+ eseries_volume_thin_volume_expansion_policy: # Default thin volume expansion policy. Choices: automatic, manual
+ eseries_volume_thin_volume_growth_alert_threshold: # Default thin volume growth alert threshold; this is the threshold for when the thin volume expansion
+ # policy will be enacted. Allowable values are between and including 10% and 99%
+ eseries_volume_ssd_cache_enabled: # Default for ssd cache which will enable the volume to use an existing SSD cache on the storage array
+ eseries_volume_host: # Default host for all volumes; the value can be any host from the Ansible inventory.
+ eseries_volume_workload_name: # Default workload tag name
+ eseries_volume_workload_metadata: # Default workload metadata
+ eseries_volume_volume_metadata: # Default volume_metadata
+ eseries_volume_owning_controller # Default preferred owning controller
+ eseries_volume_wait_for_initialization: false # Default for whether volume creation with wait for initialization to complete
+
+ # Storage Pool-Volume Mapping Default Policy Specifications
+ # ---------------------------------------------------------
+ eseries_lun_mapping_state: present # Generally specifies whether a LUN mapping should be present. This is useful when adding a default host for all
+ # volumes. Choices: present, absent
+ eseries_lun_mapping_host: # Default host for all volumes not specifically give a host either in common_volume_configuration or in
+ # eseries_storage_pool_configuration.
+
+ # Storage Pool-Volume Default Policy Specifications
+ Name schemes: Storage pool and volume names can be used to specify a naming scheme to produce a list of storage pools and volumes. Schemes are defined by
+ brackets and can be used to specify a range of lowercase letters, uppercase letters, range of single digit numbers, any top-level inventory
+ variables, and [pool] to use the current defined storage pool (volume only).
+ eseries_storage_pool_configuration:
+ - name: # Name or name scheme (see above) for the storage pool.
+ state: # Specifies whether the storage pool should exist. Choices: present, absent
+ raid_level # Volume group raid level. Choices: raid0, raid1, raid5, raid6, raidDiskPool (Default: raidDiskPool)
+ secure_pool: # Default for storage pool drive security. This flag will enable the security at rest feature. There must be
+ # sufficient FDE or FIPS security capable drives. Choices: true, false
+ criteria_drive_count: # Default storage pool drive count.
+ reserve_drive_count: # Default reserve drive count for drive reconstruction for storage pools using dynamic disk pool and the raid
+ # level must be set for raidDiskPool.
+ criteria_min_usable_capacity: # Default minimum required capacity for storage pools.
+ criteria_drive_type: # Default drive type for storage pools. Choices: hdd, ssd
+ criteria_size_unit: # Default unit size for all storage pool related sizing. Choices: bytes, b, kb, mb, gb, tb, pb, eb, zb, yb
+ criteria_drive_min_size: # Default minimum drive size for storage pools.
+ criteria_drive_require_da: # Ensures storage pools have data assurance (DA) compatible drives. Choices: true, false
+ criteria_drive_require_fde: # Ensures storage pools have drive security compatible drives. Choices: true, false
+ remove_volumes: # Ensures volumes are deleted prior to removing storage pools.
+ erase_secured_drives: # Ensures data is erased during create and delete storage pool operations. Choices: true, false
+ common_volume_configuration: # Any option that can be specified at the volume level can be generalized here at the storage pool level.
+ volumes: # List of volumes associated the storage pool.
+ - state: # Specifies whether the volume should exist (present, absent)
+ name: # (required) Name or name scheme (see above) for the volume(s) to be created in the storage pool(s)
+ host: # host or host group for the volume should be mapped to.
+ host_type: # Only required when using something other than Linux kernel 3.10 or later with DM-MP (Linux DM-MP),
+ # non-clustered Windows (Windows), or the storage system default host type is incorrect.
+ # Common host type definitions:
+ # - AIX MPIO: The Advanced Interactive Executive (AIX) OS and the native MPIO driver
+ # - AVT 4M: Silicon Graphics, Inc. (SGI) proprietary multipath driver
+ # - HP-UX: The HP-UX OS with native multipath driver
+ # - Linux ATTO: The Linux OS and the ATTO Technology, Inc. driver (must use ATTO FC HBAs)
+ # - Linux DM-MP: The Linux OS and the native DM-MP driver
+ # - Linux Pathmanager: The Linux OS and the SGI proprietary multipath driver
+ # - Mac: The Mac OS and the ATTO Technology, Inc. driver
+ # - ONTAP: FlexArray
+ # - Solaris 11 or later: The Solaris 11 or later OS and the native MPxIO driver
+ # - Solaris 10 or earlier: The Solaris 10 or earlier OS and the native MPxIO driver
+ # - SVC: IBM SAN Volume Controller
+ # - VMware: ESXi OS
+ # - Windows: Windows Server OS and Windows MPIO with a DSM driver
+ # - Windows Clustered: Clustered Windows Server OS and Windows MPIO with a DSM driver
+ # - Windows ATTO: Windows OS and the ATTO Technology, Inc. driver
+ owning_controller: # Specifies which controller will be the primary owner of the volume. Not specifying will allow the
+ # controller to choose ownership. (Choices: A, B)
+ read_ahead_enable: # Enables read ahead caching; this is good for sequential workloads to cache subsequent blocks.
+ read_cache_enable: # Enables read caching which will cache all read requests.
+ size: # Size of the volume or presented size of the thinly provisioned volume.
+ size_unit: # Unit size for the size, thin_volume_repo_size, and thin_volume_max_repo_size
+ # Choices: bytes, b, kb, mb, gb, tb, pb, eb, zb, yb
+ segment_size_kb: # Indicates the amount of data stored on a drive before moving on to the next drive in the volume group.
+ ssd_cache_enabled: # Enables ssd cache which will enable the volume to use an existing SSD cache on the storage array.
+ thin_provision: # Whether volumes should be thinly provisioned.
+ thin_volume_repo_size: # Actually allocated space for thinly provisioned volumes.
+ thin_volume_max_repo_size: # Maximum allocated space allowed for thinly provisioned volumes.
+ thin_volume_expansion_policy: # Thin volume expansion policy. Choices: automatic, manual
+ thin_volume_growth_alert_threshold: # Thin volume growth alert threshold; this is the threshold for when the thin volume expansion
+ # policy will be enacted. Allowable values are between and including 10% and 99%
+ data_assurance_enabled: # Enables whether data assurance(DA) is required to be enabled.
+ wait_for_initialization: # Whether volume creation with wait for initialization to complete
+ workload_name: # Name of the volume's workload
+ workload_metadata: # Dictionary containing arbitrary entries normally used for defining the volume(s) workload.
+ volume_metadata # Dictionary containing arbitrary entries used to define information about the volume itself.
+ # Note: mount_to_host, format_type, format_options, mount_directory, mount_options are used by netapp_eseries.host.mount role to format and mount volumes.
+ write_cache_enable: # Enables write caching which will cache all writes.
+ # created on the storage array.
+
+ # Initiator-Target Protocol Variable Defaults
+ Note that the following commands need to produce a unique list of IQNs or WWNs of the interfaces used, line separated. Overwrite as necessary.
+ eseries_initiator_protocol: fc # Storage system protocol. Choices: fc, iscsi, sas, ib_iser, ib_srp, nvme_ib, nvme_roce
+ eseries_initiator_command:
+ fc:
+ linux: "systool -c fc_host -v | grep port_name | cut -d '\"' -f 2 | cut -d 'x' -f 2 | sort | uniq"
+ windows: "(Get-InitiatorPort | Where-Object -P ConnectionType -EQ 'Fibre Channel' | Select-Object -Property PortAddress |
+ Format-Table -AutoSize -HideTableHeaders | Out-String).trim()"
+ iscsi:
+ linux: "grep -o iqn.* /etc/iscsi/initiatorname.iscsi"
+ windows: "(get-initiatorPort | select-object -property nodeaddress | sort-object | get-unique | ft -autoSize | out-string -stream |
+ select-string iqn | out-string).trim()"
+ sas:
+ # NetApp IMT for SAS attached E-Series SAN hosts recommends adding all possible SAS addresses with the base address
+ # starting at 0, and the last address ending in 3 for single port HBAs, or 7 for dual port HBAs. Since determining
+ # single vs . dual port HBAs adds complexity, we always add all 8 possible permutations of the SAS address.
+ linux: "systool -c scsi_host -v | grep host_sas_address | cut -d '\"' -f 2 | cut -d 'x' -f 2 | sort | uniq"
+ windows: "(Get-InitiatorPort | Where-Object -P ConnectionType -EQ 'SAS' | Select-Object -Property PortAddress |
+ Format-Table -AutoSize -HideTableHeaders | Out-String).trim()"
+ ib_iser:
+ linux: "grep -o iqn.* /etc/iscsi/initiatorname.iscsi"
+ windows: "" # add windows command for determining host iqn address(es)
+ ib_srp:
+ linux: "ibstat -p"
+ windows: "" # Add Windows command for determining host guid
+ nvme_ib:
+ linux: "" # Add Linux command for determining host nqn address(es)
+ windows: "" # Add Windows command for determining host nqn address(es)
+ nvme_roce:
+ linux: "" # Add Linux command for determining host nqn address(es)
+ windows: "" # Add Windows command for determining host nqn address(es)
+
+ # Manual host definitions, Linux and Windows systems can be automatically populated based on host mappings found in eseries_storage_pool_configuration
+ Note that using the automated method is preferred.
+ eseries_host_force_port: true # Default for whether ports are to be allowed to be re-assigned (boolean)
+ eseries_host_remove_unused_hostgroup: true # Forces any unused groups to be removed
+ eseries_host_object:
+ - name: # Host label as referenced by the storage array.
+ state: # Specifies whether host definition should be exist. Choices: present, absent
+ ports: # List of port definitions
+ - type: # Port protocol definition (iscsi, fc, sas, ib, nvme). Note that you should use 'iscsi' prior to Santricity version 11.60 for IB iSER.
+ label: # Arbitrary port label
+ port: # Port initiator (iqn, wwn, etc)
+ group: # Host's host group
+ host_type: # Only required when using something other than Linux kernel 3.10 or later with DM-MP (Linux DM-MP),
+ # non-clustered Windows (Windows), or the storage system default host type is incorrect.
+ # Common host type definitions:
+ # - AIX MPIO: The Advanced Interactive Executive (AIX) OS and the native MPIO driver
+ # - AVT 4M: Silicon Graphics, Inc. (SGI) proprietary multipath driver
+ # - HP-UX: The HP-UX OS with native multipath driver
+ # - Linux ATTO: The Linux OS and the ATTO Technology, Inc. driver (must use ATTO FC HBAs)
+ # - Linux DM-MP: The Linux OS and the native DM-MP driver
+ # - Linux Pathmanager: The Linux OS and the SGI proprietary multipath driver
+ # - Mac: The Mac OS and the ATTO Technology, Inc. driver
+ # - ONTAP: FlexArray
+ # - Solaris 11 or later: The Solaris 11 or later OS and the native MPxIO driver
+ # - Solaris 10 or earlier: The Solaris 10 or earlier OS and the native MPxIO driver
+ # - SVC: IBM SAN Volume Controller
+ # - VMware: ESXi OS
+ # - Windows: Windows Server OS and Windows MPIO with a DSM driver
+ # - Windows Clustered: Clustered Windows Server OS and Windows MPIO with a DSM driver
+ # - Windows ATTO: Windows OS and the ATTO Technology, Inc. driver
+
+License
+-------
+ BSD-3-Clause
+
+Author Information
+------------------
+ Nathan Swartz (@ndswartz)
+
+=======================================
+Netapp_Eseries.Santricity Release Notes
+=======================================
+
+.. contents:: Topics
+
+
+v1.1.0
+======
+
+Release Summary
+---------------
+
+This release focused on providing volume details to through the netapp_volumes_by_initiators in the na_santricity_facts module, improving on the nar_santricity_common role storage system API information and resolving issues.
+
+Minor Changes
+-------------
+
+- Add functionality to remove all inventory configuration in the nar_santricity_host role. Set configuration.eseries_remove_all_configuration=True to remove all storage pool/volume configuration, host, hostgroup, and lun mapping configuration.
+- Add host_types, host_port_protocols, host_port_information, hostside_io_interface_protocols to netapp_volumes_by_initiators in the na_santricity_facts module.
+- Add storage pool information to the volume_by_initiator facts.
+- Add storage system not found exception to the common role's build_info task.
+- Add volume_metadata option to na_santricity_volume module, add volume_metadata information to the netapp_volumes_by_initiators dictionary in na_santricity_facts module, and update the nar_santricity_host role with the option.
+- Improve nar_santricity_common storage system api determinations; attempts to discover the storage system using the information provided in the inventory before attempting to search the subnet.
+- Increased the storage system discovery connection timeouts to 30 seconds to prevent systems from not being discovered over slow connections.
+- Minimize the facts gathered for the host initiators.
+- Update ib iser determination to account for changes in firmware 11.60.2.
+- Use existing Web Services Proxy storage system identifier when one is already created and one is not provided in the inventory.
+- Utilize eseries_iscsi_iqn before searching host for iqn in nar_santricity_host role.
+
+Bugfixes
+--------
+
+- Fix check_port_type method for ib iser when ib is the port type.
+- Fix examples in the netapp_e_mgmt_interface module.
+- Fix issue with changing host port name.
+- Fix na_santricity_lun_mapping unmapping issue; previously mapped volumes failed to be unmapped.
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/changelogs/.plugin-cache.yaml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/changelogs/.plugin-cache.yaml
new file mode 100644
index 00000000..58aedcc7
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/changelogs/.plugin-cache.yaml
@@ -0,0 +1,297 @@
+plugins:
+ become: {}
+ cache: {}
+ callback: {}
+ cliconf: {}
+ connection: {}
+ httpapi: {}
+ inventory: {}
+ lookup:
+ santricity_host:
+ description: Collects host information
+ name: santricity_host
+ version_added: null
+ santricity_host_detail:
+ description: Expands the host information from santricity_host lookup
+ name: santricity_host_detail
+ version_added: null
+ santricity_storage_pool:
+ description: Storage pool information
+ name: santricity_storage_pool
+ version_added: null
+ module:
+ na_santricity_alerts:
+ description: NetApp E-Series manage email notification settings
+ name: na_santricity_alerts
+ namespace: ''
+ version_added: null
+ na_santricity_alerts_syslog:
+ description: NetApp E-Series manage syslog servers receiving storage system
+ alerts.
+ name: na_santricity_alerts_syslog
+ namespace: ''
+ version_added: null
+ na_santricity_asup:
+ description: NetApp E-Series manage auto-support settings
+ name: na_santricity_asup
+ namespace: ''
+ version_added: null
+ na_santricity_auditlog:
+ description: NetApp E-Series manage audit-log configuration
+ name: na_santricity_auditlog
+ namespace: ''
+ version_added: null
+ na_santricity_auth:
+ description: NetApp E-Series set or update the password for a storage array
+ device or SANtricity Web Services Proxy.
+ name: na_santricity_auth
+ namespace: ''
+ version_added: null
+ na_santricity_client_certificate:
+ description: NetApp E-Series manage remote server certificates.
+ name: na_santricity_client_certificate
+ namespace: ''
+ version_added: null
+ na_santricity_discover:
+ description: NetApp E-Series discover E-Series storage systems
+ name: na_santricity_discover
+ namespace: ''
+ version_added: null
+ na_santricity_drive_firmware:
+ description: NetApp E-Series manage drive firmware
+ name: na_santricity_drive_firmware
+ namespace: ''
+ version_added: null
+ na_santricity_facts:
+ description: NetApp E-Series retrieve facts about NetApp E-Series storage arrays
+ name: na_santricity_facts
+ namespace: ''
+ version_added: null
+ na_santricity_firmware:
+ description: NetApp E-Series manage firmware.
+ name: na_santricity_firmware
+ namespace: ''
+ version_added: null
+ na_santricity_global:
+ description: NetApp E-Series manage global settings configuration
+ name: na_santricity_global
+ namespace: ''
+ version_added: null
+ na_santricity_host:
+ description: NetApp E-Series manage eseries hosts
+ name: na_santricity_host
+ namespace: ''
+ version_added: null
+ na_santricity_hostgroup:
+ description: NetApp E-Series manage array host groups
+ name: na_santricity_hostgroup
+ namespace: ''
+ version_added: null
+ na_santricity_ib_iser_interface:
+ description: NetApp E-Series manage InfiniBand iSER interface configuration
+ name: na_santricity_ib_iser_interface
+ namespace: ''
+ version_added: null
+ na_santricity_iscsi_interface:
+ description: NetApp E-Series manage iSCSI interface configuration
+ name: na_santricity_iscsi_interface
+ namespace: ''
+ version_added: null
+ na_santricity_iscsi_target:
+ description: NetApp E-Series manage iSCSI target configuration
+ name: na_santricity_iscsi_target
+ namespace: ''
+ version_added: null
+ na_santricity_ldap:
+ description: NetApp E-Series manage LDAP integration to use for authentication
+ name: na_santricity_ldap
+ namespace: ''
+ version_added: null
+ na_santricity_lun_mapping:
+ description: NetApp E-Series manage lun mappings
+ name: na_santricity_lun_mapping
+ namespace: ''
+ version_added: null
+ na_santricity_mgmt_interface:
+ description: NetApp E-Series manage management interface configuration
+ name: na_santricity_mgmt_interface
+ namespace: ''
+ version_added: null
+ na_santricity_nvme_interface:
+ description: NetApp E-Series manage NVMe interface configuration
+ name: na_santricity_nvme_interface
+ namespace: ''
+ version_added: null
+ na_santricity_proxy_drive_firmware_upload:
+ description: NetApp E-Series manage proxy drive firmware files
+ name: na_santricity_proxy_drive_firmware_upload
+ namespace: ''
+ version_added: null
+ na_santricity_proxy_firmware_upload:
+ description: NetApp E-Series manage proxy firmware uploads.
+ name: na_santricity_proxy_firmware_upload
+ namespace: ''
+ version_added: null
+ na_santricity_proxy_systems:
+ description: NetApp E-Series manage SANtricity web services proxy storage arrays
+ name: na_santricity_proxy_systems
+ namespace: ''
+ version_added: null
+ na_santricity_storagepool:
+ description: NetApp E-Series manage volume groups and disk pools
+ name: na_santricity_storagepool
+ namespace: ''
+ version_added: null
+ na_santricity_syslog:
+ description: NetApp E-Series manage syslog settings
+ name: na_santricity_syslog
+ namespace: ''
+ version_added: null
+ na_santricity_volume:
+ description: NetApp E-Series manage storage volumes (standard and thin)
+ name: na_santricity_volume
+ namespace: ''
+ version_added: null
+ netapp_e_alerts:
+ description: NetApp E-Series manage email notification settings
+ name: netapp_e_alerts
+ namespace: ''
+ version_added: '2.7'
+ netapp_e_amg:
+ description: NetApp E-Series create, remove, and update asynchronous mirror
+ groups
+ name: netapp_e_amg
+ namespace: ''
+ version_added: '2.2'
+ netapp_e_amg_role:
+ description: NetApp E-Series update the role of a storage array within an Asynchronous
+ Mirror Group (AMG).
+ name: netapp_e_amg_role
+ namespace: ''
+ version_added: '2.2'
+ netapp_e_amg_sync:
+ description: NetApp E-Series conduct synchronization actions on asynchronous
+ mirror groups.
+ name: netapp_e_amg_sync
+ namespace: ''
+ version_added: '2.2'
+ netapp_e_asup:
+ description: NetApp E-Series manage auto-support settings
+ name: netapp_e_asup
+ namespace: ''
+ version_added: '2.7'
+ netapp_e_auditlog:
+ description: NetApp E-Series manage audit-log configuration
+ name: netapp_e_auditlog
+ namespace: ''
+ version_added: '2.7'
+ netapp_e_auth:
+ description: NetApp E-Series set or update the password for a storage array.
+ name: netapp_e_auth
+ namespace: ''
+ version_added: '2.2'
+ netapp_e_drive_firmware:
+ description: NetApp E-Series manage drive firmware
+ name: netapp_e_drive_firmware
+ namespace: ''
+ version_added: '2.9'
+ netapp_e_facts:
+ description: NetApp E-Series retrieve facts about NetApp E-Series storage arrays
+ name: netapp_e_facts
+ namespace: ''
+ version_added: '2.2'
+ netapp_e_firmware:
+ description: NetApp E-Series manage firmware.
+ name: netapp_e_firmware
+ namespace: ''
+ version_added: '2.9'
+ netapp_e_flashcache:
+ description: NetApp E-Series manage SSD caches
+ name: netapp_e_flashcache
+ namespace: ''
+ version_added: '2.2'
+ netapp_e_global:
+ description: NetApp E-Series manage global settings configuration
+ name: netapp_e_global
+ namespace: ''
+ version_added: '2.7'
+ netapp_e_host:
+ description: NetApp E-Series manage eseries hosts
+ name: netapp_e_host
+ namespace: ''
+ version_added: '2.2'
+ netapp_e_hostgroup:
+ description: NetApp E-Series manage array host groups
+ name: netapp_e_hostgroup
+ namespace: ''
+ version_added: '2.2'
+ netapp_e_iscsi_interface:
+ description: NetApp E-Series manage iSCSI interface configuration
+ name: netapp_e_iscsi_interface
+ namespace: ''
+ version_added: '2.7'
+ netapp_e_iscsi_target:
+ description: NetApp E-Series manage iSCSI target configuration
+ name: netapp_e_iscsi_target
+ namespace: ''
+ version_added: '2.7'
+ netapp_e_ldap:
+ description: NetApp E-Series manage LDAP integration to use for authentication
+ name: netapp_e_ldap
+ namespace: ''
+ version_added: '2.7'
+ netapp_e_lun_mapping:
+ description: NetApp E-Series create, delete, or modify lun mappings
+ name: netapp_e_lun_mapping
+ namespace: ''
+ version_added: '2.2'
+ netapp_e_mgmt_interface:
+ description: NetApp E-Series management interface configuration
+ name: netapp_e_mgmt_interface
+ namespace: ''
+ version_added: '2.7'
+ netapp_e_snapshot_group:
+ description: NetApp E-Series manage snapshot groups
+ name: netapp_e_snapshot_group
+ namespace: ''
+ version_added: '2.2'
+ netapp_e_snapshot_images:
+ description: NetApp E-Series create and delete snapshot images
+ name: netapp_e_snapshot_images
+ namespace: ''
+ version_added: '2.2'
+ netapp_e_snapshot_volume:
+ description: NetApp E-Series manage snapshot volumes.
+ name: netapp_e_snapshot_volume
+ namespace: ''
+ version_added: '2.2'
+ netapp_e_storage_system:
+ description: NetApp E-Series Web Services Proxy manage storage arrays
+ name: netapp_e_storage_system
+ namespace: ''
+ version_added: '2.2'
+ netapp_e_storagepool:
+ description: NetApp E-Series manage volume groups and disk pools
+ name: netapp_e_storagepool
+ namespace: ''
+ version_added: '2.2'
+ netapp_e_syslog:
+ description: NetApp E-Series manage syslog settings
+ name: netapp_e_syslog
+ namespace: ''
+ version_added: '2.7'
+ netapp_e_volume:
+ description: NetApp E-Series manage storage volumes (standard and thin)
+ name: netapp_e_volume
+ namespace: ''
+ version_added: '2.2'
+ netapp_e_volume_copy:
+ description: NetApp E-Series create volume copy pairs
+ name: netapp_e_volume_copy
+ namespace: ''
+ version_added: '2.2'
+ netconf: {}
+ shell: {}
+ strategy: {}
+ vars: {}
+version: 1.1.0
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/changelogs/changelog.yaml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/changelogs/changelog.yaml
new file mode 100644
index 00000000..c82ae2fb
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/changelogs/changelog.yaml
@@ -0,0 +1,54 @@
+ancestor: null
+releases:
+ 1.1.0:
+ changes:
+ bugfixes:
+ - Fix check_port_type method for ib iser when ib is the port type.
+ - Fix examples in the netapp_e_mgmt_interface module.
+ - Fix issue with changing host port name.
+ - Fix na_santricity_lun_mapping unmapping issue; previously mapped volumes failed
+ to be unmapped.
+ minor_changes:
+ - Add functionality to remove all inventory configuration in the nar_santricity_host
+ role. Set configuration.eseries_remove_all_configuration=True to remove all
+ storage pool/volume configuration, host, hostgroup, and lun mapping configuration.
+ - Add host_types, host_port_protocols, host_port_information, hostside_io_interface_protocols
+ to netapp_volumes_by_initiators in the na_santricity_facts module.
+ - Add storage pool information to the volume_by_initiator facts.
+ - Add storage system not found exception to the common role's build_info task.
+ - Add volume_metadata option to na_santricity_volume module, add volume_metadata
+ information to the netapp_volumes_by_initiators dictionary in na_santricity_facts
+ module, and update the nar_santricity_host role with the option.
+ - Improve nar_santricity_common storage system api determinations; attempts
+ to discover the storage system using the information provided in the inventory
+ before attempting to search the subnet.
+ - Increased the storage system discovery connection timeouts to 30 seconds to
+ prevent systems from not being discovered over slow connections.
+ - Minimize the facts gathered for the host initiators.
+ - Update ib iser determination to account for changes in firmware 11.60.2.
+ - Use existing Web Services Proxy storage system identifier when one is already
+ created and one is not provided in the inventory.
+ - Utilize eseries_iscsi_iqn before searching host for iqn in nar_santricity_host
+ role.
+ release_summary: This release focused on providing volume details to through
+ the netapp_volumes_by_initiators in the na_santricity_facts module, improving
+ on the nar_santricity_common role storage system API information and resolving
+ issues.
+ fragments:
+ - 1.0.9.yml
+ - add_io_communication_protocol_info_to_volume_by_initator_facts.yml
+ - add_storage_pool_info_to_volume_by_initiator_facts.yml
+ - add_storage_system_not_found_exception.yml
+ - add_undo_configuration.yml
+ - add_volume_metadata_option.yml
+ - fix_change_host_port.yml
+ - fix_ib_iser_port_type.yml
+ - fix_netapp_e_mgmt_interface_examples.yml
+ - fix_volume_unmapping_issue.yml
+ - improve_storage_system_api_determinations.yml
+ - increase_discovery_connection_timeout.yml
+ - minimize_host_initiator_facts_gathered.yml
+ - update_ib_iser_determination.yml
+ - use_existing_proxy_ssid_when_unspecified.yml
+ - utilize_eseries_iscsi_iqn_before_searching_host.yml
+ release_date: '2020-09-18'
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/changelogs/config.yaml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/changelogs/config.yaml
new file mode 100644
index 00000000..29dd0280
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/changelogs/config.yaml
@@ -0,0 +1,32 @@
+changelog_filename_template: ../CHANGELOG.rst
+changelog_filename_version_depth: 0
+changes_file: changelog.yaml
+changes_format: combined
+ignore_other_fragment_extensions: true
+keep_fragments: false
+mention_ancestor: true
+new_plugins_after_name: removed_features
+notesdir: fragments
+prelude_section_name: release_summary
+prelude_section_title: Release Summary
+sanitize_changelog: true
+sections:
+- - major_changes
+ - Major Changes
+- - minor_changes
+ - Minor Changes
+- - breaking_changes
+ - Breaking Changes / Porting Guide
+- - deprecated_features
+ - Deprecated Features
+- - removed_features
+ - Removed Features (previously deprecated)
+- - security_fixes
+ - Security Fixes
+- - bugfixes
+ - Bugfixes
+- - known_issues
+ - Known Issues
+title: Netapp_Eseries.Santricity
+trivial_section_name: trivial
+use_fqcn: true
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/doc_fragments/netapp.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/doc_fragments/netapp.py
new file mode 100644
index 00000000..b3943fb3
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/doc_fragments/netapp.py
@@ -0,0 +1,228 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Sumit Kumar <sumit4@netapp.com>, chris Archibald <carchi@netapp.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+
+ DOCUMENTATION = r'''
+options:
+ - See respective platform section for more details
+requirements:
+ - See respective platform section for more details
+notes:
+ - Ansible modules are available for the following NetApp Storage Platforms: E-Series, ONTAP, SolidFire
+'''
+
+ # Documentation fragment for Cloud Volume Services on Azure NetApp (azure_rm_netapp)
+ AZURE_RM_NETAPP = r'''
+options:
+ resource_group:
+ description:
+ - Name of the resource group.
+ required: true
+ type: str
+requirements:
+ - python >= 2.7
+ - azure >= 2.0.0
+ - Python netapp-mgmt. Install using 'pip install netapp-mgmt'
+ - Python netapp-mgmt-netapp. Install using 'pip install netapp-mgmt-netapp'
+ - For authentication with Azure NetApp log in before you run your tasks or playbook with C(az login).
+
+notes:
+ - The modules prefixed with azure_rm_netapp are built to support the Cloud Volume Services for Azure NetApp Files.
+
+seealso:
+ - name: Sign in with Azure CLI
+ link: https://docs.microsoft.com/en-us/cli/azure/authenticate-azure-cli?view=azure-cli-latest
+ description: How to authenticate using the C(az login) command.
+ '''
+
+ # Documentation fragment for ONTAP (na_ontap)
+ NA_ONTAP = r'''
+options:
+ hostname:
+ description:
+ - The hostname or IP address of the ONTAP instance.
+ type: str
+ required: true
+ username:
+ description:
+ - This can be a Cluster-scoped or SVM-scoped account, depending on whether a Cluster-level or SVM-level API is required.
+ For more information, please read the documentation U(https://mysupport.netapp.com/NOW/download/software/nmsdk/9.4/).
+ type: str
+ required: true
+ aliases: [ user ]
+ password:
+ description:
+ - Password for the specified user.
+ type: str
+ required: true
+ aliases: [ pass ]
+ https:
+ description:
+ - Enable and disable https
+ type: bool
+ default: no
+ validate_certs:
+ description:
+ - If set to C(no), the SSL certificates will not be validated.
+ - This should only set to C(False) used on personally controlled sites using self-signed certificates.
+ type: bool
+ default: yes
+ http_port:
+ description:
+ - Override the default port (80 or 443) with this port
+ type: int
+ ontapi:
+ description:
+ - The ontap api version to use
+ type: int
+ use_rest:
+ description:
+ - REST API if supported by the target system for all the resources and attributes the module requires. Otherwise will revert to ZAPI.
+ - Always -- will always use the REST API
+ - Never -- will always use the ZAPI
+ - Auto -- will try to use the REST Api
+ default: Auto
+ choices: ['Never', 'Always', 'Auto']
+ type: str
+
+
+requirements:
+ - A physical or virtual clustered Data ONTAP system. The modules support Data ONTAP 9.1 and onward
+ - Ansible 2.6
+ - Python2 netapp-lib (2017.10.30) or later. Install using 'pip install netapp-lib'
+ - Python3 netapp-lib (2018.11.13) or later. Install using 'pip install netapp-lib'
+ - To enable http on the cluster you must run the following commands 'set -privilege advanced;' 'system services web modify -http-enabled true;'
+
+notes:
+ - The modules prefixed with na\\_ontap are built to support the ONTAP storage platform.
+
+'''
+
+ # Documentation fragment for ONTAP (na_cdot)
+ ONTAP = r'''
+options:
+ hostname:
+ required: true
+ description:
+ - The hostname or IP address of the ONTAP instance.
+ username:
+ required: true
+ description:
+ - This can be a Cluster-scoped or SVM-scoped account, depending on whether a Cluster-level or SVM-level API is required.
+ For more information, please read the documentation U(https://mysupport.netapp.com/NOW/download/software/nmsdk/9.4/).
+ aliases: ['user']
+ password:
+ required: true
+ description:
+ - Password for the specified user.
+ aliases: ['pass']
+requirements:
+ - A physical or virtual clustered Data ONTAP system. The modules were developed with Clustered Data ONTAP 8.3
+ - Ansible 2.2
+ - netapp-lib (2015.9.25). Install using 'pip install netapp-lib'
+
+notes:
+ - The modules prefixed with na\\_cdot are built to support the ONTAP storage platform.
+
+'''
+
+ # Documentation fragment for SolidFire
+ SOLIDFIRE = r'''
+options:
+ hostname:
+ required: true
+ description:
+ - The hostname or IP address of the SolidFire cluster.
+ username:
+ required: true
+ description:
+ - Please ensure that the user has the adequate permissions. For more information, please read the official documentation
+ U(https://mysupport.netapp.com/documentation/docweb/index.html?productID=62636&language=en-US).
+ aliases: ['user']
+ password:
+ required: true
+ description:
+ - Password for the specified user.
+ aliases: ['pass']
+
+requirements:
+ - The modules were developed with SolidFire 10.1
+ - solidfire-sdk-python (1.1.0.92) or greater. Install using 'pip install solidfire-sdk-python'
+
+notes:
+ - The modules prefixed with na\\_elementsw are built to support the SolidFire storage platform.
+
+'''
+
+ # Documentation fragment for E-Series
+ ESERIES = r'''
+options:
+ api_username:
+ required: true
+ type: str
+ description:
+ - The username to authenticate with the SANtricity Web Services Proxy or Embedded Web Services API.
+ api_password:
+ required: true
+ type: str
+ description:
+ - The password to authenticate with the SANtricity Web Services Proxy or Embedded Web Services API.
+ api_url:
+ required: true
+ type: str
+ description:
+ - The url to the SANtricity Web Services Proxy or Embedded Web Services API.
+ Example https://prod-1.wahoo.acme.com/devmgr/v2
+ validate_certs:
+ required: false
+ default: true
+ description:
+ - Should https certificates be validated?
+ type: bool
+ ssid:
+ required: false
+ type: str
+ default: 1
+ description:
+ - The ID of the array to manage. This value must be unique for each array.
+
+notes:
+ - The E-Series Ansible modules require either an instance of the Web Services Proxy (WSP), to be available to manage
+ the storage-system, or an E-Series storage-system that supports the Embedded Web Services API.
+ - Embedded Web Services is currently available on the E2800, E5700, EF570, and newer hardware models.
+ - M(netapp_eseries.santricity.netapp_e_storage_system) may be utilized for configuring the systems managed by a WSP instance.
+'''
+
+ # Documentation fragment for AWSCVS
+ AWSCVS = """
+options:
+ api_key:
+ required: true
+ type: str
+ description:
+ - The access key to authenticate with the AWSCVS Web Services Proxy or Embedded Web Services API.
+ secret_key:
+ required: true
+ type: str
+ description:
+ - The secret_key to authenticate with the AWSCVS Web Services Proxy or Embedded Web Services API.
+ api_url:
+ required: true
+ type: str
+ description:
+ - The url to the AWSCVS Web Services Proxy or Embedded Web Services API.
+ validate_certs:
+ required: false
+ default: true
+ description:
+ - Should https certificates be validated?
+ type: bool
+notes:
+ - The modules prefixed with aws\\_cvs\\_netapp are built to Manage AWS Cloud Volume Service .
+"""
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/doc_fragments/santricity.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/doc_fragments/santricity.py
new file mode 100644
index 00000000..aa5cc5ea
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/doc_fragments/santricity.py
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+
+ DOCUMENTATION = r"""
+options:
+ - See respective platform section for more details
+requirements:
+ - See respective platform section for more details
+notes:
+ - Ansible modules are available for the following NetApp Storage Platforms: E-Series
+"""
+
+ # Documentation fragment for E-Series
+ SANTRICITY_PROXY_DOC = r"""
+options:
+ api_username:
+ required: true
+ type: str
+ description:
+ - The username to authenticate with the SANtricity Web Services Proxy or Embedded Web Services API.
+ api_password:
+ required: true
+ type: str
+ description:
+ - The password to authenticate with the SANtricity Web Services Proxy or Embedded Web Services API.
+ api_url:
+ required: true
+ type: str
+ description:
+ - The url to the SANtricity Web Services Proxy or Embedded Web Services API.
+ - Example https://prod-1.wahoo.acme.com:8443/devmgr/v2
+ validate_certs:
+ required: false
+ default: true
+ description:
+ - Should https certificates be validated?
+ type: bool
+
+notes:
+ - The E-Series Ansible modules require either an instance of the Web Services Proxy (WSP), to be available to manage
+ the storage-system, or an E-Series storage-system that supports the Embedded Web Services API.
+ - Embedded Web Services is currently available on the E2800, E5700, EF570, and newer hardware models.
+ - M(netapp_eseries.santricity.netapp_e_storage_system) may be utilized for configuring the systems managed by a WSP instance.
+"""
+
+ # Documentation fragment for E-Series
+ SANTRICITY_DOC = r"""
+options:
+ api_username:
+ required: true
+ type: str
+ description:
+ - The username to authenticate with the SANtricity Web Services Proxy or Embedded Web Services API.
+ api_password:
+ required: true
+ type: str
+ description:
+ - The password to authenticate with the SANtricity Web Services Proxy or Embedded Web Services API.
+ api_url:
+ required: true
+ type: str
+ description:
+ - The url to the SANtricity Web Services Proxy or Embedded Web Services API.
+ - Example https://prod-1.wahoo.acme.com:8443/devmgr/v2
+ validate_certs:
+ required: false
+ default: true
+ description:
+ - Should https certificates be validated?
+ type: bool
+ ssid:
+ required: false
+ type: str
+ default: 1
+ description:
+ - The ID of the array to manage. This value must be unique for each array.
+
+notes:
+ - The E-Series Ansible modules require either an instance of the Web Services Proxy (WSP), to be available to manage
+ the storage-system, or an E-Series storage-system that supports the Embedded Web Services API.
+ - Embedded Web Services is currently available on the E2800, E5700, EF570, and newer hardware models.
+ - M(netapp_eseries.santricity.netapp_e_storage_system) may be utilized for configuring the systems managed by a WSP instance.
+"""
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_host.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_host.py
new file mode 100644
index 00000000..b599b995
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_host.py
@@ -0,0 +1,105 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = """
+ lookup: santricity_hosts
+ author: Nathan Swartz
+ short_description: Collects host information
+ description:
+ - Collects current host, expected host and host group inventory definitions.
+ options:
+ inventory:
+ description:
+ - E-Series storage array inventory, hostvars[inventory_hostname].
+ - Run na_santricity_facts prior to calling
+ required: True
+ type: complex
+"""
+from ansible.errors import AnsibleError
+from ansible.plugins.lookup import LookupBase
+
+
+class LookupModule(LookupBase):
+
+ def run(self, inventory, **kwargs):
+ if isinstance(inventory, list):
+ inventory = inventory[0]
+
+ if ("eseries_storage_pool_configuration" not in inventory or not isinstance(inventory["eseries_storage_pool_configuration"], list) or
+ len(inventory["eseries_storage_pool_configuration"]) == 0):
+ return list()
+
+ if "eseries_storage_pool_configuration" not in inventory.keys():
+ raise AnsibleError("eseries_storage_pool_configuration must be defined. See nar_santricity_host role documentation.")
+
+ info = {"current_hosts": {}, "expected_hosts": {}, "host_groups": {}}
+
+ groups = []
+ hosts = []
+ non_inventory_hosts = []
+ non_inventory_groups = []
+ for group in inventory["groups"].keys():
+ groups.append(group)
+ hosts.extend(inventory["groups"][group])
+
+ if "eseries_host_object" in inventory.keys():
+ non_inventory_hosts = [host["name"] for host in inventory["eseries_host_object"]]
+ non_inventory_groups = [host["group"] for host in inventory["eseries_host_object"] if "group" in host]
+
+ # Determine expected hosts and host groups
+ for storage_pool in inventory["eseries_storage_pool_configuration"]:
+ if "volumes" in storage_pool:
+ for volume in storage_pool["volumes"]:
+
+ if (("state" in volume and volume["state"] == "present") or
+ ("eseries_volume_state" in inventory and inventory["eseries_volume_state"] == "present") or
+ ("state" not in volume and "eseries_volume_state" not in inventory)):
+ if "host" in volume:
+ if volume["host"] in groups:
+
+ if volume["host"] not in info["host_groups"].keys():
+
+ # Add all expected group hosts
+ for expected_host in inventory["groups"][volume["host"]]:
+ if "host_type" in volume:
+ info["expected_hosts"].update({expected_host: {"state": "present",
+ "host_type": volume["host_type"],
+ "group": volume["host"]}})
+ elif "common_volume_configuration" in storage_pool and "host_type" in storage_pool["common_volume_configuration"]:
+ info["expected_hosts"].update({expected_host: {"state": "present",
+ "host_type": storage_pool["common_volume_configuration"]["host_type"],
+ "group": volume["host"]}})
+ elif "eseries_system_default_host_type" in inventory:
+ info["expected_hosts"].update({expected_host: {"state": "present",
+ "host_type": inventory["eseries_system_default_host_type"],
+ "group": volume["host"]}})
+ else:
+ info["expected_hosts"].update({expected_host: {"state": "present",
+ "group": volume["host"]}})
+
+ info["host_groups"].update({volume["host"]: inventory["groups"][volume["host"]]})
+
+ elif volume["host"] in hosts:
+ if "host_type" in volume:
+ info["expected_hosts"].update({volume["host"]: {"state": "present",
+ "host_type": volume["host_type"],
+ "group": None}})
+ elif "common_volume_configuration" in storage_pool and "host_type" in storage_pool["common_volume_configuration"]:
+ info["expected_hosts"].update({volume["host"]: {"state": "present",
+ "host_type": storage_pool["common_volume_configuration"]["host_type"],
+ "group": volume["host"]}})
+ elif "eseries_system_default_host_type" in inventory:
+ info["expected_hosts"].update({volume["host"]: {"state": "present",
+ "host_type": inventory["eseries_system_default_host_type"],
+ "group": volume["host"]}})
+ else:
+ info["expected_hosts"].update({volume["host"]: {"state": "present",
+ "group": None}})
+
+ elif volume["host"] not in non_inventory_hosts and volume["host"] not in non_inventory_groups:
+ raise AnsibleError("Expected host or host group does not exist in your Ansible inventory and is not specified in"
+ " eseries_host_object variable!")
+
+ return [info]
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_host_detail.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_host_detail.py
new file mode 100644
index 00000000..70bb699f
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_host_detail.py
@@ -0,0 +1,102 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = """
+ lookup: santricity_hosts_detail
+ author: Nathan Swartz
+ short_description: Expands the host information from santricity_host lookup
+ description:
+ - Expands the host information from santricity_host lookup to include system and port information
+ options:
+ hosts:
+ description:
+ - E-Series storage array inventory, hostvars[inventory_hostname].
+ - Run na_santricity_facts prior to calling
+ required: True
+ type: list
+ hosts_info:
+ description:
+ - The registered results from the setup module from each expected_hosts, hosts_info['results'].
+ - Collected results from the setup module for each expected_hosts from the results of the santricity_host lookup plugin.
+ required: True
+ type: list
+ host_interface_ports:
+ description:
+ - List of dictionaries containing "stdout_lines" which is a list of iqn/wwpns for each expected_hosts from the results of
+ the santricity_host lookup plugin.
+ - Register the results from the shell module that is looped over each host in expected_hosts. The command issued should result
+ in a newline delineated list of iqns, nqns, or wwpns.
+ required: True
+ type: list
+ protocol:
+ description:
+ - Storage system interface protocol (iscsi, sas, fc, ib-iser, ib-srp, nvme_ib, or nvme_roce)
+ required: True
+ type: str
+
+"""
+import re
+from ansible.errors import AnsibleError
+from ansible.plugins.lookup import LookupBase
+
+
+class LookupModule(LookupBase):
+ def run(self, hosts, hosts_info, host_interface_ports, protocol, **kwargs):
+ if isinstance(hosts, list):
+ hosts = hosts[0]
+
+ if "expected_hosts" not in hosts:
+ raise AnsibleError("Invalid argument: hosts must contain the output from santricity_host lookup plugin.")
+ if not isinstance(hosts_info, list):
+ raise AnsibleError("Invalid argument: hosts_info must contain the results from the setup module for each"
+ " expected_hosts found in the output of the santricity_host lookup plugin.")
+ if not isinstance(host_interface_ports, list):
+ raise AnsibleError("Invalid argument: host_interface_ports must contain list of dictionaries containing 'stdout_lines' key"
+ " which is a list of iqns, nqns, or wwpns for each expected_hosts from the results of the santricity_host lookup plugin")
+ if protocol not in ["iscsi", "sas", "fc", "ib_iser", "ib_srp", "nvme_ib", "nvme_roce"]:
+ raise AnsibleError("Invalid argument: protocol must be a protocol from the following: iscsi, sas, fc, ib_iser, ib_srp, nvme_ib, nvme_roce.")
+
+ for host in hosts["expected_hosts"].keys():
+ sanitized_hostname = re.sub("[.:-]", "_", host)[:20]
+
+ # Add host information to expected host
+ for info in hosts_info:
+ if info["item"] == host:
+
+ # Determine host type
+ if "host_type" not in hosts["expected_hosts"][host].keys():
+ if info["ansible_facts"]["ansible_os_family"].lower() == "windows":
+ hosts["expected_hosts"][host]["host_type"] = "windows"
+ elif info["ansible_facts"]["ansible_os_family"].lower() in ["redhat", "debian", "suse"]:
+ hosts["expected_hosts"][host]["host_type"] = "linux dm-mp"
+
+ # Update hosts object
+ hosts["expected_hosts"][host].update({"sanitized_hostname": sanitized_hostname, "ports": []})
+
+ # Add SAS ports
+ for interface in host_interface_ports:
+ if interface["item"] == host and "stdout_lines" in interface.keys():
+ if protocol == "sas":
+ for index, address in enumerate([base[:-1] + str(index) for base in interface["stdout_lines"] for index in range(8)]):
+ label = "%s_%s" % (sanitized_hostname, index)
+ hosts["expected_hosts"][host]["ports"].append({"type": "sas", "label": label, "port": address})
+ elif protocol == "ib_iser" or protocol == "ib_srp":
+ for index, address in enumerate(interface["stdout_lines"]):
+ label = "%s_%s" % (sanitized_hostname, index)
+ hosts["expected_hosts"][host]["ports"].append({"type": "ib", "label": label, "port": address})
+ elif protocol == "nvme_ib":
+ for index, address in enumerate(interface["stdout_lines"]):
+ label = "%s_%s" % (sanitized_hostname, index)
+ hosts["expected_hosts"][host]["ports"].append({"type": "nvmeof", "label": label, "port": address})
+ elif protocol == "nvme_roce":
+ for index, address in enumerate(interface["stdout_lines"]):
+ label = "%s_%s" % (sanitized_hostname, index)
+ hosts["expected_hosts"][host]["ports"].append({"type": "nvmeof", "label": label, "port": address})
+ else:
+ for index, address in enumerate(interface["stdout_lines"]):
+ label = "%s_%s" % (sanitized_hostname, index)
+ hosts["expected_hosts"][host]["ports"].append({"type": protocol, "label": label, "port": address})
+
+ return [hosts]
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_lun_mapping.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_lun_mapping.py
new file mode 100644
index 00000000..6160d82c
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_lun_mapping.py
@@ -0,0 +1,141 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.lookup import LookupBase
+from ansible.errors import AnsibleError
+
+
+class LookupModule(LookupBase):
+ def run(self, array_facts, volumes, **kwargs):
+ if isinstance(array_facts, list):
+ array_facts = array_facts[0]
+
+ if isinstance(volumes, dict): # This means that there is only one volume and volumes was stripped of its list
+ volumes = [volumes]
+
+ if "storage_array_facts" not in array_facts.keys():
+ # Don't throw exceptions unless you want run to terminate!!!
+ # raise AnsibleError("Storage array information not available. Collect facts using na_santricity_facts module.")
+ return list()
+
+ # Remove any absent volumes
+ volumes = [vol for vol in volumes if "state" not in vol or vol["state"] == "present"]
+
+ self.array_facts = array_facts["storage_array_facts"]
+ self.luns_by_target = self.array_facts["netapp_luns_by_target"]
+ self.access_volume_lun = self.array_facts["netapp_default_hostgroup_access_volume_lun"]
+
+ # Search for volumes that have a specified host or host group initiator
+ mapping_info = list()
+ for volume in volumes:
+ if "host" in volume.keys():
+
+ # host initiator is already mapped on the storage system
+ if volume["host"] in self.luns_by_target:
+
+ used_luns = [lun for name, lun in self.luns_by_target[volume["host"]]]
+ for host_group in self.array_facts["netapp_host_groups"]:
+ if volume["host"] == host_group["name"]: # target is an existing host group
+ for host in host_group["hosts"]:
+ used_luns.extend([lun for name, lun in self.luns_by_target[host]])
+ break
+ elif volume["host"] in host_group["hosts"]: # target is an existing host in the host group.
+ used_luns.extend([lun for name, lun in self.luns_by_target[host_group["name"]]])
+ break
+
+ for name, lun in self.luns_by_target[volume["host"]]:
+
+ # Check whether volume is mapped to the expected host
+ if name == volume["name"]:
+ # Check whether lun option differs from existing lun
+ if "lun" in volume and volume["lun"] != lun:
+ self.change_volume_mapping_lun(volume["name"], volume["host"], volume["lun"])
+ lun = volume["lun"]
+
+ if lun in used_luns:
+ raise AnsibleError("Volume [%s] cannot be mapped to host or host group [%s] using lun number %s!"
+ % (name, volume["host"], lun))
+
+ mapping_info.append({"volume": volume["name"], "target": volume["host"], "lun": lun})
+ break
+
+ # Volume has not been mapped to host initiator
+ else:
+
+ # Check whether lun option has been used
+ if "lun" in volume:
+ if volume["lun"] in used_luns:
+ for target in self.array_facts["netapp_luns_by_target"].keys():
+ for mapped_volume, mapped_lun in [entry for entry in self.array_facts["netapp_luns_by_target"][target] if entry]:
+ if volume["lun"] == mapped_lun:
+ if volume["name"] != mapped_volume:
+ raise AnsibleError("Volume [%s] cannot be mapped to host or host group [%s] using lun number %s!"
+ % (volume["name"], volume["host"], volume["lun"]))
+ else: # volume is being remapped with the same lun number
+ self.remove_volume_mapping(mapped_volume, target)
+ lun = volume["lun"]
+ else:
+ lun = self.next_available_lun(used_luns)
+
+ mapping_info.append({"volume": volume["name"], "target": volume["host"], "lun": lun})
+ self.add_volume_mapping(volume["name"], volume["host"], lun)
+
+ else:
+ raise AnsibleError("The host or host group [%s] is not defined!" % volume["host"])
+
+ return mapping_info
+
+ def next_available_lun(self, used_luns):
+ """Find next available lun numbers."""
+ if self.access_volume_lun is not None:
+ used_luns.append(self.access_volume_lun)
+
+ lun = 1
+ while lun in used_luns:
+ lun += 1
+
+ return lun
+
+ def add_volume_mapping(self, name, host, lun):
+ """Add volume mapping to record table (luns_by_target)."""
+ # Find associated group and the groups hosts
+ for host_group in self.array_facts["netapp_host_groups"]:
+
+ if host == host_group["name"]:
+ # add to group
+ self.luns_by_target[host].append([name, lun])
+
+ # add to hosts
+ for hostgroup_host in host_group["hosts"]:
+ self.luns_by_target[hostgroup_host].append([name, lun])
+
+ break
+ else:
+ self.luns_by_target[host].append([name, lun])
+
+ def remove_volume_mapping(self, name, host):
+ """remove volume mapping to record table (luns_by_target)."""
+ # Find associated group and the groups hosts
+ for host_group in self.array_facts["netapp_host_groups"]:
+ if host == host_group["name"]:
+ # add to group
+ for entry in self.luns_by_target[host_group["name"]]:
+ if entry[0] == name:
+ del entry
+ # add to hosts
+ for hostgroup_host in host_group["hosts"]:
+ for entry in self.luns_by_target[hostgroup_host]:
+ if entry[0] == name:
+ del entry
+ break
+ else:
+ for index, entry in enumerate(self.luns_by_target[host]):
+ if entry[0] == name:
+ self.luns_by_target[host].pop(index)
+
+ def change_volume_mapping_lun(self, name, host, lun):
+ """remove volume mapping to record table (luns_by_target)."""
+ self.remove_volume_mapping(name, host)
+ self.add_volume_mapping(name, host, lun)
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_storage_pool.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_storage_pool.py
new file mode 100644
index 00000000..6a7b4d87
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_storage_pool.py
@@ -0,0 +1,81 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = """
+ lookup: santricity_sp_config
+ author: Nathan Swartz
+ short_description: Storage pool information
+ description:
+ - Retrieves storage pool information from the inventory
+"""
+import re
+from ansible.plugins.lookup import LookupBase
+from ansible.errors import AnsibleError
+from itertools import product
+
+
+class LookupModule(LookupBase):
+ def run(self, inventory, state, **kwargs):
+ if isinstance(inventory, list):
+ inventory = inventory[0]
+
+ if ("eseries_storage_pool_configuration" not in inventory or not isinstance(inventory["eseries_storage_pool_configuration"], list) or
+ len(inventory["eseries_storage_pool_configuration"]) == 0):
+ return list()
+
+ defaults_state = "present"
+ if"eseries_remove_all_configuration_state" in inventory:
+ defaults_state = inventory["eseries_remove_all_configuration_state"]
+ elif "eseries_storage_pool_state" in inventory:
+ defaults_state = inventory["eseries_storage_pool_state"]
+
+ sp_list = list()
+ for sp_info in inventory["eseries_storage_pool_configuration"]:
+
+ if not isinstance(sp_info, dict) or "name" not in sp_info:
+ raise AnsibleError("eseries_storage_pool_configuration must contain a list of dictionaries containing the necessary information.")
+
+ for sp in patternize(sp_info["name"], inventory):
+ sp_options = {"state": defaults_state}
+
+ for option in sp_info.keys():
+ sp_options.update({option: sp_info[option]})
+
+ sp_options.update({"name": sp})
+
+ if sp_options["state"] == state:
+ sp_list.append(sp_options)
+
+ return sp_list
+
+
+def patternize(pattern, inventory, storage_pool=None):
+ """Generate list of strings determined by a pattern"""
+ if storage_pool:
+ pattern = pattern.replace("[pool]", storage_pool)
+
+ if inventory:
+ inventory_tokens = re.findall(r"\[[a-zA-Z0-9_]*\]", pattern)
+ for token in inventory_tokens:
+ pattern = pattern.replace(token, str(inventory[token[1:-1]]))
+
+ tokens = re.findall(r"\[[0-9]-[0-9]\]|\[[a-z]-[a-z]\]|\[[A-Z]-[A-Z]\]", pattern)
+ segments = "%s".join(re.split(r"\[[0-9]-[0-9]\]|\[[a-z]-[a-z]\]|\[[A-Z]-[A-Z]\]", pattern))
+
+ if len(tokens) == 0:
+ return [pattern]
+
+ combinations = []
+ for token in tokens:
+ start, stop = token[1:-1].split("-")
+
+ try:
+ start = int(start)
+ stop = int(stop)
+ combinations.append([str(number) for number in range(start, stop + 1)])
+ except ValueError:
+ combinations.append([chr(number) for number in range(ord(start), ord(stop) + 1)])
+
+ return [segments % subset for subset in list(product(*combinations))]
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_volume.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_volume.py
new file mode 100644
index 00000000..8f34d170
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/lookup/santricity_volume.py
@@ -0,0 +1,91 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import re
+from ansible.plugins.lookup import LookupBase
+from ansible.errors import AnsibleError
+from itertools import product
+
+
+class LookupModule(LookupBase):
+
+ def run(self, inventory, **kwargs):
+ if isinstance(inventory, list):
+ inventory = inventory[0]
+
+ if ("eseries_storage_pool_configuration" not in inventory.keys() or not isinstance(inventory["eseries_storage_pool_configuration"], list) or
+ len(inventory["eseries_storage_pool_configuration"]) == 0):
+ return list()
+
+ vol_list = list()
+ for sp_info in inventory["eseries_storage_pool_configuration"]:
+
+ if "name" not in sp_info.keys() or "volumes" not in sp_info.keys():
+ continue
+
+ if not isinstance(sp_info["volumes"], list):
+ raise AnsibleError("Volumes must be a list")
+
+ for sp in patternize(sp_info["name"], inventory):
+ for vol_info in sp_info["volumes"]:
+
+ if not isinstance(vol_info, dict):
+ raise AnsibleError("Volume in the storage pool, %s, must be a dictionary." % sp_info["name"])
+
+ for vol in patternize(vol_info["name"], inventory, storage_pool=sp):
+ vol_options = dict()
+
+ # Add common_volume_configuration information
+ combined_volume_metadata = {}
+ if "common_volume_configuration" in sp_info:
+ for option, value in sp_info["common_volume_configuration"].items():
+ vol_options.update({option: value})
+ if "volume_metadata" in sp_info["common_volume_configuration"].keys():
+ combined_volume_metadata.update(sp_info["common_volume_configuration"]["volume_metadata"])
+
+ # Add/update volume specific information
+ for option, value in vol_info.items():
+ vol_options.update({option: value})
+ if "volume_metadata" in vol_info.keys():
+ combined_volume_metadata.update(vol_info["volume_metadata"])
+ vol_options.update({"volume_metadata": combined_volume_metadata})
+
+
+ if "state" in sp_info and sp_info["state"] == "absent":
+ vol_options.update({"state": "absent"})
+
+ vol_options.update({"name": vol, "storage_pool_name": sp})
+ vol_list.append(vol_options)
+ return vol_list
+
+
+def patternize(pattern, inventory, storage_pool=None):
+ """Generate list of strings determined by a pattern"""
+ if storage_pool:
+ pattern = pattern.replace("[pool]", storage_pool)
+
+ if inventory:
+ inventory_tokens = re.findall(r"\[[a-zA-Z0-9_]*\]", pattern)
+ for token in inventory_tokens:
+ pattern = pattern.replace(token, str(inventory[token[1:-1]]))
+
+ tokens = re.findall(r"\[[0-9]-[0-9]\]|\[[a-z]-[a-z]\]|\[[A-Z]-[A-Z]\]", pattern)
+ segments = "%s".join(re.split(r"\[[0-9]-[0-9]\]|\[[a-z]-[a-z]\]|\[[A-Z]-[A-Z]\]", pattern))
+
+ if len(tokens) == 0:
+ return [pattern]
+
+ combinations = []
+ for token in tokens:
+ start, stop = token[1:-1].split("-")
+
+ try:
+ start = int(start)
+ stop = int(stop)
+ combinations.append([str(number) for number in range(start, stop + 1)])
+ except ValueError:
+ combinations.append([chr(number) for number in range(ord(start), ord(stop) + 1)])
+
+ return [segments % subset for subset in list(product(*combinations))]
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/module_utils/netapp.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/module_utils/netapp.py
new file mode 100644
index 00000000..b87e6595
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/module_utils/netapp.py
@@ -0,0 +1,746 @@
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# Copyright (c) 2017, Sumit Kumar <sumit4@netapp.com>
+# Copyright (c) 2017, Michael Price <michael.price@netapp.com>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import json
+import os
+import random
+import mimetypes
+
+from pprint import pformat
+from ansible.module_utils import six
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+from ansible.module_utils.six.moves.urllib.error import HTTPError, URLError
+from ansible.module_utils.urls import open_url
+from ansible.module_utils.api import basic_auth_argument_spec
+from ansible.module_utils._text import to_native
+
+try:
+ from ansible.module_utils.ansible_release import __version__ as ansible_version
+except ImportError:
+ ansible_version = 'unknown'
+
+try:
+ from netapp_lib.api.zapi import zapi
+ HAS_NETAPP_LIB = True
+except ImportError:
+ HAS_NETAPP_LIB = False
+
+try:
+ import requests
+ HAS_REQUESTS = True
+except ImportError:
+ HAS_REQUESTS = False
+
+import ssl
+try:
+ from urlparse import urlparse, urlunparse
+except ImportError:
+ from urllib.parse import urlparse, urlunparse
+
+
+HAS_SF_SDK = False
+SF_BYTE_MAP = dict(
+ # Management GUI displays 1024 ** 3 as 1.1 GB, thus use 1000.
+ bytes=1,
+ b=1,
+ kb=1000,
+ mb=1000 ** 2,
+ gb=1000 ** 3,
+ tb=1000 ** 4,
+ pb=1000 ** 5,
+ eb=1000 ** 6,
+ zb=1000 ** 7,
+ yb=1000 ** 8
+)
+
+POW2_BYTE_MAP = dict(
+ # Here, 1 kb = 1024
+ bytes=1,
+ b=1,
+ kb=1024,
+ mb=1024 ** 2,
+ gb=1024 ** 3,
+ tb=1024 ** 4,
+ pb=1024 ** 5,
+ eb=1024 ** 6,
+ zb=1024 ** 7,
+ yb=1024 ** 8
+)
+
+try:
+ from solidfire.factory import ElementFactory
+ from solidfire.custom.models import TimeIntervalFrequency
+ from solidfire.models import Schedule, ScheduleInfo
+
+ HAS_SF_SDK = True
+except Exception:
+ HAS_SF_SDK = False
+
+
+def has_netapp_lib():
+ return HAS_NETAPP_LIB
+
+
+def has_sf_sdk():
+ return HAS_SF_SDK
+
+
+def na_ontap_host_argument_spec():
+
+ return dict(
+ hostname=dict(required=True, type='str'),
+ username=dict(required=True, type='str', aliases=['user']),
+ password=dict(required=True, type='str', aliases=['pass'], no_log=True),
+ https=dict(required=False, type='bool', default=False),
+ validate_certs=dict(required=False, type='bool', default=True),
+ http_port=dict(required=False, type='int'),
+ ontapi=dict(required=False, type='int'),
+ use_rest=dict(required=False, type='str', default='Auto', choices=['Never', 'Always', 'Auto'])
+ )
+
+
+def ontap_sf_host_argument_spec():
+
+ return dict(
+ hostname=dict(required=True, type='str'),
+ username=dict(required=True, type='str', aliases=['user']),
+ password=dict(required=True, type='str', aliases=['pass'], no_log=True)
+ )
+
+
+def aws_cvs_host_argument_spec():
+
+ return dict(
+ api_url=dict(required=True, type='str'),
+ validate_certs=dict(required=False, type='bool', default=True),
+ api_key=dict(required=True, type='str'),
+ secret_key=dict(required=True, type='str')
+ )
+
+
+def create_sf_connection(module, port=None):
+ hostname = module.params['hostname']
+ username = module.params['username']
+ password = module.params['password']
+
+ if HAS_SF_SDK and hostname and username and password:
+ try:
+ return_val = ElementFactory.create(hostname, username, password, port=port)
+ return return_val
+ except Exception:
+ raise Exception("Unable to create SF connection")
+ else:
+ module.fail_json(msg="the python SolidFire SDK module is required")
+
+
+def setup_na_ontap_zapi(module, vserver=None):
+ hostname = module.params['hostname']
+ username = module.params['username']
+ password = module.params['password']
+ https = module.params['https']
+ validate_certs = module.params['validate_certs']
+ port = module.params['http_port']
+ version = module.params['ontapi']
+
+ if HAS_NETAPP_LIB:
+ # set up zapi
+ server = zapi.NaServer(hostname)
+ server.set_username(username)
+ server.set_password(password)
+ if vserver:
+ server.set_vserver(vserver)
+ if version:
+ minor = version
+ else:
+ minor = 110
+ server.set_api_version(major=1, minor=minor)
+ # default is HTTP
+ if https:
+ if port is None:
+ port = 443
+ transport_type = 'HTTPS'
+ # HACK to bypass certificate verification
+ if validate_certs is False:
+ if not os.environ.get('PYTHONHTTPSVERIFY', '') and getattr(ssl, '_create_unverified_context', None):
+ ssl._create_default_https_context = ssl._create_unverified_context
+ else:
+ if port is None:
+ port = 80
+ transport_type = 'HTTP'
+ server.set_transport_type(transport_type)
+ server.set_port(port)
+ server.set_server_type('FILER')
+ return server
+ else:
+ module.fail_json(msg="the python NetApp-Lib module is required")
+
+
+def setup_ontap_zapi(module, vserver=None):
+ hostname = module.params['hostname']
+ username = module.params['username']
+ password = module.params['password']
+
+ if HAS_NETAPP_LIB:
+ # set up zapi
+ server = zapi.NaServer(hostname)
+ server.set_username(username)
+ server.set_password(password)
+ if vserver:
+ server.set_vserver(vserver)
+ # Todo : Replace hard-coded values with configurable parameters.
+ server.set_api_version(major=1, minor=110)
+ server.set_port(80)
+ server.set_server_type('FILER')
+ server.set_transport_type('HTTP')
+ return server
+ else:
+ module.fail_json(msg="the python NetApp-Lib module is required")
+
+
+def eseries_host_argument_spec():
+ """Retrieve a base argument specification common to all NetApp E-Series modules"""
+ argument_spec = basic_auth_argument_spec()
+ argument_spec.update(dict(
+ api_username=dict(type='str', required=True),
+ api_password=dict(type='str', required=True, no_log=True),
+ api_url=dict(type='str', required=True),
+ ssid=dict(type='str', required=False, default='1'),
+ validate_certs=dict(type='bool', required=False, default=True)
+ ))
+ return argument_spec
+
+
+class NetAppESeriesModule(object):
+ """Base class for all NetApp E-Series modules.
+
+ Provides a set of common methods for NetApp E-Series modules, including version checking, mode (proxy, embedded)
+ verification, http requests, secure http redirection for embedded web services, and logging setup.
+
+ Be sure to add the following lines in the module's documentation section:
+ extends_documentation_fragment:
+ - netapp.eseries
+
+ :param dict(dict) ansible_options: dictionary of ansible option definitions
+ :param str web_services_version: minimally required web services rest api version (default value: "02.00.0000.0000")
+ :param bool supports_check_mode: whether the module will support the check_mode capabilities (default=False)
+ :param list(list) mutually_exclusive: list containing list(s) of mutually exclusive options (optional)
+ :param list(list) required_if: list containing list(s) containing the option, the option value, and then
+ a list of required options. (optional)
+ :param list(list) required_one_of: list containing list(s) of options for which at least one is required. (optional)
+ :param list(list) required_together: list containing list(s) of options that are required together. (optional)
+ :param bool log_requests: controls whether to log each request (default: True)
+ """
+ DEFAULT_TIMEOUT = 60
+ DEFAULT_SECURE_PORT = "8443"
+ DEFAULT_REST_API_PATH = "devmgr/v2/"
+ DEFAULT_REST_API_ABOUT_PATH = "devmgr/utils/about"
+ DEFAULT_HEADERS = {"Content-Type": "application/json", "Accept": "application/json",
+ "netapp-client-type": "Ansible-%s" % ansible_version}
+ HTTP_AGENT = "Ansible / %s" % ansible_version
+ SIZE_UNIT_MAP = dict(bytes=1, b=1, kb=1024, mb=1024**2, gb=1024**3, tb=1024**4,
+ pb=1024**5, eb=1024**6, zb=1024**7, yb=1024**8)
+
+ def __init__(self, ansible_options, web_services_version=None, supports_check_mode=False,
+ mutually_exclusive=None, required_if=None, required_one_of=None, required_together=None,
+ log_requests=True):
+ argument_spec = eseries_host_argument_spec()
+ argument_spec.update(ansible_options)
+
+ self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=supports_check_mode,
+ mutually_exclusive=mutually_exclusive, required_if=required_if,
+ required_one_of=required_one_of, required_together=required_together)
+
+ args = self.module.params
+ self.web_services_version = web_services_version if web_services_version else "02.00.0000.0000"
+ self.ssid = args["ssid"]
+ self.url = args["api_url"]
+ self.log_requests = log_requests
+ self.creds = dict(url_username=args["api_username"],
+ url_password=args["api_password"],
+ validate_certs=args["validate_certs"])
+
+ if not self.url.endswith("/"):
+ self.url += "/"
+
+ self.is_embedded_mode = None
+ self.is_web_services_valid_cache = None
+
+ def _check_web_services_version(self):
+ """Verify proxy or embedded web services meets minimum version required for module.
+
+ The minimum required web services version is evaluated against version supplied through the web services rest
+ api. AnsibleFailJson exception will be raised when the minimum is not met or exceeded.
+
+ This helper function will update the supplied api url if secure http is not used for embedded web services
+
+ :raise AnsibleFailJson: raised when the contacted api service does not meet the minimum required version.
+ """
+ if not self.is_web_services_valid_cache:
+
+ url_parts = urlparse(self.url)
+ if not url_parts.scheme or not url_parts.netloc:
+ self.module.fail_json(msg="Failed to provide valid API URL. Example: https://192.168.1.100:8443/devmgr/v2. URL [%s]." % self.url)
+
+ if url_parts.scheme not in ["http", "https"]:
+ self.module.fail_json(msg="Protocol must be http or https. URL [%s]." % self.url)
+
+ self.url = "%s://%s/" % (url_parts.scheme, url_parts.netloc)
+ about_url = self.url + self.DEFAULT_REST_API_ABOUT_PATH
+ rc, data = request(about_url, timeout=self.DEFAULT_TIMEOUT, headers=self.DEFAULT_HEADERS, ignore_errors=True, **self.creds)
+
+ if rc != 200:
+ self.module.warn("Failed to retrieve web services about information! Retrying with secure ports. Array Id [%s]." % self.ssid)
+ self.url = "https://%s:8443/" % url_parts.netloc.split(":")[0]
+ about_url = self.url + self.DEFAULT_REST_API_ABOUT_PATH
+ try:
+ rc, data = request(about_url, timeout=self.DEFAULT_TIMEOUT, headers=self.DEFAULT_HEADERS, **self.creds)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve the webservices about information! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(error)))
+
+ major, minor, other, revision = data["version"].split(".")
+ minimum_major, minimum_minor, other, minimum_revision = self.web_services_version.split(".")
+
+ if not (major > minimum_major or
+ (major == minimum_major and minor > minimum_minor) or
+ (major == minimum_major and minor == minimum_minor and revision >= minimum_revision)):
+ self.module.fail_json(msg="Web services version does not meet minimum version required. Current version: [%s]."
+ " Version required: [%s]." % (data["version"], self.web_services_version))
+
+ self.module.log("Web services rest api version met the minimum required version.")
+ self.is_web_services_valid_cache = True
+
+ def is_embedded(self):
+ """Determine whether web services server is the embedded web services.
+
+ If web services about endpoint fails based on an URLError then the request will be attempted again using
+ secure http.
+
+ :raise AnsibleFailJson: raised when web services about endpoint failed to be contacted.
+ :return bool: whether contacted web services is running from storage array (embedded) or from a proxy.
+ """
+ self._check_web_services_version()
+
+ if self.is_embedded_mode is None:
+ about_url = self.url + self.DEFAULT_REST_API_ABOUT_PATH
+ try:
+ rc, data = request(about_url, timeout=self.DEFAULT_TIMEOUT, headers=self.DEFAULT_HEADERS, **self.creds)
+ self.is_embedded_mode = not data["runningAsProxy"]
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve the webservices about information! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(error)))
+
+ return self.is_embedded_mode
+
+ def request(self, path, data=None, method='GET', headers=None, ignore_errors=False):
+ """Issue an HTTP request to a url, retrieving an optional JSON response.
+
+ :param str path: web services rest api endpoint path (Example: storage-systems/1/graph). Note that when the
+ full url path is specified then that will be used without supplying the protocol, hostname, port and rest path.
+ :param data: data required for the request (data may be json or any python structured data)
+ :param str method: request method such as GET, POST, DELETE.
+ :param dict headers: dictionary containing request headers.
+ :param bool ignore_errors: forces the request to ignore any raised exceptions.
+ """
+ self._check_web_services_version()
+
+ if headers is None:
+ headers = self.DEFAULT_HEADERS
+
+ if not isinstance(data, str) and headers["Content-Type"] == "application/json":
+ data = json.dumps(data)
+
+ if path.startswith("/"):
+ path = path[1:]
+ request_url = self.url + self.DEFAULT_REST_API_PATH + path
+
+ if self.log_requests or True:
+ self.module.log(pformat(dict(url=request_url, data=data, method=method)))
+
+ return request(url=request_url, data=data, method=method, headers=headers, use_proxy=True, force=False, last_mod_time=None,
+ timeout=self.DEFAULT_TIMEOUT, http_agent=self.HTTP_AGENT, force_basic_auth=True, ignore_errors=ignore_errors, **self.creds)
+
+
+def create_multipart_formdata(files, fields=None, send_8kb=False):
+ """Create the data for a multipart/form request.
+
+ :param list(list) files: list of lists each containing (name, filename, path).
+ :param list(list) fields: list of lists each containing (key, value).
+ :param bool send_8kb: only sends the first 8kb of the files (default: False).
+ """
+ boundary = "---------------------------" + "".join([str(random.randint(0, 9)) for x in range(27)])
+ data_parts = list()
+ data = None
+
+ if six.PY2: # Generate payload for Python 2
+ newline = "\r\n"
+ if fields is not None:
+ for key, value in fields:
+ data_parts.extend(["--%s" % boundary,
+ 'Content-Disposition: form-data; name="%s"' % key,
+ "",
+ value])
+
+ for name, filename, path in files:
+ with open(path, "rb") as fh:
+ value = fh.read(8192) if send_8kb else fh.read()
+
+ data_parts.extend(["--%s" % boundary,
+ 'Content-Disposition: form-data; name="%s"; filename="%s"' % (name, filename),
+ "Content-Type: %s" % (mimetypes.guess_type(path)[0] or "application/octet-stream"),
+ "",
+ value])
+ data_parts.extend(["--%s--" % boundary, ""])
+ data = newline.join(data_parts)
+
+ else:
+ newline = six.b("\r\n")
+ if fields is not None:
+ for key, value in fields:
+ data_parts.extend([six.b("--%s" % boundary),
+ six.b('Content-Disposition: form-data; name="%s"' % key),
+ six.b(""),
+ six.b(value)])
+
+ for name, filename, path in files:
+ with open(path, "rb") as fh:
+ value = fh.read(8192) if send_8kb else fh.read()
+
+ data_parts.extend([six.b("--%s" % boundary),
+ six.b('Content-Disposition: form-data; name="%s"; filename="%s"' % (name, filename)),
+ six.b("Content-Type: %s" % (mimetypes.guess_type(path)[0] or "application/octet-stream")),
+ six.b(""),
+ value])
+ data_parts.extend([six.b("--%s--" % boundary), b""])
+ data = newline.join(data_parts)
+
+ headers = {
+ "Content-Type": "multipart/form-data; boundary=%s" % boundary,
+ "Content-Length": str(len(data))}
+
+ return headers, data
+
+
+def request(url, data=None, headers=None, method='GET', use_proxy=True,
+ force=False, last_mod_time=None, timeout=10, validate_certs=True,
+ url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False):
+ """Issue an HTTP request to a url, retrieving an optional JSON response."""
+
+ if headers is None:
+ headers = {"Content-Type": "application/json", "Accept": "application/json"}
+ headers.update({"netapp-client-type": "Ansible-%s" % ansible_version})
+
+ if not http_agent:
+ http_agent = "Ansible / %s" % ansible_version
+
+ try:
+ r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy,
+ force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs,
+ url_username=url_username, url_password=url_password, http_agent=http_agent,
+ force_basic_auth=force_basic_auth)
+ except HTTPError as err:
+ r = err.fp
+
+ try:
+ raw_data = r.read()
+ if raw_data:
+ data = json.loads(raw_data)
+ else:
+ raw_data = None
+ except Exception:
+ if ignore_errors:
+ pass
+ else:
+ raise Exception(raw_data)
+
+ resp_code = r.getcode()
+
+ if resp_code >= 400 and not ignore_errors:
+ raise Exception(resp_code, data)
+ else:
+ return resp_code, data
+
+
+def ems_log_event(source, server, name="Ansible", id="12345", version=ansible_version,
+ category="Information", event="setup", autosupport="false"):
+ ems_log = zapi.NaElement('ems-autosupport-log')
+ # Host name invoking the API.
+ ems_log.add_new_child("computer-name", name)
+ # ID of event. A user defined event-id, range [0..2^32-2].
+ ems_log.add_new_child("event-id", id)
+ # Name of the application invoking the API.
+ ems_log.add_new_child("event-source", source)
+ # Version of application invoking the API.
+ ems_log.add_new_child("app-version", version)
+ # Application defined category of the event.
+ ems_log.add_new_child("category", category)
+ # Description of event to log. An application defined message to log.
+ ems_log.add_new_child("event-description", event)
+ ems_log.add_new_child("log-level", "6")
+ ems_log.add_new_child("auto-support", autosupport)
+ server.invoke_successfully(ems_log, True)
+
+
+def get_cserver_zapi(server):
+ vserver_info = zapi.NaElement('vserver-get-iter')
+ query_details = zapi.NaElement.create_node_with_children('vserver-info', **{'vserver-type': 'admin'})
+ query = zapi.NaElement('query')
+ query.add_child_elem(query_details)
+ vserver_info.add_child_elem(query)
+ result = server.invoke_successfully(vserver_info,
+ enable_tunneling=False)
+ attribute_list = result.get_child_by_name('attributes-list')
+ vserver_list = attribute_list.get_child_by_name('vserver-info')
+ return vserver_list.get_child_content('vserver-name')
+
+
+def get_cserver(connection, is_rest=False):
+ if not is_rest:
+ return get_cserver_zapi(connection)
+
+ params = {'fields': 'type'}
+ api = "private/cli/vserver"
+ json, error = connection.get(api, params)
+ if json is None or error is not None:
+ # exit if there is an error or no data
+ return None
+ vservers = json.get('records')
+ if vservers is not None:
+ for vserver in vservers:
+ if vserver['type'] == 'admin': # cluster admin
+ return vserver['vserver']
+ if len(vservers) == 1: # assume vserver admin
+ return vservers[0]['vserver']
+
+ return None
+
+
+class OntapRestAPI(object):
+ def __init__(self, module, timeout=60):
+ self.module = module
+ self.username = self.module.params['username']
+ self.password = self.module.params['password']
+ self.hostname = self.module.params['hostname']
+ self.use_rest = self.module.params['use_rest']
+ self.verify = self.module.params['validate_certs']
+ self.timeout = timeout
+ self.url = 'https://' + self.hostname + '/api/'
+ self.errors = list()
+ self.debug_logs = list()
+ self.check_required_library()
+
+ def check_required_library(self):
+ if not HAS_REQUESTS:
+ self.module.fail_json(msg=missing_required_lib('requests'))
+
+ def send_request(self, method, api, params, json=None, return_status_code=False):
+ ''' send http request and process reponse, including error conditions '''
+ url = self.url + api
+ status_code = None
+ content = None
+ json_dict = None
+ json_error = None
+ error_details = None
+
+ def get_json(response):
+ ''' extract json, and error message if present '''
+ try:
+ json = response.json()
+ except ValueError:
+ return None, None
+ error = json.get('error')
+ return json, error
+
+ try:
+ response = requests.request(method, url, verify=self.verify, auth=(self.username, self.password), params=params, timeout=self.timeout, json=json)
+ content = response.content # for debug purposes
+ status_code = response.status_code
+ # If the response was successful, no Exception will be raised
+ response.raise_for_status()
+ json_dict, json_error = get_json(response)
+ except requests.exceptions.HTTPError as err:
+ __, json_error = get_json(response)
+ if json_error is None:
+ self.log_error(status_code, 'HTTP error: %s' % err)
+ error_details = str(err)
+ # If an error was reported in the json payload, it is handled below
+ except requests.exceptions.ConnectionError as err:
+ self.log_error(status_code, 'Connection error: %s' % err)
+ error_details = str(err)
+ except Exception as err:
+ self.log_error(status_code, 'Other error: %s' % err)
+ error_details = str(err)
+ if json_error is not None:
+ self.log_error(status_code, 'Endpoint error: %d: %s' % (status_code, json_error))
+ error_details = json_error
+ self.log_debug(status_code, content)
+ if return_status_code:
+ return status_code, error_details
+ return json_dict, error_details
+
+ def get(self, api, params):
+ method = 'GET'
+ return self.send_request(method, api, params)
+
+ def post(self, api, data, params=None):
+ method = 'POST'
+ return self.send_request(method, api, params, json=data)
+
+ def patch(self, api, data, params=None):
+ method = 'PATCH'
+ return self.send_request(method, api, params, json=data)
+
+ def delete(self, api, data, params=None):
+ method = 'DELETE'
+ return self.send_request(method, api, params, json=data)
+
+ def _is_rest(self, used_unsupported_rest_properties=None):
+ if self.use_rest == "Always":
+ if used_unsupported_rest_properties:
+ error = "REST API currently does not support '%s'" % \
+ ', '.join(used_unsupported_rest_properties)
+ return True, error
+ else:
+ return True, None
+ if self.use_rest == 'Never' or used_unsupported_rest_properties:
+ # force ZAPI if requested or if some parameter requires it
+ return False, None
+ method = 'HEAD'
+ api = 'cluster/software'
+ status_code, __ = self.send_request(method, api, params=None, return_status_code=True)
+ if status_code == 200:
+ return True, None
+ return False, None
+
+ def is_rest(self, used_unsupported_rest_properties=None):
+ ''' only return error if there is a reason to '''
+ use_rest, error = self._is_rest(used_unsupported_rest_properties)
+ if used_unsupported_rest_properties is None:
+ return use_rest
+ return use_rest, error
+
+ def log_error(self, status_code, message):
+ self.errors.append(message)
+ self.debug_logs.append((status_code, message))
+
+ def log_debug(self, status_code, content):
+ self.debug_logs.append((status_code, content))
+
+
+class AwsCvsRestAPI(object):
+ def __init__(self, module, timeout=60):
+ self.module = module
+ self.api_key = self.module.params['api_key']
+ self.secret_key = self.module.params['secret_key']
+ self.api_url = self.module.params['api_url']
+ self.verify = self.module.params['validate_certs']
+ self.timeout = timeout
+ self.url = 'https://' + self.api_url + '/v1/'
+ self.check_required_library()
+
+ def check_required_library(self):
+ if not HAS_REQUESTS:
+ self.module.fail_json(msg=missing_required_lib('requests'))
+
+ def send_request(self, method, api, params, json=None):
+ ''' send http request and process reponse, including error conditions '''
+ url = self.url + api
+ status_code = None
+ content = None
+ json_dict = None
+ json_error = None
+ error_details = None
+ headers = {
+ 'Content-type': "application/json",
+ 'api-key': self.api_key,
+ 'secret-key': self.secret_key,
+ 'Cache-Control': "no-cache",
+ }
+
+ def get_json(response):
+ ''' extract json, and error message if present '''
+ try:
+ json = response.json()
+
+ except ValueError:
+ return None, None
+ success_code = [200, 201, 202]
+ if response.status_code not in success_code:
+ error = json.get('message')
+ else:
+ error = None
+ return json, error
+ try:
+ response = requests.request(method, url, headers=headers, timeout=self.timeout, json=json)
+ status_code = response.status_code
+ # If the response was successful, no Exception will be raised
+ json_dict, json_error = get_json(response)
+ except requests.exceptions.HTTPError as err:
+ __, json_error = get_json(response)
+ if json_error is None:
+ error_details = str(err)
+ except requests.exceptions.ConnectionError as err:
+ error_details = str(err)
+ except Exception as err:
+ error_details = str(err)
+ if json_error is not None:
+ error_details = json_error
+
+ return json_dict, error_details
+
+ # If an error was reported in the json payload, it is handled below
+ def get(self, api, params=None):
+ method = 'GET'
+ return self.send_request(method, api, params)
+
+ def post(self, api, data, params=None):
+ method = 'POST'
+ return self.send_request(method, api, params, json=data)
+
+ def patch(self, api, data, params=None):
+ method = 'PATCH'
+ return self.send_request(method, api, params, json=data)
+
+ def put(self, api, data, params=None):
+ method = 'PUT'
+ return self.send_request(method, api, params, json=data)
+
+ def delete(self, api, data, params=None):
+ method = 'DELETE'
+ return self.send_request(method, api, params, json=data)
+
+ def get_state(self, jobId):
+ """ Method to get the state of the job """
+ method = 'GET'
+ response, status_code = self.get('Jobs/%s' % jobId)
+ while str(response['state']) not in 'done':
+ response, status_code = self.get('Jobs/%s' % jobId)
+ return 'done'
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/module_utils/santricity.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/module_utils/santricity.py
new file mode 100644
index 00000000..0e890001
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/module_utils/santricity.py
@@ -0,0 +1,431 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import json
+import random
+import mimetypes
+
+from pprint import pformat
+from ansible.module_utils import six
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+from ansible.module_utils.six.moves.urllib.error import HTTPError, URLError
+from ansible.module_utils.urls import open_url
+from ansible.module_utils.api import basic_auth_argument_spec
+from ansible.module_utils._text import to_native
+try:
+ from ansible.module_utils.ansible_release import __version__ as ansible_version
+except ImportError:
+ ansible_version = 'unknown'
+
+try:
+ from urlparse import urlparse, urlunparse
+except ImportError:
+ from urllib.parse import urlparse, urlunparse
+
+
+def eseries_host_argument_spec():
+ """Retrieve a base argument specification common to all NetApp E-Series modules"""
+ argument_spec = basic_auth_argument_spec()
+ argument_spec.update(dict(
+ api_username=dict(type="str", required=True),
+ api_password=dict(type="str", required=True, no_log=True),
+ api_url=dict(type="str", required=True),
+ ssid=dict(type="str", required=False, default="1"),
+ validate_certs=dict(type="bool", required=False, default=True)
+ ))
+ return argument_spec
+
+
+def eseries_proxy_argument_spec():
+ """Retrieve a base argument specification common to all NetApp E-Series modules for proxy specific tasks"""
+ argument_spec = basic_auth_argument_spec()
+ argument_spec.update(dict(
+ api_username=dict(type="str", required=True),
+ api_password=dict(type="str", required=True, no_log=True),
+ api_url=dict(type="str", required=True),
+ validate_certs=dict(type="bool", required=False, default=True)
+ ))
+ return argument_spec
+
+
+class NetAppESeriesModule(object):
+ """Base class for all NetApp E-Series modules.
+
+ Provides a set of common methods for NetApp E-Series modules, including version checking, mode (proxy, embedded)
+ verification, http requests, secure http redirection for embedded web services, and logging setup.
+
+ Be sure to add the following lines in the module's documentation section:
+ extends_documentation_fragment:
+ - santricity
+
+ :param dict(dict) ansible_options: dictionary of ansible option definitions
+ :param str web_services_version: minimally required web services rest api version (default value: "02.00.0000.0000")
+ :param bool supports_check_mode: whether the module will support the check_mode capabilities (default=False)
+ :param list(list) mutually_exclusive: list containing list(s) of mutually exclusive options (optional)
+ :param list(list) required_if: list containing list(s) containing the option, the option value, and then a list of required options. (optional)
+ :param list(list) required_one_of: list containing list(s) of options for which at least one is required. (optional)
+ :param list(list) required_together: list containing list(s) of options that are required together. (optional)
+ :param bool log_requests: controls whether to log each request (default: True)
+ :param bool proxy_specific_task: controls whether ssid is a default option (default: False)
+ """
+ DEFAULT_TIMEOUT = 300
+ DEFAULT_SECURE_PORT = "8443"
+ DEFAULT_BASE_PATH = "devmgr/"
+ DEFAULT_REST_API_PATH = "devmgr/v2/"
+ DEFAULT_REST_API_ABOUT_PATH = "devmgr/utils/about"
+ DEFAULT_HEADERS = {"Content-Type": "application/json", "Accept": "application/json",
+ "netapp-client-type": "Ansible-%s" % ansible_version}
+ HTTP_AGENT = "Ansible / %s" % ansible_version
+ SIZE_UNIT_MAP = dict(bytes=1, b=1, kb=1024, mb=1024**2, gb=1024**3, tb=1024**4,
+ pb=1024**5, eb=1024**6, zb=1024**7, yb=1024**8)
+
+ HOST_TYPE_INDEXES = {"aix mpio": 9, "avt 4m": 5, "hp-ux": 15, "linux atto": 24, "linux dm-mp": 28, "linux pathmanager": 25, "solaris 10 or earlier": 2,
+ "solaris 11 or later": 17, "svc": 18, "ontap": 26, "mac": 22, "vmware": 10, "windows": 1, "windows atto": 23, "windows clustered": 8}
+
+ def __init__(self, ansible_options, web_services_version=None, supports_check_mode=False,
+ mutually_exclusive=None, required_if=None, required_one_of=None, required_together=None,
+ log_requests=True, proxy_specific_task=False):
+
+ if proxy_specific_task:
+ argument_spec = eseries_proxy_argument_spec()
+ else:
+ argument_spec = eseries_host_argument_spec()
+
+ argument_spec.update(ansible_options)
+
+ self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=supports_check_mode,
+ mutually_exclusive=mutually_exclusive, required_if=required_if,
+ required_one_of=required_one_of, required_together=required_together)
+
+ args = self.module.params
+ self.web_services_version = web_services_version if web_services_version else "02.00.0000.0000"
+
+ if proxy_specific_task:
+ self.ssid = "0"
+ else:
+ self.ssid = args["ssid"]
+ self.url = args["api_url"]
+ self.log_requests = log_requests
+ self.creds = dict(url_username=args["api_username"],
+ url_password=args["api_password"],
+ validate_certs=args["validate_certs"])
+
+ if not self.url.endswith("/"):
+ self.url += "/"
+
+ self.is_proxy_used_cache = None
+ self.is_embedded_available_cache = None
+ self.is_web_services_valid_cache = None
+
+ def _check_web_services_version(self):
+ """Verify proxy or embedded web services meets minimum version required for module.
+
+ The minimum required web services version is evaluated against version supplied through the web services rest
+ api. AnsibleFailJson exception will be raised when the minimum is not met or exceeded.
+
+ This helper function will update the supplied api url if secure http is not used for embedded web services
+
+ :raise AnsibleFailJson: raised when the contacted api service does not meet the minimum required version.
+ """
+ if not self.is_web_services_valid_cache:
+
+ url_parts = urlparse(self.url)
+ if not url_parts.scheme or not url_parts.netloc:
+ self.module.fail_json(msg="Failed to provide valid API URL. Example: https://192.168.1.100:8443/devmgr/v2. URL [%s]." % self.url)
+
+ if url_parts.scheme not in ["http", "https"]:
+ self.module.fail_json(msg="Protocol must be http or https. URL [%s]." % self.url)
+
+ self.url = "%s://%s/" % (url_parts.scheme, url_parts.netloc)
+ about_url = self.url + self.DEFAULT_REST_API_ABOUT_PATH
+ rc, data = request(about_url, timeout=self.DEFAULT_TIMEOUT, headers=self.DEFAULT_HEADERS, ignore_errors=True, force_basic_auth=False, **self.creds)
+
+ if rc != 200:
+ self.module.warn("Failed to retrieve web services about information! Retrying with secure ports. Array Id [%s]." % self.ssid)
+ self.url = "https://%s:8443/" % url_parts.netloc.split(":")[0]
+ about_url = self.url + self.DEFAULT_REST_API_ABOUT_PATH
+ try:
+ rc, data = request(about_url, timeout=self.DEFAULT_TIMEOUT, headers=self.DEFAULT_HEADERS, **self.creds)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve the webservices about information! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(error)))
+
+ if len(data["version"].split(".")) == 4:
+ major, minor, other, revision = data["version"].split(".")
+ minimum_major, minimum_minor, other, minimum_revision = self.web_services_version.split(".")
+
+ if not (major > minimum_major or
+ (major == minimum_major and minor > minimum_minor) or
+ (major == minimum_major and minor == minimum_minor and revision >= minimum_revision)):
+ self.module.fail_json(msg="Web services version does not meet minimum version required. Current version: [%s]."
+ " Version required: [%s]." % (data["version"], self.web_services_version))
+ self.module.log("Web services rest api version met the minimum required version.")
+ else:
+ self.module.warn("Web services rest api version unknown!")
+
+ self.is_web_services_valid_cache = True
+
+ def is_web_services_version_met(self, version):
+ """Determines whether a particular web services version has been satisfied."""
+ split_version = version.split(".")
+ if len(split_version) != 4 or not split_version[0].isdigit() or not split_version[1].isdigit() or not split_version[3].isdigit():
+ self.module.fail_json(msg="Version is not a valid Web Services version. Version [%s]." % version)
+
+ url_parts = urlparse(self.url)
+ if not url_parts.scheme or not url_parts.netloc:
+ self.module.fail_json(msg="Failed to provide valid API URL. Example: https://192.168.1.100:8443/devmgr/v2. URL [%s]." % self.url)
+
+ if url_parts.scheme not in ["http", "https"]:
+ self.module.fail_json(msg="Protocol must be http or https. URL [%s]." % self.url)
+
+ self.url = "%s://%s/" % (url_parts.scheme, url_parts.netloc)
+ about_url = self.url + self.DEFAULT_REST_API_ABOUT_PATH
+ rc, data = request(about_url, timeout=self.DEFAULT_TIMEOUT, headers=self.DEFAULT_HEADERS, ignore_errors=True, **self.creds)
+
+ if rc != 200:
+ self.module.warn("Failed to retrieve web services about information! Retrying with secure ports. Array Id [%s]." % self.ssid)
+ self.url = "https://%s:8443/" % url_parts.netloc.split(":")[0]
+ about_url = self.url + self.DEFAULT_REST_API_ABOUT_PATH
+ try:
+ rc, data = request(about_url, timeout=self.DEFAULT_TIMEOUT, headers=self.DEFAULT_HEADERS, **self.creds)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve the webservices about information! Array Id [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ if len(data["version"].split(".")) == 4:
+ major, minor, other, revision = data["version"].split(".")
+ minimum_major, minimum_minor, other, minimum_revision = split_version
+ if not (major > minimum_major or
+ (major == minimum_major and minor > minimum_minor) or
+ (major == minimum_major and minor == minimum_minor and revision >= minimum_revision)):
+ return False
+ else:
+ return False
+ return True
+
+ def is_embedded_available(self):
+ """Determine whether the storage array has embedded services available."""
+ self._check_web_services_version()
+
+ if self.is_embedded_available_cache is None:
+
+ if self.is_proxy():
+ if self.ssid == "0" or self.ssid.lower() == "proxy":
+ self.is_embedded_available_cache = False
+ else:
+ try:
+ rc, bundle = self.request("storage-systems/%s/graph/xpath-filter?query=/sa/saData/extendedSAData/codeVersions[codeModule='bundle']"
+ % self.ssid)
+ self.is_embedded_available_cache = False
+ if bundle:
+ self.is_embedded_available_cache = True
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve information about storage system [%s]. Error [%s]." % (self.ssid, to_native(error)))
+ else: # Contacted using embedded web services
+ self.is_embedded_available_cache = True
+
+ self.module.log("embedded_available: [%s]" % ("True" if self.is_embedded_available_cache else "False"))
+ return self.is_embedded_available_cache
+
+ def is_embedded(self):
+ """Determine whether web services server is the embedded web services."""
+ return not self.is_proxy()
+
+ def is_proxy(self):
+ """Determine whether web services server is the proxy web services.
+
+ :raise AnsibleFailJson: raised when web services about endpoint failed to be contacted.
+ :return bool: whether contacted web services is running from storage array (embedded) or from a proxy.
+ """
+ self._check_web_services_version()
+
+ if self.is_proxy_used_cache is None:
+ about_url = self.url + self.DEFAULT_REST_API_ABOUT_PATH
+ try:
+ rc, data = request(about_url, timeout=self.DEFAULT_TIMEOUT, headers=self.DEFAULT_HEADERS, force_basic_auth=False, **self.creds)
+ self.is_proxy_used_cache = data["runningAsProxy"]
+
+ self.module.log("proxy: [%s]" % ("True" if self.is_proxy_used_cache else "False"))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve the webservices about information! Array Id [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ return self.is_proxy_used_cache
+
+ def request(self, path, rest_api_path=DEFAULT_REST_API_PATH, rest_api_url=None, data=None, method='GET', headers=None, ignore_errors=False, timeout=None,
+ force_basic_auth=True, log_request=None):
+ """Issue an HTTP request to a url, retrieving an optional JSON response.
+
+ :param str path: web services rest api endpoint path (Example: storage-systems/1/graph). Note that when the
+ full url path is specified then that will be used without supplying the protocol, hostname, port and rest path.
+ :param str rest_api_path: override the class DEFAULT_REST_API_PATH which is used to build the request URL.
+ :param str rest_api_url: override the class url member which contains the base url for web services.
+ :param data: data required for the request (data may be json or any python structured data)
+ :param str method: request method such as GET, POST, DELETE.
+ :param dict headers: dictionary containing request headers.
+ :param bool ignore_errors: forces the request to ignore any raised exceptions.
+ :param int timeout: duration of seconds before request finally times out.
+ :param bool force_basic_auth: Ensure that basic authentication is being used.
+ :param bool log_request: Log the request and response
+ """
+ self._check_web_services_version()
+
+ if rest_api_url is None:
+ rest_api_url = self.url
+ if headers is None:
+ headers = self.DEFAULT_HEADERS
+ if timeout is None:
+ timeout = self.DEFAULT_TIMEOUT
+ if log_request is None:
+ log_request = self.log_requests
+
+ if not isinstance(data, str) and "Content-Type" in headers and headers["Content-Type"] == "application/json":
+ data = json.dumps(data)
+
+ if path.startswith("/"):
+ path = path[1:]
+ request_url = rest_api_url + rest_api_path + path
+
+ if log_request:
+ self.module.log(pformat(dict(url=request_url, data=data, method=method, headers=headers)))
+
+ response = self._request(url=request_url, data=data, method=method, headers=headers, last_mod_time=None, timeout=timeout,
+ http_agent=self.HTTP_AGENT, force_basic_auth=force_basic_auth, ignore_errors=ignore_errors, **self.creds)
+ if log_request:
+ self.module.log(pformat(response))
+
+ return response
+
+ @staticmethod
+ def _request(url, data=None, headers=None, method='GET', use_proxy=True, force=False, last_mod_time=None, timeout=10, validate_certs=True,
+ url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False):
+ """Issue an HTTP request to a url, retrieving an optional JSON response."""
+
+ if headers is None:
+ headers = {"Content-Type": "application/json", "Accept": "application/json"}
+ headers.update({"netapp-client-type": "Ansible-%s" % ansible_version})
+
+ if not http_agent:
+ http_agent = "Ansible / %s" % ansible_version
+
+ try:
+ r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, force=force, last_mod_time=last_mod_time, timeout=timeout,
+ validate_certs=validate_certs, url_username=url_username, url_password=url_password, http_agent=http_agent,
+ force_basic_auth=force_basic_auth)
+ rc = r.getcode()
+ response = r.read()
+ if response:
+ response = json.loads(response)
+
+ except HTTPError as error:
+ rc = error.code
+ response = error.fp.read()
+ try:
+ response = json.loads(response)
+ except Exception:
+ pass
+
+ if not ignore_errors:
+ raise Exception(rc, response)
+
+ return rc, response
+
+
+def create_multipart_formdata(files, fields=None, send_8kb=False):
+ """Create the data for a multipart/form request.
+
+ :param list(list) files: list of lists each containing (name, filename, path).
+ :param list(list) fields: list of lists each containing (key, value).
+ :param bool send_8kb: only sends the first 8kb of the files (default: False).
+ """
+ boundary = "---------------------------" + "".join([str(random.randint(0, 9)) for x in range(27)])
+ data_parts = list()
+ data = None
+
+ if six.PY2: # Generate payload for Python 2
+ newline = "\r\n"
+ if fields is not None:
+ for key, value in fields:
+ data_parts.extend(["--%s" % boundary,
+ 'Content-Disposition: form-data; name="%s"' % key,
+ "",
+ value])
+
+ for name, filename, path in files:
+ with open(path, "rb") as fh:
+ value = fh.read(8192) if send_8kb else fh.read()
+
+ data_parts.extend(["--%s" % boundary,
+ 'Content-Disposition: form-data; name="%s"; filename="%s"' % (name, filename),
+ "Content-Type: %s" % (mimetypes.guess_type(path)[0] or "application/octet-stream"),
+ "",
+ value])
+ data_parts.extend(["--%s--" % boundary, ""])
+ data = newline.join(data_parts)
+
+ else:
+ newline = six.b("\r\n")
+ if fields is not None:
+ for key, value in fields:
+ data_parts.extend([six.b("--%s" % boundary),
+ six.b('Content-Disposition: form-data; name="%s"' % key),
+ six.b(""),
+ six.b(value)])
+
+ for name, filename, path in files:
+ with open(path, "rb") as fh:
+ value = fh.read(8192) if send_8kb else fh.read()
+
+ data_parts.extend([six.b("--%s" % boundary),
+ six.b('Content-Disposition: form-data; name="%s"; filename="%s"' % (name, filename)),
+ six.b("Content-Type: %s" % (mimetypes.guess_type(path)[0] or "application/octet-stream")),
+ six.b(""),
+ value])
+ data_parts.extend([six.b("--%s--" % boundary), b""])
+ data = newline.join(data_parts)
+
+ headers = {
+ "Content-Type": "multipart/form-data; boundary=%s" % boundary,
+ "Content-Length": str(len(data))}
+
+ return headers, data
+
+
+def request(url, data=None, headers=None, method='GET', use_proxy=True,
+ force=False, last_mod_time=None, timeout=10, validate_certs=True,
+ url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False):
+ """Issue an HTTP request to a url, retrieving an optional JSON response."""
+
+ if headers is None:
+ headers = {"Content-Type": "application/json", "Accept": "application/json"}
+ headers.update({"netapp-client-type": "Ansible-%s" % ansible_version})
+
+ if not http_agent:
+ http_agent = "Ansible / %s" % ansible_version
+
+ try:
+ r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy,
+ force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs,
+ url_username=url_username, url_password=url_password, http_agent=http_agent,
+ force_basic_auth=force_basic_auth)
+ except HTTPError as err:
+ r = err.fp
+
+ try:
+ raw_data = r.read()
+ if raw_data:
+ data = json.loads(raw_data)
+ else:
+ raw_data = None
+ except Exception:
+ if ignore_errors:
+ pass
+ else:
+ raise Exception(raw_data)
+
+ resp_code = r.getcode()
+
+ if resp_code >= 400 and not ignore_errors:
+ raise Exception(resp_code, data)
+ else:
+ return resp_code, data
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_alerts.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_alerts.py
new file mode 100644
index 00000000..68abc2fb
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_alerts.py
@@ -0,0 +1,253 @@
+#!/usr/bin/python
+
+# (c) 2018, 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_santricity_alerts
+short_description: NetApp E-Series manage email notification settings
+description:
+ - Certain E-Series systems have the capability to send email notifications on potentially critical events.
+ - This module will allow the owner of the system to specify email recipients for these messages.
+author: Michael Price (@lmprice)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ state:
+ description:
+ - Enable/disable the sending of email-based alerts.
+ type: str
+ default: enabled
+ required: false
+ choices:
+ - enabled
+ - disabled
+ server:
+ description:
+ - A fully qualified domain name, IPv4 address, or IPv6 address of a mail server.
+ - To use a fully qualified domain name, you must configure a DNS server on both controllers using
+ M(netapp_eseries.santricity.na_santricity_mgmt_interface).
+ - Required when I(state=enabled).
+ type: str
+ required: false
+ sender:
+ description:
+ - This is the sender that the recipient will see. It doesn't necessarily need to be a valid email account.
+ - Required when I(state=enabled).
+ type: str
+ required: false
+ contact:
+ description:
+ - Allows the owner to specify some free-form contact information to be included in the emails.
+ - This is typically utilized to provide a contact phone number.
+ type: str
+ required: false
+ recipients:
+ description:
+ - The email addresses that will receive the email notifications.
+ - Required when I(state=enabled).
+ type: list
+ required: false
+ test:
+ description:
+ - When a change is detected in the configuration, a test email will be sent.
+ - This may take a few minutes to process.
+ - Only applicable if I(state=enabled).
+ type: bool
+ default: false
+notes:
+ - Check mode is supported.
+ - Alertable messages are a subset of messages shown by the Major Event Log (MEL), of the storage-system. Examples
+ of alertable messages include drive failures, failed controllers, loss of redundancy, and other warning/critical
+ events.
+ - This API is currently only supported with the Embedded Web Services API v2.0 and higher.
+"""
+
+EXAMPLES = """
+ - name: Enable email-based alerting
+ na_santricity_alerts:
+ state: enabled
+ sender: noreply@example.com
+ server: mail@example.com
+ contact: "Phone: 1-555-555-5555"
+ recipients:
+ - name1@example.com
+ - name2@example.com
+ api_url: "10.1.1.1:8443"
+ api_username: "admin"
+ api_password: "myPass"
+
+ - name: Disable alerting
+ na_santricity_alerts:
+ state: disabled
+ api_url: "10.1.1.1:8443"
+ api_username: "admin"
+ api_password: "myPass"
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The settings have been updated.
+"""
+import re
+
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+
+class NetAppESeriesAlerts(NetAppESeriesModule):
+ def __init__(self):
+ ansible_options = dict(state=dict(type='str', required=False, default='enabled', choices=['enabled', 'disabled']),
+ server=dict(type='str', required=False),
+ sender=dict(type='str', required=False),
+ contact=dict(type='str', required=False),
+ recipients=dict(type='list', required=False),
+ test=dict(type='bool', required=False, default=False))
+
+ required_if = [['state', 'enabled', ['server', 'sender', 'recipients']]]
+ super(NetAppESeriesAlerts, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ required_if=required_if,
+ supports_check_mode=True)
+
+ args = self.module.params
+ self.alerts = args['state'] == 'enabled'
+ self.server = args['server']
+ self.sender = args['sender']
+ self.contact = args['contact']
+ self.recipients = args['recipients']
+ self.test = args['test']
+ self.check_mode = self.module.check_mode
+
+ # Very basic validation on email addresses: xx@yy.zz
+ email = re.compile(r"[^@]+@[^@]+\.[^@]+")
+
+ if self.sender and not email.match(self.sender):
+ self.module.fail_json(msg="The sender (%s) provided is not a valid email address." % self.sender)
+
+ if self.recipients is not None:
+ for recipient in self.recipients:
+ if not email.match(recipient):
+ self.module.fail_json(msg="The recipient (%s) provided is not a valid email address." % recipient)
+
+ if len(self.recipients) < 1:
+ self.module.fail_json(msg="At least one recipient address must be specified.")
+
+ def get_configuration(self):
+ """Retrieve the current storage system alert settings."""
+ if self.is_proxy():
+ if self.is_embedded_available():
+ try:
+ rc, result = self.request("storage-systems/%s/forward/devmgr/v2/storage-systems/1/device-alerts" % self.ssid)
+ return result
+ except Exception as err:
+ self.module.fail_json(msg="Failed to retrieve the alerts configuration! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+ else:
+ self.module.fail_json(msg="Setting SANtricity alerts is only available from SANtricity Web Services Proxy if the storage system has"
+ " SANtricity Web Services Embedded available. Array [%s]." % self.ssid)
+ else:
+ try:
+ rc, result = self.request("storage-systems/%s/device-alerts" % self.ssid)
+ return result
+ except Exception as err:
+ self.module.fail_json(msg="Failed to retrieve the alerts configuration! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ def update_configuration(self):
+ """Update the storage system alert settings."""
+ config = self.get_configuration()
+ update = False
+ body = dict()
+
+ if self.alerts:
+ body = dict(alertingEnabled=True)
+ if not config['alertingEnabled']:
+ update = True
+
+ body.update(emailServerAddress=self.server)
+ if config['emailServerAddress'] != self.server:
+ update = True
+
+ body.update(additionalContactInformation=self.contact, sendAdditionalContactInformation=True)
+ if self.contact and (self.contact != config['additionalContactInformation']
+ or not config['sendAdditionalContactInformation']):
+ update = True
+
+ body.update(emailSenderAddress=self.sender)
+ if config['emailSenderAddress'] != self.sender:
+ update = True
+
+ self.recipients.sort()
+ if config['recipientEmailAddresses']:
+ config['recipientEmailAddresses'].sort()
+
+ body.update(recipientEmailAddresses=self.recipients)
+ if config['recipientEmailAddresses'] != self.recipients:
+ update = True
+
+ elif config['alertingEnabled']:
+ body = {"alertingEnabled": False, "emailServerAddress": "", "emailSenderAddress": "", "sendAdditionalContactInformation": False,
+ "additionalContactInformation": "", "recipientEmailAddresses": []}
+ update = True
+
+ if update and not self.check_mode:
+ if self.is_proxy() and self.is_embedded_available():
+ try:
+ rc, result = self.request("storage-systems/%s/forward/devmgr/v2/storage-systems/1/device-alerts" % self.ssid, method="POST", data=body)
+ except Exception as err:
+ self.module.fail_json(msg="We failed to set the storage-system name! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ else:
+ try:
+ rc, result = self.request("storage-systems/%s/device-alerts" % self.ssid, method="POST", data=body)
+ except Exception as err:
+ self.module.fail_json(msg="We failed to set the storage-system name! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ return update
+
+ def send_test_email(self):
+ """Send a test email to verify that the provided configuration is valid and functional."""
+ if not self.check_mode:
+ if self.is_proxy() and self.is_embedded_available():
+ try:
+ rc, resp = self.request("storage-systems/%s/forward/devmgr/v2/storage-systems/1/device-alerts/alert-email-test" % self.ssid, method="POST")
+ if resp['response'] != 'emailSentOK':
+ self.module.fail_json(msg="The test email failed with status=[%s]! Array Id [%s]." % (resp['response'], self.ssid))
+ except Exception as err:
+ self.module.fail_json(msg="We failed to send the test email! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ else:
+ try:
+ rc, resp = self.request("storage-systems/%s/device-alerts/alert-email-test" % self.ssid, method="POST")
+ if resp['response'] != 'emailSentOK':
+ self.module.fail_json(msg="The test email failed with status=[%s]! Array Id [%s]." % (resp['response'], self.ssid))
+ except Exception as err:
+ self.module.fail_json(msg="We failed to send the test email! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ def update(self):
+ update = self.update_configuration()
+
+ if self.test and update:
+ self.send_test_email()
+
+ if self.alerts:
+ msg = 'Alerting has been enabled using server=%s, sender=%s.' % (self.server, self.sender)
+ else:
+ msg = 'Alerting has been disabled.'
+
+ self.module.exit_json(msg=msg, changed=update)
+
+
+def main():
+ alerts = NetAppESeriesAlerts()
+ alerts.update()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_alerts_syslog.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_alerts_syslog.py
new file mode 100644
index 00000000..e7c92557
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_alerts_syslog.py
@@ -0,0 +1,176 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_alerts_syslog
+short_description: NetApp E-Series manage syslog servers receiving storage system alerts.
+description:
+ - Manage the list of syslog servers that will notifications on potentially critical events.
+author: Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ servers:
+ description:
+ - List of dictionaries where each dictionary contains a syslog server entry.
+ type: list
+ required: False
+ suboptions:
+ address:
+ description:
+ - Syslog server address can be a fully qualified domain name, IPv4 address, or IPv6 address.
+ required: true
+ port:
+ description:
+ - UDP Port must be a numerical value between 0 and 65535. Typically, the UDP Port for syslog is 514.
+ required: false
+ default: 514
+ test:
+ description:
+ - This forces a test syslog message to be sent to the stated syslog server.
+ - Test will only be issued when a change is made.
+ type: bool
+ default: false
+notes:
+ - Check mode is supported.
+ - This API is currently only supported with the Embedded Web Services API v2.12 (bundled with
+ SANtricity OS 11.40.2) and higher.
+"""
+
+EXAMPLES = """
+ - name: Add two syslog server configurations to NetApp E-Series storage array.
+ na_santricity_alerts_syslog:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ servers:
+ - address: "192.168.1.100"
+ - address: "192.168.2.100"
+ port: 514
+ - address: "192.168.3.100"
+ port: 1000
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The settings have been updated.
+"""
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+
+class NetAppESeriesAlertsSyslog(NetAppESeriesModule):
+ def __init__(self):
+ ansible_options = dict(servers=dict(type="list", required=False),
+ test=dict(type="bool", default=False, require=False))
+
+ required_if = [["state", "present", ["address"]]]
+ mutually_exclusive = [["test", "absent"]]
+ super(NetAppESeriesAlertsSyslog, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ mutually_exclusive=mutually_exclusive,
+ required_if=required_if,
+ supports_check_mode=True)
+ args = self.module.params
+ if args["servers"] and len(args["servers"]) > 5:
+ self.module.fail_json(msg="Maximum number of syslog servers is 5! Array Id [%s]." % self.ssid)
+
+ self.servers = {}
+ if args["servers"] is not None:
+ for server in args["servers"]:
+ port = 514
+ if "port" in server:
+ port = server["port"]
+ self.servers.update({server["address"]: port})
+
+ self.test = args["test"]
+ self.check_mode = self.module.check_mode
+
+ # Check whether request needs to be forwarded on to the controller web services rest api.
+ self.url_path_prefix = ""
+ if not self.is_embedded() and self.ssid != 0:
+ self.url_path_prefix = "storage-systems/%s/forward/devmgr/v2/" % self.ssid
+
+ def get_current_configuration(self):
+ """Retrieve existing alert-syslog configuration."""
+ try:
+ rc, result = self.request(self.url_path_prefix + "storage-systems/%s/device-alerts/alert-syslog" % ("1" if self.url_path_prefix else self.ssid))
+ return result
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve syslog configuration! Array Id [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ def is_change_required(self):
+ """Determine whether changes are required."""
+ current_config = self.get_current_configuration()
+
+ # When syslog servers should exist, search for them.
+ if self.servers:
+ for entry in current_config["syslogReceivers"]:
+ if entry["serverName"] not in self.servers.keys() or entry["portNumber"] != self.servers[entry["serverName"]]:
+ return True
+
+ for server, port in self.servers.items():
+ for entry in current_config["syslogReceivers"]:
+ if server == entry["serverName"] and port == entry["portNumber"]:
+ break
+ else:
+ return True
+ return False
+
+ elif current_config["syslogReceivers"]:
+ return True
+
+ return False
+
+ def make_request_body(self):
+ """Generate the request body."""
+ body = {"syslogReceivers": [], "defaultFacility": 3, "defaultTag": "StorageArray"}
+
+ for server, port in self.servers.items():
+ body["syslogReceivers"].append({"serverName": server, "portNumber": port})
+
+ return body
+
+ def test_configuration(self):
+ """Send syslog test message to all systems (only option)."""
+ try:
+ rc, result = self.request(self.url_path_prefix + "storage-systems/%s/device-alerts/alert-syslog-test"
+ % ("1" if self.url_path_prefix else self.ssid), method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to send test message! Array Id [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ def update(self):
+ """Update configuration and respond to ansible."""
+ change_required = self.is_change_required()
+
+ if change_required and not self.check_mode:
+ try:
+ rc, result = self.request(self.url_path_prefix + "storage-systems/%s/device-alerts/alert-syslog" % ("1" if self.url_path_prefix else self.ssid),
+ method="POST", data=self.make_request_body())
+ except Exception as error:
+ self.module.fail_json(msg="Failed to add syslog server! Array Id [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ if self.test and self.servers:
+ self.test_configuration()
+
+ self.module.exit_json(msg="The syslog settings have been updated.", changed=change_required)
+
+
+def main():
+ settings = NetAppESeriesAlertsSyslog()
+ settings.update()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_asup.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_asup.py
new file mode 100644
index 00000000..c6922d80
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_asup.py
@@ -0,0 +1,521 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_asup
+short_description: NetApp E-Series manage auto-support settings
+description:
+ - Allow the auto-support settings to be configured for an individual E-Series storage-system
+author:
+ - Michael Price (@lmprice)
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ state:
+ description:
+ - Enable/disable the E-Series auto-support configuration or maintenance mode.
+ - When this option is enabled, configuration, logs, and other support-related information will be relayed
+ to NetApp to help better support your system. No personally identifiable information, passwords, etc, will
+ be collected.
+ - The maintenance state enables the maintenance window which allows maintenance activities to be performed on the storage array without
+ generating support cases.
+ - Maintenance mode cannot be enabled unless ASUP has previously been enabled.
+ type: str
+ default: enabled
+ choices:
+ - enabled
+ - disabled
+ - maintenance_enabled
+ - maintenance_disabled
+ active:
+ description:
+ - Enable active/proactive monitoring for ASUP. When a problem is detected by our monitoring systems, it's
+ possible that the bundle did not contain all of the required information at the time of the event.
+ Enabling this option allows NetApp support personnel to manually request transmission or re-transmission
+ of support data in order ot resolve the problem.
+ - Only applicable if I(state=enabled).
+ default: true
+ type: bool
+ start:
+ description:
+ - A start hour may be specified in a range from 0 to 23 hours.
+ - ASUP bundles will be sent daily between the provided start and end time (UTC).
+ - I(start) must be less than I(end).
+ type: int
+ default: 0
+ end:
+ description:
+ - An end hour may be specified in a range from 1 to 24 hours.
+ - ASUP bundles will be sent daily between the provided start and end time (UTC).
+ - I(start) must be less than I(end).
+ type: int
+ default: 24
+ days:
+ description:
+ - A list of days of the week that ASUP bundles will be sent. A larger, weekly bundle will be sent on one
+ of the provided days.
+ type: list
+ choices:
+ - monday
+ - tuesday
+ - wednesday
+ - thursday
+ - friday
+ - saturday
+ - sunday
+ required: false
+ aliases:
+ - schedule_days
+ - days_of_week
+ method:
+ description:
+ - AutoSupport dispatch delivery method.
+ choices:
+ - https
+ - http
+ - email
+ type: str
+ required: false
+ default: https
+ routing_type:
+ description:
+ - AutoSupport routing
+ - Required when M(method==https or method==http).
+ choices:
+ - direct
+ - proxy
+ - script
+ type: str
+ default: direct
+ required: false
+ proxy:
+ description:
+ - Information particular to the proxy delivery method.
+ - Required when M((method==https or method==http) and routing_type==proxy).
+ type: dict
+ required: false
+ suboptions:
+ host:
+ description:
+ - Proxy host IP address or fully qualified domain name.
+ - Required when M(method==http or method==https) and M(routing_type==proxy).
+ type: str
+ required: false
+ port:
+ description:
+ - Proxy host port.
+ - Required when M(method==http or method==https) and M(routing_type==proxy).
+ type: str
+ required: false
+ script:
+ description:
+ - Path to the AutoSupport routing script file.
+ - Required when M(method==http or method==https) and M(routing_type==script).
+ type: str
+ required: false
+ email:
+ description:
+ - Information particular to the e-mail delivery method.
+ - Uses the SMTP protocol.
+ - Required when M(method==email).
+ type: dict
+ required: false
+ suboptions:
+ server:
+ description:
+ - Mail server's IP address or fully qualified domain name.
+ - Required when M(routing_type==email).
+ type: str
+ required: false
+ sender:
+ description:
+ - Sender's email account
+ - Required when M(routing_type==email).
+ type: str
+ required: false
+ test_recipient:
+ description:
+ - Test verification email
+ - Required when M(routing_type==email).
+ type: str
+ required: false
+ maintenance_duration:
+ description:
+ - The duration of time the ASUP maintenance mode will be active.
+ - Permittable range is between 1 and 72 hours.
+ - Required when I(state==maintenance_enabled).
+ type: int
+ default: 24
+ required: false
+ maintenance_emails:
+ description:
+ - List of email addresses for maintenance notifications.
+ - Required when I(state==maintenance_enabled).
+ type: list
+ required: false
+ validate:
+ description:
+ - Validate ASUP configuration.
+ type: bool
+ default: false
+ required: false
+notes:
+ - Check mode is supported.
+ - Enabling ASUP will allow our support teams to monitor the logs of the storage-system in order to proactively
+ respond to issues with the system. It is recommended that all ASUP-related options be enabled, but they may be
+ disabled if desired.
+ - This API is currently only supported with the Embedded Web Services API v2.0 and higher.
+"""
+
+EXAMPLES = """
+ - name: Enable ASUP and allow pro-active retrieval of bundles
+ na_santricity_asup:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ state: enabled
+ active: true
+ days: ["saturday", "sunday"]
+ start: 17
+ end: 20
+ - name: Set the ASUP schedule to only send bundles from 12 AM CST to 3 AM CST.
+ na_santricity_asup:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ state: disabled
+ - name: Set the ASUP schedule to only send bundles from 12 AM CST to 3 AM CST.
+ na_santricity_asup:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ state: maintenance_enabled
+ maintenance_duration: 24
+ maintenance_emails:
+ - admin@example.com
+ - name: Set the ASUP schedule to only send bundles from 12 AM CST to 3 AM CST.
+ na_santricity_asup:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ state: maintenance_disabled
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The settings have been updated.
+asup:
+ description:
+ - True if ASUP is enabled.
+ returned: on success
+ sample: true
+ type: bool
+active:
+ description:
+ - True if the active option has been enabled.
+ returned: on success
+ sample: true
+ type: bool
+cfg:
+ description:
+ - Provide the full ASUP configuration.
+ returned: on success
+ type: complex
+ contains:
+ asupEnabled:
+ description:
+ - True if ASUP has been enabled.
+ type: bool
+ onDemandEnabled:
+ description:
+ - True if ASUP active monitoring has been enabled.
+ type: bool
+ daysOfWeek:
+ description:
+ - The days of the week that ASUP bundles will be sent.
+ type: list
+"""
+import time
+
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+
+class NetAppESeriesAsup(NetAppESeriesModule):
+ DAYS_OPTIONS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]
+
+ def __init__(self):
+
+ ansible_options = dict(
+ state=dict(type="str", required=False, default="enabled", choices=["enabled", "disabled", "maintenance_enabled", "maintenance_disabled"]),
+ active=dict(type="bool", required=False, default=True),
+ days=dict(type="list", required=False, aliases=["schedule_days", "days_of_week"], choices=self.DAYS_OPTIONS),
+ start=dict(type="int", required=False, default=0),
+ end=dict(type="int", required=False, default=24),
+ method=dict(type="str", required=False, choices=["https", "http", "email"], default="https"),
+ routing_type=dict(type="str", required=False, choices=["direct", "proxy", "script"], default="direct"),
+ proxy=dict(type="dict", required=False, options=dict(host=dict(type="str", required=False),
+ port=dict(type="str", required=False),
+ script=dict(type="str", required=False))),
+ email=dict(type="dict", required=False, options=dict(server=dict(type="str", required=False),
+ sender=dict(type="str", required=False),
+ test_recipient=dict(type="str", required=False))),
+ maintenance_duration=dict(type="int", required=False, default=24),
+ maintenance_emails=dict(type="list", required=False),
+ validate=dict(type="bool", require=False, default=False))
+
+ mutually_exclusive = [["host", "script"],
+ ["port", "script"]]
+
+ required_if = [["method", "https", ["routing_type"]],
+ ["method", "http", ["routing_type"]],
+ ["method", "email", ["email"]],
+ ["state", "maintenance_enabled", ["maintenance_duration", "maintenance_emails"]]]
+
+ super(NetAppESeriesAsup, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ mutually_exclusive=mutually_exclusive,
+ required_if=required_if,
+ supports_check_mode=True)
+
+ args = self.module.params
+ self.state = args["state"]
+ self.active = args["active"]
+ self.days = args["days"]
+ self.start = args["start"]
+ self.end = args["end"]
+
+ self.method = args["method"]
+ self.routing_type = args["routing_type"] if args["routing_type"] else "none"
+ self.proxy = args["proxy"]
+ self.email = args["email"]
+ self.maintenance_duration = args["maintenance_duration"]
+ self.maintenance_emails = args["maintenance_emails"]
+ self.validate = args["validate"]
+
+ if self.validate and self.email and "test_recipient" not in self.email.keys():
+ self.module.fail_json(msg="test_recipient must be provided for validating email delivery method. Array [%s]" % self.ssid)
+
+ self.check_mode = self.module.check_mode
+
+ if self.start >= self.end:
+ self.module.fail_json(msg="The value provided for the start time is invalid."
+ " It must be less than the end time.")
+ if self.start < 0 or self.start > 23:
+ self.module.fail_json(msg="The value provided for the start time is invalid. It must be between 0 and 23.")
+ else:
+ self.start = self.start * 60
+ if self.end < 1 or self.end > 24:
+ self.module.fail_json(msg="The value provided for the end time is invalid. It must be between 1 and 24.")
+ else:
+ self.end = min(self.end * 60, 1439)
+
+ if self.maintenance_duration < 1 or self.maintenance_duration > 72:
+ self.module.fail_json(msg="The maintenance duration must be equal to or between 1 and 72 hours.")
+
+ if not self.days:
+ self.days = self.DAYS_OPTIONS
+
+ # Check whether request needs to be forwarded on to the controller web services rest api.
+ self.url_path_prefix = ""
+ if not self.is_embedded() and self.ssid != 0:
+ self.url_path_prefix = "storage-systems/%s/forward/devmgr/v2/" % self.ssid
+
+ def get_configuration(self):
+ try:
+ rc, result = self.request(self.url_path_prefix + "device-asup")
+
+ if not (result["asupCapable"] and result["onDemandCapable"]):
+ self.module.fail_json(msg="ASUP is not supported on this device. Array Id [%s]." % self.ssid)
+ return result
+
+ except Exception as err:
+ self.module.fail_json(msg="Failed to retrieve ASUP configuration! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ def in_maintenance_mode(self):
+ """Determine whether storage device is currently in maintenance mode."""
+ results = False
+ try:
+ rc, key_values = self.request(self.url_path_prefix + "key-values")
+
+ for key_value in key_values:
+ if key_value["key"] == "ansible_asup_maintenance_email_list":
+ if not self.maintenance_emails:
+ self.maintenance_emails = key_value["value"].split(",")
+ elif key_value["key"] == "ansible_asup_maintenance_stop_time":
+ if time.time() < float(key_value["value"]):
+ results = True
+
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve maintenance windows information! Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ return results
+
+ def update_configuration(self):
+ config = self.get_configuration()
+ update = False
+ body = dict()
+
+ if self.state == "enabled":
+ body = dict(asupEnabled=True)
+ if not config["asupEnabled"]:
+ update = True
+
+ if (config["onDemandEnabled"] and config["remoteDiagsEnabled"]) != self.active:
+ update = True
+ body.update(dict(onDemandEnabled=self.active,
+ remoteDiagsEnabled=self.active))
+ self.days.sort()
+ config["schedule"]["daysOfWeek"].sort()
+
+ body["schedule"] = dict(daysOfWeek=self.days,
+ dailyMinTime=self.start,
+ dailyMaxTime=self.end,
+ weeklyMinTime=self.start,
+ weeklyMaxTime=self.end)
+
+ if self.days != config["schedule"]["daysOfWeek"]:
+ update = True
+ if self.start != config["schedule"]["dailyMinTime"] or self.start != config["schedule"]["weeklyMinTime"]:
+ update = True
+ elif self.end != config["schedule"]["dailyMaxTime"] or self.end != config["schedule"]["weeklyMaxTime"]:
+ update = True
+
+ if self.method in ["https", "http"]:
+ if self.routing_type == "direct":
+ body["delivery"] = dict(method=self.method,
+ routingType="direct")
+ elif self.routing_type == "proxy":
+ body["delivery"] = dict(method=self.method,
+ proxyHost=self.proxy["host"],
+ proxyPort=self.proxy["port"],
+ routingType="proxyServer")
+ elif self.routing_type == "script":
+ body["delivery"] = dict(method=self.method,
+ proxyScript=self.proxy["script"],
+ routingType="proxyScript")
+
+ else:
+ body["delivery"] = dict(method="smtp",
+ mailRelayServer=self.email["server"],
+ mailSenderAddress=self.email["sender"],
+ routingType="none")
+
+ if config["delivery"]["method"] != body["delivery"]["method"]:
+ update = True
+ elif config["delivery"]["method"] in ["https", "http"]:
+ if config["delivery"]["routingType"] != body["delivery"]["routingType"]:
+ update = True
+ elif (config["delivery"]["routingType"] == "proxy" and
+ config["delivery"]["proxyHost"] != body["delivery"]["proxyHost"] and
+ config["delivery"]["proxyPort"] != body["delivery"]["proxyPort"]):
+ update = True
+ elif config["delivery"]["routingType"] == "script" and config["delivery"]["proxyScript"] != body["delivery"]["proxyScript"]:
+ update = True
+ elif (config["delivery"]["method"] == "smtp" and
+ config["delivery"]["mailRelayServer"] != body["delivery"]["mailRelayServer"] and
+ config["delivery"]["mailSenderAddress"] != body["delivery"]["mailSenderAddress"]):
+ update = True
+
+ if self.in_maintenance_mode():
+ update = True
+
+ elif self.state == "disabled":
+ if config["asupEnabled"]: # Disable asupEnable is asup is disabled.
+ body = dict(asupEnabled=False)
+ update = True
+
+ else:
+ if not config["asupEnabled"]:
+ self.module.fail_json(msg="AutoSupport must be enabled before enabling or disabling maintenance mode. Array [%s]." % self.ssid)
+
+ if self.in_maintenance_mode() or self.state == "maintenance_enabled":
+ update = True
+
+ if update and not self.check_mode:
+ if self.state == "maintenance_enabled":
+ try:
+ rc, response = self.request(self.url_path_prefix + "device-asup/maintenance-window", method="POST",
+ data=dict(maintenanceWindowEnabled=True,
+ duration=self.maintenance_duration,
+ emailAddresses=self.maintenance_emails))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to enabled ASUP maintenance window. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ # Add maintenance information to the key-value store
+ try:
+ rc, response = self.request(self.url_path_prefix + "key-values/ansible_asup_maintenance_email_list", method="POST",
+ data=",".join(self.maintenance_emails))
+ rc, response = self.request(self.url_path_prefix + "key-values/ansible_asup_maintenance_stop_time", method="POST",
+ data=str(time.time() + 60 * 60 * self.maintenance_duration))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to store maintenance information. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ elif self.state == "maintenance_disabled":
+ try:
+ rc, response = self.request(self.url_path_prefix + "device-asup/maintenance-window", method="POST",
+ data=dict(maintenanceWindowEnabled=False,
+ emailAddresses=self.maintenance_emails))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to disable ASUP maintenance window. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ # Remove maintenance information to the key-value store
+ try:
+ rc, response = self.request(self.url_path_prefix + "key-values/ansible_asup_maintenance_email_list", method="DELETE")
+ rc, response = self.request(self.url_path_prefix + "key-values/ansible_asup_maintenance_stop_time", method="DELETE")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to store maintenance information. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ else:
+ if body["asupEnabled"] and self.validate:
+ validate_body = dict(delivery=body["delivery"])
+ if self.email:
+ validate_body["mailReplyAddress"] = self.email["test_recipient"]
+
+ try:
+ rc, response = self.request(self.url_path_prefix + "device-asup/verify-config", timeout=600, method="POST", data=validate_body)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to validate ASUP configuration! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ try:
+ rc, response = self.request(self.url_path_prefix + "device-asup", method="POST", data=body)
+ # This is going to catch cases like a connection failure
+ except Exception as err:
+ self.module.fail_json(msg="Failed to change ASUP configuration! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ return update
+
+ def apply(self):
+ update = self.update_configuration()
+ cfg = self.get_configuration()
+
+ if update:
+ self.module.exit_json(msg="The ASUP settings have been updated.", changed=update, asup=cfg["asupEnabled"], active=cfg["onDemandEnabled"], cfg=cfg)
+ else:
+ self.module.exit_json(msg="No ASUP changes required.", changed=update, asup=cfg["asupEnabled"], active=cfg["onDemandEnabled"], cfg=cfg)
+
+
+def main():
+ asup = NetAppESeriesAsup()
+ asup.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_auditlog.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_auditlog.py
new file mode 100644
index 00000000..03a533fe
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_auditlog.py
@@ -0,0 +1,200 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_auditlog
+short_description: NetApp E-Series manage audit-log configuration
+description:
+ - This module allows an e-series storage system owner to set audit-log configuration parameters.
+author: Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ max_records:
+ description:
+ - The maximum number log messages audit-log will retain.
+ - Max records must be between and including 100 and 50000.
+ type: int
+ default: 50000
+ log_level:
+ description: Filters the log messages according to the specified log level selection.
+ choices:
+ - all
+ - writeOnly
+ type: str
+ default: writeOnly
+ full_policy:
+ description: Specifies what audit-log should do once the number of entries approach the record limit.
+ choices:
+ - overWrite
+ - preventSystemAccess
+ type: str
+ default: overWrite
+ threshold:
+ description:
+ - This is the memory full percent threshold that audit-log will start issuing warning messages.
+ - Percent range must be between and including 60 and 90.
+ type: int
+ default: 90
+ force:
+ description:
+ - Forces the audit-log configuration to delete log history when log messages fullness cause immediate
+ warning or full condition.
+ - Warning! This will cause any existing audit-log messages to be deleted.
+ - This is only applicable for I(full_policy=preventSystemAccess).
+ type: bool
+ default: no
+notes:
+ - Check mode is supported.
+ - Use I(ssid=="0") or I(ssid=="proxy") to configure SANtricity Web Services Proxy auditlog settings otherwise.
+"""
+
+EXAMPLES = """
+- name: Define audit-log to prevent system access if records exceed 50000 with warnings occurring at 60% capacity.
+ na_santricity_auditlog:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ max_records: 50000
+ log_level: all
+ full_policy: preventSystemAccess
+ threshold: 60
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The settings have been updated.
+"""
+import json
+
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+
+class NetAppESeriesAuditLog(NetAppESeriesModule):
+ """Audit-log module configuration class."""
+ MAX_RECORDS = 50000
+
+ def __init__(self):
+ ansible_options = dict(max_records=dict(type="int", default=50000),
+ log_level=dict(type="str", default="writeOnly", choices=["all", "writeOnly"]),
+ full_policy=dict(type="str", default="overWrite", choices=["overWrite", "preventSystemAccess"]),
+ threshold=dict(type="int", default=90),
+ force=dict(type="bool", default=False))
+ super(NetAppESeriesAuditLog, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ supports_check_mode=True)
+
+ args = self.module.params
+ self.log_level = args["log_level"]
+ self.force = args["force"]
+ self.full_policy = args["full_policy"]
+ self.max_records = args["max_records"]
+ self.threshold = args["threshold"]
+
+ if self.max_records < 100 or self.max_records > self.MAX_RECORDS:
+ self.module.fail_json(msg="Audit-log max_records count must be between 100 and 50000: [%s]" % self.max_records)
+
+ if self.threshold < 60 or self.threshold > 90:
+ self.module.fail_json(msg="Audit-log percent threshold must be between 60 and 90: [%s]" % self.threshold)
+
+ # Append web services proxy forward end point.
+ self.url_path_prefix = ""
+ if not self.is_embedded() and self.ssid != "0" and self.ssid.lower() != "proxy":
+ self.url_path_prefix = "storage-systems/%s/forward/devmgr/v2/" % self.ssid
+
+ def get_configuration(self):
+ """Retrieve the existing audit-log configurations.
+
+ :returns: dictionary containing current audit-log configuration
+ """
+ try:
+ if self.is_proxy() and (self.ssid == "0" or self.ssid.lower() != "proxy"):
+ rc, data = self.request("audit-log/config")
+ else:
+ rc, data = self.request(self.url_path_prefix + "storage-systems/1/audit-log/config")
+ return data
+ except Exception as err:
+ self.module.fail_json(msg="Failed to retrieve the audit-log configuration! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ def build_configuration(self):
+ """Build audit-log expected configuration.
+
+ :returns: Tuple containing update boolean value and dictionary of audit-log configuration
+ """
+ config = self.get_configuration()
+
+ current = dict(auditLogMaxRecords=config["auditLogMaxRecords"],
+ auditLogLevel=config["auditLogLevel"],
+ auditLogFullPolicy=config["auditLogFullPolicy"],
+ auditLogWarningThresholdPct=config["auditLogWarningThresholdPct"])
+
+ body = dict(auditLogMaxRecords=self.max_records,
+ auditLogLevel=self.log_level,
+ auditLogFullPolicy=self.full_policy,
+ auditLogWarningThresholdPct=self.threshold)
+
+ update = current != body
+ return update, body
+
+ def delete_log_messages(self):
+ """Delete all audit-log messages."""
+ try:
+ if self.is_proxy() and (self.ssid == "0" or self.ssid.lower() != "proxy"):
+ rc, result = self.request("audit-log?clearAll=True", method="DELETE")
+ else:
+ rc, result = self.request(self.url_path_prefix + "storage-systems/1/audit-log?clearAll=True", method="DELETE")
+ except Exception as err:
+ self.module.fail_json(msg="Failed to delete audit-log messages! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ def update_configuration(self, update=None, body=None, attempt_recovery=True):
+ """Update audit-log configuration."""
+ if update is None or body is None:
+ update, body = self.build_configuration()
+
+ if update and not self.module.check_mode:
+ try:
+ if self.is_proxy() and (self.ssid == "0" or self.ssid.lower() != "proxy"):
+ rc, result = self.request("audit-log/config", data=json.dumps(body), method='POST', ignore_errors=True)
+ else:
+ rc, result = self.request(self.url_path_prefix + "storage-systems/1/audit-log/config",
+ data=json.dumps(body), method='POST', ignore_errors=True)
+
+ if rc == 422:
+ if self.force and attempt_recovery:
+ self.delete_log_messages()
+ update = self.update_configuration(update, body, False)
+ else:
+ self.module.fail_json(msg="Failed to update audit-log configuration! Array Id [%s]. Error [%s]." % (self.ssid, to_native(rc, result)))
+
+ except Exception as error:
+ self.module.fail_json(msg="Failed to update audit-log configuration! Array Id [%s]. Error [%s]." % (self.ssid, to_native(error)))
+ return update
+
+ def update(self):
+ """Update the audit-log configuration."""
+ update = self.update_configuration()
+ if update:
+ self.module.exit_json(msg="Audit-log update complete", changed=update)
+ else:
+ self.module.exit_json(msg="No audit-log changes required", changed=update)
+
+
+def main():
+ auditlog = NetAppESeriesAuditLog()
+ auditlog.update()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_auth.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_auth.py
new file mode 100644
index 00000000..bde84c9f
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_auth.py
@@ -0,0 +1,341 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_auth
+short_description: NetApp E-Series set or update the password for a storage array device or SANtricity Web Services Proxy.
+description:
+ - Sets or updates the password for a storage array device or SANtricity Web Services Proxy.
+author:
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ current_admin_password:
+ description:
+ - The current admin password.
+ - When making changes to the embedded web services's login passwords, api_password will be used and current_admin_password will be ignored.
+ - When making changes to the proxy web services's login passwords, api_password will be used and current_admin_password will be ignored.
+ - Only required when the password has been set and will be ignored if not set.
+ type: str
+ required: false
+ password:
+ description:
+ - The password you would like to set.
+ - Cannot be more than 30 characters.
+ type: str
+ required: false
+ user:
+ description:
+ - The local user account password to update
+ - For systems prior to E2800, use admin to change the rw (system password).
+ - For systems prior to E2800, all choices except admin will be ignored.
+ type: str
+ choices: ["admin", "monitor", "support", "security", "storage"]
+ default: "admin"
+ required: false
+ minimum_password_length:
+ description:
+ - This option defines the minimum password length.
+ type: int
+ required: false
+notes:
+ - Set I(ssid=="0") or I(ssid=="proxy") when attempting to change the password for SANtricity Web Services Proxy.
+ - SANtricity Web Services Proxy storage password will be updated when changing the password on a managed storage system from the proxy; This is only true
+ when the storage system has been previously contacted.
+"""
+
+EXAMPLES = """
+- name: Set the initial password
+ na_santricity_auth:
+ ssid: 1
+ api_url: https://192.168.1.100:8443/devmgr/v2
+ api_username: admin
+ api_password: adminpass
+ validate_certs: true
+ current_admin_password: currentadminpass
+ password: newpassword123
+ user: admin
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: success
+ type: str
+ sample: "Password Updated Successfully"
+"""
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+from time import sleep
+
+
+class NetAppESeriesAuth(NetAppESeriesModule):
+ def __init__(self):
+ version = "02.00.0000.0000"
+ ansible_options = dict(current_admin_password=dict(type="str", required=False, no_log=True),
+ password=dict(type="str", required=False, no_log=True),
+ user=dict(type="str", choices=["admin", "monitor", "support", "security", "storage"], default="admin", required=False),
+ minimum_password_length=dict(type="int", required=False, no_log=True))
+
+ super(NetAppESeriesAuth, self).__init__(ansible_options=ansible_options, web_services_version=version, supports_check_mode=True)
+ args = self.module.params
+ self.current_admin_password = args["current_admin_password"]
+ self.password = args["password"]
+ self.user = args["user"]
+ self.minimum_password_length = args["minimum_password_length"]
+
+ self.DEFAULT_HEADERS.update({"x-netapp-password-validate-method": "none"})
+
+ self.is_admin_password_set = None
+ self.current_password_length_requirement = None
+
+ def minimum_password_length_change_required(self):
+ """Retrieve the current storage array's global configuration."""
+ change_required = False
+ try:
+ if self.is_proxy():
+ if self.ssid == "0" or self.ssid.lower() == "proxy":
+ rc, system_info = self.request("local-users/info", force_basic_auth=False)
+
+ elif self.is_embedded_available():
+ rc, system_info = self.request("storage-systems/%s/forward/devmgr/v2/storage-systems/1/local-users/info" % self.ssid,
+ force_basic_auth=False)
+ else:
+ return False # legacy systems without embedded web services.
+ else:
+ rc, system_info = self.request("storage-systems/%s/local-users/info" % self.ssid, force_basic_auth=False)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to determine minimum password length. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ self.is_admin_password_set = system_info["adminPasswordSet"]
+ if self.minimum_password_length is not None and self.minimum_password_length != system_info["minimumPasswordLength"]:
+ change_required = True
+
+ if (self.password is not None and ((change_required and self.minimum_password_length > len(self.password)) or
+ (not change_required and system_info["minimumPasswordLength"] > len(self.password)))):
+ self.module.fail_json(msg="Password does not meet the length requirement [%s]. Array Id [%s]." % (system_info["minimumPasswordLength"], self.ssid))
+
+ return change_required
+
+ def update_minimum_password_length(self):
+ """Update automatic load balancing state."""
+ try:
+ if self.is_proxy():
+ if self.ssid == "0" or self.ssid.lower() == "proxy":
+ try:
+ if not self.is_admin_password_set:
+ self.creds["url_password"] = "admin"
+ rc, minimum_password_length = self.request("local-users/password-length", method="POST",
+ data={"minimumPasswordLength": self.minimum_password_length})
+ except Exception as error:
+ if not self.is_admin_password_set:
+ self.creds["url_password"] = ""
+ rc, minimum_password_length = self.request("local-users/password-length", method="POST",
+ data={"minimumPasswordLength": self.minimum_password_length})
+ elif self.is_embedded_available():
+ if not self.is_admin_password_set:
+ self.creds["url_password"] = ""
+ rc, minimum_password_length = self.request("storage-systems/%s/forward/devmgr/v2/storage-systems/1/local-users/password-length" % self.ssid,
+ method="POST", data={"minimumPasswordLength": self.minimum_password_length})
+ else:
+ if not self.is_admin_password_set:
+ self.creds["url_password"] = ""
+ rc, minimum_password_length = self.request("storage-systems/%s/local-users/password-length" % self.ssid, method="POST",
+ data={"minimumPasswordLength": self.minimum_password_length})
+ except Exception as error:
+ self.module.fail_json(msg="Failed to set minimum password length. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ def logout_system(self):
+ """Ensure system is logged out. This is required because login test will always succeed if previously logged in."""
+ try:
+ if self.is_proxy():
+ if self.ssid == "0" or self.ssid.lower() == "proxy":
+ rc, system_info = self.request("utils/login", rest_api_path=self.DEFAULT_BASE_PATH, method="DELETE", force_basic_auth=False)
+ elif self.is_embedded_available():
+ rc, system_info = self.request("storage-systems/%s/forward/devmgr/utils/login" % self.ssid, method="DELETE", force_basic_auth=False)
+ else:
+ # Nothing to do for legacy systems without embedded web services.
+ pass
+ else:
+ rc, system_info = self.request("utils/login", rest_api_path=self.DEFAULT_BASE_PATH, method="DELETE", force_basic_auth=False)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to log out of storage system [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ def password_change_required(self):
+ """Verify whether the current password is expected array password. Works only against embedded systems."""
+ if self.password is None:
+ return False
+
+ change_required = False
+ system_info = None
+ try:
+ if self.is_proxy():
+ if self.ssid == "0" or self.ssid.lower() == "proxy":
+ rc, system_info = self.request("local-users/info", force_basic_auth=False)
+ elif self.is_embedded_available():
+ rc, system_info = self.request("storage-systems/%s/forward/devmgr/v2/storage-systems/1/local-users/info" % self.ssid,
+ force_basic_auth=False)
+ else:
+ rc, response = self.request("storage-systems/%s/passwords" % self.ssid, ignore_errors=True)
+ system_info = {"minimumPasswordLength": 0, "adminPasswordSet": response["adminPasswordSet"]}
+ else:
+ rc, system_info = self.request("storage-systems/%s/local-users/info" % self.ssid, force_basic_auth=False)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve information about storage system [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ self.is_admin_password_set = system_info["adminPasswordSet"]
+
+ if not self.is_admin_password_set:
+ if self.user == "admin" and self.password != "":
+ change_required = True
+
+ # Determine whether user's password needs to be changed
+ else:
+ utils_login_used = False
+ self.logout_system() # This ensures that login test functions correctly. The query onlycheck=true does not work.
+
+ if self.is_proxy():
+ if self.ssid == "0" or self.ssid.lower() == "proxy":
+ utils_login_used = True
+ rc, response = self.request("utils/login?uid=%s&pwd=%s&xsrf=false&onlycheck=false" % (self.user, self.password),
+ rest_api_path=self.DEFAULT_BASE_PATH, log_request=False, ignore_errors=True, force_basic_auth=False)
+ # elif self.is_embedded_available():
+ # utils_login_used = True
+ # rc, response = self.request("storage-systems/%s/forward/devmgr/utils/login?uid=%s&pwd=%s&xsrf=false&onlycheck=false"
+ # % (self.ssid, self.user, self.password), log_request=False, ignore_errors=True, force_basic_auth=False)
+ else:
+ if self.user == "admin":
+ rc, response = self.request("storage-systems/%s/stored-password/validate" % self.ssid, method="POST", log_request=False,
+ ignore_errors=True, data={"password": self.password})
+ if rc == 200:
+ change_required = not response["isValidPassword"]
+ elif rc == 404: # endpoint did not exist, old proxy version
+ if self.is_web_services_version_met("04.10.0000.0000"):
+ self.module.fail_json(msg="For platforms before E2800 use SANtricity Web Services Proxy 4.1 or later! Array Id [%s].")
+ self.module.fail_json(msg="Failed to validate stored password! Array Id [%s].")
+ else:
+ self.module.fail_json(msg="Failed to validate stored password! Array Id [%s]." % self.ssid)
+ else:
+ self.module.fail_json(msg="Role based login not available! Only storage system password can be set for storage systems prior to E2800."
+ " Array Id [%s]." % self.ssid)
+ else:
+ utils_login_used = True
+ rc, response = self.request("utils/login?uid=%s&pwd=%s&xsrf=false&onlycheck=false" % (self.user, self.password),
+ rest_api_path=self.DEFAULT_BASE_PATH, log_request=False, ignore_errors=True, force_basic_auth=False)
+
+ # Check return codes to determine whether a change is required
+ if utils_login_used:
+ if rc == 401:
+ change_required = True
+ elif rc == 422:
+ self.module.fail_json(msg="SAML enabled! SAML disables default role based login. Array [%s]" % self.ssid)
+
+ return change_required
+
+ def set_array_admin_password(self):
+ """Set the array's admin password."""
+ if self.is_proxy():
+
+ # Update proxy's local users
+ if self.ssid == "0" or self.ssid.lower() == "proxy":
+ self.creds["url_password"] = "admin"
+ try:
+ body = {"currentAdminPassword": "", "updates": {"userName": "admin", "newPassword": self.password}}
+ rc, proxy = self.request("local-users", method="POST", data=body)
+ except Exception as error:
+ self.creds["url_password"] = ""
+ try:
+ body = {"currentAdminPassword": "", "updates": {"userName": "admin", "newPassword": self.password}}
+ rc, proxy = self.request("local-users", method="POST", data=body)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to set proxy's admin password. Error [%s]." % to_native(error))
+
+ self.creds["url_password"] = self.password
+
+ # Update password using the password endpoints, this will also update the storaged password
+ else:
+ try:
+ body = {"currentAdminPassword": "", "newPassword": self.password, "adminPassword": True}
+ rc, storage_system = self.request("storage-systems/%s/passwords" % self.ssid, method="POST", data=body)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to set storage system's admin password. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ # Update embedded local users
+ else:
+ self.creds["url_password"] = ""
+ try:
+ body = {"currentAdminPassword": "", "updates": {"userName": "admin", "newPassword": self.password}}
+ rc, proxy = self.request("storage-systems/%s/local-users" % self.ssid, method="POST", data=body)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to set embedded storage system's admin password. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+ self.creds["url_password"] = self.password
+
+ def set_array_password(self):
+ """Set the array password."""
+ if not self.is_admin_password_set:
+ self.module.fail_json(msg="Admin password not set! Set admin password before changing non-admin user passwords. Array [%s]." % self.ssid)
+
+ if self.is_proxy():
+
+ # Update proxy's local users
+ if self.ssid == "0" or self.ssid.lower() == "proxy":
+ try:
+ body = {"currentAdminPassword": self.creds["url_password"], "updates": {"userName": self.user, "newPassword": self.password}}
+ rc, proxy = self.request("local-users", method="POST", data=body)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to set proxy password. Error [%s]." % to_native(error))
+ elif self.is_embedded_available():
+ try:
+ body = {"currentAdminPassword": self.current_admin_password, "updates": {"userName": self.user, "newPassword": self.password}}
+ rc, proxy = self.request("storage-systems/%s/forward/devmgr/v2/storage-systems/1/local-users" % self.ssid, method="POST", data=body)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to set embedded user password. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ # Update embedded local users
+ else:
+ try:
+ body = {"currentAdminPassword": self.creds["url_password"], "updates": {"userName": self.user, "newPassword": self.password}}
+ rc, proxy = self.request("storage-systems/%s/local-users" % self.ssid, method="POST", data=body)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to set embedded user password. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ def apply(self):
+ """Apply any required changes."""
+ password_change_required = self.password_change_required()
+ minimum_password_length_change_required = self.minimum_password_length_change_required()
+ change_required = password_change_required or minimum_password_length_change_required
+
+ if change_required and not self.module.check_mode:
+ if minimum_password_length_change_required:
+ self.update_minimum_password_length()
+
+ if password_change_required:
+ if not self.is_admin_password_set:
+ self.set_array_admin_password()
+ else:
+ self.set_array_password()
+
+ if password_change_required and minimum_password_length_change_required:
+ self.module.exit_json(msg="'%s' password and required password length has been changed. Array [%s]."
+ % (self.user, self.ssid), changed=change_required)
+ elif password_change_required:
+ self.module.exit_json(msg="'%s' password has been changed. Array [%s]." % (self.user, self.ssid), changed=change_required)
+ elif minimum_password_length_change_required:
+ self.module.exit_json(msg="Required password length has been changed. Array [%s]." % self.ssid, changed=change_required)
+ self.module.exit_json(msg="No changes have been made. Array [%s]." % self.ssid, changed=change_required)
+
+
+def main():
+ auth = NetAppESeriesAuth()
+ auth.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_client_certificate.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_client_certificate.py
new file mode 100644
index 00000000..8d5ee4bd
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_client_certificate.py
@@ -0,0 +1,232 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+module: na_santricity_client_certificate
+short_description: NetApp E-Series manage remote server certificates.
+description: Manage NetApp E-Series storage array's remote server certificates.
+author: Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ certificates:
+ description:
+ - List of certificate files
+ - Each item must include the path to the file
+ type: list
+ required: false
+notes:
+ - Set I(ssid=="0") or I(ssid=="proxy") to specifically reference SANtricity Web Services Proxy.
+requirements:
+ - cryptography
+"""
+EXAMPLES = """
+- name: Upload certificates
+ na_santricity_client_certificate:
+ ssid: 1
+ api_url: https://192.168.1.100:8443/devmgr/v2
+ api_username: admin
+ api_password: adminpass
+ certificates: ["/path/to/certificates.crt", "/path/to/another_certificate.crt"]
+- name: Remove all certificates
+ na_santricity_client_certificate:
+ ssid: 1
+ api_url: https://192.168.1.100:8443/devmgr/v2
+ api_username: admin
+ api_password: adminpass
+"""
+RETURN = """
+changed:
+ description: Whether changes have been made.
+ type: bool
+ returned: always
+ sample: true
+add_certificates:
+ description: Any SSL certificates that were added.
+ type: list
+ returned: always
+ sample: ["added_cerificiate.crt"]
+removed_certificates:
+ description: Any SSL certificates that were removed.
+ type: list
+ returned: always
+ sample: ["removed_cerificiate.crt"]
+"""
+
+import binascii
+import os
+import re
+
+from datetime import datetime
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule, create_multipart_formdata
+from ansible.module_utils._text import to_native
+
+try:
+ from cryptography import x509
+ from cryptography.hazmat.backends import default_backend
+except ImportError:
+ HAS_CRYPTOGRAPHY = False
+else:
+ HAS_CRYPTOGRAPHY = True
+
+
+class NetAppESeriesClientCertificate(NetAppESeriesModule):
+ def __init__(self):
+ ansible_options = dict(certificates=dict(type="list", required=False))
+
+ super(NetAppESeriesClientCertificate, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ supports_check_mode=True)
+
+ args = self.module.params
+ self.certificates = args["certificates"] if args["certificates"] else []
+
+ # Check whether request needs to be forwarded on to the controller web services rest api.
+ self.url_path_prefix = ""
+ if self.is_proxy() and self.ssid != "0" and self.ssid != "PROXY":
+ self.url_path_prefix = "storage-systems/%s/forward/devmgr/v2/" % self.ssid
+
+ self.remove_certificates = list()
+ self.add_certificates = list()
+ self.certificate_fingerprint_cache = None
+ self.certificate_info_cache = None
+
+ def certificate_info(self, path):
+ """Determine the pertinent certificate information: alias, subjectDN, issuerDN, start and expire.
+
+ Note: Use only when certificate/remote-server endpoints do not exist. Used to identify certificates through
+ the sslconfig/ca endpoint.
+ """
+ certificate = None
+ with open(path, "rb") as fh:
+ data = fh.read()
+ try:
+ certificate = x509.load_pem_x509_certificate(data, default_backend())
+ except Exception as error:
+ try:
+ certificate = x509.load_der_x509_certificate(data, default_backend())
+ except Exception as error:
+ self.module.fail_json(msg="Failed to load certificate. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ if not isinstance(certificate, x509.Certificate):
+ self.module.fail_json(msg="Failed to open certificate file or invalid certificate object type. Array [%s]." % self.ssid)
+
+ return dict(start_date=certificate.not_valid_before,
+ expire_date=certificate.not_valid_after,
+ subject_dn=[attr.value for attr in certificate.subject],
+ issuer_dn=[attr.value for attr in certificate.issuer])
+
+ def certificate_fingerprint(self, path):
+ """Load x509 certificate that is either encoded DER or PEM encoding and return the certificate fingerprint."""
+ certificate = None
+ with open(path, "rb") as fh:
+ data = fh.read()
+ try:
+ certificate = x509.load_pem_x509_certificate(data, default_backend())
+ except Exception as error:
+ try:
+ certificate = x509.load_der_x509_certificate(data, default_backend())
+ except Exception as error:
+ self.module.fail_json(msg="Failed to determine certificate fingerprint. File [%s]. Array [%s]. Error [%s]."
+ % (path, self.ssid, to_native(error)))
+
+ return binascii.hexlify(certificate.fingerprint(certificate.signature_hash_algorithm)).decode("utf-8")
+
+ def determine_changes(self):
+ """Search for remote server certificate that goes by the alias or has a matching fingerprint."""
+ rc, current_certificates = self.request(self.url_path_prefix + "certificates/remote-server", ignore_errors=True)
+
+ if rc == 404: # system down or endpoint does not exist
+ rc, current_certificates = self.request(self.url_path_prefix + "sslconfig/ca?useTruststore=true", ignore_errors=True)
+
+ if rc > 299:
+ self.module.fail_json(msg="Failed to retrieve remote server certificates. Array [%s]." % self.ssid)
+
+ user_installed_certificates = [certificate for certificate in current_certificates if certificate["isUserInstalled"]]
+ existing_certificates = []
+
+ for path in self.certificates:
+ for current_certificate in user_installed_certificates:
+ info = self.certificate_info(path)
+ tmp = dict(subject_dn=[re.sub(r".*=", "", item) for item in current_certificate["subjectDN"].split(", ")],
+ issuer_dn=[re.sub(r".*=", "", item) for item in current_certificate["issuerDN"].split(", ")],
+ start_date=datetime.strptime(current_certificate["start"].split(".")[0], "%Y-%m-%dT%H:%M:%S"),
+ expire_date=datetime.strptime(current_certificate["expire"].split(".")[0], "%Y-%m-%dT%H:%M:%S"))
+ if (all([attr in info["subject_dn"] for attr in tmp["subject_dn"]]) and
+ all([attr in info["issuer_dn"] for attr in tmp["issuer_dn"]]) and
+ tmp["start_date"] == info["start_date"] and
+ tmp["expire_date"] == info["expire_date"]):
+ existing_certificates.append(current_certificate)
+ break
+ else:
+ self.add_certificates.append(path)
+ self.remove_certificates = [certificate for certificate in user_installed_certificates if certificate not in existing_certificates]
+
+ elif rc > 299:
+ self.module.fail_json(msg="Failed to retrieve remote server certificates. Array [%s]." % self.ssid)
+
+ else:
+ user_installed_certificates = [certificate for certificate in current_certificates if certificate["isUserInstalled"]]
+ existing_certificates = []
+ for path in self.certificates:
+ for current_certificate in user_installed_certificates:
+ fingerprint = self.certificate_fingerprint(path)
+ if current_certificate["sha256Fingerprint"] == fingerprint or current_certificate["shaFingerprint"] == fingerprint:
+ existing_certificates.append(current_certificate)
+ break
+ else:
+ self.add_certificates.append(path)
+ self.remove_certificates = [certificate for certificate in user_installed_certificates if certificate not in existing_certificates]
+
+ def upload_certificate(self, path):
+ """Add or update remote server certificate to the storage array."""
+ file_name = os.path.basename(path)
+ headers, data = create_multipart_formdata(files=[("file", file_name, path)])
+
+ rc, resp = self.request(self.url_path_prefix + "certificates/remote-server", method="POST", headers=headers, data=data, ignore_errors=True)
+ if rc == 404:
+ rc, resp = self.request(self.url_path_prefix + "sslconfig/ca?useTruststore=true", method="POST", headers=headers, data=data, ignore_errors=True)
+
+ if rc > 299:
+ self.module.fail_json(msg="Failed to upload certificate. Array [%s]. Error [%s, %s]." % (self.ssid, rc, resp))
+
+ def delete_certificate(self, info):
+ """Delete existing remote server certificate in the storage array truststore."""
+ rc, resp = self.request(self.url_path_prefix + "certificates/remote-server/%s" % info["alias"], method="DELETE", ignore_errors=True)
+
+ if rc == 404:
+ rc, resp = self.request(self.url_path_prefix + "sslconfig/ca/%s?useTruststore=true" % info["alias"], method="DELETE", ignore_errors=True)
+
+ if rc > 204:
+ self.module.fail_json(msg="Failed to delete certificate. Alias [%s]. Array [%s]. Error [%s, %s]." % (info["alias"], self.ssid, rc, resp))
+
+ def apply(self):
+ """Apply state changes to the storage array's truststore."""
+ changed = False
+
+ self.determine_changes()
+ if self.remove_certificates or self.add_certificates:
+ changed = True
+
+ if changed and not self.module.check_mode:
+ for info in self.remove_certificates:
+ self.delete_certificate(info)
+
+ for path in self.add_certificates:
+ self.upload_certificate(path)
+
+ self.module.exit_json(changed=changed, removed_certificates=self.remove_certificates, add_certificates=self.add_certificates)
+
+
+def main():
+ client_certs = NetAppESeriesClientCertificate()
+ client_certs.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_discover.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_discover.py
new file mode 100644
index 00000000..90e0649b
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_discover.py
@@ -0,0 +1,313 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_discover
+short_description: NetApp E-Series discover E-Series storage systems
+description: Module searches a subnet range and returns any available E-Series storage systems.
+author: Nathan Swartz (@ndswartz)
+options:
+ subnet_mask:
+ description:
+ - This is the IPv4 search range for discovering E-Series storage arrays.
+ - IPv4 subnet mask specified in CIDR form. Example 192.168.1.0/24 would search the range 192.168.1.0 to 192.168.1.255.
+ - Be sure to include all management paths in the search range.
+ type: str
+ required: true
+ ports:
+ description:
+ - This option specifies which ports to be tested during the discovery process.
+ - The first usable port will be used in the returned API url.
+ type: list
+ default: [8443]
+ required: false
+ proxy_url:
+ description:
+ - Web Services Proxy REST API URL. Example https://192.168.1.100:8443/devmgr/v2/
+ type: str
+ required: false
+ proxy_username:
+ description:
+ - Web Service Proxy username
+ type: str
+ required: false
+ proxy_password:
+ description:
+ - Web Service Proxy user password
+ type: str
+ required: false
+ proxy_validate_certs:
+ description:
+ - Whether to validate Web Service Proxy SSL certificate
+ type: bool
+ default: true
+ required: false
+ prefer_embedded:
+ description:
+ - Give preference to Web Services Embedded when an option exists for both Web Services Proxy and Embedded.
+ - Web Services Proxy will be utilized when available by default.
+ type: bool
+ default: false
+ required: false
+notes:
+ - Only available for platforms E2800 or later (SANtricity Web Services Embedded REST API must be available).
+ - All E-Series storage systems with SANtricity version 11.62 or later will be discovered.
+ - Only E-Series storage systems without a set admin password running SANtricity versions prior to 11.62 will be discovered.
+ - Use SANtricity Web Services Proxy to discover all systems regardless of SANricity version or password.
+requirements:
+ - ipaddress
+"""
+
+EXAMPLES = """
+- name: Discover all E-Series storage systems on the network.
+ na_santricity_discover:
+ subnet_mask: 192.168.1.0/24
+"""
+
+RETURN = """
+systems_found:
+ description: Success message
+ returned: on success
+ type: dict
+ sample: {"012341234123": {
+ "addresses": ["192.168.1.184", "192.168.1.185"],
+ "api_urls": ["https://192.168.1.184:8443/devmgr/v2/", "https://192.168.1.185:8443/devmgr/v2/"],
+ "label": "ExampleArray01",
+ "proxy_ssid: "",
+ "proxy_required": false},
+ "012341234567": {
+ "addresses": ["192.168.1.23", "192.168.1.24"],
+ "api_urls": ["https://192.168.1.100:8443/devmgr/v2/"],
+ "label": "ExampleArray02",
+ "proxy_ssid": "array_ssid",
+ "proxy_required": true}}
+"""
+
+import json
+import multiprocessing
+import threading
+from time import sleep
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import request
+from ansible.module_utils._text import to_native
+
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+
+try:
+ import ipaddress
+except ImportError:
+ HAS_IPADDRESS = False
+else:
+ HAS_IPADDRESS = True
+
+
+class NetAppESeriesDiscover:
+ """Discover E-Series storage systems."""
+ MAX_THREAD_POOL_SIZE = 256
+ CPU_THREAD_MULTIPLE = 32
+ SEARCH_TIMEOUT = 30
+ DEFAULT_CONNECTION_TIMEOUT_SEC = 30
+ DEFAULT_DISCOVERY_TIMEOUT_SEC = 300
+
+ def __init__(self):
+ ansible_options = dict(subnet_mask=dict(type="str", required=True),
+ ports=dict(type="list", required=False, default=[8443]),
+ proxy_url=dict(type="str", required=False),
+ proxy_username=dict(type="str", required=False),
+ proxy_password=dict(type="str", required=False, no_log=True),
+ proxy_validate_certs=dict(type="bool", default=True, required=False),
+ prefer_embedded=dict(type="bool", default=False, required=False))
+
+ required_together = [["proxy_url", "proxy_username", "proxy_password"]]
+ self.module = AnsibleModule(argument_spec=ansible_options, required_together=required_together)
+ args = self.module.params
+
+ self.subnet_mask = args["subnet_mask"]
+ self.prefer_embedded = args["prefer_embedded"]
+ self.ports = []
+ self.proxy_url = args["proxy_url"]
+ if args["proxy_url"]:
+ parsed_url = list(urlparse.urlparse(args["proxy_url"]))
+ parsed_url[2] = "/devmgr/utils/about"
+ self.proxy_about_url = urlparse.urlunparse(parsed_url)
+ parsed_url[2] = "/devmgr/v2/"
+ self.proxy_url = urlparse.urlunparse(parsed_url)
+ self.proxy_username = args["proxy_username"]
+ self.proxy_password = args["proxy_password"]
+ self.proxy_validate_certs = args["proxy_validate_certs"]
+
+ for port in args["ports"]:
+ if str(port).isdigit() and 0 < port < 2 ** 16:
+ self.ports.append(str(port))
+ else:
+ self.module.fail_json(msg="Invalid port! Ports must be positive numbers between 0 and 65536.")
+
+ self.systems_found = {}
+
+ def check_ip_address(self, systems_found, address):
+ """Determine where an E-Series storage system is available at a specific ip address."""
+ for port in self.ports:
+ if port == "8080":
+ url = "http://%s:%s/devmgr/v2/storage-systems/1/" % (address, port)
+ else:
+ url = "https://%s:%s/devmgr/v2/storage-systems/1/" % (address, port)
+ try:
+ rc, sa_data = request(url + "symbol/getSAData", validate_certs=False, force_basic_auth=False, ignore_errors=True)
+ if rc == 401: # Unauthorized
+ self.module.warn("Fail over and discover any storage system without a set admin password. This will discover systems without a set password"
+ " such as newly deployed storage systems. Address [%s]." % address)
+ # Fail over and discover any storage system without a set admin password. This will cover newly deployed systems.
+ rc, graph = request(url + "graph", validate_certs=False, url_username="admin", url_password="", timeout=self.SEARCH_TIMEOUT)
+ sa_data = graph["sa"]["saData"]
+
+ if sa_data["chassisSerialNumber"] in systems_found:
+ systems_found[sa_data["chassisSerialNumber"]]["api_urls"].append(url)
+ else:
+ systems_found.update({sa_data["chassisSerialNumber"]: {"api_urls": [url], "label": sa_data["storageArrayLabel"],
+ "addresses": [], "proxy_ssid": "", "proxy_required": False}})
+ break
+ except Exception as error:
+ pass
+
+ def no_proxy_discover(self):
+ """Discover E-Series storage systems using embedded web services."""
+ thread_pool_size = min(multiprocessing.cpu_count() * self.CPU_THREAD_MULTIPLE, self.MAX_THREAD_POOL_SIZE)
+ subnet = list(ipaddress.ip_network(u"%s" % self.subnet_mask))
+
+ thread_pool = []
+ search_count = len(subnet)
+ for start in range(0, search_count, thread_pool_size):
+ end = search_count if (search_count - start) < thread_pool_size else start + thread_pool_size
+
+ for address in subnet[start:end]:
+ thread = threading.Thread(target=self.check_ip_address, args=(self.systems_found, address))
+ thread_pool.append(thread)
+ thread.start()
+ for thread in thread_pool:
+ thread.join()
+
+ def verify_proxy_service(self):
+ """Verify proxy url points to a web services proxy."""
+ try:
+ rc, about = request(self.proxy_about_url, validate_certs=self.proxy_validate_certs)
+ if not about["runningAsProxy"]:
+ self.module.fail_json(msg="Web Services is not running as a proxy!")
+ except Exception as error:
+ self.module.fail_json(msg="Proxy is not available! Check proxy_url. Error [%s]." % to_native(error))
+
+ def test_systems_found(self, systems_found, serial, label, addresses):
+ """Verify and build api urls."""
+ api_urls = []
+ for address in addresses:
+ for port in self.ports:
+ if port == "8080":
+ url = "http://%s:%s/devmgr/" % (address, port)
+ else:
+ url = "https://%s:%s/devmgr/" % (address, port)
+
+ try:
+ rc, response = request(url + "utils/about", validate_certs=False, timeout=self.SEARCH_TIMEOUT)
+ api_urls.append(url + "v2/")
+ break
+ except Exception as error:
+ pass
+ systems_found.update({serial: {"api_urls": api_urls,
+ "label": label,
+ "addresses": addresses,
+ "proxy_ssid": "",
+ "proxy_required": False}})
+
+ def proxy_discover(self):
+ """Search for array using it's chassis serial from web services proxy."""
+ self.verify_proxy_service()
+ subnet = ipaddress.ip_network(u"%s" % self.subnet_mask)
+
+ try:
+ rc, request_id = request(self.proxy_url + "discovery", method="POST", validate_certs=self.proxy_validate_certs,
+ force_basic_auth=True, url_username=self.proxy_username, url_password=self.proxy_password,
+ data=json.dumps({"startIP": str(subnet[0]), "endIP": str(subnet[-1]),
+ "connectionTimeout": self.DEFAULT_CONNECTION_TIMEOUT_SEC}))
+
+ # Wait for discover to complete
+ try:
+ for iteration in range(self.DEFAULT_DISCOVERY_TIMEOUT_SEC):
+ rc, discovered_systems = request(self.proxy_url + "discovery?requestId=%s" % request_id["requestId"],
+ validate_certs=self.proxy_validate_certs,
+ force_basic_auth=True, url_username=self.proxy_username, url_password=self.proxy_password)
+ if not discovered_systems["discoverProcessRunning"]:
+ thread_pool = []
+ for discovered_system in discovered_systems["storageSystems"]:
+ addresses = []
+ for controller in discovered_system["controllers"]:
+ addresses.extend(controller["ipAddresses"])
+
+ # Storage systems with embedded web services.
+ if "https" in discovered_system["supportedManagementPorts"] and self.prefer_embedded:
+
+ thread = threading.Thread(target=self.test_systems_found,
+ args=(self.systems_found, discovered_system["serialNumber"], discovered_system["label"], addresses))
+ thread_pool.append(thread)
+ thread.start()
+
+ # Storage systems without embedded web services.
+ else:
+ self.systems_found.update({discovered_system["serialNumber"]: {"api_urls": [self.proxy_url],
+ "label": discovered_system["label"],
+ "addresses": addresses,
+ "proxy_ssid": "",
+ "proxy_required": True}})
+ for thread in thread_pool:
+ thread.join()
+ break
+ sleep(1)
+ else:
+ self.module.fail_json(msg="Timeout waiting for array discovery process. Subnet [%s]" % self.subnet_mask)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to get the discovery results. Error [%s]." % to_native(error))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to initiate array discovery. Error [%s]." % to_native(error))
+
+ def update_proxy_with_proxy_ssid(self):
+ """Determine the current proxy ssid for all discovered-proxy_required storage systems."""
+ # Discover all added storage systems to the proxy.
+ systems = []
+ try:
+ rc, systems = request(self.proxy_url + "storage-systems", validate_certs=self.proxy_validate_certs,
+ force_basic_auth=True, url_username=self.proxy_username, url_password=self.proxy_password)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to ascertain storage systems added to Web Services Proxy.")
+
+ for system_key, system_info in self.systems_found.items():
+ if self.systems_found[system_key]["proxy_required"]:
+ for system in systems:
+ if system_key == system["chassisSerialNumber"]:
+ self.systems_found[system_key]["proxy_ssid"] = system["id"]
+
+ def discover(self):
+ """Discover E-Series storage systems."""
+ if self.proxy_url:
+ self.proxy_discover()
+ self.update_proxy_with_proxy_ssid()
+ else:
+ self.no_proxy_discover()
+
+ self.module.exit_json(msg="Discover process complete.", systems_found=self.systems_found, changed=False)
+
+
+def main():
+ discover = NetAppESeriesDiscover()
+ discover.discover()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_drive_firmware.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_drive_firmware.py
new file mode 100644
index 00000000..cbde9395
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_drive_firmware.py
@@ -0,0 +1,209 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_drive_firmware
+short_description: NetApp E-Series manage drive firmware
+description:
+ - Ensure drive firmware version is activated on specified drive model.
+author:
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ firmware:
+ description:
+ - list of drive firmware file paths.
+ - NetApp E-Series drives require special firmware which can be downloaded from https://mysupport.netapp.com/NOW/download/tools/diskfw_eseries/
+ type: list
+ required: True
+ wait_for_completion:
+ description:
+ - This flag will cause module to wait for any upgrade actions to complete.
+ type: bool
+ default: false
+ ignore_inaccessible_drives:
+ description:
+ - This flag will determine whether drive firmware upgrade should fail if any affected drives are inaccessible.
+ type: bool
+ default: false
+ upgrade_drives_online:
+ description:
+ - This flag will determine whether drive firmware can be upgrade while drives are accepting I/O.
+ - When I(upgrade_drives_online==False) stop all I/O before running task.
+ type: bool
+ default: true
+"""
+EXAMPLES = """
+- name: Ensure correct firmware versions
+ na_santricity_drive_firmware:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ firmware: "path/to/drive_firmware"
+ wait_for_completion: true
+ ignore_inaccessible_drives: false
+"""
+RETURN = """
+msg:
+ description: Whether any drive firmware was upgraded and whether it is in progress.
+ type: str
+ returned: always
+ sample:
+ { changed: True, upgrade_in_process: True }
+"""
+import os
+import re
+
+from time import sleep
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule, create_multipart_formdata, request
+from ansible.module_utils._text import to_native
+
+
+class NetAppESeriesDriveFirmware(NetAppESeriesModule):
+ WAIT_TIMEOUT_SEC = 60 * 15
+
+ def __init__(self):
+ ansible_options = dict(
+ firmware=dict(type="list", required=True),
+ wait_for_completion=dict(type="bool", default=False),
+ ignore_inaccessible_drives=dict(type="bool", default=False),
+ upgrade_drives_online=dict(type="bool", default=True))
+
+ super(NetAppESeriesDriveFirmware, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ supports_check_mode=True)
+
+ args = self.module.params
+ self.firmware_list = args["firmware"]
+ self.wait_for_completion = args["wait_for_completion"]
+ self.ignore_inaccessible_drives = args["ignore_inaccessible_drives"]
+ self.upgrade_drives_online = args["upgrade_drives_online"]
+
+ self.upgrade_list_cache = None
+
+ self.upgrade_required_cache = None
+ self.upgrade_in_progress = False
+ self.drive_info_cache = None
+
+ def upload_firmware(self):
+ """Ensure firmware has been upload prior to uploaded."""
+ for firmware in self.firmware_list:
+ firmware_name = os.path.basename(firmware)
+ files = [("file", firmware_name, firmware)]
+ headers, data = create_multipart_formdata(files)
+ try:
+ rc, response = self.request("/files/drive", method="POST", headers=headers, data=data)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to upload drive firmware [%s]. Array [%s]. Error [%s]." % (firmware_name, self.ssid, to_native(error)))
+
+ def upgrade_list(self):
+ """Determine whether firmware is compatible with the specified drives."""
+ if self.upgrade_list_cache is None:
+ self.upgrade_list_cache = list()
+ try:
+ rc, response = self.request("storage-systems/%s/firmware/drives" % self.ssid)
+
+ # Create upgrade list, this ensures only the firmware uploaded is applied
+ for firmware in self.firmware_list:
+ filename = os.path.basename(firmware)
+
+ for uploaded_firmware in response["compatibilities"]:
+ if uploaded_firmware["filename"] == filename:
+
+ # Determine whether upgrade is required
+ drive_reference_list = []
+ for drive in uploaded_firmware["compatibleDrives"]:
+ try:
+ rc, drive_info = self.request("storage-systems/%s/drives/%s" % (self.ssid, drive["driveRef"]))
+
+ # Add drive references that are supported and differ from current firmware
+ if (drive_info["firmwareVersion"] != uploaded_firmware["firmwareVersion"] and
+ uploaded_firmware["firmwareVersion"] in uploaded_firmware["supportedFirmwareVersions"]):
+
+ if self.ignore_inaccessible_drives or (not drive_info["offline"] and drive_info["available"]):
+ drive_reference_list.append(drive["driveRef"])
+
+ if not drive["onlineUpgradeCapable"] and self.upgrade_drives_online:
+ self.module.fail_json(msg="Drive is not capable of online upgrade. Array [%s]. Drive [%s]."
+ % (self.ssid, drive["driveRef"]))
+
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve drive information. Array [%s]. Drive [%s]. Error [%s]."
+ % (self.ssid, drive["driveRef"], to_native(error)))
+
+ if drive_reference_list:
+ self.upgrade_list_cache.extend([{"filename": filename, "driveRefList": drive_reference_list}])
+
+ except Exception as error:
+ self.module.fail_json(msg="Failed to complete compatibility and health check. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ return self.upgrade_list_cache
+
+ def wait_for_upgrade_completion(self):
+ """Wait for drive firmware upgrade to complete."""
+ drive_references = [reference for drive in self.upgrade_list() for reference in drive["driveRefList"]]
+ last_status = None
+ for attempt in range(int(self.WAIT_TIMEOUT_SEC / 5)):
+ try:
+ rc, response = self.request("storage-systems/%s/firmware/drives/state" % self.ssid)
+
+ # Check drive status
+ for status in response["driveStatus"]:
+ last_status = status
+ if status["driveRef"] in drive_references:
+ if status["status"] == "okay":
+ continue
+ elif status["status"] in ["inProgress", "inProgressRecon", "pending", "notAttempted"]:
+ break
+ else:
+ self.module.fail_json(msg="Drive firmware upgrade failed. Array [%s]. Drive [%s]. Status [%s]."
+ % (self.ssid, status["driveRef"], status["status"]))
+ else:
+ self.upgrade_in_progress = False
+ break
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve drive status. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ sleep(5)
+ else:
+ self.module.fail_json(msg="Timed out waiting for drive firmware upgrade. Array [%s]. Status [%s]." % (self.ssid, last_status))
+
+ def upgrade(self):
+ """Apply firmware to applicable drives."""
+ try:
+ rc, response = self.request("storage-systems/%s/firmware/drives/initiate-upgrade?onlineUpdate=%s"
+ % (self.ssid, "true" if self.upgrade_drives_online else "false"), method="POST", data=self.upgrade_list())
+ self.upgrade_in_progress = True
+ except Exception as error:
+ self.module.fail_json(msg="Failed to upgrade drive firmware. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ if self.wait_for_completion:
+ self.wait_for_upgrade_completion()
+
+ def apply(self):
+ """Apply firmware policy has been enforced on E-Series storage system."""
+ self.upload_firmware()
+
+ if self.upgrade_list() and not self.module.check_mode:
+ self.upgrade()
+
+ self.module.exit_json(changed=True if self.upgrade_list() else False,
+ upgrade_in_process=self.upgrade_in_progress)
+
+
+def main():
+ drive_firmware = NetAppESeriesDriveFirmware()
+ drive_firmware.apply()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_facts.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_facts.py
new file mode 100644
index 00000000..55851692
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_facts.py
@@ -0,0 +1,918 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+module: na_santricity_facts
+short_description: NetApp E-Series retrieve facts about NetApp E-Series storage arrays
+description:
+ - The na_santricity_facts module returns a collection of facts regarding NetApp E-Series storage arrays.
+author:
+ - Kevin Hulquest (@hulquest)
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+'''
+
+EXAMPLES = """
+---
+- name: Get array facts
+ na_santricity_facts:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+"""
+
+RETURN = """
+ msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample:
+ - Gathered facts for storage array. Array ID [1].
+ - Gathered facts for web services proxy.
+ storage_array_facts:
+ description: provides details about the array, controllers, management interfaces, hostside interfaces,
+ driveside interfaces, disks, storage pools, volumes, snapshots, and features.
+ returned: on successful inquiry from from embedded web services rest api
+ type: complex
+ contains:
+ netapp_controllers:
+ description: storage array controller list that contains basic controller identification and status
+ type: complex
+ sample:
+ - [{"name": "A", "serial": "021632007299", "status": "optimal"},
+ {"name": "B", "serial": "021632007300", "status": "failed"}]
+ netapp_disks:
+ description: drive list that contains identification, type, and status information for each drive
+ type: complex
+ sample:
+ - [{"available": false,
+ "firmware_version": "MS02",
+ "id": "01000000500003960C8B67880000000000000000",
+ "media_type": "ssd",
+ "product_id": "PX02SMU080 ",
+ "serial_number": "15R0A08LT2BA",
+ "status": "optimal",
+ "tray_ref": "0E00000000000000000000000000000000000000",
+ "usable_bytes": "799629205504" }]
+ netapp_driveside_interfaces:
+ description: drive side interface list that contains identification, type, and speed for each interface
+ type: complex
+ sample:
+ - [{ "controller": "A", "interface_speed": "12g", "interface_type": "sas" }]
+ - [{ "controller": "B", "interface_speed": "10g", "interface_type": "iscsi" }]
+ netapp_enabled_features:
+ description: specifies the enabled features on the storage array.
+ returned: on success
+ type: complex
+ sample:
+ - [ "flashReadCache", "performanceTier", "protectionInformation", "secureVolume" ]
+ netapp_host_groups:
+ description: specifies the host groups on the storage arrays.
+ returned: on success
+ type: complex
+ sample:
+ - [{ "id": "85000000600A098000A4B28D003610705C40B964", "name": "group1" }]
+ netapp_hosts:
+ description: specifies the hosts on the storage arrays.
+ returned: on success
+ type: complex
+ sample:
+ - [{ "id": "8203800000000000000000000000000000000000",
+ "name": "host1",
+ "group_id": "85000000600A098000A4B28D003610705C40B964",
+ "host_type_index": 28,
+ "ports": [{ "type": "fc", "address": "1000FF7CFFFFFF01", "label": "FC_1" },
+ { "type": "fc", "address": "1000FF7CFFFFFF00", "label": "FC_2" }]}]
+ netapp_host_types:
+ description: lists the available host types on the storage array.
+ returned: on success
+ type: complex
+ sample:
+ - [{ "index": 0, "type": "FactoryDefault" },
+ { "index": 1, "type": "W2KNETNCL"},
+ { "index": 2, "type": "SOL" },
+ { "index": 5, "type": "AVT_4M" },
+ { "index": 6, "type": "LNX" },
+ { "index": 7, "type": "LnxALUA" },
+ { "index": 8, "type": "W2KNETCL" },
+ { "index": 9, "type": "AIX MPIO" },
+ { "index": 10, "type": "VmwTPGSALUA" },
+ { "index": 15, "type": "HPXTPGS" },
+ { "index": 17, "type": "SolTPGSALUA" },
+ { "index": 18, "type": "SVC" },
+ { "index": 22, "type": "MacTPGSALUA" },
+ { "index": 23, "type": "WinTPGSALUA" },
+ { "index": 24, "type": "LnxTPGSALUA" },
+ { "index": 25, "type": "LnxTPGSALUA_PM" },
+ { "index": 26, "type": "ONTAP_ALUA" },
+ { "index": 27, "type": "LnxTPGSALUA_SF" },
+ { "index": 28, "type": "LnxDHALUA" },
+ { "index": 29, "type": "ATTOClusterAllOS" }]
+ netapp_hostside_interfaces:
+ description: host side interface list that contains identification, configuration, type, speed, and
+ status information for each interface
+ type: complex
+ sample:
+ - [{"iscsi":
+ [{ "controller": "A",
+ "current_interface_speed": "10g",
+ "ipv4_address": "10.10.10.1",
+ "ipv4_enabled": true,
+ "ipv4_gateway": "10.10.10.1",
+ "ipv4_subnet_mask": "255.255.255.0",
+ "ipv6_enabled": false,
+ "iqn": "iqn.1996-03.com.netapp:2806.600a098000a81b6d0000000059d60c76",
+ "link_status": "up",
+ "mtu": 9000,
+ "supported_interface_speeds": [ "10g" ] }]}]
+ netapp_management_interfaces:
+ description: management interface list that contains identification, configuration, and status for
+ each interface
+ type: complex
+ sample:
+ - [{"alias": "ict-2800-A",
+ "channel": 1,
+ "controller": "A",
+ "dns_config_method": "dhcp",
+ "dns_servers": [],
+ "ipv4_address": "10.1.1.1",
+ "ipv4_address_config_method": "static",
+ "ipv4_enabled": true,
+ "ipv4_gateway": "10.113.1.1",
+ "ipv4_subnet_mask": "255.255.255.0",
+ "ipv6_enabled": false,
+ "link_status": "up",
+ "mac_address": "00A098A81B5D",
+ "name": "wan0",
+ "ntp_config_method": "disabled",
+ "ntp_servers": [],
+ "remote_ssh_access": false }]
+ netapp_storage_array:
+ description: provides storage array identification, firmware version, and available capabilities
+ type: dict
+ sample:
+ - {"chassis_serial": "021540006043",
+ "firmware": "08.40.00.01",
+ "name": "ict-2800-11_40",
+ "wwn": "600A098000A81B5D0000000059D60C76",
+ "cacheBlockSizes": [4096,
+ 8192,
+ 16384,
+ 32768],
+ "supportedSegSizes": [8192,
+ 16384,
+ 32768,
+ 65536,
+ 131072,
+ 262144,
+ 524288]}
+ netapp_storage_pools:
+ description: storage pool list that contains identification and capacity information for each pool
+ type: complex
+ sample:
+ - [{"available_capacity": "3490353782784",
+ "id": "04000000600A098000A81B5D000002B45A953A61",
+ "name": "Raid6",
+ "total_capacity": "5399466745856",
+ "used_capacity": "1909112963072" }]
+ netapp_volumes:
+ description: storage volume list that contains identification and capacity information for each volume
+ type: complex
+ sample:
+ - [{"capacity": "5368709120",
+ "id": "02000000600A098000AAC0C3000002C45A952BAA",
+ "is_thin_provisioned": false,
+ "name": "5G",
+ "parent_storage_pool_id": "04000000600A098000A81B5D000002B45A953A61" }]
+ netapp_workload_tags:
+ description: workload tag list
+ type: complex
+ sample:
+ - [{"id": "87e19568-43fb-4d8d-99ea-2811daaa2b38",
+ "name": "ftp_server",
+ "workloadAttributes": [{"key": "use",
+ "value": "general"}]}]
+ netapp_volumes_by_initiators:
+ description: list of available volumes keyed by the mapped initiators.
+ type: complex
+ sample:
+ - {"beegfs_host": [{"id": "02000000600A098000A4B9D1000015FD5C8F7F9E",
+ "meta_data": {"filetype": "ext4", "public": true},
+ "name": "some_volume",
+ "workload_name": "beegfs_metadata",
+ "workload_metadata: {"filetype": "ext4", "public": true},
+ "volume_metadata: {"format_type": "ext4",
+ "format_options": "-i 2048 -I 512 -J size=400 -Odir_index,filetype",
+ "mount_options": "noatime,nodiratime,nobarrier,_netdev",
+ "mount_directory": "/data/beegfs/"},
+ "host_types": ["nvmeof"],
+ "eui": "0000139A3885FA4500A0980000EAA272V",
+ "wwn": "600A098000A4B9D1000015FD5C8F7F9E"}]}
+ snapshot_images:
+ description: snapshot image list that contains identification, capacity, and status information for each
+ snapshot image
+ type: complex
+ sample:
+ - [{"active_cow": true,
+ "creation_method": "user",
+ "id": "34000000600A098000A81B5D00630A965B0535AC",
+ "pit_capacity": "5368709120",
+ "reposity_cap_utilization": "0",
+ "rollback_source": false,
+ "status": "optimal" }]
+ proxy_facts:
+ description: proxy storage system list
+ returned: on successful inquiry from from web services proxy's rest api
+ type: complex
+ contains:
+ ssid:
+ description: storage system id
+ type: str
+ sample: "ec8ed9d2-eba3-4cac-88fb-0954f327f1d4"
+ name:
+ description: storage system name
+ type: str
+ sample: "EF570-NVMe"
+ wwn:
+ description: storage system unique identifier
+ type: str
+ sample: "AC1100051E1E1E1E1E1E1E1E1E1E1E1E"
+ model:
+ description: NetApp E-Series model number
+ type: str
+ sample: "5700"
+ controller:
+ description: controller list that contains identification, ip addresses, and certificate information for
+ each controller
+ type: complex
+ sample: [{"certificateStatus": "selfSigned",
+ "controllerId": "070000000000000000000001",
+ "ipAddresses": ["172.17.0.5", "3.3.3.3"]}]
+ drive_types:
+ description: all available storage system drive types
+ type: list
+ sample: ["sas", "fibre"]
+ unconfigured_space:
+ description: unconfigured storage system space in bytes
+ type: str
+ sample: "982259020595200"
+ array_status:
+ description: storage system status
+ type: str
+ sample: "optimal"
+ password_status:
+ description: storage system password status
+ type: str
+ sample: "invalid"
+ certificate_status:
+ description: storage system ssl certificate status
+ type: str
+ sample: "untrusted"
+ firmware_version:
+ description: storage system install firmware version
+ type: str
+ sample: "08.50.42.99"
+ chassis_serial:
+ description: storage system chassis serial number
+ type: str
+ sample: "SX0810032"
+ asup_enabled:
+ description: storage system auto-support status
+ type: bool
+ sample: True
+"""
+
+import re
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+try:
+ from ansible.module_utils.ansible_release import __version__ as ansible_version
+except ImportError:
+ ansible_version = 'unknown'
+
+try:
+ from urlparse import urlparse, urlunparse
+except ImportError:
+ from urllib.parse import urlparse, urlunparse
+
+
+class Facts(NetAppESeriesModule):
+ def __init__(self):
+ web_services_version = "02.00.0000.0000"
+ super(Facts, self).__init__(ansible_options={},
+ web_services_version=web_services_version,
+ supports_check_mode=True)
+
+ def get_controllers(self):
+ """Retrieve a mapping of controller references to their labels."""
+ controllers = list()
+ try:
+ rc, controllers = self.request('storage-systems/%s/graph/xpath-filter?query=/controller/id' % self.ssid)
+ except Exception as err:
+ self.module.fail_json(
+ msg="Failed to retrieve controller list! Array Id [%s]. Error [%s]."
+ % (self.ssid, str(err)))
+
+ controllers.sort()
+
+ controllers_dict = {}
+ i = ord('A')
+ for controller in controllers:
+ label = chr(i)
+ controllers_dict[controller] = label
+ i += 1
+
+ return controllers_dict
+
+ def get_array_facts(self):
+ """Extract particular facts from the storage array graph"""
+ facts = dict(facts_from_proxy=(not self.is_embedded()), ssid=self.ssid)
+ controller_reference_label = self.get_controllers()
+ array_facts = None
+ hardware_inventory_facts = None
+
+ # Get the storage array graph
+ try:
+ rc, array_facts = self.request("storage-systems/%s/graph" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to obtain facts from storage array with id [%s]. Error [%s]" % (self.ssid, str(error)))
+
+ # Get the storage array hardware inventory
+ try:
+ rc, hardware_inventory_facts = self.request("storage-systems/%s/hardware-inventory" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to obtain hardware inventory from storage array with id [%s]. Error [%s]" % (self.ssid, str(error)))
+
+ facts['netapp_storage_array'] = dict(
+ name=array_facts['sa']['saData']['storageArrayLabel'],
+ chassis_serial=array_facts['sa']['saData']['chassisSerialNumber'],
+ firmware=array_facts['sa']['saData']['fwVersion'],
+ wwn=array_facts['sa']['saData']['saId']['worldWideName'],
+ segment_sizes=array_facts['sa']['featureParameters']['supportedSegSizes'],
+ cache_block_sizes=array_facts['sa']['featureParameters']['cacheBlockSizes'])
+
+ facts['netapp_controllers'] = [
+ dict(
+ name=controller_reference_label[controller['controllerRef']],
+ serial=controller['serialNumber'].strip(),
+ status=controller['status'],
+ ) for controller in array_facts['controller']]
+
+ facts['netapp_hosts'] = [
+ dict(
+ group_id=host['clusterRef'],
+ hosts_reference=host['hostRef'],
+ id=host['id'],
+ name=host['name'],
+ host_type_index=host['hostTypeIndex'],
+ ports=host['hostSidePorts']
+ ) for host in array_facts['storagePoolBundle']['host']]
+
+ facts['netapp_host_groups'] = [
+ dict(
+ id=group['id'],
+ name=group['name'],
+ hosts=[host['name'] for host in facts['netapp_hosts'] if host['group_id'] == group['id']]
+ ) for group in array_facts['storagePoolBundle']['cluster']]
+ facts['netapp_host_groups'].append(dict(
+ id='0000000000000000000000000000000000000000',
+ name='default_hostgroup',
+ hosts=[host["name"] for host in facts['netapp_hosts'] if host['group_id'] == '0000000000000000000000000000000000000000']))
+
+ facts['netapp_host_types'] = [
+ dict(
+ type=host_type['hostType'],
+ index=host_type['index']
+ ) for host_type in array_facts['sa']['hostSpecificVals']
+ if 'hostType' in host_type.keys() and host_type['hostType']
+ # This conditional ignores zero-length strings which indicates that the associated host-specific NVSRAM region has been cleared.
+ ]
+
+ facts['snapshot_images'] = [
+ dict(
+ id=snapshot['id'],
+ status=snapshot['status'],
+ pit_capacity=snapshot['pitCapacity'],
+ creation_method=snapshot['creationMethod'],
+ reposity_cap_utilization=snapshot['repositoryCapacityUtilization'],
+ active_cow=snapshot['activeCOW'],
+ rollback_source=snapshot['isRollbackSource']
+ ) for snapshot in array_facts['highLevelVolBundle']['pit']]
+
+ facts['netapp_disks'] = [
+ dict(
+ id=disk['id'],
+ available=disk['available'],
+ media_type=disk['driveMediaType'],
+ status=disk['status'],
+ usable_bytes=disk['usableCapacity'],
+ tray_ref=disk['physicalLocation']['trayRef'],
+ product_id=disk['productID'],
+ firmware_version=disk['firmwareVersion'],
+ serial_number=disk['serialNumber'].lstrip()
+ ) for disk in array_facts['drive']]
+
+ facts['netapp_management_interfaces'] = [
+ dict(controller=controller_reference_label[controller['controllerRef']],
+ name=iface['ethernet']['interfaceName'],
+ alias=iface['ethernet']['alias'],
+ channel=iface['ethernet']['channel'],
+ mac_address=iface['ethernet']['macAddr'],
+ remote_ssh_access=iface['ethernet']['rloginEnabled'],
+ link_status=iface['ethernet']['linkStatus'],
+ ipv4_enabled=iface['ethernet']['ipv4Enabled'],
+ ipv4_address_config_method=iface['ethernet']['ipv4AddressConfigMethod'].lower().replace("config", ""),
+ ipv4_address=iface['ethernet']['ipv4Address'],
+ ipv4_subnet_mask=iface['ethernet']['ipv4SubnetMask'],
+ ipv4_gateway=iface['ethernet']['ipv4GatewayAddress'],
+ ipv6_enabled=iface['ethernet']['ipv6Enabled'],
+ dns_config_method=iface['ethernet']['dnsProperties']['acquisitionProperties']['dnsAcquisitionType'],
+ dns_servers=(iface['ethernet']['dnsProperties']['acquisitionProperties']['dnsServers']
+ if iface['ethernet']['dnsProperties']['acquisitionProperties']['dnsServers'] else []),
+ ntp_config_method=iface['ethernet']['ntpProperties']['acquisitionProperties']['ntpAcquisitionType'],
+ ntp_servers=(iface['ethernet']['ntpProperties']['acquisitionProperties']['ntpServers']
+ if iface['ethernet']['ntpProperties']['acquisitionProperties']['ntpServers'] else [])
+ ) for controller in array_facts['controller'] for iface in controller['netInterfaces']]
+
+ facts['netapp_hostside_interfaces'] = [
+ dict(
+ fc=[dict(controller=controller_reference_label[controller['controllerRef']],
+ channel=iface['fibre']['channel'],
+ link_status=iface['fibre']['linkStatus'],
+ current_interface_speed=strip_interface_speed(iface['fibre']['currentInterfaceSpeed']),
+ maximum_interface_speed=strip_interface_speed(iface['fibre']['maximumInterfaceSpeed']))
+ for controller in array_facts['controller']
+ for iface in controller['hostInterfaces']
+ if iface['interfaceType'] == 'fc'],
+ ib=[dict(controller=controller_reference_label[controller['controllerRef']],
+ channel=iface['ib']['channel'],
+ link_status=iface['ib']['linkState'],
+ mtu=iface['ib']['maximumTransmissionUnit'],
+ current_interface_speed=strip_interface_speed(iface['ib']['currentSpeed']),
+ maximum_interface_speed=strip_interface_speed(iface['ib']['supportedSpeed']))
+ for controller in array_facts['controller']
+ for iface in controller['hostInterfaces']
+ if iface['interfaceType'] == 'ib'],
+ iscsi=[dict(controller=controller_reference_label[controller['controllerRef']],
+ iqn=iface['iscsi']['iqn'],
+ link_status=iface['iscsi']['interfaceData']['ethernetData']['linkStatus'],
+ ipv4_enabled=iface['iscsi']['ipv4Enabled'],
+ ipv4_address=iface['iscsi']['ipv4Data']['ipv4AddressData']['ipv4Address'],
+ ipv4_subnet_mask=iface['iscsi']['ipv4Data']['ipv4AddressData']['ipv4SubnetMask'],
+ ipv4_gateway=iface['iscsi']['ipv4Data']['ipv4AddressData']['ipv4GatewayAddress'],
+ ipv6_enabled=iface['iscsi']['ipv6Enabled'],
+ mtu=iface['iscsi']['interfaceData']['ethernetData']['maximumFramePayloadSize'],
+ current_interface_speed=strip_interface_speed(iface['iscsi']['interfaceData']
+ ['ethernetData']['currentInterfaceSpeed']),
+ supported_interface_speeds=strip_interface_speed(iface['iscsi']['interfaceData']
+ ['ethernetData']
+ ['supportedInterfaceSpeeds']))
+ for controller in array_facts['controller']
+ for iface in controller['hostInterfaces']
+ if iface['interfaceType'] == 'iscsi' and iface['iscsi']['interfaceData']['type'] == 'ethernet'],
+ sas=[dict(controller=controller_reference_label[controller['controllerRef']],
+ channel=iface['sas']['channel'],
+ current_interface_speed=strip_interface_speed(iface['sas']['currentInterfaceSpeed']),
+ maximum_interface_speed=strip_interface_speed(iface['sas']['maximumInterfaceSpeed']),
+ link_status=iface['sas']['iocPort']['state'])
+ for controller in array_facts['controller']
+ for iface in controller['hostInterfaces']
+ if iface['interfaceType'] == 'sas'])]
+
+ facts['netapp_driveside_interfaces'] = [
+ dict(
+ controller=controller_reference_label[controller['controllerRef']],
+ interface_type=interface['interfaceType'],
+ interface_speed=strip_interface_speed(
+ interface[interface['interfaceType']]['maximumInterfaceSpeed']
+ if (interface['interfaceType'] == 'sata' or
+ interface['interfaceType'] == 'sas' or
+ interface['interfaceType'] == 'fibre')
+ else (
+ interface[interface['interfaceType']]['currentSpeed']
+ if interface['interfaceType'] == 'ib'
+ else (
+ interface[interface['interfaceType']]['interfaceData']['maximumInterfaceSpeed']
+ if interface['interfaceType'] == 'iscsi' else 'unknown'
+ ))),
+ )
+ for controller in array_facts['controller']
+ for interface in controller['driveInterfaces']]
+
+ facts['netapp_storage_pools'] = [
+ dict(
+ id=storage_pool['id'],
+ name=storage_pool['name'],
+ available_capacity=storage_pool['freeSpace'],
+ total_capacity=storage_pool['totalRaidedSpace'],
+ used_capacity=storage_pool['usedSpace']
+ ) for storage_pool in array_facts['volumeGroup']]
+
+ all_volumes = list(array_facts['volume'])
+
+ facts['netapp_volumes'] = [
+ dict(
+ id=v['id'],
+ name=v['name'],
+ parent_storage_pool_id=v['volumeGroupRef'],
+ capacity=v['capacity'],
+ is_thin_provisioned=v['thinProvisioned'],
+ workload=v['metadata'],
+
+ ) for v in all_volumes]
+
+ lun_mappings = dict()
+ for host in facts['netapp_hosts']:
+ lun_mappings.update({host["name"]: []})
+ for host in facts['netapp_host_groups']:
+ lun_mappings.update({host["name"]: []})
+
+ facts['netapp_default_hostgroup_access_volume_lun'] = None
+ for lun in [a['lun'] for a in array_facts['storagePoolBundle']['lunMapping']
+ if a['type'] == 'all' and a['mapRef'] == '0000000000000000000000000000000000000000']:
+ facts['netapp_default_hostgroup_access_volume_lun'] = lun
+
+ # Get all host mappings
+ host_mappings = dict()
+ for host_mapping in [h for h in array_facts['storagePoolBundle']['lunMapping'] if h['type'] == 'host']:
+ for host_name in [h['name'] for h in facts['netapp_hosts'] if h['id'] == host_mapping['mapRef']]:
+ for volume in [v['name'] for v in facts['netapp_volumes'] if v['id'] == host_mapping['volumeRef']]:
+ if host_name in host_mappings.keys():
+ host_mappings[host_name].append((volume, host_mapping['lun']))
+ else:
+ host_mappings[host_name] = [(volume, host_mapping['lun'])]
+
+ # Get all host group mappings
+ group_mappings = dict()
+ for group_mapping in [h for h in array_facts['storagePoolBundle']['lunMapping'] if h['type'] == 'cluster']:
+ for group_name, group_hosts in [(g['name'], g['hosts']) for g in facts['netapp_host_groups'] if g['id'] == group_mapping['mapRef']]:
+ for volume in [v['name'] for v in facts['netapp_volumes'] if v['id'] == group_mapping['volumeRef']]:
+ if group_name in group_mappings.keys():
+ group_mappings[group_name].append((volume, group_mapping['lun']))
+ else:
+ group_mappings[group_name] = [(volume, group_mapping['lun'])]
+
+ for host_name in [h for h in group_hosts if h in host_mappings.keys()]:
+ if host_name in host_mappings.keys():
+ host_mappings[host_name].append((volume, group_mapping['lun']))
+ else:
+ host_mappings[host_name] = [(volume, group_mapping['lun'])]
+
+ facts['netapp_luns_by_target'] = lun_mappings
+ if host_mappings:
+ facts['netapp_luns_by_target'].update(host_mappings)
+ if group_mappings:
+ facts['netapp_luns_by_target'].update(group_mappings)
+
+ # Add all host mappings to respective groups mappings
+ for host_group in facts['netapp_host_groups']:
+ group_name = host_group['name']
+ for host in host_group['hosts']:
+ facts['netapp_luns_by_target'][group_name].extend(facts['netapp_luns_by_target'][host])
+
+ # Remove duplicate entries
+ for obj in facts['netapp_luns_by_target'].keys():
+ tmp = dict(facts['netapp_luns_by_target'][obj])
+ facts['netapp_luns_by_target'][obj] = [(k, tmp[k]) for k in tmp.keys()]
+
+ workload_tags = None
+ try:
+ rc, workload_tags = self.request("storage-systems/%s/workloads" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve workload tags. Array [%s]." % self.ssid)
+
+ facts['netapp_workload_tags'] = [
+ dict(
+ id=workload_tag['id'],
+ name=workload_tag['name'],
+ attributes=workload_tag['workloadAttributes']
+ ) for workload_tag in workload_tags]
+
+ targets = array_facts["storagePoolBundle"]["target"]
+
+ facts['netapp_hostside_io_interfaces'] = []
+ if "ioInterface" in array_facts:
+ for interface in array_facts["ioInterface"]:
+
+ # Select only the host side channels
+ if interface["channelType"] == "hostside":
+ interface_type = interface["ioInterfaceTypeData"]["interfaceType"]
+ interface_data = interface["ioInterfaceTypeData"]["fibre" if interface_type == "fc" else interface_type]
+ command_protocol_properties = interface["commandProtocolPropertiesList"]["commandProtocolProperties"]
+
+ # Build generic information for each interface entry
+ interface_info = {"protocol": "unknown",
+ "interface_reference": interface_data["interfaceRef"],
+ "controller_reference": interface["controllerRef"],
+ "channel_port_reference": interface_data["channelPortRef"] if "channelPortRef" in interface_data else "",
+ "controller": controller_reference_label[interface["controllerRef"]],
+ "channel": interface_data["channel"],
+ "part": "unknown",
+ "link_status": "unknown",
+ "speed": {"current": "unknown", "maximum": "unknown", "supported": []},
+ "mtu": None,
+ "guid": None,
+ "lid": None,
+ "nqn": None,
+ "iqn": None,
+ "wwpn": None,
+ "ipv4": None, # enabled, config_method, address, subnet, gateway
+ "ipv6": None} # for expansion if needed
+
+ # Add target information
+ for target in targets:
+ if target["nodeName"]["ioInterfaceType"] == "nvmeof":
+ interface_info.update({"nqn": target["nodeName"]["nvmeNodeName"]})
+ if target["nodeName"]["ioInterfaceType"] == "iscsi":
+ interface_info.update({"iqn": target["nodeName"]["iscsiNodeName"]})
+
+ # iSCSI IO interface
+ if interface_type == "iscsi":
+ interface_info.update({"ipv4": {"enabled": interface_data["ipv4Enabled"],
+ "config_method": interface_data["ipv4Data"]["ipv4AddressConfigMethod"],
+ "address": interface_data["ipv4Data"]["ipv4AddressData"]["ipv4Address"],
+ "subnet": interface_data["ipv4Data"]["ipv4AddressData"]["ipv4SubnetMask"],
+ "gateway": interface_data["ipv4Data"]["ipv4AddressData"]["ipv4GatewayAddress"]}})
+
+ # InfiniBand (iSER) protocol
+ if interface_data["interfaceData"]["type"] == "infiniband" and interface_data["interfaceData"]["infinibandData"]["isIser"]:
+ interface_info.update({"protocol": "ib_iser"})
+
+ # Get more details from hardware-inventory
+ for ib_port in hardware_inventory_facts["ibPorts"]:
+ if ib_port["channelPortRef"] == interface_info["channel_port_reference"]:
+ interface_info.update({"link_status": ib_port["linkState"],
+ "guid": ib_port["globalIdentifier"],
+ "lid": ib_port["localIdentifier"],
+ "speed": {"current": strip_interface_speed(ib_port["currentSpeed"]),
+ "maximum": strip_interface_speed(ib_port["supportedSpeed"])[-1],
+ "supported": strip_interface_speed(ib_port["supportedSpeed"])}})
+
+
+ # iSCSI protocol
+ elif interface_data["interfaceData"]["type"] == "ethernet":
+ ethernet_data = interface_data["interfaceData"]["ethernetData"]
+ interface_info.update({"protocol": "iscsi"})
+ interface_info.update({"part": "%s,%s" % (ethernet_data["partData"]["vendorName"], ethernet_data["partData"]["partNumber"]),
+ "link_status": ethernet_data["linkStatus"],
+ "mtu": ethernet_data["maximumFramePayloadSize"],
+ "speed": {"current": strip_interface_speed(ethernet_data["currentInterfaceSpeed"]),
+ "maximum": strip_interface_speed(ethernet_data["maximumInterfaceSpeed"]),
+ "supported": strip_interface_speed(ethernet_data["supportedInterfaceSpeeds"])}})
+
+ # Fibre Channel IO interface
+ elif interface_type == "fc":
+ interface_info.update({"wwpn": interface_data["addressId"],
+ "part": interface_data["part"],
+ "link_status": interface_data["linkStatus"],
+ "speed": {"current": strip_interface_speed(interface_data["currentInterfaceSpeed"]),
+ "maximum": strip_interface_speed(interface_data["maximumInterfaceSpeed"]),
+ "supported": "unknown"}})
+
+ # NVMe over fibre channel protocol
+ if (command_protocol_properties and command_protocol_properties[0]["commandProtocol"] == "nvme" and
+ command_protocol_properties[0]["nvmeProperties"]["commandSet"] == "nvmeof" and
+ command_protocol_properties[0]["nvmeProperties"]["nvmeofProperties"]["fcProperties"]):
+ interface_info.update({"protocol": "nvme_fc"})
+
+ # Fibre channel protocol
+ else:
+ interface_info.update({"protocol": "fc"})
+
+ # SAS IO interface
+ elif interface_type == "sas":
+ interface_info.update({"protocol": "sas",
+ "wwpn": interface_data["addressId"],
+ "part": interface_data["part"],
+ "speed": {"current": strip_interface_speed(interface_data["currentInterfaceSpeed"]),
+ "maximum": strip_interface_speed(interface_data["maximumInterfaceSpeed"]),
+ "supported": "unknown"}})
+
+ # Infiniband IO interface
+ elif interface_type == "ib":
+ interface_info.update({"link_status": interface_data["linkState"],
+ "speed": {"current": strip_interface_speed(interface_data["currentSpeed"]),
+ "maximum": strip_interface_speed(interface_data["supportedSpeed"])[-1],
+ "supported": strip_interface_speed(interface_data["supportedSpeed"])},
+ "mtu": interface_data["maximumTransmissionUnit"],
+ "guid": interface_data["globalIdentifier"],
+ "lid": interface_data["localIdentifier"]})
+
+ # Determine protocol (NVMe over Infiniband, InfiniBand iSER, InfiniBand SRP)
+ if interface_data["isNVMeSupported"]:
+ interface_info.update({"protocol": "nvme_ib"})
+ elif interface_data["isISERSupported"]:
+ interface_info.update({"protocol": "ib_iser"})
+ elif interface_data["isSRPSupported"]:
+ interface_info.update({"protocol": "ib_srp"})
+
+ # Determine command protocol information
+ if command_protocol_properties:
+ for command_protocol_property in command_protocol_properties:
+ if command_protocol_property["commandProtocol"] == "nvme":
+ if command_protocol_property["nvmeProperties"]["commandSet"] == "nvmeof":
+ ip_address_data = command_protocol_property["nvmeProperties"]["nvmeofProperties"]["ibProperties"]["ipAddressData"]
+ if ip_address_data["addressType"] == "ipv4":
+ interface_info.update({"ipv4": {"enabled": True,
+ "config_method": "configStatic",
+ "address": ip_address_data["ipv4Data"]["ipv4Address"],
+ "subnet": ip_address_data["ipv4Data"]["ipv4SubnetMask"],
+ "gateway": ip_address_data["ipv4Data"]["ipv4GatewayAddress"]}})
+
+ elif command_protocol_property["commandProtocol"] == "scsi":
+ if command_protocol_property["scsiProperties"]["scsiProtocolType"] == "iser":
+ ipv4_data = command_protocol_property["scsiProperties"]["iserProperties"]["ipv4Data"]
+ interface_info.update({"ipv4": {"enabled": True,
+ "config_method": ipv4_data["ipv4AddressConfigMethod"],
+ "address": ipv4_data["ipv4AddressData"]["ipv4Address"],
+ "subnet": ipv4_data["ipv4AddressData"]["ipv4SubnetMask"],
+ "gateway": ipv4_data["ipv4AddressData"]["ipv4GatewayAddress"]}})
+
+ # Ethernet IO interface
+ elif interface_type == "ethernet":
+ ethernet_data = interface_data["interfaceData"]["ethernetData"]
+ interface_info.update({"part": "%s,%s" % (ethernet_data["partData"]["vendorName"], ethernet_data["partData"]["partNumber"]),
+ "link_status": ethernet_data["linkStatus"],
+ "mtu": ethernet_data["maximumFramePayloadSize"],
+ "speed": {"current": strip_interface_speed(ethernet_data["currentInterfaceSpeed"]),
+ "maximum": strip_interface_speed(ethernet_data["maximumInterfaceSpeed"]),
+ "supported": strip_interface_speed(ethernet_data["supportedInterfaceSpeeds"])}})
+
+ # Determine command protocol information
+ if command_protocol_properties:
+ for command_protocol_property in command_protocol_properties:
+ if command_protocol_property["commandProtocol"] == "nvme":
+ if command_protocol_property["nvmeProperties"]["commandSet"] == "nvmeof":
+
+ nvmeof_properties = command_protocol_property["nvmeProperties"]["nvmeofProperties"]
+ if nvmeof_properties["provider"] == "providerRocev2":
+ ipv4_data = nvmeof_properties["roceV2Properties"]["ipv4Data"]
+ interface_info.update({"protocol": "nvme_roce"})
+ interface_info.update({"ipv4": {"enabled": nvmeof_properties["roceV2Properties"]["ipv4Enabled"],
+ "config_method": ipv4_data["ipv4AddressConfigMethod"],
+ "address": ipv4_data["ipv4AddressData"]["ipv4Address"],
+ "subnet": ipv4_data["ipv4AddressData"]["ipv4SubnetMask"],
+ "gateway": ipv4_data["ipv4AddressData"]["ipv4GatewayAddress"]}})
+
+ facts['netapp_hostside_io_interfaces'].append(interface_info)
+
+ # Create a dictionary of volume lists keyed by host names
+ facts['netapp_volumes_by_initiators'] = dict()
+ for mapping in array_facts['storagePoolBundle']['lunMapping']:
+ for host in facts['netapp_hosts']:
+ if mapping['mapRef'] == host['hosts_reference'] or mapping['mapRef'] == host['group_id']:
+ if host['name'] not in facts['netapp_volumes_by_initiators'].keys():
+ facts['netapp_volumes_by_initiators'].update({host['name']: []})
+
+ # Determine host io interface protocols
+ host_types = [port['type'] for port in host['ports']]
+ hostside_io_interface_protocols = []
+ host_port_protocols = []
+ host_port_information = {}
+ for interface in facts['netapp_hostside_io_interfaces']:
+ hostside_io_interface_protocols.append(interface["protocol"])
+ for host_type in host_types:
+ if host_type == "iscsi" and interface["protocol"] in ["iscsi", "ib_iser"]:
+ host_port_protocols.append(interface["protocol"])
+ if interface["protocol"] in host_port_information:
+ host_port_information[interface["protocol"]].append(interface)
+ else:
+ host_port_information.update({interface["protocol"]: [interface]})
+ elif host_type == "fc" and interface["protocol"] in ["fc"]:
+ host_port_protocols.append(interface["protocol"])
+ if interface["protocol"] in host_port_information:
+ host_port_information[interface["protocol"]].append(interface)
+ else:
+ host_port_information.update({interface["protocol"]: [interface]})
+ elif host_type == "sas" and interface["protocol"] in ["sas"]:
+ host_port_protocols.append(interface["protocol"])
+ if interface["protocol"] in host_port_information:
+ host_port_information[interface["protocol"]].append(interface)
+ else:
+ host_port_information.update({interface["protocol"]: [interface]})
+ elif host_type == "ib" and interface["protocol"] in ["ib_iser", "ib_srp"]:
+ host_port_protocols.append(interface["protocol"])
+ if interface["protocol"] in host_port_information:
+ host_port_information[interface["protocol"]].append(interface)
+ else:
+ host_port_information.update({interface["protocol"]: [interface]})
+ elif host_type == "nvmeof" and interface["protocol"] in ["nvme_ib", "nvme_fc", "nvme_roce"]:
+ host_port_protocols.append(interface["protocol"])
+ if interface["protocol"] in host_port_information:
+ host_port_information[interface["protocol"]].append(interface)
+ else:
+ host_port_information.update({interface["protocol"]: [interface]})
+
+ for volume in all_volumes:
+
+ storage_pool = [pool["name"] for pool in facts['netapp_storage_pools'] if pool["id"] == volume["volumeGroupRef"]][0]
+
+ if mapping['id'] in [volume_mapping['id'] for volume_mapping in volume['listOfMappings']]:
+
+ # Determine workload name if there is one
+ workload_name = ""
+ metadata = dict()
+ for volume_tag in volume['metadata']:
+ if volume_tag['key'] == 'workloadId':
+ for workload_tag in facts['netapp_workload_tags']:
+ if volume_tag['value'] == workload_tag['id']:
+ workload_name = workload_tag['name']
+ metadata = dict((entry['key'], entry['value'])
+ for entry in workload_tag['attributes']
+ if entry['key'] != 'profileId')
+
+ # Get volume specific metadata tags
+ volume_metadata_raw = dict()
+ volume_metadata = dict()
+ for entry in volume['metadata']:
+ volume_metadata_raw.update({entry["key"]: entry["value"]})
+
+ for sorted_key in sorted(volume_metadata_raw.keys()):
+ if re.match(".*~[0-9]$", sorted_key):
+ key = re.sub("~[0-9]$", "", sorted_key)
+ if key in volume_metadata:
+ volume_metadata[key] = volume_metadata[key] + volume_metadata_raw[sorted_key]
+ else:
+ volume_metadata.update({key: volume_metadata_raw[sorted_key]})
+ else:
+ volume_metadata.update({sorted_key: volume_metadata_raw[sorted_key]})
+
+ # Determine drive count
+ stripe_count = 0
+ vg_drive_num = sum(1 for d in array_facts['drive'] if d['currentVolumeGroupRef'] == volume['volumeGroupRef'] and not d['hotSpare'])
+
+ if volume['raidLevel'] == "raidDiskPool":
+ stripe_count = 8
+ if volume['raidLevel'] == "raid0":
+ stripe_count = vg_drive_num
+ if volume['raidLevel'] == "raid1":
+ stripe_count = int(vg_drive_num / 2)
+ if volume['raidLevel'] in ["raid3", "raid5"]:
+ stripe_count = vg_drive_num - 1
+ if volume['raidLevel'] == "raid6":
+ stripe_count = vg_drive_num - 2
+ facts['netapp_volumes_by_initiators'][host['name']].append(
+ dict(name=volume['name'],
+ storage_pool=storage_pool,
+ host_types=set(host_types),
+ host_port_information=host_port_information,
+ host_port_protocols=set(host_port_protocols),
+ hostside_io_interface_protocols=set(hostside_io_interface_protocols),
+ id=volume['id'],
+ wwn=volume['wwn'],
+ eui=volume['extendedUniqueIdentifier'],
+ workload_name=workload_name,
+ workload_metadata=metadata,
+ meta_data=metadata,
+ volume_metadata=volume_metadata,
+ raid_level=volume['raidLevel'],
+ segment_size_kb=int(volume['segmentSize'] / 1024),
+ stripe_count=stripe_count))
+
+ features = [feature for feature in array_facts['sa']['capabilities']]
+ features.extend([feature['capability'] for feature in array_facts['sa']['premiumFeatures']
+ if feature['isEnabled']])
+ features = list(set(features)) # ensure unique
+ features.sort()
+ facts['netapp_enabled_features'] = features
+
+ return facts
+
+ def get_facts(self):
+ """Get the embedded or web services proxy information."""
+ facts = self.get_array_facts()
+
+ facts_from_proxy = not self.is_embedded()
+ facts.update({"facts_from_proxy": facts_from_proxy})
+
+ self.module.exit_json(msg="Gathered facts for storage array. Array ID: [%s]." % self.ssid,
+ storage_array_facts=facts)
+
+
+def strip_interface_speed(speed):
+ """Converts symbol interface speeds to a more common notation. Example: 'speed10gig' -> '10g'"""
+ if isinstance(speed, list):
+ result = [re.match(r"speed[0-9]{1,3}[gm]", sp) for sp in speed]
+ result = [sp.group().replace("speed", "") if result else "unknown" for sp in result if sp]
+ result = ["auto" if re.match(r"auto", sp) else sp for sp in result]
+ else:
+ result = re.match(r"speed[0-9]{1,3}[gm]", speed)
+ result = result.group().replace("speed", "") if result else "unknown"
+ result = "auto" if re.match(r"auto", result.lower()) else result
+ return result
+
+
+def main():
+ facts = Facts()
+ facts.get_facts()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_firmware.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_firmware.py
new file mode 100644
index 00000000..caea3d1d
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_firmware.py
@@ -0,0 +1,613 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_firmware
+short_description: NetApp E-Series manage firmware.
+description:
+ - Ensure specific firmware versions are activated on E-Series storage system.
+author:
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ nvsram:
+ description:
+ - Path to the NVSRAM file.
+ - NetApp recommends upgrading the NVSRAM when upgrading firmware.
+ - Due to concurrency issues, use M(netapp_eseries.santricity.na_santricity_proxy_firmware_upload) to upload firmware and nvsram to SANtricity Web Services Proxy when
+ upgrading multiple systems at the same time on the same instance of the proxy.
+ type: str
+ required: false
+ firmware:
+ description:
+ - Path to the firmware file.
+ - Due to concurrency issues, use M(netapp_eseries.santricity.na_santricity_proxy_firmware_upload) to upload firmware and nvsram to SANtricity Web Services Proxy when
+ upgrading multiple systems at the same time on the same instance of the proxy.
+ type: str
+ required: True
+ wait_for_completion:
+ description:
+ - This flag will cause module to wait for any upgrade actions to complete.
+ - When changes are required to both firmware and nvsram and task is executed against SANtricity Web Services Proxy,
+ the firmware will have to complete before nvsram can be installed.
+ type: bool
+ default: false
+ clear_mel_events:
+ description:
+ - This flag will force firmware to be activated in spite of the storage system mel-event issues.
+ - Warning! This will clear all storage system mel-events. Use at your own risk!
+ type: bool
+ default: false
+"""
+EXAMPLES = """
+- name: Ensure correct firmware versions
+ na_santricity_firmware:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ nvsram: "path/to/nvsram"
+ firmware: "path/to/bundle"
+ wait_for_completion: true
+ clear_mel_events: true
+- name: Ensure correct firmware versions
+ na_santricity_firmware:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ nvsram: "path/to/nvsram"
+ firmware: "path/to/firmware"
+"""
+RETURN = """
+msg:
+ description: Status and version of firmware and NVSRAM.
+ type: str
+ returned: always
+ sample:
+"""
+import os
+import multiprocessing
+import threading
+
+from time import sleep
+from ansible.module_utils import six
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule, create_multipart_formdata, request
+from ansible.module_utils._text import to_native
+
+
+class NetAppESeriesFirmware(NetAppESeriesModule):
+ COMPATIBILITY_CHECK_TIMEOUT_SEC = 60
+ REBOOT_TIMEOUT_SEC = 30 * 60
+ MINIMUM_PROXY_VERSION = "04.10.00.0000"
+
+ def __init__(self):
+ ansible_options = dict(
+ nvsram=dict(type="str", required=False),
+ firmware=dict(type="str", required=True),
+ wait_for_completion=dict(type="bool", default=False),
+ clear_mel_events=dict(type="bool", default=False))
+
+ super(NetAppESeriesFirmware, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ supports_check_mode=True)
+
+ args = self.module.params
+ self.nvsram = args["nvsram"]
+ self.firmware = args["firmware"]
+ self.wait_for_completion = args["wait_for_completion"]
+ self.clear_mel_events = args["clear_mel_events"]
+
+ self.nvsram_name = None
+ self.firmware_name = None
+ self.is_bundle_cache = None
+ self.firmware_version_cache = None
+ self.nvsram_version_cache = None
+ self.upgrade_required = False
+ self.upgrade_in_progress = False
+ self.module_info = dict()
+
+ if self.nvsram:
+ self.nvsram_name = os.path.basename(self.nvsram)
+ if self.firmware:
+ self.firmware_name = os.path.basename(self.firmware)
+
+ self.last_known_event = -1
+ self.is_firmware_activation_started_mel_event_count = 1
+ self.is_nvsram_download_completed_mel_event_count = 1
+ self.proxy_wait_for_upgrade_mel_event_count = 1
+
+ def is_upgrade_in_progress(self):
+ """Determine whether an upgrade is already in progress."""
+ in_progress = False
+
+ if self.is_proxy():
+ try:
+ rc, status = self.request("storage-systems/%s/cfw-upgrade" % self.ssid)
+ in_progress = status["running"]
+ except Exception as error:
+ if "errorMessage" in to_native(error):
+ self.module.warn("Failed to retrieve upgrade status. Array [%s]. Error [%s]." % (self.ssid, error))
+ in_progress = False
+ else:
+ self.module.fail_json(msg="Failed to retrieve upgrade status. Array [%s]. Error [%s]." % (self.ssid, error))
+ else:
+ in_progress = False
+
+ return in_progress
+
+ def is_firmware_bundled(self):
+ """Determine whether supplied firmware is bundle."""
+ if self.is_bundle_cache is None:
+ with open(self.firmware, "rb") as fh:
+ signature = fh.read(16).lower()
+
+ if b"firmware" in signature:
+ self.is_bundle_cache = False
+ elif b"combined_content" in signature:
+ self.is_bundle_cache = True
+ else:
+ self.module.fail_json(msg="Firmware file is invalid. File [%s]. Array [%s]" % (self.firmware, self.ssid))
+
+ return self.is_bundle_cache
+
+ def firmware_version(self):
+ """Retrieve firmware version of the firmware file. Return: bytes string"""
+ if self.firmware_version_cache is None:
+
+ # Search firmware file for bundle or firmware version
+ with open(self.firmware, "rb") as fh:
+ line = fh.readline()
+ while line:
+ if self.is_firmware_bundled():
+ if b'displayableAttributeList=' in line:
+ for item in line[25:].split(b','):
+ key, value = item.split(b"|")
+ if key == b'VERSION':
+ self.firmware_version_cache = value.strip(b"\n")
+ break
+ elif b"Version:" in line:
+ self.firmware_version_cache = line.split()[-1].strip(b"\n")
+ break
+ line = fh.readline()
+ else:
+ self.module.fail_json(msg="Failed to determine firmware version. File [%s]. Array [%s]." % (self.firmware, self.ssid))
+ return self.firmware_version_cache
+
+ def nvsram_version(self):
+ """Retrieve NVSRAM version of the NVSRAM file. Return: byte string"""
+ if self.nvsram_version_cache is None:
+
+ with open(self.nvsram, "rb") as fh:
+ line = fh.readline()
+ while line:
+ if b".NVSRAM Configuration Number" in line:
+ self.nvsram_version_cache = line.split(b'"')[-2]
+ break
+ line = fh.readline()
+ else:
+ self.module.fail_json(msg="Failed to determine NVSRAM file version. File [%s]. Array [%s]." % (self.nvsram, self.ssid))
+ return self.nvsram_version_cache
+
+ def check_system_health(self):
+ """Ensure E-Series storage system is healthy. Works for both embedded and proxy web services."""
+ try:
+ rc, response = self.request("storage-systems/%s/health-check" % self.ssid, method="POST")
+ return response["successful"]
+ except Exception as error:
+ self.module.fail_json(msg="Health check failed! Array Id [%s]. Error[%s]." % (self.ssid, to_native(error)))
+
+ def embedded_check_compatibility(self):
+ """Verify files are compatible with E-Series storage system."""
+ if self.nvsram:
+ self.embedded_check_nvsram_compatibility()
+ if self.firmware:
+ self.embedded_check_bundle_compatibility()
+
+ def embedded_check_nvsram_compatibility(self):
+ """Verify the provided NVSRAM is compatible with E-Series storage system."""
+ files = [("nvsramimage", self.nvsram_name, self.nvsram)]
+ headers, data = create_multipart_formdata(files=files)
+ compatible = {}
+ try:
+ rc, compatible = self.request("firmware/embedded-firmware/%s/nvsram-compatibility-check" % self.ssid, method="POST", data=data, headers=headers)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve NVSRAM compatibility results. Array Id [%s]. Error[%s]." % (self.ssid, to_native(error)))
+
+ if not compatible["signatureTestingPassed"]:
+ self.module.fail_json(msg="Invalid NVSRAM file. File [%s]." % self.nvsram)
+ if not compatible["fileCompatible"]:
+ self.module.fail_json(msg="Incompatible NVSRAM file. File [%s]." % self.nvsram)
+
+ # Determine whether nvsram is required
+ for module in compatible["versionContents"]:
+ if module["bundledVersion"] != module["onboardVersion"]:
+ self.upgrade_required = True
+
+ # Update bundle info
+ self.module_info.update({module["module"]: {"onboard_version": module["onboardVersion"], "bundled_version": module["bundledVersion"]}})
+
+ def embedded_check_bundle_compatibility(self):
+ """Verify the provided firmware bundle is compatible with E-Series storage system."""
+ files = [("files[]", "blob", self.firmware)]
+ headers, data = create_multipart_formdata(files=files, send_8kb=True)
+ compatible = {}
+ try:
+ rc, compatible = self.request("firmware/embedded-firmware/%s/bundle-compatibility-check" % self.ssid, method="POST", data=data, headers=headers)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve bundle compatibility results. Array Id [%s]. Error[%s]." % (self.ssid, to_native(error)))
+
+ # Determine whether valid and compatible firmware
+ if not compatible["signatureTestingPassed"]:
+ self.module.fail_json(msg="Invalid firmware bundle file. File [%s]." % self.firmware)
+ if not compatible["fileCompatible"]:
+ self.module.fail_json(msg="Incompatible firmware bundle file. File [%s]." % self.firmware)
+
+ # Determine whether upgrade is required
+ for module in compatible["versionContents"]:
+ bundle_module_version = module["bundledVersion"].split(".")
+ onboard_module_version = module["onboardVersion"].split(".")
+ version_minimum_length = min(len(bundle_module_version), len(onboard_module_version))
+
+ if bundle_module_version[:version_minimum_length] != onboard_module_version[:version_minimum_length]:
+ self.upgrade_required = True
+
+ # Check whether downgrade is being attempted
+ bundle_version = module["bundledVersion"].split(".")[:2]
+ onboard_version = module["onboardVersion"].split(".")[:2]
+ if bundle_version[0] < onboard_version[0] or (bundle_version[0] == onboard_version[0] and bundle_version[1] < onboard_version[1]):
+ self.module.fail_json(msg="Downgrades are not permitted. onboard [%s] > bundled[%s]."
+ % (module["onboardVersion"], module["bundledVersion"]))
+
+ # Update bundle info
+ self.module_info.update({module["module"]: {"onboard_version": module["onboardVersion"], "bundled_version": module["bundledVersion"]}})
+
+ def embedded_firmware_activate(self):
+ """Activate firmware."""
+ rc, response = self.request("firmware/embedded-firmware/activate", method="POST", ignore_errors=True, timeout=10)
+ if rc == "422":
+ self.module.fail_json(msg="Failed to activate the staged firmware. Array Id [%s]. Error [%s]" % (self.ssid, response))
+
+ def embedded_firmware_download(self):
+ """Execute the firmware download."""
+ if self.nvsram:
+ firmware_url = "firmware/embedded-firmware?nvsram=true&staged=true"
+ headers, data = create_multipart_formdata(files=[("nvsramfile", self.nvsram_name, self.nvsram),
+ ("dlpfile", self.firmware_name, self.firmware)])
+ else:
+ firmware_url = "firmware/embedded-firmware?nvsram=false&staged=true"
+ headers, data = create_multipart_formdata(files=[("dlpfile", self.firmware_name, self.firmware)])
+
+ # Stage firmware and nvsram
+ try:
+
+ rc, response = self.request(firmware_url, method="POST", data=data, headers=headers, timeout=(30 * 60))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to stage firmware. Array Id [%s]. Error[%s]." % (self.ssid, to_native(error)))
+
+ # Activate firmware
+ activate_thread = threading.Thread(target=self.embedded_firmware_activate)
+ activate_thread.start()
+ self.wait_for_reboot()
+
+ def wait_for_reboot(self):
+ """Wait for controller A to fully reboot and web services running"""
+ reboot_started = False
+ reboot_completed = False
+ self.module.log("Controller firmware: Reboot commencing. Array Id [%s]." % self.ssid)
+ while self.wait_for_completion and not (reboot_started and reboot_completed):
+ try:
+ rc, response = self.request("storage-systems/%s/symbol/pingController?controller=a&verboseErrorResponse=true"
+ % self.ssid, method="POST", timeout=10, log_request=False)
+
+ if reboot_started and response == "ok":
+ self.module.log("Controller firmware: Reboot completed. Array Id [%s]." % self.ssid)
+ reboot_completed = True
+ sleep(2)
+ except Exception as error:
+ if not reboot_started:
+ self.module.log("Controller firmware: Reboot started. Array Id [%s]." % self.ssid)
+ reboot_started = True
+ continue
+
+ def firmware_event_logger(self):
+ """Determine if firmware activation has started."""
+ # Determine the last known event
+ try:
+ rc, events = self.request("storage-systems/%s/events" % self.ssid)
+ for event in events:
+ if int(event["eventNumber"]) > int(self.last_known_event):
+ self.last_known_event = event["eventNumber"]
+ except Exception as error:
+ self.module.fail_json(msg="Failed to determine last known event. Array Id [%s]. Error[%s]." % (self.ssid, to_native(error)))
+
+ while True:
+ try:
+ rc, events = self.request("storage-systems/%s/events?lastKnown=%s&wait=1" % (self.ssid, self.last_known_event), log_request=False)
+ for event in events:
+ if int(event["eventNumber"]) > int(self.last_known_event):
+ self.last_known_event = event["eventNumber"]
+
+ # Log firmware events
+ if event["eventType"] == "firmwareDownloadEvent":
+ self.module.log("%s" % event["status"])
+ if event["status"] == "informational" and event["statusMessage"]:
+ self.module.log("Controller firmware: %s Array Id [%s]." % (event["statusMessage"], self.ssid))
+
+ # When activation is successful, finish thread
+ if event["status"] == "activate_success":
+ self.module.log("Controller firmware activated. Array Id [%s]." % self.ssid)
+ return
+ except Exception as error:
+ pass
+
+ def wait_for_web_services(self):
+ """Wait for web services to report firmware and nvsram upgrade."""
+ # Wait for system to reflect changes
+ for count in range(int(self.REBOOT_TIMEOUT_SEC / 5)):
+ try:
+ if self.is_firmware_bundled():
+ firmware_rc, firmware_version = self.request("storage-systems/%s/graph/xpath-filter?query=/controller/"
+ "codeVersions[codeModule='bundleDisplay']" % self.ssid, log_request=False)
+ current_firmware_version = six.b(firmware_version[0]["versionString"])
+ else:
+ firmware_rc, firmware_version = self.request("storage-systems/%s/graph/xpath-filter?query=/sa/saData/fwVersion"
+ % self.ssid, log_request=False)
+ current_firmware_version = six.b(firmware_version[0])
+
+ nvsram_rc, nvsram_version = self.request("storage-systems/%s/graph/xpath-filter?query=/sa/saData/nvsramVersion" % self.ssid, log_request=False)
+ current_nvsram_version = six.b(nvsram_version[0])
+
+ if current_firmware_version == self.firmware_version() and (not self.nvsram or current_nvsram_version == self.nvsram_version()):
+ break
+ except Exception as error:
+ pass
+ sleep(5)
+ else:
+ self.module.fail_json(msg="Timeout waiting for Santricity Web Services. Array [%s]" % self.ssid)
+
+ # Wait for system to be optimal
+ for count in range(int(self.REBOOT_TIMEOUT_SEC / 5)):
+ try:
+ rc, response = self.request("storage-systems/%s" % self.ssid, log_request=False)
+
+ if response["status"] == "optimal":
+ self.upgrade_in_progress = False
+ break
+ except Exception as error:
+ pass
+ sleep(5)
+ else:
+ self.module.fail_json(msg="Timeout waiting for storage system to return to optimal status. Array [%s]" % self.ssid)
+
+ def embedded_upgrade(self):
+ """Upload and activate both firmware and NVSRAM."""
+ download_thread = threading.Thread(target=self.embedded_firmware_download)
+ event_thread = threading.Thread(target=self.firmware_event_logger)
+ download_thread.start()
+ event_thread.start()
+ download_thread.join()
+ event_thread.join()
+
+ def proxy_check_nvsram_compatibility(self, retries=10):
+ """Verify nvsram is compatible with E-Series storage system."""
+ self.module.log("Checking nvsram compatibility...")
+ data = {"storageDeviceIds": [self.ssid]}
+ try:
+ rc, check = self.request("firmware/compatibility-check", method="POST", data=data)
+ except Exception as error:
+ if retries:
+ sleep(1)
+ self.proxy_check_nvsram_compatibility(retries - 1)
+ else:
+ self.module.fail_json(msg="Failed to receive NVSRAM compatibility information. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ for count in range(int(self.COMPATIBILITY_CHECK_TIMEOUT_SEC / 5)):
+ try:
+ rc, response = self.request("firmware/compatibility-check?requestId=%s" % check["requestId"])
+ except Exception as error:
+ continue
+
+ if not response["checkRunning"]:
+ for result in response["results"][0]["nvsramFiles"]:
+ if result["filename"] == self.nvsram_name:
+ return
+ self.module.fail_json(msg="NVSRAM is not compatible. NVSRAM [%s]. Array [%s]." % (self.nvsram_name, self.ssid))
+ sleep(5)
+
+ self.module.fail_json(msg="Failed to retrieve NVSRAM status update from proxy. Array [%s]." % self.ssid)
+
+ def proxy_check_firmware_compatibility(self, retries=10):
+ """Verify firmware is compatible with E-Series storage system."""
+ check = {}
+ try:
+ rc, check = self.request("firmware/compatibility-check", method="POST", data={"storageDeviceIds": [self.ssid]})
+ except Exception as error:
+ if retries:
+ sleep(1)
+ self.proxy_check_firmware_compatibility(retries - 1)
+ else:
+ self.module.fail_json(msg="Failed to receive firmware compatibility information. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ for count in range(int(self.COMPATIBILITY_CHECK_TIMEOUT_SEC / 5)):
+ try:
+ rc, response = self.request("firmware/compatibility-check?requestId=%s" % check["requestId"])
+ except Exception as error:
+ continue
+
+ if not response["checkRunning"]:
+ for result in response["results"][0]["cfwFiles"]:
+ if result["filename"] == self.firmware_name:
+ return
+ self.module.fail_json(msg="Firmware bundle is not compatible. firmware [%s]. Array [%s]." % (self.firmware_name, self.ssid))
+ sleep(5)
+
+ self.module.fail_json(msg="Failed to retrieve firmware status update from proxy. Array [%s]." % self.ssid)
+
+ def proxy_upload_and_check_compatibility(self):
+ """Ensure firmware is uploaded and verify compatibility."""
+ cfw_files = []
+ try:
+ rc, cfw_files = self.request("firmware/cfw-files")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve existing firmware files. Error [%s]" % to_native(error))
+
+ if self.firmware:
+ for cfw_file in cfw_files:
+ if cfw_file["filename"] == self.firmware_name:
+ break
+ else:
+ fields = [("validate", "true")]
+ files = [("firmwareFile", self.firmware_name, self.firmware)]
+ headers, data = create_multipart_formdata(files=files, fields=fields)
+ try:
+ rc, response = self.request("firmware/upload", method="POST", data=data, headers=headers)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to upload firmware bundle file. File [%s]. Array [%s]. Error [%s]."
+ % (self.firmware_name, self.ssid, to_native(error)))
+ self.proxy_check_firmware_compatibility()
+
+ if self.nvsram:
+ for cfw_file in cfw_files:
+ if cfw_file["filename"] == self.nvsram_name:
+ break
+ else:
+ fields = [("validate", "true")]
+ files = [("firmwareFile", self.nvsram_name, self.nvsram)]
+ headers, data = create_multipart_formdata(files=files, fields=fields)
+ try:
+ rc, response = self.request("firmware/upload", method="POST", data=data, headers=headers)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to upload NVSRAM file. File [%s]. Array [%s]. Error [%s]."
+ % (self.nvsram_name, self.ssid, to_native(error)))
+ self.proxy_check_nvsram_compatibility()
+
+ def proxy_check_upgrade_required(self):
+ """Staging is required to collect firmware information from the web services proxy."""
+ # Verify controller consistency and get firmware versions
+ if self.firmware:
+ current_firmware_version = b""
+ try:
+ # Retrieve current bundle version
+ if self.is_firmware_bundled():
+ rc, response = self.request("storage-systems/%s/graph/xpath-filter?query=/controller/codeVersions[codeModule='bundleDisplay']" % self.ssid)
+ current_firmware_version = six.b(response[0]["versionString"])
+ else:
+ rc, response = self.request("storage-systems/%s/graph/xpath-filter?query=/sa/saData/fwVersion" % self.ssid)
+ current_firmware_version = six.b(response[0])
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve controller firmware information. Array [%s]. Error [%s]" % (self.ssid, to_native(error)))
+
+ # Determine whether upgrade is required
+ if current_firmware_version != self.firmware_version():
+
+ current = current_firmware_version.split(b".")[:2]
+ upgrade = self.firmware_version().split(b".")[:2]
+ if current[0] < upgrade[0] or (current[0] == upgrade[0] and current[1] <= upgrade[1]):
+ self.upgrade_required = True
+ else:
+ self.module.fail_json(msg="Downgrades are not permitted. Firmware [%s]. Array [%s]." % (self.firmware, self.ssid))
+
+ # Determine current NVSRAM version and whether change is required
+ if self.nvsram:
+ try:
+ rc, response = self.request("storage-systems/%s/graph/xpath-filter?query=/sa/saData/nvsramVersion" % self.ssid)
+
+ if six.b(response[0]) != self.nvsram_version():
+ self.upgrade_required = True
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve storage system's NVSRAM version. Array [%s]. Error [%s]" % (self.ssid, to_native(error)))
+
+ def proxy_wait_for_upgrade(self):
+ """Wait for SANtricity Web Services Proxy to report upgrade complete"""
+ self.module.log("(Proxy) Waiting for upgrade to complete...")
+
+ status = {}
+ while True:
+ try:
+ rc, status = self.request("storage-systems/%s/cfw-upgrade" % self.ssid, log_request=False, ignore_errors=True)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve firmware upgrade status! Array [%s]. Error[%s]." % (self.ssid, to_native(error)))
+
+ if "errorMessage" in status:
+ self.module.warn("Proxy reported an error. Checking whether upgrade completed. Array [%s]. Error [%s]." % (self.ssid, status["errorMessage"]))
+ self.wait_for_web_services()
+ break
+
+ if not status["running"]:
+ if status["activationCompletionTime"]:
+ self.upgrade_in_progress = False
+ break
+ else:
+ self.module.fail_json(msg="Failed to complete upgrade. Array [%s]." % self.ssid)
+ sleep(5)
+
+ def delete_mel_events(self):
+ """Clear all mel-events."""
+ try:
+ rc, response = self.request("storage-systems/%s/mel-events?clearCache=true&resetMel=true" % self.ssid, method="DELETE")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to clear mel-events. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ def proxy_upgrade(self):
+ """Activate previously uploaded firmware related files."""
+ self.module.log("(Proxy) Firmware upgrade commencing...")
+ body = {"stageFirmware": False, "skipMelCheck": self.clear_mel_events, "cfwFile": self.firmware_name}
+ if self.nvsram:
+ body.update({"nvsramFile": self.nvsram_name})
+
+ try:
+ rc, response = self.request("storage-systems/%s/cfw-upgrade" % self.ssid, method="POST", data=body)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to initiate firmware upgrade. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ self.upgrade_in_progress = True
+ if self.wait_for_completion:
+ self.proxy_wait_for_upgrade()
+
+ def apply(self):
+ """Upgrade controller firmware."""
+ if self.is_upgrade_in_progress():
+ self.module.fail_json(msg="Upgrade is already is progress. Array [%s]." % self.ssid)
+
+ if self.is_embedded():
+ self.embedded_check_compatibility()
+ else:
+ if not self.is_web_services_version_met(self.MINIMUM_PROXY_VERSION):
+ self.module.fail_json(msg="Minimum proxy version %s required!")
+ self.proxy_check_upgrade_required()
+
+ # This will upload the firmware files to the web services proxy but not to the controller
+ if self.upgrade_required:
+ self.proxy_upload_and_check_compatibility()
+
+ # Perform upgrade
+ if self.upgrade_required and not self.module.check_mode:
+
+ if self.clear_mel_events:
+ self.delete_mel_events()
+
+ if self.is_embedded():
+ self.embedded_upgrade()
+ else:
+ self.proxy_upgrade()
+
+ self.module.exit_json(changed=self.upgrade_required, upgrade_in_process=self.upgrade_in_progress)
+
+
+def main():
+ firmware = NetAppESeriesFirmware()
+ firmware.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_global.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_global.py
new file mode 100644
index 00000000..a02f2474
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_global.py
@@ -0,0 +1,367 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_global
+short_description: NetApp E-Series manage global settings configuration
+description:
+ - Allow the user to configure several of the global settings associated with an E-Series storage-system
+author:
+ - Michael Price (@lmprice)
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ name:
+ description:
+ - Set the name of the E-Series storage-system
+ - This label/name doesn't have to be unique.
+ - May be up to 30 characters in length.
+ type: str
+ aliases:
+ - label
+ cache_block_size:
+ description:
+ - Size of the cache's block size.
+ - All volumes on the storage system share the same cache space; therefore, the volumes can have only one cache block size.
+ - See M(netapp_eseries.santricity.na_santricity_facts) for available sizes.
+ type: int
+ required: False
+ cache_flush_threshold:
+ description:
+ - This is the percentage threshold of the amount of unwritten data that is allowed to remain on the storage array's cache before flushing.
+ type: int
+ required: False
+ default_host_type:
+ description:
+ - Default host type for the storage system.
+ - Either one of the following names can be specified, Linux DM-MP, VMWare, Windows, Windows Clustered, or a
+ host type index which can be found in M(netapp_eseries.santricity.na_santricity_facts)
+ type: str
+ required: False
+ automatic_load_balancing:
+ description:
+ - Enable automatic load balancing to allow incoming traffic from the hosts to be dynamically managed and balanced across both controllers.
+ - Automatic load balancing requires host connectivity reporting to be enabled.
+ type: str
+ choices:
+ - enabled
+ - disabled
+ required: False
+ host_connectivity_reporting:
+ description:
+ - Enable host connectivity reporting to allow host connections to be monitored for connection and multipath driver problems.
+ - When M(automatic_load_balancing==enabled) then M(host_connectivity_reporting) must be enabled
+ type: str
+ choices:
+ - enabled
+ - disabled
+ required: False
+notes:
+ - Check mode is supported.
+ - This module requires Web Services API v1.3 or newer.
+"""
+
+EXAMPLES = """
+ - name: Set the storage-system name
+ na_santricity_global:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ name: myArrayName
+ cache_block_size: 32768
+ cache_flush_threshold: 80
+ automatic_load_balancing: enabled
+ default_host_type: Linux DM-MP
+ - name: Set the storage-system name
+ na_santricity_global:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ name: myOtherArrayName
+ cache_block_size: 8192
+ cache_flush_threshold: 60
+ automatic_load_balancing: disabled
+ default_host_type: 28
+"""
+
+RETURN = """
+changed:
+ description: Whether global settings were changed
+ returned: on success
+ type: bool
+ sample: true
+array_name:
+ description: Current storage array's name
+ returned: on success
+ type: str
+ sample: arrayName
+automatic_load_balancing:
+ description: Whether automatic load balancing feature has been enabled
+ returned: on success
+ type: str
+ sample: enabled
+host_connectivity_reporting:
+ description: Whether host connectivity reporting feature has been enabled
+ returned: on success
+ type: str
+ sample: enabled
+cache_settings:
+ description: Current cache block size and flushing threshold values
+ returned: on success
+ type: dict
+ sample: {"cache_block_size": 32768, "cache_flush_threshold": 80}
+default_host_type_index:
+ description: Current default host type index
+ returned: on success
+ type: int
+ sample: 28
+"""
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+
+class NetAppESeriesGlobalSettings(NetAppESeriesModule):
+ def __init__(self):
+ version = "02.00.0000.0000"
+ ansible_options = dict(cache_block_size=dict(type="int", require=False),
+ cache_flush_threshold=dict(type="int", required=False),
+ default_host_type=dict(type="str", require=False),
+ automatic_load_balancing=dict(type="str", choices=["enabled", "disabled"], required=False),
+ host_connectivity_reporting=dict(type="str", choices=["enabled", "disabled"], required=False),
+ name=dict(type='str', required=False, aliases=['label']))
+
+ super(NetAppESeriesGlobalSettings, self).__init__(ansible_options=ansible_options,
+ web_services_version=version,
+ supports_check_mode=True)
+ args = self.module.params
+ self.name = args["name"]
+ self.cache_block_size = args["cache_block_size"]
+ self.cache_flush_threshold = args["cache_flush_threshold"]
+ self.host_type_index = args["default_host_type"]
+
+ self.autoload_enabled = None
+ if args["automatic_load_balancing"]:
+ self.autoload_enabled = args["automatic_load_balancing"] == "enabled"
+
+ self.host_connectivity_reporting_enabled = None
+ if args["host_connectivity_reporting"]:
+ self.host_connectivity_reporting_enabled = args["host_connectivity_reporting"] == "enabled"
+ elif self.autoload_enabled:
+ self.host_connectivity_reporting_enabled = True
+
+ if self.autoload_enabled and not self.host_connectivity_reporting_enabled:
+ self.module.fail_json(msg="Option automatic_load_balancing requires host_connectivity_reporting to be enabled. Array [%s]." % self.ssid)
+
+ self.current_configuration_cache = None
+
+ def get_current_configuration(self, update=False):
+ """Retrieve the current storage array's global configuration."""
+ if self.current_configuration_cache is None or update:
+ self.current_configuration_cache = dict()
+
+ # Get the storage array's capabilities and available options
+ try:
+ rc, capabilities = self.request("storage-systems/%s/capabilities" % self.ssid)
+ self.current_configuration_cache["autoload_capable"] = "capabilityAutoLoadBalancing" in capabilities["productCapabilities"]
+ self.current_configuration_cache["cache_block_size_options"] = capabilities["featureParameters"]["cacheBlockSizes"]
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve storage array capabilities. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ try:
+ rc, host_types = self.request("storage-systems/%s/host-types" % self.ssid)
+ self.current_configuration_cache["host_type_options"] = dict()
+ for host_type in host_types:
+ self.current_configuration_cache["host_type_options"].update({host_type["code"].lower(): host_type["index"]})
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve storage array host options. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ # Get the current cache settings
+ try:
+ rc, settings = self.request("storage-systems/%s/graph/xpath-filter?query=/sa" % self.ssid)
+ self.current_configuration_cache["cache_settings"] = {"cache_block_size": settings[0]["cache"]["cacheBlkSize"],
+ "cache_flush_threshold": settings[0]["cache"]["demandFlushThreshold"]}
+ self.current_configuration_cache["default_host_type_index"] = settings[0]["defaultHostTypeIndex"]
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve cache settings. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ try:
+ rc, array_info = self.request("storage-systems/%s" % self.ssid)
+ self.current_configuration_cache["autoload_enabled"] = array_info["autoLoadBalancingEnabled"]
+ self.current_configuration_cache["host_connectivity_reporting_enabled"] = array_info["hostConnectivityReportingEnabled"]
+ self.current_configuration_cache["name"] = array_info['name']
+ except Exception as error:
+ self.module.fail_json(msg="Failed to determine current configuration. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ return self.current_configuration_cache
+
+ def change_cache_block_size_required(self):
+ """Determine whether cache block size change is required."""
+ if self.cache_block_size is None:
+ return False
+
+ current_available_block_sizes = self.get_current_configuration()["cache_block_size_options"]
+ if self.cache_block_size not in current_available_block_sizes:
+ self.module.fail_json(msg="Invalid cache block size. Array [%s]. Available cache block sizes [%s]." % (self.ssid, current_available_block_sizes))
+
+ return self.cache_block_size != self.get_current_configuration()["cache_settings"]["cache_block_size"]
+
+ def change_cache_flush_threshold_required(self):
+ """Determine whether cache flush percentage change is required."""
+ if self.cache_flush_threshold is None:
+ return False
+
+ if self.cache_flush_threshold <= 0 or self.cache_flush_threshold >= 100:
+ self.module.fail_json(msg="Invalid cache flushing threshold, it must be equal to or between 0 and 100. Array [%s]" % self.ssid)
+
+ return self.cache_flush_threshold != self.get_current_configuration()["cache_settings"]["cache_flush_threshold"]
+
+ def change_host_type_required(self):
+ """Determine whether default host type change is required."""
+ if self.host_type_index is None:
+ return False
+
+ current_available_host_types = self.get_current_configuration()["host_type_options"]
+ if isinstance(self.host_type_index, str):
+ self.host_type_index = self.host_type_index.lower()
+
+ if self.host_type_index in self.HOST_TYPE_INDEXES.keys():
+ self.host_type_index = self.HOST_TYPE_INDEXES[self.host_type_index]
+ elif self.host_type_index in current_available_host_types.keys():
+ self.host_type_index = current_available_host_types[self.host_type_index]
+
+ if self.host_type_index not in current_available_host_types.values():
+ self.module.fail_json(msg="Invalid host type index! Array [%s]. Available host options [%s]." % (self.ssid, current_available_host_types))
+
+ return int(self.host_type_index) != self.get_current_configuration()["default_host_type_index"]
+
+ def change_autoload_enabled_required(self):
+ """Determine whether automatic load balancing state change is required."""
+ if self.autoload_enabled is None:
+ return False
+
+ change_required = False
+ if self.autoload_enabled and not self.get_current_configuration()["autoload_capable"]:
+ self.module.fail_json(msg="Automatic load balancing is not available. Array [%s]." % self.ssid)
+
+ if self.autoload_enabled:
+ if not self.get_current_configuration()["autoload_enabled"] or not self.get_current_configuration()["host_connectivity_reporting_enabled"]:
+ change_required = True
+ elif self.get_current_configuration()["autoload_enabled"]:
+ change_required = True
+
+ return change_required
+
+ def change_host_connectivity_reporting_enabled_required(self):
+ """Determine whether host connectivity reporting state change is required."""
+ if self.host_connectivity_reporting_enabled is None:
+ return False
+
+ return self.host_connectivity_reporting_enabled != self.get_current_configuration()["host_connectivity_reporting_enabled"]
+
+ def change_name_required(self):
+ """Determine whether storage array name change is required."""
+ if self.name is None:
+ return False
+
+ if self.name and len(self.name) > 30:
+ self.module.fail_json(msg="The provided name is invalid, it must be less than or equal to 30 characters in length. Array [%s]" % self.ssid)
+
+ return self.name != self.get_current_configuration()["name"]
+
+ def update_cache_settings(self):
+ """Update cache block size and/or flushing threshold."""
+ block_size = self.cache_block_size if self.cache_block_size else self.get_current_configuration()["cache_settings"]["cache_block_size"]
+ threshold = self.cache_flush_threshold if self.cache_flush_threshold else self.get_current_configuration()["cache_settings"]["cache_flush_threshold"]
+ try:
+ rc, cache_settings = self.request("storage-systems/%s/symbol/setSACacheParams?verboseErrorResponse=true" % self.ssid, method="POST",
+ data={"cacheBlkSize": block_size, "demandFlushAmount": threshold, "demandFlushThreshold": threshold})
+ except Exception as error:
+ self.module.fail_json(msg="Failed to set cache settings. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ def update_host_type(self):
+ """Update default host type."""
+ try:
+ rc, default_host_type = self.request("storage-systems/%s/symbol/setStorageArrayProperties?verboseErrorResponse=true" % self.ssid, method="POST",
+ data={"settings": {"defaultHostTypeIndex": self.host_type_index}})
+ except Exception as error:
+ self.module.fail_json(msg="Failed to set default host type. Array [%s]. Error [%s]" % (self.ssid, to_native(error)))
+
+ def update_autoload(self):
+ """Update automatic load balancing state."""
+ if self.autoload_enabled and not self.get_current_configuration()["host_connectivity_reporting_enabled"]:
+ try:
+ rc, host_connectivity_reporting = self.request("storage-systems/%s/symbol/setHostConnectivityReporting?verboseErrorResponse=true" % self.ssid,
+ method="POST", data={"enableHostConnectivityReporting": self.autoload_enabled})
+ except Exception as error:
+ self.module.fail_json(msg="Failed to enable host connectivity reporting which is needed for automatic load balancing state."
+ " Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ try:
+ rc, autoload = self.request("storage-systems/%s/symbol/setAutoLoadBalancing?verboseErrorResponse=true" % self.ssid,
+ method="POST", data={"enableAutoLoadBalancing": self.autoload_enabled})
+ except Exception as error:
+ self.module.fail_json(msg="Failed to set automatic load balancing state. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ def update_host_connectivity_reporting_enabled(self):
+ """Update automatic load balancing state."""
+ try:
+ rc, host_connectivity_reporting = self.request("storage-systems/%s/symbol/setHostConnectivityReporting?verboseErrorResponse=true" % self.ssid,
+ method="POST", data={"enableHostConnectivityReporting": self.host_connectivity_reporting_enabled})
+ except Exception as error:
+ self.module.fail_json(msg="Failed to enable host connectivity reporting. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ def update_name(self):
+ """Update storage array's name."""
+ try:
+ rc, result = self.request("storage-systems/%s/configuration" % self.ssid, method="POST", data={"name": self.name})
+ except Exception as err:
+ self.module.fail_json(msg="Failed to set the storage array name! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ def update(self):
+ """Ensure the storage array's global setting are correctly set."""
+ change_required = False
+ self.get_current_configuration()
+
+ if (self.change_autoload_enabled_required() or self.change_cache_block_size_required() or self.change_cache_flush_threshold_required() or
+ self.change_host_type_required() or self.change_name_required() or self.change_host_connectivity_reporting_enabled_required()):
+ change_required = True
+
+ if change_required and not self.module.check_mode:
+ if self.change_autoload_enabled_required():
+ self.update_autoload()
+ if self.change_host_connectivity_reporting_enabled_required():
+ self.update_host_connectivity_reporting_enabled()
+ if self.change_cache_block_size_required() or self.change_cache_flush_threshold_required():
+ self.update_cache_settings()
+ if self.change_host_type_required():
+ self.update_host_type()
+ if self.change_name_required():
+ self.update_name()
+
+ self.get_current_configuration(update=True)
+ self.module.exit_json(changed=change_required,
+ cache_settings=self.get_current_configuration()["cache_settings"],
+ default_host_type_index=self.get_current_configuration()["default_host_type_index"],
+ automatic_load_balancing="enabled" if self.get_current_configuration()["autoload_enabled"] else "disabled",
+ host_connectivity_reporting="enabled" if self.get_current_configuration()["host_connectivity_reporting_enabled"] else "disabled",
+ array_name=self.get_current_configuration()["name"])
+
+
+def main():
+ global_settings = NetAppESeriesGlobalSettings()
+ global_settings.update()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_host.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_host.py
new file mode 100644
index 00000000..c57e13f5
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_host.py
@@ -0,0 +1,512 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_host
+short_description: NetApp E-Series manage eseries hosts
+description: Create, update, remove hosts on NetApp E-series storage arrays
+author:
+ - Kevin Hulquest (@hulquest)
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ name:
+ description:
+ - If the host doesn't yet exist, the label/name to assign at creation time.
+ - If the hosts already exists, this will be used to uniquely identify the host to make any required changes
+ type: str
+ required: True
+ aliases:
+ - label
+ state:
+ description:
+ - Set to absent to remove an existing host
+ - Set to present to modify or create a new host definition
+ type: str
+ choices:
+ - absent
+ - present
+ default: present
+ host_type:
+ description:
+ - Host type includes operating system and multipath considerations.
+ - If not specified, the default host type will be utilized. Default host type can be set using M(netapp_eseries.santricity.na_santricity_global).
+ - For storage array specific options see M(netapp_eseries.santricity.na_santricity_facts).
+ - All values are case-insensitive.
+ - AIX MPIO - The Advanced Interactive Executive (AIX) OS and the native MPIO driver
+ - AVT 4M - Silicon Graphics, Inc. (SGI) proprietary multipath driver
+ - HP-UX - The HP-UX OS with native multipath driver
+ - Linux ATTO - The Linux OS and the ATTO Technology, Inc. driver (must use ATTO FC HBAs)
+ - Linux DM-MP - The Linux OS and the native DM-MP driver
+ - Linux Pathmanager - The Linux OS and the SGI proprietary multipath driver
+ - Mac - The Mac OS and the ATTO Technology, Inc. driver
+ - ONTAP - FlexArray
+ - Solaris 11 or later - The Solaris 11 or later OS and the native MPxIO driver
+ - Solaris 10 or earlier - The Solaris 10 or earlier OS and the native MPxIO driver
+ - SVC - IBM SAN Volume Controller
+ - VMware - ESXi OS
+ - Windows - Windows Server OS and Windows MPIO with a DSM driver
+ - Windows Clustered - Clustered Windows Server OS and Windows MPIO with a DSM driver
+ - Windows ATTO - Windows OS and the ATTO Technology, Inc. driver
+ type: str
+ required: False
+ aliases:
+ - host_type_index
+ ports:
+ description:
+ - A list of host ports you wish to associate with the host.
+ - Host ports are uniquely identified by their WWN or IQN. Their assignments to a particular host are
+ uniquely identified by a label and these must be unique.
+ type: list
+ required: False
+ suboptions:
+ type:
+ description:
+ - The interface type of the port to define.
+ - Acceptable choices depend on the capabilities of the target hardware/software platform.
+ required: true
+ choices:
+ - iscsi
+ - sas
+ - fc
+ - ib
+ - nvmeof
+ label:
+ description:
+ - A unique label to assign to this port assignment.
+ required: true
+ port:
+ description:
+ - The WWN or IQN of the hostPort to assign to this port definition.
+ required: true
+ force_port:
+ description:
+ - Allow ports that are already assigned to be re-assigned to your current host
+ required: false
+ type: bool
+"""
+
+EXAMPLES = """
+ - name: Define or update an existing host named "Host1"
+ na_santricity_host:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ name: "Host1"
+ state: present
+ host_type_index: Linux DM-MP
+ ports:
+ - type: "iscsi"
+ label: "PORT_1"
+ port: "iqn.1996-04.de.suse:01:56f86f9bd1fe"
+ - type: "fc"
+ label: "FC_1"
+ port: "10:00:FF:7C:FF:FF:FF:01"
+ - type: "fc"
+ label: "FC_2"
+ port: "10:00:FF:7C:FF:FF:FF:00"
+
+ - name: Ensure a host named "Host2" doesn"t exist
+ na_santricity_host:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ name: "Host2"
+ state: absent
+"""
+
+RETURN = """
+msg:
+ description:
+ - A user-readable description of the actions performed.
+ returned: on success
+ type: str
+ sample: The host has been created.
+id:
+ description:
+ - the unique identifier of the host on the E-Series storage-system
+ returned: on success when state=present
+ type: str
+ sample: 00000000600A098000AAC0C3003004700AD86A52
+ssid:
+ description:
+ - the unique identifer of the E-Series storage-system with the current api
+ returned: on success
+ type: str
+ sample: 1
+api_url:
+ description:
+ - the url of the API that this request was proccessed by
+ returned: on success
+ type: str
+ sample: https://webservices.example.com:8443
+"""
+import re
+
+from ansible.module_utils._text import to_native
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+
+
+class NetAppESeriesHost(NetAppESeriesModule):
+ PORT_TYPES = ["iscsi", "sas", "fc", "ib", "nvmeof"]
+
+ def __init__(self):
+ ansible_options = dict(state=dict(type="str", default="present", choices=["absent", "present"]),
+ ports=dict(type="list", required=False),
+ force_port=dict(type="bool", default=False),
+ name=dict(type="str", required=True, aliases=["label"]),
+ host_type=dict(type="str", required=False, aliases=["host_type_index"]))
+
+ super(NetAppESeriesHost, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ supports_check_mode=True)
+
+ self.check_mode = self.module.check_mode
+ args = self.module.params
+ self.ports = args["ports"]
+ self.force_port = args["force_port"]
+ self.name = args["name"]
+ self.state = args["state"]
+
+ self.post_body = dict()
+ self.all_hosts = list()
+ self.host_obj = dict()
+ self.new_ports = list()
+ self.ports_for_update = list()
+ self.ports_for_removal = list()
+
+ # Update host type with the corresponding index
+ host_type = args["host_type"]
+ if host_type:
+ host_type = host_type.lower()
+ if host_type in [key.lower() for key in list(self.HOST_TYPE_INDEXES.keys())]:
+ self.host_type_index = self.HOST_TYPE_INDEXES[host_type]
+ elif host_type.isdigit():
+ self.host_type_index = int(args["host_type"])
+ else:
+ self.module.fail_json(msg="host_type must be either a host type name or host type index found integer the documentation.")
+ else:
+ self.host_type_index = None
+
+ if not self.url.endswith("/"):
+ self.url += "/"
+
+ # Fix port representation if they are provided with colons
+ if self.ports is not None:
+ for port in self.ports:
+ port["label"] = port["label"].lower()
+ port["type"] = port["type"].lower()
+ port["port"] = port["port"].lower()
+
+ if port["type"] not in self.PORT_TYPES:
+ self.module.fail_json(msg="Invalid port type! Port interface type must be one of [%s]." % ", ".join(self.PORT_TYPES))
+
+ # Determine whether address is 16-byte WWPN and, if so, remove
+ if re.match(r"^(0x)?[0-9a-f]{16}$", port["port"].replace(":", "")):
+ port["port"] = port["port"].replace(":", '').replace("0x", "")
+
+ if port["type"] == "ib":
+ port["port"] = "0" * (32 - len(port["port"])) + port["port"]
+
+ @property
+ def default_host_type(self):
+ """Return the default host type index."""
+ try:
+ rc, default_index = self.request("storage-systems/%s/graph/xpath-filter?query=/sa/defaultHostTypeIndex" % self.ssid)
+ return default_index[0]
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve default host type index")
+
+ @property
+ def valid_host_type(self):
+ host_types = None
+ try:
+ rc, host_types = self.request("storage-systems/%s/host-types" % self.ssid)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to get host types. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ try:
+ match = list(filter(lambda host_type: host_type["index"] == self.host_type_index, host_types))[0]
+ return True
+ except IndexError:
+ self.module.fail_json(msg="There is no host type with index %s" % self.host_type_index)
+
+ def check_port_types(self):
+ """Check to see whether the port interface types are available on storage system."""
+ try:
+ rc, interfaces = self.request("storage-systems/%s/interfaces?channelType=hostside" % self.ssid)
+
+ for port in self.ports:
+ for interface in interfaces:
+
+ # Check for IB iSER
+ if port["type"] == "ib" and "iqn" in port["port"]:
+ if ((interface["ioInterfaceTypeData"]["interfaceType"] == "iscsi" and
+ interface["ioInterfaceTypeData"]["iscsi"]["interfaceData"]["type"] == "infiniband" and
+ interface["ioInterfaceTypeData"]["iscsi"]["interfaceData"]["infinibandData"]["isIser"]) or
+ (interface["ioInterfaceTypeData"]["interfaceType"] == "ib" and
+ interface["ioInterfaceTypeData"]["ib"]["isISERSupported"])):
+ port["type"] = "iscsi"
+ break
+ # Check for NVMe
+ elif (port["type"] == "nvmeof" and "commandProtocolPropertiesList" in interface and
+ "commandProtocolProperties" in interface["commandProtocolPropertiesList"] and
+ interface["commandProtocolPropertiesList"]["commandProtocolProperties"]):
+ if interface["commandProtocolPropertiesList"]["commandProtocolProperties"][0]["commandProtocol"] == "nvme":
+ break
+ # Check SAS, FC, iSCSI
+ elif ((port["type"] == "fc" and interface["ioInterfaceTypeData"]["interfaceType"] == "fibre") or
+ (port["type"] == interface["ioInterfaceTypeData"]["interfaceType"])):
+ break
+ else:
+ self.module.fail_json(msg="Invalid port type! Type [%s]. Port [%s]." % (port["type"], port["label"]))
+ except Exception as error:
+ # For older versions of web services
+ for port in self.ports:
+ if port["type"] == "ib" and "iqn" in port["port"]:
+ port["type"] = "iscsi"
+ break
+
+ def assigned_host_ports(self, apply_unassigning=False):
+ """Determine if the hostPorts requested have already been assigned and return list of required used ports."""
+ used_host_ports = {}
+ for host in self.all_hosts:
+ if host["label"] != self.name.lower():
+ for host_port in host["hostSidePorts"]:
+ for port in self.ports:
+ if port["port"] == host_port["address"] or port["label"] == host_port["label"]:
+ if not self.force_port:
+ self.module.fail_json(msg="Port label or address is already used and force_port option is set to false!")
+ else:
+ # Determine port reference
+ port_ref = [port["hostPortRef"] for port in host["ports"]
+ if port["hostPortName"] == host_port["address"]]
+ port_ref.extend([port["initiatorRef"] for port in host["initiators"]
+ if port["nodeName"]["iscsiNodeName"] == host_port["address"]])
+
+ # Create dictionary of hosts containing list of port references
+ if host["hostRef"] not in used_host_ports.keys():
+ used_host_ports.update({host["hostRef"]: port_ref})
+ else:
+ used_host_ports[host["hostRef"]].extend(port_ref)
+ # else:
+ # for host_port in host["hostSidePorts"]:
+ # for port in self.ports:
+ # if ((host_port["label"] == port["label"] and host_port["address"] != port["port"]) or
+ # (host_port["label"] != port["label"] and host_port["address"] == port["port"])):
+ # if not self.force_port:
+ # self.module.fail_json(msg="Port label or address is already used and force_port is false!")
+ # # self.module.fail_json(msg="There are no host ports available OR there are not enough unassigned host ports")
+ # else:
+ # # Determine port reference
+ # port_ref = [port["hostPortRef"] for port in host["ports"]
+ # if port["hostPortName"] == host_port["address"]]
+ # port_ref.extend([port["initiatorRef"] for port in host["initiators"]
+ # if port["nodeName"]["iscsiNodeName"] == host_port["address"]])
+ #
+ # # Create dictionary of hosts containing list of port references
+ # if host["hostRef"] not in used_host_ports.keys():
+ # used_host_ports.update({host["hostRef"]: port_ref})
+ # else:
+ # used_host_ports[host["hostRef"]].extend(port_ref)
+
+ # Unassign assigned ports
+ if apply_unassigning:
+ for host_ref in used_host_ports.keys():
+ try:
+ rc, resp = self.request("storage-systems/%s/hosts/%s" % (self.ssid, host_ref), method="POST",
+ data={"portsToRemove": used_host_ports[host_ref]})
+ except Exception as err:
+ self.module.fail_json(msg="Failed to unassign host port. Host Id [%s]. Array Id [%s]. Ports [%s]. Error [%s]."
+ % (self.host_obj["id"], self.ssid, used_host_ports[host_ref], to_native(err)))
+
+ return used_host_ports
+
+ @property
+ def host_exists(self):
+ """Determine if the requested host exists
+ As a side effect, set the full list of defined hosts in "all_hosts", and the target host in "host_obj".
+ """
+ match = False
+ all_hosts = list()
+
+ try:
+ rc, all_hosts = self.request("storage-systems/%s/hosts" % self.ssid)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to determine host existence. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ # Augment the host objects
+ for host in all_hosts:
+ host["label"] = host["label"].lower()
+ for port in host["hostSidePorts"]:
+ port["type"] = port["type"].lower()
+ port["address"] = port["address"].lower()
+ port["label"] = port["label"].lower()
+
+ # Augment hostSidePorts with their ID (this is an omission in the API)
+ ports = dict((port["label"], port["id"]) for port in host["ports"])
+ ports.update((port["label"], port["id"]) for port in host["initiators"])
+
+ for host_side_port in host["hostSidePorts"]:
+ if host_side_port["label"] in ports:
+ host_side_port["id"] = ports[host_side_port["label"]]
+
+ if host["label"] == self.name.lower():
+ self.host_obj = host
+ match = True
+
+ self.all_hosts = all_hosts
+ return match
+
+ @property
+ def needs_update(self):
+ """Determine whether we need to update the Host object
+ As a side effect, we will set the ports that we need to update (portsForUpdate), and the ports we need to add
+ (newPorts), on self.
+ """
+ changed = False
+ if self.host_obj["hostTypeIndex"] != self.host_type_index:
+ changed = True
+
+ current_host_ports = dict((port["id"], {"type": port["type"], "port": port["address"], "label": port["label"]})
+ for port in self.host_obj["hostSidePorts"])
+
+ if self.ports:
+ for port in self.ports:
+ for current_host_port_id in current_host_ports.keys():
+ if port == current_host_ports[current_host_port_id]:
+ current_host_ports.pop(current_host_port_id)
+ break
+
+ elif port["port"] == current_host_ports[current_host_port_id]["port"]:
+ if self.port_on_diff_host(port) and not self.force_port:
+ self.module.fail_json(msg="The port you specified [%s] is associated with a different host."
+ " Specify force_port as True or try a different port spec" % port)
+
+ if (port["label"] != current_host_ports[current_host_port_id]["label"] or
+ port["type"] != current_host_ports[current_host_port_id]["type"]):
+ current_host_ports.pop(current_host_port_id)
+ self.ports_for_update.append({"portRef": current_host_port_id, "port": port["port"],
+ "label": port["label"], "hostRef": self.host_obj["hostRef"]})
+ break
+ else:
+ self.new_ports.append(port)
+
+ self.ports_for_removal = list(current_host_ports.keys())
+ changed = any([self.new_ports, self.ports_for_update, self.ports_for_removal, changed])
+ return changed
+
+ def port_on_diff_host(self, arg_port):
+ """ Checks to see if a passed in port arg is present on a different host """
+ for host in self.all_hosts:
+ # Only check "other" hosts
+ if host["name"] != self.name:
+ for port in host["hostSidePorts"]:
+ # Check if the port label is found in the port dict list of each host
+ if arg_port["label"] == port["label"] or arg_port["port"] == port["address"]:
+ self.other_host = host
+
+ return True
+ return False
+
+ def update_host(self):
+ if self.ports:
+
+ # Remove ports that need reassigning from their current host.
+ self.assigned_host_ports(apply_unassigning=True)
+
+ self.post_body["portsToUpdate"] = self.ports_for_update
+ self.post_body["portsToRemove"] = self.ports_for_removal
+ self.post_body["ports"] = self.new_ports
+
+ self.post_body["hostType"] = dict(index=self.host_type_index)
+ if not self.check_mode:
+ try:
+ rc, self.host_obj = self.request("storage-systems/%s/hosts/%s" % (self.ssid, self.host_obj["id"]), method="POST", data=self.post_body, ignore_errors=True)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to update host. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ self.module.exit_json(changed=True)
+
+ def create_host(self):
+ # Remove ports that need reassigning from their current host.
+ self.assigned_host_ports(apply_unassigning=True)
+
+ # needs_reassignment = False
+ post_body = dict(name=self.name,
+ hostType=dict(index=self.host_type_index))
+
+ if self.ports:
+ post_body.update(ports=self.ports)
+
+ if not self.host_exists:
+ if not self.check_mode:
+ try:
+ rc, self.host_obj = self.request("storage-systems/%s/hosts" % self.ssid, method="POST", data=post_body, ignore_errors=True)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to create host. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+ else:
+ payload = self.build_success_payload(self.host_obj)
+ self.module.exit_json(changed=False, msg="Host already exists. Id [%s]. Host [%s]." % (self.ssid, self.name), **payload)
+
+ payload = self.build_success_payload(self.host_obj)
+ self.module.exit_json(changed=True, msg="Host created.")
+
+ def remove_host(self):
+ try:
+ rc, resp = self.request("storage-systems/%s/hosts/%s" % (self.ssid, self.host_obj["id"]), method="DELETE")
+ except Exception as err:
+ self.module.fail_json(msg="Failed to remove host. Host[%s]. Array Id [%s]. Error [%s]." % (self.host_obj["id"], self.ssid, to_native(err)))
+
+ def build_success_payload(self, host=None):
+ keys = ["id"]
+
+ if host:
+ result = dict((key, host[key]) for key in keys)
+ else:
+ result = dict()
+ result["ssid"] = self.ssid
+ result["api_url"] = self.url
+ return result
+
+ def apply(self):
+ if self.state == "present":
+ if self.host_type_index is None:
+ self.host_type_index = self.default_host_type
+
+ self.check_port_types()
+ if self.host_exists:
+ if self.needs_update and self.valid_host_type:
+ self.update_host()
+ else:
+ payload = self.build_success_payload(self.host_obj)
+ self.module.exit_json(changed=False, msg="Host already present; no changes required.", **payload)
+ elif self.valid_host_type:
+ self.create_host()
+ else:
+ payload = self.build_success_payload()
+ if self.host_exists:
+ self.remove_host()
+ self.module.exit_json(changed=True, msg="Host removed.", **payload)
+ else:
+ self.module.exit_json(changed=False, msg="Host already absent.", **payload)
+
+
+def main():
+ host = NetAppESeriesHost()
+ host.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_hostgroup.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_hostgroup.py
new file mode 100644
index 00000000..7b8a9e2a
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_hostgroup.py
@@ -0,0 +1,279 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_hostgroup
+short_description: NetApp E-Series manage array host groups
+author:
+ - Kevin Hulquest (@hulquest)
+ - Nathan Swartz (@ndswartz)
+description: Create, update or destroy host groups on a NetApp E-Series storage array.
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ state:
+ description:
+ - Whether the specified host group should exist or not.
+ type: str
+ choices: ["present", "absent"]
+ default: present
+ name:
+ description:
+ - Name of the host group to manage
+ type: str
+ required: false
+ hosts:
+ description:
+ - List of host names/labels to add to the group
+ type: list
+ required: false
+"""
+EXAMPLES = """
+ - name: Configure Hostgroup
+ na_santricity_hostgroup:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ state: present
+ name: example_hostgroup
+ hosts:
+ - host01
+ - host02
+"""
+RETURN = """
+clusterRef:
+ description: The unique identification value for this object. Other objects may use this reference value to refer to the cluster.
+ returned: always except when state is absent
+ type: str
+ sample: "3233343536373839303132333100000000000000"
+confirmLUNMappingCreation:
+ description: If true, indicates that creation of LUN-to-volume mappings should require careful confirmation from the end-user, since such a mapping
+ will alter the volume access rights of other clusters, in addition to this one.
+ returned: always
+ type: bool
+ sample: false
+hosts:
+ description: A list of the hosts that are part of the host group after all operations.
+ returned: always except when state is absent
+ type: list
+ sample: ["HostA","HostB"]
+id:
+ description: The id number of the hostgroup
+ returned: always except when state is absent
+ type: str
+ sample: "3233343536373839303132333100000000000000"
+isSAControlled:
+ description: If true, indicates that I/O accesses from this cluster are subject to the storage array's default LUN-to-volume mappings. If false,
+ indicates that I/O accesses from the cluster are subject to cluster-specific LUN-to-volume mappings.
+ returned: always except when state is absent
+ type: bool
+ sample: false
+label:
+ description: The user-assigned, descriptive label string for the cluster.
+ returned: always
+ type: str
+ sample: "MyHostGroup"
+name:
+ description: same as label
+ returned: always except when state is absent
+ type: str
+ sample: "MyHostGroup"
+protectionInformationCapableAccessMethod:
+ description: This field is true if the host has a PI capable access method.
+ returned: always except when state is absent
+ type: bool
+ sample: true
+"""
+from ansible.module_utils._text import to_native
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule, create_multipart_formdata, request
+
+
+class NetAppESeriesHostGroup(NetAppESeriesModule):
+ EXPANSION_TIMEOUT_SEC = 10
+ DEFAULT_DISK_POOL_MINIMUM_DISK_COUNT = 11
+
+ def __init__(self):
+ version = "02.00.0000.0000"
+ ansible_options = dict(
+ state=dict(choices=["present", "absent"], type="str", default="present"),
+ name=dict(required=True, type="str"),
+ hosts=dict(required=False, type="list"))
+ super(NetAppESeriesHostGroup, self).__init__(ansible_options=ansible_options,
+ web_services_version=version,
+ supports_check_mode=True)
+
+ args = self.module.params
+ self.state = args["state"]
+ self.name = args["name"]
+ self.hosts_list = args["hosts"]
+
+ self.current_host_group = None
+ self.hosts_cache = None
+
+ @property
+ def hosts(self):
+ """Retrieve a list of host reference identifiers should be associated with the host group."""
+ if self.hosts_cache is None:
+ self.hosts_cache = []
+ existing_hosts = []
+
+ if self.hosts_list:
+ try:
+ rc, existing_hosts = self.request("storage-systems/%s/hosts" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve hosts information. Array id [%s]. Error[%s]."
+ % (self.ssid, to_native(error)))
+
+ for host in self.hosts_list:
+ for existing_host in existing_hosts:
+ if host in existing_host["id"] or host.lower() in existing_host["name"].lower():
+ self.hosts_cache.append(existing_host["id"])
+ break
+ else:
+ self.module.fail_json(msg="Expected host does not exist. Array id [%s]. Host [%s]." % (self.ssid, host))
+ self.hosts_cache.sort()
+ return self.hosts_cache
+
+ @property
+ def host_groups(self):
+ """Retrieve a list of existing host groups."""
+ host_groups = []
+ hosts = []
+ try:
+ rc, host_groups = self.request("storage-systems/%s/host-groups" % self.ssid)
+ rc, hosts = self.request("storage-systems/%s/hosts" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve host group information. Array id [%s]. Error[%s]."
+ % (self.ssid, to_native(error)))
+
+ host_groups = [{"id": group["clusterRef"], "name": group["name"]} for group in host_groups]
+ for group in host_groups:
+ hosts_ids = []
+ for host in hosts:
+ if group["id"] == host["clusterRef"]:
+ hosts_ids.append(host["hostRef"])
+ group.update({"hosts": hosts_ids})
+
+ return host_groups
+
+ @property
+ def current_hosts_in_host_group(self):
+ """Retrieve the current hosts associated with the current hostgroup."""
+ current_hosts = []
+ for group in self.host_groups:
+ if group["name"] == self.name:
+ current_hosts = group["hosts"]
+ break
+
+ return current_hosts
+
+ def unassign_hosts(self, host_list=None):
+ """Unassign hosts from host group."""
+ if host_list is None:
+ host_list = self.current_host_group["hosts"]
+
+ for host_id in host_list:
+ try:
+ rc, resp = self.request("storage-systems/%s/hosts/%s/move" % (self.ssid, host_id),
+ method="POST", data={"group": "0000000000000000000000000000000000000000"})
+ except Exception as error:
+ self.module.fail_json(msg="Failed to unassign hosts from host group. Array id [%s]. Host id [%s]."
+ " Error[%s]." % (self.ssid, host_id, to_native(error)))
+
+ def delete_host_group(self, unassign_hosts=True):
+ """Delete host group"""
+ if unassign_hosts:
+ self.unassign_hosts()
+
+ try:
+ rc, resp = self.request("storage-systems/%s/host-groups/%s" % (self.ssid, self.current_host_group["id"]), method="DELETE")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to delete host group. Array id [%s]. Error[%s]." % (self.ssid, to_native(error)))
+
+ def create_host_group(self):
+ """Create host group."""
+ data = {"name": self.name, "hosts": self.hosts}
+
+ response = None
+ try:
+ rc, response = self.request("storage-systems/%s/host-groups" % self.ssid, method="POST", data=data)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to create host group. Array id [%s]. Error[%s]." % (self.ssid, to_native(error)))
+
+ return response
+
+ def update_host_group(self):
+ """Update host group."""
+ data = {"name": self.name, "hosts": self.hosts}
+
+ # unassign hosts that should not be part of the hostgroup
+ desired_host_ids = self.hosts
+ for host in self.current_hosts_in_host_group:
+ if host not in desired_host_ids:
+ self.unassign_hosts([host])
+
+ update_response = None
+ try:
+ rc, update_response = self.request("storage-systems/%s/host-groups/%s" % (self.ssid, self.current_host_group["id"]), method="POST", data=data)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to create host group. Array id [%s]. Error[%s]." % (self.ssid, to_native(error)))
+
+ return update_response
+
+ def apply(self):
+ """Apply desired host group state to the storage array."""
+ changes_required = False
+
+ # Search for existing host group match
+ for group in self.host_groups:
+ if group["name"] == self.name:
+ self.current_host_group = group
+ self.current_host_group["hosts"].sort()
+ break
+
+ # Determine whether changes are required
+ if self.state == "present":
+ if self.current_host_group:
+ if self.hosts and self.hosts != self.current_host_group["hosts"]:
+ changes_required = True
+ else:
+ if not self.name:
+ self.module.fail_json(msg="The option name must be supplied when creating a new host group. Array id [%s]." % self.ssid)
+ changes_required = True
+
+ elif self.current_host_group:
+ changes_required = True
+
+ # Apply any necessary changes
+ msg = ""
+ if changes_required and not self.module.check_mode:
+ msg = "No changes required."
+ if self.state == "present":
+ if self.current_host_group:
+ if self.hosts != self.current_host_group["hosts"]:
+ msg = self.update_host_group()
+ else:
+ msg = self.create_host_group()
+
+ elif self.current_host_group:
+ self.delete_host_group()
+ msg = "Host group deleted. Array Id [%s]. Host group [%s]." % (self.ssid, self.current_host_group["name"])
+
+ self.module.exit_json(msg=msg, changed=changes_required)
+
+
+def main():
+ hostgroup = NetAppESeriesHostGroup()
+ hostgroup.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_ib_iser_interface.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_ib_iser_interface.py
new file mode 100644
index 00000000..05119eb2
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_ib_iser_interface.py
@@ -0,0 +1,250 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_ib_iser_interface
+short_description: NetApp E-Series manage InfiniBand iSER interface configuration
+description:
+ - Configure settings of an E-Series InfiniBand iSER interface IPv4 address configuration.
+author:
+ - Michael Price (@lmprice)
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ controller:
+ description:
+ - The controller that owns the port you want to configure.
+ - Controller names are presented alphabetically, with the first controller as A, the second as B, and so on.
+ - Current hardware models have either 1 or 2 available controllers, but that is not a guaranteed hard limitation and could change in the future.
+ type: str
+ required: true
+ choices:
+ - A
+ - B
+ channel:
+ description:
+ - The InfiniBand HCA port you wish to modify.
+ - Ports start left to right and start with 1.
+ type: int
+ required: true
+ address:
+ description:
+ - The IPv4 address to assign to the interface.
+ - Should be specified in xx.xx.xx.xx form.
+ type: str
+ required: true
+notes:
+ - Check mode is supported.
+"""
+
+EXAMPLES = """
+ - name: Configure the first port on the A controller with a static IPv4 address
+ na_santricity_ib_iser_interface:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ controller: "A"
+ channel: "1"
+ address: "192.168.1.100"
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The interface settings have been updated.
+enabled:
+ description:
+ - Indicates whether IPv4 connectivity has been enabled or disabled.
+ - This does not necessarily indicate connectivity. If dhcp was enabled without a dhcp server, for instance,
+ it is unlikely that the configuration will actually be valid.
+ returned: on success
+ sample: True
+ type: bool
+"""
+import re
+
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+
+class NetAppESeriesIbIserInterface(NetAppESeriesModule):
+ def __init__(self):
+ ansible_options = dict(controller=dict(type="str", required=True, choices=["A", "B"]),
+ channel=dict(type="int"),
+ address=dict(type="str", required=True))
+
+ super(NetAppESeriesIbIserInterface, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ supports_check_mode=True)
+
+ args = self.module.params
+ self.controller = args["controller"]
+ self.channel = args["channel"]
+ self.address = args["address"]
+ self.check_mode = self.module.check_mode
+
+ self.get_target_interface_cache = None
+
+ # A relatively primitive regex to validate that the input is formatted like a valid ip address
+ address_regex = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
+ if self.address and not address_regex.match(self.address):
+ self.module.fail_json(msg="An invalid ip address was provided for address.")
+
+ def get_interfaces(self):
+ """Retrieve and filter all hostside interfaces for IB iSER."""
+ ifaces = []
+ try:
+ rc, ifaces = self.request("storage-systems/%s/interfaces?channelType=hostside" % self.ssid)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to retrieve defined host interfaces. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ # Filter out non-ib-iser interfaces
+ ib_iser_ifaces = []
+ for iface in ifaces:
+ if ((iface["ioInterfaceTypeData"]["interfaceType"] == "iscsi" and
+ iface["ioInterfaceTypeData"]["iscsi"]["interfaceData"]["type"] == "infiniband" and
+ iface["ioInterfaceTypeData"]["iscsi"]["interfaceData"]["infinibandData"]["isIser"]) or
+ (iface["ioInterfaceTypeData"]["interfaceType"] == "ib" and
+ iface["ioInterfaceTypeData"]["ib"]["isISERSupported"])):
+ ib_iser_ifaces.append(iface)
+
+ if not ib_iser_ifaces:
+ self.module.fail_json(msg="Failed to detect any InfiniBand iSER interfaces! Array [%s] - %s." % self.ssid)
+
+ return ib_iser_ifaces
+
+ def get_controllers(self):
+ """Retrieve a mapping of controller labels to their references
+ {
+ 'A': '070000000000000000000001',
+ 'B': '070000000000000000000002',
+ }
+ :return: the controllers defined on the system
+ """
+ controllers = list()
+ try:
+ rc, controllers = self.request("storage-systems/%s/graph/xpath-filter?query=/controller/id" % self.ssid)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to retrieve controller list! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ controllers.sort()
+
+ controllers_dict = {}
+ i = ord('A')
+ for controller in controllers:
+ label = chr(i)
+ controllers_dict[label] = controller
+ i += 1
+
+ return controllers_dict
+
+ def get_ib_link_status(self):
+ """Determine the infiniband link status. Returns dictionary keyed by interface reference number."""
+ link_statuses = {}
+ try:
+ rc, result = self.request("storage-systems/%s/hardware-inventory" % self.ssid)
+ for link in result["ibPorts"]:
+ link_statuses.update({link["channelPortRef"]: link["linkState"]})
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve ib link status information! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(error)))
+
+ return link_statuses
+
+ def get_target_interface(self):
+ """Search for the selected IB iSER interface"""
+ if self.get_target_interface_cache is None:
+ ifaces = self.get_interfaces()
+ ifaces_status = self.get_ib_link_status()
+ controller_id = self.get_controllers()[self.controller]
+
+ controller_ifaces = []
+ for iface in ifaces:
+ if iface["ioInterfaceTypeData"]["interfaceType"] == "iscsi" and iface["ioInterfaceTypeData"]["iscsi"]["controllerId"] == controller_id:
+ controller_ifaces.append([iface["ioInterfaceTypeData"]["iscsi"]["channel"], iface,
+ ifaces_status[iface["ioInterfaceTypeData"]["iscsi"]["channelPortRef"]]])
+ elif iface["ioInterfaceTypeData"]["interfaceType"] == "ib" and iface["ioInterfaceTypeData"]["ib"]["controllerId"] == controller_id:
+ controller_ifaces.append([iface["ioInterfaceTypeData"]["ib"]["channel"], iface,
+ iface["ioInterfaceTypeData"]["ib"]["linkState"]])
+
+ sorted_controller_ifaces = sorted(controller_ifaces)
+ if self.channel < 1 or self.channel > len(controller_ifaces):
+ status_msg = ", ".join(["%s (link %s)" % (index + 1, values[2])
+ for index, values in enumerate(sorted_controller_ifaces)])
+ self.module.fail_json(msg="Invalid controller %s HCA channel. Available channels: %s, Array Id [%s]."
+ % (self.controller, status_msg, self.ssid))
+
+ self.get_target_interface_cache = sorted_controller_ifaces[self.channel - 1][1]
+ return self.get_target_interface_cache
+
+ def is_change_required(self):
+ """Determine whether change is required."""
+ iface = self.get_target_interface()
+ if ((iface["ioInterfaceTypeData"]["interfaceType"] == "iscsi" and iface["iscsi"]["ipv4Data"]["ipv4AddressData"]["ipv4Address"] != self.address) or
+ (iface["ioInterfaceTypeData"]["interfaceType"] == "ib" and
+ iface["commandProtocolPropertiesList"]["commandProtocolProperties"][0]["scsiProperties"]["scsiProtocolType"] == "iser")):
+ return True
+
+ return False
+
+ def make_request_body(self):
+ iface = self.get_target_interface()
+ body = {"iscsiInterface": iface["ioInterfaceTypeData"][iface["ioInterfaceTypeData"]["interfaceType"]]["id"],
+ "settings": {"tcpListenPort": [],
+ "ipv4Address": [self.address],
+ "ipv4SubnetMask": [],
+ "ipv4GatewayAddress": [],
+ "ipv4AddressConfigMethod": [],
+ "maximumFramePayloadSize": [],
+ "ipv4VlanId": [],
+ "ipv4OutboundPacketPriority": [],
+ "ipv4Enabled": [],
+ "ipv6Enabled": [],
+ "ipv6LocalAddresses": [],
+ "ipv6RoutableAddresses": [],
+ "ipv6PortRouterAddress": [],
+ "ipv6AddressConfigMethod": [],
+ "ipv6OutboundPacketPriority": [],
+ "ipv6VlanId": [],
+ "ipv6HopLimit": [],
+ "ipv6NdReachableTime": [],
+ "ipv6NdRetransmitTime": [],
+ "ipv6NdStaleTimeout": [],
+ "ipv6DuplicateAddressDetectionAttempts": [],
+ "maximumInterfaceSpeed": []}}
+ return body
+
+ def update(self):
+ """Make any necessary updates."""
+ update_required = self.is_change_required()
+ if update_required and not self.check_mode:
+ try:
+ rc, result = self.request("storage-systems/%s/symbol/setIscsiInterfaceProperties"
+ % self.ssid, method="POST", data=self.make_request_body())
+ except Exception as error:
+ self.module.fail_json(msg="Failed to modify the interface! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(error)))
+ self.module.exit_json(msg="The interface settings have been updated.", changed=update_required)
+
+ self.module.exit_json(msg="No changes were required.", changed=update_required)
+
+
+def main():
+ ib_iser = NetAppESeriesIbIserInterface()
+ ib_iser.update()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_iscsi_interface.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_iscsi_interface.py
new file mode 100644
index 00000000..6da22918
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_iscsi_interface.py
@@ -0,0 +1,342 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_iscsi_interface
+short_description: NetApp E-Series manage iSCSI interface configuration
+description:
+ - Configure settings of an E-Series iSCSI interface
+author:
+ - Michael Price (@lmprice)
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ controller:
+ description:
+ - The controller that owns the port you want to configure.
+ - Controller names are presented alphabetically, with the first controller as A,
+ the second as B, and so on.
+ - Current hardware models have either 1 or 2 available controllers, but that is not a guaranteed hard
+ limitation and could change in the future.
+ type: str
+ required: true
+ choices:
+ - A
+ - B
+ port:
+ description:
+ - The controller iSCSI HIC port to modify.
+ - You can determine this value by numbering the iSCSI ports left to right on the controller you wish to modify starting with one.
+ type: int
+ required: true
+ state:
+ description:
+ - When enabled, the provided configuration will be utilized.
+ - When disabled, the IPv4 configuration will be cleared and IPv4 connectivity disabled.
+ type: str
+ choices:
+ - enabled
+ - disabled
+ default: enabled
+ address:
+ description:
+ - The IPv4 address to assign to the interface.
+ - Should be specified in xx.xx.xx.xx form.
+ - Mutually exclusive with I(config_method=dhcp)
+ type: str
+ required: false
+ subnet_mask:
+ description:
+ - The subnet mask to utilize for the interface.
+ - Should be specified in xx.xx.xx.xx form.
+ - Mutually exclusive with I(config_method=dhcp)
+ type: str
+ gateway:
+ description:
+ - The IPv4 gateway address to utilize for the interface.
+ - Should be specified in xx.xx.xx.xx form.
+ - Mutually exclusive with I(config_method=dhcp)
+ type: str
+ required: false
+ config_method:
+ description:
+ - The configuration method type to use for this interface.
+ - dhcp is mutually exclusive with I(address), I(subnet_mask), and I(gateway).
+ type: str
+ choices:
+ - dhcp
+ - static
+ default: dhcp
+ required: false
+ mtu:
+ description:
+ - The maximum transmission units (MTU), in bytes.
+ - This allows you to configure a larger value for the MTU, in order to enable jumbo frames
+ (any value > 1500).
+ - Generally, it is necessary to have your host, switches, and other components not only support jumbo
+ frames, but also have it configured properly. Therefore, unless you know what you're doing, it's best to
+ leave this at the default.
+ type: int
+ default: 1500
+ required: false
+ aliases:
+ - max_frame_size
+notes:
+ - Check mode is supported.
+ - The interface settings are applied synchronously, but changes to the interface itself (receiving a new IP address
+ via dhcp, etc), can take seconds or minutes longer to take effect.
+ - This module will not be useful/usable on an E-Series system without any iSCSI interfaces.
+ - This module requires a Web Services API version of >= 1.3.
+"""
+
+EXAMPLES = """
+ - name: Configure the first port on the A controller with a static IPv4 address
+ na_santricity_iscsi_interface:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ port: "1"
+ controller: "A"
+ config_method: static
+ address: "192.168.1.100"
+ subnet_mask: "255.255.255.0"
+ gateway: "192.168.1.1"
+
+ - name: Disable ipv4 connectivity for the second port on the B controller
+ na_santricity_iscsi_interface:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ port: "2"
+ controller: "B"
+ state: disabled
+
+ - name: Enable jumbo frames for the first 4 ports on controller A
+ na_santricity_iscsi_interface:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ port: "{{ item }}"
+ controller: "A"
+ state: enabled
+ mtu: 9000
+ config_method: dhcp
+ loop:
+ - 1
+ - 2
+ - 3
+ - 4
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The interface settings have been updated.
+"""
+import re
+
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+
+class NetAppESeriesIscsiInterface(NetAppESeriesModule):
+ def __init__(self):
+ ansible_options = dict(controller=dict(type="str", required=True, choices=["A", "B"]),
+ port=dict(type="int", required=True),
+ state=dict(type="str", required=False, default="enabled", choices=["enabled", "disabled"]),
+ address=dict(type="str", required=False),
+ subnet_mask=dict(type="str", required=False),
+ gateway=dict(type="str", required=False),
+ config_method=dict(type="str", required=False, default="dhcp", choices=["dhcp", "static"]),
+ mtu=dict(type="int", default=1500, required=False, aliases=["max_frame_size"]))
+
+ required_if = [["config_method", "static", ["address", "subnet_mask"]]]
+ super(NetAppESeriesIscsiInterface, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ required_if=required_if,
+ supports_check_mode=True)
+
+ args = self.module.params
+ self.controller = args["controller"]
+ self.port = args["port"]
+ self.mtu = args["mtu"]
+ self.state = args["state"]
+ self.address = args["address"]
+ self.subnet_mask = args["subnet_mask"]
+ self.gateway = args["gateway"]
+ self.config_method = args["config_method"]
+
+ self.check_mode = self.module.check_mode
+ self.post_body = dict()
+ self.controllers = list()
+ self.get_target_interface_cache = None
+
+ if self.mtu < 1500 or self.mtu > 9000:
+ self.module.fail_json(msg="The provided mtu is invalid, it must be > 1500 and < 9000 bytes.")
+
+ if self.config_method == "dhcp" and any([self.address, self.subnet_mask, self.gateway]):
+ self.module.fail_json(msg="A config_method of dhcp is mutually exclusive with the address,"
+ " subnet_mask, and gateway options.")
+
+ # A relatively primitive regex to validate that the input is formatted like a valid ip address
+ address_regex = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
+
+ if self.address and not address_regex.match(self.address):
+ self.module.fail_json(msg="An invalid ip address was provided for address.")
+
+ if self.subnet_mask and not address_regex.match(self.subnet_mask):
+ self.module.fail_json(msg="An invalid ip address was provided for subnet_mask.")
+
+ if self.gateway and not address_regex.match(self.gateway):
+ self.module.fail_json(msg="An invalid ip address was provided for gateway.")
+
+ @property
+ def interfaces(self):
+ ifaces = list()
+ try:
+ rc, ifaces = self.request("storage-systems/%s/graph/xpath-filter?query=/controller/hostInterfaces" % self.ssid)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to retrieve defined host interfaces. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ # Filter out non-iSCSI interfaces
+ iscsi_interfaces = []
+ for iface in [iface for iface in ifaces if iface["interfaceType"] == "iscsi"]:
+ if iface["iscsi"]["interfaceData"]["type"] == "ethernet":
+ iscsi_interfaces.append(iface)
+
+ return iscsi_interfaces
+
+ def get_controllers(self):
+ """Retrieve a mapping of controller labels to their references
+ {
+ "A": "070000000000000000000001",
+ "B": "070000000000000000000002",
+ }
+ :return: the controllers defined on the system
+ """
+ controllers = list()
+ try:
+ rc, controllers = self.request("storage-systems/%s/graph/xpath-filter?query=/controller/id" % self.ssid)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to retrieve controller list! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ controllers.sort()
+
+ controllers_dict = {}
+ i = ord("A")
+ for controller in controllers:
+ label = chr(i)
+ controllers_dict[label] = controller
+ i += 1
+
+ return controllers_dict
+
+ def get_target_interface(self):
+ """Retrieve the specific controller iSCSI interface."""
+ if self.get_target_interface_cache is None:
+ ifaces = self.interfaces
+
+ controller_ifaces = []
+ for iface in ifaces:
+ if self.controllers[self.controller] == iface["iscsi"]["controllerId"]:
+ controller_ifaces.append([iface["iscsi"]["channel"], iface, iface["iscsi"]["interfaceData"]["ethernetData"]["linkStatus"]])
+
+ sorted_controller_ifaces = sorted(controller_ifaces)
+ if self.port < 1 or self.port > len(controller_ifaces):
+ status_msg = ", ".join(["%s (link %s)" % (index + 1, values[2]) for index, values in enumerate(sorted_controller_ifaces)])
+ self.module.fail_json(msg="Invalid controller %s iSCSI port. Available ports: %s, Array Id [%s]."
+ % (self.controller, status_msg, self.ssid))
+
+ self.get_target_interface_cache = sorted_controller_ifaces[self.port - 1][1]
+ return self.get_target_interface_cache
+
+ def make_update_body(self, target_iface):
+ target_iface = target_iface["iscsi"]
+ body = dict(iscsiInterface=target_iface["id"])
+ update_required = False
+
+ if self.state == "enabled":
+ settings = dict()
+ if not target_iface["ipv4Enabled"]:
+ update_required = True
+ settings["ipv4Enabled"] = [True]
+ if self.mtu != target_iface["interfaceData"]["ethernetData"]["maximumFramePayloadSize"]:
+ update_required = True
+ settings["maximumFramePayloadSize"] = [self.mtu]
+ if self.config_method == "static":
+ ipv4Data = target_iface["ipv4Data"]["ipv4AddressData"]
+
+ if ipv4Data["ipv4Address"] != self.address:
+ update_required = True
+ settings["ipv4Address"] = [self.address]
+ if ipv4Data["ipv4SubnetMask"] != self.subnet_mask:
+ update_required = True
+ settings["ipv4SubnetMask"] = [self.subnet_mask]
+ if self.gateway is not None and ipv4Data["ipv4GatewayAddress"] != self.gateway:
+ update_required = True
+ settings["ipv4GatewayAddress"] = [self.gateway]
+
+ if target_iface["ipv4Data"]["ipv4AddressConfigMethod"] != "configStatic":
+ update_required = True
+ settings["ipv4AddressConfigMethod"] = ["configStatic"]
+
+ elif target_iface["ipv4Data"]["ipv4AddressConfigMethod"] != "configDhcp":
+ update_required = True
+ settings.update(dict(ipv4Enabled=[True],
+ ipv4AddressConfigMethod=["configDhcp"]))
+ body["settings"] = settings
+
+ else:
+ if target_iface["ipv4Enabled"]:
+ update_required = True
+ body["settings"] = dict(ipv4Enabled=[False])
+
+ return update_required, body
+
+ def update(self):
+ self.controllers = self.get_controllers()
+ if self.controller not in self.controllers:
+ self.module.fail_json(msg="The provided controller name is invalid. Valid controllers: %s." % ", ".join(self.controllers.keys()))
+
+ iface_before = self.get_target_interface()
+ update_required, body = self.make_update_body(iface_before)
+ if update_required and not self.check_mode:
+ try:
+ rc, result = self.request("storage-systems/%s/symbol/setIscsiInterfaceProperties" % self.ssid, method="POST", data=body, ignore_errors=True)
+ # We could potentially retry this a few times, but it's probably a rare enough case (unless a playbook
+ # is cancelled mid-flight), that it isn't worth the complexity.
+ if rc == 422 and result["retcode"] in ["busy", "3"]:
+ self.module.fail_json(msg="The interface is currently busy (probably processing a previously requested modification request)."
+ " This operation cannot currently be completed. Array Id [%s]. Error [%s]." % (self.ssid, result))
+ # Handle authentication issues, etc.
+ elif rc != 200:
+ self.module.fail_json(msg="Failed to modify the interface! Array Id [%s]. Error [%s]." % (self.ssid, to_native(result)))
+ # This is going to catch cases like a connection failure
+ except Exception as err:
+ self.module.fail_json(msg="Connection failure: we failed to modify the interface! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ self.module.exit_json(msg="The interface settings have been updated.", changed=update_required)
+
+
+def main():
+ iface = NetAppESeriesIscsiInterface()
+ iface.update()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_iscsi_target.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_iscsi_target.py
new file mode 100644
index 00000000..46a93d2e
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_iscsi_target.py
@@ -0,0 +1,246 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_iscsi_target
+short_description: NetApp E-Series manage iSCSI target configuration
+description:
+ - Configure the settings of an E-Series iSCSI target
+author:
+ - Michael Price (@lmprice)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ name:
+ description:
+ - The name/alias to assign to the iSCSI target.
+ - This alias is often used by the initiator software in order to make an iSCSI target easier to identify.
+ type: str
+ required: false
+ aliases:
+ - alias
+ ping:
+ description:
+ - Enable ICMP ping responses from the configured iSCSI ports.
+ type: bool
+ default: true
+ required: false
+ chap_secret:
+ description:
+ - Enable Challenge-Handshake Authentication Protocol (CHAP), utilizing this value as the password.
+ - When this value is specified, we will always trigger an update (changed=True). We have no way of verifying
+ whether or not the password has changed.
+ - The chap secret may only use ascii characters with values between 32 and 126 decimal.
+ - The chap secret must be no less than 12 characters, but no greater than 57 characters in length.
+ - The chap secret is cleared when not specified or an empty string.
+ type: str
+ required: false
+ aliases:
+ - chap
+ - password
+ unnamed_discovery:
+ description:
+ - When an initiator initiates a discovery session to an initiator port, it is considered an unnamed
+ discovery session if the iSCSI target iqn is not specified in the request.
+ - This option may be disabled to increase security if desired.
+ type: bool
+ default: true
+ required: false
+notes:
+ - Check mode is supported.
+ - Some of the settings are dependent on the settings applied to the iSCSI interfaces. These can be configured using
+ M(netapp_eseries.santricity.na_santricity_iscsi_interface).
+ - This module requires a Web Services API version of >= 1.3.
+"""
+
+EXAMPLES = """
+ - name: Enable ping responses and unnamed discovery sessions for all iSCSI ports
+ na_santricity_iscsi_target:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ name: myTarget
+ ping: true
+ unnamed_discovery: true
+
+ - name: Set the target alias and the CHAP secret
+ na_santricity_iscsi_target:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ name: myTarget
+ chap: password1234
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The iSCSI target settings have been updated.
+alias:
+ description:
+ - The alias assigned to the iSCSI target.
+ returned: on success
+ sample: myArray
+ type: str
+iqn:
+ description:
+ - The iqn (iSCSI Qualified Name), assigned to the iSCSI target.
+ returned: on success
+ sample: iqn.1992-08.com.netapp:2800.000a132000b006d2000000005a0e8f45
+ type: str
+"""
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+}
+
+
+class NetAppESeriesIscsiTarget(NetAppESeriesModule):
+ def __init__(self):
+ ansible_options = dict(name=dict(type="str", required=False, aliases=["alias"]),
+ ping=dict(type="bool", required=False, default=True),
+ chap_secret=dict(type="str", required=False, aliases=["chap", "password"], no_log=True),
+ unnamed_discovery=dict(type="bool", required=False, default=True))
+
+ super(NetAppESeriesIscsiTarget, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ supports_check_mode=True)
+
+ args = self.module.params
+
+ self.name = args["name"]
+ self.ping = args["ping"]
+ self.chap_secret = args["chap_secret"]
+ self.unnamed_discovery = args["unnamed_discovery"]
+
+ self.check_mode = self.module.check_mode
+ self.post_body = dict()
+ self.controllers = list()
+
+ if self.chap_secret:
+ if len(self.chap_secret) < 12 or len(self.chap_secret) > 57:
+ self.module.fail_json(msg="The provided CHAP secret is not valid, it must be between 12 and 57"
+ " characters in length.")
+
+ for c in self.chap_secret:
+ ordinal = ord(c)
+ if ordinal < 32 or ordinal > 126:
+ self.module.fail_json(msg="The provided CHAP secret is not valid, it may only utilize ascii"
+ " characters with decimal values between 32 and 126.")
+
+ @property
+ def target(self):
+ """Provide information on the iSCSI Target configuration
+
+ Sample:
+ {
+ "alias": "myCustomName",
+ "ping": True,
+ "unnamed_discovery": True,
+ "chap": False,
+ "iqn": "iqn.1992-08.com.netapp:2800.000a132000b006d2000000005a0e8f45",
+ }
+ """
+ target = dict()
+ try:
+ rc, data = self.request("storage-systems/%s/graph/xpath-filter?query=/storagePoolBundle/target" % self.ssid)
+ # This likely isn"t an iSCSI-enabled system
+ if not data:
+ self.module.fail_json(msg="This storage-system does not appear to have iSCSI interfaces. Array Id [%s]." % self.ssid)
+
+ data = data[0]
+ chap = any([auth for auth in data["configuredAuthMethods"]["authMethodData"] if auth["authMethod"] == "chap"])
+ target.update(dict(alias=data["alias"]["iscsiAlias"], iqn=data["nodeName"]["iscsiNodeName"], chap=chap))
+
+ rc, data = self.request("storage-systems/%s/graph/xpath-filter?query=/sa/iscsiEntityData" % self.ssid)
+
+ data = data[0]
+ target.update(dict(ping=data["icmpPingResponseEnabled"], unnamed_discovery=data["unnamedDiscoverySessionsEnabled"]))
+
+ except Exception as err:
+ self.module.fail_json(msg="Failed to retrieve the iSCSI target information. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ return target
+
+ def apply_iscsi_settings(self):
+ """Update the iSCSI target alias and CHAP settings"""
+ update = False
+ target = self.target
+
+ body = dict()
+
+ if self.name is not None and self.name != target["alias"]:
+ update = True
+ body["alias"] = self.name
+
+ # If the CHAP secret was provided, we trigger an update.
+ if self.chap_secret:
+ update = True
+ body.update(dict(enableChapAuthentication=True,
+ chapSecret=self.chap_secret))
+ # If no secret was provided, then we disable chap
+ elif target["chap"]:
+ update = True
+ body.update(dict(enableChapAuthentication=False))
+
+ if update and not self.check_mode:
+ try:
+ self.request("storage-systems/%s/iscsi/target-settings" % self.ssid, method="POST", data=body)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to update the iSCSI target settings. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ return update
+
+ def apply_target_changes(self):
+ update = False
+ target = self.target
+
+ body = dict()
+
+ if self.ping != target["ping"]:
+ update = True
+ body["icmpPingResponseEnabled"] = self.ping
+
+ if self.unnamed_discovery != target["unnamed_discovery"]:
+ update = True
+ body["unnamedDiscoverySessionsEnabled"] = self.unnamed_discovery
+
+ if update and not self.check_mode:
+ try:
+ self.request("storage-systems/%s/iscsi/entity" % self.ssid, method="POST", data=body)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to update the iSCSI target settings. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+ return update
+
+ def update(self):
+ update = self.apply_iscsi_settings()
+ update = self.apply_target_changes() or update
+
+ target = self.target
+ data = dict((key, target[key]) for key in target if key in ["iqn", "alias"])
+
+ self.module.exit_json(msg="The interface settings have been updated.", changed=update, **data)
+
+
+def main():
+ iface = NetAppESeriesIscsiTarget()
+ iface.update()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_ldap.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_ldap.py
new file mode 100644
index 00000000..ab2b2ae3
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_ldap.py
@@ -0,0 +1,391 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_ldap
+short_description: NetApp E-Series manage LDAP integration to use for authentication
+description:
+ - Configure an E-Series system to allow authentication via an LDAP server
+author:
+ - Michael Price (@lmprice)
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ state:
+ description:
+ - When I(state=="present") the defined LDAP domain will be added to the storage system.
+ - When I(state=="absent") the domain specified will be removed from the storage system.
+ - I(state=="disabled") will result in deleting all existing LDAP domains on the storage system.
+ type: str
+ choices:
+ - present
+ - absent
+ - disabled
+ default: present
+ identifier:
+ description:
+ - This is a unique identifier for the configuration (for cases where there are multiple domains configured).
+ type: str
+ default: "default"
+ required: false
+ bind_user:
+ description:
+ - This is the user account that will be used for querying the LDAP server.
+ - Required when I(bind_password) is specified.
+ - "Example: CN=MyBindAcct,OU=ServiceAccounts,DC=example,DC=com"
+ type: str
+ required: false
+ bind_password:
+ description:
+ - This is the password for the bind user account.
+ - Required when I(bind_user) is specified.
+ type: str
+ required: false
+ server_url:
+ description:
+ - This is the LDAP server url.
+ - The connection string should be specified as using the ldap or ldaps protocol along with the port information.
+ type: str
+ required: false
+ names:
+ description:
+ - The domain name[s] that will be utilized when authenticating to identify which domain to utilize.
+ - Default to use the DNS name of the I(server).
+ - The only requirement is that the name[s] be resolvable.
+ - "Example: user@example.com"
+ type: list
+ required: false
+ search_base:
+ description:
+ - The search base is used to find group memberships of the user.
+ - "Example: ou=users,dc=example,dc=com"
+ type: str
+ required: false
+ role_mappings:
+ description:
+ - This is where you specify which groups should have access to what permissions for the
+ storage-system.
+ - For example, all users in group A will be assigned all 4 available roles, which will allow access
+ to all the management functionality of the system (super-user). Those in group B only have the
+ storage.monitor role, which will allow only read-only access.
+ - This is specified as a mapping of regular expressions to a list of roles. See the examples.
+ - The roles that will be assigned to to the group/groups matching the provided regex.
+ - storage.admin allows users full read/write access to storage objects and operations.
+ - storage.monitor allows users read-only access to storage objects and operations.
+ - support.admin allows users access to hardware, diagnostic information, the Major Event
+ Log, and other critical support-related functionality, but not the storage configuration.
+ - security.admin allows users access to authentication/authorization configuration, as well
+ as the audit log configuration, and certification management.
+ type: dict
+ required: false
+ group_attributes:
+ description:
+ - The user attributes that should be considered for the group to role mapping.
+ - Typically this is used with something like "memberOf", and a user"s access is tested against group
+ membership or lack thereof.
+ type: list
+ default: ["memberOf"]
+ required: false
+ user_attribute:
+ description:
+ - This is the attribute we will use to match the provided username when a user attempts to
+ authenticate.
+ type: str
+ default: "sAMAccountName"
+ required: false
+notes:
+ - Check mode is supported
+ - This module allows you to define one or more LDAP domains identified uniquely by I(identifier) to use for
+ authentication. Authorization is determined by I(role_mappings), in that different groups of users may be given
+ different (or no), access to certain aspects of the system and API.
+ - The local user accounts will still be available if the LDAP server becomes unavailable/inaccessible.
+ - Generally, you"ll need to get the details of your organization"s LDAP server before you"ll be able to configure
+ the system for using LDAP authentication; every implementation is likely to be very different.
+ - This API is currently only supported with the Embedded Web Services API v2.0 and higher, or the Web Services Proxy
+ v3.0 and higher.
+"""
+
+EXAMPLES = """
+ - name: Disable LDAP authentication
+ na_santricity_ldap:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ state: absent
+
+ - name: Remove the "default" LDAP domain configuration
+ na_santricity_ldap:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ state: absent
+ identifier: default
+
+ - name: Define a new LDAP domain, utilizing defaults where possible
+ na_santricity_ldap:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ state: enabled
+ bind_username: "CN=MyBindAccount,OU=ServiceAccounts,DC=example,DC=com"
+ bind_password: "mySecretPass"
+ server: "ldap://example.com:389"
+ search_base: "OU=Users,DC=example,DC=com"
+ role_mappings:
+ ".*dist-dev-storage.*":
+ - storage.admin
+ - security.admin
+ - support.admin
+ - storage.monitor
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The ldap settings have been updated.
+"""
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+
+
+class NetAppESeriesLdap(NetAppESeriesModule):
+ NO_CHANGE_MSG = "No changes were necessary."
+ TEMPORARY_DOMAIN = "ANSIBLE_TMP_DOMAIN"
+
+ def __init__(self):
+ ansible_options = dict(state=dict(type="str", required=False, default="present", choices=["present", "absent", "disabled"]),
+ identifier=dict(type="str", required=False, default="default"),
+ bind_user=dict(type="str", required=False),
+ bind_password=dict(type="str", required=False, no_log=True),
+ names=dict(type="list", required=False),
+ server_url=dict(type="str", required=False),
+ search_base=dict(type="str", required=False),
+ role_mappings=dict(type="dict", required=False, no_log=True),
+ group_attributes=dict(type="list", default=["memberOf"], required=False),
+ user_attribute=dict(type="str", required=False, default="sAMAccountName"))
+
+ required_if = [["state", "present", ["server_url"]]]
+ required_together = [["bind_user", "bind_password"]]
+ super(NetAppESeriesLdap, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ required_if=required_if,
+ required_together=required_together,
+ supports_check_mode=True)
+
+ args = self.module.params
+ self.state = args["state"]
+ self.id = args["identifier"]
+ self.bind_user = args["bind_user"]
+ self.bind_password = args["bind_password"]
+ self.names = args["names"]
+ self.server = args["server_url"]
+ self.search_base = args["search_base"]
+ self.role_mappings = args["role_mappings"]
+ self.group_attributes = args["group_attributes"]
+ self.user_attribute = args["user_attribute"]
+
+ if self.server and not self.names:
+ parts = urlparse.urlparse(self.server)
+ self.names = [parts.netloc.split(':')[0]]
+
+ # Check whether request needs to be forwarded on to the controller web services rest api.
+ self.url_path_prefix = ""
+ if self.is_embedded():
+ self.url_path_prefix = "storage-systems/1/"
+ elif self.ssid != "0" and self.ssid != "proxy":
+ self.url_path_prefix = "storage-systems/%s/forward/devmgr/v2/storage-systems/1/" % self.ssid
+
+ self.existing_domain_ids = []
+ self.domain = {} # Existing LDAP domain
+ self.body = {} # Request body
+
+ def get_domains(self):
+ """Retrieve all domain information from storage system."""
+ domains = None
+ try:
+ rc, response = self.request(self.url_path_prefix + "ldap")
+ domains = response["ldapDomains"]
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve current LDAP configuration. Array Id [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ return domains
+
+ def build_request_body(self):
+ """Build the request body."""
+ self.body.update({"id": self.id, "groupAttributes": self.group_attributes, "ldapUrl": self.server, "names": self.names, "roleMapCollection": []})
+
+ if self.search_base:
+ self.body.update({"searchBase": self.search_base})
+ if self.user_attribute:
+ self.body.update({"userAttribute": self.user_attribute})
+ if self.bind_user and self.bind_password:
+ self.body.update({"bindLookupUser": {"password": self.bind_password, "user": self.bind_user}})
+ if self.role_mappings:
+ for regex, names in self.role_mappings.items():
+ for name in names:
+ self.body["roleMapCollection"].append({"groupRegex": regex, "ignorecase": True, "name": name})
+
+ def are_changes_required(self):
+ """Determine whether any changes are required and build request body."""
+ change_required = False
+ domains = self.get_domains()
+
+ if self.state == "disabled" and domains:
+ self.existing_domain_ids = [domain["id"] for domain in domains]
+ change_required = True
+
+ elif self.state == "present":
+ for domain in domains:
+ if self.id == domain["id"]:
+ self.domain = domain
+
+ if self.state == "absent":
+ change_required = True
+ elif (len(self.group_attributes) != len(domain["groupAttributes"]) or
+ any([a not in domain["groupAttributes"] for a in self.group_attributes])):
+ change_required = True
+ elif self.user_attribute != domain["userAttribute"]:
+ change_required = True
+ elif self.search_base.lower() != domain["searchBase"].lower():
+ change_required = True
+ elif self.server != domain["ldapUrl"]:
+ change_required = True
+ elif any(name not in domain["names"] for name in self.names) or any(name not in self.names for name in domain["names"]):
+ change_required = True
+ elif self.role_mappings:
+ if len(self.body["roleMapCollection"]) != len(domain["roleMapCollection"]):
+ change_required = True
+ else:
+ for role_map in self.body["roleMapCollection"]:
+ for existing_role_map in domain["roleMapCollection"]:
+ if role_map["groupRegex"] == existing_role_map["groupRegex"] and role_map["name"] == existing_role_map["name"]:
+ break
+ else:
+ change_required = True
+
+ if not change_required and self.bind_user and self.bind_password:
+ if self.bind_user != domain["bindLookupUser"]["user"]:
+ change_required = True
+ elif self.bind_password:
+ temporary_domain = None
+ try:
+ # Check whether temporary domain exists
+ if any(domain["id"] == self.TEMPORARY_DOMAIN for domain in domains):
+ self.delete_domain(self.TEMPORARY_DOMAIN)
+
+ temporary_domain = self.add_domain(temporary=True, skip_test=True)
+ rc, tests = self.request(self.url_path_prefix + "ldap/test", method="POST")
+
+ temporary_domain_test = {}
+ domain_test = {}
+ for test in tests:
+ if test["id"] == temporary_domain["id"]:
+ temporary_domain_test = test["result"]
+ if self.id == test["id"]:
+ domain_test = test["result"]
+
+ if temporary_domain_test["authenticationTestResult"] == "ok" and domain_test["authenticationTestResult"] != "ok":
+ change_required = True
+ elif temporary_domain_test["authenticationTestResult"] != "ok":
+ self.module.fail_json(msg="Failed to authenticate bind credentials! Array Id [%s]." % self.ssid)
+
+ finally:
+ if temporary_domain:
+ self.delete_domain(self.TEMPORARY_DOMAIN)
+ break
+ else:
+ change_required = True
+ elif self.state == "absent":
+ for domain in domains:
+ if self.id == domain["id"]:
+ change_required = True
+
+ return change_required
+
+ def add_domain(self, temporary=False, skip_test=False):
+ """Add domain to storage system."""
+ domain = None
+ body = self.body.copy()
+ if temporary:
+ body.update({"id": self.TEMPORARY_DOMAIN, "names": [self.TEMPORARY_DOMAIN]})
+
+ try:
+ rc, response = self.request(self.url_path_prefix + "ldap/addDomain?skipTest=%s" % ("true" if not skip_test else "false"),
+ method="POST", data=body)
+ domain = response["ldapDomains"][0]
+ except Exception as error:
+ self.module.fail_json(msg="Failed to create LDAP domain. Array Id [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ return domain
+
+ def update_domain(self):
+ """Update existing domain on storage system."""
+ try:
+ rc, response = self.request(self.url_path_prefix + "ldap/%s" % self.domain["id"], method="POST", data=self.body)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to update LDAP domain. Array Id [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ def delete_domain(self, domain_id):
+ """Delete specific domain on the storage system."""
+ try:
+ url = self.url_path_prefix + "ldap/%s" % domain_id
+ rc, response = self.request(self.url_path_prefix + "ldap/%s" % domain_id, method="DELETE")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to delete LDAP domain. Array Id [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ def disable_domains(self):
+ """Delete all existing domains on storage system."""
+ for domain_id in self.existing_domain_ids:
+ self.delete_domain(domain_id)
+
+ def apply(self):
+ """Apply any necessary changes to the LDAP configuration."""
+ self.build_request_body()
+ change_required = self.are_changes_required()
+
+ if change_required and not self.module.check_mode:
+ if self.state == "present":
+ if self.domain:
+ self.update_domain()
+ self.module.exit_json(msg="LDAP domain has been updated. Array Id: [%s]" % self.ssid, changed=change_required)
+ else:
+ self.add_domain()
+ self.module.exit_json(msg="LDAP domain has been added. Array Id: [%s]" % self.ssid, changed=change_required)
+ elif self.state == "absent":
+ if self.domain:
+ self.delete_domain(self.domain["id"])
+ self.module.exit_json(msg="LDAP domain has been removed. Array Id: [%s]" % self.ssid, changed=change_required)
+ else:
+ self.disable_domains()
+ self.module.exit_json(msg="All LDAP domains have been removed. Array Id: [%s]" % self.ssid, changed=change_required)
+
+ self.module.exit_json(msg="No changes have been made to the LDAP configuration. Array Id: [%s]" % self.ssid, changed=change_required)
+
+
+def main():
+ ldap = NetAppESeriesLdap()
+ ldap.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_lun_mapping.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_lun_mapping.py
new file mode 100644
index 00000000..d3d70fb5
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_lun_mapping.py
@@ -0,0 +1,247 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: na_santricity_lun_mapping
+author:
+ - Kevin Hulquest (@hulquest)
+ - Nathan Swartz (@ndswartz)
+short_description: NetApp E-Series manage lun mappings
+description:
+ - Create, delete, or modify mappings between a volume and a targeted host/host+ group.
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ state:
+ description:
+ - Present will ensure the mapping exists, absent will remove the mapping.
+ type: str
+ required: False
+ choices: ["present", "absent"]
+ default: "present"
+ target:
+ description:
+ - The name of host or hostgroup you wish to assign to the mapping
+ - If omitted, the default hostgroup is used.
+ - If the supplied I(volume_name) is associated with a different target, it will be updated to what is supplied here.
+ type: str
+ required: False
+ volume_name:
+ description:
+ - The name of the volume you wish to include in the mapping.
+ - Use ACCESS_VOLUME to reference the in-band access management volume.
+ type: str
+ required: True
+ aliases:
+ - volume
+ lun:
+ description:
+ - The LUN value you wish to give the mapping.
+ - If the supplied I(volume_name) is associated with a different LUN, it will be updated to what is supplied here.
+ - LUN value will be determine by the storage-system when not specified.
+ type: int
+ required: false
+'''
+
+EXAMPLES = '''
+---
+ - name: Map volume1 to the host target host1
+ na_santricity_lun_mapping:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ state: present
+ target: host1
+ volume: volume1
+ - name: Delete the lun mapping between volume1 and host1
+ na_santricity_lun_mapping:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ state: absent
+ target: host1
+ volume: volume1
+'''
+RETURN = '''
+msg:
+ description: success of the module
+ returned: always
+ type: str
+ sample: Lun mapping is complete
+'''
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+
+class NetAppESeriesLunMapping(NetAppESeriesModule):
+ def __init__(self):
+ ansible_options = dict(state=dict(required=False, choices=["present", "absent"], default="present"),
+ target=dict(required=False, default=None),
+ volume_name=dict(required=True, aliases=["volume"]),
+ lun=dict(type="int", required=False))
+
+ super(NetAppESeriesLunMapping, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ supports_check_mode=True)
+
+ args = self.module.params
+ self.state = args["state"] == "present"
+ self.target = args["target"] if args["target"] else "DEFAULT_HOSTGROUP"
+ self.volume = args["volume_name"] if args["volume_name"] != "ACCESS_VOLUME" else "Access"
+ self.lun = args["lun"]
+ self.check_mode = self.module.check_mode
+ self.mapping_info = None
+
+ if not self.url.endswith('/'):
+ self.url += '/'
+
+ def update_mapping_info(self):
+ """Collect the current state of the storage array."""
+ response = None
+ try:
+ rc, response = self.request("storage-systems/%s/graph" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve storage array graph. Id [%s]. Error [%s]" % (self.ssid, to_native(error)))
+
+ # Create dictionary containing host/cluster references mapped to their names
+ target_reference = {}
+ target_name = {}
+ target_type = {}
+
+ for host in response["storagePoolBundle"]["host"]:
+ target_reference.update({host["hostRef"]: host["name"]})
+ target_name.update({host["name"]: host["hostRef"]})
+ target_type.update({host["name"]: "host"})
+
+ for cluster in response["storagePoolBundle"]["cluster"]:
+
+ # Verify there is no ambiguity between target's type (ie host and group have the same name)
+ if cluster["name"] == self.target and self.target in target_name.keys():
+ self.module.fail_json(msg="Ambiguous target type: target name is used for both host and group targets! Id [%s]" % self.ssid)
+
+ target_reference.update({cluster["clusterRef"]: cluster["name"]})
+ target_name.update({cluster["name"]: cluster["clusterRef"]})
+ target_type.update({cluster["name"]: "group"})
+
+ target_reference.update({"0000000000000000000000000000000000000000": "DEFAULT_HOSTGROUP"})
+ target_name.update({"DEFAULT_HOSTGROUP": "0000000000000000000000000000000000000000"})
+ target_type.update({"DEFAULT_HOSTGROUP": "group"})
+
+ volume_reference = {}
+ volume_name = {}
+ lun_name = {}
+ for volume in response["volume"]:
+ volume_reference.update({volume["volumeRef"]: volume["name"]})
+ volume_name.update({volume["name"]: volume["volumeRef"]})
+ if volume["listOfMappings"]:
+ lun_name.update({volume["name"]: volume["listOfMappings"][0]["lun"]})
+ for volume in response["highLevelVolBundle"]["thinVolume"]:
+ volume_reference.update({volume["volumeRef"]: volume["name"]})
+ volume_name.update({volume["name"]: volume["volumeRef"]})
+ if volume["listOfMappings"]:
+ lun_name.update({volume["name"]: volume["listOfMappings"][0]["lun"]})
+
+ volume_name.update({response["sa"]["accessVolume"]["name"]: response["sa"]["accessVolume"]["accessVolumeRef"]})
+ volume_reference.update({response["sa"]["accessVolume"]["accessVolumeRef"]: response["sa"]["accessVolume"]["name"]})
+
+ # Build current mapping object
+ self.mapping_info = dict(lun_mapping=[dict(volume_reference=mapping["volumeRef"],
+ map_reference=mapping["mapRef"],
+ lun_mapping_reference=mapping["lunMappingRef"],
+ lun=mapping["lun"]
+ ) for mapping in response["storagePoolBundle"]["lunMapping"]],
+ volume_by_reference=volume_reference,
+ volume_by_name=volume_name,
+ lun_by_name=lun_name,
+ target_by_reference=target_reference,
+ target_by_name=target_name,
+ target_type_by_name=target_type)
+
+ def get_lun_mapping(self):
+ """Find the matching lun mapping reference.
+
+ Returns: tuple(bool, int, int): contains volume match, volume mapping reference and mapping lun
+ """
+ target_match = False
+ reference = None
+ lun = None
+
+ self.update_mapping_info()
+
+ # Verify that when a lun is specified that it does not match an existing lun value unless it is associated with
+ # the specified volume (ie for an update)
+ if self.lun and any((self.lun == lun_mapping["lun"] and
+ self.target == self.mapping_info["target_by_reference"][lun_mapping["map_reference"]] and
+ self.volume != self.mapping_info["volume_by_reference"][lun_mapping["volume_reference"]]
+ ) for lun_mapping in self.mapping_info["lun_mapping"]):
+ self.module.fail_json(msg="Option lun value is already in use for target! Array Id [%s]." % self.ssid)
+
+ # Verify volume and target exist if needed for expected state.
+ if self.state:
+ if self.volume not in self.mapping_info["volume_by_name"].keys():
+ self.module.fail_json(msg="Volume does not exist. Id [%s]." % self.ssid)
+ if self.target and self.target not in self.mapping_info["target_by_name"].keys():
+ self.module.fail_json(msg="Target does not exist. Id [%s'." % self.ssid)
+
+ for lun_mapping in self.mapping_info["lun_mapping"]:
+
+ # Find matching volume reference
+ if lun_mapping["volume_reference"] == self.mapping_info["volume_by_name"][self.volume]:
+ reference = lun_mapping["lun_mapping_reference"]
+ lun = lun_mapping["lun"]
+
+ # Determine if lun mapping is attached to target with the
+ if (lun_mapping["map_reference"] in self.mapping_info["target_by_reference"].keys() and
+ self.mapping_info["target_by_reference"][lun_mapping["map_reference"]] == self.target and
+ (self.lun is None or lun == self.lun)):
+ target_match = True
+
+ return target_match, reference, lun
+
+ def update(self):
+ """Execute the changes the require changes on the storage array."""
+ target_match, lun_reference, lun = self.get_lun_mapping()
+ update = (self.state and not target_match) or (not self.state and lun_reference)
+
+ if update and not self.check_mode:
+ try:
+ if self.state:
+ body = dict()
+ target = None if not self.target else self.mapping_info["target_by_name"][self.target]
+ if target:
+ body.update(dict(targetId=target))
+ if self.lun is not None:
+ body.update(dict(lun=self.lun))
+
+ if lun_reference:
+
+ rc, response = self.request("storage-systems/%s/volume-mappings/%s/move" % (self.ssid, lun_reference), method="POST", data=body)
+ else:
+ body.update(dict(mappableObjectId=self.mapping_info["volume_by_name"][self.volume]))
+ rc, response = self.request("storage-systems/%s/volume-mappings" % self.ssid, method="POST", data=body)
+
+ else: # Remove existing lun mapping for volume and target
+ rc, response = self.request("storage-systems/%s/volume-mappings/%s" % (self.ssid, lun_reference), method="DELETE")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to update storage array lun mapping. Id [%s]. Error [%s]" % (self.ssid, to_native(error)))
+
+ self.module.exit_json(msg="Lun mapping is complete.", changed=update)
+
+
+def main():
+ mapping = NetAppESeriesLunMapping()
+ mapping.update()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_mgmt_interface.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_mgmt_interface.py
new file mode 100644
index 00000000..6fd7c7f1
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_mgmt_interface.py
@@ -0,0 +1,531 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_mgmt_interface
+short_description: NetApp E-Series manage management interface configuration
+description:
+ - Configure the E-Series management interfaces
+author:
+ - Michael Price (@lmprice)
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ state:
+ description:
+ - Enable or disable IPv4 network interface configuration.
+ - Either IPv4 or IPv6 must be enabled otherwise error will occur.
+ choices:
+ - enabled
+ - disabled
+ default: enabled
+ type: str
+ required: false
+ controller:
+ description:
+ - The controller that owns the port you want to configure.
+ - Controller names are represented alphabetically, with the first controller as A,
+ the second as B, and so on.
+ - Current hardware models have either 1 or 2 available controllers, but that is not a guaranteed hard
+ limitation and could change in the future.
+ choices:
+ - A
+ - B
+ type: str
+ required: true
+ port:
+ description:
+ - The ethernet port configuration to modify.
+ - The channel represents the port number left to right on the controller, beginning with 1.
+ type: int
+ required: true
+ address:
+ description:
+ - The IPv4 address to assign to the interface.
+ - Should be specified in xx.xx.xx.xx form.
+ - Mutually exclusive with I(config_method=dhcp)
+ type: str
+ required: false
+ subnet_mask:
+ description:
+ - The subnet mask to utilize for the interface.
+ - Should be specified in xx.xx.xx.xx form.
+ - Mutually exclusive with I(config_method=dhcp)
+ type: str
+ required: false
+ gateway:
+ description:
+ - The IPv4 gateway address to utilize for the interface.
+ - Should be specified in xx.xx.xx.xx form.
+ - Mutually exclusive with I(config_method=dhcp)
+ type: str
+ required: false
+ config_method:
+ description:
+ - The configuration method type to use for network interface ports.
+ - dhcp is mutually exclusive with I(address), I(subnet_mask), and I(gateway).
+ choices:
+ - dhcp
+ - static
+ type: str
+ required: false
+ dns_config_method:
+ description:
+ - The configuration method type to use for DNS services.
+ - dhcp is mutually exclusive with I(dns_address), and I(dns_address_backup).
+ choices:
+ - dhcp
+ - static
+ type: str
+ required: false
+ dns_address:
+ description:
+ - Primary IPv4 DNS server address
+ type: str
+ required: false
+ dns_address_backup:
+ description:
+ - Backup IPv4 DNS server address
+ - Queried when primary DNS server fails
+ type: str
+ required: false
+ ntp_config_method:
+ description:
+ - The configuration method type to use for NTP services.
+ - disable is mutually exclusive with I(ntp_address) and I(ntp_address_backup).
+ - dhcp is mutually exclusive with I(ntp_address) and I(ntp_address_backup).
+ choices:
+ - disabled
+ - dhcp
+ - static
+ type: str
+ required: false
+ ntp_address:
+ description:
+ - Primary IPv4 NTP server address
+ type: str
+ required: false
+ ntp_address_backup:
+ description:
+ - Backup IPv4 NTP server address
+ - Queried when primary NTP server fails
+ type: str
+ required: false
+ ssh:
+ description:
+ - Enable ssh access to the controller for debug purposes.
+ - This is a controller-level setting.
+ - rlogin/telnet will be enabled for ancient equipment where ssh is not available.
+ type: bool
+ required: false
+notes:
+ - Check mode is supported.
+ - It is highly recommended to have a minimum of one up management port on each controller.
+ - When using SANtricity Web Services Proxy, use M(netapp_eseries.santricity.na_santricity_storage_system) to update management paths. This is required because of a known issue
+ and will be addressed in the proxy version 4.1. After the resolution the management ports should automatically be updated.
+ - The interface settings are applied synchronously, but changes to the interface itself (receiving a new IP address
+ via dhcp, etc), can take seconds or minutes longer to take effect.
+"""
+
+EXAMPLES = """
+ - name: Configure the first port on the A controller with a static IPv4 address
+ na_santricity_mgmt_interface:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ port: "1"
+ controller: "A"
+ config_method: static
+ address: "192.168.1.100"
+ subnet_mask: "255.255.255.0"
+ gateway: "192.168.1.1"
+
+ - name: Disable ipv4 connectivity for the second port on the B controller
+ na_santricity_mgmt_interface:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ port: "2"
+ controller: "B"
+ enable_interface: no
+
+ - name: Enable ssh access for ports one and two on controller A
+ na_santricity_mgmt_interface:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ port: "1"
+ controller: "A"
+ ssh: yes
+
+ - name: Configure static DNS settings for the first port on controller A
+ na_santricity_mgmt_interface:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ port: "1"
+ controller: "A"
+ dns_config_method: static
+ dns_address: "192.168.1.100"
+ dns_address_backup: "192.168.1.1"
+
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The interface settings have been updated.
+available_embedded_api_urls:
+ description: List containing available web services embedded REST API urls
+ returned: on success
+ type: list
+ sample:
+"""
+from time import sleep
+
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+
+
+class NetAppESeriesMgmtInterface(NetAppESeriesModule):
+ MAXIMUM_VERIFICATION_TIMEOUT = 120
+
+ def __init__(self):
+ ansible_options = dict(state=dict(type="str", choices=["enabled", "disabled"], default="enabled", required=False),
+ controller=dict(type="str", required=True, choices=["A", "B"]),
+ port=dict(type="int"),
+ address=dict(type="str", required=False),
+ subnet_mask=dict(type="str", required=False),
+ gateway=dict(type="str", required=False),
+ config_method=dict(type="str", required=False, choices=["dhcp", "static"]),
+ dns_config_method=dict(type="str", required=False, choices=["dhcp", "static"]),
+ dns_address=dict(type="str", required=False),
+ dns_address_backup=dict(type="str", required=False),
+ ntp_config_method=dict(type="str", required=False, choices=["disabled", "dhcp", "static"]),
+ ntp_address=dict(type="str", required=False),
+ ntp_address_backup=dict(type="str", required=False),
+ ssh=dict(type="bool", required=False))
+
+ required_if = [["state", "enable", ["config_method"]],
+ ["config_method", "static", ["address", "subnet_mask"]],
+ ["dns_config_method", "static", ["dns_address"]],
+ ["ntp_config_method", "static", ["ntp_address"]]]
+
+ super(NetAppESeriesMgmtInterface, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ required_if=required_if,
+ supports_check_mode=True)
+
+ args = self.module.params
+ self.enable_interface = args["state"] == "enabled"
+ self.controller = args["controller"]
+ self.channel = args["port"]
+
+ self.config_method = args["config_method"]
+ self.address = args["address"]
+ self.subnet_mask = args["subnet_mask"]
+ self.gateway = args["gateway"]
+
+ self.dns_config_method = args["dns_config_method"]
+ self.dns_address = args["dns_address"]
+ self.dns_address_backup = args["dns_address_backup"]
+
+ self.ntp_config_method = args["ntp_config_method"]
+ self.ntp_address = args["ntp_address"]
+ self.ntp_address_backup = args["ntp_address_backup"]
+
+ self.ssh = args["ssh"]
+
+ self.body = {}
+ self.interface_info = {}
+ self.alt_interface_addresses = []
+ self.all_interface_addresses = []
+ self.use_alternate_address = False
+ self.alt_url_path = None
+
+ self.available_embedded_api_urls = []
+
+ def get_controllers(self):
+ """Retrieve a mapping of controller labels to their references
+ :return: controllers defined on the system. Example: {'A': '070000000000000000000001', 'B': '070000000000000000000002'}
+ """
+ try:
+ rc, controllers = self.request("storage-systems/%s/controllers" % self.ssid)
+ except Exception as err:
+ controllers = list()
+ self.module.fail_json(msg="Failed to retrieve the controller settings. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ controllers.sort(key=lambda c: c['physicalLocation']['slot'])
+ controllers_dict = dict()
+ i = ord('A')
+ for controller in controllers:
+ label = chr(i)
+ settings = dict(controllerSlot=controller['physicalLocation']['slot'],
+ controllerRef=controller['controllerRef'],
+ ssh=controller['networkSettings']['remoteAccessEnabled'])
+ controllers_dict[label] = settings
+ i += 1
+ return controllers_dict
+
+ def update_target_interface_info(self, retries=60):
+ """Discover and update cached interface info."""
+ net_interfaces = list()
+ try:
+ rc, net_interfaces = self.request("storage-systems/%s/configuration/ethernet-interfaces" % self.ssid)
+ except Exception as error:
+ if retries > 0:
+ self.update_target_interface_info(retries=retries - 1)
+ return
+ else:
+ self.module.fail_json(msg="Failed to retrieve defined management interfaces. Array Id [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ iface = None
+ channels = {}
+ controller_info = self.get_controllers()[self.controller]
+ controller_ref = controller_info["controllerRef"]
+ controller_ssh = controller_info["ssh"]
+ for net in net_interfaces:
+ if net["controllerRef"] == controller_ref:
+ channels.update({net["channel"]: net["linkStatus"]})
+ if net["ipv4Enabled"] and net["linkStatus"] == "up":
+ self.all_interface_addresses.append(net["ipv4Address"])
+ if net["controllerRef"] == controller_ref and net["channel"] == self.channel:
+ iface = net
+ elif net["ipv4Enabled"] and net["linkStatus"] == "up":
+ self.alt_interface_addresses.append(net["ipv4Address"])
+
+ if iface is None:
+ available_controllers = ["%s (%s)" % (channel, status) for channel, status in channels.items()]
+ self.module.fail_json(msg="Invalid port number! Controller %s ports: [%s]. Array [%s]"
+ % (self.controller, ",".join(available_controllers), self.ssid))
+
+ self.interface_info.update({"channel": iface["channel"],
+ "link_status": iface["linkStatus"],
+ "enabled": iface["ipv4Enabled"],
+ "address": iface["ipv4Address"],
+ "gateway": iface["ipv4GatewayAddress"],
+ "subnet_mask": iface["ipv4SubnetMask"],
+ "dns_config_method": iface["dnsProperties"]["acquisitionProperties"]["dnsAcquisitionType"],
+ "dns_servers": iface["dnsProperties"]["acquisitionProperties"]["dnsServers"],
+ "ntp_config_method": iface["ntpProperties"]["acquisitionProperties"]["ntpAcquisitionType"],
+ "ntp_servers": iface["ntpProperties"]["acquisitionProperties"]["ntpServers"],
+ "config_method": iface["ipv4AddressConfigMethod"],
+ "controllerRef": iface["controllerRef"],
+ "controllerSlot": iface["controllerSlot"],
+ "ipv6_enabled": iface["ipv6Enabled"],
+ "id": iface["interfaceRef"],
+ "ssh": controller_ssh})
+
+ def update_body_enable_interface_setting(self):
+ """Enable or disable the IPv4 network interface."""
+ change_required = False
+ if not self.enable_interface and not self.interface_info["ipv6_enabled"]:
+ self.module.fail_json(msg="Either IPv4 or IPv6 must be enabled. Array [%s]." % self.ssid)
+
+ if self.enable_interface != self.interface_info["enabled"]:
+ change_required = True
+ self.body.update({"ipv4Enabled": self.enable_interface})
+ return change_required
+
+ def update_body_interface_settings(self):
+ """Update network interface settings."""
+ change_required = False
+ if self.config_method == "dhcp":
+ if self.interface_info["config_method"] != "configDhcp":
+ if self.interface_info["address"] in self.url:
+ self.use_alternate_address = True
+ change_required = True
+ self.body.update({"ipv4AddressConfigMethod": "configDhcp"})
+ else:
+ self.body.update({"ipv4AddressConfigMethod": "configStatic", "ipv4Address": self.address, "ipv4SubnetMask": self.subnet_mask})
+ if self.interface_info["config_method"] != "configStatic":
+ change_required = True
+ if self.address and self.interface_info["address"] != self.address:
+ if self.interface_info["address"] in self.url:
+ self.use_alternate_address = True
+ change_required = True
+ if self.subnet_mask and self.interface_info["subnet_mask"] != self.subnet_mask:
+ change_required = True
+ if self.gateway and self.interface_info["gateway"] != self.gateway:
+ self.body.update({"ipv4GatewayAddress": self.gateway})
+ change_required = True
+
+ return change_required
+
+ def update_body_dns_server_settings(self):
+ """Add DNS server information to the request body."""
+ change_required = False
+ if self.dns_config_method == "dhcp":
+ if self.interface_info["dns_config_method"] != "dhcp":
+ change_required = True
+ self.body.update({"dnsAcquisitionDescriptor": {"dnsAcquisitionType": "dhcp"}})
+
+ elif self.dns_config_method == "static":
+ dns_servers = [dict(addressType="ipv4", ipv4Address=self.dns_address)]
+ if self.dns_address_backup:
+ dns_servers.append(dict(addressType="ipv4", ipv4Address=self.dns_address_backup))
+
+ if (self.interface_info["dns_config_method"] != "stat" or
+ len(self.interface_info["dns_servers"]) != len(dns_servers) or
+ (len(self.interface_info["dns_servers"]) == 2 and
+ (self.interface_info["dns_servers"][0]["ipv4Address"] != self.dns_address or
+ self.interface_info["dns_servers"][1]["ipv4Address"] != self.dns_address_backup)) or
+ (len(self.interface_info["dns_servers"]) == 1 and
+ self.interface_info["dns_servers"][0]["ipv4Address"] != self.dns_address)):
+ change_required = True
+ self.body.update({"dnsAcquisitionDescriptor": {"dnsAcquisitionType": "stat", "dnsServers": dns_servers}})
+ return change_required
+
+ def update_body_ntp_server_settings(self):
+ """Add NTP server information to the request body."""
+ change_required = False
+ if self.ntp_config_method == "disabled":
+ if self.interface_info["ntp_config_method"] != "disabled":
+ change_required = True
+ self.body.update({"ntpAcquisitionDescriptor": {"ntpAcquisitionType": "disabled"}})
+
+ elif self.ntp_config_method == "dhcp":
+ if self.interface_info["ntp_config_method"] != "dhcp":
+ change_required = True
+ self.body.update({"ntpAcquisitionDescriptor": {"ntpAcquisitionType": "dhcp"}})
+
+ elif self.ntp_config_method == "static":
+ ntp_servers = [{"addrType": "ipvx", "ipvxAddress": {"addressType": "ipv4", "ipv4Address": self.ntp_address}}]
+ if self.ntp_address_backup:
+ ntp_servers.append({"addrType": "ipvx", "ipvxAddress": {"addressType": "ipv4", "ipv4Address": self.ntp_address_backup}})
+
+ if (self.interface_info["ntp_config_method"] != "stat" or
+ len(self.interface_info["ntp_servers"]) != len(ntp_servers) or
+ ((len(self.interface_info["ntp_servers"]) == 2 and
+ (self.interface_info["ntp_servers"][0]["ipvxAddress"]["ipv4Address"] != self.ntp_address or
+ self.interface_info["ntp_servers"][1]["ipvxAddress"]["ipv4Address"] != self.ntp_address_backup)) or
+ (len(self.interface_info["ntp_servers"]) == 1 and
+ ((self.interface_info["ntp_servers"][0]["addrType"] == "ipvx" and
+ self.interface_info["ntp_servers"][0]["ipvxAddress"]["ipv4Address"] != self.ntp_address) or
+ (self.interface_info["ntp_servers"][0]["addrType"] == "domainName" and
+ self.interface_info["ntp_servers"][0]["domainName"] != self.ntp_address))))):
+ change_required = True
+ self.body.update({"ntpAcquisitionDescriptor": {"ntpAcquisitionType": "stat", "ntpServers": ntp_servers}})
+ return change_required
+
+ def update_body_ssh_setting(self):
+ """Configure network interface ports for remote ssh access."""
+ change_required = False
+ if self.interface_info["ssh"] != self.ssh:
+ change_required = True
+ self.body.update({"enableRemoteAccess": self.ssh})
+ return change_required
+
+ def update_request_body(self):
+ """Verify all required changes have been made."""
+ self.update_target_interface_info()
+ self.body = {"controllerRef": self.get_controllers()[self.controller]["controllerRef"], "interfaceRef": self.interface_info["id"]}
+
+ change_required = False
+ if self.enable_interface is not None:
+ change_required = self.update_body_enable_interface_setting()
+ if self.config_method is not None:
+ change_required = self.update_body_interface_settings() or change_required
+ if self.dns_config_method is not None:
+ change_required = self.update_body_dns_server_settings() or change_required
+ if self.ntp_config_method is not None:
+ change_required = self.update_body_ntp_server_settings() or change_required
+ if self.ssh is not None:
+ change_required = self.update_body_ssh_setting() or change_required
+
+ self.module.log("update_request_body change_required: %s" % change_required)
+ return change_required
+
+ def update_url(self, retries=60):
+ """Update eseries base class url if on is available."""
+ for address in self.alt_interface_addresses:
+ if address not in self.url and address != "0.0.0.0":
+ parsed_url = urlparse.urlparse(self.url)
+ location = parsed_url.netloc.split(":")
+ location[0] = address
+ self.url = "%s://%s/" % (parsed_url.scheme, ":".join(location))
+ self.available_embedded_api_urls = ["%s://%s/%s" % (parsed_url.scheme, ":".join(location), self.DEFAULT_REST_API_PATH)]
+ self.module.warn("Using alternate address [%s]" % self.available_embedded_api_urls[0])
+ break
+ else:
+ if retries > 0:
+ sleep(1)
+ self.update_target_interface_info()
+ self.update_url(retries=retries - 1)
+ else:
+ self.module.warn("Unable to obtain an alternate url!")
+
+ def update(self):
+ """Update controller with new interface, dns service, ntp service and/or remote ssh access information."""
+ change_required = self.update_request_body()
+
+ # Build list of available web services rest api urls
+ self.available_embedded_api_urls = []
+ parsed_url = urlparse.urlparse(self.url)
+ location = parsed_url.netloc.split(":")
+ for address in self.all_interface_addresses:
+ location[0] = address
+ self.available_embedded_api_urls = ["%s://%s/%s" % (parsed_url.scheme, ":".join(location), self.DEFAULT_REST_API_PATH)]
+
+ if change_required and not self.module.check_mode:
+
+ # Update url if currently used interface will be modified
+ if self.is_embedded():
+ if self.use_alternate_address:
+ self.update_url()
+ if self.address:
+ parsed_url = urlparse.urlparse(self.url)
+ location = parsed_url.netloc.split(":")
+ location[0] = self.address
+ self.available_embedded_api_urls.append("%s://%s/%s" % (parsed_url.scheme, ":".join(location), self.DEFAULT_REST_API_PATH))
+ else:
+ self.available_embedded_api_urls = ["%s/%s" % (self.url, self.DEFAULT_REST_API_PATH)]
+
+ # Update management interface
+ try:
+ rc, response = self.request("storage-systems/%s/configuration/ethernet-interfaces" % self.ssid, method="POST", data=self.body)
+ except Exception as error:
+ pass
+
+ # Validate all changes have been made
+ for retries in range(self.MAXIMUM_VERIFICATION_TIMEOUT):
+ if not self.update_request_body():
+ break
+ sleep(1)
+ else:
+ self.module.warn("Changes failed to complete! Timeout waiting for management interface to update. Array [%s]." % self.ssid)
+ self.module.exit_json(msg="The interface settings have been updated.", changed=change_required,
+ available_embedded_api_urls=self.available_embedded_api_urls)
+ self.module.exit_json(msg="No changes are required.", changed=change_required,
+ available_embedded_api_urls=self.available_embedded_api_urls if self.is_embedded() else [])
+
+
+def main():
+ interface = NetAppESeriesMgmtInterface()
+ interface.update()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_nvme_interface.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_nvme_interface.py
new file mode 100644
index 00000000..3edf725a
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_nvme_interface.py
@@ -0,0 +1,305 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_nvme_interface
+short_description: NetApp E-Series manage NVMe interface configuration
+description: Configure settings of an E-Series NVMe interface
+author: Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ address:
+ description:
+ - The IPv4 address to assign to the NVMe interface
+ type: str
+ required: false
+ subnet_mask:
+ description:
+ - The subnet mask to utilize for the interface.
+ - Only applicable when configuring RoCE
+ - Mutually exclusive with I(config_method=dhcp)
+ type: str
+ required: false
+ gateway:
+ description:
+ - The IPv4 gateway address to utilize for the interface.
+ - Only applicable when configuring RoCE
+ - Mutually exclusive with I(config_method=dhcp)
+ type: str
+ required: false
+ config_method:
+ description:
+ - The configuration method type to use for this interface.
+ - Only applicable when configuring RoCE
+ - dhcp is mutually exclusive with I(address), I(subnet_mask), and I(gateway).
+ type: str
+ choices:
+ - dhcp
+ - static
+ required: false
+ default: dhcp
+ mtu:
+ description:
+ - The maximum transmission units (MTU), in bytes.
+ - Only applicable when configuring RoCE
+ - This allows you to configure a larger value for the MTU, in order to enable jumbo frames
+ (any value > 1500).
+ - Generally, it is necessary to have your host, switches, and other components not only support jumbo
+ frames, but also have it configured properly. Therefore, unless you know what you're doing, it's best to
+ leave this at the default.
+ type: int
+ default: 1500
+ required: false
+ aliases:
+ - max_frame_size
+ speed:
+ description:
+ - This is the ethernet port speed measured in Gb/s.
+ - Value must be a supported speed or auto for automatically negotiating the speed with the port.
+ - Only applicable when configuring RoCE
+ - The configured ethernet port speed should match the speed capability of the SFP on the selected port.
+ type: str
+ required: false
+ default: auto
+ state:
+ description:
+ - Whether or not the specified RoCE interface should be enabled.
+ - Only applicable when configuring RoCE
+ choices:
+ - enabled
+ - disabled
+ type: str
+ required: false
+ default: enabled
+ channel:
+ description:
+ - This option specifies the which NVMe controller channel to configure.
+ - The list of choices is not necessarily comprehensive. It depends on the number of ports
+ that are available in the system.
+ - The numerical value represents the number of the channel (typically from left to right on the HIC),
+ beginning with a value of 1.
+ type: int
+ required: false
+ controller:
+ description:
+ - The controller that owns the port you want to configure.
+ - Controller names are presented alphabetically, with the first controller as A and the second as B.
+ type: str
+ required: false
+ choices: [A, B]
+"""
+EXAMPLES = """
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The interface settings have been updated.
+"""
+import re
+
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+
+class NetAppESeriesNvmeInterface(NetAppESeriesModule):
+ def __init__(self):
+ ansible_options = dict(address=dict(type="str", required=False),
+ subnet_mask=dict(type="str", required=False),
+ gateway=dict(type="str", required=False),
+ config_method=dict(type="str", required=False, default="dhcp", choices=["dhcp", "static"]),
+ mtu=dict(type="int", default=1500, required=False, aliases=["max_frame_size"]),
+ speed=dict(type="str", default="auto", required=False),
+ state=dict(type="str", default="enabled", required=False, choices=["enabled", "disabled"]),
+ channel=dict(type="int", required=True),
+ controller=dict(type="str", required=True, choices=["A", "B"]))
+
+ required_if = [["config_method", "static", ["address", "subnet_mask"]]]
+ super(NetAppESeriesNvmeInterface, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ required_if=required_if,
+ supports_check_mode=True)
+
+ args = self.module.params
+ self.address = args["address"]
+ self.subnet_mask = args["subnet_mask"]
+ self.gateway = args["gateway"]
+ self.config_method = "configDhcp" if args["config_method"] == "dhcp" else "configStatic"
+ self.mtu = args["mtu"]
+ self.speed = args["speed"]
+ self.enabled = args["state"] == "enabled"
+ self.channel = args["channel"]
+ self.controller = args["controller"]
+
+ address_regex = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
+ if self.address and not address_regex.match(self.address):
+ self.module.fail_json(msg="An invalid ip address was provided for address.")
+ if self.subnet_mask and not address_regex.match(self.subnet_mask):
+ self.module.fail_json(msg="An invalid ip address was provided for subnet_mask.")
+ if self.gateway and not address_regex.match(self.gateway):
+ self.module.fail_json(msg="An invalid ip address was provided for gateway.")
+
+ self.get_target_interface_cache = None
+
+ def get_nvmeof_interfaces(self):
+ """Retrieve all interfaces that are using nvmeof"""
+ ifaces = list()
+ try:
+ rc, ifaces = self.request("storage-systems/%s/interfaces?channelType=hostside" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve defined host interfaces. Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(error)))
+
+ # Filter out all not nvme-nvmeof hostside interfaces.
+ nvmeof_ifaces = []
+ for iface in ifaces:
+ interface_type = iface["ioInterfaceTypeData"]["interfaceType"]
+ properties = iface["commandProtocolPropertiesList"]["commandProtocolProperties"]
+
+ try:
+ link_status = iface["ioInterfaceTypeData"]["ib"]["linkState"]
+ except Exception as error:
+ link_status = iface["ioInterfaceTypeData"]["ethernet"]["interfaceData"]["ethernetData"]["linkStatus"]
+
+ if (properties and properties[0]["commandProtocol"] == "nvme" and
+ properties[0]["nvmeProperties"]["commandSet"] == "nvmeof"):
+ nvmeof_ifaces.append({"properties": properties[0]["nvmeProperties"]["nvmeofProperties"],
+ "reference": iface["interfaceRef"],
+ "channel": iface["ioInterfaceTypeData"][iface["ioInterfaceTypeData"]["interfaceType"]]["channel"],
+ "interface_type": interface_type,
+ "interface": iface["ioInterfaceTypeData"][interface_type],
+ "controller_id": iface["controllerRef"],
+ "link_status": link_status})
+ return nvmeof_ifaces
+
+ def get_controllers(self):
+ """Retrieve a mapping of controller labels to their references"""
+ controllers = list()
+ try:
+ rc, controllers = self.request("storage-systems/%s/graph/xpath-filter?query=/controller/id" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve controller list! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(error)))
+
+ controllers.sort()
+ controllers_dict = {}
+ i = ord("A")
+ for controller in controllers:
+ label = chr(i)
+ controllers_dict[label] = controller
+ i += 1
+
+ return controllers_dict
+
+ def get_target_interface(self):
+ """Retrieve the targeted controller interface"""
+ if self.get_target_interface_cache is None:
+ ifaces = self.get_nvmeof_interfaces()
+ controller_id = self.get_controllers()[self.controller]
+
+ controller_ifaces = []
+ for iface in ifaces:
+ if iface["controller_id"] == controller_id:
+ controller_ifaces.append(iface)
+
+ sorted_controller_ifaces = sorted(controller_ifaces, key=lambda x: x["channel"])
+ if self.channel < 1 or self.channel > len(controller_ifaces):
+ status_msg = ", ".join(["%s (link %s)" % (index + 1, iface["link_status"])
+ for index, iface in enumerate(sorted_controller_ifaces)])
+ self.module.fail_json(msg="Invalid controller %s NVMe channel. Available channels: %s, Array Id [%s]."
+ % (self.controller, status_msg, self.ssid))
+
+ self.get_target_interface_cache = sorted_controller_ifaces[self.channel - 1]
+
+ return self.get_target_interface_cache
+
+ def update(self):
+ """Update the storage system's controller nvme interface if needed."""
+ update_required = False
+ body = {}
+
+ iface = self.get_target_interface()
+ if iface["properties"]["provider"] == "providerInfiniband":
+ if (iface["properties"]["ibProperties"]["ipAddressData"]["addressType"] != "ipv4" or
+ iface["properties"]["ibProperties"]["ipAddressData"]["ipv4Data"]["ipv4Address"] != self.address):
+ update_required = True
+ body = {"settings": {"ibSettings": {"networkSettings": {"ipv4Address": self.address}}}}
+
+ elif iface["properties"]["provider"] == "providerRocev2":
+ interface_data = iface["interface"]["interfaceData"]["ethernetData"]
+ current_speed = interface_data["currentInterfaceSpeed"].lower().replace("speed", "").replace("gig", "")
+ interface_supported_speeds = [str(speed).lower().replace("speed", "").replace("gig", "")
+ for speed in interface_data["supportedInterfaceSpeeds"]]
+ if self.speed not in interface_supported_speeds:
+ self.module.fail_json(msg="Unsupported interface speed! Options %s. Array [%s]."
+ % (interface_supported_speeds, self.ssid))
+
+ roce_properties = iface["properties"]["roceV2Properties"]
+ if self.enabled != roce_properties["ipv4Enabled"]:
+ update_required = True
+ if self.address and roce_properties["ipv4Data"]["ipv4AddressConfigMethod"] != self.config_method:
+ update_required = True
+ if self.address and roce_properties["ipv4Data"]["ipv4AddressData"]["ipv4Address"] != self.address:
+ update_required = True
+ if self.subnet_mask and roce_properties["ipv4Data"]["ipv4AddressData"]["ipv4SubnetMask"] != self.subnet_mask:
+ update_required = True
+ if self.gateway and roce_properties["ipv4Data"]["ipv4AddressData"]["ipv4GatewayAddress"] != self.gateway:
+ update_required = True
+ if self.speed and self.speed != current_speed:
+ update_required = True
+ if (self.mtu and iface["interface"]["interfaceData"]["ethernetData"][
+ "maximumFramePayloadSize"] != self.mtu):
+ update_required = True
+
+ if update_required:
+ body = {"id": iface["reference"], "settings": {"roceV2Settings": {
+ "networkSettings": {"ipv4Enabled": self.enabled,
+ "ipv4Settings": {"configurationMethod": self.config_method}}}}}
+
+ if self.config_method == "configStatic":
+ if self.address:
+ body["settings"]["roceV2Settings"]["networkSettings"]["ipv4Settings"].update(
+ {"address": self.address})
+ if self.subnet_mask:
+ body["settings"]["roceV2Settings"]["networkSettings"]["ipv4Settings"].update(
+ {"subnetMask": self.subnet_mask})
+ if self.gateway:
+ body["settings"]["roceV2Settings"]["networkSettings"]["ipv4Settings"].update(
+ {"gatewayAddress": self.gateway})
+ if self.speed:
+ if self.speed == "auto":
+ body["settings"]["roceV2Settings"]["networkSettings"].update({"interfaceSpeed": "speedAuto"})
+ else:
+ body["settings"]["roceV2Settings"]["networkSettings"].update(
+ {"interfaceSpeed": "speed%sgig" % self.speed})
+ if self.mtu:
+ body["settings"]["roceV2Settings"]["networkSettings"].update({"interfaceMtu": self.mtu})
+
+ if update_required and not self.module.check_mode:
+ try:
+ rc, iface = self.request("storage-systems/%s/nvmeof/interfaces/%s" % (self.ssid, iface["reference"]),
+ method="POST", data=body)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to configure interface. Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(error)))
+
+ self.module.exit_json(msg="NVMeoF interface settings have been updated.", changed=update_required)
+ self.module.exit_json(msg="No changes have been made.", changed=update_required)
+
+
+def main():
+ nvmeof_interface = NetAppESeriesNvmeInterface()
+ nvmeof_interface.update()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_proxy_drive_firmware_upload.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_proxy_drive_firmware_upload.py
new file mode 100644
index 00000000..715467e1
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_proxy_drive_firmware_upload.py
@@ -0,0 +1,150 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_proxy_drive_firmware_upload
+short_description: NetApp E-Series manage proxy drive firmware files
+description:
+ - Ensure drive firmware files are available on SANtricity Web Service Proxy.
+author:
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_proxy_doc
+options:
+ firmware:
+ description:
+ - This option can be a list of file paths and/or directories containing drive firmware.
+ - Note that only files with the extension .dlp will be attempted to be added to the proxy; all other files will be ignored.
+ - NetApp E-Series drives require special firmware which can be downloaded from https://mysupport.netapp.com/NOW/download/tools/diskfw_eseries/
+ type: list
+ required: false
+"""
+EXAMPLES = """
+- name: Ensure correct firmware versions
+ na_santricity_proxy_drive_firmware_upload:
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ firmware:
+ - "path/to/drive_firmware_file1.dlp"
+ - "path/to/drive_firmware_file2.dlp"
+ - "path/to/drive_firmware_directory"
+"""
+RETURN = """
+msg:
+ description: Whether any changes have been made to the collection of drive firmware on SANtricity Web Services Proxy.
+ type: str
+ returned: always
+"""
+import os
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule, create_multipart_formdata, request
+
+
+class NetAppESeriesProxyDriveFirmwareUpload(NetAppESeriesModule):
+ WAIT_TIMEOUT_SEC = 60 * 15
+
+ def __init__(self):
+ ansible_options = dict(firmware=dict(type="list", required=False))
+
+ super(NetAppESeriesProxyDriveFirmwareUpload, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ supports_check_mode=True,
+ proxy_specific_task=True)
+ args = self.module.params
+ self.firmware = args["firmware"]
+
+ self.files = None
+ self.add_files = []
+ self.remove_files = []
+ self.upload_failures = []
+
+ def determine_file_paths(self):
+ """Determine all the drive firmware file paths."""
+ self.files = {}
+ if self.firmware:
+ for path in self.firmware:
+
+ if not os.path.exists(path):
+ self.module.fail_json(msg="Drive firmware file does not exist! File [%s]" % path)
+ elif os.path.isdir(path):
+ if not path.endswith("/"):
+ path = path + "/"
+ for dir_filename in os.listdir(path):
+ if ".dlp" in dir_filename:
+ self.files.update({dir_filename: path + dir_filename})
+ elif ".dlp" in path:
+ name = os.path.basename(path)
+ self.files.update({name: path})
+
+ self.module.warn("%s" % self.files)
+
+ def determine_changes(self):
+ """Determine whether drive firmware files should be uploaded to the proxy."""
+ try:
+ rc, results = self.request("files/drive")
+ current_files = [result["fileName"] for result in results]
+
+ for current_file in current_files:
+ if current_file not in self.files.keys():
+ self.remove_files.append(current_file)
+
+ for expected_file in self.files.keys():
+ if expected_file not in current_files:
+ self.add_files.append(expected_file)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve proxy drive firmware file list. Error [%s]" % error)
+
+ def upload_files(self):
+ """Add drive firmware file to the proxy."""
+ for filename in self.add_files:
+ firmware_name = os.path.basename(filename)
+ files = [("file", firmware_name, self.files[filename])]
+ headers, data = create_multipart_formdata(files)
+ try:
+ rc, response = self.request("/files/drive", method="POST", headers=headers, data=data)
+ except Exception as error:
+ self.upload_failures.append(filename)
+ self.module.warn("Failed to upload drive firmware file. File [%s]." % firmware_name)
+
+ def delete_files(self):
+ """Remove drive firmware file to the proxy."""
+ for filename in self.remove_files:
+ try:
+ rc, response = self.request("files/drive/%s" % filename, method="DELETE")
+ except Exception as error:
+ self.upload_failures.append(filename)
+ self.module.warn("Failed to delete drive firmware file. File [%s]" % filename)
+
+ def apply(self):
+ """Apply state to the web services proxy."""
+ change_required = False
+ if not self.is_proxy():
+ self.module.fail_json(msg="Module can only be executed against SANtricity Web Services Proxy.")
+
+ self.determine_file_paths()
+ self.determine_changes()
+
+ if self.add_files or self.remove_files:
+ change_required = True
+
+ if change_required and not self.module.check_mode:
+ self.upload_files()
+ self.delete_files()
+
+ self.module.exit_json(changed=change_required, files_added=self.add_files, files_removed=self.remove_files)
+
+
+def main():
+ proxy_firmware_upload = NetAppESeriesProxyDriveFirmwareUpload()
+ proxy_firmware_upload.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_proxy_firmware_upload.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_proxy_firmware_upload.py
new file mode 100644
index 00000000..68183fd5
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_proxy_firmware_upload.py
@@ -0,0 +1,149 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_proxy_firmware_upload
+short_description: NetApp E-Series manage proxy firmware uploads.
+description:
+ - Ensure specific firmware versions are available on SANtricity Web Services Proxy.
+author:
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_proxy_doc
+options:
+ firmware:
+ description:
+ - List of paths and/or directories containing firmware/NVSRAM files.
+ - All firmware/NVSRAM files that are not specified will be removed from the proxy if they exist.
+ type: list
+ required: false
+"""
+EXAMPLES = """
+- name: Ensure proxy has the expected firmware versions.
+ na_santricity_proxy_firmware_upload:
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ firmware:
+ - "path/to/firmware/dlp_files"
+ - "path/to/nvsram.dlp"
+ - "path/to/firmware.dlp"
+"""
+RETURN = """
+msg:
+ description: Status and version of firmware and NVSRAM.
+ type: str
+ returned: always
+ sample:
+"""
+import os
+
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule, create_multipart_formdata, request
+
+
+class NetAppESeriesProxyFirmwareUpload(NetAppESeriesModule):
+ def __init__(self):
+ ansible_options = dict(firmware=dict(type="list", required=False))
+ super(NetAppESeriesProxyFirmwareUpload, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ supports_check_mode=True,
+ proxy_specific_task=True)
+
+ args = self.module.params
+ self.firmware = args["firmware"]
+ self.files = None
+ self.add_files = []
+ self.remove_files = []
+ self.upload_failures = []
+
+ def determine_file_paths(self):
+ """Determine all the drive firmware file paths."""
+ self.files = {}
+ if self.firmware:
+ for firmware_path in self.firmware:
+
+ if not os.path.exists(firmware_path):
+ self.module.fail_json(msg="Drive firmware file does not exist! File [%s]" % firmware_path)
+ elif os.path.isdir(firmware_path):
+ if not firmware_path.endswith("/"):
+ firmware_path = firmware_path + "/"
+
+ for dir_filename in os.listdir(firmware_path):
+ if ".dlp" in dir_filename:
+ self.files.update({dir_filename: firmware_path + dir_filename})
+ elif ".dlp" in firmware_path:
+ name = os.path.basename(firmware_path)
+ self.files.update({name: firmware_path})
+
+ def determine_changes(self):
+ """Determine whether files need to be added or removed."""
+ try:
+ rc, results = self.request("firmware/cfw-files")
+ current_files = [result["filename"] for result in results]
+
+ for current_file in current_files:
+ if current_file not in self.files.keys():
+ self.remove_files.append(current_file)
+
+ for expected_file in self.files.keys():
+ if expected_file not in current_files:
+ self.add_files.append(expected_file)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve current firmware file listing.")
+
+ def upload_files(self):
+ """Upload firmware and nvsram file."""
+ for filename in self.add_files:
+ fields = [("validate", "true")]
+ files = [("firmwareFile", filename, self.files[filename])]
+ headers, data = create_multipart_formdata(files=files, fields=fields)
+ try:
+ rc, response = self.request("firmware/upload/", method="POST", data=data, headers=headers)
+ except Exception as error:
+ self.upload_failures.append(filename)
+ self.module.warn(msg="Failed to upload firmware file. File [%s]" % filename)
+
+ def delete_files(self):
+ """Remove firmware and nvsram file."""
+ for filename in self.remove_files:
+ try:
+ rc, response = self.request("firmware/upload/%s" % filename, method="DELETE")
+ except Exception as error:
+ self.upload_failures.append(filename)
+ self.module.warn(msg="Failed to delete firmware file. File [%s]" % filename)
+
+ def apply(self):
+ """Upgrade controller firmware."""
+ change_required = False
+ if not self.is_proxy():
+ self.module.fail_json(msg="Module can only be executed against SANtricity Web Services Proxy.")
+
+ self.determine_file_paths()
+ self.determine_changes()
+ if self.add_files or self.remove_files:
+ change_required = True
+
+ if change_required and not self.module.check_mode:
+ self.upload_files()
+ self.delete_files()
+
+ if self.upload_failures:
+ self.module.fail_json(msg="Some file failed to be uploaded! changed=%s, Files_added [%s]. Files_removed [%s]. Upload_failures [%s]"
+ % (change_required, self.add_files, self.remove_files, self.upload_failures))
+ self.module.exit_json(changed=change_required, files_added=self.add_files, files_removed=self.remove_files)
+
+
+def main():
+ proxy_firmware_upload = NetAppESeriesProxyFirmwareUpload()
+ proxy_firmware_upload.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_proxy_systems.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_proxy_systems.py
new file mode 100644
index 00000000..68101a3f
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_proxy_systems.py
@@ -0,0 +1,579 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_proxy_systems
+short_description: NetApp E-Series manage SANtricity web services proxy storage arrays
+description:
+ - Manage the arrays accessible via a NetApp Web Services Proxy for NetApp E-series storage arrays.
+author:
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_proxy_doc
+options:
+ add_discovered_systems:
+ description:
+ - This flag will force all discovered storage systems to be added to SANtricity Web Services Proxy.
+ type: bool
+ required: false
+ default: false
+ systems:
+ description:
+ - List of storage system information which defines which systems should be added on SANtricity Web Services Proxy.
+ - Accepts a simple serial number list or list of dictionary containing at minimum the serial or addresses key from the sub-option list.
+ - Note that the serial number will be used as the storage system identifier when an identifier is not specified.
+ - When I(add_discovered_systems == False) and any system serial number not supplied that is discovered will be removed from the proxy.
+ type: list
+ required: False
+ default: []
+ suboptions:
+ ssid:
+ description:
+ - This is the Web Services Proxy's identifier for a storage system.
+ - When ssid is not specified then either the serial or first controller IPv4 address will be used instead.
+ type: str
+ required: false
+ serial:
+ description:
+ - Storage system's serial number which can be located on the top of every NetApp E-Series enclosure.
+ - Include any leading zeros.
+ - Mutually exclusive with the sub-option address.
+ type: str
+ required: false
+ addresses:
+ description:
+ - List of storage system's IPv4 addresses.
+ - Mutually exclusive with the sub-option serial.
+ type: list
+ required: false
+ password:
+ description:
+ - This is the storage system admin password.
+ - When not provided I(default_password) will be used.
+ - The storage system admin password will be set on the device itself with the provided admin password if it is not set.
+ type: str
+ required: false
+ tags:
+ description:
+ - Optional meta tags to associate to the storage system
+ type: dict
+ required: false
+ subnet_mask:
+ description:
+ - This is the IPv4 search range for discovering E-Series storage arrays.
+ - IPv4 subnet mask specified in CIDR form. Example 192.168.1.0/24 would search the range 192.168.1.0 to 192.168.1.255.
+ - Be sure to include all management paths in the search range.
+ type: str
+ required: false
+ password:
+ description:
+ - Default storage system password which will be used anytime when password has not been provided in the I(systems) sub-options.
+ - The storage system admin password will be set on the device itself with the provided admin password if it is not set.
+ type: str
+ required: false
+ tags:
+ description:
+ - Default meta tags to associate with all storage systems if not otherwise specified in I(systems) sub-options.
+ type: dict
+ required: false
+ accept_certificate:
+ description:
+ - Accept the storage system's certificate automatically even when it is self-signed.
+ - Use M(netapp_eseries.santricity.na_santricity_certificates) to add certificates to SANtricity Web Services Proxy.
+ - SANtricity Web Services Proxy will fail to add any untrusted storage system.
+ type: bool
+ required: false
+ default: true
+"""
+
+EXAMPLES = """
+---
+ - name: Add storage systems to SANtricity Web Services Proxy
+ na_santricity_proxy_systems:
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ subnet_mask: 192.168.1.0/24
+ password: password
+ tags:
+ tag: value
+ accept_certificate: True
+ systems:
+ - ssid: "system1"
+ serial: "056233035640"
+ password: "asecretpassword"
+ tags:
+ use: corporate
+ location: sunnyvale
+ - ssid: "system2"
+ addresses:
+ - 192.168.1.100
+ - 192.168.2.100 # Second is not be required. It will be discovered
+ password: "anothersecretpassword"
+ - serial: "021324673799"
+ - "021637323454"
+ - name: Add storage system to SANtricity Web Services Proxy with serial number list only. The serial numbers will be used to identify each system.
+ na_santricity_proxy_systems:
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ subnet_mask: 192.168.1.0/24
+ password: password
+ accept_certificate: True
+ systems:
+ - "1144FG123018"
+ - "721716500123"
+ - "123540006043"
+ - "112123001239"
+ - name: Add all discovered storage system to SANtricity Web Services Proxy found in the IP address range 192.168.1.0 to 192.168.1.255.
+ na_santricity_proxy_systems:
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ add_discovered_systems: True
+ subnet_mask: 192.168.1.0/24
+ password: password
+ accept_certificate: True
+"""
+RETURN = """
+msg:
+ description: Description of actions performed.
+ type: str
+ returned: always
+ sample: "Storage systems [system1, system2, 1144FG123018, 721716500123, 123540006043, 112123001239] were added."
+"""
+import json
+import threading
+
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+from time import sleep
+
+try:
+ import ipaddress
+except ImportError:
+ HAS_IPADDRESS = False
+else:
+ HAS_IPADDRESS = True
+
+
+class NetAppESeriesProxySystems(NetAppESeriesModule):
+ DEFAULT_CONNECTION_TIMEOUT_SEC = 30
+ DEFAULT_GRAPH_DISCOVERY_TIMEOUT = 30
+ DEFAULT_PASSWORD_STATE_TIMEOUT = 30
+ DEFAULT_DISCOVERY_TIMEOUT_SEC = 300
+
+ def __init__(self):
+ ansible_options = dict(add_discovered_systems=dict(type="bool", required=False, default=False),
+ subnet_mask=dict(type="str", required=False),
+ password=dict(type="str", required=False, default="", no_log=True),
+ tags=dict(type="dict", required=False),
+ accept_certificate=dict(type="bool", required=False, default=True),
+ systems=dict(type="list", required=False, default=[], suboptions=dict(ssid=dict(type="str", required=False),
+ serial=dict(type="str", required=False),
+ addresses=dict(type="list", required=False),
+ password=dict(type="str", required=False, no_log=True),
+ tags=dict(type="dict", required=False))))
+
+ super(NetAppESeriesProxySystems, self).__init__(ansible_options=ansible_options,
+ web_services_version="04.10.0000.0000",
+ supports_check_mode=True,
+ proxy_specific_task=True)
+ args = self.module.params
+ self.add_discovered_systems = args["add_discovered_systems"]
+ self.subnet_mask = args["subnet_mask"]
+ self.accept_certificate = args["accept_certificate"]
+ self.default_password = args["password"]
+
+ self.default_meta_tags = []
+ if "tags" in args and args["tags"]:
+ for key in args["tags"].keys():
+ if isinstance(args["tags"][key], list):
+ self.default_meta_tags.append({"key": key, "valueList": args["tags"][key]})
+ else:
+ self.default_meta_tags.append({"key": key, "valueList": [args["tags"][key]]})
+ self.default_meta_tags = sorted(self.default_meta_tags, key=lambda x: x["key"])
+
+ self.undiscovered_systems = []
+ self.systems_to_remove = []
+ self.systems_to_update = []
+ self.systems_to_add = []
+
+ self.serial_numbers = []
+ self.systems = []
+ if args["systems"]:
+ for system in args["systems"]:
+
+ if isinstance(system, str): # system is a serial number
+ self.serial_numbers.append(system)
+ self.systems.append({"ssid": system,
+ "serial": system,
+ "password": self.default_password,
+ "password_valid": None,
+ "password_set": None,
+ "stored_password_valid": None,
+ "meta_tags": self.default_meta_tags,
+ "controller_addresses": [],
+ "embedded_available": None,
+ "accept_certificate": False,
+ "current_info": {},
+ "changes": {},
+ "updated_required": False,
+ "failed": False,
+ "discovered": False})
+ elif isinstance(system, dict): # system is a dictionary of system details
+ if "ssid" not in system:
+ if "serial" in system and system["serial"]:
+ system.update({"ssid": system["serial"]})
+ elif "addresses" in system and system["addresses"]:
+ system.update({"ssid": system["addresses"][0]})
+ if "password" not in system:
+ system.update({"password": self.default_password})
+
+ if "serial" in system and system["serial"]:
+ self.serial_numbers.append(system["serial"])
+
+ # Structure meta tags for Web Services
+ meta_tags = self.default_meta_tags
+ if "meta_tags" in system and system["meta_tags"]:
+ for key in system["meta_tags"].keys():
+ if isinstance(system["meta_tags"][key], list):
+ meta_tags.append({"key": key, "valueList": system["meta_tags"][key]})
+ else:
+ meta_tags.append({"key": key, "valueList": [system["meta_tags"][key]]})
+ meta_tags = sorted(meta_tags, key=lambda x: x["key"])
+
+ self.systems.append({"ssid": str(system["ssid"]),
+ "serial": system["serial"] if "serial" in system else "",
+ "password": system["password"],
+ "password_valid": None,
+ "password_set": None,
+ "stored_password_valid": None,
+ "meta_tags": meta_tags,
+ "controller_addresses": system["addresses"] if "addresses" in system else [],
+ "embedded_available": None,
+ "accept_certificate": False,
+ "current_info": {},
+ "changes": {},
+ "updated_required": False,
+ "failed": False,
+ "discovered": False})
+ else:
+ self.module.fail_json(msg="Invalid system! All systems must either be a simple serial number or a dictionary. Failed system: %s" % system)
+
+ # Update default request headers
+ self.DEFAULT_HEADERS.update({"x-netapp-password-validate-method": "none"})
+
+ def discover_array(self):
+ """Search for array using the world wide identifier."""
+ subnet = ipaddress.ip_network(u"%s" % self.subnet_mask)
+
+ try:
+ rc, request_id = self.request("discovery", method="POST", data={"startIP": str(subnet[0]), "endIP": str(subnet[-1]),
+ "connectionTimeout": self.DEFAULT_CONNECTION_TIMEOUT_SEC})
+
+ # Wait for discover to complete
+ discovered_systems = None
+ try:
+ for iteration in range(self.DEFAULT_DISCOVERY_TIMEOUT_SEC):
+ rc, discovered_systems = self.request("discovery?requestId=%s" % request_id["requestId"])
+ if not discovered_systems["discoverProcessRunning"]:
+ break
+ sleep(1)
+ else:
+ self.module.fail_json(msg="Timeout waiting for array discovery process. Subnet [%s]" % self.subnet_mask)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to get the discovery results. Error [%s]." % to_native(error))
+
+ if not discovered_systems:
+ self.module.warn("Discovery found no systems. IP starting address [%s]. IP ending address: [%s]." % (str(subnet[0]), str(subnet[-1])))
+ else:
+ # Add all newly discovered systems. This is ignore any supplied systems to prevent any duplicates.
+ if self.add_discovered_systems:
+ for discovered_system in discovered_systems["storageSystems"]:
+ if discovered_system["serialNumber"] not in self.serial_numbers:
+ self.systems.append({"ssid": discovered_system["serialNumber"],
+ "serial": discovered_system["serialNumber"],
+ "password": self.default_password,
+ "password_valid": None,
+ "password_set": None,
+ "stored_password_valid": None,
+ "meta_tags": self.default_meta_tags,
+ "controller_addresses": [],
+ "embedded_available": None,
+ "accept_certificate": False,
+ "current_info": {},
+ "changes": {},
+ "updated_required": False,
+ "failed": False,
+ "discovered": False})
+
+ # Update controller_addresses
+ for system in self.systems:
+ for discovered_system in discovered_systems["storageSystems"]:
+ if (system["serial"] == discovered_system["serialNumber"] or
+ (system["controller_addresses"] and
+ all([address in discovered_system["ipAddresses"] for address in system["controller_addresses"]]))):
+ system["controller_addresses"] = sorted(discovered_system["ipAddresses"])
+ system["embedded_available"] = "https" in discovered_system["supportedManagementPorts"]
+ system["accept_certificate"] = system["embedded_available"] and self.accept_certificate
+ system["discovered"] = True
+ break
+ else: # Remove any undiscovered system from the systems list
+
+ self.undiscovered_systems.append(system["ssid"])
+ # self.systems.remove(system)
+
+ except Exception as error:
+ self.module.fail_json(msg="Failed to initiate array discovery. Error [%s]." % to_native(error))
+
+ def update_storage_systems_info(self):
+ """Get current web services proxy storage systems."""
+ try:
+ rc, existing_systems = self.request("storage-systems")
+
+ # Mark systems for adding or removing
+ for system in self.systems:
+ for existing_system in existing_systems:
+ if system["ssid"] == existing_system["id"]:
+ system["current_info"] = existing_system
+
+ if system["current_info"]["passwordStatus"] in ["unknown", "securityLockout"]:
+ system["failed"] = True
+ self.module.warn("Skipping storage system [%s] because of current password status [%s]"
+ % (system["ssid"], system["current_info"]["passwordStatus"]))
+ if system["current_info"]["metaTags"]:
+ system["current_info"]["metaTags"] = sorted(system["current_info"]["metaTags"], key=lambda x: x["key"])
+ break
+ else:
+ self.systems_to_add.append(system)
+
+ # Mark systems for removing
+ for existing_system in existing_systems:
+ for system in self.systems:
+ if existing_system["id"] == system["ssid"]:
+
+ # Leave existing but undiscovered storage systems alone and throw a warning.
+ if existing_system["id"] in self.undiscovered_systems:
+ self.undiscovered_systems.remove(existing_system["id"])
+ self.module.warn("Expected storage system exists on the proxy but was failed to be discovered. Array [%s]." % existing_system["id"])
+ break
+ else:
+ self.systems_to_remove.append(existing_system["id"])
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve storage systems. Error [%s]." % to_native(error))
+
+ def set_password(self, system):
+ """Determine whether password has been set and, if it hasn't been set, set it."""
+ if system["embedded_available"] and system["controller_addresses"]:
+ for url in ["https://%s:8443/devmgr" % system["controller_addresses"][0],
+ "https://%s:443/devmgr" % system["controller_addresses"][0],
+ "http://%s:8080/devmgr" % system["controller_addresses"][0]]:
+ try:
+ rc, response = self._request("%s/utils/login?uid=admin&xsrf=false&onlycheck=true" % url, ignore_errors=True, url_username="admin",
+ url_password="", validate_certs=False)
+
+ if rc == 200: # successful login without password
+ system["password_set"] = False
+ if system["password"]:
+ try:
+ rc, storage_system = self._request("%s/v2/storage-systems/1/passwords" % url, method="POST", url_username="admin",
+ headers=self.DEFAULT_HEADERS, url_password="", validate_certs=False,
+ data=json.dumps({"currentAdminPassword": "", "adminPassword": True,
+ "newPassword": system["password"]}))
+
+ except Exception as error:
+ system["failed"] = True
+ self.module.warn("Failed to set storage system password. Array [%s]." % system["ssid"])
+ break
+
+ elif rc == 401: # unauthorized
+ system["password_set"] = True
+ break
+ except Exception as error:
+ pass
+ else:
+ self.module.warn("Failed to retrieve array password state. Array [%s]." % system["ssid"])
+ system["failed"] = True
+
+ def update_system_changes(self, system):
+ """Determine whether storage system configuration changes are required """
+ if system["current_info"]:
+ system["changes"] = dict()
+
+ # Check if management paths should be updated
+ if (sorted(system["controller_addresses"]) != sorted(system["current_info"]["managementPaths"]) or
+ system["current_info"]["ip1"] not in system["current_info"]["managementPaths"] or
+ system["current_info"]["ip2"] not in system["current_info"]["managementPaths"]):
+ system["changes"].update({"controllerAddresses": system["controller_addresses"]})
+
+ # Check for expected meta tag count
+ if len(system["meta_tags"]) != len(system["current_info"]["metaTags"]):
+ if len(system["meta_tags"]) == 0:
+ system["changes"].update({"removeAllTags": True})
+ else:
+ system["changes"].update({"metaTags": system["meta_tags"]})
+
+ # Check for expected meta tag key-values
+ else:
+ for index in range(len(system["meta_tags"])):
+ if (system["current_info"]["metaTags"][index]["key"] != system["meta_tags"][index]["key"] or
+ sorted(system["current_info"]["metaTags"][index]["valueList"]) != sorted(system["meta_tags"][index]["valueList"])):
+ system["changes"].update({"metaTags": system["meta_tags"]})
+ break
+
+ # Check whether CA certificate should be accepted
+ if system["accept_certificate"] and not all([controller["certificateStatus"] == "trusted" for controller in system["current_info"]["controllers"]]):
+ system["changes"].update({"acceptCertificate": True})
+
+ if system["id"] not in self.undiscovered_systems and system["changes"]:
+ self.systems_to_update.append(system)
+
+ def add_system(self, system):
+ """Add basic storage system definition to the web services proxy."""
+ self.set_password(system)
+
+ body = {"id": system["ssid"],
+ "controllerAddresses": system["controller_addresses"],
+ "password": system["password"]}
+ if system["accept_certificate"]: # Set only if embedded is available and accept_certificates==True
+ body.update({"acceptCertificate": system["accept_certificate"]})
+ if system["meta_tags"]:
+ body.update({"metaTags": system["meta_tags"]})
+
+ try:
+ rc, storage_system = self.request("storage-systems", method="POST", data=body)
+ except Exception as error:
+ self.module.warn("Failed to add storage system. Array [%s]. Error [%s]" % (system["ssid"], to_native(error)))
+ return # Skip the password validation.
+
+ # Ensure the password is validated
+ for retries in range(5):
+ sleep(1)
+ try:
+ rc, storage_system = self.request("storage-systems/%s/validatePassword" % system["ssid"], method="POST")
+ break
+ except Exception as error:
+ continue
+ else:
+ self.module.warn("Failed to validate password status. Array [%s]. Error [%s]" % (system["ssid"], to_native(error)))
+
+ def update_system(self, system):
+ """Update storage system configuration."""
+ try:
+ rc, storage_system = self.request("storage-systems/%s" % system["ssid"], method="POST", data=system["changes"])
+ except Exception as error:
+ self.module.warn("Failed to update storage system. Array [%s]. Error [%s]" % (system["ssid"], to_native(error)))
+
+ def remove_system(self, ssid):
+ """Remove storage system."""
+ try:
+ rc, storage_system = self.request("storage-systems/%s" % ssid, method="DELETE")
+ except Exception as error:
+ self.module.warn("Failed to remove storage system. Array [%s]. Error [%s]." % (ssid, to_native(error)))
+
+ def apply(self):
+ """Determine whether changes are required and, if necessary, apply them."""
+ if self.is_embedded():
+ self.module.fail_json(msg="Cannot add/remove storage systems to SANtricity Web Services Embedded instance.")
+
+ if self.add_discovered_systems or self.systems:
+ if self.subnet_mask:
+ self.discover_array()
+ self.update_storage_systems_info()
+
+ # Determine whether the storage system requires updating
+ thread_pool = []
+ for system in self.systems:
+ if not system["failed"]:
+ thread = threading.Thread(target=self.update_system_changes, args=(system,))
+ thread_pool.append(thread)
+ thread.start()
+ for thread in thread_pool:
+ thread.join()
+ else:
+ self.update_storage_systems_info()
+
+ changes_required = False
+ if self.systems_to_add or self.systems_to_update or self.systems_to_remove:
+ changes_required = True
+
+ if changes_required and not self.module.check_mode:
+ add_msg = ""
+ update_msg = ""
+ remove_msg = ""
+
+ # Remove storage systems
+ if self.systems_to_remove:
+ ssids = []
+ thread_pool = []
+ for ssid in self.systems_to_remove:
+ thread = threading.Thread(target=self.remove_system, args=(ssid,))
+ thread_pool.append(thread)
+ thread.start()
+ ssids.append(ssid)
+ for thread in thread_pool:
+ thread.join()
+ if ssids:
+ remove_msg = "system%s removed: %s" % ("s" if len(ssids) > 1 else "", ", ".join(ssids))
+
+ thread_pool = []
+
+ # Add storage systems
+ if self.systems_to_add:
+ ssids = []
+ for system in self.systems_to_add:
+ if not system["failed"]:
+ thread = threading.Thread(target=self.add_system, args=(system,))
+ thread_pool.append(thread)
+ thread.start()
+ ssids.append(system["ssid"])
+ if ssids:
+ add_msg = "system%s added: %s" % ("s" if len(ssids) > 1 else "", ", ".join(ssids))
+
+ # Update storage systems
+ if self.systems_to_update:
+ ssids = []
+ for system in self.systems_to_update:
+ if not system["failed"]:
+ thread = threading.Thread(target=self.update_system, args=(system,))
+ thread_pool.append(thread)
+ thread.start()
+ ssids.append(system["ssid"])
+ if ssids:
+ update_msg = "system%s updated: %s" % ("s" if len(ssids) > 1 else "", ", ".join(ssids))
+
+ # Wait for storage systems to be added or updated
+ for thread in thread_pool:
+ thread.join()
+
+ # Report module actions
+ if self.undiscovered_systems:
+ undiscovered_msg = "system%s undiscovered: %s" % ("s " if len(self.undiscovered_systems) > 1 else "", ", ".join(self.undiscovered_systems))
+ self.module.fail_json(msg=(", ".join([msg for msg in [add_msg, update_msg, remove_msg, undiscovered_msg] if msg])), changed=changes_required)
+
+ self.module.exit_json(msg=", ".join([msg for msg in [add_msg, update_msg, remove_msg] if msg]), changed=changes_required)
+
+ # Report no changes
+ if self.undiscovered_systems:
+ self.module.fail_json(msg="No changes were made; however the following system(s) failed to be discovered: %s."
+ % self.undiscovered_systems, changed=changes_required)
+ self.module.exit_json(msg="No changes were made.", changed=changes_required)
+
+
+def main():
+ proxy_systems = NetAppESeriesProxySystems()
+ proxy_systems.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_storagepool.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_storagepool.py
new file mode 100644
index 00000000..a1307d59
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_storagepool.py
@@ -0,0 +1,926 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_storagepool
+short_description: NetApp E-Series manage volume groups and disk pools
+description: Create or remove volume groups and disk pools for NetApp E-series storage arrays.
+author:
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ state:
+ description:
+ - Whether the specified storage pool should exist or not.
+ - Note that removing a storage pool currently requires the removal of all defined volumes first.
+ type: str
+ choices: ["present", "absent"]
+ default: "present"
+ name:
+ description:
+ - The name of the storage pool to manage
+ type: str
+ required: true
+ criteria_drive_count:
+ description:
+ - The number of disks to use for building the storage pool.
+ - When I(state=="present") then I(criteria_drive_count) or I(criteria_min_usable_capacity) must be specified.
+ - The pool will be expanded if this number exceeds the number of disks already in place (See expansion note below)
+ type: int
+ required: false
+ criteria_min_usable_capacity:
+ description:
+ - The minimum size of the storage pool (in size_unit).
+ - When I(state=="present") then I(criteria_drive_count) or I(criteria_min_usable_capacity) must be specified.
+ - The pool will be expanded if this value exceeds its current size. (See expansion note below)
+ type: float
+ required: false
+ criteria_drive_type:
+ description:
+ - The type of disk (hdd or ssd) to use when searching for candidates to use.
+ - When not specified each drive type will be evaluated until successful drive candidates are found starting with
+ the most prevalent drive type.
+ type: str
+ choices: ["hdd","ssd"]
+ required: false
+ criteria_size_unit:
+ description:
+ - The unit used to interpret size parameters
+ type: str
+ choices: ["bytes", "b", "kb", "mb", "gb", "tb", "pb", "eb", "zb", "yb"]
+ default: "gb"
+ required: false
+ criteria_drive_min_size:
+ description:
+ - The minimum individual drive size (in size_unit) to consider when choosing drives for the storage pool.
+ type: float
+ required: false
+ criteria_drive_interface_type:
+ description:
+ - The interface type to use when selecting drives for the storage pool
+ - If not provided then all interface types will be considered.
+ type: str
+ choices: ["sas", "sas4k", "fibre", "fibre520b", "scsi", "sata", "pata"]
+ required: false
+ criteria_drive_require_da:
+ description:
+ - Ensures the storage pool will be created with only data assurance (DA) capable drives.
+ - Only available for new storage pools; existing storage pools cannot be converted.
+ type: bool
+ default: false
+ required: false
+ criteria_drive_require_fde:
+ description:
+ - Whether full disk encryption ability is required for drives to be added to the storage pool
+ type: bool
+ default: false
+ required: false
+ raid_level:
+ description:
+ - The RAID level of the storage pool to be created.
+ - Required only when I(state=="present").
+ - When I(raid_level=="raidDiskPool") then I(criteria_drive_count >= 10 or criteria_drive_count >= 11) is required
+ depending on the storage array specifications.
+ - When I(raid_level=="raid0") then I(1<=criteria_drive_count) is required.
+ - When I(raid_level=="raid1") then I(2<=criteria_drive_count) is required.
+ - When I(raid_level=="raid3") then I(3<=criteria_drive_count<=30) is required.
+ - When I(raid_level=="raid5") then I(3<=criteria_drive_count<=30) is required.
+ - When I(raid_level=="raid6") then I(5<=criteria_drive_count<=30) is required.
+ - Note that raidAll will be treated as raidDiskPool and raid3 as raid5.
+ type: str
+ default: "raidDiskPool"
+ choices: ["raidAll", "raid0", "raid1", "raid3", "raid5", "raid6", "raidDiskPool"]
+ required: false
+ secure_pool:
+ description:
+ - Enables security at rest feature on the storage pool.
+ - Will only work if all drives in the pool are security capable (FDE, FIPS, or mix)
+ - Warning, once security is enabled it is impossible to disable without erasing the drives.
+ type: bool
+ required: false
+ reserve_drive_count:
+ description:
+ - Set the number of drives reserved by the storage pool for reconstruction operations.
+ - Only valid on raid disk pools.
+ type: int
+ required: false
+ remove_volumes:
+ description:
+ - Prior to removing a storage pool, delete all volumes in the pool.
+ type: bool
+ default: true
+ required: false
+ erase_secured_drives:
+ description:
+ - If I(state=="absent") then all storage pool drives will be erase
+ - If I(state=="present") then delete all available storage array drives that have security enabled.
+ type: bool
+ default: true
+ required: false
+notes:
+ - The expansion operations are non-blocking due to the time consuming nature of expanding volume groups
+ - Traditional volume groups (raid0, raid1, raid5, raid6) are performed in steps dictated by the storage array. Each
+ required step will be attempted until the request fails which is likely because of the required expansion time.
+ - raidUnsupported will be treated as raid0, raidAll as raidDiskPool and raid3 as raid5.
+ - Tray loss protection and drawer loss protection will be chosen if at all possible.
+"""
+EXAMPLES = """
+- name: No disk groups
+ na_santricity_storagepool:
+ ssid: "{{ ssid }}"
+ name: "{{ item }}"
+ state: absent
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ validate_certs: "{{ netapp_api_validate_certs }}"
+"""
+RETURN = """
+msg:
+ description: Success message
+ returned: success
+ type: str
+ sample: Json facts for the pool that was created.
+"""
+import functools
+from itertools import groupby
+from time import sleep
+
+from pprint import pformat
+from ansible.module_utils._text import to_native
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+
+
+def get_most_common_elements(iterator):
+ """Returns a generator containing a descending list of most common elements."""
+ if not isinstance(iterator, list):
+ raise TypeError("iterator must be a list.")
+
+ grouped = [(key, len(list(group))) for key, group in groupby(sorted(iterator))]
+ return sorted(grouped, key=lambda x: x[1], reverse=True)
+
+
+def memoize(func):
+ """Generic memoizer for any function with any number of arguments including zero."""
+
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ class MemoizeFuncArgs(dict):
+ def __missing__(self, _key):
+ self[_key] = func(*args, **kwargs)
+ return self[_key]
+
+ key = str((args, kwargs)) if args and kwargs else "no_argument_response"
+ return MemoizeFuncArgs().__getitem__(key)
+
+ return wrapper
+
+
+class NetAppESeriesStoragePool(NetAppESeriesModule):
+ EXPANSION_TIMEOUT_SEC = 10
+ DEFAULT_DISK_POOL_MINIMUM_DISK_COUNT = 11
+
+ def __init__(self):
+ version = "02.00.0000.0000"
+ ansible_options = dict(
+ state=dict(choices=["present", "absent"], default="present", type="str"),
+ name=dict(required=True, type="str"),
+ criteria_size_unit=dict(choices=["bytes", "b", "kb", "mb", "gb", "tb", "pb", "eb", "zb", "yb"],
+ default="gb", type="str"),
+ criteria_drive_count=dict(type="int"),
+ criteria_drive_interface_type=dict(choices=["sas", "sas4k", "fibre", "fibre520b", "scsi", "sata", "pata"],
+ type="str"),
+ criteria_drive_type=dict(choices=["ssd", "hdd"], type="str", required=False),
+ criteria_drive_min_size=dict(type="float"),
+ criteria_drive_require_da=dict(type="bool", required=False),
+ criteria_drive_require_fde=dict(type="bool", required=False),
+ criteria_min_usable_capacity=dict(type="float"),
+ raid_level=dict(choices=["raidAll", "raid0", "raid1", "raid3", "raid5", "raid6", "raidDiskPool"],
+ default="raidDiskPool"),
+ erase_secured_drives=dict(type="bool", default=True),
+ secure_pool=dict(type="bool", default=False),
+ reserve_drive_count=dict(type="int"),
+ remove_volumes=dict(type="bool", default=True))
+
+ required_if = [["state", "present", ["raid_level"]]]
+ super(NetAppESeriesStoragePool, self).__init__(ansible_options=ansible_options,
+ web_services_version=version,
+ supports_check_mode=True,
+ required_if=required_if)
+
+ args = self.module.params
+ self.state = args["state"]
+ self.ssid = args["ssid"]
+ self.name = args["name"]
+ self.criteria_drive_count = args["criteria_drive_count"]
+ self.criteria_min_usable_capacity = args["criteria_min_usable_capacity"]
+ self.criteria_size_unit = args["criteria_size_unit"]
+ self.criteria_drive_min_size = args["criteria_drive_min_size"]
+ self.criteria_drive_type = args["criteria_drive_type"]
+ self.criteria_drive_interface_type = args["criteria_drive_interface_type"]
+ self.criteria_drive_require_fde = args["criteria_drive_require_fde"]
+ self.criteria_drive_require_da = args["criteria_drive_require_da"]
+ self.raid_level = args["raid_level"]
+ self.erase_secured_drives = args["erase_secured_drives"]
+ self.secure_pool = args["secure_pool"]
+ self.reserve_drive_count = args["reserve_drive_count"]
+ self.remove_volumes = args["remove_volumes"]
+ self.pool_detail = None
+
+ # Change all sizes to be measured in bytes
+ if self.criteria_min_usable_capacity:
+ self.criteria_min_usable_capacity = int(self.criteria_min_usable_capacity *
+ self.SIZE_UNIT_MAP[self.criteria_size_unit])
+ if self.criteria_drive_min_size:
+ self.criteria_drive_min_size = int(self.criteria_drive_min_size *
+ self.SIZE_UNIT_MAP[self.criteria_size_unit])
+ self.criteria_size_unit = "bytes"
+
+ # Adjust unused raid level option to reflect documentation
+ if self.raid_level == "raidAll":
+ self.raid_level = "raidDiskPool"
+ if self.raid_level == "raid3":
+ self.raid_level = "raid5"
+
+ @property
+ @memoize
+ def available_drives(self):
+ """Determine the list of available drives"""
+ return [drive["id"] for drive in self.drives if drive["available"] and drive["status"] == "optimal"]
+
+ @property
+ @memoize
+ def available_drive_types(self):
+ """Determine the types of available drives sorted by the most common first."""
+ types = [drive["driveMediaType"] for drive in self.drives]
+ return [entry[0] for entry in get_most_common_elements(types)]
+
+ @property
+ @memoize
+ def available_drive_interface_types(self):
+ """Determine the types of available drives."""
+ interfaces = [drive["phyDriveType"] for drive in self.drives]
+ return [entry[0] for entry in get_most_common_elements(interfaces)]
+
+ @property
+ def storage_pool_drives(self):
+ """Retrieve list of drives found in storage pool."""
+ return [drive for drive in self.drives if drive["currentVolumeGroupRef"] == self.pool_detail["id"] and not drive["hotSpare"]]
+
+ @property
+ def expandable_drive_count(self):
+ """Maximum number of drives that a storage pool can be expended at a given time."""
+ capabilities = None
+ if self.raid_level == "raidDiskPool":
+ return len(self.available_drives)
+
+ try:
+ rc, capabilities = self.request("storage-systems/%s/capabilities" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to fetch maximum expandable drive count. Array id [%s]. Error[%s]."
+ % (self.ssid, to_native(error)))
+
+ return capabilities["featureParameters"]["maxDCEDrives"]
+
+ @property
+ def disk_pool_drive_minimum(self):
+ """Provide the storage array's minimum disk pool drive count."""
+ rc, attr = self.request("storage-systems/%s/symbol/getSystemAttributeDefaults" % self.ssid, ignore_errors=True)
+
+ # Standard minimum is 11 drives but some allow 10 drives. 10 will be the default
+ if (rc != 200 or "minimumDriveCount" not in attr["defaults"]["diskPoolDefaultAttributes"].keys() or
+ attr["defaults"]["diskPoolDefaultAttributes"]["minimumDriveCount"] == 0):
+ return self.DEFAULT_DISK_POOL_MINIMUM_DISK_COUNT
+
+ return attr["defaults"]["diskPoolDefaultAttributes"]["minimumDriveCount"]
+
+ def get_available_drive_capacities(self, drive_id_list=None):
+ """Determine the list of available drive capacities."""
+ if drive_id_list:
+ available_drive_capacities = set([int(drive["usableCapacity"]) for drive in self.drives
+ if drive["id"] in drive_id_list and drive["available"] and
+ drive["status"] == "optimal"])
+ else:
+ available_drive_capacities = set([int(drive["usableCapacity"]) for drive in self.drives
+ if drive["available"] and drive["status"] == "optimal"])
+
+ self.module.log("available drive capacities: %s" % available_drive_capacities)
+ return list(available_drive_capacities)
+
+ @property
+ def drives(self):
+ """Retrieve list of drives found in storage pool."""
+ drives = None
+ try:
+ rc, drives = self.request("storage-systems/%s/drives" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to fetch disk drives. Array id [%s]. Error[%s]."
+ % (self.ssid, to_native(error)))
+
+ return drives
+
+ def is_drive_count_valid(self, drive_count):
+ """Validate drive count criteria is met."""
+ if self.criteria_drive_count and drive_count < self.criteria_drive_count:
+ return False
+
+ if self.raid_level == "raidDiskPool":
+ return drive_count >= self.disk_pool_drive_minimum
+ if self.raid_level == "raid0":
+ return drive_count > 0
+ if self.raid_level == "raid1":
+ return drive_count >= 2 and (drive_count % 2) == 0
+ if self.raid_level in ["raid3", "raid5"]:
+ return 3 <= drive_count <= 30
+ if self.raid_level == "raid6":
+ return 5 <= drive_count <= 30
+ return False
+
+ @property
+ def storage_pool(self):
+ """Retrieve storage pool information."""
+ storage_pools_resp = None
+ try:
+ rc, storage_pools_resp = self.request("storage-systems/%s/storage-pools" % self.ssid)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to get storage pools. Array id [%s]. Error[%s]. State[%s]."
+ % (self.ssid, to_native(err), self.state))
+
+ pool_detail = [pool for pool in storage_pools_resp if pool["name"] == self.name]
+ return pool_detail[0] if pool_detail else dict()
+
+ @property
+ def storage_pool_volumes(self):
+ """Retrieve list of volumes associated with storage pool."""
+ volumes_resp = None
+ try:
+ rc, volumes_resp = self.request("storage-systems/%s/volumes" % self.ssid)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to get storage pools. Array id [%s]. Error[%s]. State[%s]."
+ % (self.ssid, to_native(err), self.state))
+
+ group_ref = self.storage_pool["volumeGroupRef"]
+ storage_pool_volume_list = [volume["id"] for volume in volumes_resp if volume["volumeGroupRef"] == group_ref]
+ return storage_pool_volume_list
+
+ def get_ddp_capacity(self, expansion_drive_list):
+ """Return the total usable capacity based on the additional drives."""
+
+ def get_ddp_error_percent(_drive_count, _extent_count):
+ """Determine the space reserved for reconstruction"""
+ if _drive_count <= 36:
+ if _extent_count <= 600:
+ return 0.40
+ elif _extent_count <= 1400:
+ return 0.35
+ elif _extent_count <= 6200:
+ return 0.20
+ elif _extent_count <= 50000:
+ return 0.15
+ elif _drive_count <= 64:
+ if _extent_count <= 600:
+ return 0.20
+ elif _extent_count <= 1400:
+ return 0.15
+ elif _extent_count <= 6200:
+ return 0.10
+ elif _extent_count <= 50000:
+ return 0.05
+ elif _drive_count <= 480:
+ if _extent_count <= 600:
+ return 0.20
+ elif _extent_count <= 1400:
+ return 0.15
+ elif _extent_count <= 6200:
+ return 0.10
+ elif _extent_count <= 50000:
+ return 0.05
+
+ self.module.fail_json(msg="Drive count exceeded the error percent table. Array[%s]" % self.ssid)
+
+ def get_ddp_reserved_drive_count(_disk_count):
+ """Determine the number of reserved drive."""
+ reserve_count = 0
+
+ if self.reserve_drive_count:
+ reserve_count = self.reserve_drive_count
+ elif _disk_count >= 256:
+ reserve_count = 8
+ elif _disk_count >= 192:
+ reserve_count = 7
+ elif _disk_count >= 128:
+ reserve_count = 6
+ elif _disk_count >= 64:
+ reserve_count = 4
+ elif _disk_count >= 32:
+ reserve_count = 3
+ elif _disk_count >= 12:
+ reserve_count = 2
+ elif _disk_count == 11:
+ reserve_count = 1
+
+ return reserve_count
+
+ if self.pool_detail:
+ drive_count = len(self.storage_pool_drives) + len(expansion_drive_list)
+ else:
+ drive_count = len(expansion_drive_list)
+
+ drive_usable_capacity = min(min(self.get_available_drive_capacities()),
+ min(self.get_available_drive_capacities(expansion_drive_list)))
+ drive_data_extents = ((drive_usable_capacity - 8053063680) / 536870912)
+ maximum_stripe_count = (drive_count * drive_data_extents) / 10
+
+ error_percent = get_ddp_error_percent(drive_count, drive_data_extents)
+ error_overhead = (drive_count * drive_data_extents / 10 * error_percent + 10) / 10
+
+ total_stripe_count = maximum_stripe_count - error_overhead
+ stripe_count_per_drive = total_stripe_count / drive_count
+ reserved_stripe_count = get_ddp_reserved_drive_count(drive_count) * stripe_count_per_drive
+ available_stripe_count = total_stripe_count - reserved_stripe_count
+
+ return available_stripe_count * 4294967296
+
+ @memoize
+ def get_candidate_drives(self):
+ """Retrieve set of drives candidates for creating a new storage pool."""
+
+ def get_candidate_drive_request():
+ """Perform request for new volume creation."""
+
+ candidates_list = list()
+ drive_types = [self.criteria_drive_type] if self.criteria_drive_type else self.available_drive_types
+ interface_types = [self.criteria_drive_interface_type] \
+ if self.criteria_drive_interface_type else self.available_drive_interface_types
+
+ for interface_type in interface_types:
+ for drive_type in drive_types:
+ candidates = None
+ volume_candidate_request_data = dict(
+ type="diskPool" if self.raid_level == "raidDiskPool" else "traditional",
+ diskPoolVolumeCandidateRequestData=dict(
+ reconstructionReservedDriveCount=65535))
+ candidate_selection_type = dict(
+ candidateSelectionType="count",
+ driveRefList=dict(driveRef=self.available_drives))
+ criteria = dict(raidLevel=self.raid_level,
+ phyDriveType=interface_type,
+ dssPreallocEnabled=False,
+ securityType="capable" if self.criteria_drive_require_fde else "none",
+ driveMediaType=drive_type,
+ onlyProtectionInformationCapable=True if self.criteria_drive_require_da else False,
+ volumeCandidateRequestData=volume_candidate_request_data,
+ allocateReserveSpace=False,
+ securityLevel="fde" if self.criteria_drive_require_fde else "none",
+ candidateSelectionType=candidate_selection_type)
+
+ try:
+ rc, candidates = self.request("storage-systems/%s/symbol/getVolumeCandidates?verboseError"
+ "Response=true" % self.ssid, data=criteria, method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve volume candidates. Array [%s]. Error [%s]."
+ % (self.ssid, to_native(error)))
+
+ if candidates:
+ candidates_list.extend(candidates["volumeCandidate"])
+
+ if candidates_list:
+ def candidate_sort_function(entry):
+ """Orders candidates based on tray/drawer loss protection."""
+ preference = 3
+ if entry["drawerLossProtection"]:
+ preference -= 1
+ if entry["trayLossProtection"]:
+ preference -= 2
+ return preference
+ candidates_list.sort(key=candidate_sort_function)
+
+ return candidates_list
+
+ # Determine the appropriate candidate list
+ for candidate in get_candidate_drive_request():
+
+ # Evaluate candidates for required drive count, collective drive usable capacity and minimum drive size
+ if self.criteria_drive_count:
+ if self.criteria_drive_count != int(candidate["driveCount"]):
+ continue
+ if self.criteria_min_usable_capacity:
+ if ((self.raid_level == "raidDiskPool" and self.criteria_min_usable_capacity >
+ self.get_ddp_capacity(candidate["driveRefList"]["driveRef"])) or
+ self.criteria_min_usable_capacity > int(candidate["usableSize"])):
+ continue
+ if self.criteria_drive_min_size:
+ if self.criteria_drive_min_size > min(self.get_available_drive_capacities(candidate["driveRefList"]["driveRef"])):
+ continue
+
+ return candidate
+
+ self.module.fail_json(msg="Not enough drives to meet the specified criteria. Array [%s]." % self.ssid)
+
+ @memoize
+ def get_expansion_candidate_drives(self):
+ """Retrieve required expansion drive list.
+
+ Note: To satisfy the expansion criteria each item in the candidate list must added specified group since there
+ is a potential limitation on how many drives can be incorporated at a time.
+ * Traditional raid volume groups must be added two drives maximum at a time. No limits on raid disk pools.
+
+ :return list(candidate): list of candidate structures from the getVolumeGroupExpansionCandidates symbol endpoint
+ """
+
+ def get_expansion_candidate_drive_request():
+ """Perform the request for expanding existing volume groups or disk pools.
+
+ Note: the list of candidate structures do not necessarily produce candidates that meet all criteria.
+ """
+ candidates_list = None
+ url = "storage-systems/%s/symbol/getVolumeGroupExpansionCandidates?verboseErrorResponse=true" % self.ssid
+ if self.raid_level == "raidDiskPool":
+ url = "storage-systems/%s/symbol/getDiskPoolExpansionCandidates?verboseErrorResponse=true" % self.ssid
+
+ try:
+ rc, candidates_list = self.request(url, method="POST", data=self.pool_detail["id"])
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve volume candidates. Array [%s]. Error [%s]."
+ % (self.ssid, to_native(error)))
+
+ return candidates_list["candidates"]
+
+ required_candidate_list = list()
+ required_additional_drives = 0
+ required_additional_capacity = 0
+ total_required_capacity = 0
+
+ # determine whether and how much expansion is need to satisfy the specified criteria
+ if self.criteria_min_usable_capacity:
+ total_required_capacity = self.criteria_min_usable_capacity
+ required_additional_capacity = self.criteria_min_usable_capacity - int(self.pool_detail["totalRaidedSpace"])
+
+ if self.criteria_drive_count:
+ required_additional_drives = self.criteria_drive_count - len(self.storage_pool_drives)
+
+ # Determine the appropriate expansion candidate list
+ if required_additional_drives > 0 or required_additional_capacity > 0:
+ for candidate in get_expansion_candidate_drive_request():
+
+ if self.criteria_drive_min_size:
+ if self.criteria_drive_min_size > min(self.get_available_drive_capacities(candidate["drives"])):
+ continue
+
+ if self.raid_level == "raidDiskPool":
+ if (len(candidate["drives"]) >= required_additional_drives and
+ self.get_ddp_capacity(candidate["drives"]) >= total_required_capacity):
+ required_candidate_list.append(candidate)
+ break
+ else:
+ required_additional_drives -= len(candidate["drives"])
+ required_additional_capacity -= int(candidate["usableCapacity"])
+ required_candidate_list.append(candidate)
+
+ # Determine if required drives and capacities are satisfied
+ if required_additional_drives <= 0 and required_additional_capacity <= 0:
+ break
+ else:
+ self.module.fail_json(msg="Not enough drives to meet the specified criteria. Array [%s]." % self.ssid)
+
+ return required_candidate_list
+
+ def get_reserve_drive_count(self):
+ """Retrieve the current number of reserve drives for raidDiskPool (Only for raidDiskPool)."""
+
+ if not self.pool_detail:
+ self.module.fail_json(msg="The storage pool must exist. Array [%s]." % self.ssid)
+
+ if self.raid_level != "raidDiskPool":
+ self.module.fail_json(msg="The storage pool must be a raidDiskPool. Pool [%s]. Array [%s]."
+ % (self.pool_detail["id"], self.ssid))
+
+ return self.pool_detail["volumeGroupData"]["diskPoolData"]["reconstructionReservedDriveCount"]
+
+ def get_maximum_reserve_drive_count(self):
+ """Retrieve the maximum number of reserve drives for storage pool (Only for raidDiskPool)."""
+ if self.raid_level != "raidDiskPool":
+ self.module.fail_json(msg="The storage pool must be a raidDiskPool. Pool [%s]. Array [%s]."
+ % (self.pool_detail["id"], self.ssid))
+
+ drives_ids = list()
+
+ if self.pool_detail:
+ drives_ids.extend(self.storage_pool_drives)
+ for candidate in self.get_expansion_candidate_drives():
+ drives_ids.extend((candidate["drives"]))
+ else:
+ candidate = self.get_candidate_drives()
+ drives_ids.extend(candidate["driveRefList"]["driveRef"])
+
+ drive_count = len(drives_ids)
+ maximum_reserve_drive_count = min(int(drive_count * 0.2 + 1), drive_count - 10)
+ if maximum_reserve_drive_count > 10:
+ maximum_reserve_drive_count = 10
+
+ return maximum_reserve_drive_count
+
+ def set_reserve_drive_count(self, check_mode=False):
+ """Set the reserve drive count for raidDiskPool."""
+ changed = False
+
+ if self.raid_level == "raidDiskPool" and self.reserve_drive_count:
+ maximum_count = self.get_maximum_reserve_drive_count()
+
+ if self.reserve_drive_count < 0 or self.reserve_drive_count > maximum_count:
+ self.module.fail_json(msg="Supplied reserve drive count is invalid or exceeds the maximum allowed. "
+ "Note that it may be necessary to wait for expansion operations to complete "
+ "before the adjusting the reserve drive count. Maximum [%s]. Array [%s]."
+ % (maximum_count, self.ssid))
+
+ if self.reserve_drive_count != self.get_reserve_drive_count():
+ changed = True
+
+ if not check_mode:
+ try:
+ rc, resp = self.request("storage-systems/%s/symbol/setDiskPoolReservedDriveCount" % self.ssid,
+ method="POST", data=dict(volumeGroupRef=self.pool_detail["id"],
+ newDriveCount=self.reserve_drive_count))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to set reserve drive count for disk pool. Disk Pool [%s]."
+ " Array [%s]." % (self.pool_detail["id"], self.ssid))
+
+ return changed
+
+ def erase_all_available_secured_drives(self, check_mode=False):
+ """Erase all available drives that have encryption at rest feature enabled."""
+ changed = False
+ drives_list = list()
+ for drive in self.drives:
+ if drive["available"] and drive["fdeEnabled"]:
+ changed = True
+ drives_list.append(drive["id"])
+
+ if drives_list and not check_mode:
+ try:
+ rc, resp = self.request("storage-systems/%s/symbol/reprovisionDrive?verboseErrorResponse=true"
+ % self.ssid, method="POST", data=dict(driveRef=drives_list))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to erase all secured drives. Array [%s]" % self.ssid)
+
+ return changed
+
+ def create_storage_pool(self):
+ """Create new storage pool."""
+ url = "storage-systems/%s/symbol/createVolumeGroup?verboseErrorResponse=true" % self.ssid
+ request_body = dict(label=self.name,
+ candidate=self.get_candidate_drives())
+
+ if self.raid_level == "raidDiskPool":
+ url = "storage-systems/%s/symbol/createDiskPool?verboseErrorResponse=true" % self.ssid
+
+ request_body.update(
+ dict(backgroundOperationPriority="useDefault",
+ criticalReconstructPriority="useDefault",
+ degradedReconstructPriority="useDefault",
+ poolUtilizationCriticalThreshold=65535,
+ poolUtilizationWarningThreshold=0))
+
+ if self.reserve_drive_count:
+ request_body.update(dict(volumeCandidateData=dict(
+ diskPoolVolumeCandidateData=dict(reconstructionReservedDriveCount=self.reserve_drive_count))))
+
+ try:
+ rc, resp = self.request(url, method="POST", data=request_body)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to create storage pool. Array id [%s]. Error[%s]."
+ % (self.ssid, to_native(error)))
+
+ # Update drive and storage pool information
+ self.pool_detail = self.storage_pool
+
+ def delete_storage_pool(self):
+ """Delete storage pool."""
+ storage_pool_drives = [drive["id"] for drive in self.storage_pool_drives if drive["fdeEnabled"]]
+ try:
+ delete_volumes_parameter = "?delete-volumes=true" if self.remove_volumes else ""
+ rc, resp = self.request("storage-systems/%s/storage-pools/%s%s"
+ % (self.ssid, self.pool_detail["id"], delete_volumes_parameter), method="DELETE")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to delete storage pool. Pool id [%s]. Array id [%s]. Error[%s]."
+ % (self.pool_detail["id"], self.ssid, to_native(error)))
+
+ if storage_pool_drives and self.erase_secured_drives:
+ try:
+ rc, resp = self.request("storage-systems/%s/symbol/reprovisionDrive?verboseErrorResponse=true"
+ % self.ssid, method="POST", data=dict(driveRef=storage_pool_drives))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to erase drives prior to creating new storage pool. Array [%s]."
+ " Error [%s]." % (self.ssid, to_native(error)))
+
+ def secure_storage_pool(self, check_mode=False):
+ """Enable security on an existing storage pool"""
+ self.pool_detail = self.storage_pool
+ needs_secure_pool = False
+
+ if not self.secure_pool and self.pool_detail["securityType"] == "enabled":
+ self.module.fail_json(msg="It is not possible to disable storage pool security! See array documentation.")
+ if self.secure_pool and self.pool_detail["securityType"] != "enabled":
+ needs_secure_pool = True
+
+ if needs_secure_pool and not check_mode:
+ try:
+ rc, resp = self.request("storage-systems/%s/storage-pools/%s" % (self.ssid, self.pool_detail["id"]),
+ data=dict(securePool=True), method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to secure storage pool. Pool id [%s]. Array [%s]. Error"
+ " [%s]." % (self.pool_detail["id"], self.ssid, to_native(error)))
+
+ self.pool_detail = self.storage_pool
+ return needs_secure_pool
+
+ def migrate_raid_level(self, check_mode=False):
+ """Request storage pool raid level migration."""
+ needs_migration = self.raid_level != self.pool_detail["raidLevel"]
+ if needs_migration and self.pool_detail["raidLevel"] == "raidDiskPool":
+ self.module.fail_json(msg="Raid level cannot be changed for disk pools")
+
+ if needs_migration and not check_mode:
+ sp_raid_migrate_req = dict(raidLevel=self.raid_level)
+
+ try:
+ rc, resp = self.request("storage-systems/%s/storage-pools/%s/raid-type-migration"
+ % (self.ssid, self.name), data=sp_raid_migrate_req, method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to change the raid level of storage pool. Array id [%s]."
+ " Error[%s]." % (self.ssid, to_native(error)))
+
+ self.pool_detail = self.storage_pool
+ return needs_migration
+
+ def expand_storage_pool(self, check_mode=False):
+ """Add drives to existing storage pool.
+
+ :return bool: whether drives were required to be added to satisfy the specified criteria."""
+ expansion_candidate_list = self.get_expansion_candidate_drives()
+ changed_required = bool(expansion_candidate_list)
+ estimated_completion_time = 0.0
+
+ # build expandable groupings of traditional raid candidate
+ required_expansion_candidate_list = list()
+ while expansion_candidate_list:
+ subset = list()
+ while expansion_candidate_list and len(subset) < self.expandable_drive_count:
+ subset.extend(expansion_candidate_list.pop()["drives"])
+ required_expansion_candidate_list.append(subset)
+
+ if required_expansion_candidate_list and not check_mode:
+ url = "storage-systems/%s/symbol/startVolumeGroupExpansion?verboseErrorResponse=true" % self.ssid
+ if self.raid_level == "raidDiskPool":
+ url = "storage-systems/%s/symbol/startDiskPoolExpansion?verboseErrorResponse=true" % self.ssid
+
+ while required_expansion_candidate_list:
+ candidate_drives_list = required_expansion_candidate_list.pop()
+ request_body = dict(volumeGroupRef=self.pool_detail["volumeGroupRef"],
+ driveRef=candidate_drives_list)
+ try:
+ rc, resp = self.request(url, method="POST", data=request_body)
+ except Exception as error:
+ rc, actions_resp = self.request("storage-systems/%s/storage-pools/%s/action-progress"
+ % (self.ssid, self.pool_detail["id"]), ignore_errors=True)
+ if rc == 200 and actions_resp:
+ actions = [action["currentAction"] for action in actions_resp
+ if action["volumeRef"] in self.storage_pool_volumes]
+ self.module.fail_json(msg="Failed to add drives to the storage pool possibly because of actions"
+ " in progress. Actions [%s]. Pool id [%s]. Array id [%s]. Error[%s]."
+ % (", ".join(actions), self.pool_detail["id"], self.ssid,
+ to_native(error)))
+
+ self.module.fail_json(msg="Failed to add drives to storage pool. Pool id [%s]. Array id [%s]."
+ " Error[%s]." % (self.pool_detail["id"], self.ssid, to_native(error)))
+
+ # Wait for expansion completion unless it is the last request in the candidate list
+ if required_expansion_candidate_list:
+ for dummy in range(self.EXPANSION_TIMEOUT_SEC):
+ rc, actions_resp = self.request("storage-systems/%s/storage-pools/%s/action-progress"
+ % (self.ssid, self.pool_detail["id"]), ignore_errors=True)
+ if rc == 200:
+ for action in actions_resp:
+ if (action["volumeRef"] in self.storage_pool_volumes and
+ action["currentAction"] == "remappingDce"):
+ sleep(1)
+ estimated_completion_time = action["estimatedTimeToCompletion"]
+ break
+ else:
+ estimated_completion_time = 0.0
+ break
+
+ return changed_required, estimated_completion_time
+
+ def apply(self):
+ """Apply requested state to storage array."""
+ changed = False
+
+ if self.state == "present":
+ if self.criteria_drive_count is None and self.criteria_min_usable_capacity is None:
+ self.module.fail_json(msg="One of criteria_min_usable_capacity or criteria_drive_count must be"
+ " specified.")
+ if self.criteria_drive_count and not self.is_drive_count_valid(self.criteria_drive_count):
+ self.module.fail_json(msg="criteria_drive_count must be valid for the specified raid level.")
+
+ self.pool_detail = self.storage_pool
+ self.module.log(pformat(self.pool_detail))
+
+ if self.state == "present" and self.erase_secured_drives:
+ self.erase_all_available_secured_drives(check_mode=True)
+
+ # Determine whether changes need to be applied to the storage array
+ if self.pool_detail:
+
+ if self.state == "absent":
+ changed = True
+
+ elif self.state == "present":
+
+ if self.criteria_drive_count and self.criteria_drive_count < len(self.storage_pool_drives):
+ self.module.fail_json(msg="Failed to reduce the size of the storage pool. Array [%s]. Pool [%s]."
+ % (self.ssid, self.pool_detail["id"]))
+
+ if self.criteria_drive_type and self.criteria_drive_type != self.pool_detail["driveMediaType"]:
+ self.module.fail_json(msg="Failed! It is not possible to modify storage pool media type."
+ " Array [%s]. Pool [%s]." % (self.ssid, self.pool_detail["id"]))
+
+ if (self.criteria_drive_require_da is not None and self.criteria_drive_require_da !=
+ self.pool_detail["protectionInformationCapabilities"]["protectionInformationCapable"]):
+ self.module.fail_json(msg="Failed! It is not possible to modify DA-capability. Array [%s]."
+ " Pool [%s]." % (self.ssid, self.pool_detail["id"]))
+
+ # Evaluate current storage pool for required change.
+ needs_expansion, estimated_completion_time = self.expand_storage_pool(check_mode=True)
+ if needs_expansion:
+ changed = True
+ if self.migrate_raid_level(check_mode=True):
+ changed = True
+ if self.secure_storage_pool(check_mode=True):
+ changed = True
+ if self.set_reserve_drive_count(check_mode=True):
+ changed = True
+
+ elif self.state == "present":
+ changed = True
+
+ # Apply changes to storage array
+ msg = "No changes were required for the storage pool [%s]."
+ if changed and not self.module.check_mode:
+ if self.state == "present":
+ if self.erase_secured_drives:
+ self.erase_all_available_secured_drives()
+
+ if self.pool_detail:
+ change_list = list()
+
+ # Expansion needs to occur before raid level migration to account for any sizing needs.
+ expanded, estimated_completion_time = self.expand_storage_pool()
+ if expanded:
+ change_list.append("expanded")
+ if self.migrate_raid_level():
+ change_list.append("raid migration")
+ if self.secure_storage_pool():
+ change_list.append("secured")
+ if self.set_reserve_drive_count():
+ change_list.append("adjusted reserve drive count")
+
+ if change_list:
+ msg = "Following changes have been applied to the storage pool [%s]: " + ", ".join(change_list)
+
+ if expanded:
+ msg += "\nThe expansion operation will complete in an estimated %s minutes."\
+ % estimated_completion_time
+ else:
+ self.create_storage_pool()
+ msg = "Storage pool [%s] was created."
+
+ if self.secure_storage_pool():
+ msg = "Storage pool [%s] was created and secured."
+ if self.set_reserve_drive_count():
+ msg += " Adjusted reserve drive count."
+
+ elif self.pool_detail:
+ self.delete_storage_pool()
+ msg = "Storage pool [%s] removed."
+
+ self.pool_detail = self.storage_pool
+ self.module.log(pformat(self.pool_detail))
+ self.module.log(msg % self.name)
+ self.module.exit_json(msg=msg % self.name, changed=changed, **self.pool_detail)
+
+
+def main():
+ storage_pool = NetAppESeriesStoragePool()
+ storage_pool.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_syslog.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_syslog.py
new file mode 100644
index 00000000..76ca6e0c
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_syslog.py
@@ -0,0 +1,248 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_syslog
+short_description: NetApp E-Series manage syslog settings
+description:
+ - Allow the syslog settings to be configured for an individual E-Series storage-system
+author: Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ state:
+ description:
+ - Add or remove the syslog server configuration for E-Series storage array.
+ - Existing syslog server configuration will be removed or updated when its address matches I(address).
+ - Fully qualified hostname that resolve to an IPv4 address that matches I(address) will not be
+ treated as a match.
+ type: str
+ choices:
+ - present
+ - absent
+ default: present
+ required: false
+ address:
+ description:
+ - The syslog server's IPv4 address or a fully qualified hostname.
+ - All existing syslog configurations will be removed when I(state=absent) and I(address=None).
+ type: str
+ required: false
+ port:
+ description:
+ - This is the port the syslog server is using.
+ type: int
+ default: 514
+ required: false
+ protocol:
+ description:
+ - This is the transmission protocol the syslog server's using to receive syslog messages.
+ type: str
+ default: udp
+ choices:
+ - udp
+ - tcp
+ - tls
+ required: false
+ components:
+ description:
+ - The e-series logging components define the specific logs to transfer to the syslog server.
+ - At the time of writing, 'auditLog' is the only logging component but more may become available.
+ type: list
+ default: ["auditLog"]
+ required: false
+ test:
+ description:
+ - This forces a test syslog message to be sent to the stated syslog server.
+ - Only attempts transmission when I(state=present).
+ type: bool
+ default: false
+ required: false
+notes:
+ - Check mode is supported.
+ - This API is currently only supported with the Embedded Web Services API v2.12 (bundled with
+ SANtricity OS 11.40.2) and higher.
+"""
+
+EXAMPLES = """
+ - name: Add two syslog server configurations to NetApp E-Series storage array.
+ na_santricity_syslog:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ state: present
+ address: "{{ item }}"
+ port: 514
+ protocol: tcp
+ component: "auditLog"
+ loop:
+ - "192.168.1.1"
+ - "192.168.1.100"
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The settings have been updated.
+syslog:
+ description:
+ - True if syslog server configuration has been added to e-series storage array.
+ returned: on success
+ sample: True
+ type: bool
+"""
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+
+class NetAppESeriesSyslog(NetAppESeriesModule):
+ def __init__(self):
+ ansible_options = dict(
+ state=dict(choices=["present", "absent"], required=False, default="present"),
+ address=dict(type="str", required=False),
+ port=dict(type="int", default=514, required=False),
+ protocol=dict(choices=["tcp", "tls", "udp"], default="udp", required=False),
+ components=dict(type="list", required=False, default=["auditLog"]),
+ test=dict(type="bool", default=False, require=False))
+
+ required_if = [["state", "present", ["address", "port", "protocol", "components"]]]
+ mutually_exclusive = [["test", "absent"]]
+ super(NetAppESeriesSyslog, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ mutually_exclusive=mutually_exclusive,
+ required_if=required_if,
+ supports_check_mode=True)
+ args = self.module.params
+
+ self.syslog = args["state"] in ["present"]
+ self.address = args["address"]
+ self.port = args["port"]
+ self.protocol = args["protocol"]
+ self.components = args["components"]
+ self.test = args["test"]
+ self.ssid = args["ssid"]
+ self.url = args["api_url"]
+ self.creds = dict(url_password=args["api_password"],
+ validate_certs=args["validate_certs"],
+ url_username=args["api_username"], )
+
+ self.components.sort()
+ self.check_mode = self.module.check_mode
+
+ # Check whether request needs to be forwarded on to the controller web services rest api.
+ self.url_path_prefix = ""
+ if not self.is_embedded() and self.ssid != 0:
+ self.url_path_prefix = "storage-systems/%s/forward/devmgr/v2/" % self.ssid
+
+ def get_configuration(self):
+ """Retrieve existing syslog configuration."""
+ try:
+ rc, result = self.request(self.url_path_prefix + "storage-systems/%s/syslog" % self.ssid)
+ return result
+ except Exception as err:
+ self.module.fail_json(msg="Failed to retrieve syslog configuration! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ def test_configuration(self, body):
+ """Send test syslog message to the storage array.
+
+ Allows fix number of retries to occur before failure is issued to give the storage array time to create
+ new syslog server record.
+ """
+ try:
+ rc, result = self.request(self.url_path_prefix + "storage-systems/%s/syslog/%s/test" % (self.ssid, body["id"]), method='POST')
+ except Exception as err:
+ self.module.fail_json(msg="We failed to send test message! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ def update_configuration(self):
+ """Post the syslog request to array."""
+ config_match = None
+ perfect_match = None
+ update = False
+ body = dict()
+
+ # search existing configuration for syslog server entry match
+ configs = self.get_configuration()
+ if self.address:
+ for config in configs:
+ if config["serverAddress"] == self.address:
+ config_match = config
+ if (config["port"] == self.port and config["protocol"] == self.protocol and
+ len(config["components"]) == len(self.components) and
+ all([component["type"] in self.components for component in config["components"]])):
+ perfect_match = config_match
+ break
+
+ # generate body for the http request
+ if self.syslog:
+ if not perfect_match:
+ update = True
+ if config_match:
+ body.update(dict(id=config_match["id"]))
+ components = [dict(type=component_type) for component_type in self.components]
+ body.update(dict(serverAddress=self.address, port=self.port,
+ protocol=self.protocol, components=components))
+ self.make_configuration_request(body)
+
+ elif config_match:
+
+ # remove specific syslog server configuration
+ if self.address:
+ update = True
+ body.update(dict(id=config_match["id"]))
+ self.make_configuration_request(body)
+
+ # if no address is specified, remove all syslog server configurations
+ elif configs:
+ update = True
+ for config in configs:
+ body.update(dict(id=config["id"]))
+ self.make_configuration_request(body)
+
+ return update
+
+ def make_configuration_request(self, body):
+ # make http request(s)
+ if not self.check_mode:
+ try:
+ if self.syslog:
+ if "id" in body:
+ rc, result = self.request(self.url_path_prefix + "storage-systems/%s/syslog/%s" % (self.ssid, body["id"]),
+ method='POST', data=body)
+ else:
+ rc, result = self.request(self.url_path_prefix + "storage-systems/%s/syslog" % self.ssid, method='POST', data=body)
+ body.update(result)
+
+ # send syslog test message
+ if self.test:
+ self.test_configuration(body)
+
+ elif "id" in body:
+ rc, result = self.request(self.url_path_prefix + "storage-systems/%s/syslog/%s" % (self.ssid, body["id"]), method='DELETE')
+
+ # This is going to catch cases like a connection failure
+ except Exception as err:
+ self.module.fail_json(msg="We failed to modify syslog configuration! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ def update(self):
+ """Update configuration and respond to ansible."""
+ update = self.update_configuration()
+ self.module.exit_json(msg="The syslog settings have been updated.", changed=update)
+
+
+def main():
+ settings = NetAppESeriesSyslog()
+ settings.update()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_volume.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_volume.py
new file mode 100644
index 00000000..d7a37279
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/na_santricity_volume.py
@@ -0,0 +1,896 @@
+#!/usr/bin/python
+
+# (c) 2020, NetApp, Inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: na_santricity_volume
+short_description: NetApp E-Series manage storage volumes (standard and thin)
+description:
+ - Create or remove volumes (standard and thin) for NetApp E/EF-series storage arrays.
+author:
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.santricity_doc
+options:
+ state:
+ description:
+ - Whether the specified volume should exist
+ type: str
+ choices: ["present", "absent"]
+ default: "present"
+ name:
+ description:
+ - The name of the volume to manage.
+ type: str
+ required: true
+ storage_pool_name:
+ description:
+ - Required only when requested I(state=="present").
+ - Name of the storage pool wherein the volume should reside.
+ type: str
+ required: false
+ size_unit:
+ description:
+ - The unit used to interpret the size parameter
+ type: str
+ choices: ["bytes", "b", "kb", "mb", "gb", "tb", "pb", "eb", "zb", "yb"]
+ default: "gb"
+ size:
+ description:
+ - Required only when I(state=="present").
+ - Size of the volume in I(size_unit).
+ - Size of the virtual volume in the case of a thin volume in I(size_unit).
+ - Maximum virtual volume size of a thin provisioned volume is 256tb; however other OS-level restrictions may
+ exist.
+ type: float
+ required: true
+ segment_size_kb:
+ description:
+ - Segment size of the volume
+ - All values are in kibibytes.
+ - Some common choices include 8, 16, 32, 64, 128, 256, and 512 but options are system
+ dependent.
+ - Retrieve the definitive s ystem list from M(netapp_eseries.santricity.na_santricity_facts) under segment_sizes.
+ - When the storage pool is a raidDiskPool then the segment size must be 128kb.
+ - Segment size migrations are not allowed in this module
+ type: int
+ default: 128
+ thin_provision:
+ description:
+ - Whether the volume should be thin provisioned.
+ - Thin volumes can only be created when I(raid_level=="raidDiskPool").
+ - Generally, use of thin-provisioning is not recommended due to performance impacts.
+ type: bool
+ default: false
+ required: false
+ thin_volume_repo_size:
+ description:
+ - This value (in size_unit) sets the allocated space for the thin provisioned repository.
+ - Initial value must between or equal to 4gb and 256gb in increments of 4gb.
+ - During expansion operations the increase must be between or equal to 4gb and 256gb in increments of 4gb.
+ - This option has no effect during expansion if I(thin_volume_expansion_policy=="automatic").
+ - Generally speaking you should almost always use I(thin_volume_expansion_policy=="automatic).
+ type: int
+ required: false
+ thin_volume_max_repo_size:
+ description:
+ - This is the maximum amount the thin volume repository will be allowed to grow.
+ - Only has significance when I(thin_volume_expansion_policy=="automatic").
+ - When the percentage I(thin_volume_repo_size) of I(thin_volume_max_repo_size) exceeds
+ I(thin_volume_growth_alert_threshold) then a warning will be issued and the storage array will execute
+ the I(thin_volume_expansion_policy) policy.
+ - Expansion operations when I(thin_volume_expansion_policy=="automatic") will increase the maximum
+ repository size.
+ - Default will be the same as I(size).
+ type: float
+ required: false
+ thin_volume_expansion_policy:
+ description:
+ - This is the thin volume expansion policy.
+ - When I(thin_volume_expansion_policy=="automatic") and I(thin_volume_growth_alert_threshold) is exceed the
+ I(thin_volume_max_repo_size) will be automatically expanded.
+ - When I(thin_volume_expansion_policy=="manual") and I(thin_volume_growth_alert_threshold) is exceeded the
+ storage system will wait for manual intervention.
+ - The thin volume_expansion policy can not be modified on existing thin volumes in this module.
+ - Generally speaking you should almost always use I(thin_volume_expansion_policy=="automatic).
+ type: str
+ choices: ["automatic", "manual"]
+ default: "automatic"
+ required: false
+ thin_volume_growth_alert_threshold:
+ description:
+ - This is the thin provision repository utilization threshold (in percent).
+ - When the percentage of used storage of the maximum repository size exceeds this value then a alert will
+ be issued and the I(thin_volume_expansion_policy) will be executed.
+ - Values must be between or equal to 10 and 99.
+ type: int
+ default: 95
+ required: false
+ owning_controller:
+ description:
+ - Specifies which controller will be the primary owner of the volume
+ - Not specifying will allow the controller to choose ownership.
+ type: str
+ choices: ["A", "B"]
+ required: false
+ ssd_cache_enabled:
+ description:
+ - Whether an existing SSD cache should be enabled on the volume (fails if no SSD cache defined)
+ - The default value is to ignore existing SSD cache setting.
+ type: bool
+ default: false
+ required: false
+ data_assurance_enabled:
+ description:
+ - Determines whether data assurance (DA) should be enabled for the volume
+ - Only available when creating a new volume and on a storage pool with drives supporting the DA capability.
+ type: bool
+ default: false
+ required: false
+ read_cache_enable:
+ description:
+ - Indicates whether read caching should be enabled for the volume.
+ type: bool
+ default: true
+ required: false
+ read_ahead_enable:
+ description:
+ - Indicates whether or not automatic cache read-ahead is enabled.
+ - This option has no effect on thinly provisioned volumes since the architecture for thin volumes cannot
+ benefit from read ahead caching.
+ type: bool
+ default: true
+ required: false
+ write_cache_enable:
+ description:
+ - Indicates whether write-back caching should be enabled for the volume.
+ type: bool
+ default: true
+ required: false
+ cache_without_batteries:
+ description:
+ - Indicates whether caching should be used without battery backup.
+ - Warning, M(cache_without_batteries==true) and the storage system looses power and there is no battery backup, data will be lost!
+ type: bool
+ default: false
+ required: false
+ workload_name:
+ description:
+ - Label for the workload defined by the metadata.
+ - When I(workload_name) and I(metadata) are specified then the defined workload will be added to the storage
+ array.
+ - When I(workload_name) exists on the storage array but the metadata is different then the workload
+ definition will be updated. (Changes will update all associated volumes!)
+ - Existing workloads can be retrieved using M(netapp_eseries.santricity.na_santricity_facts).
+ type: str
+ required: false
+ workload_metadata:
+ description:
+ - Dictionary containing meta data for the use, user, location, etc of the volume (dictionary is arbitrarily
+ defined for whatever the user deems useful)
+ - When I(workload_name) exists on the storage array but the metadata is different then the workload
+ definition will be updated. (Changes will update all associated volumes!)
+ - I(workload_name) must be specified when I(metadata) are defined.
+ - Dictionary key cannot be longer than 16 characters
+ - Dictionary values cannot be longer than 60 characters
+ type: dict
+ required: false
+ aliases:
+ - metadata
+ volume_metadata:
+ description:
+ - Dictionary containing metadata for the volume itself.
+ - Dictionary key cannot be longer than 14 characters
+ - Dictionary values cannot be longer than 240 characters
+ type: dict
+ required: false
+ wait_for_initialization:
+ description:
+ - Forces the module to wait for expansion operations to complete before continuing.
+ type: bool
+ default: false
+ required: false
+"""
+EXAMPLES = """
+- name: Create simple volume with workload tags (volume meta data)
+ na_santricity_volume:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ state: present
+ name: volume
+ storage_pool_name: storage_pool
+ size: 300
+ size_unit: gb
+ workload_name: volume_tag
+ metadata:
+ key1: value1
+ key2: value2
+
+- name: Create a thin volume
+ na_santricity_volume:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ state: present
+ name: volume1
+ storage_pool_name: storage_pool
+ size: 131072
+ size_unit: gb
+ thin_provision: true
+ thin_volume_repo_size: 32
+ thin_volume_max_repo_size: 1024
+
+- name: Expand thin volume's virtual size
+ na_santricity_volume:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ state: present
+ name: volume1
+ storage_pool_name: storage_pool
+ size: 262144
+ size_unit: gb
+ thin_provision: true
+ thin_volume_repo_size: 32
+ thin_volume_max_repo_size: 1024
+
+- name: Expand thin volume's maximum repository size
+ na_santricity_volume:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ state: present
+ name: volume1
+ storage_pool_name: storage_pool
+ size: 262144
+ size_unit: gb
+ thin_provision: true
+ thin_volume_repo_size: 32
+ thin_volume_max_repo_size: 2048
+
+- name: Delete volume
+ na_santricity_volume:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ state: absent
+ name: volume
+"""
+RETURN = """
+msg:
+ description: State of volume
+ type: str
+ returned: always
+ sample: "Standard volume [workload_vol_1] has been created."
+"""
+
+import time
+
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+
+class NetAppESeriesVolume(NetAppESeriesModule):
+ VOLUME_CREATION_BLOCKING_TIMEOUT_SEC = 300
+ MAXIMUM_VOLUME_METADATA_KEY_LENGTH = 14
+ MAXIMUM_VOLUME_METADATA_VALUE_LENGTH = 240
+ MAXIMUM_VOLUME_METADATA_VALUE_SEGMENT_LENGTH = 60
+
+ def __init__(self):
+ ansible_options = dict(
+ state=dict(choices=["present", "absent"], default="present"),
+ name=dict(required=True, type="str"),
+ storage_pool_name=dict(type="str"),
+ size_unit=dict(default="gb", choices=["bytes", "b", "kb", "mb", "gb", "tb", "pb", "eb", "zb", "yb"], type="str"),
+ size=dict(type="float"),
+ segment_size_kb=dict(type="int", default=128, required=False),
+ owning_controller=dict(type="str", choices=["A", "B"], required=False),
+ ssd_cache_enabled=dict(type="bool", default=False),
+ data_assurance_enabled=dict(type="bool", default=False),
+ thin_provision=dict(type="bool", default=False),
+ thin_volume_repo_size=dict(type="int", required=False),
+ thin_volume_max_repo_size=dict(type="float", required=False),
+ thin_volume_expansion_policy=dict(type="str", choices=["automatic", "manual"], default="automatic", required=False),
+ thin_volume_growth_alert_threshold=dict(type="int", default=95),
+ read_cache_enable=dict(type="bool", default=True),
+ read_ahead_enable=dict(type="bool", default=True),
+ write_cache_enable=dict(type="bool", default=True),
+ cache_without_batteries=dict(type="bool", default=False),
+ workload_name=dict(type="str", required=False),
+ workload_metadata=dict(type="dict", require=False, aliases=["metadata"]),
+ volume_metadata=dict(type="dict", require=False),
+ wait_for_initialization=dict(type="bool", default=False))
+
+ required_if = [
+ ["state", "present", ["storage_pool_name", "size"]],
+ ["thin_provision", "true", ["thin_volume_repo_size"]]
+ ]
+
+ super(NetAppESeriesVolume, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ supports_check_mode=True,
+ required_if=required_if)
+
+ args = self.module.params
+ self.state = args["state"]
+ self.name = args["name"]
+ self.storage_pool_name = args["storage_pool_name"]
+ self.size_unit = args["size_unit"]
+ self.segment_size_kb = args["segment_size_kb"]
+ if args["size"]:
+ self.size_b = self.convert_to_aligned_bytes(args["size"])
+
+ self.owning_controller_id = None
+ if args["owning_controller"]:
+ self.owning_controller_id = "070000000000000000000001" if args["owning_controller"] == "A" else "070000000000000000000002"
+
+ self.read_cache_enable = args["read_cache_enable"]
+ self.read_ahead_enable = args["read_ahead_enable"]
+ self.write_cache_enable = args["write_cache_enable"]
+ self.ssd_cache_enabled = args["ssd_cache_enabled"]
+ self.cache_without_batteries = args["cache_without_batteries"]
+ self.data_assurance_enabled = args["data_assurance_enabled"]
+
+ self.thin_provision = args["thin_provision"]
+ self.thin_volume_expansion_policy = args["thin_volume_expansion_policy"]
+ self.thin_volume_growth_alert_threshold = int(args["thin_volume_growth_alert_threshold"])
+ self.thin_volume_repo_size_b = None
+ self.thin_volume_max_repo_size_b = None
+
+ if args["thin_volume_repo_size"]:
+ self.thin_volume_repo_size_b = self.convert_to_aligned_bytes(args["thin_volume_repo_size"])
+ if args["thin_volume_max_repo_size"]:
+ self.thin_volume_max_repo_size_b = self.convert_to_aligned_bytes(args["thin_volume_max_repo_size"])
+
+ self.workload_name = args["workload_name"]
+ self.wait_for_initialization = args["wait_for_initialization"]
+
+ # convert metadata to a list of dictionaries containing the keys "key" and "value" corresponding to
+ # each of the workload attributes dictionary entries
+ self.metadata = []
+ if self.state == "present" and args["workload_metadata"]:
+ if not self.workload_name:
+ self.module.fail_json(msg="When metadata is specified then the name for the workload must be specified. Array [%s]." % self.ssid)
+
+ for key, value in args["workload_metadata"].items():
+ self.metadata.append({"key": key, "value": value})
+
+ self.volume_metadata = []
+ if self.state == "present" and args["volume_metadata"]:
+ for key, value in args["volume_metadata"].items():
+ key, value = str(key), str(value)
+
+ if len(key) > self.MAXIMUM_VOLUME_METADATA_KEY_LENGTH:
+ self.module.fail_json(msg="Volume metadata keys must be less than %s characters long. Array [%s]."
+ % (str(self.MAXIMUM_VOLUME_METADATA_KEY_LENGTH), self.ssid))
+
+ if len(value) > self.MAXIMUM_VOLUME_METADATA_VALUE_LENGTH:
+ self.module.fail_json(msg="Volume metadata values must be less than %s characters long. Array [%s]."
+ % (str(self.MAXIMUM_VOLUME_METADATA_VALUE_LENGTH), self.ssid))
+
+ if value:
+ for index, start in enumerate(range(0, len(value), self.MAXIMUM_VOLUME_METADATA_VALUE_SEGMENT_LENGTH)):
+ if len(value) > start + self.MAXIMUM_VOLUME_METADATA_VALUE_SEGMENT_LENGTH:
+ self.volume_metadata.append({"key": "%s~%s" % (key, str(index)), "value": value[start:start + self.MAXIMUM_VOLUME_METADATA_VALUE_SEGMENT_LENGTH]})
+ else:
+ self.volume_metadata.append({"key": "%s~%s" % (key, str(index)), "value": value[start:len(value)]})
+ else:
+ self.volume_metadata.append({"key": "%s~0" % key, "value": ""})
+
+ if self.state == "present" and self.thin_provision:
+ if not self.thin_volume_max_repo_size_b:
+ self.thin_volume_max_repo_size_b = self.size_b
+
+ if not self.thin_volume_expansion_policy:
+ self.thin_volume_expansion_policy = "automatic"
+
+ if self.size_b > 256 * 1024 ** 4:
+ self.module.fail_json(msg="Thin provisioned volumes must be less than or equal to 256tb is size."
+ " Attempted size [%sg]" % (self.size_b * 1024 ** 3))
+
+ if (self.thin_volume_repo_size_b and self.thin_volume_max_repo_size_b and
+ self.thin_volume_repo_size_b > self.thin_volume_max_repo_size_b):
+ self.module.fail_json(msg="The initial size of the thin volume must not be larger than the maximum"
+ " repository size. Array [%s]." % self.ssid)
+
+ if self.thin_volume_growth_alert_threshold < 10 or self.thin_volume_growth_alert_threshold > 99:
+ self.module.fail_json(msg="thin_volume_growth_alert_threshold must be between or equal to 10 and 99."
+ "thin_volume_growth_alert_threshold [%s]. Array [%s]."
+ % (self.thin_volume_growth_alert_threshold, self.ssid))
+
+ self.volume_detail = None
+ self.pool_detail = None
+ self.workload_id = None
+
+ def convert_to_aligned_bytes(self, size):
+ """Convert size to the truncated byte size that aligns on the segment size."""
+ size_bytes = int(size * self.SIZE_UNIT_MAP[self.size_unit])
+ segment_size_bytes = int(self.segment_size_kb * self.SIZE_UNIT_MAP["kb"])
+ segment_count = int(size_bytes / segment_size_bytes)
+ return segment_count * segment_size_bytes
+
+ def get_volume(self):
+ """Retrieve volume details from storage array."""
+ volumes = list()
+ thin_volumes = list()
+ try:
+ rc, volumes = self.request("storage-systems/%s/volumes" % self.ssid)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to obtain list of thick volumes. Array Id [%s]. Error[%s]."
+ % (self.ssid, to_native(err)))
+ try:
+ rc, thin_volumes = self.request("storage-systems/%s/thin-volumes" % self.ssid)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to obtain list of thin volumes. Array Id [%s]. Error[%s]."
+ % (self.ssid, to_native(err)))
+
+ volume_detail = [volume for volume in volumes + thin_volumes if volume["name"] == self.name]
+ return volume_detail[0] if volume_detail else dict()
+
+ def wait_for_volume_availability(self, retries=VOLUME_CREATION_BLOCKING_TIMEOUT_SEC / 5):
+ """Waits until volume becomes available.
+
+ :raises AnsibleFailJson when retries are exhausted.
+ """
+ if retries == 0:
+ self.module.fail_json(msg="Timed out waiting for the volume %s to become available. Array [%s]."
+ % (self.name, self.ssid))
+ if not self.get_volume():
+ time.sleep(5)
+ self.wait_for_volume_availability(retries=retries - 1)
+
+ def wait_for_volume_action(self, timeout=None):
+ """Waits until volume action is complete is complete.
+ :param: int timeout: Wait duration measured in seconds. Waits indefinitely when None.
+ """
+ action = "unknown"
+ percent_complete = None
+ while action != "complete":
+ time.sleep(5)
+
+ try:
+ rc, operations = self.request("storage-systems/%s/symbol/getLongLivedOpsProgress" % self.ssid)
+
+ # Search long lived operations for volume
+ action = "complete"
+ for operation in operations["longLivedOpsProgress"]:
+ if operation["volAction"] is not None:
+ for key in operation.keys():
+ if (operation[key] is not None and "volumeRef" in operation[key] and
+ (operation[key]["volumeRef"] == self.volume_detail["id"] or
+ ("storageVolumeRef" in self.volume_detail and operation[key]["volumeRef"] == self.volume_detail["storageVolumeRef"]))):
+ action = operation["volAction"]
+ percent_complete = operation["init"]["percentComplete"]
+ except Exception as err:
+ self.module.fail_json(msg="Failed to get volume expansion progress. Volume [%s]. Array Id [%s]."
+ " Error[%s]." % (self.name, self.ssid, to_native(err)))
+
+ if timeout is not None:
+ if timeout <= 0:
+ self.module.warn("Expansion action, %s, failed to complete during the allotted time. Time remaining"
+ " [%s]. Array Id [%s]." % (action, percent_complete, self.ssid))
+ self.module.fail_json(msg="Expansion action failed to complete. Time remaining [%s]. Array Id [%s]." % (percent_complete, self.ssid))
+ if timeout:
+ timeout -= 5
+
+ self.module.log("Expansion action, %s, is %s complete." % (action, percent_complete))
+ self.module.log("Expansion action is complete.")
+
+ def get_storage_pool(self):
+ """Retrieve storage pool details from the storage array."""
+ storage_pools = list()
+ try:
+ rc, storage_pools = self.request("storage-systems/%s/storage-pools" % self.ssid)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to obtain list of storage pools. Array Id [%s]. Error[%s]."
+ % (self.ssid, to_native(err)))
+
+ pool_detail = [storage_pool for storage_pool in storage_pools if storage_pool["name"] == self.storage_pool_name]
+ return pool_detail[0] if pool_detail else dict()
+
+ def check_storage_pool_sufficiency(self):
+ """Perform a series of checks as to the sufficiency of the storage pool for the volume."""
+ if not self.pool_detail:
+ self.module.fail_json(msg='Requested storage pool (%s) not found' % self.storage_pool_name)
+
+ if not self.volume_detail:
+ if self.thin_provision and not self.pool_detail['diskPool']:
+ self.module.fail_json(msg='Thin provisioned volumes can only be created on raid disk pools.')
+
+ if (self.data_assurance_enabled and not
+ (self.pool_detail["protectionInformationCapabilities"]["protectionInformationCapable"] and
+ self.pool_detail["protectionInformationCapabilities"]["protectionType"] == "type2Protection")):
+ self.module.fail_json(msg="Data Assurance (DA) requires the storage pool to be DA-compatible."
+ " Array [%s]." % self.ssid)
+
+ if int(self.pool_detail["freeSpace"]) < self.size_b and not self.thin_provision:
+ self.module.fail_json(msg="Not enough storage pool free space available for the volume's needs."
+ " Array [%s]." % self.ssid)
+ else:
+ # Check for expansion
+ if (int(self.pool_detail["freeSpace"]) < int(self.volume_detail["totalSizeInBytes"]) - self.size_b and
+ not self.thin_provision):
+ self.module.fail_json(msg="Not enough storage pool free space available for the volume's needs."
+ " Array [%s]." % self.ssid)
+
+ def update_workload_tags(self, check_mode=False):
+ """Check the status of the workload tag and update storage array definitions if necessary.
+
+ When the workload attributes are not provided but an existing workload tag name is, then the attributes will be
+ used.
+
+ :return bool: Whether changes were required to be made."""
+ change_required = False
+ workload_tags = None
+ request_body = None
+ ansible_profile_id = None
+
+ if self.workload_name:
+ try:
+ rc, workload_tags = self.request("storage-systems/%s/workloads" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve storage array workload tags. Array [%s]" % self.ssid)
+
+ ansible_profile_id = "Other_1"
+ request_body = dict(name=self.workload_name,
+ profileId=ansible_profile_id,
+ workloadInstanceIndex=None,
+ isValid=True)
+
+ # evaluate and update storage array when needed
+ for tag in workload_tags:
+ if tag["name"] == self.workload_name:
+ self.workload_id = tag["id"]
+
+ if not self.metadata:
+ break
+
+ # Determine if core attributes (everything but profileId) is the same
+ metadata_set = set(tuple(sorted(attr.items())) for attr in self.metadata)
+ tag_set = set(tuple(sorted(attr.items()))
+ for attr in tag["workloadAttributes"] if attr["key"] != "profileId")
+ if metadata_set != tag_set:
+ self.module.log("Workload tag change is required!")
+ change_required = True
+
+ # only perform the required action when check_mode==False
+ if change_required and not check_mode:
+ self.metadata.append(dict(key="profileId", value=ansible_profile_id))
+ request_body.update(dict(isNewWorkloadInstance=False,
+ isWorkloadDataInitialized=True,
+ isWorkloadCardDataToBeReset=True,
+ workloadAttributes=self.metadata))
+ try:
+ rc, resp = self.request("storage-systems/%s/workloads/%s" % (self.ssid, tag["id"]),
+ data=request_body, method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to create new workload tag. Array [%s]. Error [%s]"
+ % (self.ssid, to_native(error)))
+ self.module.log("Workload tag [%s] required change." % self.workload_name)
+ break
+
+ # existing workload tag not found so create new workload tag
+ else:
+ change_required = True
+ self.module.log("Workload tag creation is required!")
+
+ if change_required and not check_mode:
+ if self.metadata:
+ self.metadata.append(dict(key="profileId", value=ansible_profile_id))
+ else:
+ self.metadata = [dict(key="profileId", value=ansible_profile_id)]
+
+ request_body.update(dict(isNewWorkloadInstance=True,
+ isWorkloadDataInitialized=False,
+ isWorkloadCardDataToBeReset=False,
+ workloadAttributes=self.metadata))
+ try:
+ rc, resp = self.request("storage-systems/%s/workloads" % self.ssid,
+ method="POST", data=request_body)
+ self.workload_id = resp["id"]
+ except Exception as error:
+ self.module.fail_json(msg="Failed to create new workload tag. Array [%s]. Error [%s]"
+ % (self.ssid, to_native(error)))
+ self.module.log("Workload tag [%s] was added." % self.workload_name)
+
+ return change_required
+
+ def get_volume_property_changes(self):
+ """Retrieve the volume update request body when change(s) are required.
+
+ :raise AnsibleFailJson when attempting to change segment size on existing volume.
+ :return dict: request body when change(s) to a volume's properties are required.
+ """
+ change = False
+ request_body = dict(flashCache=self.ssd_cache_enabled, metaTags=[],
+ cacheSettings=dict(readCacheEnable=self.read_cache_enable,
+ writeCacheEnable=self.write_cache_enable))
+
+ # check for invalid modifications
+ if self.segment_size_kb * 1024 != int(self.volume_detail["segmentSize"]):
+ self.module.fail_json(msg="Existing volume segment size is %s and cannot be modified."
+ % self.volume_detail["segmentSize"])
+
+ # common thick/thin volume properties
+ if (self.read_cache_enable != self.volume_detail["cacheSettings"]["readCacheEnable"] or
+ self.write_cache_enable != self.volume_detail["cacheSettings"]["writeCacheEnable"] or
+ self.ssd_cache_enabled != self.volume_detail["flashCached"]):
+ change = True
+
+ # controller ownership
+ if self.owning_controller_id and self.owning_controller_id != self.volume_detail["preferredManager"]:
+ change = True
+ request_body.update(dict(owningControllerId=self.owning_controller_id))
+
+ # volume meta tags
+ request_body["metaTags"].extend(self.volume_metadata)
+ for entry in self.volume_metadata:
+ if entry not in self.volume_detail["metadata"]:
+ change = True
+
+ if self.workload_name:
+ request_body["metaTags"].extend([{"key": "workloadId", "value": self.workload_id},
+ {"key": "volumeTypeId", "value": "volume"}])
+
+ if ({"key": "workloadId", "value": self.workload_id} not in self.volume_detail["metadata"] or
+ {"key": "volumeTypeId", "value": "volume"} not in self.volume_detail["metadata"]):
+ change = True
+
+ if len(self.volume_detail["metadata"]) != len(request_body["metaTags"]):
+ change = True
+
+ # thick/thin volume specific properties
+ if self.thin_provision:
+ if self.thin_volume_growth_alert_threshold != int(self.volume_detail["growthAlertThreshold"]):
+ change = True
+ request_body.update(dict(growthAlertThreshold=self.thin_volume_growth_alert_threshold))
+ if self.thin_volume_expansion_policy != self.volume_detail["expansionPolicy"]:
+ change = True
+ request_body.update(dict(expansionPolicy=self.thin_volume_expansion_policy))
+ else:
+ if self.read_ahead_enable != (int(self.volume_detail["cacheSettings"]["readAheadMultiplier"]) > 0):
+ change = True
+ request_body["cacheSettings"].update(dict(readAheadEnable=self.read_ahead_enable))
+ if self.cache_without_batteries != self.volume_detail["cacheSettings"]["cwob"]:
+ change = True
+ request_body["cacheSettings"].update(dict(cacheWithoutBatteries=self.cache_without_batteries))
+
+ return request_body if change else dict()
+
+ def get_expand_volume_changes(self):
+ """Expand the storage specifications for the existing thick/thin volume.
+
+ :raise AnsibleFailJson when a thick/thin volume expansion request fails.
+ :return dict: dictionary containing all the necessary values for volume expansion request
+ """
+ request_body = dict()
+
+ if self.size_b < int(self.volume_detail["capacity"]):
+ self.module.fail_json(msg="Reducing the size of volumes is not permitted. Volume [%s]. Array [%s]"
+ % (self.name, self.ssid))
+
+ if self.volume_detail["thinProvisioned"]:
+ if self.size_b > int(self.volume_detail["capacity"]):
+ request_body.update(dict(sizeUnit="bytes", newVirtualSize=self.size_b))
+ self.module.log("Thin volume virtual size have been expanded.")
+
+ if self.volume_detail["expansionPolicy"] == "automatic":
+ if self.thin_volume_max_repo_size_b > int(self.volume_detail["provisionedCapacityQuota"]):
+ request_body.update(dict(sizeUnit="bytes", newRepositorySize=self.thin_volume_max_repo_size_b))
+ self.module.log("Thin volume maximum repository size have been expanded (automatic policy).")
+
+ elif self.volume_detail["expansionPolicy"] == "manual":
+ if self.thin_volume_repo_size_b > int(self.volume_detail["currentProvisionedCapacity"]):
+ change = self.thin_volume_repo_size_b - int(self.volume_detail["currentProvisionedCapacity"])
+ if change < 4 * 1024 ** 3 or change > 256 * 1024 ** 3 or change % (4 * 1024 ** 3) != 0:
+ self.module.fail_json(msg="The thin volume repository increase must be between or equal to 4gb"
+ " and 256gb in increments of 4gb. Attempted size [%sg]."
+ % (self.thin_volume_repo_size_b * 1024 ** 3))
+
+ request_body.update(dict(sizeUnit="bytes", newRepositorySize=self.thin_volume_repo_size_b))
+ self.module.log("Thin volume maximum repository size have been expanded (manual policy).")
+
+ elif self.size_b > int(self.volume_detail["capacity"]):
+ request_body.update(dict(sizeUnit="bytes", expansionSize=self.size_b))
+ self.module.log("Volume storage capacities have been expanded.")
+
+ return request_body
+
+ def create_volume(self):
+ """Create thick/thin volume according to the specified criteria."""
+ body = dict(name=self.name, poolId=self.pool_detail["id"], sizeUnit="bytes",
+ dataAssuranceEnabled=self.data_assurance_enabled)
+
+ if self.volume_metadata:
+ body.update({"metaTags": self.volume_metadata})
+
+ if self.thin_provision:
+ body.update(dict(virtualSize=self.size_b,
+ repositorySize=self.thin_volume_repo_size_b,
+ maximumRepositorySize=self.thin_volume_max_repo_size_b,
+ expansionPolicy=self.thin_volume_expansion_policy,
+ growthAlertThreshold=self.thin_volume_growth_alert_threshold))
+ try:
+ rc, volume = self.request("storage-systems/%s/thin-volumes" % self.ssid, data=body, method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to create thin volume. Volume [%s]. Array Id [%s]. Error[%s]."
+ % (self.name, self.ssid, to_native(error)))
+
+ self.module.log("New thin volume created [%s]." % self.name)
+
+ else:
+ body.update(dict(size=self.size_b, segSize=self.segment_size_kb))
+ try:
+ rc, volume = self.request("storage-systems/%s/volumes" % self.ssid, data=body, method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to create volume. Volume [%s]. Array Id [%s]. Error[%s]."
+ % (self.name, self.ssid, to_native(error)))
+
+ self.module.log("New volume created [%s]." % self.name)
+
+ def update_volume_properties(self):
+ """Update existing thin-volume or volume properties.
+
+ :raise AnsibleFailJson when either thick/thin volume update request fails.
+ :return bool: whether update was applied
+ """
+ self.wait_for_volume_availability()
+ self.volume_detail = self.get_volume()
+
+ request_body = self.get_volume_property_changes()
+
+ if request_body:
+ if self.thin_provision:
+ try:
+ rc, resp = self.request("storage-systems/%s/thin-volumes/%s"
+ % (self.ssid, self.volume_detail["id"]), data=request_body, method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to update thin volume properties. Volume [%s]. Array Id [%s]."
+ " Error[%s]." % (self.name, self.ssid, to_native(error)))
+ else:
+ try:
+ rc, resp = self.request("storage-systems/%s/volumes/%s" % (self.ssid, self.volume_detail["id"]),
+ data=request_body, method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to update volume properties. Volume [%s]. Array Id [%s]."
+ " Error[%s]." % (self.name, self.ssid, to_native(error)))
+ return True
+ return False
+
+ def expand_volume(self):
+ """Expand the storage specifications for the existing thick/thin volume.
+
+ :raise AnsibleFailJson when a thick/thin volume expansion request fails.
+ """
+ request_body = self.get_expand_volume_changes()
+ if request_body:
+ if self.volume_detail["thinProvisioned"]:
+ try:
+ rc, resp = self.request("storage-systems/%s/thin-volumes/%s/expand"
+ % (self.ssid, self.volume_detail["id"]), data=request_body, method="POST")
+ except Exception as err:
+ self.module.fail_json(msg="Failed to expand thin volume. Volume [%s]. Array Id [%s]. Error[%s]."
+ % (self.name, self.ssid, to_native(err)))
+ self.module.log("Thin volume specifications have been expanded.")
+
+ else:
+ try:
+ rc, resp = self.request(
+ "storage-systems/%s/volumes/%s/expand" % (self.ssid, self.volume_detail['id']),
+ data=request_body, method="POST")
+ except Exception as err:
+ self.module.fail_json(msg="Failed to expand volume. Volume [%s]. Array Id [%s]. Error[%s]."
+ % (self.name, self.ssid, to_native(err)))
+
+ self.module.log("Volume storage capacities have been expanded.")
+
+ def delete_volume(self):
+ """Delete existing thin/thick volume."""
+ if self.thin_provision:
+ try:
+ rc, resp = self.request("storage-systems/%s/thin-volumes/%s" % (self.ssid, self.volume_detail["id"]),
+ method="DELETE")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to delete thin volume. Volume [%s]. Array Id [%s]. Error[%s]."
+ % (self.name, self.ssid, to_native(error)))
+ self.module.log("Thin volume deleted [%s]." % self.name)
+ else:
+ try:
+ rc, resp = self.request("storage-systems/%s/volumes/%s" % (self.ssid, self.volume_detail["id"]),
+ method="DELETE")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to delete volume. Volume [%s]. Array Id [%s]. Error[%s]."
+ % (self.name, self.ssid, to_native(error)))
+ self.module.log("Volume deleted [%s]." % self.name)
+
+ def apply(self):
+ """Determine and apply any changes necessary to satisfy the specified criteria.
+
+ :raise AnsibleExitJson when completes successfully"""
+ change = False
+ msg = None
+
+ self.volume_detail = self.get_volume()
+ self.pool_detail = self.get_storage_pool()
+
+ # Determine whether changes need to be applied to existing workload tags
+ if self.state == 'present' and self.update_workload_tags(check_mode=True):
+ change = True
+
+ # Determine if any changes need to be applied
+ if self.volume_detail:
+ if self.state == 'absent':
+ change = True
+
+ elif self.state == 'present':
+ if self.get_expand_volume_changes() or self.get_volume_property_changes():
+ change = True
+
+ elif self.state == 'present':
+ if self.thin_provision and (self.thin_volume_repo_size_b < 4 * 1024 ** 3 or
+ self.thin_volume_repo_size_b > 256 * 1024 ** 3 or
+ self.thin_volume_repo_size_b % (4 * 1024 ** 3) != 0):
+ self.module.fail_json(msg="The initial thin volume repository size must be between 4gb and 256gb in"
+ " increments of 4gb. Attempted size [%sg]."
+ % (self.thin_volume_repo_size_b * 1024 ** 3))
+ change = True
+
+ self.module.log("Update required: [%s]." % change)
+
+ # Apply any necessary changes
+ if change and not self.module.check_mode:
+ if self.state == 'present':
+ if self.update_workload_tags():
+ msg = "Workload tag change occurred."
+
+ if not self.volume_detail:
+ self.check_storage_pool_sufficiency()
+ self.create_volume()
+ self.update_volume_properties()
+ msg = msg[:-1] + " and volume [%s] was created." if msg else "Volume [%s] has been created."
+ else:
+ if self.update_volume_properties():
+ msg = "Volume [%s] properties were updated."
+
+ if self.get_expand_volume_changes():
+ self.expand_volume()
+ msg = msg[:-1] + " and was expanded." if msg else "Volume [%s] was expanded."
+
+ if self.wait_for_initialization:
+ self.module.log("Waiting for volume operation to complete.")
+ self.wait_for_volume_action()
+
+ elif self.state == 'absent':
+ self.delete_volume()
+ msg = "Volume [%s] has been deleted."
+
+ else:
+ msg = "Volume [%s] does not exist." if self.state == 'absent' else "Volume [%s] exists."
+
+ self.module.exit_json(msg=(msg % self.name if msg and "%s" in msg else msg), changed=change)
+
+
+def main():
+ volume = NetAppESeriesVolume()
+ volume.apply()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_alerts.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_alerts.py
new file mode 100644
index 00000000..cea2107f
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_alerts.py
@@ -0,0 +1,286 @@
+#!/usr/bin/python
+
+# (c) 2018, 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
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = """
+---
+module: netapp_e_alerts
+short_description: NetApp E-Series manage email notification settings
+description:
+ - Certain E-Series systems have the capability to send email notifications on potentially critical events.
+ - This module will allow the owner of the system to specify email recipients for these messages.
+version_added: '2.7'
+author: Michael Price (@lmprice)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ state:
+ description:
+ - Enable/disable the sending of email-based alerts.
+ default: enabled
+ required: false
+ type: str
+ choices:
+ - enabled
+ - disabled
+ server:
+ description:
+ - A fully qualified domain name, IPv4 address, or IPv6 address of a mail server.
+ - To use a fully qualified domain name, you must configure a DNS server on both controllers using
+ M(netapp_eseries.santricity.netapp_e_mgmt_interface).
+ - Required when I(state=enabled).
+ type: str
+ required: no
+ sender:
+ description:
+ - This is the sender that the recipient will see. It doesn't necessarily need to be a valid email account.
+ - Required when I(state=enabled).
+ type: str
+ required: no
+ contact:
+ description:
+ - Allows the owner to specify some free-form contact information to be included in the emails.
+ - This is typically utilized to provide a contact phone number.
+ type: str
+ required: no
+ recipients:
+ description:
+ - The email addresses that will receive the email notifications.
+ - Required when I(state=enabled).
+ type: list
+ required: no
+ test:
+ description:
+ - When a change is detected in the configuration, a test email will be sent.
+ - This may take a few minutes to process.
+ - Only applicable if I(state=enabled).
+ default: no
+ type: bool
+ log_path:
+ description:
+ - Path to a file on the Ansible control node to be used for debug logging
+ type: str
+ required: no
+notes:
+ - Check mode is supported.
+ - Alertable messages are a subset of messages shown by the Major Event Log (MEL), of the storage-system. Examples
+ of alertable messages include drive failures, failed controllers, loss of redundancy, and other warning/critical
+ events.
+ - This API is currently only supported with the Embedded Web Services API v2.0 and higher.
+"""
+
+EXAMPLES = """
+ - name: Enable email-based alerting
+ netapp_e_alerts:
+ state: enabled
+ sender: noreply@example.com
+ server: mail@example.com
+ contact: "Phone: 1-555-555-5555"
+ recipients:
+ - name1@example.com
+ - name2@example.com
+ api_url: "10.1.1.1:8443"
+ api_username: "admin"
+ api_password: "myPass"
+
+ - name: Disable alerting
+ netapp_e_alerts:
+ state: disabled
+ api_url: "10.1.1.1:8443"
+ api_username: "admin"
+ api_password: "myPass"
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The settings have been updated.
+"""
+
+import json
+import logging
+from pprint import pformat
+import re
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import request, eseries_host_argument_spec
+from ansible.module_utils._text import to_native
+
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+}
+
+
+class Alerts(object):
+ def __init__(self):
+ argument_spec = eseries_host_argument_spec()
+ argument_spec.update(dict(
+ state=dict(type='str', required=False, default='enabled',
+ choices=['enabled', 'disabled']),
+ server=dict(type='str', required=False, ),
+ sender=dict(type='str', required=False, ),
+ contact=dict(type='str', required=False, ),
+ recipients=dict(type='list', required=False, ),
+ test=dict(type='bool', required=False, default=False, ),
+ log_path=dict(type='str', required=False),
+ ))
+
+ required_if = [
+ ['state', 'enabled', ['server', 'sender', 'recipients']]
+ ]
+
+ self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if)
+ args = self.module.params
+ self.alerts = args['state'] == 'enabled'
+ self.server = args['server']
+ self.sender = args['sender']
+ self.contact = args['contact']
+ self.recipients = args['recipients']
+ self.test = args['test']
+
+ self.ssid = args['ssid']
+ self.url = args['api_url']
+ self.creds = dict(url_password=args['api_password'],
+ validate_certs=args['validate_certs'],
+ url_username=args['api_username'], )
+
+ self.check_mode = self.module.check_mode
+
+ log_path = args['log_path']
+
+ # logging setup
+ self._logger = logging.getLogger(self.__class__.__name__)
+
+ if log_path:
+ logging.basicConfig(
+ level=logging.DEBUG, filename=log_path, filemode='w',
+ format='%(relativeCreated)dms %(levelname)s %(module)s.%(funcName)s:%(lineno)d\n %(message)s')
+
+ if not self.url.endswith('/'):
+ self.url += '/'
+
+ # Very basic validation on email addresses: xx@yy.zz
+ email = re.compile(r"[^@]+@[^@]+\.[^@]+")
+
+ if self.sender and not email.match(self.sender):
+ self.module.fail_json(msg="The sender (%s) provided is not a valid email address." % self.sender)
+
+ if self.recipients is not None:
+ for recipient in self.recipients:
+ if not email.match(recipient):
+ self.module.fail_json(msg="The recipient (%s) provided is not a valid email address." % recipient)
+
+ if len(self.recipients) < 1:
+ self.module.fail_json(msg="At least one recipient address must be specified.")
+
+ def get_configuration(self):
+ try:
+ (rc, result) = request(self.url + 'storage-systems/%s/device-alerts' % self.ssid, headers=HEADERS,
+ **self.creds)
+ self._logger.info("Current config: %s", pformat(result))
+ return result
+
+ except Exception as err:
+ self.module.fail_json(msg="Failed to retrieve the alerts configuration! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ def update_configuration(self):
+ config = self.get_configuration()
+ update = False
+ body = dict()
+
+ if self.alerts:
+ body = dict(alertingEnabled=True)
+ if not config['alertingEnabled']:
+ update = True
+
+ body.update(emailServerAddress=self.server)
+ if config['emailServerAddress'] != self.server:
+ update = True
+
+ body.update(additionalContactInformation=self.contact, sendAdditionalContactInformation=True)
+ if self.contact and (self.contact != config['additionalContactInformation']
+ or not config['sendAdditionalContactInformation']):
+ update = True
+
+ body.update(emailSenderAddress=self.sender)
+ if config['emailSenderAddress'] != self.sender:
+ update = True
+
+ self.recipients.sort()
+ if config['recipientEmailAddresses']:
+ config['recipientEmailAddresses'].sort()
+
+ body.update(recipientEmailAddresses=self.recipients)
+ if config['recipientEmailAddresses'] != self.recipients:
+ update = True
+
+ elif config['alertingEnabled']:
+ body = dict(alertingEnabled=False)
+ update = True
+
+ self._logger.debug(pformat(body))
+
+ if update and not self.check_mode:
+ try:
+ (rc, result) = request(self.url + 'storage-systems/%s/device-alerts' % self.ssid, method='POST',
+ data=json.dumps(body), headers=HEADERS, **self.creds)
+ # This is going to catch cases like a connection failure
+ except Exception as err:
+ self.module.fail_json(msg="We failed to set the storage-system name! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+ return update
+
+ def send_test_email(self):
+ """Send a test email to verify that the provided configuration is valid and functional."""
+ if not self.check_mode:
+ try:
+ (rc, result) = request(self.url + 'storage-systems/%s/device-alerts/alert-email-test' % self.ssid,
+ timeout=300, method='POST', headers=HEADERS, **self.creds)
+
+ if result['response'] != 'emailSentOK':
+ self.module.fail_json(msg="The test email failed with status=[%s]! Array Id [%s]."
+ % (result['response'], self.ssid))
+
+ # This is going to catch cases like a connection failure
+ except Exception as err:
+ self.module.fail_json(msg="We failed to send the test email! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ def update(self):
+ update = self.update_configuration()
+
+ if self.test and update:
+ self._logger.info("An update was detected and test=True, running a test.")
+ self.send_test_email()
+
+ if self.alerts:
+ msg = 'Alerting has been enabled using server=%s, sender=%s.' % (self.server, self.sender)
+ else:
+ msg = 'Alerting has been disabled.'
+
+ self.module.exit_json(msg=msg, changed=update, )
+
+ def __call__(self, *args, **kwargs):
+ self.update()
+
+
+def main():
+ alerts = Alerts()
+ alerts()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_amg.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_amg.py
new file mode 100644
index 00000000..e2bfa419
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_amg.py
@@ -0,0 +1,268 @@
+#!/usr/bin/python
+# (c) 2016, 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
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+
+DOCUMENTATION = """
+---
+module: netapp_e_amg
+short_description: NetApp E-Series create, remove, and update asynchronous mirror groups
+description:
+ - Allows for the creation, removal and updating of Asynchronous Mirror Groups for NetApp E-series storage arrays
+version_added: '2.2'
+author: Kevin Hulquest (@hulquest)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ name:
+ description:
+ - The name of the async array you wish to target, or create.
+ - If C(state) is present and the name isn't found, it will attempt to create.
+ type: str
+ required: yes
+ new_name:
+ description:
+ - New async array name
+ type: str
+ required: no
+ secondaryArrayId:
+ description:
+ - The ID of the secondary array to be used in mirroring process
+ type: str
+ required: yes
+ syncIntervalMinutes:
+ description:
+ - The synchronization interval in minutes
+ type: int
+ default: 10
+ manualSync:
+ description:
+ - Setting this to true will cause other synchronization values to be ignored
+ type: bool
+ default: 'no'
+ recoveryWarnThresholdMinutes:
+ description:
+ - Recovery point warning threshold (minutes). The user will be warned when the age of the last good failures point exceeds this value
+ type: int
+ default: 20
+ repoUtilizationWarnThreshold:
+ description:
+ - Recovery point warning threshold
+ type: int
+ default: 80
+ interfaceType:
+ description:
+ - The intended protocol to use if both Fibre and iSCSI are available.
+ type: str
+ choices:
+ - iscsi
+ - fibre
+ syncWarnThresholdMinutes:
+ description:
+ - The threshold (in minutes) for notifying the user that periodic synchronization has taken too long to complete.
+ default: 10
+ type: int
+ state:
+ description:
+ - A C(state) of present will either create or update the async mirror group.
+ - A C(state) of absent will remove the async mirror group.
+ type: str
+ choices: [ absent, present ]
+ required: yes
+"""
+
+EXAMPLES = """
+ - name: AMG removal
+ na_eseries_amg:
+ state: absent
+ ssid: "{{ ssid }}"
+ secondaryArrayId: "{{amg_secondaryArrayId}}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ new_name: "{{amg_array_name}}"
+ name: "{{amg_name}}"
+ when: amg_create
+
+ - name: AMG create
+ netapp_e_amg:
+ state: present
+ ssid: "{{ ssid }}"
+ secondaryArrayId: "{{amg_secondaryArrayId}}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ new_name: "{{amg_array_name}}"
+ name: "{{amg_name}}"
+ when: amg_create
+"""
+
+RETURN = """
+msg:
+ description: Successful creation
+ returned: success
+ type: str
+ sample: '{"changed": true, "connectionType": "fc", "groupRef": "3700000060080E5000299C24000006E857AC7EEC", "groupState": "optimal", "id": "3700000060080E5000299C24000006E857AC7EEC", "label": "amg_made_by_ansible", "localRole": "primary", "mirrorChannelRemoteTarget": "9000000060080E5000299C24005B06E557AC7EEC", "orphanGroup": false, "recoveryPointAgeAlertThresholdMinutes": 20, "remoteRole": "secondary", "remoteTarget": {"nodeName": {"ioInterfaceType": "fc", "iscsiNodeName": null, "remoteNodeWWN": "20040080E5299F1C"}, "remoteRef": "9000000060080E5000299C24005B06E557AC7EEC", "scsiinitiatorTargetBaseProperties": {"ioInterfaceType": "fc", "iscsiinitiatorTargetBaseParameters": null}}, "remoteTargetId": "ansible2", "remoteTargetName": "Ansible2", "remoteTargetWwn": "60080E5000299F880000000056A25D56", "repositoryUtilizationWarnThreshold": 80, "roleChangeProgress": "none", "syncActivity": "idle", "syncCompletionTimeAlertThresholdMinutes": 10, "syncIntervalMinutes": 10, "worldWideName": "60080E5000299C24000006E857AC7EEC"}'
+""" # NOQA
+
+import json
+import traceback
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils._text import to_native
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import request, eseries_host_argument_spec
+
+
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+}
+
+
+def has_match(module, ssid, api_url, api_pwd, api_usr, body):
+ compare_keys = ['syncIntervalMinutes', 'syncWarnThresholdMinutes',
+ 'recoveryWarnThresholdMinutes', 'repoUtilizationWarnThreshold']
+ desired_state = dict((x, (body.get(x))) for x in compare_keys)
+ label_exists = False
+ matches_spec = False
+ current_state = None
+ async_id = None
+ api_data = None
+ desired_name = body.get('name')
+ endpoint = 'storage-systems/%s/async-mirrors' % ssid
+ url = api_url + endpoint
+ try:
+ rc, data = request(url, url_username=api_usr, url_password=api_pwd, headers=HEADERS)
+ except Exception as e:
+ module.exit_json(msg="Error finding a match. Message: %s" % to_native(e), exception=traceback.format_exc())
+
+ for async_group in data:
+ if async_group['label'] == desired_name:
+ label_exists = True
+ api_data = async_group
+ async_id = async_group['groupRef']
+ current_state = dict(
+ syncIntervalMinutes=async_group['syncIntervalMinutes'],
+ syncWarnThresholdMinutes=async_group['syncCompletionTimeAlertThresholdMinutes'],
+ recoveryWarnThresholdMinutes=async_group['recoveryPointAgeAlertThresholdMinutes'],
+ repoUtilizationWarnThreshold=async_group['repositoryUtilizationWarnThreshold'],
+ )
+
+ if current_state == desired_state:
+ matches_spec = True
+
+ return label_exists, matches_spec, api_data, async_id
+
+
+def create_async(module, ssid, api_url, api_pwd, api_usr, body):
+ endpoint = 'storage-systems/%s/async-mirrors' % ssid
+ url = api_url + endpoint
+ post_data = json.dumps(body)
+ try:
+ rc, data = request(url, data=post_data, method='POST', url_username=api_usr, url_password=api_pwd,
+ headers=HEADERS)
+ except Exception as e:
+ module.exit_json(msg="Exception while creating aysnc mirror group. Message: %s" % to_native(e),
+ exception=traceback.format_exc())
+ return data
+
+
+def update_async(module, ssid, api_url, pwd, user, body, new_name, async_id):
+ endpoint = 'storage-systems/%s/async-mirrors/%s' % (ssid, async_id)
+ url = api_url + endpoint
+ compare_keys = ['syncIntervalMinutes', 'syncWarnThresholdMinutes',
+ 'recoveryWarnThresholdMinutes', 'repoUtilizationWarnThreshold']
+ desired_state = dict((x, (body.get(x))) for x in compare_keys)
+
+ if new_name:
+ desired_state['new_name'] = new_name
+
+ post_data = json.dumps(desired_state)
+
+ try:
+ rc, data = request(url, data=post_data, method='POST', headers=HEADERS,
+ url_username=user, url_password=pwd)
+ except Exception as e:
+ module.exit_json(msg="Exception while updating async mirror group. Message: %s" % to_native(e),
+ exception=traceback.format_exc())
+
+ return data
+
+
+def remove_amg(module, ssid, api_url, pwd, user, async_id):
+ endpoint = 'storage-systems/%s/async-mirrors/%s' % (ssid, async_id)
+ url = api_url + endpoint
+ try:
+ rc, data = request(url, method='DELETE', url_username=user, url_password=pwd,
+ headers=HEADERS)
+ except Exception as e:
+ module.exit_json(msg="Exception while removing async mirror group. Message: %s" % to_native(e),
+ exception=traceback.format_exc())
+
+ return
+
+
+def main():
+ argument_spec = eseries_host_argument_spec()
+ argument_spec.update(dict(
+ name=dict(required=True, type='str'),
+ new_name=dict(required=False, type='str'),
+ secondaryArrayId=dict(required=True, type='str'),
+ syncIntervalMinutes=dict(required=False, default=10, type='int'),
+ manualSync=dict(required=False, default=False, type='bool'),
+ recoveryWarnThresholdMinutes=dict(required=False, default=20, type='int'),
+ repoUtilizationWarnThreshold=dict(required=False, default=80, type='int'),
+ interfaceType=dict(required=False, choices=['fibre', 'iscsi'], type='str'),
+ state=dict(required=True, choices=['present', 'absent']),
+ syncWarnThresholdMinutes=dict(required=False, default=10, type='int')
+ ))
+
+ module = AnsibleModule(argument_spec=argument_spec)
+
+ p = module.params
+
+ ssid = p.pop('ssid')
+ api_url = p.pop('api_url')
+ user = p.pop('api_username')
+ pwd = p.pop('api_password')
+ new_name = p.pop('new_name')
+ state = p.pop('state')
+
+ if not api_url.endswith('/'):
+ api_url += '/'
+
+ name_exists, spec_matches, api_data, async_id = has_match(module, ssid, api_url, pwd, user, p)
+
+ if state == 'present':
+ if name_exists and spec_matches:
+ module.exit_json(changed=False, msg="Desired state met", **api_data)
+ elif name_exists and not spec_matches:
+ results = update_async(module, ssid, api_url, pwd, user,
+ p, new_name, async_id)
+ module.exit_json(changed=True,
+ msg="Async mirror group updated", async_id=async_id,
+ **results)
+ elif not name_exists:
+ results = create_async(module, ssid, api_url, user, pwd, p)
+ module.exit_json(changed=True, **results)
+
+ elif state == 'absent':
+ if name_exists:
+ remove_amg(module, ssid, api_url, pwd, user, async_id)
+ module.exit_json(changed=True, msg="Async mirror group removed.",
+ async_id=async_id)
+ else:
+ module.exit_json(changed=False,
+ msg="Async Mirror group: %s already absent" % p['name'])
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_amg_role.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_amg_role.py
new file mode 100644
index 00000000..a67506f3
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_amg_role.py
@@ -0,0 +1,244 @@
+#!/usr/bin/python
+
+# (c) 2016, 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
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+
+DOCUMENTATION = """
+---
+module: netapp_e_amg_role
+short_description: NetApp E-Series update the role of a storage array within an Asynchronous Mirror Group (AMG).
+description:
+ - Update a storage array to become the primary or secondary instance in an asynchronous mirror group
+version_added: '2.2'
+author: Kevin Hulquest (@hulquest)
+options:
+ api_username:
+ required: true
+ type: str
+ description:
+ - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API.
+ api_password:
+ required: true
+ type: str
+ description:
+ - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API.
+ api_url:
+ required: true
+ type: str
+ description:
+ - The url to the SANtricity WebServices Proxy or embedded REST API.
+ validate_certs:
+ required: false
+ default: true
+ description:
+ - Should https certificates be validated?
+ type: bool
+ ssid:
+ description:
+ - The ID of the primary storage array for the async mirror action
+ required: yes
+ type: str
+ name:
+ description:
+ - Name of the role
+ required: yes
+ type: str
+ role:
+ description:
+ - Whether the array should be the primary or secondary array for the AMG
+ required: yes
+ type: str
+ choices: ['primary', 'secondary']
+ noSync:
+ description:
+ - Whether to avoid synchronization prior to role reversal
+ required: no
+ default: no
+ type: bool
+ force:
+ description:
+ - Whether to force the role reversal regardless of the online-state of the primary
+ required: no
+ default: no
+ type: bool
+"""
+
+EXAMPLES = """
+ - name: Update the role of a storage array
+ netapp_e_amg_role:
+ name: updating amg role
+ role: primary
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ validate_certs: "{{ netapp_api_validate_certs }}"
+"""
+
+RETURN = """
+msg:
+ description: Failure message
+ returned: failure
+ type: str
+ sample: "No Async Mirror Group with the name."
+"""
+import json
+import traceback
+
+from ansible.module_utils.api import basic_auth_argument_spec
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.six.moves.urllib.error import HTTPError
+from ansible.module_utils._text import to_native
+from ansible.module_utils.urls import open_url
+
+
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+}
+
+
+def request(url, data=None, headers=None, method='GET', use_proxy=True,
+ force=False, last_mod_time=None, timeout=10, validate_certs=True,
+ url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False):
+ try:
+ r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy,
+ force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs,
+ url_username=url_username, url_password=url_password, http_agent=http_agent,
+ force_basic_auth=force_basic_auth)
+ except HTTPError as e:
+ r = e.fp
+
+ try:
+ raw_data = r.read()
+ if raw_data:
+ data = json.loads(raw_data)
+ else:
+ raw_data = None
+ except Exception:
+ if ignore_errors:
+ pass
+ else:
+ raise Exception(raw_data)
+
+ resp_code = r.getcode()
+
+ if resp_code >= 400 and not ignore_errors:
+ raise Exception(resp_code, data)
+ else:
+ return resp_code, data
+
+
+def has_match(module, ssid, api_url, api_pwd, api_usr, body, name):
+ amg_exists = False
+ has_desired_role = False
+ amg_id = None
+ amg_data = None
+ get_amgs = 'storage-systems/%s/async-mirrors' % ssid
+ url = api_url + get_amgs
+ try:
+ amg_rc, amgs = request(url, url_username=api_usr, url_password=api_pwd,
+ headers=HEADERS)
+ except Exception:
+ module.fail_json(msg="Failed to find AMGs on storage array. Id [%s]" % (ssid))
+
+ for amg in amgs:
+ if amg['label'] == name:
+ amg_exists = True
+ amg_id = amg['id']
+ amg_data = amg
+ if amg['localRole'] == body.get('role'):
+ has_desired_role = True
+
+ return amg_exists, has_desired_role, amg_id, amg_data
+
+
+def update_amg(module, ssid, api_url, api_usr, api_pwd, body, amg_id):
+ endpoint = 'storage-systems/%s/async-mirrors/%s/role' % (ssid, amg_id)
+ url = api_url + endpoint
+ post_data = json.dumps(body)
+ try:
+ request(url, data=post_data, method='POST', url_username=api_usr,
+ url_password=api_pwd, headers=HEADERS)
+ except Exception as e:
+ module.fail_json(
+ msg="Failed to change role of AMG. Id [%s]. AMG Id [%s]. Error [%s]" % (ssid, amg_id, to_native(e)),
+ exception=traceback.format_exc())
+
+ status_endpoint = 'storage-systems/%s/async-mirrors/%s' % (ssid, amg_id)
+ status_url = api_url + status_endpoint
+ try:
+ rc, status = request(status_url, method='GET', url_username=api_usr,
+ url_password=api_pwd, headers=HEADERS)
+ except Exception as e:
+ module.fail_json(
+ msg="Failed to check status of AMG after role reversal. "
+ "Id [%s]. AMG Id [%s]. Error [%s]" % (ssid, amg_id, to_native(e)),
+ exception=traceback.format_exc())
+
+ # Here we wait for the role reversal to complete
+ if 'roleChangeProgress' in status:
+ while status['roleChangeProgress'] != "none":
+ try:
+ rc, status = request(status_url, method='GET',
+ url_username=api_usr, url_password=api_pwd, headers=HEADERS)
+ except Exception as e:
+ module.fail_json(
+ msg="Failed to check status of AMG after role reversal. "
+ "Id [%s]. AMG Id [%s]. Error [%s]" % (ssid, amg_id, to_native(e)),
+ exception=traceback.format_exc())
+ return status
+
+
+def main():
+ argument_spec = basic_auth_argument_spec()
+ argument_spec.update(dict(
+ name=dict(required=True, type='str'),
+ role=dict(required=True, choices=['primary', 'secondary']),
+ noSync=dict(required=False, type='bool', default=False),
+ force=dict(required=False, type='bool', default=False),
+ ssid=dict(required=True, type='str'),
+ api_url=dict(required=True),
+ api_username=dict(required=False),
+ api_password=dict(required=False, no_log=True),
+ ))
+
+ module = AnsibleModule(argument_spec=argument_spec)
+
+ p = module.params
+
+ ssid = p.pop('ssid')
+ api_url = p.pop('api_url')
+ user = p.pop('api_username')
+ pwd = p.pop('api_password')
+ name = p.pop('name')
+
+ if not api_url.endswith('/'):
+ api_url += '/'
+
+ agm_exists, has_desired_role, async_id, amg_data = has_match(module, ssid, api_url, pwd, user, p, name)
+
+ if not agm_exists:
+ module.fail_json(msg="No Async Mirror Group with the name: '%s' was found" % name)
+ elif has_desired_role:
+ module.exit_json(changed=False, **amg_data)
+
+ else:
+ amg_data = update_amg(module, ssid, api_url, user, pwd, p, async_id)
+ if amg_data:
+ module.exit_json(changed=True, **amg_data)
+ else:
+ module.exit_json(changed=True, msg="AMG role changed.")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_amg_sync.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_amg_sync.py
new file mode 100644
index 00000000..056accd6
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_amg_sync.py
@@ -0,0 +1,267 @@
+#!/usr/bin/python
+
+# (c) 2016, 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
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+
+DOCUMENTATION = """
+---
+module: netapp_e_amg_sync
+short_description: NetApp E-Series conduct synchronization actions on asynchronous mirror groups.
+description:
+ - Allows for the initialization, suspension and resumption of an asynchronous mirror group's synchronization for NetApp E-series storage arrays.
+version_added: '2.2'
+author: Kevin Hulquest (@hulquest)
+options:
+ api_username:
+ required: true
+ type: str
+ description:
+ - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API.
+ api_password:
+ required: true
+ type: str
+ description:
+ - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API.
+ api_url:
+ required: true
+ type: str
+ description:
+ - The url to the SANtricity WebServices Proxy or embedded REST API.
+ validate_certs:
+ required: false
+ default: true
+ description:
+ - Should https certificates be validated?
+ type: bool
+ ssid:
+ description:
+ - The ID of the storage array containing the AMG you wish to target
+ type: str
+ name:
+ description:
+ - The name of the async mirror group you wish to target
+ type: str
+ required: yes
+ state:
+ description:
+ - The synchronization action you'd like to take.
+ - If C(running) then it will begin syncing if there is no active sync or will resume a suspended sync. If there is already a sync in
+ progress, it will return with an OK status.
+ - If C(suspended) it will suspend any ongoing sync action, but return OK if there is no active sync or if the sync is already suspended
+ type: str
+ choices:
+ - running
+ - suspended
+ required: yes
+ delete_recovery_point:
+ description:
+ - Indicates whether the failures point can be deleted on the secondary if necessary to achieve the synchronization.
+ - If true, and if the amount of unsynchronized data exceeds the CoW repository capacity on the secondary for any member volume, the last
+ failures point will be deleted and synchronization will continue.
+ - If false, the synchronization will be suspended if the amount of unsynchronized data exceeds the CoW Repository capacity on the secondary
+ and the failures point will be preserved.
+ - "NOTE: This only has impact for newly launched syncs."
+ type: bool
+ default: no
+"""
+EXAMPLES = """
+ - name: start AMG async
+ netapp_e_amg_sync:
+ name: "{{ amg_sync_name }}"
+ state: running
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+"""
+RETURN = """
+json:
+ description: The object attributes of the AMG.
+ returned: success
+ type: str
+ example:
+ {
+ "changed": false,
+ "connectionType": "fc",
+ "groupRef": "3700000060080E5000299C24000006EF57ACAC70",
+ "groupState": "optimal",
+ "id": "3700000060080E5000299C24000006EF57ACAC70",
+ "label": "made_with_ansible",
+ "localRole": "primary",
+ "mirrorChannelRemoteTarget": "9000000060080E5000299C24005B06E557AC7EEC",
+ "orphanGroup": false,
+ "recoveryPointAgeAlertThresholdMinutes": 20,
+ "remoteRole": "secondary",
+ "remoteTarget": {
+ "nodeName": {
+ "ioInterfaceType": "fc",
+ "iscsiNodeName": null,
+ "remoteNodeWWN": "20040080E5299F1C"
+ },
+ "remoteRef": "9000000060080E5000299C24005B06E557AC7EEC",
+ "scsiinitiatorTargetBaseProperties": {
+ "ioInterfaceType": "fc",
+ "iscsiinitiatorTargetBaseParameters": null
+ }
+ },
+ "remoteTargetId": "ansible2",
+ "remoteTargetName": "Ansible2",
+ "remoteTargetWwn": "60080E5000299F880000000056A25D56",
+ "repositoryUtilizationWarnThreshold": 80,
+ "roleChangeProgress": "none",
+ "syncActivity": "idle",
+ "syncCompletionTimeAlertThresholdMinutes": 10,
+ "syncIntervalMinutes": 10,
+ "worldWideName": "60080E5000299C24000006EF57ACAC70"
+ }
+"""
+import json
+
+from ansible.module_utils.api import basic_auth_argument_spec
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.six.moves.urllib.error import HTTPError
+from ansible.module_utils.urls import open_url
+
+
+def request(url, data=None, headers=None, method='GET', use_proxy=True,
+ force=False, last_mod_time=None, timeout=10, validate_certs=True,
+ url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False):
+ try:
+ r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy,
+ force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs,
+ url_username=url_username, url_password=url_password, http_agent=http_agent,
+ force_basic_auth=force_basic_auth)
+ except HTTPError as e:
+ r = e.fp
+
+ try:
+ raw_data = r.read()
+ if raw_data:
+ data = json.loads(raw_data)
+ else:
+ raw_data = None
+ except Exception:
+ if ignore_errors:
+ pass
+ else:
+ raise Exception(raw_data)
+
+ resp_code = r.getcode()
+
+ if resp_code >= 400 and not ignore_errors:
+ raise Exception(resp_code, data)
+ else:
+ return resp_code, data
+
+
+class AMGsync(object):
+ def __init__(self):
+ argument_spec = basic_auth_argument_spec()
+ argument_spec.update(dict(
+ api_username=dict(type='str', required=True),
+ api_password=dict(type='str', required=True, no_log=True),
+ api_url=dict(type='str', required=True),
+ name=dict(required=True, type='str'),
+ ssid=dict(required=True, type='str'),
+ state=dict(required=True, type='str', choices=['running', 'suspended']),
+ delete_recovery_point=dict(required=False, type='bool', default=False)
+ ))
+ self.module = AnsibleModule(argument_spec=argument_spec)
+ args = self.module.params
+ self.name = args['name']
+ self.ssid = args['ssid']
+ self.state = args['state']
+ self.delete_recovery_point = args['delete_recovery_point']
+ try:
+ self.user = args['api_username']
+ self.pwd = args['api_password']
+ self.url = args['api_url']
+ except KeyError:
+ self.module.fail_json(msg="You must pass in api_username"
+ "and api_password and api_url to the module.")
+ self.certs = args['validate_certs']
+
+ self.post_headers = {
+ "Accept": "application/json",
+ "Content-Type": "application/json"
+ }
+ self.amg_id, self.amg_obj = self.get_amg()
+
+ def get_amg(self):
+ endpoint = self.url + '/storage-systems/%s/async-mirrors' % self.ssid
+ (rc, amg_objs) = request(endpoint, url_username=self.user, url_password=self.pwd, validate_certs=self.certs,
+ headers=self.post_headers)
+ try:
+ amg_id = filter(lambda d: d['label'] == self.name, amg_objs)[0]['id']
+ amg_obj = filter(lambda d: d['label'] == self.name, amg_objs)[0]
+ except IndexError:
+ self.module.fail_json(
+ msg="There is no async mirror group %s associated with storage array %s" % (self.name, self.ssid))
+ return amg_id, amg_obj
+
+ @property
+ def current_state(self):
+ amg_id, amg_obj = self.get_amg()
+ return amg_obj['syncActivity']
+
+ def run_sync_action(self):
+ # If we get to this point we know that the states differ, and there is no 'err' state,
+ # so no need to revalidate
+
+ post_body = dict()
+ if self.state == 'running':
+ if self.current_state == 'idle':
+ if self.delete_recovery_point:
+ post_body.update(dict(deleteRecoveryPointIfNecessary=self.delete_recovery_point))
+ suffix = 'sync'
+ else:
+ # In a suspended state
+ suffix = 'resume'
+ else:
+ suffix = 'suspend'
+
+ endpoint = self.url + "/storage-systems/%s/async-mirrors/%s/%s" % (self.ssid, self.amg_id, suffix)
+
+ (rc, resp) = request(endpoint, method='POST', url_username=self.user, url_password=self.pwd,
+ validate_certs=self.certs, data=json.dumps(post_body), headers=self.post_headers,
+ ignore_errors=True)
+
+ if not str(rc).startswith('2'):
+ self.module.fail_json(msg=str(resp['errorMessage']))
+
+ return resp
+
+ def apply(self):
+ state_map = dict(
+ running=['active'],
+ suspended=['userSuspended', 'internallySuspended', 'paused'],
+ err=['unkown', '_UNDEFINED'])
+
+ if self.current_state not in state_map[self.state]:
+ if self.current_state in state_map['err']:
+ self.module.fail_json(
+ msg="The sync is a state of '%s', this requires manual intervention. " +
+ "Please investigate and try again" % self.current_state)
+ else:
+ self.amg_obj = self.run_sync_action()
+
+ (ret, amg) = self.get_amg()
+ self.module.exit_json(changed=False, **amg)
+
+
+def main():
+ sync = AMGsync()
+ sync.apply()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_asup.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_asup.py
new file mode 100644
index 00000000..f039626a
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_asup.py
@@ -0,0 +1,314 @@
+#!/usr/bin/python
+
+# (c) 2018, 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
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = """
+---
+module: netapp_e_asup
+short_description: NetApp E-Series manage auto-support settings
+description:
+ - Allow the auto-support settings to be configured for an individual E-Series storage-system
+version_added: '2.7'
+author: Michael Price (@lmprice)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ state:
+ description:
+ - Enable/disable the E-Series auto-support configuration.
+ - When this option is enabled, configuration, logs, and other support-related information will be relayed
+ to NetApp to help better support your system. No personally identifiable information, passwords, etc, will
+ be collected.
+ default: enabled
+ type: str
+ choices:
+ - enabled
+ - disabled
+ aliases:
+ - asup
+ - auto_support
+ - autosupport
+ active:
+ description:
+ - Enable active/proactive monitoring for ASUP. When a problem is detected by our monitoring systems, it's
+ possible that the bundle did not contain all of the required information at the time of the event.
+ Enabling this option allows NetApp support personnel to manually request transmission or re-transmission
+ of support data in order ot resolve the problem.
+ - Only applicable if I(state=enabled).
+ default: yes
+ type: bool
+ start:
+ description:
+ - A start hour may be specified in a range from 0 to 23 hours.
+ - ASUP bundles will be sent daily between the provided start and end time (UTC).
+ - I(start) must be less than I(end).
+ aliases:
+ - start_time
+ default: 0
+ type: int
+ end:
+ description:
+ - An end hour may be specified in a range from 1 to 24 hours.
+ - ASUP bundles will be sent daily between the provided start and end time (UTC).
+ - I(start) must be less than I(end).
+ aliases:
+ - end_time
+ default: 24
+ type: int
+ days:
+ description:
+ - A list of days of the week that ASUP bundles will be sent. A larger, weekly bundle will be sent on one
+ of the provided days.
+ choices:
+ - monday
+ - tuesday
+ - wednesday
+ - thursday
+ - friday
+ - saturday
+ - sunday
+ required: no
+ type: list
+ aliases:
+ - days_of_week
+ - schedule_days
+ verbose:
+ description:
+ - Provide the full ASUP configuration in the return.
+ default: no
+ required: no
+ type: bool
+ log_path:
+ description:
+ - A local path to a file to be used for debug logging
+ type: str
+ required: no
+notes:
+ - Check mode is supported.
+ - Enabling ASUP will allow our support teams to monitor the logs of the storage-system in order to proactively
+ respond to issues with the system. It is recommended that all ASUP-related options be enabled, but they may be
+ disabled if desired.
+ - This API is currently only supported with the Embedded Web Services API v2.0 and higher.
+"""
+
+EXAMPLES = """
+ - name: Enable ASUP and allow pro-active retrieval of bundles
+ netapp_e_asup:
+ state: enabled
+ active: yes
+ api_url: "10.1.1.1:8443"
+ api_username: "admin"
+ api_password: "myPass"
+
+ - name: Set the ASUP schedule to only send bundles from 12 AM CST to 3 AM CST.
+ netapp_e_asup:
+ start: 17
+ end: 20
+ api_url: "10.1.1.1:8443"
+ api_username: "admin"
+ api_password: "myPass"
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The settings have been updated.
+asup:
+ description:
+ - True if ASUP is enabled.
+ returned: on success
+ sample: True
+ type: bool
+active:
+ description:
+ - True if the active option has been enabled.
+ returned: on success
+ sample: True
+ type: bool
+cfg:
+ description:
+ - Provide the full ASUP configuration.
+ returned: on success when I(verbose=true).
+ type: complex
+ contains:
+ asupEnabled:
+ description:
+ - True if ASUP has been enabled.
+ type: bool
+ onDemandEnabled:
+ description:
+ - True if ASUP active monitoring has been enabled.
+ type: bool
+ daysOfWeek:
+ description:
+ - The days of the week that ASUP bundles will be sent.
+ type: list
+"""
+
+import json
+import logging
+from pprint import pformat
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import request, eseries_host_argument_spec
+from ansible.module_utils._text import to_native
+
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+}
+
+
+class Asup(object):
+ DAYS_OPTIONS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']
+
+ def __init__(self):
+ argument_spec = eseries_host_argument_spec()
+ argument_spec.update(dict(
+ state=dict(type='str', required=False, default='enabled', aliases=['asup', 'auto_support', 'autosupport'],
+ choices=['enabled', 'disabled']),
+ active=dict(type='bool', required=False, default=True, ),
+ days=dict(type='list', required=False, aliases=['schedule_days', 'days_of_week'],
+ choices=self.DAYS_OPTIONS),
+ start=dict(type='int', required=False, default=0, aliases=['start_time']),
+ end=dict(type='int', required=False, default=24, aliases=['end_time']),
+ verbose=dict(type='bool', required=False, default=False),
+ log_path=dict(type='str', required=False),
+ ))
+
+ self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, )
+ args = self.module.params
+ self.asup = args['state'] == 'enabled'
+ self.active = args['active']
+ self.days = args['days']
+ self.start = args['start']
+ self.end = args['end']
+ self.verbose = args['verbose']
+
+ self.ssid = args['ssid']
+ self.url = args['api_url']
+ self.creds = dict(url_password=args['api_password'],
+ validate_certs=args['validate_certs'],
+ url_username=args['api_username'], )
+
+ self.check_mode = self.module.check_mode
+
+ log_path = args['log_path']
+
+ # logging setup
+ self._logger = logging.getLogger(self.__class__.__name__)
+
+ if log_path:
+ logging.basicConfig(
+ level=logging.DEBUG, filename=log_path, filemode='w',
+ format='%(relativeCreated)dms %(levelname)s %(module)s.%(funcName)s:%(lineno)d\n %(message)s')
+
+ if not self.url.endswith('/'):
+ self.url += '/'
+
+ if self.start >= self.end:
+ self.module.fail_json(msg="The value provided for the start time is invalid."
+ " It must be less than the end time.")
+ if self.start < 0 or self.start > 23:
+ self.module.fail_json(msg="The value provided for the start time is invalid. It must be between 0 and 23.")
+ else:
+ self.start = self.start * 60
+ if self.end < 1 or self.end > 24:
+ self.module.fail_json(msg="The value provided for the end time is invalid. It must be between 1 and 24.")
+ else:
+ self.end = min(self.end * 60, 1439)
+
+ if not self.days:
+ self.days = self.DAYS_OPTIONS
+
+ def get_configuration(self):
+ try:
+ (rc, result) = request(self.url + 'device-asup', headers=HEADERS, **self.creds)
+
+ if not (result['asupCapable'] and result['onDemandCapable']):
+ self.module.fail_json(msg="ASUP is not supported on this device. Array Id [%s]." % (self.ssid))
+ return result
+
+ except Exception as err:
+ self.module.fail_json(msg="Failed to retrieve ASUP configuration! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ def update_configuration(self):
+ config = self.get_configuration()
+ update = False
+ body = dict()
+
+ if self.asup:
+ body = dict(asupEnabled=True)
+ if not config['asupEnabled']:
+ update = True
+
+ if (config['onDemandEnabled'] and config['remoteDiagsEnabled']) != self.active:
+ update = True
+ body.update(dict(onDemandEnabled=self.active,
+ remoteDiagsEnabled=self.active))
+ self.days.sort()
+ config['schedule']['daysOfWeek'].sort()
+
+ body['schedule'] = dict(daysOfWeek=self.days,
+ dailyMinTime=self.start,
+ dailyMaxTime=self.end,
+ weeklyMinTime=self.start,
+ weeklyMaxTime=self.end)
+
+ if self.days != config['schedule']['daysOfWeek']:
+ update = True
+ if self.start != config['schedule']['dailyMinTime'] or self.start != config['schedule']['weeklyMinTime']:
+ update = True
+ elif self.end != config['schedule']['dailyMaxTime'] or self.end != config['schedule']['weeklyMaxTime']:
+ update = True
+
+ elif config['asupEnabled']:
+ body = dict(asupEnabled=False)
+ update = True
+
+ self._logger.info(pformat(body))
+
+ if update and not self.check_mode:
+ try:
+ (rc, result) = request(self.url + 'device-asup', method='POST',
+ data=json.dumps(body), headers=HEADERS, **self.creds)
+ # This is going to catch cases like a connection failure
+ except Exception as err:
+ self.module.fail_json(msg="We failed to set the storage-system name! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ return update
+
+ def update(self):
+ update = self.update_configuration()
+ cfg = self.get_configuration()
+ if self.verbose:
+ self.module.exit_json(msg="The ASUP settings have been updated.", changed=update,
+ asup=cfg['asupEnabled'], active=cfg['onDemandEnabled'], cfg=cfg)
+ else:
+ self.module.exit_json(msg="The ASUP settings have been updated.", changed=update,
+ asup=cfg['asupEnabled'], active=cfg['onDemandEnabled'])
+
+ def __call__(self, *args, **kwargs):
+ self.update()
+
+
+def main():
+ settings = Asup()
+ settings()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_auditlog.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_auditlog.py
new file mode 100644
index 00000000..814a72d3
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_auditlog.py
@@ -0,0 +1,286 @@
+#!/usr/bin/python
+
+# (c) 2018, 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
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = """
+---
+module: netapp_e_auditlog
+short_description: NetApp E-Series manage audit-log configuration
+description:
+ - This module allows an e-series storage system owner to set audit-log configuration parameters.
+version_added: '2.7'
+author: Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ max_records:
+ description:
+ - The maximum number log messages audit-log will retain.
+ - Max records must be between and including 100 and 50000.
+ default: 50000
+ type: int
+ log_level:
+ description: Filters the log messages according to the specified log level selection.
+ choices:
+ - all
+ - writeOnly
+ default: writeOnly
+ type: str
+ full_policy:
+ description: Specifies what audit-log should do once the number of entries approach the record limit.
+ choices:
+ - overWrite
+ - preventSystemAccess
+ default: overWrite
+ type: str
+ threshold:
+ description:
+ - This is the memory full percent threshold that audit-log will start issuing warning messages.
+ - Percent range must be between and including 60 and 90.
+ default: 90
+ type: int
+ force:
+ description:
+ - Forces the audit-log configuration to delete log history when log messages fullness cause immediate
+ warning or full condition.
+ - Warning! This will cause any existing audit-log messages to be deleted.
+ - This is only applicable for I(full_policy=preventSystemAccess).
+ type: bool
+ default: no
+ log_path:
+ description: A local path to a file to be used for debug logging.
+ required: no
+ type: str
+notes:
+ - Check mode is supported.
+ - This module is currently only supported with the Embedded Web Services API v3.0 and higher.
+"""
+
+EXAMPLES = """
+- name: Define audit-log to prevent system access if records exceed 50000 with warnings occurring at 60% capacity.
+ netapp_e_auditlog:
+ api_url: "https://{{ netapp_e_api_host }}/devmgr/v2"
+ api_username: "{{ netapp_e_api_username }}"
+ api_password: "{{ netapp_e_api_password }}"
+ ssid: "{{ netapp_e_ssid }}"
+ validate_certs: no
+ max_records: 50000
+ log_level: all
+ full_policy: preventSystemAccess
+ threshold: 60
+ log_path: /path/to/log_file.log
+- name: Define audit-log utilize the default values.
+ netapp_e_auditlog:
+ api_url: "https://{{ netapp_e_api_host }}/devmgr/v2"
+ api_username: "{{ netapp_e_api_username }}"
+ api_password: "{{ netapp_e_api_password }}"
+ ssid: "{{ netapp_e_ssid }}"
+- name: Force audit-log configuration when full or warning conditions occur while enacting preventSystemAccess policy.
+ netapp_e_auditlog:
+ api_url: "https://{{ netapp_e_api_host }}/devmgr/v2"
+ api_username: "{{ netapp_e_api_username }}"
+ api_password: "{{ netapp_e_api_password }}"
+ ssid: "{{ netapp_e_ssid }}"
+ max_records: 5000
+ log_level: all
+ full_policy: preventSystemAccess
+ threshold: 60
+ force: yes
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The settings have been updated.
+"""
+
+import json
+import logging
+from pprint import pformat
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import request, eseries_host_argument_spec
+from ansible.module_utils._text import to_native
+
+try:
+ from urlparse import urlparse, urlunparse
+except Exception:
+ from urllib.parse import urlparse, urlunparse
+
+
+class AuditLog(object):
+ """Audit-log module configuration class."""
+ MAX_RECORDS = 50000
+ HEADERS = {"Content-Type": "application/json",
+ "Accept": "application/json"}
+
+ def __init__(self):
+ argument_spec = eseries_host_argument_spec()
+ argument_spec.update(dict(
+ max_records=dict(type="int", default=50000),
+ log_level=dict(type="str", default="writeOnly", choices=["all", "writeOnly"]),
+ full_policy=dict(type="str", default="overWrite", choices=["overWrite", "preventSystemAccess"]),
+ threshold=dict(type="int", default=90),
+ force=dict(type="bool", default=False),
+ log_path=dict(type='str', required=False)))
+
+ self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ args = self.module.params
+
+ self.max_records = args["max_records"]
+ if self.max_records < 100 or self.max_records > self.MAX_RECORDS:
+ self.module.fail_json(msg="Audit-log max_records count must be between 100 and 50000: [%s]"
+ % self.max_records)
+ self.threshold = args["threshold"]
+ if self.threshold < 60 or self.threshold > 90:
+ self.module.fail_json(msg="Audit-log percent threshold must be between 60 and 90: [%s]" % self.threshold)
+ self.log_level = args["log_level"]
+ self.full_policy = args["full_policy"]
+ self.force = args["force"]
+ self.ssid = args['ssid']
+ self.url = args['api_url']
+ if not self.url.endswith('/'):
+ self.url += '/'
+ self.creds = dict(url_password=args['api_password'],
+ validate_certs=args['validate_certs'],
+ url_username=args['api_username'], )
+
+ # logging setup
+ log_path = args['log_path']
+ self._logger = logging.getLogger(self.__class__.__name__)
+
+ if log_path:
+ logging.basicConfig(
+ level=logging.DEBUG, filename=log_path, filemode='w',
+ format='%(relativeCreated)dms %(levelname)s %(module)s.%(funcName)s:%(lineno)d\n %(message)s')
+
+ self.proxy_used = self.is_proxy()
+ self._logger.info(self.proxy_used)
+ self.check_mode = self.module.check_mode
+
+ def is_proxy(self):
+ """Determine whether the API is embedded or proxy."""
+ try:
+
+ # replace http url path with devmgr/utils/about
+ about_url = list(urlparse(self.url))
+ about_url[2] = "devmgr/utils/about"
+ about_url = urlunparse(about_url)
+
+ rc, data = request(about_url, timeout=300, headers=self.HEADERS, **self.creds)
+
+ return data["runningAsProxy"]
+ except Exception as err:
+ self.module.fail_json(msg="Failed to retrieve the webservices about information! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ def get_configuration(self):
+ """Retrieve the existing audit-log configurations.
+
+ :returns: dictionary containing current audit-log configuration
+ """
+ try:
+ if self.proxy_used:
+ rc, data = request(self.url + "audit-log/config", timeout=300, headers=self.HEADERS, **self.creds)
+ else:
+ rc, data = request(self.url + "storage-systems/%s/audit-log/config" % self.ssid,
+ timeout=300, headers=self.HEADERS, **self.creds)
+ return data
+ except Exception as err:
+ self.module.fail_json(msg="Failed to retrieve the audit-log configuration! "
+ "Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ def build_configuration(self):
+ """Build audit-log expected configuration.
+
+ :returns: Tuple containing update boolean value and dictionary of audit-log configuration
+ """
+ config = self.get_configuration()
+
+ current = dict(auditLogMaxRecords=config["auditLogMaxRecords"],
+ auditLogLevel=config["auditLogLevel"],
+ auditLogFullPolicy=config["auditLogFullPolicy"],
+ auditLogWarningThresholdPct=config["auditLogWarningThresholdPct"])
+
+ body = dict(auditLogMaxRecords=self.max_records,
+ auditLogLevel=self.log_level,
+ auditLogFullPolicy=self.full_policy,
+ auditLogWarningThresholdPct=self.threshold)
+
+ update = current != body
+
+ self._logger.info(pformat(update))
+ self._logger.info(pformat(body))
+ return update, body
+
+ def delete_log_messages(self):
+ """Delete all audit-log messages."""
+ self._logger.info("Deleting audit-log messages...")
+ try:
+ if self.proxy_used:
+ rc, result = request(self.url + "audit-log?clearAll=True", timeout=300,
+ method="DELETE", headers=self.HEADERS, **self.creds)
+ else:
+ rc, result = request(self.url + "storage-systems/%s/audit-log?clearAll=True" % self.ssid, timeout=300,
+ method="DELETE", headers=self.HEADERS, **self.creds)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to delete audit-log messages! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ def update_configuration(self, update=None, body=None, attempt_recovery=True):
+ """Update audit-log configuration."""
+ if update is None or body is None:
+ update, body = self.build_configuration()
+
+ if update and not self.check_mode:
+ try:
+ if self.proxy_used:
+ rc, result = request(self.url + "storage-systems/audit-log/config", timeout=300,
+ data=json.dumps(body), method='POST', headers=self.HEADERS,
+ ignore_errors=True, **self.creds)
+ else:
+ rc, result = request(self.url + "storage-systems/%s/audit-log/config" % self.ssid, timeout=300,
+ data=json.dumps(body), method='POST', headers=self.HEADERS,
+ ignore_errors=True, **self.creds)
+
+ if rc == 422:
+ if self.force and attempt_recovery:
+ self.delete_log_messages()
+ update = self.update_configuration(update, body, False)
+ else:
+ self.module.fail_json(msg="Failed to update audit-log configuration! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(rc, result)))
+
+ except Exception as error:
+ self.module.fail_json(msg="Failed to update audit-log configuration! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(error)))
+ return update
+
+ def update(self):
+ """Update the audit-log configuration."""
+ update = self.update_configuration()
+ self.module.exit_json(msg="Audit-log update complete", changed=update)
+
+ def __call__(self):
+ self.update()
+
+
+def main():
+ auditlog = AuditLog()
+ auditlog()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_auth.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_auth.py
new file mode 100644
index 00000000..ac5c14c0
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_auth.py
@@ -0,0 +1,283 @@
+#!/usr/bin/python
+
+# (c) 2016, 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
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = '''
+---
+module: netapp_e_auth
+short_description: NetApp E-Series set or update the password for a storage array.
+description:
+ - Sets or updates the password for a storage array. When the password is updated on the storage array, it must be updated on the SANtricity Web
+ Services proxy. Note, all storage arrays do not have a Monitor or RO role.
+version_added: "2.2"
+author: Kevin Hulquest (@hulquest)
+options:
+ validate_certs:
+ required: false
+ default: true
+ description:
+ - Should https certificates be validated?
+ type: bool
+ name:
+ description:
+ - The name of the storage array. Note that if more than one storage array with this name is detected, the task will fail and you'll have to use
+ the ID instead.
+ required: False
+ type: str
+ ssid:
+ description:
+ - the identifier of the storage array in the Web Services Proxy.
+ required: False
+ type: str
+ set_admin:
+ description:
+ - Boolean value on whether to update the admin password. If set to false then the RO account is updated.
+ type: bool
+ default: False
+ current_password:
+ description:
+ - The current admin password. This is not required if the password hasn't been set before.
+ required: False
+ type: str
+ new_password:
+ description:
+ - The password you would like to set. Cannot be more than 30 characters.
+ required: True
+ type: str
+ api_url:
+ description:
+ - The full API url.
+ - "Example: http://ENDPOINT:8080/devmgr/v2"
+ - This can optionally be set via an environment variable, API_URL
+ required: False
+ type: str
+ api_username:
+ description:
+ - The username used to authenticate against the API
+ - This can optionally be set via an environment variable, API_USERNAME
+ required: False
+ type: str
+ api_password:
+ description:
+ - The password used to authenticate against the API
+ - This can optionally be set via an environment variable, API_PASSWORD
+ required: False
+ type: str
+'''
+
+EXAMPLES = '''
+- name: Test module
+ netapp_e_auth:
+ name: trex
+ current_password: OldPasswd
+ new_password: NewPasswd
+ set_admin: yes
+ api_url: '{{ netapp_api_url }}'
+ api_username: '{{ netapp_api_username }}'
+ api_password: '{{ netapp_api_password }}'
+'''
+
+RETURN = '''
+msg:
+ description: Success message
+ returned: success
+ type: str
+ sample: "Password Updated Successfully"
+'''
+import json
+import traceback
+
+from ansible.module_utils.api import basic_auth_argument_spec
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.six.moves.urllib.error import HTTPError
+from ansible.module_utils._text import to_native
+from ansible.module_utils.urls import open_url
+
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ "x-netapp-password-validate-method": "none"
+
+}
+
+
+def request(url, data=None, headers=None, method='GET', use_proxy=True,
+ force=False, last_mod_time=None, timeout=10, validate_certs=True,
+ url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False):
+ try:
+ r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy,
+ force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs,
+ url_username=url_username, url_password=url_password, http_agent=http_agent,
+ force_basic_auth=force_basic_auth)
+ except HTTPError as e:
+ r = e.fp
+
+ try:
+ raw_data = r.read()
+ if raw_data:
+ data = json.loads(raw_data)
+ else:
+ raw_data = None
+ except Exception:
+ if ignore_errors:
+ pass
+ else:
+ raise Exception(raw_data)
+
+ resp_code = r.getcode()
+
+ if resp_code >= 400 and not ignore_errors:
+ raise Exception(resp_code, data)
+ else:
+ return resp_code, data
+
+
+def get_ssid(module, name, api_url, user, pwd):
+ count = 0
+ all_systems = 'storage-systems'
+ systems_url = api_url + all_systems
+ rc, data = request(systems_url, headers=HEADERS, url_username=user, url_password=pwd,
+ validate_certs=module.validate_certs)
+ for system in data:
+ if system['name'] == name:
+ count += 1
+ if count > 1:
+ module.fail_json(
+ msg="You supplied a name for the Storage Array but more than 1 array was found with that name. " +
+ "Use the id instead")
+ else:
+ ssid = system['id']
+ else:
+ continue
+
+ if count == 0:
+ module.fail_json(msg="No storage array with the name %s was found" % name)
+
+ else:
+ return ssid
+
+
+def get_pwd_status(module, ssid, api_url, user, pwd):
+ pwd_status = "storage-systems/%s/passwords" % ssid
+ url = api_url + pwd_status
+ try:
+ rc, data = request(url, headers=HEADERS, url_username=user, url_password=pwd,
+ validate_certs=module.validate_certs)
+ return data['readOnlyPasswordSet'], data['adminPasswordSet']
+ except HTTPError as e:
+ module.fail_json(msg="There was an issue with connecting, please check that your "
+ "endpoint is properly defined and your credentials are correct: %s" % to_native(e))
+
+
+def update_storage_system_pwd(module, ssid, pwd, api_url, api_usr, api_pwd):
+ """Update the stored storage-system password"""
+ update_pwd = 'storage-systems/%s' % ssid
+ url = api_url + update_pwd
+ post_body = json.dumps(dict(storedPassword=pwd))
+ try:
+ rc, data = request(url, data=post_body, method='POST', headers=HEADERS, url_username=api_usr,
+ url_password=api_pwd, validate_certs=module.validate_certs)
+ return rc, data
+ except Exception as e:
+ module.fail_json(msg="Failed to update system password. Id [%s]. Error [%s]" % (ssid, to_native(e)))
+
+
+def set_password(module, ssid, api_url, user, pwd, current_password=None, new_password=None, set_admin=False):
+ """Set the storage-system password"""
+ set_pass = "storage-systems/%s/passwords" % ssid
+ url = api_url + set_pass
+
+ if not current_password:
+ current_password = ""
+
+ post_body = json.dumps(
+ dict(currentAdminPassword=current_password, adminPassword=set_admin, newPassword=new_password))
+
+ try:
+ rc, data = request(url, method='POST', data=post_body, headers=HEADERS, url_username=user, url_password=pwd,
+ ignore_errors=True, validate_certs=module.validate_certs)
+ except Exception as e:
+ module.fail_json(msg="Failed to set system password. Id [%s]. Error [%s]" % (ssid, to_native(e)),
+ exception=traceback.format_exc())
+
+ if rc == 422:
+ post_body = json.dumps(dict(currentAdminPassword='', adminPassword=set_admin, newPassword=new_password))
+ try:
+ rc, data = request(url, method='POST', data=post_body, headers=HEADERS, url_username=user, url_password=pwd,
+ validate_certs=module.validate_certs)
+ except Exception:
+ # TODO(lorenp): Resolve ignored rc, data
+ module.fail_json(msg="Wrong or no admin password supplied. Please update your playbook and try again")
+
+ if int(rc) >= 300:
+ module.fail_json(msg="Failed to set system password. Id [%s] Code [%s]. Error [%s]" % (ssid, rc, data))
+
+ rc, update_data = update_storage_system_pwd(module, ssid, new_password, api_url, user, pwd)
+
+ if int(rc) < 300:
+ return update_data
+ else:
+ module.fail_json(msg="%s:%s" % (rc, update_data))
+
+
+def main():
+ argument_spec = basic_auth_argument_spec()
+ argument_spec.update(dict(
+ name=dict(required=False, type='str'),
+ ssid=dict(required=False, type='str'),
+ current_password=dict(required=False, no_log=True),
+ new_password=dict(required=True, no_log=True),
+ set_admin=dict(required=True, type='bool'),
+ api_url=dict(required=True),
+ api_username=dict(required=False),
+ api_password=dict(required=False, no_log=True)
+ )
+ )
+ module = AnsibleModule(argument_spec=argument_spec, mutually_exclusive=[['name', 'ssid']],
+ required_one_of=[['name', 'ssid']])
+
+ name = module.params['name']
+ ssid = module.params['ssid']
+ current_password = module.params['current_password']
+ new_password = module.params['new_password']
+ set_admin = module.params['set_admin']
+ user = module.params['api_username']
+ pwd = module.params['api_password']
+ api_url = module.params['api_url']
+ module.validate_certs = module.params['validate_certs']
+
+ if not api_url.endswith('/'):
+ api_url += '/'
+
+ if name:
+ ssid = get_ssid(module, name, api_url, user, pwd)
+
+ ro_pwd, admin_pwd = get_pwd_status(module, ssid, api_url, user, pwd)
+
+ if admin_pwd and not current_password:
+ module.fail_json(
+ msg="Admin account has a password set. " +
+ "You must supply current_password in order to update the RO or Admin passwords")
+
+ if len(new_password) > 30:
+ module.fail_json(msg="Passwords must not be greater than 30 characters in length")
+
+ result = set_password(module, ssid, api_url, user, pwd, current_password=current_password,
+ new_password=new_password, set_admin=set_admin)
+
+ module.exit_json(changed=True, msg="Password Updated Successfully",
+ password_set=result['passwordSet'],
+ password_status=result['passwordStatus'])
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_drive_firmware.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_drive_firmware.py
new file mode 100644
index 00000000..e74bac77
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_drive_firmware.py
@@ -0,0 +1,215 @@
+#!/usr/bin/python
+
+# (c) 2016, 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
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = """
+---
+module: netapp_e_drive_firmware
+version_added: "2.9"
+short_description: NetApp E-Series manage drive firmware
+description:
+ - Ensure drive firmware version is activated on specified drive model.
+author:
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ firmware:
+ description:
+ - list of drive firmware file paths.
+ - NetApp E-Series drives require special firmware which can be downloaded from https://mysupport.netapp.com/NOW/download/tools/diskfw_eseries/
+ type: list
+ required: True
+ wait_for_completion:
+ description:
+ - This flag will cause module to wait for any upgrade actions to complete.
+ type: bool
+ default: false
+ ignore_inaccessible_drives:
+ description:
+ - This flag will determine whether drive firmware upgrade should fail if any affected drives are inaccessible.
+ type: bool
+ default: false
+ upgrade_drives_online:
+ description:
+ - This flag will determine whether drive firmware can be upgrade while drives are accepting I/O.
+ - When I(upgrade_drives_online==False) stop all I/O before running task.
+ type: bool
+ default: true
+"""
+EXAMPLES = """
+- name: Ensure correct firmware versions
+ nac_santricity_drive_firmware:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ firmware: "path/to/drive_firmware"
+ wait_for_completion: true
+ ignore_inaccessible_drives: false
+"""
+RETURN = """
+msg:
+ description: Whether any drive firmware was upgraded and whether it is in progress.
+ type: str
+ returned: always
+ sample:
+ { changed: True, upgrade_in_process: True }
+"""
+import os
+import re
+
+from time import sleep
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import NetAppESeriesModule, create_multipart_formdata
+from ansible.module_utils._text import to_native, to_text, to_bytes
+
+
+class NetAppESeriesDriveFirmware(NetAppESeriesModule):
+ WAIT_TIMEOUT_SEC = 60 * 15
+
+ def __init__(self):
+ ansible_options = dict(
+ firmware=dict(type="list", required=True),
+ wait_for_completion=dict(type="bool", default=False),
+ ignore_inaccessible_drives=dict(type="bool", default=False),
+ upgrade_drives_online=dict(type="bool", default=True))
+
+ super(NetAppESeriesDriveFirmware, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ supports_check_mode=True)
+
+ args = self.module.params
+ self.firmware_list = args["firmware"]
+ self.wait_for_completion = args["wait_for_completion"]
+ self.ignore_inaccessible_drives = args["ignore_inaccessible_drives"]
+ self.upgrade_drives_online = args["upgrade_drives_online"]
+
+ self.upgrade_list_cache = None
+
+ self.upgrade_required_cache = None
+ self.upgrade_in_progress = False
+ self.drive_info_cache = None
+
+ def upload_firmware(self):
+ """Ensure firmware has been upload prior to uploaded."""
+ for firmware in self.firmware_list:
+ firmware_name = os.path.basename(firmware)
+ files = [("file", firmware_name, firmware)]
+ headers, data = create_multipart_formdata(files)
+ try:
+ rc, response = self.request("/files/drive", method="POST", headers=headers, data=data)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to upload drive firmware [%s]. Array [%s]. Error [%s]." % (firmware_name, self.ssid, to_native(error)))
+
+ def upgrade_list(self):
+ """Determine whether firmware is compatible with the specified drives."""
+ if self.upgrade_list_cache is None:
+ self.upgrade_list_cache = list()
+ try:
+ rc, response = self.request("storage-systems/%s/firmware/drives" % self.ssid)
+
+ # Create upgrade list, this ensures only the firmware uploaded is applied
+ for firmware in self.firmware_list:
+ filename = os.path.basename(firmware)
+
+ for uploaded_firmware in response["compatibilities"]:
+ if uploaded_firmware["filename"] == filename:
+
+ # Determine whether upgrade is required
+ drive_reference_list = []
+ for drive in uploaded_firmware["compatibleDrives"]:
+ try:
+ rc, drive_info = self.request("storage-systems/%s/drives/%s" % (self.ssid, drive["driveRef"]))
+
+ # Add drive references that are supported and differ from current firmware
+ if (drive_info["firmwareVersion"] != uploaded_firmware["firmwareVersion"] and
+ uploaded_firmware["firmwareVersion"] in uploaded_firmware["supportedFirmwareVersions"]):
+
+ if self.ignore_inaccessible_drives or (not drive_info["offline"] and drive_info["available"]):
+ drive_reference_list.append(drive["driveRef"])
+
+ if not drive["onlineUpgradeCapable"] and self.upgrade_drives_online:
+ self.module.fail_json(msg="Drive is not capable of online upgrade. Array [%s]. Drive [%s]."
+ % (self.ssid, drive["driveRef"]))
+
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve drive information. Array [%s]. Drive [%s]. Error [%s]."
+ % (self.ssid, drive["driveRef"], to_native(error)))
+
+ if drive_reference_list:
+ self.upgrade_list_cache.extend([{"filename": filename, "driveRefList": drive_reference_list}])
+
+ except Exception as error:
+ self.module.fail_json(msg="Failed to complete compatibility and health check. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ return self.upgrade_list_cache
+
+ def wait_for_upgrade_completion(self):
+ """Wait for drive firmware upgrade to complete."""
+ drive_references = [reference for drive in self.upgrade_list() for reference in drive["driveRefList"]]
+ last_status = None
+ for attempt in range(int(self.WAIT_TIMEOUT_SEC / 5)):
+ try:
+ rc, response = self.request("storage-systems/%s/firmware/drives/state" % self.ssid)
+
+ # Check drive status
+ for status in response["driveStatus"]:
+ last_status = status
+ if status["driveRef"] in drive_references:
+ if status["status"] == "okay":
+ continue
+ elif status["status"] in ["inProgress", "inProgressRecon", "pending", "notAttempted"]:
+ break
+ else:
+ self.module.fail_json(msg="Drive firmware upgrade failed. Array [%s]. Drive [%s]. Status [%s]."
+ % (self.ssid, status["driveRef"], status["status"]))
+ else:
+ self.upgrade_in_progress = False
+ break
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve drive status. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ sleep(5)
+ else:
+ self.module.fail_json(msg="Timed out waiting for drive firmware upgrade. Array [%s]. Status [%s]." % (self.ssid, last_status))
+
+ def upgrade(self):
+ """Apply firmware to applicable drives."""
+ try:
+ rc, response = self.request("storage-systems/%s/firmware/drives/initiate-upgrade?onlineUpdate=%s"
+ % (self.ssid, "true" if self.upgrade_drives_online else "false"), method="POST", data=self.upgrade_list())
+ self.upgrade_in_progress = True
+ except Exception as error:
+ self.module.fail_json(msg="Failed to upgrade drive firmware. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ if self.wait_for_completion:
+ self.wait_for_upgrade_completion()
+
+ def apply(self):
+ """Apply firmware policy has been enforced on E-Series storage system."""
+ self.upload_firmware()
+
+ if self.upgrade_list() and not self.module.check_mode:
+ self.upgrade()
+
+ self.module.exit_json(changed=True if self.upgrade_list() else False,
+ upgrade_in_process=self.upgrade_in_progress)
+
+
+def main():
+ drive_firmware = NetAppESeriesDriveFirmware()
+ drive_firmware.apply()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_facts.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_facts.py
new file mode 100644
index 00000000..3734a477
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_facts.py
@@ -0,0 +1,530 @@
+#!/usr/bin/python
+
+# (c) 2016, 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
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = '''
+module: netapp_e_facts
+short_description: NetApp E-Series retrieve facts about NetApp E-Series storage arrays
+description:
+ - The netapp_e_facts module returns a collection of facts regarding NetApp E-Series storage arrays.
+version_added: '2.2'
+author:
+ - Kevin Hulquest (@hulquest)
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+'''
+
+EXAMPLES = """
+---
+- name: Get array facts
+ netapp_e_facts:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+"""
+
+RETURN = """
+ msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample:
+ - Gathered facts for storage array. Array ID [1].
+ - Gathered facts for web services proxy.
+ storage_array_facts:
+ description: provides details about the array, controllers, management interfaces, hostside interfaces,
+ driveside interfaces, disks, storage pools, volumes, snapshots, and features.
+ returned: on successful inquiry from from embedded web services rest api
+ type: complex
+ contains:
+ netapp_controllers:
+ description: storage array controller list that contains basic controller identification and status
+ type: complex
+ sample:
+ - [{"name": "A", "serial": "021632007299", "status": "optimal"},
+ {"name": "B", "serial": "021632007300", "status": "failed"}]
+ netapp_disks:
+ description: drive list that contains identification, type, and status information for each drive
+ type: complex
+ sample:
+ - [{"available": false,
+ "firmware_version": "MS02",
+ "id": "01000000500003960C8B67880000000000000000",
+ "media_type": "ssd",
+ "product_id": "PX02SMU080 ",
+ "serial_number": "15R0A08LT2BA",
+ "status": "optimal",
+ "tray_ref": "0E00000000000000000000000000000000000000",
+ "usable_bytes": "799629205504" }]
+ netapp_driveside_interfaces:
+ description: drive side interface list that contains identification, type, and speed for each interface
+ type: complex
+ sample:
+ - [{ "controller": "A", "interface_speed": "12g", "interface_type": "sas" }]
+ - [{ "controller": "B", "interface_speed": "10g", "interface_type": "iscsi" }]
+ netapp_enabled_features:
+ description: specifies the enabled features on the storage array.
+ returned: on success
+ type: complex
+ sample:
+ - [ "flashReadCache", "performanceTier", "protectionInformation", "secureVolume" ]
+ netapp_host_groups:
+ description: specifies the host groups on the storage arrays.
+ returned: on success
+ type: complex
+ sample:
+ - [{ "id": "85000000600A098000A4B28D003610705C40B964", "name": "group1" }]
+ netapp_hosts:
+ description: specifies the hosts on the storage arrays.
+ returned: on success
+ type: complex
+ sample:
+ - [{ "id": "8203800000000000000000000000000000000000",
+ "name": "host1",
+ "group_id": "85000000600A098000A4B28D003610705C40B964",
+ "host_type_index": 28,
+ "ports": [{ "type": "fc", "address": "1000FF7CFFFFFF01", "label": "FC_1" },
+ { "type": "fc", "address": "1000FF7CFFFFFF00", "label": "FC_2" }]}]
+ netapp_host_types:
+ description: lists the available host types on the storage array.
+ returned: on success
+ type: complex
+ sample:
+ - [{ "index": 0, "type": "FactoryDefault" },
+ { "index": 1, "type": "W2KNETNCL"},
+ { "index": 2, "type": "SOL" },
+ { "index": 5, "type": "AVT_4M" },
+ { "index": 6, "type": "LNX" },
+ { "index": 7, "type": "LnxALUA" },
+ { "index": 8, "type": "W2KNETCL" },
+ { "index": 9, "type": "AIX MPIO" },
+ { "index": 10, "type": "VmwTPGSALUA" },
+ { "index": 15, "type": "HPXTPGS" },
+ { "index": 17, "type": "SolTPGSALUA" },
+ { "index": 18, "type": "SVC" },
+ { "index": 22, "type": "MacTPGSALUA" },
+ { "index": 23, "type": "WinTPGSALUA" },
+ { "index": 24, "type": "LnxTPGSALUA" },
+ { "index": 25, "type": "LnxTPGSALUA_PM" },
+ { "index": 26, "type": "ONTAP_ALUA" },
+ { "index": 27, "type": "LnxTPGSALUA_SF" },
+ { "index": 28, "type": "LnxDHALUA" },
+ { "index": 29, "type": "ATTOClusterAllOS" }]
+ netapp_hostside_interfaces:
+ description: host side interface list that contains identification, configuration, type, speed, and
+ status information for each interface
+ type: complex
+ sample:
+ - [{"iscsi":
+ [{ "controller": "A",
+ "current_interface_speed": "10g",
+ "ipv4_address": "10.10.10.1",
+ "ipv4_enabled": true,
+ "ipv4_gateway": "10.10.10.1",
+ "ipv4_subnet_mask": "255.255.255.0",
+ "ipv6_enabled": false,
+ "iqn": "iqn.1996-03.com.netapp:2806.600a098000a81b6d0000000059d60c76",
+ "link_status": "up",
+ "mtu": 9000,
+ "supported_interface_speeds": [ "10g" ] }]}]
+ netapp_management_interfaces:
+ description: management interface list that contains identification, configuration, and status for
+ each interface
+ type: complex
+ sample:
+ - [{"alias": "ict-2800-A",
+ "channel": 1,
+ "controller": "A",
+ "dns_config_method": "dhcp",
+ "dns_servers": [],
+ "ipv4_address": "10.1.1.1",
+ "ipv4_address_config_method": "static",
+ "ipv4_enabled": true,
+ "ipv4_gateway": "10.113.1.1",
+ "ipv4_subnet_mask": "255.255.255.0",
+ "ipv6_enabled": false,
+ "link_status": "up",
+ "mac_address": "00A098A81B5D",
+ "name": "wan0",
+ "ntp_config_method": "disabled",
+ "ntp_servers": [],
+ "remote_ssh_access": false }]
+ netapp_storage_array:
+ description: provides storage array identification, firmware version, and available capabilities
+ type: dict
+ sample:
+ - {"chassis_serial": "021540006043",
+ "firmware": "08.40.00.01",
+ "name": "ict-2800-11_40",
+ "wwn": "600A098000A81B5D0000000059D60C76",
+ "cacheBlockSizes": [4096,
+ 8192,
+ 16384,
+ 32768],
+ "supportedSegSizes": [8192,
+ 16384,
+ 32768,
+ 65536,
+ 131072,
+ 262144,
+ 524288]}
+ netapp_storage_pools:
+ description: storage pool list that contains identification and capacity information for each pool
+ type: complex
+ sample:
+ - [{"available_capacity": "3490353782784",
+ "id": "04000000600A098000A81B5D000002B45A953A61",
+ "name": "Raid6",
+ "total_capacity": "5399466745856",
+ "used_capacity": "1909112963072" }]
+ netapp_volumes:
+ description: storage volume list that contains identification and capacity information for each volume
+ type: complex
+ sample:
+ - [{"capacity": "5368709120",
+ "id": "02000000600A098000AAC0C3000002C45A952BAA",
+ "is_thin_provisioned": false,
+ "name": "5G",
+ "parent_storage_pool_id": "04000000600A098000A81B5D000002B45A953A61" }]
+ netapp_workload_tags:
+ description: workload tag list
+ type: complex
+ sample:
+ - [{"id": "87e19568-43fb-4d8d-99ea-2811daaa2b38",
+ "name": "ftp_server",
+ "workloadAttributes": [{"key": "use",
+ "value": "general"}]}]
+ netapp_volumes_by_initiators:
+ description: list of available volumes keyed by the mapped initiators.
+ type: complex
+ sample:
+ - {"192_168_1_1": [{"id": "02000000600A098000A4B9D1000015FD5C8F7F9E",
+ "meta_data": {"filetype": "xfs", "public": true},
+ "name": "some_volume",
+ "workload_name": "test2_volumes",
+ "wwn": "600A098000A4B9D1000015FD5C8F7F9E"}]}
+ snapshot_images:
+ description: snapshot image list that contains identification, capacity, and status information for each
+ snapshot image
+ type: complex
+ sample:
+ - [{"active_cow": true,
+ "creation_method": "user",
+ "id": "34000000600A098000A81B5D00630A965B0535AC",
+ "pit_capacity": "5368709120",
+ "reposity_cap_utilization": "0",
+ "rollback_source": false,
+ "status": "optimal" }]
+"""
+
+from re import match
+from pprint import pformat
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import NetAppESeriesModule
+
+
+class Facts(NetAppESeriesModule):
+ def __init__(self):
+ web_services_version = "02.00.0000.0000"
+ super(Facts, self).__init__(ansible_options={},
+ web_services_version=web_services_version,
+ supports_check_mode=True)
+
+ def get_controllers(self):
+ """Retrieve a mapping of controller references to their labels."""
+ controllers = list()
+ try:
+ rc, controllers = self.request('storage-systems/%s/graph/xpath-filter?query=/controller/id' % self.ssid)
+ except Exception as err:
+ self.module.fail_json(
+ msg="Failed to retrieve controller list! Array Id [%s]. Error [%s]."
+ % (self.ssid, str(err)))
+
+ controllers.sort()
+
+ controllers_dict = {}
+ i = ord('A')
+ for controller in controllers:
+ label = chr(i)
+ controllers_dict[controller] = label
+ i += 1
+
+ return controllers_dict
+
+ def get_array_facts(self):
+ """Extract particular facts from the storage array graph"""
+ facts = dict(facts_from_proxy=(not self.is_embedded()), ssid=self.ssid)
+ controller_reference_label = self.get_controllers()
+ array_facts = None
+
+ # Get the storage array graph
+ try:
+ rc, array_facts = self.request("storage-systems/%s/graph" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to obtain facts from storage array with id [%s]. Error [%s]" % (self.ssid, str(error)))
+
+ facts['netapp_storage_array'] = dict(
+ name=array_facts['sa']['saData']['storageArrayLabel'],
+ chassis_serial=array_facts['sa']['saData']['chassisSerialNumber'],
+ firmware=array_facts['sa']['saData']['fwVersion'],
+ wwn=array_facts['sa']['saData']['saId']['worldWideName'],
+ segment_sizes=array_facts['sa']['featureParameters']['supportedSegSizes'],
+ cache_block_sizes=array_facts['sa']['featureParameters']['cacheBlockSizes'])
+
+ facts['netapp_controllers'] = [
+ dict(
+ name=controller_reference_label[controller['controllerRef']],
+ serial=controller['serialNumber'].strip(),
+ status=controller['status'],
+ ) for controller in array_facts['controller']]
+
+ facts['netapp_host_groups'] = [
+ dict(
+ id=group['id'],
+ name=group['name']
+ ) for group in array_facts['storagePoolBundle']['cluster']]
+
+ facts['netapp_hosts'] = [
+ dict(
+ group_id=host['clusterRef'],
+ hosts_reference=host['hostRef'],
+ id=host['id'],
+ name=host['name'],
+ host_type_index=host['hostTypeIndex'],
+ posts=host['hostSidePorts']
+ ) for host in array_facts['storagePoolBundle']['host']]
+
+ facts['netapp_host_types'] = [
+ dict(
+ type=host_type['hostType'],
+ index=host_type['index']
+ ) for host_type in array_facts['sa']['hostSpecificVals']
+ if 'hostType' in host_type.keys() and host_type['hostType']
+ # This conditional ignores zero-length strings which indicates that the associated host-specific NVSRAM region has been cleared.
+ ]
+ facts['snapshot_images'] = [
+ dict(
+ id=snapshot['id'],
+ status=snapshot['status'],
+ pit_capacity=snapshot['pitCapacity'],
+ creation_method=snapshot['creationMethod'],
+ reposity_cap_utilization=snapshot['repositoryCapacityUtilization'],
+ active_cow=snapshot['activeCOW'],
+ rollback_source=snapshot['isRollbackSource']
+ ) for snapshot in array_facts['highLevelVolBundle']['pit']]
+
+ facts['netapp_disks'] = [
+ dict(
+ id=disk['id'],
+ available=disk['available'],
+ media_type=disk['driveMediaType'],
+ status=disk['status'],
+ usable_bytes=disk['usableCapacity'],
+ tray_ref=disk['physicalLocation']['trayRef'],
+ product_id=disk['productID'],
+ firmware_version=disk['firmwareVersion'],
+ serial_number=disk['serialNumber'].lstrip()
+ ) for disk in array_facts['drive']]
+
+ facts['netapp_management_interfaces'] = [
+ dict(controller=controller_reference_label[controller['controllerRef']],
+ name=iface['ethernet']['interfaceName'],
+ alias=iface['ethernet']['alias'],
+ channel=iface['ethernet']['channel'],
+ mac_address=iface['ethernet']['macAddr'],
+ remote_ssh_access=iface['ethernet']['rloginEnabled'],
+ link_status=iface['ethernet']['linkStatus'],
+ ipv4_enabled=iface['ethernet']['ipv4Enabled'],
+ ipv4_address_config_method=iface['ethernet']['ipv4AddressConfigMethod'].lower().replace("config", ""),
+ ipv4_address=iface['ethernet']['ipv4Address'],
+ ipv4_subnet_mask=iface['ethernet']['ipv4SubnetMask'],
+ ipv4_gateway=iface['ethernet']['ipv4GatewayAddress'],
+ ipv6_enabled=iface['ethernet']['ipv6Enabled'],
+ dns_config_method=iface['ethernet']['dnsProperties']['acquisitionProperties']['dnsAcquisitionType'],
+ dns_servers=(iface['ethernet']['dnsProperties']['acquisitionProperties']['dnsServers']
+ if iface['ethernet']['dnsProperties']['acquisitionProperties']['dnsServers'] else []),
+ ntp_config_method=iface['ethernet']['ntpProperties']['acquisitionProperties']['ntpAcquisitionType'],
+ ntp_servers=(iface['ethernet']['ntpProperties']['acquisitionProperties']['ntpServers']
+ if iface['ethernet']['ntpProperties']['acquisitionProperties']['ntpServers'] else [])
+ ) for controller in array_facts['controller'] for iface in controller['netInterfaces']]
+
+ facts['netapp_hostside_interfaces'] = [
+ dict(
+ fc=[dict(controller=controller_reference_label[controller['controllerRef']],
+ channel=iface['fibre']['channel'],
+ link_status=iface['fibre']['linkStatus'],
+ current_interface_speed=strip_interface_speed(iface['fibre']['currentInterfaceSpeed']),
+ maximum_interface_speed=strip_interface_speed(iface['fibre']['maximumInterfaceSpeed']))
+ for controller in array_facts['controller']
+ for iface in controller['hostInterfaces']
+ if iface['interfaceType'] == 'fc'],
+ ib=[dict(controller=controller_reference_label[controller['controllerRef']],
+ channel=iface['ib']['channel'],
+ link_status=iface['ib']['linkState'],
+ mtu=iface['ib']['maximumTransmissionUnit'],
+ current_interface_speed=strip_interface_speed(iface['ib']['currentSpeed']),
+ maximum_interface_speed=strip_interface_speed(iface['ib']['supportedSpeed']))
+ for controller in array_facts['controller']
+ for iface in controller['hostInterfaces']
+ if iface['interfaceType'] == 'ib'],
+ iscsi=[dict(controller=controller_reference_label[controller['controllerRef']],
+ iqn=iface['iscsi']['iqn'],
+ link_status=iface['iscsi']['interfaceData']['ethernetData']['linkStatus'],
+ ipv4_enabled=iface['iscsi']['ipv4Enabled'],
+ ipv4_address=iface['iscsi']['ipv4Data']['ipv4AddressData']['ipv4Address'],
+ ipv4_subnet_mask=iface['iscsi']['ipv4Data']['ipv4AddressData']['ipv4SubnetMask'],
+ ipv4_gateway=iface['iscsi']['ipv4Data']['ipv4AddressData']['ipv4GatewayAddress'],
+ ipv6_enabled=iface['iscsi']['ipv6Enabled'],
+ mtu=iface['iscsi']['interfaceData']['ethernetData']['maximumFramePayloadSize'],
+ current_interface_speed=strip_interface_speed(iface['iscsi']['interfaceData']
+ ['ethernetData']['currentInterfaceSpeed']),
+ supported_interface_speeds=strip_interface_speed(iface['iscsi']['interfaceData']
+ ['ethernetData']
+ ['supportedInterfaceSpeeds']))
+ for controller in array_facts['controller']
+ for iface in controller['hostInterfaces']
+ if iface['interfaceType'] == 'iscsi'],
+ sas=[dict(controller=controller_reference_label[controller['controllerRef']],
+ channel=iface['sas']['channel'],
+ current_interface_speed=strip_interface_speed(iface['sas']['currentInterfaceSpeed']),
+ maximum_interface_speed=strip_interface_speed(iface['sas']['maximumInterfaceSpeed']),
+ link_status=iface['sas']['iocPort']['state'])
+ for controller in array_facts['controller']
+ for iface in controller['hostInterfaces']
+ if iface['interfaceType'] == 'sas'])]
+
+ facts['netapp_driveside_interfaces'] = [
+ dict(
+ controller=controller_reference_label[controller['controllerRef']],
+ interface_type=interface['interfaceType'],
+ interface_speed=strip_interface_speed(
+ interface[interface['interfaceType']]['maximumInterfaceSpeed']
+ if (interface['interfaceType'] == 'sata' or
+ interface['interfaceType'] == 'sas' or
+ interface['interfaceType'] == 'fibre')
+ else (
+ interface[interface['interfaceType']]['currentSpeed']
+ if interface['interfaceType'] == 'ib'
+ else (
+ interface[interface['interfaceType']]['interfaceData']['maximumInterfaceSpeed']
+ if interface['interfaceType'] == 'iscsi' else 'unknown'
+ ))),
+ )
+ for controller in array_facts['controller']
+ for interface in controller['driveInterfaces']]
+
+ facts['netapp_storage_pools'] = [
+ dict(
+ id=storage_pool['id'],
+ name=storage_pool['name'],
+ available_capacity=storage_pool['freeSpace'],
+ total_capacity=storage_pool['totalRaidedSpace'],
+ used_capacity=storage_pool['usedSpace']
+ ) for storage_pool in array_facts['volumeGroup']]
+
+ all_volumes = list(array_facts['volume'])
+
+ facts['netapp_volumes'] = [
+ dict(
+ id=v['id'],
+ name=v['name'],
+ parent_storage_pool_id=v['volumeGroupRef'],
+ capacity=v['capacity'],
+ is_thin_provisioned=v['thinProvisioned'],
+ workload=v['metadata'],
+ ) for v in all_volumes]
+
+ workload_tags = None
+ try:
+ rc, workload_tags = self.request("storage-systems/%s/workloads" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve workload tags. Array [%s]." % self.ssid)
+
+ facts['netapp_workload_tags'] = [
+ dict(
+ id=workload_tag['id'],
+ name=workload_tag['name'],
+ attributes=workload_tag['workloadAttributes']
+ ) for workload_tag in workload_tags]
+
+ # Create a dictionary of volume lists keyed by host names
+ facts['netapp_volumes_by_initiators'] = dict()
+ for mapping in array_facts['storagePoolBundle']['lunMapping']:
+ for host in facts['netapp_hosts']:
+ if mapping['mapRef'] == host['hosts_reference'] or mapping['mapRef'] == host['group_id']:
+ if host['name'] not in facts['netapp_volumes_by_initiators'].keys():
+ facts['netapp_volumes_by_initiators'].update({host['name']: []})
+
+ for volume in all_volumes:
+ if mapping['id'] in [volume_mapping['id'] for volume_mapping in volume['listOfMappings']]:
+
+ # Determine workload name if there is one
+ workload_name = ""
+ metadata = dict()
+ for volume_tag in volume['metadata']:
+ if volume_tag['key'] == 'workloadId':
+ for workload_tag in facts['netapp_workload_tags']:
+ if volume_tag['value'] == workload_tag['id']:
+ workload_name = workload_tag['name']
+ metadata = dict((entry['key'], entry['value'])
+ for entry in workload_tag['attributes']
+ if entry['key'] != 'profileId')
+
+ facts['netapp_volumes_by_initiators'][host['name']].append(
+ dict(name=volume['name'],
+ id=volume['id'],
+ wwn=volume['wwn'],
+ workload_name=workload_name,
+ meta_data=metadata))
+
+ features = [feature for feature in array_facts['sa']['capabilities']]
+ features.extend([feature['capability'] for feature in array_facts['sa']['premiumFeatures']
+ if feature['isEnabled']])
+ features = list(set(features)) # ensure unique
+ features.sort()
+ facts['netapp_enabled_features'] = features
+
+ return facts
+
+ def get_facts(self):
+ """Get the embedded or web services proxy information."""
+ facts = self.get_array_facts()
+
+ self.module.log("isEmbedded: %s" % self.is_embedded())
+ self.module.log(pformat(facts))
+
+ self.module.exit_json(msg="Gathered facts for storage array. Array ID: [%s]." % self.ssid,
+ storage_array_facts=facts)
+
+
+def strip_interface_speed(speed):
+ """Converts symbol interface speeds to a more common notation. Example: 'speed10gig' -> '10g'"""
+ if isinstance(speed, list):
+ result = [match(r"speed[0-9]{1,3}[gm]", sp) for sp in speed]
+ result = [sp.group().replace("speed", "") if result else "unknown" for sp in result if sp]
+ result = ["auto" if match(r"auto", sp) else sp for sp in result]
+ else:
+ result = match(r"speed[0-9]{1,3}[gm]", speed)
+ result = result.group().replace("speed", "") if result else "unknown"
+ result = "auto" if match(r"auto", result.lower()) else result
+ return result
+
+
+def main():
+ facts = Facts()
+ facts.get_facts()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_firmware.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_firmware.py
new file mode 100644
index 00000000..c2f7f745
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_firmware.py
@@ -0,0 +1,488 @@
+#!/usr/bin/python
+
+# (c) 2016, 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
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = """
+---
+module: netapp_e_firmware
+version_added: "2.9"
+short_description: NetApp E-Series manage firmware.
+description:
+ - Ensure specific firmware versions are activated on E-Series storage system.
+author:
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ nvsram:
+ description:
+ - Path to the NVSRAM file.
+ type: str
+ required: true
+ firmware:
+ description:
+ - Path to the firmware file.
+ type: str
+ required: true
+ wait_for_completion:
+ description:
+ - This flag will cause module to wait for any upgrade actions to complete.
+ type: bool
+ default: false
+ ignore_health_check:
+ description:
+ - This flag will force firmware to be activated in spite of the health check.
+ - Use at your own risk. Certain non-optimal states could result in data loss.
+ type: bool
+ default: false
+"""
+EXAMPLES = """
+- name: Ensure correct firmware versions
+ netapp_e_firmware:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ nvsram: "path/to/nvsram"
+ bundle: "path/to/bundle"
+ wait_for_completion: true
+- name: Ensure correct firmware versions
+ netapp_e_firmware:
+ ssid: "1"
+ api_url: "https://192.168.1.100:8443/devmgr/v2"
+ api_username: "admin"
+ api_password: "adminpass"
+ validate_certs: true
+ nvsram: "path/to/nvsram"
+ firmware: "path/to/firmware"
+"""
+RETURN = """
+msg:
+ description: Status and version of firmware and NVSRAM.
+ type: str
+ returned: always
+ sample:
+"""
+import os
+
+from time import sleep
+from ansible.module_utils import six
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import NetAppESeriesModule, create_multipart_formdata, request
+from ansible.module_utils._text import to_native, to_text, to_bytes
+
+
+class NetAppESeriesFirmware(NetAppESeriesModule):
+ HEALTH_CHECK_TIMEOUT_MS = 120000
+ REBOOT_TIMEOUT_SEC = 15 * 60
+ FIRMWARE_COMPATIBILITY_CHECK_TIMEOUT_SEC = 60
+ DEFAULT_TIMEOUT = 60 * 15 # This will override the NetAppESeriesModule request method timeout.
+
+ def __init__(self):
+ ansible_options = dict(
+ nvsram=dict(type="str", required=True),
+ firmware=dict(type="str", required=True),
+ wait_for_completion=dict(type="bool", default=False),
+ ignore_health_check=dict(type="bool", default=False))
+
+ super(NetAppESeriesFirmware, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ supports_check_mode=True)
+
+ args = self.module.params
+ self.nvsram = args["nvsram"]
+ self.firmware = args["firmware"]
+ self.wait_for_completion = args["wait_for_completion"]
+ self.ignore_health_check = args["ignore_health_check"]
+
+ self.nvsram_name = None
+ self.firmware_name = None
+ self.is_bundle_cache = None
+ self.firmware_version_cache = None
+ self.nvsram_version_cache = None
+ self.upgrade_required = False
+ self.upgrade_in_progress = False
+ self.module_info = dict()
+
+ self.nvsram_name = os.path.basename(self.nvsram)
+ self.firmware_name = os.path.basename(self.firmware)
+
+ def is_firmware_bundled(self):
+ """Determine whether supplied firmware is bundle."""
+ if self.is_bundle_cache is None:
+ with open(self.firmware, "rb") as fh:
+ signature = fh.read(16).lower()
+
+ if b"firmware" in signature:
+ self.is_bundle_cache = False
+ elif b"combined_content" in signature:
+ self.is_bundle_cache = True
+ else:
+ self.module.fail_json(msg="Firmware file is invalid. File [%s]. Array [%s]" % (self.firmware, self.ssid))
+
+ return self.is_bundle_cache
+
+ def firmware_version(self):
+ """Retrieve firmware version of the firmware file. Return: bytes string"""
+ if self.firmware_version_cache is None:
+
+ # Search firmware file for bundle or firmware version
+ with open(self.firmware, "rb") as fh:
+ line = fh.readline()
+ while line:
+ if self.is_firmware_bundled():
+ if b'displayableAttributeList=' in line:
+ for item in line[25:].split(b','):
+ key, value = item.split(b"|")
+ if key == b'VERSION':
+ self.firmware_version_cache = value.strip(b"\n")
+ break
+ elif b"Version:" in line:
+ self.firmware_version_cache = line.split()[-1].strip(b"\n")
+ break
+ line = fh.readline()
+ else:
+ self.module.fail_json(msg="Failed to determine firmware version. File [%s]. Array [%s]." % (self.firmware, self.ssid))
+ return self.firmware_version_cache
+
+ def nvsram_version(self):
+ """Retrieve NVSRAM version of the NVSRAM file. Return: byte string"""
+ if self.nvsram_version_cache is None:
+
+ with open(self.nvsram, "rb") as fh:
+ line = fh.readline()
+ while line:
+ if b".NVSRAM Configuration Number" in line:
+ self.nvsram_version_cache = line.split(b'"')[-2]
+ break
+ line = fh.readline()
+ else:
+ self.module.fail_json(msg="Failed to determine NVSRAM file version. File [%s]. Array [%s]." % (self.nvsram, self.ssid))
+ return self.nvsram_version_cache
+
+ def check_system_health(self):
+ """Ensure E-Series storage system is healthy. Works for both embedded and proxy web services."""
+ try:
+ rc, request_id = self.request("health-check", method="POST", data={"onlineOnly": True, "storageDeviceIds": [self.ssid]})
+
+ while True:
+ sleep(1)
+
+ try:
+ rc, response = self.request("health-check?requestId=%s" % request_id["requestId"])
+
+ if not response["healthCheckRunning"]:
+ return response["results"][0]["successful"]
+ elif int(response["results"][0]["processingTimeMS"]) > self.HEALTH_CHECK_TIMEOUT_MS:
+ self.module.fail_json(msg="Health check failed to complete. Array Id [%s]." % self.ssid)
+
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve health check status. Array Id [%s]. Error[%s]." % (self.ssid, to_native(error)))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to initiate health check. Array Id [%s]. Error[%s]." % (self.ssid, to_native(error)))
+
+ self.module.fail_json(msg="Failed to retrieve health check status. Array Id [%s]. Error[%s]." % self.ssid)
+
+ def embedded_check_compatibility(self):
+ """Verify files are compatible with E-Series storage system."""
+ self.embedded_check_nvsram_compatibility()
+ self.embedded_check_bundle_compatibility()
+
+ def embedded_check_nvsram_compatibility(self):
+ """Verify the provided NVSRAM is compatible with E-Series storage system."""
+
+ # Check nvsram compatibility
+ try:
+ files = [("nvsramimage", self.nvsram_name, self.nvsram)]
+ headers, data = create_multipart_formdata(files=files)
+
+ rc, nvsram_compatible = self.request("firmware/embedded-firmware/%s/nvsram-compatibility-check" % self.ssid,
+ method="POST", data=data, headers=headers)
+
+ if not nvsram_compatible["signatureTestingPassed"]:
+ self.module.fail_json(msg="Invalid NVSRAM file. File [%s]." % self.nvsram)
+ if not nvsram_compatible["fileCompatible"]:
+ self.module.fail_json(msg="Incompatible NVSRAM file. File [%s]." % self.nvsram)
+
+ # Determine whether nvsram is required
+ for module in nvsram_compatible["versionContents"]:
+ if module["bundledVersion"] != module["onboardVersion"]:
+ self.upgrade_required = True
+
+ # Update bundle info
+ self.module_info.update({module["module"]: {"onboard_version": module["onboardVersion"], "bundled_version": module["bundledVersion"]}})
+
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve NVSRAM compatibility results. Array Id [%s]. Error[%s]." % (self.ssid, to_native(error)))
+
+ def embedded_check_bundle_compatibility(self):
+ """Verify the provided firmware bundle is compatible with E-Series storage system."""
+ try:
+ files = [("files[]", "blob", self.firmware)]
+ headers, data = create_multipart_formdata(files=files, send_8kb=True)
+ rc, bundle_compatible = self.request("firmware/embedded-firmware/%s/bundle-compatibility-check" % self.ssid,
+ method="POST", data=data, headers=headers)
+
+ # Determine whether valid and compatible firmware
+ if not bundle_compatible["signatureTestingPassed"]:
+ self.module.fail_json(msg="Invalid firmware bundle file. File [%s]." % self.firmware)
+ if not bundle_compatible["fileCompatible"]:
+ self.module.fail_json(msg="Incompatible firmware bundle file. File [%s]." % self.firmware)
+
+ # Determine whether upgrade is required
+ for module in bundle_compatible["versionContents"]:
+
+ bundle_module_version = module["bundledVersion"].split(".")
+ onboard_module_version = module["onboardVersion"].split(".")
+ version_minimum_length = min(len(bundle_module_version), len(onboard_module_version))
+ if bundle_module_version[:version_minimum_length] != onboard_module_version[:version_minimum_length]:
+ self.upgrade_required = True
+
+ # Check whether downgrade is being attempted
+ bundle_version = module["bundledVersion"].split(".")[:2]
+ onboard_version = module["onboardVersion"].split(".")[:2]
+ if bundle_version[0] < onboard_version[0] or (bundle_version[0] == onboard_version[0] and bundle_version[1] < onboard_version[1]):
+ self.module.fail_json(msg="Downgrades are not permitted. onboard [%s] > bundled[%s]."
+ % (module["onboardVersion"], module["bundledVersion"]))
+
+ # Update bundle info
+ self.module_info.update({module["module"]: {"onboard_version": module["onboardVersion"], "bundled_version": module["bundledVersion"]}})
+
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve bundle compatibility results. Array Id [%s]. Error[%s]." % (self.ssid, to_native(error)))
+
+ def embedded_wait_for_upgrade(self):
+ """Wait for SANtricity Web Services Embedded to be available after reboot."""
+ for count in range(0, self.REBOOT_TIMEOUT_SEC):
+ try:
+ rc, response = self.request("storage-systems/%s/graph/xpath-filter?query=/sa/saData" % self.ssid)
+ bundle_display = [m["versionString"] for m in response[0]["extendedSAData"]["codeVersions"] if m["codeModule"] == "bundleDisplay"][0]
+ if rc == 200 and six.b(bundle_display) == self.firmware_version() and six.b(response[0]["nvsramVersion"]) == self.nvsram_version():
+ self.upgrade_in_progress = False
+ break
+ except Exception as error:
+ pass
+ sleep(1)
+ else:
+ self.module.fail_json(msg="Timeout waiting for Santricity Web Services Embedded. Array [%s]" % self.ssid)
+
+ def embedded_upgrade(self):
+ """Upload and activate both firmware and NVSRAM."""
+ files = [("nvsramfile", self.nvsram_name, self.nvsram),
+ ("dlpfile", self.firmware_name, self.firmware)]
+ headers, data = create_multipart_formdata(files=files)
+ try:
+ rc, response = self.request("firmware/embedded-firmware?staged=false&nvsram=true", method="POST", data=data, headers=headers)
+ self.upgrade_in_progress = True
+ except Exception as error:
+ self.module.fail_json(msg="Failed to upload and activate firmware. Array Id [%s]. Error[%s]." % (self.ssid, to_native(error)))
+ if self.wait_for_completion:
+ self.embedded_wait_for_upgrade()
+
+ def proxy_check_nvsram_compatibility(self):
+ """Verify nvsram is compatible with E-Series storage system."""
+ data = {"storageDeviceIds": [self.ssid]}
+ try:
+ rc, check = self.request("firmware/compatibility-check", method="POST", data=data)
+ for count in range(0, int((self.FIRMWARE_COMPATIBILITY_CHECK_TIMEOUT_SEC / 5))):
+ sleep(5)
+ try:
+ rc, response = self.request("firmware/compatibility-check?requestId=%s" % check["requestId"])
+ if not response["checkRunning"]:
+ for result in response["results"][0]["nvsramFiles"]:
+ if result["filename"] == self.nvsram_name:
+ return
+ self.module.fail_json(msg="NVSRAM is not compatible. NVSRAM [%s]. Array [%s]." % (self.nvsram_name, self.ssid))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve NVSRAM status update from proxy. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to receive NVSRAM compatibility information. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ def proxy_check_firmware_compatibility(self):
+ """Verify firmware is compatible with E-Series storage system."""
+ data = {"storageDeviceIds": [self.ssid]}
+ try:
+ rc, check = self.request("firmware/compatibility-check", method="POST", data=data)
+ for count in range(0, int((self.FIRMWARE_COMPATIBILITY_CHECK_TIMEOUT_SEC / 5))):
+ sleep(5)
+ try:
+ rc, response = self.request("firmware/compatibility-check?requestId=%s" % check["requestId"])
+ if not response["checkRunning"]:
+ for result in response["results"][0]["cfwFiles"]:
+ if result["filename"] == self.firmware_name:
+ return
+ self.module.fail_json(msg="Firmware bundle is not compatible. firmware [%s]. Array [%s]." % (self.firmware_name, self.ssid))
+
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve firmware status update from proxy. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to receive firmware compatibility information. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ def proxy_upload_and_check_compatibility(self):
+ """Ensure firmware is uploaded and verify compatibility."""
+ try:
+ rc, cfw_files = self.request("firmware/cfw-files")
+ for file in cfw_files:
+ if file["filename"] == self.nvsram_name:
+ break
+ else:
+ fields = [("validate", "true")]
+ files = [("firmwareFile", self.nvsram_name, self.nvsram)]
+ headers, data = create_multipart_formdata(files=files, fields=fields)
+ try:
+ rc, response = self.request("firmware/upload", method="POST", data=data, headers=headers)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to upload NVSRAM file. File [%s]. Array [%s]. Error [%s]."
+ % (self.nvsram_name, self.ssid, to_native(error)))
+
+ self.proxy_check_nvsram_compatibility()
+
+ for file in cfw_files:
+ if file["filename"] == self.firmware_name:
+ break
+ else:
+ fields = [("validate", "true")]
+ files = [("firmwareFile", self.firmware_name, self.firmware)]
+ headers, data = create_multipart_formdata(files=files, fields=fields)
+ try:
+ rc, response = self.request("firmware/upload", method="POST", data=data, headers=headers)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to upload firmware bundle file. File [%s]. Array [%s]. Error [%s]."
+ % (self.firmware_name, self.ssid, to_native(error)))
+
+ self.proxy_check_firmware_compatibility()
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve existing existing firmware files. Error [%s]" % to_native(error))
+
+ def proxy_check_upgrade_required(self):
+ """Staging is required to collect firmware information from the web services proxy."""
+ # Verify controller consistency and get firmware versions
+ try:
+ # Retrieve current bundle version
+ if self.is_firmware_bundled():
+ rc, response = self.request("storage-systems/%s/graph/xpath-filter?query=/controller/codeVersions[codeModule='bundleDisplay']" % self.ssid)
+ current_firmware_version = six.b(response[0]["versionString"])
+ else:
+ rc, response = self.request("storage-systems/%s/graph/xpath-filter?query=/sa/saData/fwVersion" % self.ssid)
+ current_firmware_version = six.b(response[0])
+
+ # Determine whether upgrade is required
+ if current_firmware_version != self.firmware_version():
+
+ current = current_firmware_version.split(b".")[:2]
+ upgrade = self.firmware_version().split(b".")[:2]
+ if current[0] < upgrade[0] or (current[0] == upgrade[0] and current[1] <= upgrade[1]):
+ self.upgrade_required = True
+ else:
+ self.module.fail_json(msg="Downgrades are not permitted. Firmware [%s]. Array [%s]." % (self.firmware, self.ssid))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve controller firmware information. Array [%s]. Error [%s]" % (self.ssid, to_native(error)))
+ # Determine current NVSRAM version and whether change is required
+ try:
+ rc, response = self.request("storage-systems/%s/graph/xpath-filter?query=/sa/saData/nvsramVersion" % self.ssid)
+ if six.b(response[0]) != self.nvsram_version():
+ self.upgrade_required = True
+
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve storage system's NVSRAM version. Array [%s]. Error [%s]" % (self.ssid, to_native(error)))
+
+ def proxy_wait_for_upgrade(self, request_id):
+ """Wait for SANtricity Web Services Proxy to report upgrade complete"""
+ if self.is_firmware_bundled():
+ while True:
+ try:
+ sleep(5)
+ rc, response = self.request("batch/cfw-upgrade/%s" % request_id)
+
+ if response["status"] == "complete":
+ self.upgrade_in_progress = False
+ break
+ elif response["status"] in ["failed", "cancelled"]:
+ self.module.fail_json(msg="Firmware upgrade failed to complete. Array [%s]." % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve firmware upgrade status. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+ else:
+ for count in range(0, int(self.REBOOT_TIMEOUT_SEC / 5)):
+ try:
+ sleep(5)
+ rc_firmware, firmware = self.request("storage-systems/%s/graph/xpath-filter?query=/sa/saData/fwVersion" % self.ssid)
+ rc_nvsram, nvsram = self.request("storage-systems/%s/graph/xpath-filter?query=/sa/saData/nvsramVersion" % self.ssid)
+
+ if six.b(firmware[0]) == self.firmware_version() and six.b(nvsram[0]) == self.nvsram_version():
+ self.upgrade_in_progress = False
+ break
+ except Exception as error:
+ pass
+ else:
+ self.module.fail_json(msg="Timed out waiting for firmware upgrade to complete. Array [%s]." % self.ssid)
+
+ def proxy_upgrade(self):
+ """Activate previously uploaded firmware related files."""
+ request_id = None
+ if self.is_firmware_bundled():
+ data = {"activate": True,
+ "firmwareFile": self.firmware_name,
+ "nvsramFile": self.nvsram_name,
+ "systemInfos": [{"systemId": self.ssid,
+ "allowNonOptimalActivation": self.ignore_health_check}]}
+ try:
+ rc, response = self.request("batch/cfw-upgrade", method="POST", data=data)
+ request_id = response["requestId"]
+ except Exception as error:
+ self.module.fail_json(msg="Failed to initiate firmware upgrade. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ else:
+ data = {"stageFirmware": False,
+ "skipMelCheck": self.ignore_health_check,
+ "cfwFile": self.firmware_name,
+ "nvsramFile": self.nvsram_name}
+ try:
+ rc, response = self.request("storage-systems/%s/cfw-upgrade" % self.ssid, method="POST", data=data)
+ request_id = response["requestId"]
+ except Exception as error:
+ self.module.fail_json(msg="Failed to initiate firmware upgrade. Array [%s]. Error [%s]." % (self.ssid, to_native(error)))
+
+ self.upgrade_in_progress = True
+ if self.wait_for_completion:
+ self.proxy_wait_for_upgrade(request_id)
+
+ def apply(self):
+ """Upgrade controller firmware."""
+ self.check_system_health()
+
+ # Verify firmware compatibility and whether changes are required
+ if self.is_embedded():
+ self.embedded_check_compatibility()
+ else:
+ self.proxy_check_upgrade_required()
+
+ # This will upload the firmware files to the web services proxy but not to the controller
+ if self.upgrade_required:
+ self.proxy_upload_and_check_compatibility()
+
+ # Perform upgrade
+ if self.upgrade_required and not self.module.check_mode:
+ if self.is_embedded():
+ self.embedded_upgrade()
+ else:
+ self.proxy_upgrade()
+
+ self.module.exit_json(changed=self.upgrade_required, upgrade_in_process=self.upgrade_in_progress, status=self.module_info)
+
+
+def main():
+ firmware = NetAppESeriesFirmware()
+ firmware.apply()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_flashcache.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_flashcache.py
new file mode 100644
index 00000000..3ffacedd
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_flashcache.py
@@ -0,0 +1,442 @@
+#!/usr/bin/python
+
+# (c) 2016, 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
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+
+DOCUMENTATION = '''
+module: netapp_e_flashcache
+author: Kevin Hulquest (@hulquest)
+version_added: '2.2'
+short_description: NetApp E-Series manage SSD caches
+description:
+- Create or remove SSD caches on a NetApp E-Series storage array.
+options:
+ api_username:
+ required: true
+ type: str
+ description:
+ - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API.
+ api_password:
+ required: true
+ type: str
+ description:
+ - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API.
+ api_url:
+ required: true
+ type: str
+ description:
+ - The url to the SANtricity WebServices Proxy or embedded REST API.
+ validate_certs:
+ required: false
+ default: true
+ description:
+ - Should https certificates be validated?
+ type: bool
+ ssid:
+ required: true
+ type: str
+ description:
+ - The ID of the array to manage (as configured on the web services proxy).
+ state:
+ required: true
+ type: str
+ description:
+ - Whether the specified SSD cache should exist or not.
+ choices: ['present', 'absent']
+ default: present
+ name:
+ required: true
+ type: str
+ description:
+ - The name of the SSD cache to manage
+ io_type:
+ description:
+ - The type of workload to optimize the cache for.
+ choices: ['filesystem','database','media']
+ default: filesystem
+ type: str
+ disk_count:
+ type: int
+ description:
+ - The minimum number of disks to use for building the cache. The cache will be expanded if this number exceeds the number of disks already in place
+ disk_refs:
+ description:
+ - List of disk references
+ type: list
+ size_unit:
+ description:
+ - The unit to be applied to size arguments
+ choices: ['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb']
+ default: gb
+ type: str
+ cache_size_min:
+ description:
+ - The minimum size (in size_units) of the ssd cache. The cache will be expanded if this exceeds the current size of the cache.
+ type: int
+ criteria_disk_phy_type:
+ description:
+ - Type of physical disk
+ choices: ['sas', 'sas4k', 'fibre', 'fibre520b', 'scsi', 'sata', 'pata']
+ type: str
+ log_mode:
+ type: str
+ description:
+ - Log mode
+ log_path:
+ type: str
+ description:
+ - Log path
+'''
+
+EXAMPLES = """
+ - name: Flash Cache
+ netapp_e_flashcache:
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ validate_certs: "{{ netapp_api_validate_certs }}"
+ name: SSDCacheBuiltByAnsible
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: success
+ type: str
+ sample: json for newly created flash cache
+"""
+import json
+import logging
+import sys
+import traceback
+
+from ansible.module_utils.api import basic_auth_argument_spec
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.six.moves import reduce
+from ansible.module_utils.six.moves.urllib.error import HTTPError
+from ansible.module_utils._text import to_native
+from ansible.module_utils.urls import open_url
+
+
+def request(url, data=None, headers=None, method='GET', use_proxy=True,
+ force=False, last_mod_time=None, timeout=10, validate_certs=True,
+ url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False):
+ try:
+ r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy,
+ force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs,
+ url_username=url_username, url_password=url_password, http_agent=http_agent,
+ force_basic_auth=force_basic_auth)
+ except HTTPError as err:
+ r = err.fp
+
+ try:
+ raw_data = r.read()
+ if raw_data:
+ data = json.loads(raw_data)
+ else:
+ raw_data = None
+ except Exception:
+ if ignore_errors:
+ pass
+ else:
+ raise Exception(raw_data)
+
+ resp_code = r.getcode()
+
+ if resp_code >= 400 and not ignore_errors:
+ raise Exception(resp_code, data)
+ else:
+ return resp_code, data
+
+
+class NetAppESeriesFlashCache(object):
+ def __init__(self):
+ self.name = None
+ self.log_mode = None
+ self.log_path = None
+ self.api_url = None
+ self.api_username = None
+ self.api_password = None
+ self.ssid = None
+ self.validate_certs = None
+ self.disk_count = None
+ self.size_unit = None
+ self.cache_size_min = None
+ self.io_type = None
+ self.driveRefs = None
+ self.state = None
+ self._size_unit_map = dict(
+ bytes=1,
+ b=1,
+ kb=1024,
+ mb=1024 ** 2,
+ gb=1024 ** 3,
+ tb=1024 ** 4,
+ pb=1024 ** 5,
+ eb=1024 ** 6,
+ zb=1024 ** 7,
+ yb=1024 ** 8
+ )
+
+ argument_spec = basic_auth_argument_spec()
+ argument_spec.update(dict(
+ api_username=dict(type='str', required=True),
+ api_password=dict(type='str', required=True, no_log=True),
+ api_url=dict(type='str', required=True),
+ state=dict(default='present', choices=['present', 'absent'], type='str'),
+ ssid=dict(required=True, type='str'),
+ name=dict(required=True, type='str'),
+ disk_count=dict(type='int'),
+ disk_refs=dict(type='list'),
+ cache_size_min=dict(type='int'),
+ io_type=dict(default='filesystem', choices=['filesystem', 'database', 'media']),
+ size_unit=dict(default='gb', choices=['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'],
+ type='str'),
+ criteria_disk_phy_type=dict(choices=['sas', 'sas4k', 'fibre', 'fibre520b', 'scsi', 'sata', 'pata'],
+ type='str'),
+ log_mode=dict(type='str'),
+ log_path=dict(type='str'),
+ ))
+ self.module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_if=[
+
+ ],
+ mutually_exclusive=[
+
+ ],
+ # TODO: update validation for various selection criteria
+ supports_check_mode=True
+ )
+
+ self.__dict__.update(self.module.params)
+
+ # logging setup
+ self._logger = logging.getLogger(self.__class__.__name__)
+ self.debug = self._logger.debug
+
+ if self.log_mode == 'file' and self.log_path:
+ logging.basicConfig(level=logging.DEBUG, filename=self.log_path)
+ elif self.log_mode == 'stderr':
+ logging.basicConfig(level=logging.DEBUG, stream=sys.stderr)
+
+ self.post_headers = dict(Accept="application/json")
+ self.post_headers['Content-Type'] = 'application/json'
+
+ def get_candidate_disks(self, disk_count, size_unit='gb', capacity=None):
+ self.debug("getting candidate disks...")
+
+ drives_req = dict(
+ driveCount=disk_count,
+ sizeUnit=size_unit,
+ driveType='ssd',
+ )
+
+ if capacity:
+ drives_req['targetUsableCapacity'] = capacity
+
+ (rc, drives_resp) = request(self.api_url + "/storage-systems/%s/drives" % (self.ssid),
+ data=json.dumps(drives_req), headers=self.post_headers, method='POST',
+ url_username=self.api_username, url_password=self.api_password,
+ validate_certs=self.validate_certs)
+
+ if rc == 204:
+ self.module.fail_json(msg='Cannot find disks to match requested criteria for ssd cache')
+
+ disk_ids = [d['id'] for d in drives_resp]
+ bytes = reduce(lambda s, d: s + int(d['usableCapacity']), drives_resp, 0)
+
+ return (disk_ids, bytes)
+
+ def create_cache(self):
+ (disk_ids, bytes) = self.get_candidate_disks(disk_count=self.disk_count, size_unit=self.size_unit,
+ capacity=self.cache_size_min)
+
+ self.debug("creating ssd cache...")
+
+ create_fc_req = dict(
+ driveRefs=disk_ids,
+ name=self.name
+ )
+
+ (rc, self.resp) = request(self.api_url + "/storage-systems/%s/flash-cache" % (self.ssid),
+ data=json.dumps(create_fc_req), headers=self.post_headers, method='POST',
+ url_username=self.api_username, url_password=self.api_password,
+ validate_certs=self.validate_certs)
+
+ def update_cache(self):
+ self.debug('updating flash cache config...')
+ update_fc_req = dict(
+ name=self.name,
+ configType=self.io_type
+ )
+
+ (rc, self.resp) = request(self.api_url + "/storage-systems/%s/flash-cache/configure" % (self.ssid),
+ data=json.dumps(update_fc_req), headers=self.post_headers, method='POST',
+ url_username=self.api_username, url_password=self.api_password,
+ validate_certs=self.validate_certs)
+
+ def delete_cache(self):
+ self.debug('deleting flash cache...')
+ (rc, self.resp) = request(self.api_url + "/storage-systems/%s/flash-cache" % (self.ssid), method='DELETE',
+ url_username=self.api_username, url_password=self.api_password,
+ validate_certs=self.validate_certs, ignore_errors=True)
+
+ @property
+ def needs_more_disks(self):
+ if len(self.cache_detail['driveRefs']) < self.disk_count:
+ self.debug("needs resize: current disk count %s < requested requested count %s",
+ len(self.cache_detail['driveRefs']), self.disk_count)
+ return True
+
+ @property
+ def needs_less_disks(self):
+ if len(self.cache_detail['driveRefs']) > self.disk_count:
+ self.debug("needs resize: current disk count %s < requested requested count %s",
+ len(self.cache_detail['driveRefs']), self.disk_count)
+ return True
+
+ @property
+ def current_size_bytes(self):
+ return int(self.cache_detail['fcDriveInfo']['fcWithDrives']['usedCapacity'])
+
+ @property
+ def requested_size_bytes(self):
+ if self.cache_size_min:
+ return self.cache_size_min * self._size_unit_map[self.size_unit]
+ else:
+ return 0
+
+ @property
+ def needs_more_capacity(self):
+ if self.current_size_bytes < self.requested_size_bytes:
+ self.debug("needs resize: current capacity %sb is less than requested minimum %sb",
+ self.current_size_bytes, self.requested_size_bytes)
+ return True
+
+ @property
+ def needs_resize(self):
+ return self.needs_more_disks or self.needs_more_capacity or self.needs_less_disks
+
+ def resize_cache(self):
+ # increase up to disk count first, then iteratively add disks until we meet requested capacity
+
+ # TODO: perform this calculation in check mode
+ current_disk_count = len(self.cache_detail['driveRefs'])
+ proposed_new_disks = 0
+
+ proposed_additional_bytes = 0
+ proposed_disk_ids = []
+
+ if self.needs_more_disks:
+ proposed_disk_count = self.disk_count - current_disk_count
+
+ (disk_ids, bytes) = self.get_candidate_disks(disk_count=proposed_disk_count)
+ proposed_additional_bytes = bytes
+ proposed_disk_ids = disk_ids
+
+ while self.current_size_bytes + proposed_additional_bytes < self.requested_size_bytes:
+ proposed_new_disks += 1
+ (disk_ids, bytes) = self.get_candidate_disks(disk_count=proposed_new_disks)
+ proposed_disk_ids = disk_ids
+ proposed_additional_bytes = bytes
+
+ add_drives_req = dict(
+ driveRef=proposed_disk_ids
+ )
+
+ self.debug("adding drives to flash-cache...")
+ (rc, self.resp) = request(self.api_url + "/storage-systems/%s/flash-cache/addDrives" % (self.ssid),
+ data=json.dumps(add_drives_req), headers=self.post_headers, method='POST',
+ url_username=self.api_username, url_password=self.api_password,
+ validate_certs=self.validate_certs)
+
+ elif self.needs_less_disks and self.driveRefs:
+ rm_drives = dict(driveRef=self.driveRefs)
+ (rc, self.resp) = request(self.api_url + "/storage-systems/%s/flash-cache/removeDrives" % (self.ssid),
+ data=json.dumps(rm_drives), headers=self.post_headers, method='POST',
+ url_username=self.api_username, url_password=self.api_password,
+ validate_certs=self.validate_certs)
+
+ def apply(self):
+ result = dict(changed=False)
+ (rc, cache_resp) = request(self.api_url + "/storage-systems/%s/flash-cache" % (self.ssid),
+ url_username=self.api_username, url_password=self.api_password,
+ validate_certs=self.validate_certs, ignore_errors=True)
+
+ if rc == 200:
+ self.cache_detail = cache_resp
+ else:
+ self.cache_detail = None
+
+ if rc not in [200, 404]:
+ raise Exception(
+ "Unexpected error code %s fetching flash cache detail. Response data was %s" % (rc, cache_resp))
+
+ if self.state == 'present':
+ if self.cache_detail:
+ # TODO: verify parameters against detail for changes
+ if self.cache_detail['name'] != self.name:
+ self.debug("CHANGED: name differs")
+ result['changed'] = True
+ if self.cache_detail['flashCacheBase']['configType'] != self.io_type:
+ self.debug("CHANGED: io_type differs")
+ result['changed'] = True
+ if self.needs_resize:
+ self.debug("CHANGED: resize required")
+ result['changed'] = True
+ else:
+ self.debug("CHANGED: requested state is 'present' but cache does not exist")
+ result['changed'] = True
+ else: # requested state is absent
+ if self.cache_detail:
+ self.debug("CHANGED: requested state is 'absent' but cache exists")
+ result['changed'] = True
+
+ if not result['changed']:
+ self.debug("no changes, exiting...")
+ self.module.exit_json(**result)
+
+ if self.module.check_mode:
+ self.debug("changes pending in check mode, exiting early...")
+ self.module.exit_json(**result)
+
+ if self.state == 'present':
+ if not self.cache_detail:
+ self.create_cache()
+ else:
+ if self.needs_resize:
+ self.resize_cache()
+
+ # run update here as well, since io_type can't be set on creation
+ self.update_cache()
+
+ elif self.state == 'absent':
+ self.delete_cache()
+
+ # TODO: include other details about the storage pool (size, type, id, etc)
+ self.module.exit_json(changed=result['changed'], **self.resp)
+
+
+def main():
+ sp = NetAppESeriesFlashCache()
+ try:
+ sp.apply()
+ except Exception as e:
+ sp.debug("Exception in apply(): \n%s", to_native(e))
+ sp.module.fail_json(msg="Failed to create flash cache. Error[%s]" % to_native(e),
+ exception=traceback.format_exc())
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_global.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_global.py
new file mode 100644
index 00000000..1284b289
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_global.py
@@ -0,0 +1,159 @@
+#!/usr/bin/python
+
+# (c) 2018, 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
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = """
+---
+module: netapp_e_global
+short_description: NetApp E-Series manage global settings configuration
+description:
+ - Allow the user to configure several of the global settings associated with an E-Series storage-system
+version_added: '2.7'
+author: Michael Price (@lmprice)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ name:
+ description:
+ - Set the name of the E-Series storage-system
+ - This label/name doesn't have to be unique.
+ - May be up to 30 characters in length.
+ type: str
+ aliases:
+ - label
+ log_path:
+ description:
+ - A local path to a file to be used for debug logging
+ required: no
+ type: str
+notes:
+ - Check mode is supported.
+ - This module requires Web Services API v1.3 or newer.
+"""
+
+EXAMPLES = """
+ - name: Set the storage-system name
+ netapp_e_global:
+ name: myArrayName
+ api_url: "10.1.1.1:8443"
+ api_username: "admin"
+ api_password: "myPass"
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The settings have been updated.
+name:
+ description:
+ - The current name/label of the storage-system.
+ returned: on success
+ sample: myArrayName
+ type: str
+"""
+import json
+import logging
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import request, eseries_host_argument_spec
+from ansible.module_utils._text import to_native
+
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+}
+
+
+class GlobalSettings(object):
+ def __init__(self):
+ argument_spec = eseries_host_argument_spec()
+ argument_spec.update(dict(
+ name=dict(type='str', required=False, aliases=['label']),
+ log_path=dict(type='str', required=False),
+ ))
+
+ self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, )
+ args = self.module.params
+ self.name = args['name']
+
+ self.ssid = args['ssid']
+ self.url = args['api_url']
+ self.creds = dict(url_password=args['api_password'],
+ validate_certs=args['validate_certs'],
+ url_username=args['api_username'], )
+
+ self.check_mode = self.module.check_mode
+
+ log_path = args['log_path']
+
+ # logging setup
+ self._logger = logging.getLogger(self.__class__.__name__)
+
+ if log_path:
+ logging.basicConfig(
+ level=logging.DEBUG, filename=log_path, filemode='w',
+ format='%(relativeCreated)dms %(levelname)s %(module)s.%(funcName)s:%(lineno)d\n %(message)s')
+
+ if not self.url.endswith('/'):
+ self.url += '/'
+
+ if self.name and len(self.name) > 30:
+ self.module.fail_json(msg="The provided name is invalid, it must be < 30 characters in length.")
+
+ def get_name(self):
+ try:
+ (rc, result) = request(self.url + 'storage-systems/%s' % self.ssid, headers=HEADERS, **self.creds)
+ if result['status'] in ['offline', 'neverContacted']:
+ self.module.fail_json(msg="This storage-system is offline! Array Id [%s]." % (self.ssid))
+ return result['name']
+ except Exception as err:
+ self.module.fail_json(msg="Connection failure! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ def update_name(self):
+ name = self.get_name()
+ update = False
+ if self.name != name:
+ update = True
+
+ body = dict(name=self.name)
+
+ if update and not self.check_mode:
+ try:
+ (rc, result) = request(self.url + 'storage-systems/%s/configuration' % self.ssid, method='POST',
+ data=json.dumps(body), headers=HEADERS, **self.creds)
+ self._logger.info("Set name to %s.", result['name'])
+ # This is going to catch cases like a connection failure
+ except Exception as err:
+ self.module.fail_json(
+ msg="We failed to set the storage-system name! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+ return update
+
+ def update(self):
+ update = self.update_name()
+ name = self.get_name()
+
+ self.module.exit_json(msg="The requested settings have been updated.", changed=update, name=name)
+
+ def __call__(self, *args, **kwargs):
+ self.update()
+
+
+def main():
+ settings = GlobalSettings()
+ settings()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_host.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_host.py
new file mode 100644
index 00000000..3d6b4a78
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_host.py
@@ -0,0 +1,544 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# (c) 2018, 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
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = """
+---
+module: netapp_e_host
+short_description: NetApp E-Series manage eseries hosts
+description: Create, update, remove hosts on NetApp E-series storage arrays
+version_added: '2.2'
+author:
+ - Kevin Hulquest (@hulquest)
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ name:
+ description:
+ - If the host doesn't yet exist, the label/name to assign at creation time.
+ - If the hosts already exists, this will be used to uniquely identify the host to make any required changes
+ required: True
+ type: str
+ aliases:
+ - label
+ state:
+ description:
+ - Set to absent to remove an existing host
+ - Set to present to modify or create a new host definition
+ choices:
+ - absent
+ - present
+ default: present
+ type: str
+ version_added: 2.7
+ host_type:
+ description:
+ - This is the type of host to be mapped
+ - Required when C(state=present)
+ - Either one of the following names can be specified, Linux DM-MP, VMWare, Windows, Windows Clustered, or a
+ host type index which can be found in M(netapp_eseries.santricity.netapp_e_facts)
+ type: str
+ aliases:
+ - host_type_index
+ ports:
+ description:
+ - A list of host ports you wish to associate with the host.
+ - Host ports are uniquely identified by their WWN or IQN. Their assignments to a particular host are
+ uniquely identified by a label and these must be unique.
+ required: False
+ type: list
+ suboptions:
+ type:
+ description:
+ - The interface type of the port to define.
+ - Acceptable choices depend on the capabilities of the target hardware/software platform.
+ required: true
+ choices:
+ - iscsi
+ - sas
+ - fc
+ - ib
+ - nvmeof
+ - ethernet
+ label:
+ description:
+ - A unique label to assign to this port assignment.
+ required: true
+ port:
+ description:
+ - The WWN or IQN of the hostPort to assign to this port definition.
+ required: true
+ force_port:
+ description:
+ - Allow ports that are already assigned to be re-assigned to your current host
+ required: false
+ type: bool
+ version_added: 2.7
+ group:
+ description:
+ - The unique identifier of the host-group you want the host to be a member of; this is used for clustering.
+ required: False
+ type: str
+ aliases:
+ - cluster
+ log_path:
+ description:
+ - A local path to a file to be used for debug logging
+ required: False
+ type: str
+ version_added: 2.7
+"""
+
+EXAMPLES = """
+ - name: Define or update an existing host named 'Host1'
+ netapp_e_host:
+ ssid: "1"
+ api_url: "10.113.1.101:8443"
+ api_username: admin
+ api_password: myPassword
+ name: "Host1"
+ state: present
+ host_type_index: Linux DM-MP
+ ports:
+ - type: 'iscsi'
+ label: 'PORT_1'
+ port: 'iqn.1996-04.de.suse:01:56f86f9bd1fe'
+ - type: 'fc'
+ label: 'FC_1'
+ port: '10:00:FF:7C:FF:FF:FF:01'
+ - type: 'fc'
+ label: 'FC_2'
+ port: '10:00:FF:7C:FF:FF:FF:00'
+
+ - name: Ensure a host named 'Host2' doesn't exist
+ netapp_e_host:
+ ssid: "1"
+ api_url: "10.113.1.101:8443"
+ api_username: admin
+ api_password: myPassword
+ name: "Host2"
+ state: absent
+"""
+
+RETURN = """
+msg:
+ description:
+ - A user-readable description of the actions performed.
+ returned: on success
+ type: str
+ sample: The host has been created.
+id:
+ description:
+ - the unique identifier of the host on the E-Series storage-system
+ returned: on success when state=present
+ type: str
+ sample: 00000000600A098000AAC0C3003004700AD86A52
+ version_added: "2.6"
+
+ssid:
+ description:
+ - the unique identifier of the E-Series storage-system with the current api
+ returned: on success
+ type: str
+ sample: 1
+ version_added: "2.6"
+
+api_url:
+ description:
+ - the url of the API that this request was processed by
+ returned: on success
+ type: str
+ sample: https://webservices.example.com:8443
+ version_added: "2.6"
+"""
+import json
+import logging
+import re
+from pprint import pformat
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import request, eseries_host_argument_spec
+from ansible.module_utils._text import to_native
+
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+}
+
+
+class Host(object):
+ HOST_TYPE_INDEXES = {"linux dm-mp": 28, "vmware": 10, "windows": 1, "windows clustered": 8}
+
+ def __init__(self):
+ argument_spec = eseries_host_argument_spec()
+ argument_spec.update(dict(
+ state=dict(type='str', default='present', choices=['absent', 'present']),
+ group=dict(type='str', required=False, aliases=['cluster']),
+ ports=dict(type='list', required=False),
+ force_port=dict(type='bool', default=False),
+ name=dict(type='str', required=True, aliases=['label']),
+ host_type=dict(type='str', aliases=['host_type_index']),
+ log_path=dict(type='str', required=False),
+ ))
+
+ self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ self.check_mode = self.module.check_mode
+ args = self.module.params
+ self.group = args['group']
+ self.ports = args['ports']
+ self.force_port = args['force_port']
+ self.name = args['name']
+ self.state = args['state']
+ self.ssid = args['ssid']
+ self.url = args['api_url']
+ self.user = args['api_username']
+ self.pwd = args['api_password']
+ self.certs = args['validate_certs']
+
+ self.post_body = dict()
+ self.all_hosts = list()
+ self.host_obj = dict()
+ self.newPorts = list()
+ self.portsForUpdate = list()
+ self.portsForRemoval = list()
+
+ # Update host type with the corresponding index
+ host_type = args['host_type_index']
+ if host_type:
+ host_type = host_type.lower()
+ if host_type in [key.lower() for key in list(self.HOST_TYPE_INDEXES.keys())]:
+ self.host_type_index = self.HOST_TYPE_INDEXES[host_type]
+ elif host_type.isdigit():
+ self.host_type_index = int(args['host_type_index'])
+ else:
+ self.module.fail_json(msg="host_type must be either a host type name or host type index found integer"
+ " the documentation.")
+
+ # logging setup
+ self._logger = logging.getLogger(self.__class__.__name__)
+ if args['log_path']:
+ logging.basicConfig(
+ level=logging.DEBUG, filename=args['log_path'], filemode='w',
+ format='%(relativeCreated)dms %(levelname)s %(module)s.%(funcName)s:%(lineno)d\n %(message)s')
+
+ if not self.url.endswith('/'):
+ self.url += '/'
+
+ # Ensure when state==present then host_type_index is defined
+ if self.state == "present" and self.host_type_index is None:
+ self.module.fail_json(msg="Host_type_index is required when state=='present'. Array Id: [%s]" % self.ssid)
+
+ # Fix port representation if they are provided with colons
+ if self.ports is not None:
+ for port in self.ports:
+ port['label'] = port['label'].lower()
+ port['type'] = port['type'].lower()
+ port['port'] = port['port'].lower()
+
+ # Determine whether address is 16-byte WWPN and, if so, remove
+ if re.match(r'^(0x)?[0-9a-f]{16}$', port['port'].replace(':', '')):
+ port['port'] = port['port'].replace(':', '').replace('0x', '')
+
+ def valid_host_type(self):
+ host_types = None
+ try:
+ (rc, host_types) = request(self.url + 'storage-systems/%s/host-types' % self.ssid, url_password=self.pwd,
+ url_username=self.user, validate_certs=self.certs, headers=HEADERS)
+ except Exception as err:
+ self.module.fail_json(
+ msg="Failed to get host types. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ try:
+ match = list(filter(lambda host_type: host_type['index'] == self.host_type_index, host_types))[0]
+ return True
+ except IndexError:
+ self.module.fail_json(msg="There is no host type with index %s" % self.host_type_index)
+
+ def assigned_host_ports(self, apply_unassigning=False):
+ """Determine if the hostPorts requested have already been assigned and return list of required used ports."""
+ used_host_ports = {}
+ for host in self.all_hosts:
+ if host['label'] != self.name:
+ for host_port in host['hostSidePorts']:
+ for port in self.ports:
+ if port['port'] == host_port["address"] or port['label'] == host_port['label']:
+ if not self.force_port:
+ self.module.fail_json(msg="There are no host ports available OR there are not enough"
+ " unassigned host ports")
+ else:
+ # Determine port reference
+ port_ref = [port["hostPortRef"] for port in host["ports"]
+ if port["hostPortName"] == host_port["address"]]
+ port_ref.extend([port["initiatorRef"] for port in host["initiators"]
+ if port["nodeName"]["iscsiNodeName"] == host_port["address"]])
+
+ # Create dictionary of hosts containing list of port references
+ if host["hostRef"] not in used_host_ports.keys():
+ used_host_ports.update({host["hostRef"]: port_ref})
+ else:
+ used_host_ports[host["hostRef"]].extend(port_ref)
+ else:
+ for host_port in host['hostSidePorts']:
+ for port in self.ports:
+ if ((host_port['label'] == port['label'] and host_port['address'] != port['port']) or
+ (host_port['label'] != port['label'] and host_port['address'] == port['port'])):
+ if not self.force_port:
+ self.module.fail_json(msg="There are no host ports available OR there are not enough"
+ " unassigned host ports")
+ else:
+ # Determine port reference
+ port_ref = [port["hostPortRef"] for port in host["ports"]
+ if port["hostPortName"] == host_port["address"]]
+ port_ref.extend([port["initiatorRef"] for port in host["initiators"]
+ if port["nodeName"]["iscsiNodeName"] == host_port["address"]])
+
+ # Create dictionary of hosts containing list of port references
+ if host["hostRef"] not in used_host_ports.keys():
+ used_host_ports.update({host["hostRef"]: port_ref})
+ else:
+ used_host_ports[host["hostRef"]].extend(port_ref)
+
+ # Unassign assigned ports
+ if apply_unassigning:
+ for host_ref in used_host_ports.keys():
+ try:
+ rc, resp = request(self.url + 'storage-systems/%s/hosts/%s' % (self.ssid, host_ref),
+ url_username=self.user, url_password=self.pwd, headers=HEADERS,
+ validate_certs=self.certs, method='POST',
+ data=json.dumps({"portsToRemove": used_host_ports[host_ref]}))
+ except Exception as err:
+ self.module.fail_json(msg="Failed to unassign host port. Host Id [%s]. Array Id [%s]. Ports [%s]."
+ " Error [%s]." % (self.host_obj['id'], self.ssid,
+ used_host_ports[host_ref], to_native(err)))
+
+ return used_host_ports
+
+ def group_id(self):
+ if self.group:
+ try:
+ (rc, all_groups) = request(self.url + 'storage-systems/%s/host-groups' % self.ssid,
+ url_password=self.pwd,
+ url_username=self.user, validate_certs=self.certs, headers=HEADERS)
+ except Exception as err:
+ self.module.fail_json(
+ msg="Failed to get host groups. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ try:
+ group_obj = list(filter(lambda group: group['name'] == self.group, all_groups))[0]
+ return group_obj['id']
+ except IndexError:
+ self.module.fail_json(msg="No group with the name: %s exists" % self.group)
+ else:
+ # Return the value equivalent of no group
+ return "0000000000000000000000000000000000000000"
+
+ def host_exists(self):
+ """Determine if the requested host exists
+ As a side effect, set the full list of defined hosts in 'all_hosts', and the target host in 'host_obj'.
+ """
+ match = False
+ all_hosts = list()
+
+ try:
+ (rc, all_hosts) = request(self.url + 'storage-systems/%s/hosts' % self.ssid, url_password=self.pwd,
+ url_username=self.user, validate_certs=self.certs, headers=HEADERS)
+ except Exception as err:
+ self.module.fail_json(
+ msg="Failed to determine host existence. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ # Augment the host objects
+ for host in all_hosts:
+ for port in host['hostSidePorts']:
+ port['type'] = port['type'].lower()
+ port['address'] = port['address'].lower()
+ port['label'] = port['label'].lower()
+
+ # Augment hostSidePorts with their ID (this is an omission in the API)
+ ports = dict((port['label'], port['id']) for port in host['ports'])
+ ports.update((port['label'], port['id']) for port in host['initiators'])
+
+ for host_side_port in host['hostSidePorts']:
+ if host_side_port['label'] in ports:
+ host_side_port['id'] = ports[host_side_port['label']]
+
+ if host['label'] == self.name:
+ self.host_obj = host
+ match = True
+
+ self.all_hosts = all_hosts
+ return match
+
+ def needs_update(self):
+ """Determine whether we need to update the Host object
+ As a side effect, we will set the ports that we need to update (portsForUpdate), and the ports we need to add
+ (newPorts), on self.
+ """
+ changed = False
+ if (self.host_obj["clusterRef"].lower() != self.group_id().lower() or
+ self.host_obj["hostTypeIndex"] != self.host_type_index):
+ self._logger.info("Either hostType or the clusterRef doesn't match, an update is required.")
+ changed = True
+ current_host_ports = dict((port["id"], {"type": port["type"], "port": port["address"], "label": port["label"]})
+ for port in self.host_obj["hostSidePorts"])
+
+ if self.ports:
+ for port in self.ports:
+ for current_host_port_id in current_host_ports.keys():
+ if port == current_host_ports[current_host_port_id]:
+ current_host_ports.pop(current_host_port_id)
+ break
+ elif port["port"] == current_host_ports[current_host_port_id]["port"]:
+ if self.port_on_diff_host(port) and not self.force_port:
+ self.module.fail_json(msg="The port you specified [%s] is associated with a different host."
+ " Specify force_port as True or try a different port spec" % port)
+
+ if (port["label"] != current_host_ports[current_host_port_id]["label"] or
+ port["type"] != current_host_ports[current_host_port_id]["type"]):
+ current_host_ports.pop(current_host_port_id)
+ self.portsForUpdate.append({"portRef": current_host_port_id, "port": port["port"],
+ "label": port["label"], "hostRef": self.host_obj["hostRef"]})
+ break
+ else:
+ self.newPorts.append(port)
+
+ self.portsForRemoval = list(current_host_ports.keys())
+ changed = any([self.newPorts, self.portsForUpdate, self.portsForRemoval, changed])
+
+ return changed
+
+ def port_on_diff_host(self, arg_port):
+ """ Checks to see if a passed in port arg is present on a different host """
+ for host in self.all_hosts:
+ # Only check 'other' hosts
+ if host['name'] != self.name:
+ for port in host['hostSidePorts']:
+ # Check if the port label is found in the port dict list of each host
+ if arg_port['label'] == port['label'] or arg_port['port'] == port['address']:
+ self.other_host = host
+ return True
+ return False
+
+ def update_host(self):
+ self._logger.info("Beginning the update for host=%s.", self.name)
+
+ if self.ports:
+
+ # Remove ports that need reassigning from their current host.
+ self.assigned_host_ports(apply_unassigning=True)
+
+ self.post_body["portsToUpdate"] = self.portsForUpdate
+ self.post_body["ports"] = self.newPorts
+ self._logger.info("Requested ports: %s", pformat(self.ports))
+ else:
+ self._logger.info("No host ports were defined.")
+
+ if self.group:
+ self.post_body['groupId'] = self.group_id()
+
+ self.post_body['hostType'] = dict(index=self.host_type_index)
+
+ api = self.url + 'storage-systems/%s/hosts/%s' % (self.ssid, self.host_obj['id'])
+ self._logger.info("POST => url=%s, body=%s.", api, pformat(self.post_body))
+
+ if not self.check_mode:
+ try:
+ (rc, self.host_obj) = request(api, url_username=self.user, url_password=self.pwd, headers=HEADERS,
+ validate_certs=self.certs, method='POST', data=json.dumps(self.post_body))
+ except Exception as err:
+ self.module.fail_json(
+ msg="Failed to update host. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ payload = self.build_success_payload(self.host_obj)
+ self.module.exit_json(changed=True, **payload)
+
+ def create_host(self):
+ self._logger.info("Creating host definition.")
+
+ # Remove ports that need reassigning from their current host.
+ self.assigned_host_ports(apply_unassigning=True)
+
+ # needs_reassignment = False
+ post_body = dict(
+ name=self.name,
+ hostType=dict(index=self.host_type_index),
+ groupId=self.group_id(),
+ )
+
+ if self.ports:
+ post_body.update(ports=self.ports)
+
+ api = self.url + "storage-systems/%s/hosts" % self.ssid
+ self._logger.info('POST => url=%s, body=%s', api, pformat(post_body))
+
+ if not self.check_mode:
+ if not self.host_exists():
+ try:
+ (rc, self.host_obj) = request(api, method='POST', url_username=self.user, url_password=self.pwd, validate_certs=self.certs,
+ data=json.dumps(post_body), headers=HEADERS)
+ except Exception as err:
+ self.module.fail_json(
+ msg="Failed to create host. Array Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+ else:
+ payload = self.build_success_payload(self.host_obj)
+ self.module.exit_json(changed=False, msg="Host already exists. Id [%s]. Host [%s]." % (self.ssid, self.name), **payload)
+
+ payload = self.build_success_payload(self.host_obj)
+ self.module.exit_json(changed=True, msg='Host created.', **payload)
+
+ def remove_host(self):
+ try:
+ (rc, resp) = request(self.url + "storage-systems/%s/hosts/%s" % (self.ssid, self.host_obj['id']),
+ method='DELETE',
+ url_username=self.user, url_password=self.pwd, validate_certs=self.certs)
+ except Exception as err:
+ self.module.fail_json(
+ msg="Failed to remove host. Host[%s]. Array Id [%s]. Error [%s]." % (self.host_obj['id'],
+ self.ssid,
+ to_native(err)))
+
+ def build_success_payload(self, host=None):
+ keys = ['id']
+ if host is not None:
+ result = dict((key, host[key]) for key in keys)
+ else:
+ result = dict()
+ result['ssid'] = self.ssid
+ result['api_url'] = self.url
+ return result
+
+ def apply(self):
+ if self.state == 'present':
+ if self.host_exists():
+ if self.needs_update() and self.valid_host_type():
+ self.update_host()
+ else:
+ payload = self.build_success_payload(self.host_obj)
+ self.module.exit_json(changed=False, msg="Host already present; no changes required.", **payload)
+ elif self.valid_host_type():
+ self.create_host()
+ else:
+ payload = self.build_success_payload()
+ if self.host_exists():
+ self.remove_host()
+ self.module.exit_json(changed=True, msg="Host removed.", **payload)
+ else:
+ self.module.exit_json(changed=False, msg="Host already absent.", **payload)
+
+
+def main():
+ host = Host()
+ host.apply()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_hostgroup.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_hostgroup.py
new file mode 100644
index 00000000..87676106
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_hostgroup.py
@@ -0,0 +1,307 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2016, 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
+
+
+ANSIBLE_METADATA = {"metadata_version": "1.1",
+ "status": ["deprecated"],
+ "supported_by": "community"}
+
+
+DOCUMENTATION = """
+---
+module: netapp_e_hostgroup
+version_added: "2.2"
+short_description: NetApp E-Series manage array host groups
+author:
+ - Kevin Hulquest (@hulquest)
+ - Nathan Swartz (@ndswartz)
+description: Create, update or destroy host groups on a NetApp E-Series storage array.
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ state:
+ required: true
+ description:
+ - Whether the specified host group should exist or not.
+ type: str
+ choices: ["present", "absent"]
+ name:
+ required: false
+ description:
+ - Name of the host group to manage
+ - This option is mutually exclusive with I(id).
+ type: str
+ new_name:
+ required: false
+ description:
+ - Specify this when you need to update the name of a host group
+ type: str
+ id:
+ required: false
+ description:
+ - Host reference identifier for the host group to manage.
+ - This option is mutually exclusive with I(name).
+ type: str
+ hosts:
+ required: false
+ description:
+ - List of host names/labels to add to the group
+ type: list
+"""
+EXAMPLES = """
+ - name: Configure Hostgroup
+ netapp_e_hostgroup:
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ validate_certs: "{{ netapp_api_validate_certs }}"
+ state: present
+"""
+RETURN = """
+clusterRef:
+ description: The unique identification value for this object. Other objects may use this reference value to refer to the cluster.
+ returned: always except when state is absent
+ type: str
+ sample: "3233343536373839303132333100000000000000"
+confirmLUNMappingCreation:
+ description: If true, indicates that creation of LUN-to-volume mappings should require careful confirmation from the end-user, since such a mapping
+ will alter the volume access rights of other clusters, in addition to this one.
+ returned: always
+ type: bool
+ sample: false
+hosts:
+ description: A list of the hosts that are part of the host group after all operations.
+ returned: always except when state is absent
+ type: list
+ sample: ["HostA","HostB"]
+id:
+ description: The id number of the hostgroup
+ returned: always except when state is absent
+ type: str
+ sample: "3233343536373839303132333100000000000000"
+isSAControlled:
+ description: If true, indicates that I/O accesses from this cluster are subject to the storage array's default LUN-to-volume mappings. If false,
+ indicates that I/O accesses from the cluster are subject to cluster-specific LUN-to-volume mappings.
+ returned: always except when state is absent
+ type: bool
+ sample: false
+label:
+ description: The user-assigned, descriptive label string for the cluster.
+ returned: always
+ type: str
+ sample: "MyHostGroup"
+name:
+ description: same as label
+ returned: always except when state is absent
+ type: str
+ sample: "MyHostGroup"
+protectionInformationCapableAccessMethod:
+ description: This field is true if the host has a PI capable access method.
+ returned: always except when state is absent
+ type: bool
+ sample: true
+"""
+
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+
+class NetAppESeriesHostGroup(NetAppESeriesModule):
+ EXPANSION_TIMEOUT_SEC = 10
+ DEFAULT_DISK_POOL_MINIMUM_DISK_COUNT = 11
+
+ def __init__(self):
+ version = "02.00.0000.0000"
+ ansible_options = dict(
+ state=dict(required=True, choices=["present", "absent"], type="str"),
+ name=dict(required=False, type="str"),
+ new_name=dict(required=False, type="str"),
+ id=dict(required=False, type="str"),
+ hosts=dict(required=False, type="list"))
+ mutually_exclusive = [["name", "id"]]
+ super(NetAppESeriesHostGroup, self).__init__(ansible_options=ansible_options,
+ web_services_version=version,
+ supports_check_mode=True,
+ mutually_exclusive=mutually_exclusive)
+
+ args = self.module.params
+ self.state = args["state"]
+ self.name = args["name"]
+ self.new_name = args["new_name"]
+ self.id = args["id"]
+ self.hosts_list = args["hosts"]
+
+ self.current_host_group = None
+
+ @property
+ def hosts(self):
+ """Retrieve a list of host reference identifiers should be associated with the host group."""
+ host_list = []
+ existing_hosts = []
+
+ if self.hosts_list:
+ try:
+ rc, existing_hosts = self.request("storage-systems/%s/hosts" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve hosts information. Array id [%s]. Error[%s]."
+ % (self.ssid, to_native(error)))
+
+ for host in self.hosts_list:
+ for existing_host in existing_hosts:
+ if host in existing_host["id"] or host in existing_host["name"]:
+ host_list.append(existing_host["id"])
+ break
+ else:
+ self.module.fail_json(msg="Expected host does not exist. Array id [%s]. Host [%s]."
+ % (self.ssid, host))
+
+ return host_list
+
+ @property
+ def host_groups(self):
+ """Retrieve a list of existing host groups."""
+ host_groups = []
+ hosts = []
+ try:
+ rc, host_groups = self.request("storage-systems/%s/host-groups" % self.ssid)
+ rc, hosts = self.request("storage-systems/%s/hosts" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve host group information. Array id [%s]. Error[%s]."
+ % (self.ssid, to_native(error)))
+
+ host_groups = [{"id": group["clusterRef"], "name": group["name"]} for group in host_groups]
+ for group in host_groups:
+ hosts_ids = []
+ for host in hosts:
+ if group["id"] == host["clusterRef"]:
+ hosts_ids.append(host["hostRef"])
+ group.update({"hosts": hosts_ids})
+
+ return host_groups
+
+ @property
+ def current_hosts_in_host_group(self):
+ """Retrieve the current hosts associated with the current hostgroup."""
+ current_hosts = []
+ for group in self.host_groups:
+ if (self.name and group["name"] == self.name) or (self.id and group["id"] == self.id):
+ current_hosts = group["hosts"]
+
+ return current_hosts
+
+ def unassign_hosts(self, host_list=None):
+ """Unassign hosts from host group."""
+ if host_list is None:
+ host_list = self.current_host_group["hosts"]
+
+ for host_id in host_list:
+ try:
+ rc, resp = self.request("storage-systems/%s/hosts/%s/move" % (self.ssid, host_id),
+ method="POST", data={"group": "0000000000000000000000000000000000000000"})
+ except Exception as error:
+ self.module.fail_json(msg="Failed to unassign hosts from host group. Array id [%s]. Host id [%s]."
+ " Error[%s]." % (self.ssid, host_id, to_native(error)))
+
+ def delete_host_group(self, unassign_hosts=True):
+ """Delete host group"""
+ if unassign_hosts:
+ self.unassign_hosts()
+
+ try:
+ rc, resp = self.request("storage-systems/%s/host-groups/%s" % (self.ssid, self.current_host_group["id"]),
+ method="DELETE")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to delete host group. Array id [%s]. Error[%s]."
+ % (self.ssid, to_native(error)))
+
+ def create_host_group(self):
+ """Create host group."""
+ data = {"name": self.name, "hosts": self.hosts}
+
+ response = None
+ try:
+ rc, response = self.request("storage-systems/%s/host-groups" % self.ssid, method="POST", data=data)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to create host group. Array id [%s]. Error[%s]."
+ % (self.ssid, to_native(error)))
+
+ return response
+
+ def update_host_group(self):
+ """Update host group."""
+ data = {"name": self.new_name if self.new_name else self.name,
+ "hosts": self.hosts}
+
+ # unassign hosts that should not be part of the hostgroup
+ desired_host_ids = self.hosts
+ for host in self.current_hosts_in_host_group:
+ if host not in desired_host_ids:
+ self.unassign_hosts([host])
+
+ update_response = None
+ try:
+ rc, update_response = self.request("storage-systems/%s/host-groups/%s"
+ % (self.ssid, self.current_host_group["id"]), method="POST", data=data)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to create host group. Array id [%s]. Error[%s]."
+ % (self.ssid, to_native(error)))
+
+ return update_response
+
+ def apply(self):
+ """Apply desired host group state to the storage array."""
+ changes_required = False
+
+ # Search for existing host group match
+ for group in self.host_groups:
+ if (self.id and group["id"] == self.id) or (self.name and group["name"] == self.name):
+ self.current_host_group = group
+
+ # Determine whether changes are required
+ if self.state == "present":
+ if self.current_host_group:
+ if (self.new_name and self.new_name != self.name) or self.hosts != self.current_host_group["hosts"]:
+ changes_required = True
+ else:
+ if not self.name:
+ self.module.fail_json(msg="The option name must be supplied when creating a new host group."
+ " Array id [%s]." % self.ssid)
+ changes_required = True
+
+ elif self.current_host_group:
+ changes_required = True
+
+ # Apply any necessary changes
+ msg = ""
+ if changes_required and not self.module.check_mode:
+ msg = "No changes required."
+ if self.state == "present":
+ if self.current_host_group:
+ if ((self.new_name and self.new_name != self.name) or
+ (self.hosts != self.current_host_group["hosts"])):
+ msg = self.update_host_group()
+ else:
+ msg = self.create_host_group()
+
+ elif self.current_host_group:
+ self.delete_host_group()
+ msg = "Host group deleted. Array Id [%s]. Host Name [%s]. Host Id [%s]."\
+ % (self.ssid, self.current_host_group["name"], self.current_host_group["id"])
+
+ self.module.exit_json(msg=msg, changed=changes_required)
+
+
+def main():
+ hostgroup = NetAppESeriesHostGroup()
+ hostgroup.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_iscsi_interface.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_iscsi_interface.py
new file mode 100644
index 00000000..5e290f74
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_iscsi_interface.py
@@ -0,0 +1,407 @@
+#!/usr/bin/python
+
+# (c) 2018, 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
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = """
+---
+module: netapp_e_iscsi_interface
+short_description: NetApp E-Series manage iSCSI interface configuration
+description:
+ - Configure settings of an E-Series iSCSI interface
+version_added: '2.7'
+author: Michael Price (@lmprice)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ controller:
+ description:
+ - The controller that owns the port you want to configure.
+ - Controller names are presented alphabetically, with the first controller as A,
+ the second as B, and so on.
+ - Current hardware models have either 1 or 2 available controllers, but that is not a guaranteed hard
+ limitation and could change in the future.
+ required: yes
+ type: str
+ choices:
+ - A
+ - B
+ name:
+ description:
+ - The channel of the port to modify the configuration of.
+ - The list of choices is not necessarily comprehensive. It depends on the number of ports
+ that are available in the system.
+ - The numerical value represents the number of the channel (typically from left to right on the HIC),
+ beginning with a value of 1.
+ required: yes
+ type: int
+ aliases:
+ - channel
+ state:
+ description:
+ - When enabled, the provided configuration will be utilized.
+ - When disabled, the IPv4 configuration will be cleared and IPv4 connectivity disabled.
+ choices:
+ - enabled
+ - disabled
+ default: enabled
+ type: str
+ address:
+ description:
+ - The IPv4 address to assign to the interface.
+ - Should be specified in xx.xx.xx.xx form.
+ - Mutually exclusive with I(config_method=dhcp)
+ type: str
+ subnet_mask:
+ description:
+ - The subnet mask to utilize for the interface.
+ - Should be specified in xx.xx.xx.xx form.
+ - Mutually exclusive with I(config_method=dhcp)
+ type: str
+ gateway:
+ description:
+ - The IPv4 gateway address to utilize for the interface.
+ - Should be specified in xx.xx.xx.xx form.
+ - Mutually exclusive with I(config_method=dhcp)
+ type: str
+ config_method:
+ description:
+ - The configuration method type to use for this interface.
+ - dhcp is mutually exclusive with I(address), I(subnet_mask), and I(gateway).
+ choices:
+ - dhcp
+ - static
+ default: dhcp
+ type: str
+ mtu:
+ description:
+ - The maximum transmission units (MTU), in bytes.
+ - This allows you to configure a larger value for the MTU, in order to enable jumbo frames
+ (any value > 1500).
+ - Generally, it is necessary to have your host, switches, and other components not only support jumbo
+ frames, but also have it configured properly. Therefore, unless you know what you're doing, it's best to
+ leave this at the default.
+ default: 1500
+ type: int
+ aliases:
+ - max_frame_size
+ log_path:
+ description:
+ - A local path to a file to be used for debug logging
+ type: str
+ required: no
+notes:
+ - Check mode is supported.
+ - The interface settings are applied synchronously, but changes to the interface itself (receiving a new IP address
+ via dhcp, etc), can take seconds or minutes longer to take effect.
+ - This module will not be useful/usable on an E-Series system without any iSCSI interfaces.
+ - This module requires a Web Services API version of >= 1.3.
+"""
+
+EXAMPLES = """
+ - name: Configure the first port on the A controller with a static IPv4 address
+ netapp_e_iscsi_interface:
+ name: "1"
+ controller: "A"
+ config_method: static
+ address: "192.168.1.100"
+ subnet_mask: "255.255.255.0"
+ gateway: "192.168.1.1"
+ ssid: "1"
+ api_url: "10.1.1.1:8443"
+ api_username: "admin"
+ api_password: "myPass"
+
+ - name: Disable ipv4 connectivity for the second port on the B controller
+ netapp_e_iscsi_interface:
+ name: "2"
+ controller: "B"
+ state: disabled
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+
+ - name: Enable jumbo frames for the first 4 ports on controller A
+ netapp_e_iscsi_interface:
+ name: "{{ item | int }}"
+ controller: "A"
+ state: enabled
+ mtu: 9000
+ config_method: dhcp
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ loop:
+ - 1
+ - 2
+ - 3
+ - 4
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The interface settings have been updated.
+enabled:
+ description:
+ - Indicates whether IPv4 connectivity has been enabled or disabled.
+ - This does not necessarily indicate connectivity. If dhcp was enabled without a dhcp server, for instance,
+ it is unlikely that the configuration will actually be valid.
+ returned: on success
+ sample: True
+ type: bool
+"""
+import json
+import logging
+from pprint import pformat
+import re
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import request, eseries_host_argument_spec
+from ansible.module_utils._text import to_native
+
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+}
+
+
+class IscsiInterface(object):
+ def __init__(self):
+ argument_spec = eseries_host_argument_spec()
+ argument_spec.update(dict(
+ controller=dict(type='str', required=True, choices=['A', 'B']),
+ name=dict(type='int', aliases=['channel']),
+ state=dict(type='str', required=False, default='enabled', choices=['enabled', 'disabled']),
+ address=dict(type='str', required=False),
+ subnet_mask=dict(type='str', required=False),
+ gateway=dict(type='str', required=False),
+ config_method=dict(type='str', required=False, default='dhcp', choices=['dhcp', 'static']),
+ mtu=dict(type='int', default=1500, required=False, aliases=['max_frame_size']),
+ log_path=dict(type='str', required=False),
+ ))
+
+ required_if = [
+ ["config_method", "static", ["address", "subnet_mask"]],
+ ]
+
+ self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if, )
+ args = self.module.params
+ self.controller = args['controller']
+ self.name = args['name']
+ self.mtu = args['mtu']
+ self.state = args['state']
+ self.address = args['address']
+ self.subnet_mask = args['subnet_mask']
+ self.gateway = args['gateway']
+ self.config_method = args['config_method']
+
+ self.ssid = args['ssid']
+ self.url = args['api_url']
+ self.creds = dict(url_password=args['api_password'],
+ validate_certs=args['validate_certs'],
+ url_username=args['api_username'], )
+
+ self.check_mode = self.module.check_mode
+ self.post_body = dict()
+ self.controllers = list()
+
+ log_path = args['log_path']
+
+ # logging setup
+ self._logger = logging.getLogger(self.__class__.__name__)
+
+ if log_path:
+ logging.basicConfig(
+ level=logging.DEBUG, filename=log_path, filemode='w',
+ format='%(relativeCreated)dms %(levelname)s %(module)s.%(funcName)s:%(lineno)d\n %(message)s')
+
+ if not self.url.endswith('/'):
+ self.url += '/'
+
+ if self.mtu < 1500 or self.mtu > 9000:
+ self.module.fail_json(msg="The provided mtu is invalid, it must be > 1500 and < 9000 bytes.")
+
+ if self.config_method == 'dhcp' and any([self.address, self.subnet_mask, self.gateway]):
+ self.module.fail_json(msg='A config_method of dhcp is mutually exclusive with the address,'
+ ' subnet_mask, and gateway options.')
+
+ # A relatively primitive regex to validate that the input is formatted like a valid ip address
+ address_regex = re.compile(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}')
+
+ if self.address and not address_regex.match(self.address):
+ self.module.fail_json(msg="An invalid ip address was provided for address.")
+
+ if self.subnet_mask and not address_regex.match(self.subnet_mask):
+ self.module.fail_json(msg="An invalid ip address was provided for subnet_mask.")
+
+ if self.gateway and not address_regex.match(self.gateway):
+ self.module.fail_json(msg="An invalid ip address was provided for gateway.")
+
+ @property
+ def interfaces(self):
+ ifaces = list()
+ try:
+ (rc, ifaces) = request(self.url + 'storage-systems/%s/graph/xpath-filter?query=/controller/hostInterfaces'
+ % self.ssid, headers=HEADERS, **self.creds)
+ except Exception as err:
+ self.module.fail_json(
+ msg="Failed to retrieve defined host interfaces. Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ # Filter out non-iSCSI interfaces
+ ifaces = [iface['iscsi'] for iface in ifaces if iface['interfaceType'] == 'iscsi']
+
+ return ifaces
+
+ def get_controllers(self):
+ """Retrieve a mapping of controller labels to their references
+ {
+ 'A': '070000000000000000000001',
+ 'B': '070000000000000000000002',
+ }
+ :return: the controllers defined on the system
+ """
+ controllers = list()
+ try:
+ (rc, controllers) = request(self.url + 'storage-systems/%s/graph/xpath-filter?query=/controller/id'
+ % self.ssid, headers=HEADERS, **self.creds)
+ except Exception as err:
+ self.module.fail_json(
+ msg="Failed to retrieve controller list! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ controllers.sort()
+
+ controllers_dict = {}
+ i = ord('A')
+ for controller in controllers:
+ label = chr(i)
+ controllers_dict[label] = controller
+ i += 1
+
+ return controllers_dict
+
+ def fetch_target_interface(self):
+ interfaces = self.interfaces
+
+ for iface in interfaces:
+ if iface['channel'] == self.name and self.controllers[self.controller] == iface['controllerId']:
+ return iface
+
+ channels = sorted(set((str(iface['channel'])) for iface in interfaces
+ if self.controllers[self.controller] == iface['controllerId']))
+
+ self.module.fail_json(msg="The requested channel of %s is not valid. Valid channels include: %s."
+ % (self.name, ", ".join(channels)))
+
+ def make_update_body(self, target_iface):
+ body = dict(iscsiInterface=target_iface['id'])
+ update_required = False
+
+ self._logger.info("Requested state=%s.", self.state)
+ self._logger.info("config_method: current=%s, requested=%s",
+ target_iface['ipv4Data']['ipv4AddressConfigMethod'], self.config_method)
+
+ if self.state == 'enabled':
+ settings = dict()
+ if not target_iface['ipv4Enabled']:
+ update_required = True
+ settings['ipv4Enabled'] = [True]
+ if self.mtu != target_iface['interfaceData']['ethernetData']['maximumFramePayloadSize']:
+ update_required = True
+ settings['maximumFramePayloadSize'] = [self.mtu]
+ if self.config_method == 'static':
+ ipv4Data = target_iface['ipv4Data']['ipv4AddressData']
+
+ if ipv4Data['ipv4Address'] != self.address:
+ update_required = True
+ settings['ipv4Address'] = [self.address]
+ if ipv4Data['ipv4SubnetMask'] != self.subnet_mask:
+ update_required = True
+ settings['ipv4SubnetMask'] = [self.subnet_mask]
+ if self.gateway is not None and ipv4Data['ipv4GatewayAddress'] != self.gateway:
+ update_required = True
+ settings['ipv4GatewayAddress'] = [self.gateway]
+
+ if target_iface['ipv4Data']['ipv4AddressConfigMethod'] != 'configStatic':
+ update_required = True
+ settings['ipv4AddressConfigMethod'] = ['configStatic']
+
+ elif (target_iface['ipv4Data']['ipv4AddressConfigMethod'] != 'configDhcp'):
+ update_required = True
+ settings.update(dict(ipv4Enabled=[True],
+ ipv4AddressConfigMethod=['configDhcp']))
+ body['settings'] = settings
+
+ else:
+ if target_iface['ipv4Enabled']:
+ update_required = True
+ body['settings'] = dict(ipv4Enabled=[False])
+
+ self._logger.info("Update required ?=%s", update_required)
+ self._logger.info("Update body: %s", pformat(body))
+
+ return update_required, body
+
+ def update(self):
+ self.controllers = self.get_controllers()
+ if self.controller not in self.controllers:
+ self.module.fail_json(msg="The provided controller name is invalid. Valid controllers: %s."
+ % ", ".join(self.controllers.keys()))
+
+ iface_before = self.fetch_target_interface()
+ update_required, body = self.make_update_body(iface_before)
+ if update_required and not self.check_mode:
+ try:
+ url = (self.url +
+ 'storage-systems/%s/symbol/setIscsiInterfaceProperties' % self.ssid)
+ (rc, result) = request(url, method='POST', data=json.dumps(body), headers=HEADERS, timeout=300,
+ ignore_errors=True, **self.creds)
+ # We could potentially retry this a few times, but it's probably a rare enough case (unless a playbook
+ # is cancelled mid-flight), that it isn't worth the complexity.
+ if rc == 422 and result['retcode'] in ['busy', '3']:
+ self.module.fail_json(
+ msg="The interface is currently busy (probably processing a previously requested modification"
+ " request). This operation cannot currently be completed. Array Id [%s]. Error [%s]."
+ % (self.ssid, result))
+ # Handle authentication issues, etc.
+ elif rc != 200:
+ self.module.fail_json(
+ msg="Failed to modify the interface! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(result)))
+ self._logger.debug("Update request completed successfully.")
+ # This is going to catch cases like a connection failure
+ except Exception as err:
+ self.module.fail_json(
+ msg="Connection failure: we failed to modify the interface! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ iface_after = self.fetch_target_interface()
+
+ self.module.exit_json(msg="The interface settings have been updated.", changed=update_required,
+ enabled=iface_after['ipv4Enabled'])
+
+ def __call__(self, *args, **kwargs):
+ self.update()
+
+
+def main():
+ iface = IscsiInterface()
+ iface()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_iscsi_target.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_iscsi_target.py
new file mode 100644
index 00000000..040d3e78
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_iscsi_target.py
@@ -0,0 +1,297 @@
+#!/usr/bin/python
+
+# (c) 2018, 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
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = """
+---
+module: netapp_e_iscsi_target
+short_description: NetApp E-Series manage iSCSI target configuration
+description:
+ - Configure the settings of an E-Series iSCSI target
+version_added: '2.7'
+author: Michael Price (@lmprice)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ name:
+ description:
+ - The name/alias to assign to the iSCSI target.
+ - This alias is often used by the initiator software in order to make an iSCSI target easier to identify.
+ type: str
+ aliases:
+ - alias
+ ping:
+ description:
+ - Enable ICMP ping responses from the configured iSCSI ports.
+ type: bool
+ default: yes
+ chap_secret:
+ description:
+ - Enable Challenge-Handshake Authentication Protocol (CHAP), utilizing this value as the password.
+ - When this value is specified, we will always trigger an update (changed=True). We have no way of verifying
+ whether or not the password has changed.
+ - The chap secret may only use ascii characters with values between 32 and 126 decimal.
+ - The chap secret must be no less than 12 characters, but no greater than 57 characters in length.
+ - The chap secret is cleared when not specified or an empty string.
+ type: str
+ aliases:
+ - chap
+ - password
+ unnamed_discovery:
+ description:
+ - When an initiator initiates a discovery session to an initiator port, it is considered an unnamed
+ discovery session if the iSCSI target iqn is not specified in the request.
+ - This option may be disabled to increase security if desired.
+ type: bool
+ default: yes
+ log_path:
+ description:
+ - A local path (on the Ansible controller), to a file to be used for debug logging.
+ type: str
+ required: no
+notes:
+ - Check mode is supported.
+ - Some of the settings are dependent on the settings applied to the iSCSI interfaces. These can be configured using
+ M(netapp_eseries.santricity.netapp_e_iscsi_interface).
+ - This module requires a Web Services API version of >= 1.3.
+"""
+
+EXAMPLES = """
+ - name: Enable ping responses and unnamed discovery sessions for all iSCSI ports
+ netapp_e_iscsi_target:
+ api_url: "https://localhost:8443/devmgr/v2"
+ api_username: admin
+ api_password: myPassword
+ ssid: "1"
+ validate_certs: no
+ name: myTarget
+ ping: yes
+ unnamed_discovery: yes
+
+ - name: Set the target alias and the CHAP secret
+ netapp_e_iscsi_target:
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ name: myTarget
+ chap: password1234
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The iSCSI target settings have been updated.
+alias:
+ description:
+ - The alias assigned to the iSCSI target.
+ returned: on success
+ sample: myArray
+ type: str
+iqn:
+ description:
+ - The iqn (iSCSI Qualified Name), assigned to the iSCSI target.
+ returned: on success
+ sample: iqn.1992-08.com.netapp:2800.000a132000b006d2000000005a0e8f45
+ type: str
+"""
+import json
+import logging
+from pprint import pformat
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import request, eseries_host_argument_spec
+from ansible.module_utils._text import to_native
+
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+}
+
+
+class IscsiTarget(object):
+ def __init__(self):
+ argument_spec = eseries_host_argument_spec()
+ argument_spec.update(dict(
+ name=dict(type='str', required=False, aliases=['alias']),
+ ping=dict(type='bool', required=False, default=True),
+ chap_secret=dict(type='str', required=False, aliases=['chap', 'password'], no_log=True),
+ unnamed_discovery=dict(type='bool', required=False, default=True),
+ log_path=dict(type='str', required=False),
+ ))
+
+ self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, )
+ args = self.module.params
+
+ self.name = args['name']
+ self.ping = args['ping']
+ self.chap_secret = args['chap_secret']
+ self.unnamed_discovery = args['unnamed_discovery']
+
+ self.ssid = args['ssid']
+ self.url = args['api_url']
+ self.creds = dict(url_password=args['api_password'],
+ validate_certs=args['validate_certs'],
+ url_username=args['api_username'], )
+
+ self.check_mode = self.module.check_mode
+ self.post_body = dict()
+ self.controllers = list()
+
+ log_path = args['log_path']
+
+ # logging setup
+ self._logger = logging.getLogger(self.__class__.__name__)
+
+ if log_path:
+ logging.basicConfig(
+ level=logging.DEBUG, filename=log_path, filemode='w',
+ format='%(relativeCreated)dms %(levelname)s %(module)s.%(funcName)s:%(lineno)d\n %(message)s')
+
+ if not self.url.endswith('/'):
+ self.url += '/'
+
+ if self.chap_secret:
+ if len(self.chap_secret) < 12 or len(self.chap_secret) > 57:
+ self.module.fail_json(msg="The provided CHAP secret is not valid, it must be between 12 and 57"
+ " characters in length.")
+
+ for c in self.chap_secret:
+ ordinal = ord(c)
+ if ordinal < 32 or ordinal > 126:
+ self.module.fail_json(msg="The provided CHAP secret is not valid, it may only utilize ascii"
+ " characters with decimal values between 32 and 126.")
+
+ @property
+ def target(self):
+ """Provide information on the iSCSI Target configuration
+
+ Sample:
+ {
+ 'alias': 'myCustomName',
+ 'ping': True,
+ 'unnamed_discovery': True,
+ 'chap': False,
+ 'iqn': 'iqn.1992-08.com.netapp:2800.000a132000b006d2000000005a0e8f45',
+ }
+ """
+ target = dict()
+ try:
+ (rc, data) = request(self.url + 'storage-systems/%s/graph/xpath-filter?query=/storagePoolBundle/target'
+ % self.ssid, headers=HEADERS, **self.creds)
+ # This likely isn't an iSCSI-enabled system
+ if not data:
+ self.module.fail_json(
+ msg="This storage-system doesn't appear to have iSCSI interfaces. Array Id [%s]." % (self.ssid))
+
+ data = data[0]
+
+ chap = any(
+ [auth for auth in data['configuredAuthMethods']['authMethodData'] if auth['authMethod'] == 'chap'])
+
+ target.update(dict(alias=data['alias']['iscsiAlias'],
+ iqn=data['nodeName']['iscsiNodeName'],
+ chap=chap))
+
+ (rc, data) = request(self.url + 'storage-systems/%s/graph/xpath-filter?query=/sa/iscsiEntityData'
+ % self.ssid, headers=HEADERS, **self.creds)
+
+ data = data[0]
+ target.update(dict(ping=data['icmpPingResponseEnabled'],
+ unnamed_discovery=data['unnamedDiscoverySessionsEnabled']))
+
+ except Exception as err:
+ self.module.fail_json(
+ msg="Failed to retrieve the iSCSI target information. Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ return target
+
+ def apply_iscsi_settings(self):
+ """Update the iSCSI target alias and CHAP settings"""
+ update = False
+ target = self.target
+
+ body = dict()
+
+ if self.name is not None and self.name != target['alias']:
+ update = True
+ body['alias'] = self.name
+
+ # If the CHAP secret was provided, we trigger an update.
+ if self.chap_secret:
+ update = True
+ body.update(dict(enableChapAuthentication=True,
+ chapSecret=self.chap_secret))
+ # If no secret was provided, then we disable chap
+ elif target['chap']:
+ update = True
+ body.update(dict(enableChapAuthentication=False))
+
+ if update and not self.check_mode:
+ try:
+ request(self.url + 'storage-systems/%s/iscsi/target-settings' % self.ssid, method='POST',
+ data=json.dumps(body), headers=HEADERS, **self.creds)
+ except Exception as err:
+ self.module.fail_json(
+ msg="Failed to update the iSCSI target settings. Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ return update
+
+ def apply_target_changes(self):
+ update = False
+ target = self.target
+
+ body = dict()
+
+ if self.ping != target['ping']:
+ update = True
+ body['icmpPingResponseEnabled'] = self.ping
+
+ if self.unnamed_discovery != target['unnamed_discovery']:
+ update = True
+ body['unnamedDiscoverySessionsEnabled'] = self.unnamed_discovery
+
+ self._logger.info(pformat(body))
+ if update and not self.check_mode:
+ try:
+ request(self.url + 'storage-systems/%s/iscsi/entity' % self.ssid, method='POST',
+ data=json.dumps(body), timeout=60, headers=HEADERS, **self.creds)
+ except Exception as err:
+ self.module.fail_json(
+ msg="Failed to update the iSCSI target settings. Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+ return update
+
+ def update(self):
+ update = self.apply_iscsi_settings()
+ update = self.apply_target_changes() or update
+
+ target = self.target
+ data = dict((key, target[key]) for key in target if key in ['iqn', 'alias'])
+
+ self.module.exit_json(msg="The interface settings have been updated.", changed=update, **data)
+
+ def __call__(self, *args, **kwargs):
+ self.update()
+
+
+def main():
+ iface = IscsiTarget()
+ iface()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_ldap.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_ldap.py
new file mode 100644
index 00000000..e3bb61e6
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_ldap.py
@@ -0,0 +1,401 @@
+#!/usr/bin/python
+
+# (c) 2018, 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
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = '''
+---
+module: netapp_e_ldap
+short_description: NetApp E-Series manage LDAP integration to use for authentication
+description:
+ - Configure an E-Series system to allow authentication via an LDAP server
+version_added: '2.7'
+author: Michael Price (@lmprice)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ state:
+ description:
+ - Enable/disable LDAP support on the system. Disabling will clear out any existing defined domains.
+ choices:
+ - present
+ - absent
+ default: present
+ type: str
+ identifier:
+ description:
+ - This is a unique identifier for the configuration (for cases where there are multiple domains configured).
+ - If this is not specified, but I(state=present), we will utilize a default value of 'default'.
+ type: str
+ username:
+ description:
+ - This is the user account that will be used for querying the LDAP server.
+ - "Example: CN=MyBindAcct,OU=ServiceAccounts,DC=example,DC=com"
+ required: yes
+ type: str
+ aliases:
+ - bind_username
+ password:
+ description:
+ - This is the password for the bind user account.
+ required: yes
+ type: str
+ aliases:
+ - bind_password
+ attributes:
+ description:
+ - The user attributes that should be considered for the group to role mapping.
+ - Typically this is used with something like 'memberOf', and a user's access is tested against group
+ membership or lack thereof.
+ default: memberOf
+ type: list
+ server:
+ description:
+ - This is the LDAP server url.
+ - The connection string should be specified as using the ldap or ldaps protocol along with the port
+ information.
+ aliases:
+ - server_url
+ required: yes
+ type: str
+ name:
+ description:
+ - The domain name[s] that will be utilized when authenticating to identify which domain to utilize.
+ - Default to use the DNS name of the I(server).
+ - The only requirement is that the name[s] be resolvable.
+ - "Example: user@example.com"
+ required: no
+ type: list
+ search_base:
+ description:
+ - The search base is used to find group memberships of the user.
+ - "Example: ou=users,dc=example,dc=com"
+ required: yes
+ type: str
+ role_mappings:
+ description:
+ - This is where you specify which groups should have access to what permissions for the
+ storage-system.
+ - For example, all users in group A will be assigned all 4 available roles, which will allow access
+ to all the management functionality of the system (super-user). Those in group B only have the
+ storage.monitor role, which will allow only read-only access.
+ - This is specified as a mapping of regular expressions to a list of roles. See the examples.
+ - The roles that will be assigned to to the group/groups matching the provided regex.
+ - storage.admin allows users full read/write access to storage objects and operations.
+ - storage.monitor allows users read-only access to storage objects and operations.
+ - support.admin allows users access to hardware, diagnostic information, the Major Event
+ Log, and other critical support-related functionality, but not the storage configuration.
+ - security.admin allows users access to authentication/authorization configuration, as well
+ as the audit log configuration, and certification management.
+ type: dict
+ required: yes
+ user_attribute:
+ description:
+ - This is the attribute we will use to match the provided username when a user attempts to
+ authenticate.
+ type: str
+ default: sAMAccountName
+ log_path:
+ description:
+ - A local path to a file to be used for debug logging
+ required: no
+ type: str
+notes:
+ - Check mode is supported.
+ - This module allows you to define one or more LDAP domains identified uniquely by I(identifier) to use for
+ authentication. Authorization is determined by I(role_mappings), in that different groups of users may be given
+ different (or no), access to certain aspects of the system and API.
+ - The local user accounts will still be available if the LDAP server becomes unavailable/inaccessible.
+ - Generally, you'll need to get the details of your organization's LDAP server before you'll be able to configure
+ the system for using LDAP authentication; every implementation is likely to be very different.
+ - This API is currently only supported with the Embedded Web Services API v2.0 and higher, or the Web Services Proxy
+ v3.0 and higher.
+'''
+
+EXAMPLES = '''
+ - name: Disable LDAP authentication
+ netapp_e_ldap:
+ api_url: "10.1.1.1:8443"
+ api_username: "admin"
+ api_password: "myPass"
+ ssid: "1"
+ state: absent
+
+ - name: Remove the 'default' LDAP domain configuration
+ netapp_e_ldap:
+ state: absent
+ identifier: default
+
+ - name: Define a new LDAP domain, utilizing defaults where possible
+ netapp_e_ldap:
+ state: present
+ bind_username: "CN=MyBindAccount,OU=ServiceAccounts,DC=example,DC=com"
+ bind_password: "mySecretPass"
+ server: "ldap://example.com:389"
+ search_base: 'OU=Users,DC=example,DC=com'
+ role_mappings:
+ ".*dist-dev-storage.*":
+ - storage.admin
+ - security.admin
+ - support.admin
+ - storage.monitor
+'''
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The ldap settings have been updated.
+"""
+
+import json
+import logging
+
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import request, eseries_host_argument_spec
+from ansible.module_utils._text import to_native
+
+
+class Ldap(object):
+ NO_CHANGE_MSG = "No changes were necessary."
+
+ def __init__(self):
+ argument_spec = eseries_host_argument_spec()
+ argument_spec.update(dict(
+ state=dict(type='str', required=False, default='present',
+ choices=['present', 'absent']),
+ identifier=dict(type='str', required=False, ),
+ username=dict(type='str', required=False, aliases=['bind_username']),
+ password=dict(type='str', required=False, aliases=['bind_password'], no_log=True),
+ name=dict(type='list', required=False, ),
+ server=dict(type='str', required=False, aliases=['server_url']),
+ search_base=dict(type='str', required=False, ),
+ role_mappings=dict(type='dict', required=False, ),
+ user_attribute=dict(type='str', required=False, default='sAMAccountName'),
+ attributes=dict(type='list', default=['memberOf'], required=False, ),
+ log_path=dict(type='str', required=False),
+ ))
+
+ required_if = [
+ ["state", "present", ["username", "password", "server", "search_base", "role_mappings", ]]
+ ]
+
+ self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if)
+ args = self.module.params
+ self.ldap = args['state'] == 'present'
+ self.identifier = args['identifier']
+ self.username = args['username']
+ self.password = args['password']
+ self.names = args['name']
+ self.server = args['server']
+ self.search_base = args['search_base']
+ self.role_mappings = args['role_mappings']
+ self.user_attribute = args['user_attribute']
+ self.attributes = args['attributes']
+
+ self.ssid = args['ssid']
+ self.url = args['api_url']
+ self.creds = dict(url_password=args['api_password'],
+ validate_certs=args['validate_certs'],
+ url_username=args['api_username'],
+ timeout=60)
+
+ self.check_mode = self.module.check_mode
+
+ log_path = args['log_path']
+
+ # logging setup
+ self._logger = logging.getLogger(self.__class__.__name__)
+
+ if log_path:
+ logging.basicConfig(
+ level=logging.DEBUG, filename=log_path, filemode='w',
+ format='%(relativeCreated)dms %(levelname)s %(module)s.%(funcName)s:%(lineno)d\n %(message)s')
+
+ if not self.url.endswith('/'):
+ self.url += '/'
+
+ self.embedded = None
+ self.base_path = None
+
+ def make_configuration(self):
+ if not self.identifier:
+ self.identifier = 'default'
+
+ if not self.names:
+ parts = urlparse.urlparse(self.server)
+ netloc = parts.netloc
+ if ':' in netloc:
+ netloc = netloc.split(':')[0]
+ self.names = [netloc]
+
+ roles = list()
+ for regex in self.role_mappings:
+ for role in self.role_mappings[regex]:
+ roles.append(dict(groupRegex=regex,
+ ignoreCase=True,
+ name=role))
+
+ domain = dict(id=self.identifier,
+ ldapUrl=self.server,
+ bindLookupUser=dict(user=self.username, password=self.password),
+ roleMapCollection=roles,
+ groupAttributes=self.attributes,
+ names=self.names,
+ searchBase=self.search_base,
+ userAttribute=self.user_attribute,
+ )
+
+ return domain
+
+ def is_embedded(self):
+ """Determine whether or not we're using the embedded or proxy implementation of Web Services"""
+ if self.embedded is None:
+ url = self.url
+ try:
+ parts = urlparse.urlparse(url)
+ parts = parts._replace(path='/devmgr/utils/')
+ url = urlparse.urlunparse(parts)
+
+ (rc, result) = request(url + 'about', **self.creds)
+ self.embedded = not result['runningAsProxy']
+ except Exception as err:
+ self._logger.exception("Failed to retrieve the About information.")
+ self.module.fail_json(msg="Failed to determine the Web Services implementation type!"
+ " Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ return self.embedded
+
+ def get_full_configuration(self):
+ try:
+ (rc, result) = request(self.url + self.base_path, **self.creds)
+ return result
+ except Exception as err:
+ self._logger.exception("Failed to retrieve the LDAP configuration.")
+ self.module.fail_json(msg="Failed to retrieve LDAP configuration! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ def get_configuration(self, identifier):
+ try:
+ (rc, result) = request(self.url + self.base_path + '%s' % (identifier), ignore_errors=True, **self.creds)
+ if rc == 200:
+ return result
+ elif rc == 404:
+ return None
+ else:
+ self.module.fail_json(msg="Failed to retrieve LDAP configuration! Array Id [%s]. Error [%s]."
+ % (self.ssid, result))
+ except Exception as err:
+ self._logger.exception("Failed to retrieve the LDAP configuration.")
+ self.module.fail_json(msg="Failed to retrieve LDAP configuration! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ def update_configuration(self):
+ # Define a new domain based on the user input
+ domain = self.make_configuration()
+
+ # This is the current list of configurations
+ current = self.get_configuration(self.identifier)
+
+ update = current != domain
+ msg = "No changes were necessary for [%s]." % self.identifier
+ self._logger.info("Is updated: %s", update)
+ if update and not self.check_mode:
+ msg = "The configuration changes were made for [%s]." % self.identifier
+ try:
+ if current is None:
+ api = self.base_path + 'addDomain'
+ else:
+ api = self.base_path + '%s' % (domain['id'])
+
+ (rc, result) = request(self.url + api, method='POST', data=json.dumps(domain), **self.creds)
+ except Exception as err:
+ self._logger.exception("Failed to modify the LDAP configuration.")
+ self.module.fail_json(msg="Failed to modify LDAP configuration! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ return msg, update
+
+ def clear_single_configuration(self, identifier=None):
+ if identifier is None:
+ identifier = self.identifier
+
+ configuration = self.get_configuration(identifier)
+ updated = False
+ msg = self.NO_CHANGE_MSG
+ if configuration:
+ updated = True
+ msg = "The LDAP domain configuration for [%s] was cleared." % identifier
+ if not self.check_mode:
+ try:
+ (rc, result) = request(self.url + self.base_path + '%s' % identifier, method='DELETE', **self.creds)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to remove LDAP configuration! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+ return msg, updated
+
+ def clear_configuration(self):
+ configuration = self.get_full_configuration()
+ updated = False
+ msg = self.NO_CHANGE_MSG
+ if configuration['ldapDomains']:
+ updated = True
+ msg = "The LDAP configuration for all domains was cleared."
+ if not self.check_mode:
+ try:
+ (rc, result) = request(self.url + self.base_path, method='DELETE', ignore_errors=True, **self.creds)
+
+ # Older versions of NetApp E-Series restAPI does not possess an API to remove all existing configs
+ if rc == 405:
+ for config in configuration['ldapDomains']:
+ self.clear_single_configuration(config['id'])
+
+ except Exception as err:
+ self.module.fail_json(msg="Failed to clear LDAP configuration! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+ return msg, updated
+
+ def get_base_path(self):
+ embedded = self.is_embedded()
+ if embedded:
+ return 'storage-systems/%s/ldap/' % self.ssid
+ else:
+ return '/ldap/'
+
+ def update(self):
+ self.base_path = self.get_base_path()
+
+ if self.ldap:
+ msg, update = self.update_configuration()
+ elif self.identifier:
+ msg, update = self.clear_single_configuration()
+ else:
+ msg, update = self.clear_configuration()
+ self.module.exit_json(msg=msg, changed=update, )
+
+ def __call__(self, *args, **kwargs):
+ self.update()
+
+
+def main():
+ settings = Ldap()
+ settings()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_lun_mapping.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_lun_mapping.py
new file mode 100644
index 00000000..1b190ad3
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_lun_mapping.py
@@ -0,0 +1,291 @@
+#!/usr/bin/python
+
+# (c) 2016, 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
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = '''
+---
+module: netapp_e_lun_mapping
+author:
+ - Kevin Hulquest (@hulquest)
+ - Nathan Swartz (@ndswartz)
+short_description: NetApp E-Series create, delete, or modify lun mappings
+description:
+ - Create, delete, or modify mappings between a volume and a targeted host/host+ group.
+version_added: "2.2"
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ state:
+ description:
+ - Present will ensure the mapping exists, absent will remove the mapping.
+ required: True
+ type: str
+ choices: ["present", "absent"]
+ target:
+ description:
+ - The name of host or hostgroup you wish to assign to the mapping
+ - If omitted, the default hostgroup is used.
+ - If the supplied I(volume_name) is associated with a different target, it will be updated to what is supplied here.
+ type: str
+ required: False
+ volume_name:
+ description:
+ - The name of the volume you wish to include in the mapping.
+ required: True
+ type: str
+ aliases:
+ - volume
+ lun:
+ description:
+ - The LUN value you wish to give the mapping.
+ - If the supplied I(volume_name) is associated with a different LUN, it will be updated to what is supplied here.
+ - LUN value will be determine by the storage-system when not specified.
+ version_added: 2.7
+ type: int
+ required: no
+ target_type:
+ description:
+ - This option specifies the whether the target should be a host or a group of hosts
+ - Only necessary when the target name is used for both a host and a group of hosts
+ choices:
+ - host
+ - group
+ version_added: 2.7
+ type: str
+ required: no
+'''
+
+EXAMPLES = '''
+---
+ - name: Map volume1 to the host target host1
+ netapp_e_lun_mapping:
+ ssid: 1
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ validate_certs: no
+ state: present
+ target: host1
+ volume: volume1
+ - name: Delete the lun mapping between volume1 and host1
+ netapp_e_lun_mapping:
+ ssid: 1
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ validate_certs: yes
+ state: absent
+ target: host1
+ volume: volume1
+'''
+RETURN = '''
+msg:
+ description: success of the module
+ returned: always
+ type: str
+ sample: Lun mapping is complete
+'''
+import json
+import logging
+from pprint import pformat
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import request, eseries_host_argument_spec
+from ansible.module_utils._text import to_native
+
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json"
+}
+
+
+class LunMapping(object):
+ def __init__(self):
+ argument_spec = eseries_host_argument_spec()
+ argument_spec.update(dict(
+ state=dict(required=True, choices=["present", "absent"]),
+ target=dict(required=False, default=None),
+ volume_name=dict(required=True, aliases=["volume"]),
+ lun=dict(type="int", required=False),
+ target_type=dict(required=False, choices=["host", "group"])))
+ self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ args = self.module.params
+
+ self.state = args["state"] in ["present"]
+ self.target = args["target"]
+ self.volume = args["volume_name"]
+ self.lun = args["lun"]
+ self.target_type = args["target_type"]
+ self.ssid = args["ssid"]
+ self.url = args["api_url"]
+ self.check_mode = self.module.check_mode
+ self.creds = dict(url_username=args["api_username"],
+ url_password=args["api_password"],
+ validate_certs=args["validate_certs"])
+ self.mapping_info = None
+
+ if not self.url.endswith('/'):
+ self.url += '/'
+
+ def update_mapping_info(self):
+ """Collect the current state of the storage array."""
+ response = None
+ try:
+ rc, response = request(self.url + "storage-systems/%s/graph" % self.ssid,
+ method="GET", headers=HEADERS, **self.creds)
+
+ except Exception as error:
+ self.module.fail_json(
+ msg="Failed to retrieve storage array graph. Id [%s]. Error [%s]" % (self.ssid, to_native(error)))
+
+ # Create dictionary containing host/cluster references mapped to their names
+ target_reference = {}
+ target_name = {}
+ target_type = {}
+
+ if self.target_type is None or self.target_type == "host":
+ for host in response["storagePoolBundle"]["host"]:
+ target_reference.update({host["hostRef"]: host["name"]})
+ target_name.update({host["name"]: host["hostRef"]})
+ target_type.update({host["name"]: "host"})
+
+ if self.target_type is None or self.target_type == "group":
+ for cluster in response["storagePoolBundle"]["cluster"]:
+
+ # Verify there is no ambiguity between target's type (ie host and group has the same name)
+ if self.target and self.target_type is None and cluster["name"] == self.target and \
+ self.target in target_name.keys():
+ self.module.fail_json(msg="Ambiguous target type: target name is used for both host and group"
+ " targets! Id [%s]" % self.ssid)
+
+ target_reference.update({cluster["clusterRef"]: cluster["name"]})
+ target_name.update({cluster["name"]: cluster["clusterRef"]})
+ target_type.update({cluster["name"]: "group"})
+
+ volume_reference = {}
+ volume_name = {}
+ lun_name = {}
+ for volume in response["volume"]:
+ volume_reference.update({volume["volumeRef"]: volume["name"]})
+ volume_name.update({volume["name"]: volume["volumeRef"]})
+ if volume["listOfMappings"]:
+ lun_name.update({volume["name"]: volume["listOfMappings"][0]["lun"]})
+ for volume in response["highLevelVolBundle"]["thinVolume"]:
+ volume_reference.update({volume["volumeRef"]: volume["name"]})
+ volume_name.update({volume["name"]: volume["volumeRef"]})
+ if volume["listOfMappings"]:
+ lun_name.update({volume["name"]: volume["listOfMappings"][0]["lun"]})
+
+ # Build current mapping object
+ self.mapping_info = dict(lun_mapping=[dict(volume_reference=mapping["volumeRef"],
+ map_reference=mapping["mapRef"],
+ lun_mapping_reference=mapping["lunMappingRef"],
+ lun=mapping["lun"]
+ ) for mapping in response["storagePoolBundle"]["lunMapping"]],
+ volume_by_reference=volume_reference,
+ volume_by_name=volume_name,
+ lun_by_name=lun_name,
+ target_by_reference=target_reference,
+ target_by_name=target_name,
+ target_type_by_name=target_type)
+
+ def get_lun_mapping(self):
+ """Find the matching lun mapping reference.
+
+ Returns: tuple(bool, int, int): contains volume match, volume mapping reference and mapping lun
+ """
+ target_match = False
+ reference = None
+ lun = None
+
+ self.update_mapping_info()
+
+ # Verify that when a lun is specified that it does not match an existing lun value unless it is associated with
+ # the specified volume (ie for an update)
+ if self.lun and any((self.lun == lun_mapping["lun"] and
+ self.target == self.mapping_info["target_by_reference"][lun_mapping["map_reference"]] and
+ self.volume != self.mapping_info["volume_by_reference"][lun_mapping["volume_reference"]]
+ ) for lun_mapping in self.mapping_info["lun_mapping"]):
+ self.module.fail_json(msg="Option lun value is already in use for target! Array Id [%s]." % self.ssid)
+
+ # Verify that when target_type is specified then it matches the target's actually type
+ if self.target and self.target_type and self.target in self.mapping_info["target_type_by_name"].keys() and \
+ self.mapping_info["target_type_by_name"][self.target] != self.target_type:
+ self.module.fail_json(
+ msg="Option target does not match the specified target_type! Id [%s]." % self.ssid)
+
+ # Verify volume and target exist if needed for expected state.
+ if self.state:
+ if self.volume not in self.mapping_info["volume_by_name"].keys():
+ self.module.fail_json(msg="Volume does not exist. Id [%s]." % self.ssid)
+ if self.target and self.target not in self.mapping_info["target_by_name"].keys():
+ self.module.fail_json(msg="Target does not exist. Id [%s'." % self.ssid)
+
+ for lun_mapping in self.mapping_info["lun_mapping"]:
+
+ # Find matching volume reference
+ if lun_mapping["volume_reference"] == self.mapping_info["volume_by_name"][self.volume]:
+ reference = lun_mapping["lun_mapping_reference"]
+ lun = lun_mapping["lun"]
+
+ # Determine if lun mapping is attached to target with the
+ if (lun_mapping["map_reference"] in self.mapping_info["target_by_reference"].keys() and
+ self.mapping_info["target_by_reference"][lun_mapping["map_reference"]] == self.target and
+ (self.lun is None or lun == self.lun)):
+ target_match = True
+
+ return target_match, reference, lun
+
+ def update(self):
+ """Execute the changes the require changes on the storage array."""
+ target_match, lun_reference, lun = self.get_lun_mapping()
+ update = (self.state and not target_match) or (not self.state and target_match)
+
+ if update and not self.check_mode:
+ try:
+ if self.state:
+ body = dict()
+ target = None if not self.target else self.mapping_info["target_by_name"][self.target]
+ if target:
+ body.update(dict(targetId=target))
+ if self.lun is not None:
+ body.update(dict(lun=self.lun))
+
+ if lun_reference:
+
+ rc, response = request(self.url + "storage-systems/%s/volume-mappings/%s/move"
+ % (self.ssid, lun_reference), method="POST", data=json.dumps(body),
+ headers=HEADERS, **self.creds)
+ else:
+ body.update(dict(mappableObjectId=self.mapping_info["volume_by_name"][self.volume]))
+ rc, response = request(self.url + "storage-systems/%s/volume-mappings" % self.ssid,
+ method="POST", data=json.dumps(body), headers=HEADERS, **self.creds)
+
+ else: # Remove existing lun mapping for volume and target
+ rc, response = request(self.url + "storage-systems/%s/volume-mappings/%s"
+ % (self.ssid, lun_reference),
+ method="DELETE", headers=HEADERS, **self.creds)
+ except Exception as error:
+ self.module.fail_json(
+ msg="Failed to update storage array lun mapping. Id [%s]. Error [%s]"
+ % (self.ssid, to_native(error)))
+
+ self.module.exit_json(msg="Lun mapping is complete.", changed=update)
+
+
+def main():
+ lun_mapping = LunMapping()
+ lun_mapping.update()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_mgmt_interface.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_mgmt_interface.py
new file mode 100644
index 00000000..58d133fa
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_mgmt_interface.py
@@ -0,0 +1,723 @@
+#!/usr/bin/python
+
+# (c) 2018, 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
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = """
+---
+module: netapp_e_mgmt_interface
+short_description: NetApp E-Series management interface configuration
+description:
+ - Configure the E-Series management interfaces
+version_added: '2.7'
+author:
+ - Michael Price (@lmprice)
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ state:
+ description:
+ - Enable or disable IPv4 network interface configuration.
+ - Either IPv4 or IPv6 must be enabled otherwise error will occur.
+ - Only required when enabling or disabling IPv4 network interface
+ choices:
+ - enable
+ - disable
+ required: no
+ type: str
+ aliases:
+ - enable_interface
+ controller:
+ description:
+ - The controller that owns the port you want to configure.
+ - Controller names are represented alphabetically, with the first controller as A,
+ the second as B, and so on.
+ - Current hardware models have either 1 or 2 available controllers, but that is not a guaranteed hard
+ limitation and could change in the future.
+ required: yes
+ type: str
+ choices:
+ - A
+ - B
+ name:
+ description:
+ - The port to modify the configuration for.
+ - The list of choices is not necessarily comprehensive. It depends on the number of ports
+ that are present in the system.
+ - The name represents the port number (typically from left to right on the controller),
+ beginning with a value of 1.
+ - Mutually exclusive with I(channel).
+ type: str
+ aliases:
+ - port
+ - iface
+ channel:
+ description:
+ - The port to modify the configuration for.
+ - The channel represents the port number (typically from left to right on the controller),
+ beginning with a value of 1.
+ - Mutually exclusive with I(name).
+ type: int
+ address:
+ description:
+ - The IPv4 address to assign to the interface.
+ - Should be specified in xx.xx.xx.xx form.
+ - Mutually exclusive with I(config_method=dhcp)
+ type: str
+ required: no
+ subnet_mask:
+ description:
+ - The subnet mask to utilize for the interface.
+ - Should be specified in xx.xx.xx.xx form.
+ - Mutually exclusive with I(config_method=dhcp)
+ type: str
+ required: no
+ gateway:
+ description:
+ - The IPv4 gateway address to utilize for the interface.
+ - Should be specified in xx.xx.xx.xx form.
+ - Mutually exclusive with I(config_method=dhcp)
+ type: str
+ required: no
+ config_method:
+ description:
+ - The configuration method type to use for network interface ports.
+ - dhcp is mutually exclusive with I(address), I(subnet_mask), and I(gateway).
+ choices:
+ - dhcp
+ - static
+ type: str
+ required: no
+ dns_config_method:
+ description:
+ - The configuration method type to use for DNS services.
+ - dhcp is mutually exclusive with I(dns_address), and I(dns_address_backup).
+ choices:
+ - dhcp
+ - static
+ type: str
+ required: no
+ dns_address:
+ description:
+ - Primary IPv4 DNS server address
+ type: str
+ required: no
+ dns_address_backup:
+ description:
+ - Backup IPv4 DNS server address
+ - Queried when primary DNS server fails
+ type: str
+ required: no
+ ntp_config_method:
+ description:
+ - The configuration method type to use for NTP services.
+ - disable is mutually exclusive with I(ntp_address) and I(ntp_address_backup).
+ - dhcp is mutually exclusive with I(ntp_address) and I(ntp_address_backup).
+ choices:
+ - disable
+ - dhcp
+ - static
+ type: str
+ required: no
+ ntp_address:
+ description:
+ - Primary IPv4 NTP server address
+ type: str
+ required: no
+ ntp_address_backup:
+ description:
+ - Backup IPv4 NTP server address
+ - Queried when primary NTP server fails
+ required: no
+ type: str
+ ssh:
+ type: bool
+ description:
+ - Enable ssh access to the controller for debug purposes.
+ - This is a controller-level setting.
+ - rlogin/telnet will be enabled for ancient equipment where ssh is not available.
+ required: no
+ log_path:
+ description:
+ - A local path to a file to be used for debug logging
+ type: str
+ required: no
+notes:
+ - Check mode is supported.
+ - The interface settings are applied synchronously, but changes to the interface itself (receiving a new IP address
+ via dhcp, etc), can take seconds or minutes longer to take effect.
+ - "Known issue: Changes specifically to down ports will result in a failure. However, this may not be the case in up
+ coming NetApp E-Series firmware releases (released after firmware version 11.40.2)."
+"""
+
+EXAMPLES = """
+ - name: Configure the first port on the A controller with a static IPv4 address
+ netapp_e_mgmt_interface:
+ channel: 1
+ controller: "A"
+ config_method: static
+ address: "192.168.1.100"
+ subnet_mask: "255.255.255.0"
+ gateway: "192.168.1.1"
+ ssid: "1"
+ api_url: "10.1.1.1:8443"
+ api_username: "admin"
+ api_password: "myPass"
+
+ - name: Disable ipv4 connectivity for the second port on the B controller
+ netapp_e_mgmt_interface:
+ channel: 2
+ controller: "B"
+ enable_interface: no
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+
+ - name: Enable ssh access for ports one and two on controller A
+ netapp_e_mgmt_interface:
+ channel: {{ item }}
+ controller: "A"
+ ssh: yes
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ loop:
+ - 1
+ - 2
+
+ - name: Configure static DNS settings for the first port on controller A
+ netapp_e_mgmt_interface:
+ channel: 1
+ controller: "A"
+ dns_config_method: static
+ dns_address: "192.168.1.100"
+ dns_address_backup: "192.168.1.1"
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+
+ - name: Configure static NTP settings for ports one and two on controller B
+ netapp_e_mgmt_interface:
+ channel: {{ item }}
+ controller: "B"
+ ntp_config_method: static
+ ntp_address: "129.100.1.100"
+ ntp_address_backup: "127.100.1.1"
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ loop:
+ - 1
+ - 2
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The interface settings have been updated.
+enabled:
+ description:
+ - Indicates whether IPv4 connectivity has been enabled or disabled.
+ - This does not necessarily indicate connectivity. If dhcp was enabled absent a dhcp server, for instance,
+ it is unlikely that the configuration will actually be valid.
+ returned: on success
+ sample: True
+ type: bool
+"""
+import json
+import logging
+from pprint import pformat, pprint
+import time
+import socket
+
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import request, eseries_host_argument_spec
+from ansible.module_utils._text import to_native
+
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+}
+
+
+class MgmtInterface(object):
+ MAX_RETRIES = 15
+
+ def __init__(self):
+ argument_spec = eseries_host_argument_spec()
+ argument_spec.update(dict(
+ state=dict(type="str", choices=["enable", "disable"],
+ aliases=["enable_interface"], required=False),
+ controller=dict(type="str", required=True, choices=["A", "B"]),
+ name=dict(type="str", aliases=["port", "iface"]),
+ channel=dict(type="int"),
+ address=dict(type="str", required=False),
+ subnet_mask=dict(type="str", required=False),
+ gateway=dict(type="str", required=False),
+ config_method=dict(type="str", required=False, choices=["dhcp", "static"]),
+ dns_config_method=dict(type="str", required=False, choices=["dhcp", "static"]),
+ dns_address=dict(type="str", required=False),
+ dns_address_backup=dict(type="str", required=False),
+ ntp_config_method=dict(type="str", required=False, choices=["disable", "dhcp", "static"]),
+ ntp_address=dict(type="str", required=False),
+ ntp_address_backup=dict(type="str", required=False),
+ ssh=dict(type="bool", required=False),
+ log_path=dict(type="str", required=False),
+ ))
+
+ required_if = [
+ ["state", "enable", ["config_method"]],
+ ["config_method", "static", ["address", "subnet_mask"]],
+ ["dns_config_method", "static", ["dns_address"]],
+ ["ntp_config_method", "static", ["ntp_address"]],
+ ]
+
+ mutually_exclusive = [
+ ["name", "channel"],
+ ]
+
+ self.module = AnsibleModule(argument_spec=argument_spec,
+ supports_check_mode=True,
+ required_if=required_if,
+ mutually_exclusive=mutually_exclusive)
+ args = self.module.params
+
+ self.controller = args["controller"]
+ self.name = args["name"]
+ self.channel = args["channel"]
+
+ self.config_method = args["config_method"]
+ self.address = args["address"]
+ self.subnet_mask = args["subnet_mask"]
+ self.gateway = args["gateway"]
+ self.enable_interface = None if args["state"] is None else args["state"] == "enable"
+
+ self.dns_config_method = args["dns_config_method"]
+ self.dns_address = args["dns_address"]
+ self.dns_address_backup = args["dns_address_backup"]
+
+ self.ntp_config_method = args["ntp_config_method"]
+ self.ntp_address = args["ntp_address"]
+ self.ntp_address_backup = args["ntp_address_backup"]
+
+ self.ssh = args["ssh"]
+
+ self.ssid = args["ssid"]
+ self.url = args["api_url"]
+ self.creds = dict(url_password=args["api_password"],
+ validate_certs=args["validate_certs"],
+ url_username=args["api_username"], )
+
+ self.retries = 0
+
+ self.check_mode = self.module.check_mode
+ self.post_body = dict()
+
+ log_path = args["log_path"]
+
+ # logging setup
+ self._logger = logging.getLogger(self.__class__.__name__)
+
+ if log_path:
+ logging.basicConfig(
+ level=logging.DEBUG, filename=log_path, filemode='w',
+ format='%(relativeCreated)dms %(levelname)s %(module)s.%(funcName)s:%(lineno)d\n %(message)s')
+
+ if not self.url.endswith('/'):
+ self.url += '/'
+
+ @property
+ def controllers(self):
+ """Retrieve a mapping of controller labels to their references
+ {
+ 'A': '070000000000000000000001',
+ 'B': '070000000000000000000002',
+ }
+ :return: the controllers defined on the system
+ """
+ try:
+ (rc, controllers) = request(self.url + 'storage-systems/%s/controllers'
+ % self.ssid, headers=HEADERS, **self.creds)
+ except Exception as err:
+ controllers = list()
+ self.module.fail_json(
+ msg="Failed to retrieve the controller settings. Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ controllers.sort(key=lambda c: c['physicalLocation']['slot'])
+
+ controllers_dict = dict()
+ i = ord('A')
+ for controller in controllers:
+ label = chr(i)
+ settings = dict(controllerSlot=controller['physicalLocation']['slot'],
+ controllerRef=controller['controllerRef'],
+ ssh=controller['networkSettings']['remoteAccessEnabled'])
+ controllers_dict[label] = settings
+ i += 1
+
+ return controllers_dict
+
+ @property
+ def interface(self):
+ net_interfaces = list()
+ try:
+ (rc, net_interfaces) = request(self.url + 'storage-systems/%s/configuration/ethernet-interfaces'
+ % self.ssid, headers=HEADERS, **self.creds)
+ except Exception as err:
+ self.module.fail_json(
+ msg="Failed to retrieve defined management interfaces. Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ controllers = self.controllers
+ controller = controllers[self.controller]
+
+ net_interfaces = [iface for iface in net_interfaces if iface["controllerRef"] == controller["controllerRef"]]
+
+ # Find the correct interface
+ iface = None
+ for net in net_interfaces:
+ if self.name:
+ if net["alias"] == self.name or net["interfaceName"] == self.name:
+ iface = net
+ break
+ elif self.channel:
+ if net["channel"] == self.channel:
+ iface = net
+ break
+
+ if iface is None:
+ identifier = self.name if self.name is not None else self.channel
+ self.module.fail_json(msg="We could not find an interface matching [%s] on Array=[%s]."
+ % (identifier, self.ssid))
+
+ return dict(alias=iface["alias"],
+ channel=iface["channel"],
+ link_status=iface["linkStatus"],
+ enabled=iface["ipv4Enabled"],
+ address=iface["ipv4Address"],
+ gateway=iface["ipv4GatewayAddress"],
+ subnet_mask=iface["ipv4SubnetMask"],
+ dns_config_method=iface["dnsProperties"]["acquisitionProperties"]["dnsAcquisitionType"],
+ dns_servers=iface["dnsProperties"]["acquisitionProperties"]["dnsServers"],
+ ntp_config_method=iface["ntpProperties"]["acquisitionProperties"]["ntpAcquisitionType"],
+ ntp_servers=iface["ntpProperties"]["acquisitionProperties"]["ntpServers"],
+ config_method=iface["ipv4AddressConfigMethod"],
+ controllerRef=iface["controllerRef"],
+ controllerSlot=iface["controllerSlot"],
+ ipv6Enabled=iface["ipv6Enabled"],
+ id=iface["interfaceRef"], )
+
+ def get_enable_interface_settings(self, iface, expected_iface, update, body):
+ """Enable or disable the IPv4 network interface."""
+ if self.enable_interface:
+ if not iface["enabled"]:
+ update = True
+ body["ipv4Enabled"] = True
+ else:
+ if iface["enabled"]:
+ update = True
+ body["ipv4Enabled"] = False
+
+ expected_iface["enabled"] = body["ipv4Enabled"]
+ return update, expected_iface, body
+
+ def get_interface_settings(self, iface, expected_iface, update, body):
+ """Update network interface settings."""
+
+ if self.config_method == "dhcp":
+ if iface["config_method"] != "configDhcp":
+ update = True
+ body["ipv4AddressConfigMethod"] = "configDhcp"
+
+ else:
+ if iface["config_method"] != "configStatic":
+ update = True
+ body["ipv4AddressConfigMethod"] = "configStatic"
+
+ if iface["address"] != self.address:
+ update = True
+ body["ipv4Address"] = self.address
+
+ if iface["subnet_mask"] != self.subnet_mask:
+ update = True
+ body["ipv4SubnetMask"] = self.subnet_mask
+
+ if self.gateway and iface["gateway"] != self.gateway:
+ update = True
+ body["ipv4GatewayAddress"] = self.gateway
+
+ expected_iface["address"] = body["ipv4Address"]
+ expected_iface["subnet_mask"] = body["ipv4SubnetMask"]
+ expected_iface["gateway"] = body["ipv4GatewayAddress"]
+
+ expected_iface["config_method"] = body["ipv4AddressConfigMethod"]
+
+ return update, expected_iface, body
+
+ def get_dns_server_settings(self, iface, expected_iface, update, body):
+ """Add DNS server information to the request body."""
+ if self.dns_config_method == "dhcp":
+ if iface["dns_config_method"] != "dhcp":
+ update = True
+ body["dnsAcquisitionDescriptor"] = dict(dnsAcquisitionType="dhcp")
+
+ elif self.dns_config_method == "static":
+ dns_servers = [dict(addressType="ipv4", ipv4Address=self.dns_address)]
+ if self.dns_address_backup:
+ dns_servers.append(dict(addressType="ipv4", ipv4Address=self.dns_address_backup))
+
+ body["dnsAcquisitionDescriptor"] = dict(dnsAcquisitionType="stat", dnsServers=dns_servers)
+
+ if (iface["dns_config_method"] != "stat" or
+ len(iface["dns_servers"]) != len(dns_servers) or
+ (len(iface["dns_servers"]) == 2 and
+ (iface["dns_servers"][0]["ipv4Address"] != self.dns_address or
+ iface["dns_servers"][1]["ipv4Address"] != self.dns_address_backup)) or
+ (len(iface["dns_servers"]) == 1 and
+ iface["dns_servers"][0]["ipv4Address"] != self.dns_address)):
+ update = True
+
+ expected_iface["dns_servers"] = dns_servers
+
+ expected_iface["dns_config_method"] = body["dnsAcquisitionDescriptor"]["dnsAcquisitionType"]
+ return update, expected_iface, body
+
+ def get_ntp_server_settings(self, iface, expected_iface, update, body):
+ """Add NTP server information to the request body."""
+ if self.ntp_config_method == "disable":
+ if iface["ntp_config_method"] != "disabled":
+ update = True
+ body["ntpAcquisitionDescriptor"] = dict(ntpAcquisitionType="disabled")
+
+ elif self.ntp_config_method == "dhcp":
+ if iface["ntp_config_method"] != "dhcp":
+ update = True
+ body["ntpAcquisitionDescriptor"] = dict(ntpAcquisitionType="dhcp")
+
+ elif self.ntp_config_method == "static":
+ ntp_servers = [dict(addrType="ipvx", ipvxAddress=dict(addressType="ipv4", ipv4Address=self.ntp_address))]
+ if self.ntp_address_backup:
+ ntp_servers.append(dict(addrType="ipvx",
+ ipvxAddress=dict(addressType="ipv4", ipv4Address=self.ntp_address_backup)))
+
+ body["ntpAcquisitionDescriptor"] = dict(ntpAcquisitionType="stat", ntpServers=ntp_servers)
+
+ if (iface["ntp_config_method"] != "stat" or
+ len(iface["ntp_servers"]) != len(ntp_servers) or
+ ((len(iface["ntp_servers"]) == 2 and
+ (iface["ntp_servers"][0]["ipvxAddress"]["ipv4Address"] != self.ntp_address or
+ iface["ntp_servers"][1]["ipvxAddress"]["ipv4Address"] != self.ntp_address_backup)) or
+ (len(iface["ntp_servers"]) == 1 and
+ iface["ntp_servers"][0]["ipvxAddress"]["ipv4Address"] != self.ntp_address))):
+ update = True
+
+ expected_iface["ntp_servers"] = ntp_servers
+
+ expected_iface["ntp_config_method"] = body["ntpAcquisitionDescriptor"]["ntpAcquisitionType"]
+ return update, expected_iface, body
+
+ def get_remote_ssh_settings(self, settings, update, body):
+ """Configure network interface ports for remote ssh access."""
+ if self.ssh != settings["ssh"]:
+ update = True
+
+ body["enableRemoteAccess"] = self.ssh
+ return update, body
+
+ def update_array(self, settings, iface):
+ """Update controller with new interface, dns service, ntp service and/or remote ssh access information.
+
+ :returns: whether information passed will modify the controller's current state
+ :rtype: bool
+ """
+ update = False
+ body = dict(controllerRef=settings['controllerRef'],
+ interfaceRef=iface['id'])
+ expected_iface = iface.copy()
+
+ # Check if api url is using the effected management interface to change itself
+ update_used_matching_address = False
+ if self.enable_interface and self.config_method:
+ netloc = list(urlparse.urlparse(self.url))[1]
+ address = netloc.split(":")[0]
+ address_info = socket.getaddrinfo(address, 8443)
+ url_address_info = socket.getaddrinfo(iface["address"], 8443)
+ update_used_matching_address = any(info in url_address_info for info in address_info)
+
+ self._logger.info("update_used_matching_address: %s", update_used_matching_address)
+
+ # Populate the body of the request and check for changes
+ if self.enable_interface is not None:
+ update, expected_iface, body = self.get_enable_interface_settings(iface, expected_iface, update, body)
+
+ if self.config_method is not None:
+ update, expected_iface, body = self.get_interface_settings(iface, expected_iface, update, body)
+
+ if self.dns_config_method is not None:
+ update, expected_iface, body = self.get_dns_server_settings(iface, expected_iface, update, body)
+
+ if self.ntp_config_method is not None:
+ update, expected_iface, body = self.get_ntp_server_settings(iface, expected_iface, update, body)
+
+ if self.ssh is not None:
+ update, body = self.get_remote_ssh_settings(settings, update, body)
+ iface["ssh"] = self.ssh
+ expected_iface["ssh"] = self.ssh
+
+ # debug information
+ self._logger.info(pformat(body))
+ self._logger.info(pformat(iface))
+ self._logger.info(pformat(expected_iface))
+
+ if self.check_mode:
+ return update
+
+ if update and not self.check_mode:
+ if not update_used_matching_address:
+ try:
+ (rc, data) = request(self.url + 'storage-systems/%s/configuration/ethernet-interfaces'
+ % self.ssid, method='POST', data=json.dumps(body), headers=HEADERS,
+ timeout=300, ignore_errors=True, **self.creds)
+ if rc == 422:
+ if data['retcode'] == "4" or data['retcode'] == "illegalParam":
+ if not (body['ipv4Enabled'] or iface['ipv6Enabled']):
+ self.module.fail_json(msg="This storage-system already has IPv6 connectivity disabled. "
+ "DHCP configuration for IPv4 is required at a minimum."
+ " Array Id [%s] Message [%s]."
+ % (self.ssid, data['errorMessage']))
+ else:
+ self.module.fail_json(msg="We failed to configure the management interface. Array Id "
+ "[%s] Message [%s]." % (self.ssid, data))
+ elif rc >= 300:
+ self.module.fail_json(
+ msg="We failed to configure the management interface. Array Id [%s] Message [%s]." %
+ (self.ssid, data))
+
+ # This is going to catch cases like a connection failure
+ except Exception as err:
+ self.module.fail_json(
+ msg="Connection failure: we failed to modify the network settings! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+ else:
+ self.update_api_address_interface_match(body)
+
+ return self.validate_changes(expected_iface) if update and iface["link_status"] != "up" else update
+
+ def update_api_address_interface_match(self, body):
+ """Change network interface address which matches the api_address"""
+ try:
+ try:
+ (rc, data) = request(self.url + 'storage-systems/%s/configuration/ethernet-interfaces' % self.ssid,
+ use_proxy=False, force=True, ignore_errors=True, method='POST',
+ data=json.dumps(body), headers=HEADERS, timeout=10, **self.creds)
+ except Exception:
+ url_parts = list(urlparse.urlparse(self.url))
+ domain = url_parts[1].split(":")
+ domain[0] = self.address
+ url_parts[1] = ":".join(domain)
+ expected_url = urlparse.urlunparse(url_parts)
+ self._logger.info(pformat(expected_url))
+
+ (rc, data) = request(expected_url + 'storage-systems/%s/configuration/ethernet-interfaces' % self.ssid,
+ headers=HEADERS, timeout=300, **self.creds)
+ return
+ except Exception as err:
+ self._logger.info(type(err))
+ self.module.fail_json(
+ msg="Connection failure: we failed to modify the network settings! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ def validate_changes(self, expected_iface, retry=6):
+ """Validate interface changes were applied to the controller interface port. 30 second timeout"""
+ if self.interface != expected_iface:
+ time.sleep(5)
+ if retry:
+ return self.validate_changes(expected_iface, retry - 1)
+
+ self.module.fail_json(msg="Update failure: we failed to verify the necessary state change.")
+
+ return True
+
+ def check_health(self):
+ """It's possible, due to a previous operation, for the API to report a 424 (offline) status for the
+ storage-system. Therefore, we run a manual check with retries to attempt to contact the system before we
+ continue.
+ """
+ try:
+ (rc, data) = request(self.url + 'storage-systems/%s/controllers'
+ % self.ssid, headers=HEADERS,
+ ignore_errors=True, **self.creds)
+
+ # We've probably recently changed the interface settings and it's still coming back up: retry.
+ if rc == 424:
+ if self.retries < self.MAX_RETRIES:
+ self.retries += 1
+ self._logger.info("We hit a 424, retrying in 5s.")
+ time.sleep(5)
+ self.check_health()
+ else:
+ self.module.fail_json(
+ msg="We failed to pull storage-system information. Array Id [%s] Message [%s]." %
+ (self.ssid, data))
+ elif rc >= 300:
+ self.module.fail_json(
+ msg="We failed to pull storage-system information. Array Id [%s] Message [%s]." %
+ (self.ssid, data))
+ # This is going to catch cases like a connection failure
+ except Exception as err:
+ if self.retries < self.MAX_RETRIES:
+ self._logger.info("We hit a connection failure, retrying in 5s.")
+ self.retries += 1
+ time.sleep(5)
+ self.check_health()
+ else:
+ self.module.fail_json(
+ msg="Connection failure: we failed to modify the network settings! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ def update(self):
+ """Update storage system with necessary changes."""
+ # Check if the storage array can be contacted
+ self.check_health()
+
+ # make the necessary changes to the storage system
+ settings = self.controllers[self.controller]
+ iface = self.interface
+ self._logger.info(pformat(settings))
+ self._logger.info(pformat(iface))
+ update = self.update_array(settings, iface)
+
+ self.module.exit_json(msg="The interface settings have been updated.", changed=update)
+
+ def __call__(self, *args, **kwargs):
+ self.update()
+
+
+def main():
+ iface = MgmtInterface()
+ iface()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_snapshot_group.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_snapshot_group.py
new file mode 100644
index 00000000..8bcee43f
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_snapshot_group.py
@@ -0,0 +1,376 @@
+#!/usr/bin/python
+
+# (c) 2016, 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
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+
+DOCUMENTATION = """
+---
+module: netapp_e_snapshot_group
+short_description: NetApp E-Series manage snapshot groups
+description:
+ - Create, update, delete snapshot groups for NetApp E-series storage arrays
+version_added: '2.2'
+author: Kevin Hulquest (@hulquest)
+options:
+ ssid:
+ description:
+ - Storage system identifier
+ type: str
+ api_username:
+ required: true
+ description:
+ - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API.
+ type: str
+ api_password:
+ required: true
+ description:
+ - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API.
+ type: str
+ api_url:
+ required: true
+ description:
+ - The url to the SANtricity WebServices Proxy or embedded REST API.
+ type: str
+ validate_certs:
+ required: false
+ default: true
+ description:
+ - Should https certificates be validated?
+ type: bool
+ state:
+ description:
+ - Whether to ensure the group is present or absent.
+ required: True
+ type: str
+ choices:
+ - present
+ - absent
+ name:
+ description:
+ - The name to give the snapshot group
+ type: str
+ required: True
+ base_volume_name:
+ description:
+ - The name of the base volume or thin volume to use as the base for the new snapshot group.
+ - If a snapshot group with an identical C(name) already exists but with a different base volume
+ an error will be returned.
+ type: str
+ required: True
+ repo_pct:
+ description:
+ - The size of the repository in relation to the size of the base volume
+ required: False
+ type: int
+ default: 20
+ warning_threshold:
+ description:
+ - The repository utilization warning threshold, as a percentage of the repository volume capacity.
+ required: False
+ type: int
+ default: 80
+ delete_limit:
+ description:
+ - The automatic deletion indicator.
+ - If non-zero, the oldest snapshot image will be automatically deleted when creating a new snapshot image to keep the total number of
+ snapshot images limited to the number specified.
+ - This value is overridden by the consistency group setting if this snapshot group is associated with a consistency group.
+ required: False
+ type: int
+ default: 30
+ full_policy:
+ description:
+ - The behavior on when the data repository becomes full.
+ - This value is overridden by consistency group setting if this snapshot group is associated with a consistency group
+ required: False
+ default: purgepit
+ type: str
+ choices: ['unknown', 'failbasewrites', 'purgepit']
+ storage_pool_name:
+ required: True
+ description:
+ - The name of the storage pool on which to allocate the repository volume.
+ type: str
+ rollback_priority:
+ required: False
+ description:
+ - The importance of the rollback operation.
+ - This value is overridden by consistency group setting if this snapshot group is associated with a consistency group
+ choices: ['highest', 'high', 'medium', 'low', 'lowest']
+ type: str
+ default: medium
+"""
+
+EXAMPLES = """
+ - name: Configure Snapshot group
+ netapp_e_snapshot_group:
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ validate_certs: "{{ netapp_api_validate_certs }}"
+ base_volume_name: SSGroup_test
+ name=: OOSS_Group
+ repo_pct: 20
+ warning_threshold: 85
+ delete_limit: 30
+ full_policy: purgepit
+ storage_pool_name: Disk_Pool_1
+ rollback_priority: medium
+"""
+RETURN = """
+msg:
+ description: Success message
+ returned: success
+ type: str
+ sample: json facts for newly created snapshot group.
+"""
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+}
+import json
+
+from ansible.module_utils.api import basic_auth_argument_spec
+from ansible.module_utils.basic import AnsibleModule
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.urls import open_url
+from ansible.module_utils.six.moves.urllib.error import HTTPError
+
+
+def request(url, data=None, headers=None, method='GET', use_proxy=True,
+ force=False, last_mod_time=None, timeout=10, validate_certs=True,
+ url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False):
+ try:
+ r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy,
+ force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs,
+ url_username=url_username, url_password=url_password, http_agent=http_agent,
+ force_basic_auth=force_basic_auth)
+ except HTTPError as err:
+ r = err.fp
+
+ try:
+ raw_data = r.read()
+ if raw_data:
+ data = json.loads(raw_data)
+ else:
+ raw_data = None
+ except Exception:
+ if ignore_errors:
+ pass
+ else:
+ raise Exception(raw_data)
+
+ resp_code = r.getcode()
+
+ if resp_code >= 400 and not ignore_errors:
+ raise Exception(resp_code, data)
+ else:
+ return resp_code, data
+
+
+class SnapshotGroup(object):
+ def __init__(self):
+
+ argument_spec = basic_auth_argument_spec()
+ argument_spec.update(
+ api_username=dict(type='str', required=True),
+ api_password=dict(type='str', required=True, no_log=True),
+ api_url=dict(type='str', required=True),
+ state=dict(required=True, choices=['present', 'absent']),
+ base_volume_name=dict(required=True),
+ name=dict(required=True),
+ repo_pct=dict(default=20, type='int'),
+ warning_threshold=dict(default=80, type='int'),
+ delete_limit=dict(default=30, type='int'),
+ full_policy=dict(default='purgepit', choices=['unknown', 'failbasewrites', 'purgepit']),
+ rollback_priority=dict(default='medium', choices=['highest', 'high', 'medium', 'low', 'lowest']),
+ storage_pool_name=dict(type='str'),
+ ssid=dict(required=True),
+ )
+
+ self.module = AnsibleModule(argument_spec=argument_spec)
+
+ self.post_data = dict()
+ self.warning_threshold = self.module.params['warning_threshold']
+ self.base_volume_name = self.module.params['base_volume_name']
+ self.name = self.module.params['name']
+ self.repo_pct = self.module.params['repo_pct']
+ self.delete_limit = self.module.params['delete_limit']
+ self.full_policy = self.module.params['full_policy']
+ self.rollback_priority = self.module.params['rollback_priority']
+ self.storage_pool_name = self.module.params['storage_pool_name']
+ self.state = self.module.params['state']
+
+ self.url = self.module.params['api_url']
+ self.user = self.module.params['api_username']
+ self.pwd = self.module.params['api_password']
+ self.certs = self.module.params['validate_certs']
+ self.ssid = self.module.params['ssid']
+
+ if not self.url.endswith('/'):
+ self.url += '/'
+
+ self.changed = False
+
+ @property
+ def pool_id(self):
+ pools = 'storage-systems/%s/storage-pools' % self.ssid
+ url = self.url + pools
+ try:
+ (rc, data) = request(url, headers=HEADERS, url_username=self.user, url_password=self.pwd)
+ except Exception as err:
+ self.module.fail_json(msg="Snapshot group module - Failed to fetch storage pools. " +
+ "Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+
+ for pool in data:
+ if pool['name'] == self.storage_pool_name:
+ self.pool_data = pool
+ return pool['id']
+
+ self.module.fail_json(msg="No storage pool with the name: '%s' was found" % self.name)
+
+ @property
+ def volume_id(self):
+ volumes = 'storage-systems/%s/volumes' % self.ssid
+ url = self.url + volumes
+ try:
+ rc, data = request(url, headers=HEADERS, url_username=self.user, url_password=self.pwd,
+ validate_certs=self.certs)
+ except Exception as err:
+ self.module.fail_json(msg="Snapshot group module - Failed to fetch volumes. " +
+ "Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+ qty = 0
+ for volume in data:
+ if volume['name'] == self.base_volume_name:
+ qty += 1
+
+ if qty > 1:
+ self.module.fail_json(msg="More than one volume with the name: %s was found, "
+ "please ensure your volume has a unique name" % self.base_volume_name)
+ else:
+ Id = volume['id']
+ self.volume = volume
+
+ try:
+ return Id
+ except NameError:
+ self.module.fail_json(msg="No volume with the name: %s, was found" % self.base_volume_name)
+
+ @property
+ def snapshot_group_id(self):
+ url = self.url + 'storage-systems/%s/snapshot-groups' % self.ssid
+ try:
+ rc, data = request(url, headers=HEADERS, url_username=self.user, url_password=self.pwd,
+ validate_certs=self.certs)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to fetch snapshot groups. " +
+ "Id [%s]. Error [%s]." % (self.ssid, to_native(err)))
+ for ssg in data:
+ if ssg['name'] == self.name:
+ self.ssg_data = ssg
+ return ssg['id']
+
+ return None
+
+ @property
+ def ssg_needs_update(self):
+ if self.ssg_data['fullWarnThreshold'] != self.warning_threshold or \
+ self.ssg_data['autoDeleteLimit'] != self.delete_limit or \
+ self.ssg_data['repFullPolicy'] != self.full_policy or \
+ self.ssg_data['rollbackPriority'] != self.rollback_priority:
+ return True
+ else:
+ return False
+
+ def create_snapshot_group(self):
+ self.post_data = dict(
+ baseMappableObjectId=self.volume_id,
+ name=self.name,
+ repositoryPercentage=self.repo_pct,
+ warningThreshold=self.warning_threshold,
+ autoDeleteLimit=self.delete_limit,
+ fullPolicy=self.full_policy,
+ storagePoolId=self.pool_id,
+ )
+ snapshot = 'storage-systems/%s/snapshot-groups' % self.ssid
+ url = self.url + snapshot
+ try:
+ rc, self.ssg_data = request(url, data=json.dumps(self.post_data), method='POST', headers=HEADERS,
+ url_username=self.user, url_password=self.pwd, validate_certs=self.certs)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to create snapshot group. " +
+ "Snapshot group [%s]. Id [%s]. Error [%s]." % (self.name,
+ self.ssid,
+ to_native(err)))
+
+ if not self.snapshot_group_id:
+ self.snapshot_group_id = self.ssg_data['id']
+
+ if self.ssg_needs_update:
+ self.update_ssg()
+ else:
+ self.module.exit_json(changed=True, **self.ssg_data)
+
+ def update_ssg(self):
+ self.post_data = dict(
+ warningThreshold=self.warning_threshold,
+ autoDeleteLimit=self.delete_limit,
+ fullPolicy=self.full_policy,
+ rollbackPriority=self.rollback_priority
+ )
+
+ url = self.url + "storage-systems/%s/snapshot-groups/%s" % (self.ssid, self.snapshot_group_id)
+ try:
+ rc, self.ssg_data = request(url, data=json.dumps(self.post_data), method='POST', headers=HEADERS,
+ url_username=self.user, url_password=self.pwd, validate_certs=self.certs)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to update snapshot group. " +
+ "Snapshot group [%s]. Id [%s]. Error [%s]." % (self.name,
+ self.ssid,
+ to_native(err)))
+
+ def apply(self):
+ if self.state == 'absent':
+ if self.snapshot_group_id:
+ try:
+ rc, resp = request(
+ self.url + 'storage-systems/%s/snapshot-groups/%s' % (self.ssid, self.snapshot_group_id),
+ method='DELETE', headers=HEADERS, url_password=self.pwd, url_username=self.user,
+ validate_certs=self.certs)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to delete snapshot group. " +
+ "Snapshot group [%s]. Id [%s]. Error [%s]." % (self.name,
+ self.ssid,
+ to_native(err)))
+ self.module.exit_json(changed=True, msg="Snapshot group removed", **self.ssg_data)
+ else:
+ self.module.exit_json(changed=False, msg="Snapshot group absent")
+
+ elif self.snapshot_group_id:
+ if self.ssg_needs_update:
+ self.update_ssg()
+ self.module.exit_json(changed=True, **self.ssg_data)
+ else:
+ self.module.exit_json(changed=False, **self.ssg_data)
+ else:
+ self.create_snapshot_group()
+
+
+def main():
+ vg = SnapshotGroup()
+ vg.apply()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_snapshot_images.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_snapshot_images.py
new file mode 100644
index 00000000..f0ea8fb6
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_snapshot_images.py
@@ -0,0 +1,257 @@
+#!/usr/bin/python
+
+# (c) 2016, 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
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+
+DOCUMENTATION = """
+---
+module: netapp_e_snapshot_images
+short_description: NetApp E-Series create and delete snapshot images
+description:
+ - Create and delete snapshots images on snapshot groups for NetApp E-series storage arrays.
+ - Only the oldest snapshot image can be deleted so consistency is preserved.
+ - "Related: Snapshot volumes are created from snapshot images."
+version_added: '2.2'
+author: Kevin Hulquest (@hulquest)
+options:
+ ssid:
+ description:
+ - Storage system identifier
+ type: str
+ api_username:
+ required: true
+ description:
+ - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API.
+ type: str
+ api_password:
+ required: true
+ description:
+ - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API.
+ type: str
+ api_url:
+ required: true
+ description:
+ - The url to the SANtricity WebServices Proxy or embedded REST API.
+ type: str
+ validate_certs:
+ required: false
+ default: true
+ description:
+ - Should https certificates be validated?
+ type: bool
+ snapshot_group:
+ description:
+ - The name of the snapshot group in which you want to create a snapshot image.
+ required: True
+ type: str
+ state:
+ description:
+ - Whether a new snapshot image should be created or oldest be deleted.
+ required: True
+ type: str
+ choices: ['create', 'remove']
+"""
+EXAMPLES = """
+ - name: Create Snapshot
+ netapp_e_snapshot_images:
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ validate_certs: "{{ validate_certs }}"
+ snapshot_group: "3300000060080E5000299C24000005B656D9F394"
+ state: 'create'
+"""
+RETURN = """
+---
+ msg:
+ description: State of operation
+ type: str
+ returned: always
+ sample: "Created snapshot image"
+ image_id:
+ description: ID of snapshot image
+ type: str
+ returned: state == created
+ sample: "3400000060080E5000299B640063074057BC5C5E "
+"""
+
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+}
+import json
+
+from ansible.module_utils.api import basic_auth_argument_spec
+from ansible.module_utils.basic import AnsibleModule
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.urls import open_url
+from ansible.module_utils.six.moves.urllib.error import HTTPError
+
+
+def request(url, data=None, headers=None, method='GET', use_proxy=True,
+ force=False, last_mod_time=None, timeout=10, validate_certs=True,
+ url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False):
+ try:
+ r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy,
+ force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs,
+ url_username=url_username, url_password=url_password, http_agent=http_agent,
+ force_basic_auth=force_basic_auth)
+ except HTTPError as err:
+ r = err.fp
+
+ try:
+ raw_data = r.read()
+ if raw_data:
+ data = json.loads(raw_data)
+ else:
+ raw_data = None
+ except Exception:
+ if ignore_errors:
+ pass
+ else:
+ raise Exception(raw_data)
+
+ resp_code = r.getcode()
+
+ if resp_code >= 400 and not ignore_errors:
+ raise Exception(resp_code, data)
+ else:
+ return resp_code, data
+
+
+def snapshot_group_from_name(module, ssid, api_url, api_pwd, api_usr, name):
+ snap_groups = 'storage-systems/%s/snapshot-groups' % ssid
+ snap_groups_url = api_url + snap_groups
+ (ret, snapshot_groups) = request(snap_groups_url, url_username=api_usr, url_password=api_pwd, headers=HEADERS,
+ validate_certs=module.params['validate_certs'])
+
+ snapshot_group_id = None
+ for snapshot_group in snapshot_groups:
+ if name == snapshot_group['label']:
+ snapshot_group_id = snapshot_group['pitGroupRef']
+ break
+ if snapshot_group_id is None:
+ module.fail_json(msg="Failed to lookup snapshot group. Group [%s]. Id [%s]." % (name, ssid))
+
+ return snapshot_group
+
+
+def oldest_image(module, ssid, api_url, api_pwd, api_usr, name):
+ get_status = 'storage-systems/%s/snapshot-images' % ssid
+ url = api_url + get_status
+
+ try:
+ (ret, images) = request(url, url_username=api_usr, url_password=api_pwd, headers=HEADERS,
+ validate_certs=module.params['validate_certs'])
+ except Exception as err:
+ module.fail_json(msg="Failed to get snapshot images for group. Group [%s]. Id [%s]. Error [%s]" %
+ (name, ssid, to_native(err)))
+ if not images:
+ module.exit_json(msg="There are no snapshot images to remove. Group [%s]. Id [%s]." % (name, ssid))
+
+ oldest = min(images, key=lambda x: x['pitSequenceNumber'])
+ if oldest is None or "pitRef" not in oldest:
+ module.fail_json(msg="Failed to lookup oldest snapshot group. Group [%s]. Id [%s]." % (name, ssid))
+
+ return oldest
+
+
+def create_image(module, ssid, api_url, pwd, user, p, snapshot_group):
+ snapshot_group_obj = snapshot_group_from_name(module, ssid, api_url, pwd, user, snapshot_group)
+ snapshot_group_id = snapshot_group_obj['pitGroupRef']
+ endpoint = 'storage-systems/%s/snapshot-images' % ssid
+ url = api_url + endpoint
+ post_data = json.dumps({'groupId': snapshot_group_id})
+
+ image_data = request(url, data=post_data, method='POST', url_username=user, url_password=pwd, headers=HEADERS,
+ validate_certs=module.params['validate_certs'])
+
+ if image_data[1]['status'] == 'optimal':
+ status = True
+ id = image_data[1]['id']
+ else:
+ status = False
+ id = ''
+
+ return status, id
+
+
+def delete_image(module, ssid, api_url, pwd, user, snapshot_group):
+ image = oldest_image(module, ssid, api_url, pwd, user, snapshot_group)
+ image_id = image['pitRef']
+ endpoint = 'storage-systems/%s/snapshot-images/%s' % (ssid, image_id)
+ url = api_url + endpoint
+
+ try:
+ (ret, image_data) = request(url, method='DELETE', url_username=user, url_password=pwd, headers=HEADERS,
+ validate_certs=module.params['validate_certs'])
+ except Exception as e:
+ image_data = (e[0], e[1])
+
+ if ret == 204:
+ deleted_status = True
+ error_message = ''
+ else:
+ deleted_status = False
+ error_message = image_data[1]['errorMessage']
+
+ return deleted_status, error_message
+
+
+def main():
+ argument_spec = basic_auth_argument_spec()
+ argument_spec.update(dict(
+ snapshot_group=dict(required=True, type='str'),
+ ssid=dict(required=True, type='str'),
+ api_url=dict(required=True),
+ api_username=dict(required=False),
+ api_password=dict(required=False, no_log=True),
+ validate_certs=dict(required=False, type='bool', default=True),
+ state=dict(required=True, choices=['create', 'remove'], type='str'),
+ ))
+ module = AnsibleModule(argument_spec)
+
+ p = module.params
+
+ ssid = p.pop('ssid')
+ api_url = p.pop('api_url')
+ user = p.pop('api_username')
+ pwd = p.pop('api_password')
+ snapshot_group = p.pop('snapshot_group')
+ desired_state = p.pop('state')
+
+ if not api_url.endswith('/'):
+ api_url += '/'
+
+ if desired_state == 'create':
+ created_status, snapshot_id = create_image(module, ssid, api_url, pwd, user, p, snapshot_group)
+
+ if created_status:
+ module.exit_json(changed=True, msg='Created snapshot image', image_id=snapshot_id)
+ else:
+ module.fail_json(
+ msg="Could not create snapshot image on system %s, in snapshot group %s" % (ssid, snapshot_group))
+ else:
+ deleted, error_msg = delete_image(module, ssid, api_url, pwd, user, snapshot_group)
+
+ if deleted:
+ module.exit_json(changed=True, msg='Deleted snapshot image for snapshot group [%s]' % (snapshot_group))
+ else:
+ module.fail_json(
+ msg="Could not create snapshot image on system %s, in snapshot group %s --- %s" % (
+ ssid, snapshot_group, error_msg))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_snapshot_volume.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_snapshot_volume.py
new file mode 100644
index 00000000..c8484915
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_snapshot_volume.py
@@ -0,0 +1,289 @@
+#!/usr/bin/python
+
+# (c) 2016, 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
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+
+DOCUMENTATION = """
+---
+module: netapp_e_snapshot_volume
+short_description: NetApp E-Series manage snapshot volumes.
+description:
+ - Create, update, remove snapshot volumes for NetApp E/EF-Series storage arrays.
+version_added: '2.2'
+author: Kevin Hulquest (@hulquest)
+notes:
+ - Only I(full_threshold) is supported for update operations. If the snapshot volume already exists and the threshold matches, then an C(ok) status
+ will be returned, no other changes can be made to a pre-existing snapshot volume.
+options:
+ api_username:
+ required: true
+ description:
+ - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API.
+ type: str
+ api_password:
+ required: true
+ description:
+ - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API.
+ type: str
+ api_url:
+ required: true
+ description:
+ - The url to the SANtricity WebServices Proxy or embedded REST API.
+ type: str
+ validate_certs:
+ required: false
+ default: true
+ description:
+ - Should https certificates be validated?
+ type: bool
+ ssid:
+ description:
+ - storage array ID
+ type: str
+ required: true
+ snapshot_image_id:
+ required: True
+ type: str
+ description:
+ - The identifier of the snapshot image used to create the new snapshot volume.
+ - "Note: You'll likely want to use the M(netapp_eseries.santricity.netapp_e_facts) module to find the ID of the image you want."
+ full_threshold:
+ description:
+ - The repository utilization warning threshold percentage
+ default: 85
+ type: int
+ name:
+ required: True
+ description:
+ - The name you wish to give the snapshot volume
+ type: str
+ view_mode:
+ required: True
+ type: str
+ description:
+ - The snapshot volume access mode
+ choices: ['readOnly', 'readWrite', 'modeUnknown', '__Undefined']
+ default: 'readOnly'
+ repo_percentage:
+ description:
+ - The size of the view in relation to the size of the base volume
+ default: 20
+ type: int
+ storage_pool_name:
+ description:
+ - Name of the storage pool on which to allocate the repository volume.
+ type: str
+ required: True
+ state:
+ description:
+ - Whether to create or remove the snapshot volume
+ required: True
+ type: str
+ choices:
+ - absent
+ - present
+"""
+EXAMPLES = """
+ - name: Snapshot volume
+ netapp_e_snapshot_volume:
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}/"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ state: present
+ storage_pool_name: "{{ snapshot_volume_storage_pool_name }}"
+ snapshot_image_id: "{{ snapshot_volume_image_id }}"
+ name: "{{ snapshot_volume_name }}"
+"""
+RETURN = """
+msg:
+ description: Success message
+ returned: success
+ type: str
+ sample: Json facts for the volume that was created.
+"""
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+}
+import json
+
+from ansible.module_utils.api import basic_auth_argument_spec
+from ansible.module_utils.basic import AnsibleModule
+
+from ansible.module_utils.urls import open_url
+from ansible.module_utils.six.moves.urllib.error import HTTPError
+
+
+def request(url, data=None, headers=None, method='GET', use_proxy=True,
+ force=False, last_mod_time=None, timeout=10, validate_certs=True,
+ url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False):
+ try:
+ r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy,
+ force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs,
+ url_username=url_username, url_password=url_password, http_agent=http_agent,
+ force_basic_auth=force_basic_auth)
+ except HTTPError as err:
+ r = err.fp
+
+ try:
+ raw_data = r.read()
+ if raw_data:
+ data = json.loads(raw_data)
+ else:
+ raw_data = None
+ except Exception:
+ if ignore_errors:
+ pass
+ else:
+ raise Exception(raw_data)
+
+ resp_code = r.getcode()
+
+ if resp_code >= 400 and not ignore_errors:
+ raise Exception(resp_code, data)
+ else:
+ return resp_code, data
+
+
+class SnapshotVolume(object):
+ def __init__(self):
+ argument_spec = basic_auth_argument_spec()
+ argument_spec.update(dict(
+ api_username=dict(type='str', required=True),
+ api_password=dict(type='str', required=True, no_log=True),
+ api_url=dict(type='str', required=True),
+ ssid=dict(type='str', required=True),
+ snapshot_image_id=dict(type='str', required=True),
+ full_threshold=dict(type='int', default=85),
+ name=dict(type='str', required=True),
+ view_mode=dict(type='str', default='readOnly',
+ choices=['readOnly', 'readWrite', 'modeUnknown', '__Undefined']),
+ repo_percentage=dict(type='int', default=20),
+ storage_pool_name=dict(type='str', required=True),
+ state=dict(type='str', required=True, choices=['absent', 'present'])
+ ))
+
+ self.module = AnsibleModule(argument_spec=argument_spec)
+ args = self.module.params
+ self.state = args['state']
+ self.ssid = args['ssid']
+ self.snapshot_image_id = args['snapshot_image_id']
+ self.full_threshold = args['full_threshold']
+ self.name = args['name']
+ self.view_mode = args['view_mode']
+ self.repo_percentage = args['repo_percentage']
+ self.storage_pool_name = args['storage_pool_name']
+ self.url = args['api_url']
+ self.user = args['api_username']
+ self.pwd = args['api_password']
+ self.certs = args['validate_certs']
+
+ if not self.url.endswith('/'):
+ self.url += '/'
+
+ @property
+ def pool_id(self):
+ pools = 'storage-systems/%s/storage-pools' % self.ssid
+ url = self.url + pools
+ (rc, data) = request(url, headers=HEADERS, url_username=self.user, url_password=self.pwd,
+ validate_certs=self.certs)
+
+ for pool in data:
+ if pool['name'] == self.storage_pool_name:
+ self.pool_data = pool
+ return pool['id']
+
+ self.module.fail_json(msg="No storage pool with the name: '%s' was found" % self.name)
+
+ @property
+ def ss_vol_exists(self):
+ rc, ss_vols = request(self.url + 'storage-systems/%s/snapshot-volumes' % self.ssid, headers=HEADERS,
+ url_username=self.user, url_password=self.pwd, validate_certs=self.certs)
+ if ss_vols:
+ for ss_vol in ss_vols:
+ if ss_vol['name'] == self.name:
+ self.ss_vol = ss_vol
+ return True
+ else:
+ return False
+
+ return False
+
+ @property
+ def ss_vol_needs_update(self):
+ if self.ss_vol['fullWarnThreshold'] != self.full_threshold:
+ return True
+ else:
+ return False
+
+ def create_ss_vol(self):
+ post_data = dict(
+ snapshotImageId=self.snapshot_image_id,
+ fullThreshold=self.full_threshold,
+ name=self.name,
+ viewMode=self.view_mode,
+ repositoryPercentage=self.repo_percentage,
+ repositoryPoolId=self.pool_id
+ )
+
+ rc, create_resp = request(self.url + 'storage-systems/%s/snapshot-volumes' % self.ssid,
+ data=json.dumps(post_data), headers=HEADERS, url_username=self.user,
+ url_password=self.pwd, validate_certs=self.certs, method='POST')
+
+ self.ss_vol = create_resp
+ # Doing a check after creation because the creation call fails to set the specified warning threshold
+ if self.ss_vol_needs_update:
+ self.update_ss_vol()
+ else:
+ self.module.exit_json(changed=True, **create_resp)
+
+ def update_ss_vol(self):
+ post_data = dict(
+ fullThreshold=self.full_threshold,
+ )
+
+ rc, resp = request(self.url + 'storage-systems/%s/snapshot-volumes/%s' % (self.ssid, self.ss_vol['id']),
+ data=json.dumps(post_data), headers=HEADERS, url_username=self.user, url_password=self.pwd,
+ method='POST', validate_certs=self.certs)
+
+ self.module.exit_json(changed=True, **resp)
+
+ def remove_ss_vol(self):
+ rc, resp = request(self.url + 'storage-systems/%s/snapshot-volumes/%s' % (self.ssid, self.ss_vol['id']),
+ headers=HEADERS, url_username=self.user, url_password=self.pwd, validate_certs=self.certs,
+ method='DELETE')
+ self.module.exit_json(changed=True, msg="Volume successfully deleted")
+
+ def apply(self):
+ if self.state == 'present':
+ if self.ss_vol_exists:
+ if self.ss_vol_needs_update:
+ self.update_ss_vol()
+ else:
+ self.module.exit_json(changed=False, **self.ss_vol)
+ else:
+ self.create_ss_vol()
+ else:
+ if self.ss_vol_exists:
+ self.remove_ss_vol()
+ else:
+ self.module.exit_json(changed=False, msg="Volume already absent")
+
+
+def main():
+ sv = SnapshotVolume()
+ sv.apply()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_storage_system.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_storage_system.py
new file mode 100644
index 00000000..a0f0d005
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_storage_system.py
@@ -0,0 +1,310 @@
+#!/usr/bin/python
+
+# (c) 2016, 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
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+
+DOCUMENTATION = '''
+module: netapp_e_storage_system
+version_added: "2.2"
+short_description: NetApp E-Series Web Services Proxy manage storage arrays
+description:
+- Manage the arrays accessible via a NetApp Web Services Proxy for NetApp E-series storage arrays.
+options:
+ api_username:
+ description:
+ - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API.
+ type: str
+ required: true
+ api_password:
+ description:
+ - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API.
+ type: str
+ required: true
+ api_url:
+ description:
+ - The url to the SANtricity WebServices Proxy or embedded REST API.
+ required: true
+ type: str
+ validate_certs:
+ description:
+ - Should https certificates be validated?
+ type: bool
+ default: 'yes'
+ ssid:
+ description:
+ - The ID of the array to manage. This value must be unique for each array.
+ type: str
+ required: true
+ state:
+ description:
+ - Whether the specified array should be configured on the Web Services Proxy or not.
+ required: true
+ type: str
+ choices: ['present', 'absent']
+ controller_addresses:
+ description:
+ - The list addresses for the out-of-band management adapter or the agent host. Mutually exclusive of array_wwn parameter.
+ type: list
+ required: true
+ array_wwn:
+ description:
+ - The WWN of the array to manage. Only necessary if in-band managing multiple arrays on the same agent host. Mutually exclusive of
+ controller_addresses parameter.
+ type: str
+ array_password:
+ description:
+ - The management password of the array to manage, if set.
+ type: str
+ enable_trace:
+ description:
+ - Enable trace logging for SYMbol calls to the storage system.
+ type: bool
+ default: 'no'
+ meta_tags:
+ description:
+ - Optional meta tags to associate to this storage system
+ type: list
+ array_status_timeout_sec:
+ description:
+ - Array status timeout measured in seconds
+ default: 60
+ type: int
+author: Kevin Hulquest (@hulquest)
+'''
+
+EXAMPLES = '''
+---
+ - name: Presence of storage system
+ netapp_e_storage_system:
+ ssid: "{{ item.key }}"
+ state: present
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ validate_certs: "{{ netapp_api_validate_certs }}"
+ controller_addresses:
+ - "{{ item.value.address1 }}"
+ - "{{ item.value.address2 }}"
+ with_dict: "{{ storage_systems }}"
+ when: check_storage_system
+'''
+
+RETURN = '''
+msg:
+ description: State of request
+ type: str
+ returned: always
+ sample: 'Storage system removed.'
+'''
+import json
+from datetime import datetime as dt, timedelta
+from time import sleep
+
+from ansible.module_utils.api import basic_auth_argument_spec
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils._text import to_native
+from ansible.module_utils.urls import open_url
+from ansible.module_utils.six.moves.urllib.error import HTTPError
+
+
+def request(url, data=None, headers=None, method='GET', use_proxy=True,
+ force=False, last_mod_time=None, timeout=10, validate_certs=True,
+ url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False):
+ try:
+ r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy,
+ force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs,
+ url_username=url_username, url_password=url_password, http_agent=http_agent,
+ force_basic_auth=force_basic_auth)
+ except HTTPError as err:
+ r = err.fp
+
+ try:
+ raw_data = r.read()
+ if raw_data:
+ data = json.loads(raw_data)
+ else:
+ raw_data = None
+ except Exception:
+ if ignore_errors:
+ pass
+ else:
+ raise Exception(raw_data)
+
+ resp_code = r.getcode()
+
+ if resp_code >= 400 and not ignore_errors:
+ raise Exception(resp_code, data)
+ else:
+ return resp_code, data
+
+
+def do_post(ssid, api_url, post_headers, api_usr, api_pwd, validate_certs, request_body, timeout):
+ (rc, resp) = request(api_url + "/storage-systems", data=request_body, headers=post_headers,
+ method='POST', url_username=api_usr, url_password=api_pwd,
+ validate_certs=validate_certs)
+ status = None
+ return_resp = resp
+ if 'status' in resp:
+ status = resp['status']
+
+ if rc == 201:
+ status = 'neverContacted'
+ fail_after_time = dt.utcnow() + timedelta(seconds=timeout)
+
+ while status == 'neverContacted':
+ if dt.utcnow() > fail_after_time:
+ raise Exception("web proxy timed out waiting for array status")
+
+ sleep(1)
+ (rc, system_resp) = request(api_url + "/storage-systems/%s" % ssid,
+ headers=dict(Accept="application/json"), url_username=api_usr,
+ url_password=api_pwd, validate_certs=validate_certs,
+ ignore_errors=True)
+ status = system_resp['status']
+ return_resp = system_resp
+
+ return status, return_resp
+
+
+def main():
+ argument_spec = basic_auth_argument_spec()
+ argument_spec.update(dict(
+ state=dict(required=True, choices=['present', 'absent']),
+ ssid=dict(required=True, type='str'),
+ controller_addresses=dict(type='list'),
+ array_wwn=dict(required=False, type='str'),
+ array_password=dict(required=False, type='str', no_log=True),
+ array_status_timeout_sec=dict(default=60, type='int'),
+ enable_trace=dict(default=False, type='bool'),
+ meta_tags=dict(type='list')
+ ))
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ mutually_exclusive=[['controller_addresses', 'array_wwn']],
+ required_if=[('state', 'present', ['controller_addresses'])]
+ )
+
+ p = module.params
+
+ state = p['state']
+ ssid = p['ssid']
+ controller_addresses = p['controller_addresses']
+ array_wwn = p['array_wwn']
+ array_password = p['array_password']
+ array_status_timeout_sec = p['array_status_timeout_sec']
+ validate_certs = p['validate_certs']
+ meta_tags = p['meta_tags']
+ enable_trace = p['enable_trace']
+
+ api_usr = p['api_username']
+ api_pwd = p['api_password']
+ api_url = p['api_url']
+
+ changed = False
+ array_exists = False
+
+ try:
+ (rc, resp) = request(api_url + "/storage-systems/%s" % ssid, headers=dict(Accept="application/json"),
+ url_username=api_usr, url_password=api_pwd, validate_certs=validate_certs,
+ ignore_errors=True)
+ except Exception as err:
+ module.fail_json(msg="Error accessing storage-system with id [%s]. Error [%s]" % (ssid, to_native(err)))
+
+ array_exists = True
+ array_detail = resp
+
+ if rc == 200:
+ if state == 'absent':
+ changed = True
+ array_exists = False
+ elif state == 'present':
+ current_addresses = frozenset(i for i in (array_detail['ip1'], array_detail['ip2']) if i)
+ if set(controller_addresses) != current_addresses:
+ changed = True
+ if array_detail['wwn'] != array_wwn and array_wwn is not None:
+ module.fail_json(
+ msg='It seems you may have specified a bad WWN. The storage system ID you specified, %s, currently has the WWN of %s' %
+ (ssid, array_detail['wwn'])
+ )
+ elif rc == 404:
+ if state == 'present':
+ changed = True
+ array_exists = False
+ else:
+ changed = False
+ module.exit_json(changed=changed, msg="Storage system was not present.")
+
+ if changed and not module.check_mode:
+ if state == 'present':
+ if not array_exists:
+ # add the array
+ array_add_req = dict(
+ id=ssid,
+ controllerAddresses=controller_addresses,
+ metaTags=meta_tags,
+ enableTrace=enable_trace
+ )
+
+ if array_wwn:
+ array_add_req['wwn'] = array_wwn
+
+ if array_password:
+ array_add_req['password'] = array_password
+
+ post_headers = dict(Accept="application/json")
+ post_headers['Content-Type'] = 'application/json'
+ request_data = json.dumps(array_add_req)
+
+ try:
+ (rc, resp) = do_post(ssid, api_url, post_headers, api_usr, api_pwd, validate_certs, request_data,
+ array_status_timeout_sec)
+ except Exception as err:
+ module.fail_json(msg="Failed to add storage system. Id[%s]. Request body [%s]. Error[%s]." %
+ (ssid, request_data, to_native(err)))
+
+ else: # array exists, modify...
+ post_headers = dict(Accept="application/json")
+ post_headers['Content-Type'] = 'application/json'
+ post_body = dict(
+ controllerAddresses=controller_addresses,
+ removeAllTags=True,
+ enableTrace=enable_trace,
+ metaTags=meta_tags
+ )
+
+ try:
+ (rc, resp) = do_post(ssid, api_url, post_headers, api_usr, api_pwd, validate_certs, post_body,
+ array_status_timeout_sec)
+ except Exception as err:
+ module.fail_json(msg="Failed to update storage system. Id[%s]. Request body [%s]. Error[%s]." %
+ (ssid, post_body, to_native(err)))
+
+ elif state == 'absent':
+ # delete the array
+ try:
+ (rc, resp) = request(api_url + "/storage-systems/%s" % ssid, method='DELETE',
+ url_username=api_usr,
+ url_password=api_pwd, validate_certs=validate_certs)
+ except Exception as err:
+ module.fail_json(msg="Failed to remove storage array. Id[%s]. Error[%s]." % (ssid, to_native(err)))
+
+ if rc == 422:
+ module.exit_json(changed=changed, msg="Storage system was not presented.")
+ if rc == 204:
+ module.exit_json(changed=changed, msg="Storage system removed.")
+
+ module.exit_json(changed=changed, **resp)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_storagepool.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_storagepool.py
new file mode 100644
index 00000000..5c74a415
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_storagepool.py
@@ -0,0 +1,941 @@
+#!/usr/bin/python
+
+# (c) 2016, 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
+
+ANSIBLE_METADATA = {"metadata_version": "1.1",
+ "status": ["deprecated"],
+ "supported_by": "community"}
+
+DOCUMENTATION = """
+---
+module: netapp_e_storagepool
+short_description: NetApp E-Series manage volume groups and disk pools
+description: Create or remove volume groups and disk pools for NetApp E-series storage arrays.
+version_added: '2.2'
+author:
+ - Kevin Hulquest (@hulquest)
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ state:
+ description:
+ - Whether the specified storage pool should exist or not.
+ - Note that removing a storage pool currently requires the removal of all defined volumes first.
+ required: true
+ type: str
+ choices: ["present", "absent"]
+ name:
+ description:
+ - The name of the storage pool to manage
+ type: str
+ required: true
+ criteria_drive_count:
+ description:
+ - The number of disks to use for building the storage pool.
+ - When I(state=="present") then I(criteria_drive_count) or I(criteria_min_usable_capacity) must be specified.
+ - The pool will be expanded if this number exceeds the number of disks already in place (See expansion note below)
+ required: false
+ type: int
+ criteria_min_usable_capacity:
+ description:
+ - The minimum size of the storage pool (in size_unit).
+ - When I(state=="present") then I(criteria_drive_count) or I(criteria_min_usable_capacity) must be specified.
+ - The pool will be expanded if this value exceeds its current size. (See expansion note below)
+ required: false
+ type: float
+ criteria_drive_type:
+ description:
+ - The type of disk (hdd or ssd) to use when searching for candidates to use.
+ - When not specified each drive type will be evaluated until successful drive candidates are found starting with
+ the most prevalent drive type.
+ required: false
+ type: str
+ choices: ["hdd","ssd"]
+ criteria_size_unit:
+ description:
+ - The unit used to interpret size parameters
+ choices: ["bytes", "b", "kb", "mb", "gb", "tb", "pb", "eb", "zb", "yb"]
+ type: str
+ default: "gb"
+ criteria_drive_min_size:
+ description:
+ - The minimum individual drive size (in size_unit) to consider when choosing drives for the storage pool.
+ type: float
+ criteria_drive_interface_type:
+ description:
+ - The interface type to use when selecting drives for the storage pool
+ - If not provided then all interface types will be considered.
+ choices: ["sas", "sas4k", "fibre", "fibre520b", "scsi", "sata", "pata"]
+ type: str
+ required: false
+ criteria_drive_require_da:
+ description:
+ - Ensures the storage pool will be created with only data assurance (DA) capable drives.
+ - Only available for new storage pools; existing storage pools cannot be converted.
+ default: false
+ type: bool
+ version_added: '2.9'
+ criteria_drive_require_fde:
+ description:
+ - Whether full disk encryption ability is required for drives to be added to the storage pool
+ default: false
+ type: bool
+ raid_level:
+ description:
+ - The RAID level of the storage pool to be created.
+ - Required only when I(state=="present").
+ - When I(raid_level=="raidDiskPool") then I(criteria_drive_count >= 10 or criteria_drive_count >= 11) is required
+ depending on the storage array specifications.
+ - When I(raid_level=="raid0") then I(1<=criteria_drive_count) is required.
+ - When I(raid_level=="raid1") then I(2<=criteria_drive_count) is required.
+ - When I(raid_level=="raid3") then I(3<=criteria_drive_count<=30) is required.
+ - When I(raid_level=="raid5") then I(3<=criteria_drive_count<=30) is required.
+ - When I(raid_level=="raid6") then I(5<=criteria_drive_count<=30) is required.
+ - Note that raidAll will be treated as raidDiskPool and raid3 as raid5.
+ required: false
+ choices: ["raidAll", "raid0", "raid1", "raid3", "raid5", "raid6", "raidDiskPool"]
+ type: str
+ default: "raidDiskPool"
+ secure_pool:
+ description:
+ - Enables security at rest feature on the storage pool.
+ - Will only work if all drives in the pool are security capable (FDE, FIPS, or mix)
+ - Warning, once security is enabled it is impossible to disable without erasing the drives.
+ required: false
+ type: bool
+ reserve_drive_count:
+ description:
+ - Set the number of drives reserved by the storage pool for reconstruction operations.
+ - Only valid on raid disk pools.
+ type: int
+ required: false
+ remove_volumes:
+ description:
+ - Prior to removing a storage pool, delete all volumes in the pool.
+ default: true
+ type: bool
+ erase_secured_drives:
+ description:
+ - If I(state=="absent") then all storage pool drives will be erase
+ - If I(state=="present") then delete all available storage array drives that have security enabled.
+ default: true
+ type: bool
+notes:
+ - The expansion operations are non-blocking due to the time consuming nature of expanding volume groups
+ - Traditional volume groups (raid0, raid1, raid5, raid6) are performed in steps dictated by the storage array. Each
+ required step will be attempted until the request fails which is likely because of the required expansion time.
+ - raidUnsupported will be treated as raid0, raidAll as raidDiskPool and raid3 as raid5.
+ - Tray loss protection and drawer loss protection will be chosen if at all possible.
+"""
+EXAMPLES = """
+- name: No disk groups
+ netapp_e_storagepool:
+ ssid: "{{ ssid }}"
+ name: "{{ item }}"
+ state: absent
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ validate_certs: "{{ netapp_api_validate_certs }}"
+"""
+RETURN = """
+msg:
+ description: Success message
+ returned: success
+ type: str
+ sample: Json facts for the pool that was created.
+"""
+import functools
+from itertools import groupby
+from time import sleep
+from pprint import pformat
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+
+def get_most_common_elements(iterator):
+ """Returns a generator containing a descending list of most common elements."""
+ if not isinstance(iterator, list):
+ raise TypeError("iterator must be a list.")
+
+ grouped = [(key, len(list(group))) for key, group in groupby(sorted(iterator))]
+ return sorted(grouped, key=lambda x: x[1], reverse=True)
+
+
+def memoize(func):
+ """Generic memoizer for any function with any number of arguments including zero."""
+
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ class MemoizeFuncArgs(dict):
+ def __missing__(self, _key):
+ self[_key] = func(*args, **kwargs)
+ return self[_key]
+
+ key = str((args, kwargs)) if args and kwargs else "no_argument_response"
+ return MemoizeFuncArgs().__getitem__(key)
+
+ return wrapper
+
+
+class NetAppESeriesStoragePool(NetAppESeriesModule):
+ EXPANSION_TIMEOUT_SEC = 10
+ DEFAULT_DISK_POOL_MINIMUM_DISK_COUNT = 11
+
+ def __init__(self):
+ version = "02.00.0000.0000"
+ ansible_options = dict(
+ state=dict(required=True, choices=["present", "absent"], type="str"),
+ name=dict(required=True, type="str"),
+ criteria_size_unit=dict(choices=["bytes", "b", "kb", "mb", "gb", "tb", "pb", "eb", "zb", "yb"],
+ default="gb", type="str"),
+ criteria_drive_count=dict(type="int"),
+ criteria_drive_interface_type=dict(choices=["sas", "sas4k", "fibre", "fibre520b", "scsi", "sata", "pata"],
+ type="str"),
+ criteria_drive_type=dict(choices=["ssd", "hdd"], type="str", required=False),
+ criteria_drive_min_size=dict(type="float"),
+ criteria_drive_require_da=dict(type="bool", required=False),
+ criteria_drive_require_fde=dict(type="bool", required=False),
+ criteria_min_usable_capacity=dict(type="float"),
+ raid_level=dict(choices=["raidAll", "raid0", "raid1", "raid3", "raid5", "raid6", "raidDiskPool"],
+ default="raidDiskPool"),
+ erase_secured_drives=dict(type="bool", default=True),
+ secure_pool=dict(type="bool", default=False),
+ reserve_drive_count=dict(type="int"),
+ remove_volumes=dict(type="bool", default=True))
+
+ required_if = [["state", "present", ["raid_level"]]]
+ super(NetAppESeriesStoragePool, self).__init__(ansible_options=ansible_options,
+ web_services_version=version,
+ supports_check_mode=True,
+ required_if=required_if)
+
+ args = self.module.params
+ self.state = args["state"]
+ self.ssid = args["ssid"]
+ self.name = args["name"]
+ self.criteria_drive_count = args["criteria_drive_count"]
+ self.criteria_min_usable_capacity = args["criteria_min_usable_capacity"]
+ self.criteria_size_unit = args["criteria_size_unit"]
+ self.criteria_drive_min_size = args["criteria_drive_min_size"]
+ self.criteria_drive_type = args["criteria_drive_type"]
+ self.criteria_drive_interface_type = args["criteria_drive_interface_type"]
+ self.criteria_drive_require_fde = args["criteria_drive_require_fde"]
+ self.criteria_drive_require_da = args["criteria_drive_require_da"]
+ self.raid_level = args["raid_level"]
+ self.erase_secured_drives = args["erase_secured_drives"]
+ self.secure_pool = args["secure_pool"]
+ self.reserve_drive_count = args["reserve_drive_count"]
+ self.remove_volumes = args["remove_volumes"]
+ self.pool_detail = None
+
+ # Change all sizes to be measured in bytes
+ if self.criteria_min_usable_capacity:
+ self.criteria_min_usable_capacity = int(self.criteria_min_usable_capacity *
+ self.SIZE_UNIT_MAP[self.criteria_size_unit])
+ if self.criteria_drive_min_size:
+ self.criteria_drive_min_size = int(self.criteria_drive_min_size *
+ self.SIZE_UNIT_MAP[self.criteria_size_unit])
+ self.criteria_size_unit = "bytes"
+
+ # Adjust unused raid level option to reflect documentation
+ if self.raid_level == "raidAll":
+ self.raid_level = "raidDiskPool"
+ if self.raid_level == "raid3":
+ self.raid_level = "raid5"
+
+ @property
+ @memoize
+ def available_drives(self):
+ """Determine the list of available drives"""
+ return [drive["id"] for drive in self.drives if drive["available"] and drive["status"] == "optimal"]
+
+ @property
+ @memoize
+ def available_drive_types(self):
+ """Determine the types of available drives sorted by the most common first."""
+ types = [drive["driveMediaType"] for drive in self.drives]
+ return [entry[0] for entry in get_most_common_elements(types)]
+
+ @property
+ @memoize
+ def available_drive_interface_types(self):
+ """Determine the types of available drives."""
+ interfaces = [drive["phyDriveType"] for drive in self.drives]
+ return [entry[0] for entry in get_most_common_elements(interfaces)]
+
+ @property
+ def storage_pool_drives(self):
+ """Retrieve list of drives found in storage pool."""
+ return [drive for drive in self.drives if drive["currentVolumeGroupRef"] == self.pool_detail["id"] and not drive["hotSpare"]]
+
+ @property
+ def expandable_drive_count(self):
+ """Maximum number of drives that a storage pool can be expanded at a given time."""
+ capabilities = None
+ if self.raid_level == "raidDiskPool":
+ return len(self.available_drives)
+
+ try:
+ rc, capabilities = self.request("storage-systems/%s/capabilities" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to fetch maximum expandable drive count. Array id [%s]. Error[%s]."
+ % (self.ssid, to_native(error)))
+
+ return capabilities["featureParameters"]["maxDCEDrives"]
+
+ @property
+ def disk_pool_drive_minimum(self):
+ """Provide the storage array's minimum disk pool drive count."""
+ rc, attr = self.request("storage-systems/%s/symbol/getSystemAttributeDefaults" % self.ssid, ignore_errors=True)
+
+ # Standard minimum is 11 drives but some allow 10 drives. 10 will be the default
+ if (rc != 200 or "minimumDriveCount" not in attr["defaults"]["diskPoolDefaultAttributes"].keys() or
+ attr["defaults"]["diskPoolDefaultAttributes"]["minimumDriveCount"] == 0):
+ return self.DEFAULT_DISK_POOL_MINIMUM_DISK_COUNT
+
+ return attr["defaults"]["diskPoolDefaultAttributes"]["minimumDriveCount"]
+
+ def get_available_drive_capacities(self, drive_id_list=None):
+ """Determine the list of available drive capacities."""
+ if drive_id_list:
+ available_drive_capacities = set([int(drive["usableCapacity"]) for drive in self.drives
+ if drive["id"] in drive_id_list and drive["available"] and
+ drive["status"] == "optimal"])
+ else:
+ available_drive_capacities = set([int(drive["usableCapacity"]) for drive in self.drives
+ if drive["available"] and drive["status"] == "optimal"])
+
+ self.module.log("available drive capacities: %s" % available_drive_capacities)
+ return list(available_drive_capacities)
+
+ @property
+ def drives(self):
+ """Retrieve list of drives found in storage pool."""
+ drives = None
+ try:
+ rc, drives = self.request("storage-systems/%s/drives" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to fetch disk drives. Array id [%s]. Error[%s]."
+ % (self.ssid, to_native(error)))
+
+ return drives
+
+ def is_drive_count_valid(self, drive_count):
+ """Validate drive count criteria is met."""
+ if self.criteria_drive_count and drive_count < self.criteria_drive_count:
+ return False
+
+ if self.raid_level == "raidDiskPool":
+ return drive_count >= self.disk_pool_drive_minimum
+ if self.raid_level == "raid0":
+ return drive_count > 0
+ if self.raid_level == "raid1":
+ return drive_count >= 2 and (drive_count % 2) == 0
+ if self.raid_level in ["raid3", "raid5"]:
+ return 3 <= drive_count <= 30
+ if self.raid_level == "raid6":
+ return 5 <= drive_count <= 30
+ return False
+
+ @property
+ def storage_pool(self):
+ """Retrieve storage pool information."""
+ storage_pools_resp = None
+ try:
+ rc, storage_pools_resp = self.request("storage-systems/%s/storage-pools" % self.ssid)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to get storage pools. Array id [%s]. Error[%s]. State[%s]."
+ % (self.ssid, to_native(err), self.state))
+
+ pool_detail = [pool for pool in storage_pools_resp if pool["name"] == self.name]
+ return pool_detail[0] if pool_detail else dict()
+
+ @property
+ def storage_pool_volumes(self):
+ """Retrieve list of volumes associated with storage pool."""
+ volumes_resp = None
+ try:
+ rc, volumes_resp = self.request("storage-systems/%s/volumes" % self.ssid)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to get storage pools. Array id [%s]. Error[%s]. State[%s]."
+ % (self.ssid, to_native(err), self.state))
+
+ group_ref = self.storage_pool["volumeGroupRef"]
+ storage_pool_volume_list = [volume["id"] for volume in volumes_resp if volume["volumeGroupRef"] == group_ref]
+ return storage_pool_volume_list
+
+ def get_ddp_capacity(self, expansion_drive_list):
+ """Return the total usable capacity based on the additional drives."""
+
+ def get_ddp_error_percent(_drive_count, _extent_count):
+ """Determine the space reserved for reconstruction"""
+ if _drive_count <= 36:
+ if _extent_count <= 600:
+ return 0.40
+ elif _extent_count <= 1400:
+ return 0.35
+ elif _extent_count <= 6200:
+ return 0.20
+ elif _extent_count <= 50000:
+ return 0.15
+ elif _drive_count <= 64:
+ if _extent_count <= 600:
+ return 0.20
+ elif _extent_count <= 1400:
+ return 0.15
+ elif _extent_count <= 6200:
+ return 0.10
+ elif _extent_count <= 50000:
+ return 0.05
+ elif _drive_count <= 480:
+ if _extent_count <= 600:
+ return 0.20
+ elif _extent_count <= 1400:
+ return 0.15
+ elif _extent_count <= 6200:
+ return 0.10
+ elif _extent_count <= 50000:
+ return 0.05
+
+ self.module.fail_json(msg="Drive count exceeded the error percent table. Array[%s]" % self.ssid)
+
+ def get_ddp_reserved_drive_count(_disk_count):
+ """Determine the number of reserved drive."""
+ reserve_count = 0
+
+ if self.reserve_drive_count:
+ reserve_count = self.reserve_drive_count
+ elif _disk_count >= 256:
+ reserve_count = 8
+ elif _disk_count >= 192:
+ reserve_count = 7
+ elif _disk_count >= 128:
+ reserve_count = 6
+ elif _disk_count >= 64:
+ reserve_count = 4
+ elif _disk_count >= 32:
+ reserve_count = 3
+ elif _disk_count >= 12:
+ reserve_count = 2
+ elif _disk_count == 11:
+ reserve_count = 1
+
+ return reserve_count
+
+ if self.pool_detail:
+ drive_count = len(self.storage_pool_drives) + len(expansion_drive_list)
+ else:
+ drive_count = len(expansion_drive_list)
+
+ drive_usable_capacity = min(min(self.get_available_drive_capacities()),
+ min(self.get_available_drive_capacities(expansion_drive_list)))
+ drive_data_extents = ((drive_usable_capacity - 8053063680) / 536870912)
+ maximum_stripe_count = (drive_count * drive_data_extents) / 10
+
+ error_percent = get_ddp_error_percent(drive_count, drive_data_extents)
+ error_overhead = (drive_count * drive_data_extents / 10 * error_percent + 10) / 10
+
+ total_stripe_count = maximum_stripe_count - error_overhead
+ stripe_count_per_drive = total_stripe_count / drive_count
+ reserved_stripe_count = get_ddp_reserved_drive_count(drive_count) * stripe_count_per_drive
+ available_stripe_count = total_stripe_count - reserved_stripe_count
+
+ return available_stripe_count * 4294967296
+
+ @memoize
+ def get_candidate_drives(self):
+ """Retrieve set of drives candidates for creating a new storage pool."""
+
+ def get_candidate_drive_request():
+ """Perform request for new volume creation."""
+ candidates_list = list()
+ drive_types = [self.criteria_drive_type] if self.criteria_drive_type else self.available_drive_types
+ interface_types = [self.criteria_drive_interface_type] \
+ if self.criteria_drive_interface_type else self.available_drive_interface_types
+
+ for interface_type in interface_types:
+ for drive_type in drive_types:
+ candidates = None
+ volume_candidate_request_data = dict(
+ type="diskPool" if self.raid_level == "raidDiskPool" else "traditional",
+ diskPoolVolumeCandidateRequestData=dict(
+ reconstructionReservedDriveCount=65535))
+ candidate_selection_type = dict(
+ candidateSelectionType="count",
+ driveRefList=dict(driveRef=self.available_drives))
+ criteria = dict(raidLevel=self.raid_level,
+ phyDriveType=interface_type,
+ dssPreallocEnabled=False,
+ securityType="capable" if self.criteria_drive_require_fde else "none",
+ driveMediaType=drive_type,
+ onlyProtectionInformationCapable=True if self.criteria_drive_require_da else False,
+ volumeCandidateRequestData=volume_candidate_request_data,
+ allocateReserveSpace=False,
+ securityLevel="fde" if self.criteria_drive_require_fde else "none",
+ candidateSelectionType=candidate_selection_type)
+
+ try:
+ rc, candidates = self.request("storage-systems/%s/symbol/getVolumeCandidates?verboseError"
+ "Response=true" % self.ssid, data=criteria, method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve volume candidates. Array [%s]. Error [%s]."
+ % (self.ssid, to_native(error)))
+
+ if candidates:
+ candidates_list.extend(candidates["volumeCandidate"])
+
+ # Sort output based on tray and then drawer protection first
+ tray_drawer_protection = list()
+ tray_protection = list()
+ drawer_protection = list()
+ no_protection = list()
+ sorted_candidates = list()
+ for item in candidates_list:
+ if item["trayLossProtection"]:
+ if item["drawerLossProtection"]:
+ tray_drawer_protection.append(item)
+ else:
+ tray_protection.append(item)
+ elif item["drawerLossProtection"]:
+ drawer_protection.append(item)
+ else:
+ no_protection.append(item)
+
+ if tray_drawer_protection:
+ sorted_candidates.extend(tray_drawer_protection)
+ if tray_protection:
+ sorted_candidates.extend(tray_protection)
+ if drawer_protection:
+ sorted_candidates.extend(drawer_protection)
+ if no_protection:
+ sorted_candidates.extend(no_protection)
+
+ return sorted_candidates
+
+ # Determine the appropriate candidate list
+ for candidate in get_candidate_drive_request():
+
+ # Evaluate candidates for required drive count, collective drive usable capacity and minimum drive size
+ if self.criteria_drive_count:
+ if self.criteria_drive_count != int(candidate["driveCount"]):
+ continue
+ if self.criteria_min_usable_capacity:
+ if ((self.raid_level == "raidDiskPool" and self.criteria_min_usable_capacity >
+ self.get_ddp_capacity(candidate["driveRefList"]["driveRef"])) or
+ self.criteria_min_usable_capacity > int(candidate["usableSize"])):
+ continue
+ if self.criteria_drive_min_size:
+ if self.criteria_drive_min_size > min(self.get_available_drive_capacities(candidate["driveRefList"]["driveRef"])):
+ continue
+
+ return candidate
+
+ self.module.fail_json(msg="Not enough drives to meet the specified criteria. Array [%s]." % self.ssid)
+
+ @memoize
+ def get_expansion_candidate_drives(self):
+ """Retrieve required expansion drive list.
+
+ Note: To satisfy the expansion criteria each item in the candidate list must added specified group since there
+ is a potential limitation on how many drives can be incorporated at a time.
+ * Traditional raid volume groups must be added two drives maximum at a time. No limits on raid disk pools.
+
+ :return list(candidate): list of candidate structures from the getVolumeGroupExpansionCandidates symbol endpoint
+ """
+
+ def get_expansion_candidate_drive_request():
+ """Perform the request for expanding existing volume groups or disk pools.
+
+ Note: the list of candidate structures do not necessarily produce candidates that meet all criteria.
+ """
+ candidates_list = None
+ url = "storage-systems/%s/symbol/getVolumeGroupExpansionCandidates?verboseErrorResponse=true" % self.ssid
+ if self.raid_level == "raidDiskPool":
+ url = "storage-systems/%s/symbol/getDiskPoolExpansionCandidates?verboseErrorResponse=true" % self.ssid
+
+ try:
+ rc, candidates_list = self.request(url, method="POST", data=self.pool_detail["id"])
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve volume candidates. Array [%s]. Error [%s]."
+ % (self.ssid, to_native(error)))
+
+ return candidates_list["candidates"]
+
+ required_candidate_list = list()
+ required_additional_drives = 0
+ required_additional_capacity = 0
+ total_required_capacity = 0
+
+ # determine whether and how much expansion is need to satisfy the specified criteria
+ if self.criteria_min_usable_capacity:
+ total_required_capacity = self.criteria_min_usable_capacity
+ required_additional_capacity = self.criteria_min_usable_capacity - int(self.pool_detail["totalRaidedSpace"])
+
+ if self.criteria_drive_count:
+ required_additional_drives = self.criteria_drive_count - len(self.storage_pool_drives)
+
+ # Determine the appropriate expansion candidate list
+ if required_additional_drives > 0 or required_additional_capacity > 0:
+ for candidate in get_expansion_candidate_drive_request():
+
+ if self.criteria_drive_min_size:
+ if self.criteria_drive_min_size > min(self.get_available_drive_capacities(candidate["drives"])):
+ continue
+
+ if self.raid_level == "raidDiskPool":
+ if (len(candidate["drives"]) >= required_additional_drives and
+ self.get_ddp_capacity(candidate["drives"]) >= total_required_capacity):
+ required_candidate_list.append(candidate)
+ break
+ else:
+ required_additional_drives -= len(candidate["drives"])
+ required_additional_capacity -= int(candidate["usableCapacity"])
+ required_candidate_list.append(candidate)
+
+ # Determine if required drives and capacities are satisfied
+ if required_additional_drives <= 0 and required_additional_capacity <= 0:
+ break
+ else:
+ self.module.fail_json(msg="Not enough drives to meet the specified criteria. Array [%s]." % self.ssid)
+
+ return required_candidate_list
+
+ def get_reserve_drive_count(self):
+ """Retrieve the current number of reserve drives for raidDiskPool (Only for raidDiskPool)."""
+
+ if not self.pool_detail:
+ self.module.fail_json(msg="The storage pool must exist. Array [%s]." % self.ssid)
+
+ if self.raid_level != "raidDiskPool":
+ self.module.fail_json(msg="The storage pool must be a raidDiskPool. Pool [%s]. Array [%s]."
+ % (self.pool_detail["id"], self.ssid))
+
+ return self.pool_detail["volumeGroupData"]["diskPoolData"]["reconstructionReservedDriveCount"]
+
+ def get_maximum_reserve_drive_count(self):
+ """Retrieve the maximum number of reserve drives for storage pool (Only for raidDiskPool)."""
+ if self.raid_level != "raidDiskPool":
+ self.module.fail_json(msg="The storage pool must be a raidDiskPool. Pool [%s]. Array [%s]."
+ % (self.pool_detail["id"], self.ssid))
+
+ drives_ids = list()
+
+ if self.pool_detail:
+ drives_ids.extend(self.storage_pool_drives)
+ for candidate in self.get_expansion_candidate_drives():
+ drives_ids.extend((candidate["drives"]))
+ else:
+ candidate = self.get_candidate_drives()
+ drives_ids.extend(candidate["driveRefList"]["driveRef"])
+
+ drive_count = len(drives_ids)
+ maximum_reserve_drive_count = min(int(drive_count * 0.2 + 1), drive_count - 10)
+ if maximum_reserve_drive_count > 10:
+ maximum_reserve_drive_count = 10
+
+ return maximum_reserve_drive_count
+
+ def set_reserve_drive_count(self, check_mode=False):
+ """Set the reserve drive count for raidDiskPool."""
+ changed = False
+
+ if self.raid_level == "raidDiskPool" and self.reserve_drive_count:
+ maximum_count = self.get_maximum_reserve_drive_count()
+
+ if self.reserve_drive_count < 0 or self.reserve_drive_count > maximum_count:
+ self.module.fail_json(msg="Supplied reserve drive count is invalid or exceeds the maximum allowed. "
+ "Note that it may be necessary to wait for expansion operations to complete "
+ "before the adjusting the reserve drive count. Maximum [%s]. Array [%s]."
+ % (maximum_count, self.ssid))
+
+ if self.reserve_drive_count != self.get_reserve_drive_count():
+ changed = True
+
+ if not check_mode:
+ try:
+ rc, resp = self.request("storage-systems/%s/symbol/setDiskPoolReservedDriveCount" % self.ssid,
+ method="POST", data=dict(volumeGroupRef=self.pool_detail["id"],
+ newDriveCount=self.reserve_drive_count))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to set reserve drive count for disk pool. Disk Pool [%s]."
+ " Array [%s]." % (self.pool_detail["id"], self.ssid))
+
+ return changed
+
+ def erase_all_available_secured_drives(self, check_mode=False):
+ """Erase all available drives that have encryption at rest feature enabled."""
+ changed = False
+ drives_list = list()
+ for drive in self.drives:
+ if drive["available"] and drive["fdeEnabled"]:
+ changed = True
+ drives_list.append(drive["id"])
+
+ if drives_list and not check_mode:
+ try:
+ rc, resp = self.request("storage-systems/%s/symbol/reprovisionDrive?verboseErrorResponse=true"
+ % self.ssid, method="POST", data=dict(driveRef=drives_list))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to erase all secured drives. Array [%s]" % self.ssid)
+
+ return changed
+
+ def create_storage_pool(self):
+ """Create new storage pool."""
+ url = "storage-systems/%s/symbol/createVolumeGroup?verboseErrorResponse=true" % self.ssid
+ request_body = dict(label=self.name,
+ candidate=self.get_candidate_drives())
+
+ if self.raid_level == "raidDiskPool":
+ url = "storage-systems/%s/symbol/createDiskPool?verboseErrorResponse=true" % self.ssid
+
+ request_body.update(
+ dict(backgroundOperationPriority="useDefault",
+ criticalReconstructPriority="useDefault",
+ degradedReconstructPriority="useDefault",
+ poolUtilizationCriticalThreshold=65535,
+ poolUtilizationWarningThreshold=0))
+
+ if self.reserve_drive_count:
+ request_body.update(dict(volumeCandidateData=dict(
+ diskPoolVolumeCandidateData=dict(reconstructionReservedDriveCount=self.reserve_drive_count))))
+
+ try:
+ rc, resp = self.request(url, method="POST", data=request_body)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to create storage pool. Array id [%s]. Error[%s]."
+ % (self.ssid, to_native(error)))
+
+ # Update drive and storage pool information
+ self.pool_detail = self.storage_pool
+
+ def delete_storage_pool(self):
+ """Delete storage pool."""
+ storage_pool_drives = [drive["id"] for drive in self.storage_pool_drives if drive["fdeEnabled"]]
+ try:
+ delete_volumes_parameter = "?delete-volumes=true" if self.remove_volumes else ""
+ rc, resp = self.request("storage-systems/%s/storage-pools/%s%s"
+ % (self.ssid, self.pool_detail["id"], delete_volumes_parameter), method="DELETE")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to delete storage pool. Pool id [%s]. Array id [%s]. Error[%s]."
+ % (self.pool_detail["id"], self.ssid, to_native(error)))
+
+ if storage_pool_drives and self.erase_secured_drives:
+ try:
+ rc, resp = self.request("storage-systems/%s/symbol/reprovisionDrive?verboseErrorResponse=true"
+ % self.ssid, method="POST", data=dict(driveRef=storage_pool_drives))
+ except Exception as error:
+ self.module.fail_json(msg="Failed to erase drives prior to creating new storage pool. Array [%s]."
+ " Error [%s]." % (self.ssid, to_native(error)))
+
+ def secure_storage_pool(self, check_mode=False):
+ """Enable security on an existing storage pool"""
+ self.pool_detail = self.storage_pool
+ needs_secure_pool = False
+
+ if not self.secure_pool and self.pool_detail["securityType"] == "enabled":
+ self.module.fail_json(msg="It is not possible to disable storage pool security! See array documentation.")
+ if self.secure_pool and self.pool_detail["securityType"] != "enabled":
+ needs_secure_pool = True
+
+ if needs_secure_pool and not check_mode:
+ try:
+ rc, resp = self.request("storage-systems/%s/storage-pools/%s" % (self.ssid, self.pool_detail["id"]),
+ data=dict(securePool=True), method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to secure storage pool. Pool id [%s]. Array [%s]. Error"
+ " [%s]." % (self.pool_detail["id"], self.ssid, to_native(error)))
+
+ self.pool_detail = self.storage_pool
+ return needs_secure_pool
+
+ def migrate_raid_level(self, check_mode=False):
+ """Request storage pool raid level migration."""
+ needs_migration = self.raid_level != self.pool_detail["raidLevel"]
+ if needs_migration and self.pool_detail["raidLevel"] == "raidDiskPool":
+ self.module.fail_json(msg="Raid level cannot be changed for disk pools")
+
+ if needs_migration and not check_mode:
+ sp_raid_migrate_req = dict(raidLevel=self.raid_level)
+
+ try:
+ rc, resp = self.request("storage-systems/%s/storage-pools/%s/raid-type-migration"
+ % (self.ssid, self.name), data=sp_raid_migrate_req, method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to change the raid level of storage pool. Array id [%s]."
+ " Error[%s]." % (self.ssid, to_native(error)))
+
+ self.pool_detail = self.storage_pool
+ return needs_migration
+
+ def expand_storage_pool(self, check_mode=False):
+ """Add drives to existing storage pool.
+
+ :return bool: whether drives were required to be added to satisfy the specified criteria."""
+ expansion_candidate_list = self.get_expansion_candidate_drives()
+ changed_required = bool(expansion_candidate_list)
+ estimated_completion_time = 0.0
+
+ # build expandable groupings of traditional raid candidate
+ required_expansion_candidate_list = list()
+ while expansion_candidate_list:
+ subset = list()
+ while expansion_candidate_list and len(subset) < self.expandable_drive_count:
+ subset.extend(expansion_candidate_list.pop()["drives"])
+ required_expansion_candidate_list.append(subset)
+
+ if required_expansion_candidate_list and not check_mode:
+ url = "storage-systems/%s/symbol/startVolumeGroupExpansion?verboseErrorResponse=true" % self.ssid
+ if self.raid_level == "raidDiskPool":
+ url = "storage-systems/%s/symbol/startDiskPoolExpansion?verboseErrorResponse=true" % self.ssid
+
+ while required_expansion_candidate_list:
+ candidate_drives_list = required_expansion_candidate_list.pop()
+ request_body = dict(volumeGroupRef=self.pool_detail["volumeGroupRef"],
+ driveRef=candidate_drives_list)
+ try:
+ rc, resp = self.request(url, method="POST", data=request_body)
+ except Exception as error:
+ rc, actions_resp = self.request("storage-systems/%s/storage-pools/%s/action-progress"
+ % (self.ssid, self.pool_detail["id"]), ignore_errors=True)
+ if rc == 200 and actions_resp:
+ actions = [action["currentAction"] for action in actions_resp
+ if action["volumeRef"] in self.storage_pool_volumes]
+ self.module.fail_json(msg="Failed to add drives to the storage pool possibly because of actions"
+ " in progress. Actions [%s]. Pool id [%s]. Array id [%s]. Error[%s]."
+ % (", ".join(actions), self.pool_detail["id"], self.ssid,
+ to_native(error)))
+
+ self.module.fail_json(msg="Failed to add drives to storage pool. Pool id [%s]. Array id [%s]."
+ " Error[%s]." % (self.pool_detail["id"], self.ssid, to_native(error)))
+
+ # Wait for expansion completion unless it is the last request in the candidate list
+ if required_expansion_candidate_list:
+ for dummy in range(self.EXPANSION_TIMEOUT_SEC):
+ rc, actions_resp = self.request("storage-systems/%s/storage-pools/%s/action-progress"
+ % (self.ssid, self.pool_detail["id"]), ignore_errors=True)
+ if rc == 200:
+ for action in actions_resp:
+ if (action["volumeRef"] in self.storage_pool_volumes and
+ action["currentAction"] == "remappingDce"):
+ sleep(1)
+ estimated_completion_time = action["estimatedTimeToCompletion"]
+ break
+ else:
+ estimated_completion_time = 0.0
+ break
+
+ return changed_required, estimated_completion_time
+
+ def apply(self):
+ """Apply requested state to storage array."""
+ changed = False
+
+ if self.state == "present":
+ if self.criteria_drive_count is None and self.criteria_min_usable_capacity is None:
+ self.module.fail_json(msg="One of criteria_min_usable_capacity or criteria_drive_count must be"
+ " specified.")
+ if self.criteria_drive_count and not self.is_drive_count_valid(self.criteria_drive_count):
+ self.module.fail_json(msg="criteria_drive_count must be valid for the specified raid level.")
+
+ self.pool_detail = self.storage_pool
+ self.module.log(pformat(self.pool_detail))
+
+ if self.state == "present" and self.erase_secured_drives:
+ self.erase_all_available_secured_drives(check_mode=True)
+
+ # Determine whether changes need to be applied to the storage array
+ if self.pool_detail:
+
+ if self.state == "absent":
+ changed = True
+
+ elif self.state == "present":
+
+ if self.criteria_drive_count and self.criteria_drive_count < len(self.storage_pool_drives):
+ self.module.fail_json(msg="Failed to reduce the size of the storage pool. Array [%s]. Pool [%s]."
+ % (self.ssid, self.pool_detail["id"]))
+
+ if self.criteria_drive_type and self.criteria_drive_type != self.pool_detail["driveMediaType"]:
+ self.module.fail_json(msg="Failed! It is not possible to modify storage pool media type."
+ " Array [%s]. Pool [%s]." % (self.ssid, self.pool_detail["id"]))
+
+ if (self.criteria_drive_require_da is not None and self.criteria_drive_require_da !=
+ self.pool_detail["protectionInformationCapabilities"]["protectionInformationCapable"]):
+ self.module.fail_json(msg="Failed! It is not possible to modify DA-capability. Array [%s]."
+ " Pool [%s]." % (self.ssid, self.pool_detail["id"]))
+
+ # Evaluate current storage pool for required change.
+ needs_expansion, estimated_completion_time = self.expand_storage_pool(check_mode=True)
+ if needs_expansion:
+ changed = True
+ if self.migrate_raid_level(check_mode=True):
+ changed = True
+ if self.secure_storage_pool(check_mode=True):
+ changed = True
+ if self.set_reserve_drive_count(check_mode=True):
+ changed = True
+
+ elif self.state == "present":
+ changed = True
+
+ # Apply changes to storage array
+ msg = "No changes were required for the storage pool [%s]."
+ if changed and not self.module.check_mode:
+ if self.state == "present":
+ if self.erase_secured_drives:
+ self.erase_all_available_secured_drives()
+
+ if self.pool_detail:
+ change_list = list()
+
+ # Expansion needs to occur before raid level migration to account for any sizing needs.
+ expanded, estimated_completion_time = self.expand_storage_pool()
+ if expanded:
+ change_list.append("expanded")
+ if self.migrate_raid_level():
+ change_list.append("raid migration")
+ if self.secure_storage_pool():
+ change_list.append("secured")
+ if self.set_reserve_drive_count():
+ change_list.append("adjusted reserve drive count")
+
+ if change_list:
+ msg = "Following changes have been applied to the storage pool [%s]: " + ", ".join(change_list)
+
+ if expanded:
+ msg += "\nThe expansion operation will complete in an estimated %s minutes."\
+ % estimated_completion_time
+ else:
+ self.create_storage_pool()
+ msg = "Storage pool [%s] was created."
+
+ if self.secure_storage_pool():
+ msg = "Storage pool [%s] was created and secured."
+ if self.set_reserve_drive_count():
+ msg += " Adjusted reserve drive count."
+
+ elif self.pool_detail:
+ self.delete_storage_pool()
+ msg = "Storage pool [%s] removed."
+
+ self.pool_detail = self.storage_pool
+ self.module.log(pformat(self.pool_detail))
+ self.module.log(msg % self.name)
+ self.module.exit_json(msg=msg % self.name, changed=changed, **self.pool_detail)
+
+
+def main():
+ storage_pool = NetAppESeriesStoragePool()
+ storage_pool.apply()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_syslog.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_syslog.py
new file mode 100644
index 00000000..1e6e8588
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_syslog.py
@@ -0,0 +1,286 @@
+#!/usr/bin/python
+
+# (c) 2018, 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
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = """
+---
+module: netapp_e_syslog
+short_description: NetApp E-Series manage syslog settings
+description:
+ - Allow the syslog settings to be configured for an individual E-Series storage-system
+version_added: '2.7'
+author: Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ state:
+ description:
+ - Add or remove the syslog server configuration for E-Series storage array.
+ - Existing syslog server configuration will be removed or updated when its address matches I(address).
+ - Fully qualified hostname that resolve to an IPv4 address that matches I(address) will not be
+ treated as a match.
+ choices:
+ - present
+ - absent
+ type: str
+ default: present
+ address:
+ description:
+ - The syslog server's IPv4 address or a fully qualified hostname.
+ - All existing syslog configurations will be removed when I(state=absent) and I(address=None).
+ type: str
+ port:
+ description:
+ - This is the port the syslog server is using.
+ default: 514
+ type: int
+ protocol:
+ description:
+ - This is the transmission protocol the syslog server's using to receive syslog messages.
+ choices:
+ - udp
+ - tcp
+ - tls
+ default: udp
+ type: str
+ components:
+ description:
+ - The e-series logging components define the specific logs to transfer to the syslog server.
+ - At the time of writing, 'auditLog' is the only logging component but more may become available.
+ default: ["auditLog"]
+ type: list
+ test:
+ description:
+ - This forces a test syslog message to be sent to the stated syslog server.
+ - Only attempts transmission when I(state=present).
+ type: bool
+ default: no
+ log_path:
+ description:
+ - This argument specifies a local path for logging purposes.
+ type: str
+ required: no
+notes:
+ - Check mode is supported.
+ - This API is currently only supported with the Embedded Web Services API v2.12 (bundled with
+ SANtricity OS 11.40.2) and higher.
+"""
+
+EXAMPLES = """
+ - name: Add two syslog server configurations to NetApp E-Series storage array.
+ netapp_e_syslog:
+ state: present
+ address: "{{ item }}"
+ port: 514
+ protocol: tcp
+ component: "auditLog"
+ api_url: "10.1.1.1:8443"
+ api_username: "admin"
+ api_password: "myPass"
+ loop:
+ - "192.168.1.1"
+ - "192.168.1.100"
+"""
+
+RETURN = """
+msg:
+ description: Success message
+ returned: on success
+ type: str
+ sample: The settings have been updated.
+syslog:
+ description:
+ - True if syslog server configuration has been added to e-series storage array.
+ returned: on success
+ sample: True
+ type: bool
+"""
+
+import json
+import logging
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import request, eseries_host_argument_spec
+from ansible.module_utils._text import to_native
+
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+}
+
+
+class Syslog(object):
+ def __init__(self):
+ argument_spec = eseries_host_argument_spec()
+ argument_spec.update(dict(
+ state=dict(choices=["present", "absent"], required=False, default="present"),
+ address=dict(type="str", required=False),
+ port=dict(type="int", default=514, required=False),
+ protocol=dict(choices=["tcp", "tls", "udp"], default="udp", required=False),
+ components=dict(type="list", required=False, default=["auditLog"]),
+ test=dict(type="bool", default=False, require=False),
+ log_path=dict(type="str", required=False),
+ ))
+
+ required_if = [
+ ["state", "present", ["address", "port", "protocol", "components"]],
+ ]
+
+ mutually_exclusive = [
+ ["test", "absent"],
+ ]
+
+ self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if,
+ mutually_exclusive=mutually_exclusive)
+ args = self.module.params
+
+ self.syslog = args["state"] in ["present"]
+ self.address = args["address"]
+ self.port = args["port"]
+ self.protocol = args["protocol"]
+ self.components = args["components"]
+ self.test = args["test"]
+ self.ssid = args["ssid"]
+ self.url = args["api_url"]
+ self.creds = dict(url_password=args["api_password"],
+ validate_certs=args["validate_certs"],
+ url_username=args["api_username"], )
+
+ self.components.sort()
+
+ self.check_mode = self.module.check_mode
+
+ # logging setup
+ log_path = args["log_path"]
+ self._logger = logging.getLogger(self.__class__.__name__)
+ if log_path:
+ logging.basicConfig(
+ level=logging.DEBUG, filename=log_path, filemode='w',
+ format='%(relativeCreated)dms %(levelname)s %(module)s.%(funcName)s:%(lineno)d\n %(message)s')
+
+ if not self.url.endswith('/'):
+ self.url += '/'
+
+ def get_configuration(self):
+ """Retrieve existing syslog configuration."""
+ try:
+ (rc, result) = request(self.url + "storage-systems/{0}/syslog".format(self.ssid),
+ headers=HEADERS, **self.creds)
+ return result
+ except Exception as err:
+ self.module.fail_json(msg="Failed to retrieve syslog configuration! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ def test_configuration(self, body):
+ """Send test syslog message to the storage array.
+
+ Allows fix number of retries to occur before failure is issued to give the storage array time to create
+ new syslog server record.
+ """
+ try:
+ (rc, result) = request(self.url + "storage-systems/{0}/syslog/{1}/test".format(self.ssid, body["id"]),
+ method='POST', headers=HEADERS, **self.creds)
+ except Exception as err:
+ self.module.fail_json(
+ msg="We failed to send test message! Array Id [{0}]. Error [{1}].".format(self.ssid, to_native(err)))
+
+ def update_configuration(self):
+ """Post the syslog request to array."""
+ config_match = None
+ perfect_match = None
+ update = False
+ body = dict()
+
+ # search existing configuration for syslog server entry match
+ configs = self.get_configuration()
+ if self.address:
+ for config in configs:
+ if config["serverAddress"] == self.address:
+ config_match = config
+ if (config["port"] == self.port and config["protocol"] == self.protocol and
+ len(config["components"]) == len(self.components) and
+ all([component["type"] in self.components for component in config["components"]])):
+ perfect_match = config_match
+ break
+
+ # generate body for the http request
+ if self.syslog:
+ if not perfect_match:
+ update = True
+ if config_match:
+ body.update(dict(id=config_match["id"]))
+ components = [dict(type=component_type) for component_type in self.components]
+ body.update(dict(serverAddress=self.address, port=self.port,
+ protocol=self.protocol, components=components))
+ self._logger.info(body)
+ self.make_configuration_request(body)
+
+ # remove specific syslog server configuration
+ elif self.address:
+ update = True
+ body.update(dict(id=config_match["id"]))
+ self._logger.info(body)
+ self.make_configuration_request(body)
+
+ # if no address is specified, remove all syslog server configurations
+ elif configs:
+ update = True
+ for config in configs:
+ body.update(dict(id=config["id"]))
+ self._logger.info(body)
+ self.make_configuration_request(body)
+
+ return update
+
+ def make_configuration_request(self, body):
+ # make http request(s)
+ if not self.check_mode:
+ try:
+ if self.syslog:
+ if "id" in body:
+ (rc, result) = request(
+ self.url + "storage-systems/{0}/syslog/{1}".format(self.ssid, body["id"]),
+ method='POST', data=json.dumps(body), headers=HEADERS, **self.creds)
+ else:
+ (rc, result) = request(self.url + "storage-systems/{0}/syslog".format(self.ssid),
+ method='POST', data=json.dumps(body), headers=HEADERS, **self.creds)
+ body.update(result)
+
+ # send syslog test message
+ if self.test:
+ self.test_configuration(body)
+
+ elif "id" in body:
+ (rc, result) = request(self.url + "storage-systems/{0}/syslog/{1}".format(self.ssid, body["id"]),
+ method='DELETE', headers=HEADERS, **self.creds)
+
+ # This is going to catch cases like a connection failure
+ except Exception as err:
+ self.module.fail_json(msg="We failed to modify syslog configuration! Array Id [%s]. Error [%s]."
+ % (self.ssid, to_native(err)))
+
+ def update(self):
+ """Update configuration and respond to ansible."""
+ update = self.update_configuration()
+ self.module.exit_json(msg="The syslog settings have been updated.", changed=update)
+
+ def __call__(self, *args, **kwargs):
+ self.update()
+
+
+def main():
+ settings = Syslog()
+ settings()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_volume.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_volume.py
new file mode 100644
index 00000000..0bac2cca
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_volume.py
@@ -0,0 +1,868 @@
+#!/usr/bin/python
+
+# (c) 2016, 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
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = """
+---
+module: netapp_e_volume
+version_added: "2.2"
+short_description: NetApp E-Series manage storage volumes (standard and thin)
+description:
+ - Create or remove volumes (standard and thin) for NetApp E/EF-series storage arrays.
+author:
+ - Kevin Hulquest (@hulquest)
+ - Nathan Swartz (@ndswartz)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ state:
+ description:
+ - Whether the specified volume should exist
+ required: true
+ type: str
+ choices: ['present', 'absent']
+ name:
+ description:
+ - The name of the volume to manage.
+ type: str
+ required: true
+ storage_pool_name:
+ description:
+ - Required only when requested I(state=='present').
+ - Name of the storage pool wherein the volume should reside.
+ type: str
+ required: false
+ size_unit:
+ description:
+ - The unit used to interpret the size parameter
+ choices: ['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb']
+ type: str
+ default: 'gb'
+ size:
+ description:
+ - Required only when I(state=='present').
+ - Size of the volume in I(size_unit).
+ - Size of the virtual volume in the case of a thin volume in I(size_unit).
+ - Maximum virtual volume size of a thin provisioned volume is 256tb; however other OS-level restrictions may
+ exist.
+ type: float
+ required: true
+ segment_size_kb:
+ description:
+ - Segment size of the volume
+ - All values are in kibibytes.
+ - Some common choices include '8', '16', '32', '64', '128', '256', and '512' but options are system
+ dependent.
+ - Retrieve the definitive system list from M(netapp_eseries.santricity.netapp_e_facts) under segment_sizes.
+ - When the storage pool is a raidDiskPool then the segment size must be 128kb.
+ - Segment size migrations are not allowed in this module
+ type: int
+ default: '128'
+ thin_provision:
+ description:
+ - Whether the volume should be thin provisioned.
+ - Thin volumes can only be created when I(raid_level=="raidDiskPool").
+ - Generally, use of thin-provisioning is not recommended due to performance impacts.
+ type: bool
+ default: false
+ thin_volume_repo_size:
+ description:
+ - This value (in size_unit) sets the allocated space for the thin provisioned repository.
+ - Initial value must between or equal to 4gb and 256gb in increments of 4gb.
+ - During expansion operations the increase must be between or equal to 4gb and 256gb in increments of 4gb.
+ - This option has no effect during expansion if I(thin_volume_expansion_policy=="automatic").
+ - Generally speaking you should almost always use I(thin_volume_expansion_policy=="automatic).
+ type: int
+ required: false
+ thin_volume_max_repo_size:
+ description:
+ - This is the maximum amount the thin volume repository will be allowed to grow.
+ - Only has significance when I(thin_volume_expansion_policy=="automatic").
+ - When the percentage I(thin_volume_repo_size) of I(thin_volume_max_repo_size) exceeds
+ I(thin_volume_growth_alert_threshold) then a warning will be issued and the storage array will execute
+ the I(thin_volume_expansion_policy) policy.
+ - Expansion operations when I(thin_volume_expansion_policy=="automatic") will increase the maximum
+ repository size.
+ - The default will be the same as size (in size_unit)
+ type: float
+ thin_volume_expansion_policy:
+ description:
+ - This is the thin volume expansion policy.
+ - When I(thin_volume_expansion_policy=="automatic") and I(thin_volume_growth_alert_threshold) is exceed the
+ I(thin_volume_max_repo_size) will be automatically expanded.
+ - When I(thin_volume_expansion_policy=="manual") and I(thin_volume_growth_alert_threshold) is exceeded the
+ storage system will wait for manual intervention.
+ - The thin volume_expansion policy can not be modified on existing thin volumes in this module.
+ - Generally speaking you should almost always use I(thin_volume_expansion_policy=="automatic).
+ choices: ["automatic", "manual"]
+ default: "automatic"
+ type: str
+ version_added: 2.8
+ thin_volume_growth_alert_threshold:
+ description:
+ - This is the thin provision repository utilization threshold (in percent).
+ - When the percentage of used storage of the maximum repository size exceeds this value then a alert will
+ be issued and the I(thin_volume_expansion_policy) will be executed.
+ - Values must be between or equal to 10 and 99.
+ default: 95
+ type: int
+ version_added: 2.8
+ owning_controller:
+ description:
+ - Specifies which controller will be the primary owner of the volume
+ - Not specifying will allow the controller to choose ownership.
+ required: false
+ choices: ["A", "B"]
+ type: str
+ version_added: 2.9
+ ssd_cache_enabled:
+ description:
+ - Whether an existing SSD cache should be enabled on the volume (fails if no SSD cache defined)
+ - The default value is to ignore existing SSD cache setting.
+ type: bool
+ default: false
+ data_assurance_enabled:
+ description:
+ - Determines whether data assurance (DA) should be enabled for the volume
+ - Only available when creating a new volume and on a storage pool with drives supporting the DA capability.
+ type: bool
+ default: false
+ read_cache_enable:
+ description:
+ - Indicates whether read caching should be enabled for the volume.
+ type: bool
+ default: true
+ version_added: 2.8
+ read_ahead_enable:
+ description:
+ - Indicates whether or not automatic cache read-ahead is enabled.
+ - This option has no effect on thinly provisioned volumes since the architecture for thin volumes cannot
+ benefit from read ahead caching.
+ type: bool
+ default: true
+ version_added: 2.8
+ write_cache_enable:
+ description:
+ - Indicates whether write-back caching should be enabled for the volume.
+ type: bool
+ default: true
+ version_added: 2.8
+ cache_without_batteries:
+ description:
+ - Indicates whether caching should be used without battery backup.
+ - Warning, M(cache_without_batteries==true) and the storage system looses power and there is no battery backup, data will be lost!
+ type: bool
+ default: false
+ version_added: 2.9
+ workload_name:
+ description:
+ - Label for the workload defined by the metadata.
+ - When I(workload_name) and I(metadata) are specified then the defined workload will be added to the storage
+ array.
+ - When I(workload_name) exists on the storage array but the metadata is different then the workload
+ definition will be updated. (Changes will update all associated volumes!)
+ - Existing workloads can be retrieved using M(netapp_eseries.santricity.netapp_e_facts).
+ required: false
+ type: str
+ version_added: 2.8
+ metadata:
+ description:
+ - Dictionary containing meta data for the use, user, location, etc of the volume (dictionary is arbitrarily
+ defined for whatever the user deems useful)
+ - When I(workload_name) exists on the storage array but the metadata is different then the workload
+ definition will be updated. (Changes will update all associated volumes!)
+ - I(workload_name) must be specified when I(metadata) are defined.
+ type: dict
+ required: false
+ version_added: 2.8
+ wait_for_initialization:
+ description:
+ - Forces the module to wait for expansion operations to complete before continuing.
+ type: bool
+ default: false
+ version_added: 2.8
+ initialization_timeout:
+ description:
+ - Duration in seconds before the wait_for_initialization operation will terminate.
+ - M(wait_for_initialization==True) to have any effect on module's operations.
+ type: int
+ required: false
+ version_added: 2.9
+"""
+EXAMPLES = """
+- name: Create simple volume with workload tags (volume meta data)
+ netapp_e_volume:
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ validate_certs: "{{ netapp_api_validate_certs }}"
+ state: present
+ name: volume
+ storage_pool_name: storage_pool
+ size: 300
+ size_unit: gb
+ workload_name: volume_tag
+ metadata:
+ key1: value1
+ key2: value2
+- name: Create a thin volume
+ netapp_e_volume:
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ validate_certs: "{{ netapp_api_validate_certs }}"
+ state: present
+ name: volume1
+ storage_pool_name: storage_pool
+ size: 131072
+ size_unit: gb
+ thin_provision: true
+ thin_volume_repo_size: 32
+ thin_volume_max_repo_size: 1024
+- name: Expand thin volume's virtual size
+ netapp_e_volume:
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ validate_certs: "{{ netapp_api_validate_certs }}"
+ state: present
+ name: volume1
+ storage_pool_name: storage_pool
+ size: 262144
+ size_unit: gb
+ thin_provision: true
+ thin_volume_repo_size: 32
+ thin_volume_max_repo_size: 1024
+- name: Expand thin volume's maximum repository size
+ netapp_e_volume:
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ validate_certs: "{{ netapp_api_validate_certs }}"
+ state: present
+ name: volume1
+ storage_pool_name: storage_pool
+ size: 262144
+ size_unit: gb
+ thin_provision: true
+ thin_volume_repo_size: 32
+ thin_volume_max_repo_size: 2048
+- name: Delete volume
+ netapp_e_volume:
+ ssid: "{{ ssid }}"
+ api_url: "{{ netapp_api_url }}"
+ api_username: "{{ netapp_api_username }}"
+ api_password: "{{ netapp_api_password }}"
+ validate_certs: "{{ netapp_api_validate_certs }}"
+ state: absent
+ name: volume
+"""
+RETURN = """
+msg:
+ description: State of volume
+ type: str
+ returned: always
+ sample: "Standard volume [workload_vol_1] has been created."
+"""
+from time import sleep
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import NetAppESeriesModule
+from ansible.module_utils._text import to_native
+
+
+class NetAppESeriesVolume(NetAppESeriesModule):
+ VOLUME_CREATION_BLOCKING_TIMEOUT_SEC = 300
+
+ def __init__(self):
+ ansible_options = dict(
+ state=dict(required=True, choices=["present", "absent"]),
+ name=dict(required=True, type="str"),
+ storage_pool_name=dict(type="str"),
+ size_unit=dict(default="gb", choices=["bytes", "b", "kb", "mb", "gb", "tb", "pb", "eb", "zb", "yb"],
+ type="str"),
+ size=dict(type="float"),
+ segment_size_kb=dict(type="int", default=128),
+ owning_controller=dict(required=False, choices=['A', 'B']),
+ ssd_cache_enabled=dict(type="bool", default=False),
+ data_assurance_enabled=dict(type="bool", default=False),
+ thin_provision=dict(type="bool", default=False),
+ thin_volume_repo_size=dict(type="int"),
+ thin_volume_max_repo_size=dict(type="float"),
+ thin_volume_expansion_policy=dict(type="str", choices=["automatic", "manual"], default="automatic"),
+ thin_volume_growth_alert_threshold=dict(type="int", default=95),
+ read_cache_enable=dict(type="bool", default=True),
+ read_ahead_enable=dict(type="bool", default=True),
+ write_cache_enable=dict(type="bool", default=True),
+ cache_without_batteries=dict(type="bool", default=False),
+ workload_name=dict(type="str", required=False),
+ metadata=dict(type="dict", require=False),
+ wait_for_initialization=dict(type="bool", default=False),
+ initialization_timeout=dict(type="int", required=False))
+
+ required_if = [
+ ["state", "present", ["storage_pool_name", "size"]],
+ ["thin_provision", "true", ["thin_volume_repo_size"]]
+ ]
+
+ super(NetAppESeriesVolume, self).__init__(ansible_options=ansible_options,
+ web_services_version="02.00.0000.0000",
+ supports_check_mode=True,
+ required_if=required_if)
+
+ args = self.module.params
+ self.state = args["state"]
+ self.name = args["name"]
+ self.storage_pool_name = args["storage_pool_name"]
+ self.size_unit = args["size_unit"]
+ self.segment_size_kb = args["segment_size_kb"]
+ if args["size"]:
+ self.size_b = self.convert_to_aligned_bytes(args["size"])
+
+ self.owning_controller_id = None
+ if args["owning_controller"]:
+ self.owning_controller_id = "070000000000000000000001" if args["owning_controller"] == "A" else "070000000000000000000002"
+
+ self.read_cache_enable = args["read_cache_enable"]
+ self.read_ahead_enable = args["read_ahead_enable"]
+ self.write_cache_enable = args["write_cache_enable"]
+ self.ssd_cache_enabled = args["ssd_cache_enabled"]
+ self.cache_without_batteries = args["cache_without_batteries"]
+ self.data_assurance_enabled = args["data_assurance_enabled"]
+
+ self.thin_provision = args["thin_provision"]
+ self.thin_volume_expansion_policy = args["thin_volume_expansion_policy"]
+ self.thin_volume_growth_alert_threshold = int(args["thin_volume_growth_alert_threshold"])
+ self.thin_volume_repo_size_b = None
+ self.thin_volume_max_repo_size_b = None
+
+ if args["thin_volume_repo_size"]:
+ self.thin_volume_repo_size_b = self.convert_to_aligned_bytes(args["thin_volume_repo_size"])
+ if args["thin_volume_max_repo_size"]:
+ self.thin_volume_max_repo_size_b = self.convert_to_aligned_bytes(args["thin_volume_max_repo_size"])
+
+ self.workload_name = args["workload_name"]
+ self.metadata = args["metadata"]
+ self.wait_for_initialization = args["wait_for_initialization"]
+ self.initialization_timeout = args["initialization_timeout"]
+
+ # convert metadata to a list of dictionaries containing the keys "key" and "value" corresponding to
+ # each of the workload attributes dictionary entries
+ metadata = []
+ if self.metadata:
+ if not self.workload_name:
+ self.module.fail_json(msg="When metadata is specified then the name for the workload must be specified."
+ " Array [%s]." % self.ssid)
+ for key in self.metadata.keys():
+ metadata.append(dict(key=key, value=self.metadata[key]))
+ self.metadata = metadata
+
+ if self.thin_provision:
+ if not self.thin_volume_max_repo_size_b:
+ self.thin_volume_max_repo_size_b = self.size_b
+
+ if not self.thin_volume_expansion_policy:
+ self.thin_volume_expansion_policy = "automatic"
+
+ if self.size_b > 256 * 1024 ** 4:
+ self.module.fail_json(msg="Thin provisioned volumes must be less than or equal to 256tb is size."
+ " Attempted size [%sg]" % (self.size_b * 1024 ** 3))
+
+ if (self.thin_volume_repo_size_b and self.thin_volume_max_repo_size_b and
+ self.thin_volume_repo_size_b > self.thin_volume_max_repo_size_b):
+ self.module.fail_json(msg="The initial size of the thin volume must not be larger than the maximum"
+ " repository size. Array [%s]." % self.ssid)
+
+ if self.thin_volume_growth_alert_threshold < 10 or self.thin_volume_growth_alert_threshold > 99:
+ self.module.fail_json(msg="thin_volume_growth_alert_threshold must be between or equal to 10 and 99."
+ "thin_volume_growth_alert_threshold [%s]. Array [%s]."
+ % (self.thin_volume_growth_alert_threshold, self.ssid))
+
+ self.volume_detail = None
+ self.pool_detail = None
+ self.workload_id = None
+
+ def convert_to_aligned_bytes(self, size):
+ """Convert size to the truncated byte size that aligns on the segment size."""
+ size_bytes = int(size * self.SIZE_UNIT_MAP[self.size_unit])
+ segment_size_bytes = int(self.segment_size_kb * self.SIZE_UNIT_MAP["kb"])
+ segment_count = int(size_bytes / segment_size_bytes)
+ return segment_count * segment_size_bytes
+
+ def get_volume(self):
+ """Retrieve volume details from storage array."""
+ volumes = list()
+ thin_volumes = list()
+ try:
+ rc, volumes = self.request("storage-systems/%s/volumes" % self.ssid)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to obtain list of thick volumes. Array Id [%s]. Error[%s]."
+ % (self.ssid, to_native(err)))
+ try:
+ rc, thin_volumes = self.request("storage-systems/%s/thin-volumes" % self.ssid)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to obtain list of thin volumes. Array Id [%s]. Error[%s]."
+ % (self.ssid, to_native(err)))
+
+ volume_detail = [volume for volume in volumes + thin_volumes if volume["name"] == self.name]
+ return volume_detail[0] if volume_detail else dict()
+
+ def wait_for_volume_availability(self, retries=VOLUME_CREATION_BLOCKING_TIMEOUT_SEC / 5):
+ """Waits until volume becomes available.
+
+ :raises AnsibleFailJson when retries are exhausted.
+ """
+ if retries == 0:
+ self.module.fail_json(msg="Timed out waiting for the volume %s to become available. Array [%s]."
+ % (self.name, self.ssid))
+ if not self.get_volume():
+ sleep(5)
+ self.wait_for_volume_availability(retries=retries - 1)
+
+ def wait_for_volume_action(self, timeout=None):
+ """Waits until volume action is complete is complete.
+ :param: int timeout: Wait duration measured in seconds. Waits indefinitely when None.
+ """
+ action = "unknown"
+ percent_complete = None
+ while action != "complete":
+ sleep(5)
+
+ try:
+ rc, operations = self.request("storage-systems/%s/symbol/getLongLivedOpsProgress" % self.ssid)
+
+ # Search long lived operations for volume
+ action = "complete"
+ for operation in operations["longLivedOpsProgress"]:
+ if operation["volAction"] is not None:
+ for key in operation.keys():
+ if (operation[key] is not None and "volumeRef" in operation[key] and
+ (operation[key]["volumeRef"] == self.volume_detail["id"] or
+ ("storageVolumeRef" in self.volume_detail and operation[key]["volumeRef"] == self.volume_detail["storageVolumeRef"]))):
+ action = operation["volAction"]
+ percent_complete = operation["init"]["percentComplete"]
+ except Exception as err:
+ self.module.fail_json(msg="Failed to get volume expansion progress. Volume [%s]. Array Id [%s]."
+ " Error[%s]." % (self.name, self.ssid, to_native(err)))
+
+ if timeout is not None:
+ if timeout <= 0:
+ self.module.warn("Expansion action, %s, failed to complete during the allotted time. Time remaining"
+ " [%s]. Array Id [%s]." % (action, percent_complete, self.ssid))
+ self.module.fail_json(msg="Expansion action failed to complete. Time remaining [%s]. Array Id [%s]." % (percent_complete, self.ssid))
+ if timeout:
+ timeout -= 5
+
+ self.module.log("Expansion action, %s, is %s complete." % (action, percent_complete))
+ self.module.log("Expansion action is complete.")
+
+ def get_storage_pool(self):
+ """Retrieve storage pool details from the storage array."""
+ storage_pools = list()
+ try:
+ rc, storage_pools = self.request("storage-systems/%s/storage-pools" % self.ssid)
+ except Exception as err:
+ self.module.fail_json(msg="Failed to obtain list of storage pools. Array Id [%s]. Error[%s]."
+ % (self.ssid, to_native(err)))
+
+ pool_detail = [storage_pool for storage_pool in storage_pools if storage_pool["name"] == self.storage_pool_name]
+ return pool_detail[0] if pool_detail else dict()
+
+ def check_storage_pool_sufficiency(self):
+ """Perform a series of checks as to the sufficiency of the storage pool for the volume."""
+ if not self.pool_detail:
+ self.module.fail_json(msg='Requested storage pool (%s) not found' % self.storage_pool_name)
+
+ if not self.volume_detail:
+ if self.thin_provision and not self.pool_detail['diskPool']:
+ self.module.fail_json(msg='Thin provisioned volumes can only be created on raid disk pools.')
+
+ if (self.data_assurance_enabled and not
+ (self.pool_detail["protectionInformationCapabilities"]["protectionInformationCapable"] and
+ self.pool_detail["protectionInformationCapabilities"]["protectionType"] == "type2Protection")):
+ self.module.fail_json(msg="Data Assurance (DA) requires the storage pool to be DA-compatible."
+ " Array [%s]." % self.ssid)
+
+ if int(self.pool_detail["freeSpace"]) < self.size_b and not self.thin_provision:
+ self.module.fail_json(msg="Not enough storage pool free space available for the volume's needs."
+ " Array [%s]." % self.ssid)
+ else:
+ # Check for expansion
+ if (int(self.pool_detail["freeSpace"]) < int(self.volume_detail["totalSizeInBytes"]) - self.size_b and
+ not self.thin_provision):
+ self.module.fail_json(msg="Not enough storage pool free space available for the volume's needs."
+ " Array [%s]." % self.ssid)
+
+ def update_workload_tags(self, check_mode=False):
+ """Check the status of the workload tag and update storage array definitions if necessary.
+
+ When the workload attributes are not provided but an existing workload tag name is, then the attributes will be
+ used.
+
+ :return bool: Whether changes were required to be made."""
+ change_required = False
+ workload_tags = None
+ request_body = None
+ ansible_profile_id = None
+
+ if self.workload_name:
+ try:
+ rc, workload_tags = self.request("storage-systems/%s/workloads" % self.ssid)
+ except Exception as error:
+ self.module.fail_json(msg="Failed to retrieve storage array workload tags. Array [%s]" % self.ssid)
+
+ # Generate common indexed Ansible workload tag
+ current_tag_index_list = [int(pair["value"].replace("ansible_workload_", ""))
+ for tag in workload_tags for pair in tag["workloadAttributes"]
+ if pair["key"] == "profileId" and "ansible_workload_" in pair["value"] and
+ str(pair["value"]).replace("ansible_workload_", "").isdigit()]
+
+ tag_index = 1
+ if current_tag_index_list:
+ tag_index = max(current_tag_index_list) + 1
+
+ ansible_profile_id = "ansible_workload_%d" % tag_index
+ request_body = dict(name=self.workload_name,
+ profileId=ansible_profile_id,
+ workloadInstanceIndex=None,
+ isValid=True)
+
+ # evaluate and update storage array when needed
+ for tag in workload_tags:
+ if tag["name"] == self.workload_name:
+ self.workload_id = tag["id"]
+
+ if not self.metadata:
+ break
+
+ # Determine if core attributes (everything but profileId) is the same
+ metadata_set = set(tuple(sorted(attr.items())) for attr in self.metadata)
+ tag_set = set(tuple(sorted(attr.items()))
+ for attr in tag["workloadAttributes"] if attr["key"] != "profileId")
+ if metadata_set != tag_set:
+ self.module.log("Workload tag change is required!")
+ change_required = True
+
+ # only perform the required action when check_mode==False
+ if change_required and not check_mode:
+ self.metadata.append(dict(key="profileId", value=ansible_profile_id))
+ request_body.update(dict(isNewWorkloadInstance=False,
+ isWorkloadDataInitialized=True,
+ isWorkloadCardDataToBeReset=True,
+ workloadAttributes=self.metadata))
+ try:
+ rc, resp = self.request("storage-systems/%s/workloads/%s" % (self.ssid, tag["id"]),
+ data=request_body, method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to create new workload tag. Array [%s]. Error [%s]"
+ % (self.ssid, to_native(error)))
+ self.module.log("Workload tag [%s] required change." % self.workload_name)
+ break
+
+ # existing workload tag not found so create new workload tag
+ else:
+ change_required = True
+ self.module.log("Workload tag creation is required!")
+
+ if change_required and not check_mode:
+ if self.metadata:
+ self.metadata.append(dict(key="profileId", value=ansible_profile_id))
+ else:
+ self.metadata = [dict(key="profileId", value=ansible_profile_id)]
+
+ request_body.update(dict(isNewWorkloadInstance=True,
+ isWorkloadDataInitialized=False,
+ isWorkloadCardDataToBeReset=False,
+ workloadAttributes=self.metadata))
+ try:
+ rc, resp = self.request("storage-systems/%s/workloads" % self.ssid,
+ method="POST", data=request_body)
+ self.workload_id = resp["id"]
+ except Exception as error:
+ self.module.fail_json(msg="Failed to create new workload tag. Array [%s]. Error [%s]"
+ % (self.ssid, to_native(error)))
+ self.module.log("Workload tag [%s] was added." % self.workload_name)
+
+ return change_required
+
+ def get_volume_property_changes(self):
+ """Retrieve the volume update request body when change(s) are required.
+
+ :raise AnsibleFailJson when attempting to change segment size on existing volume.
+ :return dict: request body when change(s) to a volume's properties are required.
+ """
+ change = False
+ request_body = dict(flashCache=self.ssd_cache_enabled, metaTags=[],
+ cacheSettings=dict(readCacheEnable=self.read_cache_enable,
+ writeCacheEnable=self.write_cache_enable))
+
+ # check for invalid modifications
+ if self.segment_size_kb * 1024 != int(self.volume_detail["segmentSize"]):
+ self.module.fail_json(msg="Existing volume segment size is %s and cannot be modified."
+ % self.volume_detail["segmentSize"])
+
+ # common thick/thin volume properties
+ if (self.read_cache_enable != self.volume_detail["cacheSettings"]["readCacheEnable"] or
+ self.write_cache_enable != self.volume_detail["cacheSettings"]["writeCacheEnable"] or
+ self.ssd_cache_enabled != self.volume_detail["flashCached"]):
+ change = True
+
+ # controller ownership
+ if self.owning_controller_id and self.owning_controller_id != self.volume_detail["preferredManager"]:
+ change = True
+ request_body.update(dict(owningControllerId=self.owning_controller_id))
+
+ if self.workload_name:
+ request_body.update(dict(metaTags=[dict(key="workloadId", value=self.workload_id),
+ dict(key="volumeTypeId", value="volume")]))
+ if {"key": "workloadId", "value": self.workload_id} not in self.volume_detail["metadata"]:
+ change = True
+ elif self.volume_detail["metadata"]:
+ change = True
+
+ # thick/thin volume specific properties
+ if self.thin_provision:
+ if self.thin_volume_growth_alert_threshold != int(self.volume_detail["growthAlertThreshold"]):
+ change = True
+ request_body.update(dict(growthAlertThreshold=self.thin_volume_growth_alert_threshold))
+ if self.thin_volume_expansion_policy != self.volume_detail["expansionPolicy"]:
+ change = True
+ request_body.update(dict(expansionPolicy=self.thin_volume_expansion_policy))
+ else:
+ if self.read_ahead_enable != (int(self.volume_detail["cacheSettings"]["readAheadMultiplier"]) > 0):
+ change = True
+ request_body["cacheSettings"].update(dict(readAheadEnable=self.read_ahead_enable))
+ if self.cache_without_batteries != self.volume_detail["cacheSettings"]["cwob"]:
+ change = True
+ request_body["cacheSettings"].update(dict(cacheWithoutBatteries=self.cache_without_batteries))
+
+ return request_body if change else dict()
+
+ def get_expand_volume_changes(self):
+ """Expand the storage specifications for the existing thick/thin volume.
+
+ :raise AnsibleFailJson when a thick/thin volume expansion request fails.
+ :return dict: dictionary containing all the necessary values for volume expansion request
+ """
+ request_body = dict()
+
+ if self.size_b < int(self.volume_detail["capacity"]):
+ self.module.fail_json(msg="Reducing the size of volumes is not permitted. Volume [%s]. Array [%s]"
+ % (self.name, self.ssid))
+
+ if self.volume_detail["thinProvisioned"]:
+ if self.size_b > int(self.volume_detail["capacity"]):
+ request_body.update(dict(sizeUnit="bytes", newVirtualSize=self.size_b))
+ self.module.log("Thin volume virtual size have been expanded.")
+
+ if self.volume_detail["expansionPolicy"] == "automatic":
+ if self.thin_volume_max_repo_size_b > int(self.volume_detail["provisionedCapacityQuota"]):
+ request_body.update(dict(sizeUnit="bytes", newRepositorySize=self.thin_volume_max_repo_size_b))
+ self.module.log("Thin volume maximum repository size have been expanded (automatic policy).")
+
+ elif self.volume_detail["expansionPolicy"] == "manual":
+ if self.thin_volume_repo_size_b > int(self.volume_detail["currentProvisionedCapacity"]):
+ change = self.thin_volume_repo_size_b - int(self.volume_detail["currentProvisionedCapacity"])
+ if change < 4 * 1024 ** 3 or change > 256 * 1024 ** 3 or change % (4 * 1024 ** 3) != 0:
+ self.module.fail_json(msg="The thin volume repository increase must be between or equal to 4gb"
+ " and 256gb in increments of 4gb. Attempted size [%sg]."
+ % (self.thin_volume_repo_size_b * 1024 ** 3))
+
+ request_body.update(dict(sizeUnit="bytes", newRepositorySize=self.thin_volume_repo_size_b))
+ self.module.log("Thin volume maximum repository size have been expanded (manual policy).")
+
+ elif self.size_b > int(self.volume_detail["capacity"]):
+ request_body.update(dict(sizeUnit="bytes", expansionSize=self.size_b))
+ self.module.log("Volume storage capacities have been expanded.")
+
+ return request_body
+
+ def create_volume(self):
+ """Create thick/thin volume according to the specified criteria."""
+ body = dict(name=self.name, poolId=self.pool_detail["id"], sizeUnit="bytes",
+ dataAssuranceEnabled=self.data_assurance_enabled)
+
+ if self.thin_provision:
+ body.update(dict(virtualSize=self.size_b,
+ repositorySize=self.thin_volume_repo_size_b,
+ maximumRepositorySize=self.thin_volume_max_repo_size_b,
+ expansionPolicy=self.thin_volume_expansion_policy,
+ growthAlertThreshold=self.thin_volume_growth_alert_threshold))
+ try:
+ rc, volume = self.request("storage-systems/%s/thin-volumes" % self.ssid, data=body, method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to create thin volume. Volume [%s]. Array Id [%s]. Error[%s]."
+ % (self.name, self.ssid, to_native(error)))
+
+ self.module.log("New thin volume created [%s]." % self.name)
+
+ else:
+ body.update(dict(size=self.size_b, segSize=self.segment_size_kb))
+ try:
+ rc, volume = self.request("storage-systems/%s/volumes" % self.ssid, data=body, method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to create volume. Volume [%s]. Array Id [%s]. Error[%s]."
+ % (self.name, self.ssid, to_native(error)))
+
+ self.module.log("New volume created [%s]." % self.name)
+
+ def update_volume_properties(self):
+ """Update existing thin-volume or volume properties.
+
+ :raise AnsibleFailJson when either thick/thin volume update request fails.
+ :return bool: whether update was applied
+ """
+ self.wait_for_volume_availability()
+ self.volume_detail = self.get_volume()
+
+ request_body = self.get_volume_property_changes()
+
+ if request_body:
+ if self.thin_provision:
+ try:
+ rc, resp = self.request("storage-systems/%s/thin-volumes/%s"
+ % (self.ssid, self.volume_detail["id"]), data=request_body, method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to update thin volume properties. Volume [%s]. Array Id [%s]."
+ " Error[%s]." % (self.name, self.ssid, to_native(error)))
+ else:
+ try:
+ rc, resp = self.request("storage-systems/%s/volumes/%s" % (self.ssid, self.volume_detail["id"]),
+ data=request_body, method="POST")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to update volume properties. Volume [%s]. Array Id [%s]."
+ " Error[%s]." % (self.name, self.ssid, to_native(error)))
+ return True
+ return False
+
+ def expand_volume(self):
+ """Expand the storage specifications for the existing thick/thin volume.
+
+ :raise AnsibleFailJson when a thick/thin volume expansion request fails.
+ """
+ request_body = self.get_expand_volume_changes()
+ if request_body:
+ if self.volume_detail["thinProvisioned"]:
+ try:
+ rc, resp = self.request("storage-systems/%s/thin-volumes/%s/expand"
+ % (self.ssid, self.volume_detail["id"]), data=request_body, method="POST")
+ except Exception as err:
+ self.module.fail_json(msg="Failed to expand thin volume. Volume [%s]. Array Id [%s]. Error[%s]."
+ % (self.name, self.ssid, to_native(err)))
+ self.module.log("Thin volume specifications have been expanded.")
+
+ else:
+ try:
+ rc, resp = self.request(
+ "storage-systems/%s/volumes/%s/expand" % (self.ssid, self.volume_detail['id']),
+ data=request_body, method="POST")
+ except Exception as err:
+ self.module.fail_json(msg="Failed to expand volume. Volume [%s]. Array Id [%s]. Error[%s]."
+ % (self.name, self.ssid, to_native(err)))
+
+ self.module.log("Volume storage capacities have been expanded.")
+
+ def delete_volume(self):
+ """Delete existing thin/thick volume."""
+ if self.thin_provision:
+ try:
+ rc, resp = self.request("storage-systems/%s/thin-volumes/%s" % (self.ssid, self.volume_detail["id"]),
+ method="DELETE")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to delete thin volume. Volume [%s]. Array Id [%s]. Error[%s]."
+ % (self.name, self.ssid, to_native(error)))
+ self.module.log("Thin volume deleted [%s]." % self.name)
+ else:
+ try:
+ rc, resp = self.request("storage-systems/%s/volumes/%s" % (self.ssid, self.volume_detail["id"]),
+ method="DELETE")
+ except Exception as error:
+ self.module.fail_json(msg="Failed to delete volume. Volume [%s]. Array Id [%s]. Error[%s]."
+ % (self.name, self.ssid, to_native(error)))
+ self.module.log("Volume deleted [%s]." % self.name)
+
+ def apply(self):
+ """Determine and apply any changes necessary to satisfy the specified criteria.
+
+ :raise AnsibleExitJson when completes successfully"""
+ change = False
+ msg = None
+
+ self.volume_detail = self.get_volume()
+ self.pool_detail = self.get_storage_pool()
+
+ # Determine whether changes need to be applied to existing workload tags
+ if self.state == 'present' and self.update_workload_tags(check_mode=True):
+ change = True
+
+ # Determine if any changes need to be applied
+ if self.volume_detail:
+ if self.state == 'absent':
+ change = True
+
+ elif self.state == 'present':
+ if self.get_expand_volume_changes() or self.get_volume_property_changes():
+ change = True
+
+ elif self.state == 'present':
+ if self.thin_provision and (self.thin_volume_repo_size_b < 4 * 1024 ** 3 or
+ self.thin_volume_repo_size_b > 256 * 1024 ** 3 or
+ self.thin_volume_repo_size_b % (4 * 1024 ** 3) != 0):
+ self.module.fail_json(msg="The initial thin volume repository size must be between 4gb and 256gb in"
+ " increments of 4gb. Attempted size [%sg]."
+ % (self.thin_volume_repo_size_b * 1024 ** 3))
+ change = True
+
+ self.module.log("Update required: [%s]." % change)
+
+ # Apply any necessary changes
+ if change and not self.module.check_mode:
+ if self.state == 'present':
+ if self.update_workload_tags():
+ msg = "Workload tag change occurred."
+
+ if not self.volume_detail:
+ self.check_storage_pool_sufficiency()
+ self.create_volume()
+ self.update_volume_properties()
+ msg = msg[:-1] + " and volume [%s] was created." if msg else "Volume [%s] has been created."
+ else:
+ if self.update_volume_properties():
+ msg = "Volume [%s] properties were updated."
+
+ if self.get_expand_volume_changes():
+ self.expand_volume()
+ msg = msg[:-1] + " and was expanded." if msg else "Volume [%s] was expanded."
+
+ if self.wait_for_initialization:
+ self.module.log("Waiting for volume operation to complete.")
+ self.wait_for_volume_action(timeout=self.initialization_timeout)
+
+ elif self.state == 'absent':
+ self.delete_volume()
+ msg = "Volume [%s] has been deleted."
+
+ else:
+ msg = "Volume [%s] does not exist." if self.state == 'absent' else "Volume [%s] exists."
+
+ self.module.exit_json(msg=(msg % self.name if msg and "%s" in msg else msg), changed=change)
+
+
+def main():
+ volume = NetAppESeriesVolume()
+ volume.apply()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_volume_copy.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_volume_copy.py
new file mode 100644
index 00000000..a6748a54
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/plugins/modules/netapp_e_volume_copy.py
@@ -0,0 +1,431 @@
+#!/usr/bin/python
+
+# (c) 2016, 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
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['deprecated'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = """
+---
+module: netapp_e_volume_copy
+short_description: NetApp E-Series create volume copy pairs
+description:
+ - Create and delete snapshots images on volume groups for NetApp E-series storage arrays.
+version_added: '2.2'
+author: Kevin Hulquest (@hulquest)
+extends_documentation_fragment:
+ - netapp_eseries.santricity.santricity.netapp.eseries
+options:
+ ssid:
+ description:
+ - Storage system identifier
+ type: str
+ default: '1'
+ api_username:
+ required: true
+ description:
+ - The username to authenticate with the SANtricity WebServices Proxy or embedded REST API.
+ type: str
+ api_password:
+ required: true
+ description:
+ - The password to authenticate with the SANtricity WebServices Proxy or embedded REST API.
+ type: str
+ api_url:
+ required: true
+ description:
+ - The url to the SANtricity WebServices Proxy or embedded REST API, for example C(https://prod-1.wahoo.acme.com/devmgr/v2).
+ type: str
+ validate_certs:
+ required: false
+ default: true
+ type: bool
+ description:
+ - Should https certificates be validated?
+ source_volume_id:
+ description:
+ - The id of the volume copy source.
+ - If used, must be paired with destination_volume_id
+ - Mutually exclusive with volume_copy_pair_id, and search_volume_id
+ type: str
+ destination_volume_id:
+ description:
+ - The id of the volume copy destination.
+ - If used, must be paired with source_volume_id
+ - Mutually exclusive with volume_copy_pair_id, and search_volume_id
+ type: str
+ volume_copy_pair_id:
+ description:
+ - The id of a given volume copy pair
+ - Mutually exclusive with destination_volume_id, source_volume_id, and search_volume_id
+ - Can use to delete or check presence of volume pairs
+ - Must specify this or (destination_volume_id and source_volume_id)
+ type: str
+ state:
+ description:
+ - Whether the specified volume copy pair should exist or not.
+ required: True
+ choices: ['present', 'absent']
+ type: str
+ create_copy_pair_if_does_not_exist:
+ description:
+ - Defines if a copy pair will be created if it does not exist.
+ - If set to True destination_volume_id and source_volume_id are required.
+ type: bool
+ default: True
+ start_stop_copy:
+ description:
+ - starts a re-copy or stops a copy in progress
+ - "Note: If you stop the initial file copy before it it done the copy pair will be destroyed"
+ - Requires volume_copy_pair_id
+ type: str
+ choices: ['start', 'stop']
+ search_volume_id:
+ description:
+ - Searches for all valid potential target and source volumes that could be used in a copy_pair
+ - Mutually exclusive with volume_copy_pair_id, destination_volume_id and source_volume_id
+ type: str
+ copy_priority:
+ description:
+ - Copy priority level
+ required: False
+ default: 0
+ type: int
+ onlineCopy:
+ description:
+ - Whether copy should be online
+ required: False
+ default: False
+ type: bool
+ targetWriteProtected:
+ description:
+ - Whether target should be write protected
+ required: False
+ default: True
+ type: bool
+"""
+EXAMPLES = """
+---
+msg:
+ description: Success message
+ returned: success
+ type: str
+ sample: Json facts for the volume copy that was created.
+"""
+RETURN = """
+msg:
+ description: Success message
+ returned: success
+ type: str
+ sample: Created Volume Copy Pair with ID
+"""
+
+import json
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils._text import to_native
+from ansible_collections.netapp_eseries.santricity.plugins.module_utils.netapp import request
+
+HEADERS = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+}
+
+
+def find_volume_copy_pair_id_from_source_volume_id_and_destination_volume_id(params):
+ get_status = 'storage-systems/%s/volume-copy-jobs' % params['ssid']
+ url = params['api_url'] + get_status
+
+ (rc, resp) = request(url, method='GET', url_username=params['api_username'],
+ url_password=params['api_password'], headers=HEADERS,
+ validate_certs=params['validate_certs'])
+
+ volume_copy_pair_id = None
+ for potential_copy_pair in resp:
+ if potential_copy_pair['sourceVolume'] == params['source_volume_id']:
+ if potential_copy_pair['sourceVolume'] == params['source_volume_id']:
+ volume_copy_pair_id = potential_copy_pair['id']
+
+ return volume_copy_pair_id
+
+
+def create_copy_pair(params):
+ get_status = 'storage-systems/%s/volume-copy-jobs' % params['ssid']
+ url = params['api_url'] + get_status
+
+ rData = {
+ "sourceId": params['source_volume_id'],
+ "targetId": params['destination_volume_id']
+ }
+
+ (rc, resp) = request(url, data=json.dumps(rData), ignore_errors=True, method='POST',
+ url_username=params['api_username'], url_password=params['api_password'], headers=HEADERS,
+ validate_certs=params['validate_certs'])
+ if rc != 200:
+ return False, (rc, resp)
+ else:
+ return True, (rc, resp)
+
+
+def delete_copy_pair_by_copy_pair_id(params):
+ get_status = 'storage-systems/%s/volume-copy-jobs/%s?retainRepositories=false' % (
+ params['ssid'], params['volume_copy_pair_id'])
+ url = params['api_url'] + get_status
+
+ (rc, resp) = request(url, ignore_errors=True, method='DELETE',
+ url_username=params['api_username'], url_password=params['api_password'], headers=HEADERS,
+ validate_certs=params['validate_certs'])
+ if rc != 204:
+ return False, (rc, resp)
+ else:
+ return True, (rc, resp)
+
+
+def find_volume_copy_pair_id_by_volume_copy_pair_id(params):
+ get_status = 'storage-systems/%s/volume-copy-jobs/%s?retainRepositories=false' % (
+ params['ssid'], params['volume_copy_pair_id'])
+ url = params['api_url'] + get_status
+
+ (rc, resp) = request(url, ignore_errors=True, method='DELETE',
+ url_username=params['api_username'], url_password=params['api_password'], headers=HEADERS,
+ validate_certs=params['validate_certs'])
+ if rc != 200:
+ return False, (rc, resp)
+ else:
+ return True, (rc, resp)
+
+
+def start_stop_copy(params):
+ get_status = 'storage-systems/%s/volume-copy-jobs-control/%s?control=%s' % (
+ params['ssid'], params['volume_copy_pair_id'], params['start_stop_copy'])
+ url = params['api_url'] + get_status
+
+ (response_code, response_data) = request(url, ignore_errors=True, method='POST',
+ url_username=params['api_username'], url_password=params['api_password'],
+ headers=HEADERS,
+ validate_certs=params['validate_certs'])
+
+ if response_code == 200:
+ return True, response_data[0]['percentComplete']
+ else:
+ return False, response_data
+
+
+def check_copy_status(params):
+ get_status = 'storage-systems/%s/volume-copy-jobs-control/%s' % (
+ params['ssid'], params['volume_copy_pair_id'])
+ url = params['api_url'] + get_status
+
+ (response_code, response_data) = request(url, ignore_errors=True, method='GET',
+ url_username=params['api_username'], url_password=params['api_password'],
+ headers=HEADERS,
+ validate_certs=params['validate_certs'])
+
+ if response_code == 200:
+ if response_data['percentComplete'] != -1:
+
+ return True, response_data['percentComplete']
+ else:
+ return False, response_data['percentComplete']
+ else:
+ return False, response_data
+
+
+def find_valid_copy_pair_targets_and_sources(params):
+ get_status = 'storage-systems/%s/volumes' % params['ssid']
+ url = params['api_url'] + get_status
+
+ (response_code, response_data) = request(url, ignore_errors=True, method='GET',
+ url_username=params['api_username'], url_password=params['api_password'],
+ headers=HEADERS,
+ validate_certs=params['validate_certs'])
+
+ if response_code == 200:
+ source_capacity = None
+ candidates = []
+ for volume in response_data:
+ if volume['id'] == params['search_volume_id']:
+ source_capacity = volume['capacity']
+ else:
+ candidates.append(volume)
+
+ potential_sources = []
+ potential_targets = []
+
+ for volume in candidates:
+ if volume['capacity'] > source_capacity:
+ if volume['volumeCopyTarget'] is False:
+ if volume['volumeCopySource'] is False:
+ potential_targets.append(volume['id'])
+ else:
+ if volume['volumeCopyTarget'] is False:
+ if volume['volumeCopySource'] is False:
+ potential_sources.append(volume['id'])
+
+ return potential_targets, potential_sources
+
+ else:
+ raise Exception("Response [%s]" % response_code)
+
+
+def main():
+ module = AnsibleModule(argument_spec=dict(
+ source_volume_id=dict(type='str'),
+ destination_volume_id=dict(type='str'),
+ copy_priority=dict(required=False, default=0, type='int'),
+ ssid=dict(type='str', default='1'),
+ api_url=dict(required=True),
+ api_username=dict(required=False),
+ api_password=dict(required=False, no_log=True),
+ validate_certs=dict(required=False, default=True, type='bool'),
+ targetWriteProtected=dict(required=False, default=True, type='bool'),
+ onlineCopy=dict(required=False, default=False, type='bool'),
+ volume_copy_pair_id=dict(type='str'),
+ state=dict(required=True, choices=['present', 'absent'], type='str'),
+ create_copy_pair_if_does_not_exist=dict(required=False, default=True, type='bool'),
+ start_stop_copy=dict(required=False, choices=['start', 'stop'], type='str'),
+ search_volume_id=dict(type='str'),
+ ),
+ mutually_exclusive=[['volume_copy_pair_id', 'destination_volume_id'],
+ ['volume_copy_pair_id', 'source_volume_id'],
+ ['volume_copy_pair_id', 'search_volume_id'],
+ ['search_volume_id', 'destination_volume_id'],
+ ['search_volume_id', 'source_volume_id'],
+ ],
+ required_together=[['source_volume_id', 'destination_volume_id'],
+ ],
+ required_if=[["create_copy_pair_if_does_not_exist", True, ['source_volume_id', 'destination_volume_id'], ],
+ ["start_stop_copy", 'stop', ['volume_copy_pair_id'], ],
+ ["start_stop_copy", 'start', ['volume_copy_pair_id'], ],
+ ]
+
+ )
+ params = module.params
+
+ if not params['api_url'].endswith('/'):
+ params['api_url'] += '/'
+
+ # Check if we want to search
+ if params['search_volume_id'] is not None:
+ try:
+ potential_targets, potential_sources = find_valid_copy_pair_targets_and_sources(params)
+ except Exception as e:
+ module.fail_json(msg="Failed to find valid copy pair candidates. Error [%s]" % to_native(e))
+
+ module.exit_json(changed=False,
+ msg=' Valid source devices found: %s Valid target devices found: %s' % (len(potential_sources), len(potential_targets)),
+ search_volume_id=params['search_volume_id'],
+ valid_targets=potential_targets,
+ valid_sources=potential_sources)
+
+ # Check if we want to start or stop a copy operation
+ if params['start_stop_copy'] == 'start' or params['start_stop_copy'] == 'stop':
+
+ # Get the current status info
+ currenty_running, status_info = check_copy_status(params)
+
+ # If we want to start
+ if params['start_stop_copy'] == 'start':
+
+ # If we have already started
+ if currenty_running is True:
+ module.exit_json(changed=False, msg='Volume Copy Pair copy has started.',
+ volume_copy_pair_id=params['volume_copy_pair_id'], percent_done=status_info)
+ # If we need to start
+ else:
+
+ start_status, info = start_stop_copy(params)
+
+ if start_status is True:
+ module.exit_json(changed=True, msg='Volume Copy Pair copy has started.',
+ volume_copy_pair_id=params['volume_copy_pair_id'], percent_done=info)
+ else:
+ module.fail_json(msg="Could not start volume copy pair Error: %s" % info)
+
+ # If we want to stop
+ else:
+ # If it has already stopped
+ if currenty_running is False:
+ module.exit_json(changed=False, msg='Volume Copy Pair copy is stopped.',
+ volume_copy_pair_id=params['volume_copy_pair_id'])
+
+ # If we need to stop it
+ else:
+ start_status, info = start_stop_copy(params)
+
+ if start_status is True:
+ module.exit_json(changed=True, msg='Volume Copy Pair copy has been stopped.',
+ volume_copy_pair_id=params['volume_copy_pair_id'])
+ else:
+ module.fail_json(msg="Could not stop volume copy pair Error: %s" % info)
+
+ # If we want the copy pair to exist we do this stuff
+ if params['state'] == 'present':
+
+ # We need to check if it exists first
+ if params['volume_copy_pair_id'] is None:
+ params['volume_copy_pair_id'] = find_volume_copy_pair_id_from_source_volume_id_and_destination_volume_id(
+ params)
+
+ # If no volume copy pair is found we need need to make it.
+ if params['volume_copy_pair_id'] is None:
+
+ # In order to create we can not do so with just a volume_copy_pair_id
+
+ copy_began_status, (rc, resp) = create_copy_pair(params)
+
+ if copy_began_status is True:
+ module.exit_json(changed=True, msg='Created Volume Copy Pair with ID: %s' % resp['id'])
+ else:
+ module.fail_json(msg="Could not create volume copy pair Code: %s Error: %s" % (rc, resp))
+
+ # If it does exist we do nothing
+ else:
+ # We verify that it exists
+ exist_status, (exist_status_code, exist_status_data) = find_volume_copy_pair_id_by_volume_copy_pair_id(
+ params)
+
+ if exist_status:
+ module.exit_json(changed=False,
+ msg=' Volume Copy Pair with ID: %s exists' % params['volume_copy_pair_id'])
+ else:
+ if exist_status_code == 404:
+ module.fail_json(
+ msg=' Volume Copy Pair with ID: %s does not exist. Can not create without source_volume_id and destination_volume_id' %
+ params['volume_copy_pair_id'])
+ else:
+ module.fail_json(msg="Could not find volume copy pair Code: %s Error: %s" % (
+ exist_status_code, exist_status_data))
+
+ module.fail_json(msg="Done")
+
+ # If we want it to not exist we do this
+ else:
+
+ if params['volume_copy_pair_id'] is None:
+ params['volume_copy_pair_id'] = find_volume_copy_pair_id_from_source_volume_id_and_destination_volume_id(
+ params)
+
+ # We delete it by the volume_copy_pair_id
+ delete_status, (delete_status_code, delete_status_data) = delete_copy_pair_by_copy_pair_id(params)
+
+ if delete_status is True:
+ module.exit_json(changed=True,
+ msg=' Volume Copy Pair with ID: %s was deleted' % params['volume_copy_pair_id'])
+ else:
+ if delete_status_code == 404:
+ module.exit_json(changed=False,
+ msg=' Volume Copy Pair with ID: %s does not exist' % params['volume_copy_pair_id'])
+ else:
+ module.fail_json(msg="Could not delete volume copy pair Code: %s Error: %s" % (
+ delete_status_code, delete_status_data))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/.travis.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/.travis.yml
new file mode 100644
index 00000000..36bbf620
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/.travis.yml
@@ -0,0 +1,29 @@
+---
+language: python
+python: "2.7"
+
+# Use the new container infrastructure
+sudo: false
+
+# Install ansible
+addons:
+ apt:
+ packages:
+ - python-pip
+
+install:
+ # Install ansible
+ - pip install ansible
+
+ # Check ansible version
+ - ansible --version
+
+ # Create ansible.cfg with correct roles_path
+ - printf '[defaults]\nroles_path=../' >ansible.cfg
+
+script:
+ # Basic role syntax check
+ - ansible-playbook tests/test.yml -i tests/inventory --syntax-check
+
+notifications:
+ webhooks: https://galaxy.ansible.com/api/v1/notifications/ \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/README.md b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/README.md
new file mode 100644
index 00000000..84b0efc6
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/README.md
@@ -0,0 +1,138 @@
+nar_santricity_common
+=====================
+ Discover NetApp E-Series storage systems and configures SANtricity Web Services Proxy.
+
+ The following variables with be added to the runtime host inventory.
+ current_eseries_api_url: # Web Services REST API URL
+ current_eseries_api_username: # Web Services REST API username
+ current_eseries_api_password: # Web Services REST API password
+ current_eseries_ssid: # Arbitrary string for the proxy to represent the storage system.
+ current_eseries_validate_certs: # Indicates whether SSL certificates should be verified.
+ current_eseries_api_is_proxy: # Indicates whether Web Services REST API is running on a proxy.
+
+
+Requirements
+------------
+ - Ansible 2.9 or later
+ - NetApp E-Series E2800 platform or newer or NetApp E-Series SANtricity Web Services Proxy configured for older E-Series storage systems.
+
+
+Example Playbook
+----------------
+ - hosts: eseries_storage_systems
+ gather_facts: false
+ collection:
+ - netapp_eseries.santricity
+ tasks:
+ - name: Configure SANtricity Web Services and discover storage systems
+ import_role:
+ name: nar_santricity_common
+
+
+Example Inventory Host file using discovery with proxy
+------------------------------------------------------
+ eseries_proxy_api_url: https://192.168.1.100:8443/devmgr/v2/
+ eseries_proxy_api_password: admin_password
+ eseries_subnet: 192.168.1.0/24 # This should only be defined at the group level once when utilizing Web Services Proxy and should be broad enough to include all systems being added to proxy instance.
+ eseries_system_serial: "012345678901" # Be sure to quote if the serial is all numbers and begins with zero.
+ eseries_system_password: admin_password
+ eseries_validate_certs: false
+ (...)
+
+
+Example Inventory Host file using discovery without proxy
+---------------------------------------------------------
+**Note that while eseries_management_interfaces or eseries_system_api_url are optional, including at least one of them will prevent the discovery mechanism from being used when the system can be reached from their information.
+ eseries_subnet: 192.168.1.0/24
+ eseries_system_serial: "012345678901" # Be sure to quote if the serial is all numbers and begins with zero.
+ eseries_system_password: admin_password
+ eseries_validate_certs: false
+ (...)
+
+
+Example Inventory Host file without using discovery (Embedded Web Services)
+---------------------------------------------------------------------------
+ eseries_system_api_url: https://192.168.1.200:8443/devmgr/v2/
+ eseries_system_password: admin_password
+ eseries_validate_certs: false
+ (...)
+
+
+Example Inventory Host file without using discovery (Proxy Web Services - system must have already been added to the proxy)
+------------------------------------------------------------------------
+ eseries_proxy_ssid: storage_ssid
+ eseries_proxy_api_url: https://192.168.2.200:8443/devmgr/v2/
+ eseries_proxy_api_password: admin_password
+ (...)
+
+
+Notes
+-----
+Use SANtricity Web Services Proxy to avoid the need to discover the storage systems each time nar_santricity_common is executed. The first time nar_santricity_common is executed will add the storage systems the proxy so that they can be recalled without the need to search the subnet each subsequent execution.
+The na_santricity_proxy_systems module is used to add storage systems to the proxy but required a complete list of desired systems since it will ensure that only the systems provided will remain on the proxy. As a result any system that is not included will be removed from the proxy.
+
+Role Variables
+--------------
+ eseries_subnet: # Network subnet to search for the storage system specified in CIDR form. Example: 192.168.1.0/24
+ # Note: eseries_subnet should only be defined once at the group level when utilizing the Web Services Proxy.
+ eseries_template_api_url: # Template for the web services api url. Default: https://0.0.0.0:8443/devmgr/v2/
+ eseries_prefer_embedded: false # Overrides the default behavior of using Web Services Proxy when eseries_proxy_api_url is defined. This will only effect storage systems that have Embedded Web Services.
+ eseries_validate_certs: true # Indicates Whether SSL certificates should be verified. Used for both embedded and proxy. Choices: true, false
+
+ # Storage system specific variables
+ eseries_proxy_ssid: # Arbitrary string for the proxy to represent the storage system. eseries_system_serial will be used when not defined.
+ eseries_system_serial: # Storage system serial number. Be sure to quote if the serial is all numbers and begins with zero. (This is located on a label at the top-left towards the front on the device)
+ eseries_system_addresses: # Storage system management IP addresses. Only required when eseries_system_serial or eseries_system_api_url are not defined. When not specified, addresses will be populated with eseries_management_interfaces controller addresses.
+ eseries_system_api_url: # Url for the storage system's for embedded web services rest api. Example: https://192.168.10.100/devmgr/v2
+ eseries_system_username: admin # Username for the storage system's for embedded web services rest api
+ eseries_system_password: # Password for the storage system's for embedded web services rest api and when the admin password has not been set eseries_system_password will be used to set it.
+ eseries_system_tags: # Meta tags to associate with storage system when added to the proxy.
+
+ # Storage system management interface information
+ Note: eseries_management_interfaces will be used when eseries_system_serial, eseries_system_api_url, or eseries_system_addresses are not defined.
+ eseries_management_interfaces: # Subset of the eseries_management_interface variable found in the nar_santricity_management role
+ controller_a:
+ - address: # Controller A port 1's IP address
+ - address: # Controller A port 2's IP address
+ controller_b:
+ - address: # Controller B port 1's IP address
+ - address: # Controller B port 2's IP address
+
+ # Web Services Proxy specific variable
+ Note: eseries_proxy_* variables are required to discover storage systems prior to SANtricity OS version 11.60.2.
+ eseries_proxy_api_url: # Url for the storage system's for proxy web services rest api. Example: https://192.168.10.100/devmgr/v2
+ eseries_proxy_api_username: # Username for the storage system's for proxy web services rest api.
+ eseries_proxy_api_password: # Password for the storage system's for proxy web services rest api and when the admin password has not been set eseries_proxy_api_password will be used to set it.
+ eseries_proxy_monitor_password: # Proxy password for the monitor username
+ eseries_proxy_security_password: # Proxy password for the security username
+ eseries_proxy_storage_password: # Proxy password for the monitor username
+ eseries_proxy_support_password: # Proxy password for the support username
+ eseries_proxy_accept_certifications: # Force automatic acceptance of all storage system's certificate
+ eseries_proxy_default_system_tags: # Default meta tags to associate with all storage systems
+ eseries_proxy_default_password: # Default password to associate with all storage systems. This is overridden by eseries_system_password.
+
+ # LDAP configuration defaults
+ eseries_proxy_ldap_state: # Whether LDAP should be configured for the proxy`
+ eseries_proxy_ldap_identifier: # The user attributes that should be considered for the group to role mapping
+ eseries_proxy_ldap_user_attribute: # Attribute used to the provided username during authentication.
+ eseries_proxy_ldap_bind_username: # User account that will be used for querying the LDAP server.
+ eseries_proxy_ldap_bind_password: # Password for the bind user account
+ eseries_proxy_ldap_server: # LDAP server URL.
+ eseries_proxy_ldap_search_base: # Search base used for find user's group membership
+ eseries_proxy_ldap_role_mappings: # Dictionary of user groups, each containing the list of access roles.
+ # Role choices: storage.admin - allows users full read/writes access to storage objects and operations.
+ # storage.monitor - allows users read-only access to storage objects and operations.
+ # storage.admin - allows users access to hardware, diagnostic information, major event logs,
+ # and other critical support-related functionality, but not the sorage configuration.
+ # security.admin - allows users access to authentication/authorization configuration, as
+ # well as the audit log configuration, adn certification management.
+
+
+License
+-------
+ BSD-3-Clause
+
+
+Author Information
+------------------
+ Nathan Swartz (@ndswartz)
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/defaults/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/defaults/main.yml
new file mode 100644
index 00000000..42dfdc84
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/defaults/main.yml
@@ -0,0 +1,44 @@
+---
+#eseries_subnet: # Network subnet to search for the storage system specified in CIDR form. Example: 192.168.1.0/24
+eseries_template_api_url: https://0.0.0.0:8443/devmgr/v2/ # Template for the web services api url. Default: https://0.0.0.0:8443/devmgr/v2/
+#eseries_validate_certs: # Whether SSL certificates should be verified. Used for both embedded and proxy. Choices: true, false
+eseries_prefer_embedded: false # Overrides the default behavior of using Web Services Proxy when eseries_proxy_api_url is defined. This will only
+ # effect storage systems that have Embedded Web Services.
+
+# Storage system specific variables
+# ---------------------------------
+#eseries_proxy_ssid: # Arbitrary string for the proxy to represent the storage system. eseries_system_serial will be used when not defined.
+#eseries_system_serial: # Storage system serial number (This is located on a label at the top-left towards the front on the device)
+#eseries_system_addresses: # Storage system management IP addresses. Only required when eseries_system_serial or eseries_system_api_url are not
+ # defined. When not specified, addresses will be populated with eseries_management_interfaces controller addresses
+#eseries_system_api_url: # Url for the storage system's for embedded web services rest api. Example: https://192.168.10.100/devmgr/v2
+eseries_system_username: admin # Username for the storage system's for embedded web services rest api
+#eseries_system_password: # Password for the storage system's for embedded web services rest api and when the admin password has not been set
+ # eseries_system_password will be used to set it.
+#eseries_system_tags: # Meta tags to associate with storage system when added to the proxy.
+
+# Storage system management interface information
+# -----------------------------------------------
+# Note: eseries_management_interfaces will be used when eseries_system_serial, eseries_system_api_url, or eseries_system_addresses are not defined.
+#eseries_management_interfaces: # Subset of the eseries_management_interface variable found in the nar_santricity_management role
+# controller_a:
+# - address: # Controller A port 1's IP address
+# - address: # Controller A port 2's IP address
+# controller_b:
+# - address: # Controller B port 1's IP address
+# - address: # Controller B port 2's IP address
+
+# Web Services Proxy specific variable
+# ------------------------------------
+# Note: eseries_proxy_* variables are required to discover storage systems prior to SANtricity OS version 11.60.2.
+#eseries_proxy_api_url: # Url for the storage system's for proxy web services rest api. Example: https://192.168.10.100/devmgr/v2
+eseries_proxy_api_username: admin # Username for the storage system's for proxy web services rest api.
+#eseries_proxy_api_password: # Password for the storage system's for proxy web services rest api and when the admin password has not been set
+ # eseries_proxy_api_password will be used to set it.
+#eseries_proxy_monitor_password: # Proxy password for the monitor username
+#eseries_proxy_security_password: # Proxy password for the security username
+#eseries_proxy_storage_password: # Proxy password for the monitor username
+#eseries_proxy_support_password: # Proxy password for the support username
+#eseries_proxy_accept_certifications: # Force automatic acceptance of all storage system's certificate
+#eseries_proxy_default_system_tags: # Default meta tags to associate with all storage systems
+#eseries_proxy_default_password: # Default password to associate with all storage systems. This is overridden by eseries_system_password.
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/meta/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/meta/main.yml
new file mode 100644
index 00000000..40e4f11f
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/meta/main.yml
@@ -0,0 +1,13 @@
+galaxy_info:
+ author: Nathan Swartz (@ndswartz)
+ description: Discover NetApp E-Series storage systems and configures SANtricity Web Services Proxy.
+ company: NetApp, Inc
+ license: BSD-3-Clause
+ platforms: []
+ min_ansible_version: 2.9
+ galaxy_tags:
+ - netapp
+ - eseries
+ - storage
+
+dependencies: [] \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/build_info.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/build_info.yml
new file mode 100644
index 00000000..aeefe758
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/build_info.yml
@@ -0,0 +1,37 @@
+- name: Collect storage system facts
+ uri:
+ url: |-
+ {%- if eseries_prefer_embedded == True -%}
+ {{- eseries_system_api_url | default(eseries_proxy_api_url) | regex_replace('v2[\/]?$', 'utils/about') -}}
+ {%- else -%}
+ {{- eseries_proxy_api_url | default(eseries_system_api_url) | regex_replace('v2[\/]?$', 'utils/about') -}}
+ {%- endif -%}
+ headers:
+ Content-Type: "application/json"
+ Accept: "application/json"
+ validate_certs: false
+ register: about
+ failed_when: false
+ when: eseries_proxy_api_url is defined or eseries_system_api_url is defined
+ tags: always
+
+- name: Determine whether SANtricity Web Services REST API is proxy and information
+ set_fact:
+ current_eseries_api_is_proxy: "{{ about['json']['runningAsProxy'] | default(False) }}"
+ tags: always
+
+- name: Collect Web Services information from either proxy or embedded with a preference for embedded.
+ include_tasks: collect_facts/prefer_embedded.yml
+ when: (current_eseries_api_is_proxy == True and eseries_prefer_embedded == True) or current_eseries_api_is_proxy == False
+ tags: always
+
+- name: Collect Web Services information from proxy.
+ include_tasks: collect_facts/prefer_proxy.yml
+ when: current_eseries_api_is_proxy == True and current_eseries_api_url is not defined
+ tags: always
+
+- name: Check whether current_eseries_api_url is defined
+ fail:
+ msg: "Could not determine or discover storage system contact information!"
+ when: current_eseries_api_url is not defined or current_eseries_ssid is not defined
+ tags: always
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/collect_facts/discovery.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/collect_facts/discovery.yml
new file mode 100644
index 00000000..f9abe1f8
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/collect_facts/discovery.yml
@@ -0,0 +1,41 @@
+- name: Discover storage systems from all subnet ranges
+ na_santricity_discover:
+ proxy_url: "{{ item['value']['proxy_url'] }}"
+ proxy_username: "{{ item['value']['proxy_username'] }}"
+ proxy_password: "{{ item['value']['proxy_password'] }}"
+ proxy_validate_certs: "{{ item['value']['proxy_validate_certs'] }}"
+ subnet_mask: "{{ item['key'] }}"
+ prefer_embedded: "{{ item['value']['prefer_embedded'] }}"
+ run_once: true
+# no_log: true
+ register: discovered_systems
+ loop: "{{ subnets | dict2items }}"
+ tags: always
+ vars:
+ subnets: |-
+ {#- Build a dictionary subnet searches and any proxies should they be available #}
+ {%- set systems = {} %}
+ {%- for array in ansible_play_hosts_all %}
+ {%- if "eseries_subnet" in hostvars[array] %}
+
+ {%- if "eseries_proxy_api_url" in (hostvars[array].keys() | list) -%}
+ {%- if systems.update({hostvars[array]["eseries_subnet"]: {
+ "proxy_url": hostvars[array]["eseries_proxy_api_url"] | default(omit),
+ "proxy_username": hostvars[array]["eseries_proxy_api_username"] | default("admin"),
+ "proxy_password": hostvars[array]["eseries_proxy_api_password"] | default(omit),
+ "prefer_embedded": hostvars[array]["eseries_prefer_embedded"] | default(omit),
+ "proxy_validate_certs": hostvars[array]["eseries_validate_certs"] | default(omit)}}) %}
+ {%- endif %}
+ {%- else -%}
+ {%- if systems.update({hostvars[array]["eseries_subnet"]: {
+ "proxy_url": hostvars[array]["eseries_proxy_api_url"] | default(omit),
+ "proxy_username": hostvars[array]["eseries_proxy_api_username"] | default(omit),
+ "proxy_password": hostvars[array]["eseries_proxy_api_password"] | default(omit),
+ "prefer_embedded": hostvars[array]["eseries_prefer_embedded"] | default(omit),
+ "proxy_validate_certs": hostvars[array]["eseries_validate_certs"] | default(omit)}}) %}
+ {%- endif %}
+ {%- endif -%}
+
+ {%- endif %}
+ {%- endfor %}
+ {{ systems }}
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/collect_facts/prefer_embedded.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/collect_facts/prefer_embedded.yml
new file mode 100644
index 00000000..ca733164
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/collect_facts/prefer_embedded.yml
@@ -0,0 +1,183 @@
+# Test whether eseries_system_api_url is a valid storage system web services api.
+- name: Validate the storage system embedded Web Services REST API supplied in eseries_system_api_url.
+ block:
+ - name: Determine management url based on eseries_system_api_url.
+ set_fact:
+ eseries_api_url_list: "{{ [eseries_system_api_url] }}"
+
+ - name: Check for valid storage system Web Services API url.
+ include_tasks: collect_facts/validate_system_api_url.yml
+
+ - name: Set Web Services REST API credentials.Set Web Services REST API credentials.
+ set_fact:
+ current_eseries_api_is_proxy: False
+ current_eseries_ssid: "{{ current_eseries_ssid | default('1') }}"
+ current_eseries_api_username: "{{ eseries_system_username | default('admin') }}"
+ current_eseries_api_password: "{{ eseries_system_password }}"
+ current_eseries_validate_certs: "{{ eseries_validate_certs | default(omit) }}"
+ when: current_eseries_api_url is defined
+ no_log: true
+ when: (current_eseries_api_url is not defined or current_eseries_ssid is not defined) and eseries_system_api_url is defined
+ tags: always
+
+
+# Test when a valid storage system web services api can be extrapolated from eseries_management_interfaces
+- name: Validate the storage system embedded Web Services REST API derived from eseries_management_interfaces.
+ block:
+ - name: Determine management interface IP addresses
+ set_fact:
+ eseries_api_url_list: |-
+ {%- set addresses = [] -%}
+ {%- set url_info = eseries_template_api_url | urlsplit %}
+ {%- for address in [eseries_management_interfaces["controller_a"][0]["address"] | default(""),
+ eseries_management_interfaces["controller_a"][1]["address"] | default(""),
+ eseries_management_interfaces["controller_b"][0]["address"] | default(""),
+ eseries_management_interfaces["controller_b"][1]["address"] | default("")] -%}
+ {%- if address != "" and addresses.append([url_info["scheme"], "://", address, ":", url_info["port"], url_info["path"]] | join("")) %}{%- endif -%}
+ {%- endfor %}
+ {{ addresses }}
+
+ - name: Check for valid storage system Web Services API url.
+ include_tasks: collect_facts/validate_system_api_url.yml
+
+ - name: Set Web Services REST API credentials.
+ set_fact:
+ current_eseries_api_is_proxy: False
+ current_eseries_ssid: "{{ current_eseries_ssid | default('1') }}"
+ current_eseries_api_username: "{{ eseries_system_username | default('admin') }}"
+ current_eseries_api_password: "{{ eseries_system_password }}"
+ current_eseries_validate_certs: "{{ eseries_validate_certs | default(omit) }}"
+ when: current_eseries_api_url is defined
+ no_log: true
+ when: (current_eseries_api_url is not defined or current_eseries_ssid is not defined) and eseries_management_interfaces is defined
+ tags: always
+
+
+# If proxy is available get information from there and avoid the discovery process.
+- name: Attempt to retrieve the storage system from Web Services Proxy.
+ block:
+ - name: Determine existing storage systems in Web Services Proxy.
+ uri:
+ url: "{{ eseries_proxy_api_url | regex_replace('v2[\/]?$', 'v2/storage-systems') }}"
+ headers:
+ Content-Type: "application/json"
+ Accept: "application/json"
+ url_username: "{{ eseries_proxy_api_username | default('admin') }}"
+ url_password: "{{ eseries_proxy_api_password }}"
+ validate_certs: false
+ register: proxy_systems
+ - name: Determine associated management interface IP addresses.
+ set_fact:
+ eseries_system_addresses: |-
+ {%- set addresses = [] %}
+ {%- set url_info = eseries_template_api_url | urlsplit %}
+ {%- for address in [eseries_management_interfaces["controller_a"][0]["address"] | default(""),
+ eseries_management_interfaces["controller_a"][1]["address"] | default(""),
+ eseries_management_interfaces["controller_b"][0]["address"] | default(""),
+ eseries_management_interfaces["controller_b"][1]["address"] | default("")] %}
+ {%- if address != "" and addresses.append(address) -%}{%- endif %}
+ {%- endfor %}
+ {{ addresses }}
+ when: eseries_management_interfaces is defined
+
+ - name: Determine storage system SSID based on storage system serial number or associated IP addresses.
+ set_fact:
+ eseries_api_url_list: |-
+ {#- Determine any system that either has the expected serial number or a management ip address -#}
+ {%- set ssids = [] -%}
+ {%- set addresses = [] -%}
+
+ {#- Search discovered storage systems -#}
+ {%- set url_info = eseries_template_api_url | urlsplit %}
+ {%- for system in proxy_systems["json"] -%}
+
+ {#- Check for serial number match -#}
+ {%- if eseries_system_serial is defined and system["chassisSerialNumber"] == eseries_system_serial -%}
+ {%- if ssids.append(system["id"]) -%}{%- endif -%}
+ {%- for address in system["managementPaths"] -%}
+ {%- if addresses.append([url_info["scheme"], "://", address, ":", url_info["port"], url_info["path"]] | join("")) %}{%- endif -%}
+ {%- endfor -%}
+
+ {%- elif eseries_proxy_ssid is defined and eseries_proxy_ssid == system["id"] -%}
+ {%- if ssids.append(system["id"]) -%}{%- endif -%}
+ {%- for address in system["managementPaths"] -%}
+ {%- if addresses.append([url_info["scheme"], "://", address, ":", url_info["port"], url_info["path"]] | join("")) %}{%- endif -%}
+ {%- endfor -%}
+
+ {%- elif eseries_system_addresses is defined and eseries_system_addresses | length > 0 -%}
+ {%- for address in eseries_system_addresses -%}
+ {%- if address in system["managementPaths"] -%}
+ {%- if ssids.append(system["id"]) -%}{%- endif -%}
+ {%- for address in system["managementPaths"] -%}
+ {%- if addresses.append([url_info["scheme"], "://", address, ":", url_info["port"], url_info["path"]] | join("")) %}{%- endif -%}
+ {%- endfor -%}
+ {%- endif -%}
+ {%- endfor -%}
+ {%- endif -%}
+ {%- endfor -%}
+
+ {%- if ssids | unique | length == 1 -%}
+ {{- addresses -}}
+ {%- else -%}[]{%- endif -%}
+
+ - name: Check for valid storage system Web Services API url.
+ include_tasks: collect_facts/validate_system_api_url.yml
+
+ - name: Determine Web Services Embedded REST API information.
+ set_fact:
+ current_eseries_api_is_proxy: False
+ current_eseries_ssid: "{{ current_eseries_ssid | default('1') }}"
+ current_eseries_api_username: "{{ eseries_system_username | default('admin') }}"
+ current_eseries_api_password: "{{ eseries_system_password }}"
+ current_eseries_validate_certs: "{{ eseries_validate_certs | default(omit) }}"
+ when: current_eseries_api_url is defined
+ when: current_eseries_api_is_proxy == True and (current_eseries_api_url is not defined or current_eseries_ssid is not defined)
+ tags: always
+
+# Try discovering eseries_system_api_url if known eseries_system_api_url is not valid
+- name: Attempt to discover storage system.
+ block:
+ - name: Search subnet for storage system.
+ include_tasks: collect_facts/discovery.yml
+ when: discovered_systems is not defined
+
+ - name: Check whether eseries_system_serial and eseries_subnet are defined.
+ fail:
+ msg: "eseries_system_serial and eseries_subnet must be defined in order to search for storage system."
+ when: eseries_system_serial is not defined or eseries_subnet is not defined
+
+ - name: Determine storage system Web Services information
+ set_fact:
+ current_eseries_api_info: |-
+ {% set info = {} %}
+ {%- for result in discovered_systems["results"] -%}
+ {%- for serial in (result["systems_found"].keys() | list) if eseries_system_serial and serial == eseries_system_serial -%}
+ {%- if info.update(result["systems_found"][serial]) %}{%- endif -%}
+ {%- endfor %}
+ {%- endfor %}
+ {{ info }}
+# no_log: true
+
+ - name: Check whether storage system was discovered.
+ fail:
+ msg: "Storage system failed to be discovered! Serial [{{ eseries_system_serial }}]. Subnet [{{ eseries_subnet }}]."
+ when: current_eseries_api_info == {}
+
+ - name: Set storage systems Web Services URL information
+ set_fact:
+ current_eseries_api_is_proxy: "{{ current_eseries_api_info['proxy_required'] }}"
+ current_eseries_api_url: "{{ current_eseries_api_info['api_urls'][0] }}"
+ current_eseries_ssid: |-
+ {%- if current_eseries_api_info["proxy_required"] == False -%}
+ 1
+ {%- elif current_eseries_api_info["proxy_required"] == True and current_eseries_api_info['proxy_ssid'] != "" -%}
+ {{- current_eseries_api_info['proxy_ssid'] -}}
+ {%- else -%}
+ {{- eseries_system_serial -}}
+ {%- endif -%}
+ current_eseries_api_username: "{% if current_eseries_api_info['proxy_required'] %}{{ eseries_proxy_api_username | default('admin') }}{% else %}{{ eseries_system_username | default('admin') }}{% endif %}"
+ current_eseries_api_password: "{% if current_eseries_api_info['proxy_required'] %}{{ eseries_proxy_api_password }}{% else %}{{ eseries_system_password }}{% endif %}"
+ current_eseries_validate_certs: "{{ eseries_validate_certs | default(omit) }}"
+ no_log: true
+ when: current_eseries_api_url is not defined or current_eseries_ssid is not defined
+ tags: always
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/collect_facts/prefer_proxy.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/collect_facts/prefer_proxy.yml
new file mode 100644
index 00000000..35838ab1
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/collect_facts/prefer_proxy.yml
@@ -0,0 +1,106 @@
+- name: Determine existing storage systems in Web Services Proxy.
+ uri:
+ url: "{{ eseries_proxy_api_url | regex_replace('v2[\/]?$', 'v2/storage-systems') }}"
+ headers:
+ Content-Type: "application/json"
+ Accept: "application/json"
+ url_username: "{{ eseries_proxy_api_username | default('admin') }}"
+ url_password: "{{ eseries_proxy_api_password }}"
+ validate_certs: false
+ register: proxy_systems
+ tags: always
+
+- name: Determine associated management interface IP addresses.
+ set_fact:
+ eseries_system_addresses: |-
+ {%- set addresses = [] %}
+ {%- set url_info = eseries_template_api_url | urlsplit %}
+ {%- for address in [eseries_management_interfaces["controller_a"][0]["address"] | default(""),
+ eseries_management_interfaces["controller_a"][1]["address"] | default(""),
+ eseries_management_interfaces["controller_b"][0]["address"] | default(""),
+ eseries_management_interfaces["controller_b"][1]["address"] | default("")] %}
+ {%- if address != "" and addresses.append(address) -%}{%- endif %}
+ {%- endfor %}
+ {{ addresses }}
+ when: eseries_management_interfaces is defined
+ tags: always
+
+- name: Determine storage system SSID based on storage system serial number or associated IP addresses.
+ set_fact:
+ eseries_ssid_list: |-
+ {#- Determine any system that either has the expected serial number or a management ip address -#}
+ {%- set ssids = [] -%}
+
+ {#- Search discovered storage systems -#}
+ {%- for system in proxy_systems["json"] -%}
+
+ {#- Check for serial number match -#}
+ {%- if eseries_system_serial is defined and system["chassisSerialNumber"] == eseries_system_serial -%}
+ {%- if ssids.append(system["id"]) -%}{%- endif -%}
+
+ {%- elif eseries_proxy_ssid is defined and eseries_proxy_ssid == system["id"] -%}
+ {%- if ssids.append(system["id"]) -%}{%- endif -%}
+
+ {%- elif eseries_system_addresses is defined and eseries_system_addresses | length > 0 -%}
+ {%- for address in eseries_system_addresses -%}
+ {%- if address in system["managementPaths"] -%}
+ {%- if ssids.append(system["id"]) -%}{%- endif -%}
+ {%- endif -%}
+ {%- endfor -%}
+ {%- endif -%}
+ {%- endfor -%}
+ {{- ssids | unique -}}
+ tags: always
+
+- name: Use the Web Services Proxy REST API
+ set_fact:
+ current_eseries_api_url: "{{ eseries_proxy_api_url }}"
+ current_eseries_ssid: "{{ eseries_ssid_list[0] }}"
+ current_eseries_api_username: "{{ eseries_proxy_api_username | default('admin') }}"
+ current_eseries_api_password: "{{ eseries_proxy_api_password }}"
+ current_eseries_validate_certs: "{{ eseries_validate_certs | default(omit) }}"
+# no_log: true
+ when: eseries_ssid_list | length == 1 and (eseries_proxy_ssid is not defined or eseries_proxy_ssid == eseries_ssid_list[0])
+ tags: always
+
+- name: Search subnet for storage system.
+ block:
+ - name: Search subnet for storage system.
+ include_tasks: collect_facts/discovery.yml
+ when: discovered_systems is not defined
+
+ - name: Check whether eseries_system_serial and eseries_subnet are defined.
+ fail:
+ msg: "eseries_system_serial and eseries_subnet must be defined in order to search for storage system."
+ when: eseries_system_serial is not defined or eseries_subnet is not defined
+
+ - name: Determine storage system Web Services information
+ set_fact:
+ current_eseries_api_info: |-
+ {% set info = {} %}
+ {%- for result in discovered_systems["results"] -%}
+ {%- for serial in (result["systems_found"].keys() | list) if eseries_system_serial and serial == eseries_system_serial -%}
+ {%- if info.update(result["systems_found"][serial]) %}{%- endif -%}
+ {%- endfor %}
+ {%- endfor %}
+ {{ info }}
+# no_log: true
+
+ - name: Set storage systems Web Services URL information
+ set_fact:
+ current_eseries_api_is_proxy: "{{ current_eseries_api_info['proxy_required'] }}"
+ current_eseries_api_url: "{{ current_eseries_api_info['api_urls'][0] }}"
+ current_eseries_ssid: |-
+ {%- if eseries_proxy_ssid is defined -%}
+ {{- eseries_proxy_ssid -}}
+ {%- elif current_eseries_api_info["proxy_ssid"] != "" -%}
+ {{- current_eseries_api_info["proxy_ssid"] -}}
+ {%- else -%}
+ {{- eseries_system_serial -}}
+ {%- endif -%}
+ current_eseries_api_username: "{{ eseries_proxy_api_username | default('admin') }}"
+ current_eseries_api_password: "{{ eseries_proxy_api_password }}"
+ current_eseries_validate_certs: "{{ eseries_validate_certs | default(omit) }}"
+# no_log: true
+ when: current_eseries_api_url is not defined or current_eseries_ssid is not defined
+ tags: always
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/collect_facts/validate_system_api_url.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/collect_facts/validate_system_api_url.yml
new file mode 100644
index 00000000..6717d596
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/collect_facts/validate_system_api_url.yml
@@ -0,0 +1,23 @@
+- name: Validate storage system urls.
+ block:
+ - name: Collect storage system facts.
+ uri:
+ url: "{{ item | regex_replace('v2[\/]?$', 'utils/about') }}"
+ headers:
+ Content-Type: "application/json"
+ Accept: "application/json"
+ validate_certs: false
+ ignore_errors: true
+ register: about_results
+ loop: "{{ lookup('list', eseries_api_url_list) }}"
+
+ - name: Determine the first successful Web Services REST API url.
+ set_fact:
+ current_eseries_api_url: |-
+ {%- set valid_urls = [] %}
+ {%- for result in about_results["results"] if not result["failed"] -%}
+ {%- if valid_urls.append(result['item']) %}{%- endif %}
+ {%- endfor %}
+ {{ valid_urls[0] | default(omit) }}
+ when: eseries_api_url_list is defined and eseries_api_url_list | length > 0
+ tags: always
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/main.yml
new file mode 100644
index 00000000..fe5afded
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/main.yml
@@ -0,0 +1,17 @@
+- name: Configure SANtricity Web Services Proxy's passwords
+ import_tasks: proxy_security.yml
+ delegate_to: localhost
+ run_once: true
+
+- name: Build information for Web Services
+ import_tasks: build_info.yml
+ delegate_to: localhost
+ tags:
+ - always
+
+- name: Configure SANtricity WebServices Proxy
+ import_tasks: proxy.yml
+ delegate_to: localhost
+ run_once: true
+ tags:
+ - always
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/proxy.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/proxy.yml
new file mode 100644
index 00000000..fe64e0e3
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/proxy.yml
@@ -0,0 +1,48 @@
+- name: Add storage systems to SANtricity Web Services Proxy
+ netapp_eseries.santricity.na_santricity_proxy_systems:
+ api_url: "{{ item['key'] }}"
+ api_username: "{{ item['value']['proxy_username'] }}"
+ api_password: "{{ item['value']['proxy_password'] }}"
+ validate_certs: "{{ item['value']['proxy_validate_certs'] | default(omit) }}"
+ accept_certificate: "{{ item['value']['proxy_accept_certifications'] | default(omit) }}"
+ subnet_mask: "{{ item['value']['proxy_subnet'] }}"
+ password: "{{ item['value']['proxy_default_password'] | default(omit) }}"
+ tags: "{{ item['value']['proxy_default_system_tags'] | default(omit) }}"
+ systems: "{{ item['value']['proxy_systems'] }}"
+ loop: "{{ lookup('dict', proxy_systems_info, wantlist=True) }}"
+# no_log: true
+ vars:
+ proxy_systems_info: |-
+ {#- Build a dictionary of all inventoried proxies keyed by their api url #}
+ {%- set systems = {} %}
+ {%- for array in ansible_play_hosts_all %}
+ {%- if hostvars[array]["current_eseries_api_is_proxy"] %}
+ {%- set array_info = {} %}
+ {%- if "eseries_system_serial" in hostvars[array] or "eseries_system_addresses" in hostvars[array] %}
+ {%- if array_info.update({
+ "ssid": hostvars[array]["current_eseries_ssid"] | default(omit),
+ "password": hostvars[array]["eseries_system_password"] | default(omit),
+ "serial": hostvars[array]["eseries_system_serial"] | default(omit),
+ "addresses": hostvars[array]["eseries_system_addresses"] | default(omit),
+ "tags": hostvars[array]["eseries_system_tags"] | default(omit)}) %}
+ {%- endif %}
+ {%- endif %}
+ {%- if "eseries_proxy_api_url" in hostvars[array] and "eseries_proxy_api_password" in hostvars[array] %}
+ {%- if hostvars[array]["eseries_proxy_api_url"] in systems %}
+ {%- if systems[hostvars[array]["eseries_proxy_api_url"]]["proxy_systems"].append(array_info) %}{%- endif %}
+ {%- else %}
+ {%- if systems.update({hostvars[array]["eseries_proxy_api_url"]: {
+ "proxy_username": hostvars[array]["eseries_proxy_api_username"] | default("admin"),
+ "proxy_password": hostvars[array]["eseries_proxy_api_password"] | default(omit),
+ "proxy_subnet": hostvars[array]["eseries_subnet"] | default(omit),
+ "proxy_systems": [array_info],
+ "proxy_validate_certs": hostvars[array]["eseries_validate_certs"] | default(omit),
+ "proxy_accept_certifications": hostvars[array]["eseries_proxy_accept_certifications"] | default(omit),
+ "proxy_default_system_tags": hostvars[array]["eseries_proxy_default_system_tags"] | default(omit),
+ "proxy_default_password": hostvars[array]["eseries_proxy_default_password"] | default(omit)}}) %}
+ {%- endif %}
+ {%- endif %}
+ {%- endif %}
+ {%- endif %}
+ {%- endfor %}
+ {{ systems }}
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/proxy_security.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/proxy_security.yml
new file mode 100644
index 00000000..6cf4a594
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_common/tasks/proxy_security.yml
@@ -0,0 +1,128 @@
+- name: Ensure proxy admin password has been set
+ na_santricity_auth:
+ ssid: proxy
+ api_url: "{{ item['key'] }}"
+ api_username: "{{ item['value']['proxy_username'] }}"
+ api_password: "{{ item['value']['current_password'] | default(item['value']['proxy_password']) }}"
+ validate_certs: "{{ item['value']['proxy_validate_certs'] }}"
+ minimum_password_length: "{{ item['value']['proxy_minimum_password_length'] }}"
+ user: admin
+ password: "{{ item['value']['proxy_password'] }}"
+ loop: "{{ lookup('dict', proxy_admin, wantlist=True) }}"
+ no_log: true
+ vars:
+ proxy_admin: |-
+ {#- Build a dictionary of all inventoried proxies keyed by their api url #}
+ {%- set systems = {} %}
+ {%- for array in ansible_play_hosts_all %}
+ {%- if "eseries_proxy_api_url" in hostvars[array] and "eseries_proxy_api_password" in hostvars[array] %}
+ {%- if systems.update({hostvars[array]["eseries_proxy_api_url"]: {
+ "proxy_username": hostvars[array]["eseries_proxy_api_username"] | default('admin'),
+ "proxy_password": hostvars[array]["eseries_proxy_api_password"],
+ "current_proxy_password": hostvars[array]["eseries_proxy_current_api_password"] | default(omit),
+ "proxy_validate_certs": hostvars[array]["eseries_validate_certs"] | default(omit),
+ "proxy_minimum_password_length": hostvars[array]["eseries_proxy_minimum_password_length"] | default(omit)}}) %}
+ {%- endif %}
+ {%- endif %}
+ {%- endfor %}
+ {{ systems }}
+ tags:
+ - always
+
+- name: Ensure proxy non-admin passwords have been set
+ na_santricity_auth:
+ ssid: proxy
+ api_url: "{{ item['value']['proxy_url'] }}"
+ api_username: "{{ item['value']['proxy_url_username'] }}"
+ api_password: "{{ item['value']['proxy_url_password'] }}"
+ validate_certs: "{{ eseries_validate_certs | default(omit) }}"
+ user: "{{ item['value']['proxy_username'] }}"
+ password: "{{ item['value']['proxy_password'] }}"
+ loop: "{{ lookup('dict', proxy_non_admin, wantlist=True) }}"
+ no_log: true
+ vars:
+ proxy_non_admin: |-
+ {#- Build a dictionary of all inventoried proxies keyed by their api url containing non-admin usernames/passwords #}
+ {%- set systems = {} %}
+ {%- for array in ansible_play_hosts_all %}
+ {%- if "eseries_proxy_api_url" in hostvars[array] and "eseries_proxy_api_password" in hostvars[array] and
+ (("eseries_proxy_monitor_password" in hostvars[array] and hostvars[array]["eseries_proxy_monitor_password"]) or
+ ("eseries_proxy_security_password" in hostvars[array] and hostvars[array]["eseries_proxy_security_password"]) or
+ ("eseries_proxy_storage_password" in hostvars[array] and hostvars[array]["eseries_proxy_storage_password"]) or
+ ("eseries_proxy_support_password" in hostvars[array] and hostvars[array]["eseries_proxy_support_password"])) %}
+ {%- if "eseries_proxy_monitor_password" in hostvars[array] and hostvars[array]["eseries_proxy_monitor_password"] and
+ systems.update({[hostvars[array]["eseries_proxy_api_url"], 'monitor'] | join("-"): {
+ "proxy_url": hostvars[array]["eseries_proxy_api_url"],
+ "proxy_url_username": hostvars[array]["eseries_proxy_api_username"] | default('admin'),
+ "proxy_url_password": hostvars[array]["eseries_proxy_api_password"],
+ "proxy_username": 'monitor',
+ "proxy_password": hostvars[array]["eseries_proxy_monitor_password"]}}) %}
+ {%- endif %}
+ {%- if "eseries_proxy_security_password" in hostvars[array] and hostvars[array]["eseries_proxy_security_password"] and
+ systems.update({[hostvars[array]["eseries_proxy_api_url"], 'security'] | join("-"): {
+ "proxy_url": hostvars[array]["eseries_proxy_api_url"],
+ "proxy_url_username": hostvars[array]["eseries_proxy_api_username"] | default('admin'),
+ "proxy_url_password": hostvars[array]["eseries_proxy_api_password"],
+ "proxy_username": 'security',
+ "proxy_password": hostvars[array]["eseries_proxy_security_password"]}}) %}
+ {%- endif %}
+ {%- if "eseries_proxy_storage_password" in hostvars[array] and hostvars[array]["eseries_proxy_storage_password"] and
+ systems.update({[hostvars[array]["eseries_proxy_api_url"], 'storage'] | join("-"): {
+ "proxy_url": hostvars[array]["eseries_proxy_api_url"],
+ "proxy_url_username": hostvars[array]["eseries_proxy_api_username"] | default('admin'),
+ "proxy_url_password": hostvars[array]["eseries_proxy_api_password"],
+ "proxy_username": 'storage',
+ "proxy_password": hostvars[array]["eseries_proxy_storage_password"]}}) %}
+ {%- endif %}
+ {%- if "eseries_proxy_support_password" in hostvars[array] and hostvars[array]["eseries_proxy_support_password"] and
+ systems.update({[hostvars[array]["eseries_proxy_api_url"], 'support'] | join("-"): {
+ "proxy_url": hostvars[array]["eseries_proxy_api_url"],
+ "proxy_url_username": hostvars[array]["eseries_proxy_api_username"] | default('admin'),
+ "proxy_url_password": hostvars[array]["eseries_proxy_api_password"],
+ "proxy_username": 'support',
+ "proxy_password": hostvars[array]["eseries_proxy_support_password"]}}) %}
+ {%- endif %}
+ {%- endif %}
+ {%- endfor %}
+ {{ systems }}
+
+- name: Ensure proxy LDAP have been configured
+ na_santricity_ldap:
+ ssid: proxy
+ api_url: "{{ item['key'] }}"
+ api_username: "{{ item['value']['proxy_username'] }}"
+ api_password: "{{ item['value']['current_password'] | default(item['value']['proxy_password']) }}"
+ validate_certs: "{{ item['value']['proxy_validate_certs'] }}"
+ state: "{{ item['value']['ldap_state'] }}"
+ identifier: "{{ item['value']['ldap_identifier'] | default(omit) }}"
+ server_url: "{{ item['value']['ldap_server'] | default(omit) }}"
+ bind_user: "{{ item['value']['ldap_bind_username'] | default(omit) }}"
+ bind_password: "{{ item['value']['ldap_bind_password'] | default(omit) }}"
+ search_base: "{{ item['value']['ldap_search_base'] | default(omit) }}"
+ user_attribute: "{{ item['value']['ldap_user_attribute'] | default(omit) }}"
+ role_mappings: "{{ item['value']['ldap_role_mappings'] | default(omit) }}"
+ ignore_errors: true
+ loop: "{{ lookup('dict', proxy_admin, wantlist=True) }}"
+ vars:
+ proxy_admin: |-
+ {#- Build a dictionary of all inventoried proxies keyed by their api url #}
+ {%- set systems = {} %}
+ {%- for array in ansible_play_hosts_all %}
+ {%- if "eseries_proxy_api_url" in hostvars[array] and "eseries_proxy_api_password" in hostvars[array] and "eseries_proxy_ldap_state" in hostvars[array] %}
+ {%- if systems.update({hostvars[array]["eseries_proxy_api_url"]: {
+ "proxy_username": hostvars[array]["eseries_proxy_api_username"] | default('admin'),
+ "proxy_password": hostvars[array]["eseries_proxy_api_password"],
+ "current_proxy_password": hostvars[array]["eseries_proxy_current_api_password"] | default(omit),
+ "proxy_validate_certs": hostvars[array]["eseries_validate_certs"] | default(omit),
+ "ldap_state": hostvars[array]["eseries_proxy_ldap_state"],
+ "ldap_identifier": hostvars[array]["eseries_proxy_ldap_identifier"] | default(omit),
+ "ldap_server": hostvars[array]["eseries_proxy_ldap_server"] | default(omit),
+ "ldap_bind_username": hostvars[array]["eseries_proxy_ldap_bind_username"] | default(omit),
+ "ldap_bind_password": hostvars[array]["eseries_proxy_ldap_bind_password"] | default(omit),
+ "ldap_search_base": hostvars[array]["eseries_proxy_ldap_search_base"] | default(omit),
+ "ldap_user_attribute": hostvars[array]["eseries_proxy_ldap_user_attribute"] | default(omit),
+ "ldap_role_mappings": hostvars[array]["eseries_proxy_ldap_role_mappings"] | default(omit)}}) %}
+ {%- endif %}
+ {%- endif %}
+ {%- endfor %}
+ {{ systems }}
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/README.md b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/README.md
new file mode 100644
index 00000000..0041e8d8
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/README.md
@@ -0,0 +1,339 @@
+nar_santricity_host
+=========
+ Configures storage pools, volumes, hosts, host groups, and port interfaces for NetApp E-Series storage arrays
+ using iSCSI, FC, SAS, IB, NVMe protocols.
+
+Requirements
+------------
+ - Ansible 2.9 or later
+ - NetApp E-Series E2800 platform or newer or NetApp E-Series SANtricity Web Services Proxy configured for older E-Series Storage arrays.
+
+Example Playbook
+----------------
+ - hosts: eseries_storage_systems
+ gather_facts: false
+ collection:
+ - netapp_eseries.santricity
+ tasks:
+ - name: Ensure NetApp E-Series storage system is properly configured
+ import_role:
+ name: nar_santricity_host
+
+Example Storage System Inventory File (Discover storage system with proxy)
+-------------------------------------
+ eseries_system_serial: "012345678901" # Be sure to quote if the serial is all numbers and begins with zero.
+ eseries_system_password: admin_password
+ eseries_proxy_api_url: https://192.168.1.100:8443/devmgr/v2/
+ eseries_proxy_api_password: admin_password
+ eseries_subnet: 192.168.1.0/24
+ eseries_prefer_embedded: true
+ eseries_validate_certs: false
+
+ eseries_initiator_protocol: iscsi
+
+ # Controller port definitions
+ eseries_controller_port_config_method: static
+ eseries_controller_port_subnet_mask: 255.255.255.0
+ eseries_controller_port:
+ controller_a:
+ ports:
+ - address: 192.168.2.100
+ - address: 192.168.2.110
+ controller_b:
+ ports:
+ - address: 192.168.3.100
+ - address: 192.168.3.110
+
+ # Storage pool and volume configuration
+ eseries_storage_pool_configuration:
+ - name: pool[1-2]
+ raid_level: raid6
+ criteria_drive_count: 10
+ volumes:
+ - name: "[pool]_volume[A-C]"
+ host: server_group
+ size: 4096
+
+Example Storage System Inventory File (Without storage system discovery)
+-------------------------------------
+ eseries_system_api_url: https://192.168.1.200:8443/devmgr/v2/
+ eseries_system_password: admin_password
+ eseries_validate_certs: false
+
+ (...) # Same as the previous example
+
+Role Variables
+--------------
+**Note that when values are specified below, they indicate the default value.**
+
+ # Web Services Embedded information
+ eseries_subnet: # Network subnet to search for the storage system specified in CIDR form. Example: 192.168.1.0/24
+ eseries_system_serial: # Storage system serial number. Be sure to quote if the serial is all numbers and begins with zero. (This is located
+ # on a label at the top-left towards the front on the device)
+ eseries_system_addresses: # Storage system management IP addresses. Only required when eseries_system_serial or eseries_system_api_url are not
+ # defined. When not specified, addresses will be populated with eseries_management_interfaces controller addresses.
+ eseries_system_api_url: # Url for the storage system's for embedded web services rest api. Example: https://192.168.10.100/devmgr/v2
+ eseries_system_username: admin # Username for the storage system's for embedded web services rest api
+ eseries_system_password: # Password for the storage system's for embedded web services rest api and when the admin password has not been set
+ # eseries_system_password will be used to set it.
+ eseries_proxy_ssid: # Arbitrary string for the proxy to represent the storage system. eseries_system_serial will be used when not defined.
+ eseries_template_api_url: # Template for the web services api url. Default: https://0.0.0.0:8443/devmgr/v2/
+ eseries_prefer_embedded: false # Overrides the default behavior of using Web Services Proxy when eseries_proxy_api_url is defined. This will only effect storage systems that have Embedded Web Services.
+ eseries_validate_certs: true # Indicates Whether SSL certificates should be verified. Used for both embedded and proxy. Choices: true, false
+
+ # Web Services Proxy information
+ Note: eseries_proxy_* variables are required to discover storage systems prior to SANtricity OS version 11.60.2.
+ eseries_proxy_api_url: # Url for the storage system's for proxy web services rest api. Example: https://192.168.10.100/devmgr/v2
+ eseries_proxy_api_username: # Username for the storage system's for proxy web services rest api.
+ eseries_proxy_api_password: # Password for the storage system's for proxy web services rest api and when the admin password has not been set
+ # eseries_proxy_api_password will be used to set it.
+
+ # Controller iSCSI Interface Port Default Policy Specifications
+ eseries_controller_iscsi_port_state: enabled # Generally specifies whether a controller port definition should be applied Choices: enabled, disabled
+ eseries_controller_iscsi_port_config_method: dhcp # General port configuration method definition for both controllers. Choices: static, dhcp
+ eseries_controller_iscsi_port_gateway: # General port IPv4 gateway for both controllers.
+ eseries_controller_iscsi_port_subnet_mask: # General port IPv4 subnet mask for both controllers.
+ eseries_controller_iscsi_port_mtu: 9000 # General port maximum transfer units (MTU) for both controllers. Any value greater than 1500 (bytes).
+ eseries_controller_iscsi_port:
+ controller_a: # Ordered list of controller A channel definition.
+ - state: # Whether the port should be enabled. Choices: enabled, disabled
+ config_method: # Port configuration method Choices: static, dhcp
+ address: # Port IPv4 address
+ gateway: # Port IPv4 gateway
+ subnet_mask: # Port IPv4 subnet_mask
+ mtu: # Port IPv4 mtu
+ controller_b: # Ordered list of controller B channel definition.
+ - (...) # Same as controller A but for controller B
+
+ # Controller InfiniBand iSER Interface Channel
+ eseries_controller_ib_iser_port:
+ controller_a: # Ordered list of controller A channel address definition.
+ - # Port IPv4 address for channel 1
+ - (...) # So on and so forth
+ controller_b: # Ordered list of controller B channel address definition.
+
+ # Controller NVMe over InfiniBand Interface Channel
+ eseries_controller_nvme_ib_port:
+ controller_a: # Ordered list of controller A channel address definition.
+ - # Port IPv4 address for channel 1
+ - (...) # So on and so forth
+ controller_b: # Ordered list of controller B channel address definition.
+
+ # Controller NVMe RoCE Interface Port Default Policy Specifications
+ eseries_controller_nvme_roce_port_state: enabled # Generally specifies whether a controller port definition should be applied Choices: enabled, disabled
+ eseries_controller_nvme_roce_port_config_method: dhcp # General port configuration method definition for both controllers. Choices: static, dhcp
+ eseries_controller_nvme_roce_port_gateway: # General port IPv4 gateway for both controllers.
+ eseries_controller_nvme_roce_port_subnet_mask: # General port IPv4 subnet mask for both controllers.
+ eseries_controller_nvme_roce_port_mtu: 9000 # General port maximum transfer units (MTU). Any value greater than 1500 (bytes).
+ eseries_controller_nvme_roce_port_speed: auto # General interface speed. Value must be a supported speed or auto for automatically negotiating the speed with the port.
+ eseries_controller_nvme_roce_port:
+ controller_a: # Ordered list of controller A channel definition.
+ - state: # Whether the port should be enabled.
+ config_method: # Port configuration method Choices: static, dhcp
+ address: # Port IPv4 address
+ subnet_mask: # Port IPv4 subnet_mask
+ gateway: # Port IPv4 gateway
+ mtu: # Port IPv4 mtu
+ speed: # Port IPv4 speed
+ controller_b: # Ordered list of controller B channel definition.
+ - (...) # Same as controller A but for controller B
+
+ # Target discovery specifications
+ Note: add the following to ansible-playbook command to update the chap secret: --extra-vars "eseries_target_chap_secret_update=True
+ eseries_target_name: # iSCSI target name that will be seen by the initiator
+ eseries_target_ping: True # Enables ICMP ping response from the configured iSCSI ports (boolean)
+ eseries_target_unnamed_discovery: True # Whether the iSCSI target iqn should be returned when an initiator performs a discovery session.
+ eseries_target_chap_secret: # iSCSI chap secret. When left blank, the chap secret will be removed from the storage system.
+ eseries_target_chap_secret_update: False # DO NOT REMOVE! Since na_santricity_iscsi_target cannot compare the chap secret with the current and chooses to always
+ # return changed=True, this flag is used to force the module to update the chap secret. It is preferable to
+ # leave this value False and to add the --extra-vars "eseries_target_chap_secret_update=True".
+
+ # Storage Pool Default Policy Specifications
+ eseries_storage_pool_state: present # Default storage pool state. Choices: present, absent
+ eseries_storage_pool_raid_level: raidDiskPool # Default volume raid level. Choices: raid0, raid1, raid5, raid6, raidDiskPool
+ eseries_storage_pool_secure_pool: false # Default for storage pool drive security. This flag will enable the security at rest feature. There
+ # must be sufficient FDE or FIPS security capable drives. Choices: true, false
+ eseries_storage_pool_criteria_drive_count: # Default storage pool drive count.
+ eseries_storage_pool_reserve_drive_count: # Default reserve drive count for drive reconstruction for storage pools using dynamic disk pool and
+ # the raid level must be set for raidDiskPool.
+ eseries_storage_pool_criteria_min_usable_capacity: # Default minimum required capacity for storage pools.
+ eseries_storage_pool_criteria_drive_type: # Default drive type for storage pools. Choices: hdd, ssd
+ eseries_storage_pool_criteria_size_unit: gb # Default unit size for all storage pool related sizing.
+ # Choices: bytes, b, kb, mb, gb, tb, pb, eb, zb, yb
+ eseries_storage_pool_criteria_drive_min_size: # Default minimum drive size for storage pools.
+ eseries_storage_pool_criteria_drive_require_da: # Default for whether storage pools are required to have data assurance (DA) compatible drives.
+ # Choices: true, false
+ eseries_storage_pool_criteria_drive_require_fde: # Default for whether storage pools are required to have drive security compatible drives.
+ # Choices: true, false
+ eseries_storage_pool_remove_volumes: # Default policy for deleting volumes prior to removing storage pools.
+ eseries_storage_pool_erase_secured_drives: # Default policy for erasing the content drives during create and delete storage pool operations.
+ # Choices: true, false
+
+ # Volume Default Policy Specifications
+ eseries_volume_state: present # Default volume state. Choices: present, absent
+ eseries_volume_size_unit: gb # Default unit size for all volume sizing options.
+ eseries_volume_size: # Default volume size or the presented size for thinly provisioned volumes.
+ eseries_volume_data_assurance_enabled: # Default for whether data assurance(DA) is required to be enabled.
+ eseries_volume_segment_size_kb: # Default segment size measured in kib.
+ eseries_volume_read_cache_enable: # Default for read caching which will cache all read requests.
+ eseries_volume_read_ahead_enable: # Default for read ahead caching; this is good for sequential workloads to cache subsequent blocks.
+ eseries_volume_write_cache_enable: # Default for write caching which will cache all writes.
+ eseries_volume_cache_without_batteries: # Default for allowing caching when batteries are not present.
+ eseries_volume_thin_provision: # Default for whether volumes should be thinly provisioned.
+ eseries_volume_thin_volume_repo_size: # Default for actually allocated space for thinly provisioned volumes.
+ eseries_volume_thin_volume_max_repo_size: # Default for the maximum allocated space allowed for thinly provisioned volumes.
+ eseries_volume_thin_volume_expansion_policy: # Default thin volume expansion policy. Choices: automatic, manual
+ eseries_volume_thin_volume_growth_alert_threshold: # Default thin volume growth alert threshold; this is the threshold for when the thin volume expansion
+ # policy will be enacted. Allowable values are between and including 10% and 99%
+ eseries_volume_ssd_cache_enabled: # Default for ssd cache which will enable the volume to use an existing SSD cache on the storage array.
+ eseries_volume_host: # Default host for all volumes; the value can be any host from the Ansible inventory. Any initiator may be
+ # used whether connected or not since the storage array does not require connectivity in order to create
+ # host objects.
+ eseries_volume_workload_name: # Default workload tag name
+ eseries_volume_workload_metadata: # Default workload metadata
+ eseries_volume_volume_metadata: # Default volume_metadata
+ eseries_volume_owning_controller # Default preferred owning controller
+ eseries_volume_wait_for_initialization: false # Default for whether volume creation with wait for initialization to complete
+
+ # Storage Pool-Volume Mapping Default Policy Specifications
+ # ---------------------------------------------------------
+ eseries_lun_mapping_state: present # Generally specifies whether a LUN mapping should be present. This is useful when adding a default host for all
+ # volumes. Choices: present, absent
+ eseries_lun_mapping_host: # Default host for all volumes not specifically give a host either in common_volume_configuration or in
+ # eseries_storage_pool_configuration.
+
+ # Storage Pool-Volume Default Policy Specifications
+ Name schemes: Storage pool and volume names can be used to specify a naming scheme to produce a list of storage pools and volumes. The scheme are defined by
+ brackets and can be used to specify a range of lowercase letters, uppercase letters, range of single digit numbers, any top-level inventory
+ variables, and the current defined storage pool (volume only).
+ eseries_storage_pool_configuration:
+ - name: # Name or name scheme (see above) for the storage pool.
+ state: # Specifies whether the storage pool should exist (present, absent). When removing an existing storage array all of the
+ # volumes must be defined with state=absent.
+ raid_level # Volume group raid level. Choices: raid0, raid1, raid5, raid6, raidDiskPool (Default: raidDiskPool)
+ secure_pool: # Default for storage pool drive security. This flag will enable the security at rest feature. There must be sufficient FDE
+ # or FIPS security capable drives. Choices: true, false
+ criteria_drive_count: # Default storage pool drive count.
+ reserve_drive_count: # Default reserve drive count for drive reconstruction for storage pools using dynamic disk pool and the raid level must be
+ # set for raidDiskPool.
+ criteria_min_usable_capacity: # Default minimum required capacity for storage pools.
+ criteria_drive_type: # Default drive type for storage pools. Choices: hdd, ssd
+ criteria_size_unit: # Default unit size for all storage pool related sizing. Choices: bytes, b, kb, mb, gb, tb, pb, eb, zb, yb
+ criteria_drive_min_size: # Default minimum drive size for storage pools.
+ criteria_drive_require_da: # Default for whether storage pools are required to have data assurance (DA) compatible drives. Choices: true, false
+ criteria_drive_require_fde: # Default for whether storage pools are required to have drive security compatible drives. Choices: true, false
+ remove_volumes: # Default policy for deleting volumes prior to removing storage pools.
+ erase_secured_drives: # Default policy for erasing the content drives during create and delete storage pool operations. Choices: true, false
+ common_volume_configuration: # Any option that can be specified at the volume level can be generalized here at the storage pool level. This is useful when
+ # all volumes share common configuration definitions.
+ volumes: # List of volumes associated the storage pool.
+ - state: # Specifies whether the volume should exist (present, absent)
+ name: # (required) Name or name scheme (see above) for the volume(s) to be created in the storage pool(s)
+ host: # host or host group for the volume should be mapped to.
+ host_type: # Only required when using something other than Linux kernel 3.10 or later with DM-MP (Linux DM-MP),
+ # non-clustered Windows (Windows), or the storage system default host type is incorrect. Common definitions below:
+ # - AIX MPIO: The Advanced Interactive Executive (AIX) OS and the native MPIO driver
+ # - AVT 4M: Silicon Graphics, Inc. (SGI) proprietary multipath driver; refer to the SGI installation documentation for more information
+ # - HP-UX: The HP-UX OS with native multipath driver
+ # - Linux ATTO: The Linux OS and the ATTO Technology, Inc. driver (must use ATTO FC HBAs)
+ # - Linux DM-MP: The Linux OS and the native DM-MP driver
+ # - Linux Pathmanager: The Linux OS and the SGI proprietary multipath driver; refer to the SGI installation documentation for more information
+ # - Mac: The Mac OS and the ATTO Technology, Inc. driver
+ # - ONTAP: FlexArray
+ # - Solaris 11 or later: The Solaris 11 or later OS and the native MPxIO driver
+ # - Solaris 10 or earlier: The Solaris 10 or earlier OS and the native MPxIO driver
+ # - SVC: IBM SAN Volume Controller
+ # - VMware: ESXi OS
+ # - Windows: Windows Server OS and Windows MPIO with a DSM driver
+ # - Windows Clustered: Clustered Windows Server OS and Windows MPIO with a DSM driver
+ # - Windows ATTO: Windows OS and the ATTO Technology, Inc. driver
+ size: # Size of the volume or presented size of the thinly provisioned volume.
+ size_unit: # Unit size for the size, thin_volume_repo_size, and thin_volume_max_repo_size
+ # Choices: bytes, b, kb, mb, gb, tb, pb, eb, zb, yb
+ segment_size_kb: # Indicates the amount of data stored on a drive before moving on to the next drive in the volume group. Does not apply to pool volumes.
+ thin_provision: # Whether volumes should be thinly provisioned.
+ thin_volume_repo_size: # Actually allocated space for thinly provisioned volumes.
+ thin_volume_max_repo_size: # Maximum allocated space allowed for thinly provisioned volumes.
+ thin_volume_expansion_policy: # Thin volume expansion policy. Choices: automatic, manual
+ thin_volume_growth_alert_threshold: # Thin volume growth alert threshold; this is the threshold for when the thin volume expansion
+ # policy will be enacted. Allowable values are between and including 10% and 99%
+ ssd_cache_enabled: # Enables ssd cache which will enable the volume to use an existing SSD cache on the storage array.
+ data_assurance_enabled: # Enables whether data assurance(DA) is required to be enabled.
+ read_cache_enable: # Enables read caching which will cache all read requests.
+ read_ahead_enable: # Enables read ahead caching; this is good for sequential workloads to cache subsequent blocks.
+ write_cache_enable: # Enables write caching which will cache all writes.
+ workload_name: # Name of the volume's workload. This can be defined using the metadata option or, if already defined, specify one already
+ # created on the storage array.
+ workload_metadata: # Dictionary containing arbitrary entries normally used for defining the volume(s) workload.
+ volume_metadata # Dictionary containing arbitrary entries used to define information about the volume itself.
+ # Note: format_type, format_options[0-9]?, mount_directory, mount_options[0-9]? are used by netapp_eseries.host.mount role to format and mount volumes.
+ wait_for_initialization: # Whether volume creation with wait for initialization to complete
+
+ # Initiator-Target Protocol Variable Defaults
+ Note that the following commands need to produce a unique list of IQNs or WWNs of the interfaces used, line separated. Overwrite as necessary.
+ eseries_initiator_protocol: fc # This variable defines which protocol the storage array will use. Choices: fc, iscsi, sas, ib_iser, ib_srp, nvme_ib, nvme_roce
+ eseries_initiator_command:
+ fc:
+ linux: "systool -c fc_host -v | grep port_name | cut -d '\"' -f 2 | cut -d 'x' -f 2 | sort | uniq"
+ windows: "(Get-InitiatorPort | Where-Object -P ConnectionType -EQ 'Fibre Channel' | Select-Object -Property PortAddress |
+ Format-Table -AutoSize -HideTableHeaders | Out-String).trim()"
+ iscsi:
+ linux: "grep -o iqn.* /etc/iscsi/initiatorname.iscsi"
+ windows: "(get-initiatorPort | select-object -property nodeaddress | sort-object | get-unique | ft -autoSize | out-string -stream |
+ select-string iqn | out-string).trim()"
+ sas:
+ # NetApp IMT for SAS attached E-Series SAN hosts recommends adding all possible SAS addresses with the base address
+ # starting at 0, and the last address ending in 3 for single port HBAs, or 7 for dual port HBAs. Since determining
+ # single vs . dual port HBAs adds complexity, we always add all 8 possible permutations of the SAS address.
+ linux: "systool -c scsi_host -v | grep host_sas_address | cut -d '\"' -f 2 | cut -d 'x' -f 2 | sort | uniq"
+ windows: "(Get-InitiatorPort | Where-Object -P ConnectionType -EQ 'SAS' | Select-Object -Property PortAddress |
+ Format-Table -AutoSize -HideTableHeaders | Out-String).trim()"
+ ib_iser:
+ linux: "grep -o iqn.* /etc/iscsi/initiatorname.iscsi"
+ windows: "" # add windows command for determining host iqn address(es)
+ ib_srp:
+ linux: "ibstat -p"
+ windows: "" # Add Windows command for determining host guid
+ nvme_ib:
+ linux: "" # Add Linux command for determining host nqn address(es)
+ windows: "" # Add Windows command for determining host nqn address(es)
+ nvme_roce:
+ linux: "" # Add Linux command for determining host nqn address(es)
+ windows: "" # Add Windows command for determining host nqn address(es)
+
+ # Manual host definitions, Linux and Windows systems can be automatically populated based on host mappings found in eseries_storage_pool_configuration
+ eseries_host_force_port: true # Default for whether ports are to be allowed to be re-assigned (boolean)
+ eseries_host_remove_unused_hostgroup: true # Forces any unused groups to be removed
+ eseries_host_object:
+ - name: # Host label as referenced by the storage array.
+ state: # Specifies whether host definition should be exist. Choices: present, absent
+ ports: # List of port definitions
+ - type: # Port protocol definition (iscsi, fc, sas, ib, nvme). Note that you should use 'iscsi' prior to Santricity version 11.60 for IB iSER.
+ label: # Arbitrary port label
+ port: # Port initiator (iqn, wwn, etc)
+ group: # Host's host group
+ host_type: # Only required when using something other than Linux kernel 3.10 or later with DM-MP (Linux DM-MP),
+ # non-clustered Windows (Windows), or the storage system default host type is incorrect. Common definitions below:
+ # - AIX MPIO: The Advanced Interactive Executive (AIX) OS and the native MPIO driver
+ # - AVT 4M: Silicon Graphics, Inc. (SGI) proprietary multipath driver; refer to the SGI installation documentation for more information
+ # - HP-UX: The HP-UX OS with native multipath driver
+ # - Linux ATTO: The Linux OS and the ATTO Technology, Inc. driver (must use ATTO FC HBAs)
+ # - Linux DM-MP: The Linux OS and the native DM-MP driver
+ # - Linux Pathmanager: The Linux OS and the SGI proprietary multipath driver; refer to the SGI installation documentation for more information
+ # - Mac: The Mac OS and the ATTO Technology, Inc. driver
+ # - ONTAP: FlexArray
+ # - Solaris 11 or later: The Solaris 11 or later OS and the native MPxIO driver
+ # - Solaris 10 or earlier: The Solaris 10 or earlier OS and the native MPxIO driver
+ # - SVC: IBM SAN Volume Controller
+ # - VMware: ESXi OS
+ # - Windows: Windows Server OS and Windows MPIO with a DSM driver
+ # - Windows Clustered: Clustered Windows Server OS and Windows MPIO with a DSM driver
+ # - Windows ATTO: Windows OS and the ATTO Technology, Inc. driver
+
+License
+-------
+ BSD-3-Clause
+
+Author Information
+------------------
+ Nathan Swartz (@ndswartz)
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/defaults/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/defaults/main.yml
new file mode 100644
index 00000000..5dd8cbcc
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/defaults/main.yml
@@ -0,0 +1,294 @@
+# Web Services Embedded information
+#eseries_subnet: # Network subnet to search for the storage system specified in CIDR form. Example: 192.168.1.0/24
+#eseries_system_serial: # Storage system serial number. Be sure to quote if the serial is all numbers and begins with zero. (This is located
+ # on a label at the top-left towards the front on the device)
+#eseries_system_addresses: # Storage system management IP addresses. Only required when eseries_system_serial or eseries_system_api_url are not
+ # defined. When not specified, addresses will be populated with eseries_management_interfaces controller addresses.
+#eseries_system_api_url: # Url for the storage system's for embedded web services rest api. Example: https://192.168.10.100/devmgr/v2
+#eseries_system_username: # Username for the storage system's for embedded web services rest api
+#eseries_system_password: # Password for the storage system's for embedded web services rest api and when the admin password has not been set
+ # eseries_system_password will be used to set it.
+#eseries_proxy_ssid: # Arbitrary string for the proxy to represent the storage system. eseries_system_serial will be used when not defined.
+#eseries_template_api_url: # Template for the web services api url. Default: https://0.0.0.0:8443/devmgr/v2/
+#eseries_validate_certs: # Indicates Whether SSL certificates should be verified. Used for both embedded and proxy. Choices: true, false
+
+# Web Services Proxy information
+# Note: eseries_proxy_* variables are required to discover storage systems prior to SANtricity OS version 11.60.2.
+#eseries_proxy_api_url: # Url for the storage system's for proxy web services rest api. Example: https://192.168.10.100/devmgr/v2
+#eseries_proxy_api_username: # Username for the storage system's for proxy web services rest api.
+#eseries_proxy_api_password: # Password for the storage system's for proxy web services rest api and when the admin password has not been set
+ # eseries_proxy_api_password will be used to set it.
+
+
+# Storage Pool Default Policy Specifications
+# ------------------------------------------
+eseries_storage_pool_state: present # Default storage pool state. Choices: present, absent
+eseries_storage_pool_raid_level: raidDiskPool # Default volume raid level. Choices: raid0, raid1, raid5, raid6, raidDiskPool
+eseries_storage_pool_secure_pool: false # Default for storage pool drive security. This flag will enable the security at rest feature. There
+ # must be sufficient FDE or FIPS security capable drives. Type: boolean
+#eseries_storage_pool_criteria_drive_count: # Default storage pool drive count.
+#eseries_storage_pool_reserve_drive_count: # Default reserve drive count for drive reconstruction for storage pools using dynamic disk pool and
+ # the raid level must be set for raidDiskPool.
+#eseries_storage_pool_criteria_min_usable_capacity: # Default minimum required capacity for storage pools.
+#eseries_storage_pool_criteria_drive_type: # Default drive type for storage pools. Choices: hdd, ssd
+eseries_storage_pool_criteria_size_unit: gb # Default unit size for all storage pool related sizing.
+ # Choices: bytes, b, kb, mb, gb, tb, pb, eb, zb, yb
+#eseries_storage_pool_criteria_drive_min_size: # Default minimum drive size for storage pools.
+#eseries_storage_pool_criteria_drive_require_da: # Default for whether storage pools are required to have data assurance (DA) compatible drives.
+ # Type: boolean
+#eseries_storage_pool_criteria_drive_require_fde: # Default for whether storage pools are required to have drive security compatible drives.
+ # Type: boolean
+eseries_storage_pool_remove_volumes: True # Default policy for deleting volumes prior to removing storage pools.
+#eseries_storage_pool_erase_secured_drives: # Default policy for erasing the content drives during create and delete storage pool operations.
+ # Type: boolean
+
+# Volume Default Policy Specifications
+# ------------------------------------
+eseries_volume_state: present # Default volume state. Choices: present, absent
+eseries_volume_size_unit: gb # Default unit size for all volume sizing options.
+#eseries_volume_size: # Default volume size or the presented size for thinly provisioned volumes.
+#eseries_volume_data_assurance_enabled: # Default for whether data assurance(DA) is required to be enabled.
+#eseries_volume_segment_size_kb: # Default segment size measured in kib.
+#eseries_volume_read_cache_enable: # Default for read caching which will cache all read requests.
+#eseries_volume_read_ahead_enable: # Default for read ahead caching; this is good for sequential workloads to cache subsequent blocks.
+#eseries_volume_write_cache_enable: # Default for write caching which will cache all writes.
+#eseries_volume_cache_without_batteries: # Default for allowing caching when batteries are not present.
+#eseries_volume_thin_provision: # Default for whether volumes should be thinly provisioned.
+#eseries_volume_thin_volume_repo_size: # Default for actually allocated space for thinly provisioned volumes.
+#eseries_volume_thin_volume_max_repo_size: # Default for the maximum allocated space allowed for thinly provisioned volumes.
+#eseries_volume_thin_volume_expansion_policy: # Default thin volume expansion policy. Choices: automatic, manual
+#eseries_volume_thin_volume_growth_alert_threshold: # Default thin volume growth alert threshold; this is the threshold for when the thin volume expansion
+ # policy will be enacted. Allowable values are between and including 10% and 99%
+#eseries_volume_ssd_cache_enabled: # Default for ssd cache which will enable the volume to use an existing SSD cache on the storage array.
+#eseries_volume_host: # Default host for all volumes; the value can be any host from the Ansible inventory. Any initiator may be
+ # used whether connected or not since the storage array does not require connectivity in order to create
+ # host objects.
+#eseries_volume_workload_name: # Default workload tag name
+#eseries_volume_metadata: # Default metadata
+#eseries_volume_owning_controller # Default preferred owning controller
+eseries_volume_wait_for_initialization: false # Default for whether volume creation with wait for initialization to complete
+
+
+# Storage Pool-Volume Mapping Default Policy Specifications
+# ---------------------------------------------------------
+eseries_lun_mapping_state: present # Generally specifies whether a LUN mapping should be present. This is useful when adding a default host for all
+ # volumes. Choices: present, absent
+#eseries_lun_mapping_host: # Default host for all volumes not specifically give a host either in common_volume_configuration or in
+ # eseries_storage_pool_configuration.
+
+
+# Controller iSCSI Interface Port Default Policy Specifications
+# -------------------------------------------------------
+eseries_controller_iscsi_port_state: enabled # Generally specifies whether a controller port definition should be applied Choices: enabled, disabled
+eseries_controller_iscsi_port_config_method: dhcp # General port configuration method definition for both controllers. Choices: static, dhcp
+#eseries_controller_iscsi_port_gateway: # General port IPv4 gateway for both controllers.
+#eseries_controller_iscsi_port_subnet_mask: # General port IPv4 subnet mask for both controllers.
+eseries_controller_iscsi_port_mtu: 9000 # General port maximum transfer units (MTU) for both controllers. Any value greater than 1500 (bytes).
+#eseries_controller_iscsi_port:
+# controller_a: # Controller A port definition.
+# state: # General definitions for all ports on controller A. Any option specified in the ports definition can be
+ # specified here to generalize for all controller A ports. Choices: enabled, disabled
+# config_method: # Port configuration method Choices: static, dhcp
+# address: # Port IPv4 address
+# gateway: # Port IPv4 gateway
+# subnet_mask: # Port IPv4 subnet_mask
+# mtu: # Port IPv4 mtu
+# ports: # List containing ports definitions
+# - state: # Whether the port should be enabled. Choices: enabled, disabled
+# config_method: # Port configuration method Choices: static, dhcp
+# address: # Port IPv4 address
+# gateway: # Port IPv4 gateway
+# subnet_mask: # Port IPv4 subnet_mask
+# controller_b: # Controller B port definition.
+# (...) # Same as controller A but for controller B
+
+
+
+# Controller InfiniBand iSER Interface Channel
+# --------------------------------------------
+#eseries_controller_ib_iser_port:
+# controller_a: # Ordered list of controller A channel address definition.
+# - # Port IPv4 address for channel 1
+# - (...) # So on and so forth
+# controller_b: # Ordered list of controller B channel address definition.
+
+# Controller NVMe over InfiniBand Interface Channel
+# -------------------------------------------------
+#eseries_controller_nvme_ib_port:
+# controller_a: # Ordered list of controller A channel address definition.
+# - # Port IPv4 address for channel 1
+# - (...) # So on and so forth
+# controller_b: # Ordered list of controller B channel address definition.
+
+# Controller NVMe RoCE Interface Port Default Policy Specifications
+# -------------------------------------------------------
+eseries_controller_nvme_roce_port_state: enabled # Generally specifies whether a controller port definition should be applied Choices: enabled, disabled
+eseries_controller_nvme_roce_port_config_method: dhcp # General port configuration method definition for both controllers. Choices: static, dhcp
+#eseries_controller_nvme_roce_port_gateway: # General port IPv4 gateway for both controllers.
+#eseries_controller_nvme_roce_port_subnet_mask: # General port IPv4 subnet mask for both controllers.
+eseries_controller_nvme_roce_port_mtu: 9000 # General port maximum transfer units (MTU). Any value greater than 1500 (bytes).
+eseries_controller_nvme_roce_port_speed: auto # General interface speed. Value must be a supported speed or auto for automatically negotiating the speed with the port.
+#eseries_controller_nvme_roce_port:
+# controller_a: # Controller A port definition.
+# state: # General definitions for all ports on controller A. Any option specified in the ports definition can be
+ # specified here to generalize for all controller A ports.
+# config_method: # Port configuration method Choices: static, dhcp
+# address: # Port IPv4 address
+# gateway: # Port IPv4 gateway
+# subnet_mask: # Port IPv4 subnet_mask
+# mtu: # Port IPv4 mtu
+# speed: # Port IPv4 speed
+# ports: # List containing ports definitions
+# - channel: # Channel of the port to modify. This will be a numerical value that represents the port; typically read
+ # left to right on the HIC.
+# state: # Whether the port should be enabled.
+# config_method: # Port configuration method Choices: static, dhcp
+# address: # Port IPv4 address
+# gateway: # Port IPv4 gateway
+# subnet_mask: # Port IPv4 subnet_mask
+# controller_b: # Controller B port definition.
+# state: # General definitions for all ports on controller B. Any option specified in the ports definition can be
+ # specified here to generalize for all controller A ports.
+# config_method: # Port configuration method Choices: static, dhcp
+# address: # Port IPv4 address
+# gateway: # Port IPv4 gateway
+# subnet_mask: # Port IPv4 subnet_mask
+# mtu: # Port IPv4 mtu
+# speed: # Port IPv4 speed
+# ports: # List containing ports definitions
+# - channel: # Channel of the port to modify. This will be a numerical value that represents the port; typically read
+ # left to right on the HIC.
+# state: # Whether the port should be enabled.
+# config_method: # Port configuration method Choices: static, dhcp
+# address: # Port IPv4 address
+# gateway: # Port IPv4 gateway
+# subnet_mask: # Port IPv4 subnet_mask
+
+
+# Target Discovery Default Policy Specifications
+# ----------------------------------------------
+#eseries_target_name: # iSCSI target name
+eseries_target_ping: True # Enables ICMP ping response from the configured iSCSI ports (boolean)
+eseries_target_unnamed_discovery: True # Whether the iSCSI target iqn should be returned when an initiator performs a discovery session.
+eseries_target_chap_secret: # iSCSI chap secret. When left blank, the chap secret will be removed from the storage system.
+eseries_target_chap_secret_update: False # DO NOT REMOVE! Since netapp_e_iscsi_target cannot compare the chap secret with the current and chooses to always
+ # return changed=True, this flag is used to force the module to update the chap secret. It is preferable to
+ # leave this value False and to add the --extra-vars "eseries_target_chap_secret_update=True".
+
+# Host Default Policy Specifications
+# ----------------------------------
+eseries_host_force_port: True # Default for whether ports are to be allowed to be re-assigned (boolean)
+eseries_host_remove_unused_hostgroup: True # Forces any unused groups to be removed
+#eseries_host_object:
+# - name: # Host label as referenced by the storage array.
+# state:
+# host_type: # Windows (non-clustering) 1, Windows (clustering) 6, Vmware 10, Linux (using kernal 3.10 and later) 28
+# group: # Host's host group
+# ports: # List of port definitions
+# - type: # Port protocol definition (iscsi, fc, sas, ib, nvme)
+# label: # Arbitrary port label
+# port: # Port initiator (iqn, wwn, etc)
+
+
+# Storage Pool-Volume Default Policy Specifications
+# -------------------------------------------------
+# Name schemes: Storage pool and volume names can be used to specify a naming scheme to produce a list of storage pools and volumes. The scheme are defined by
+# brackets and can be used to specify a range of lowercase letters, uppercase letters, range of single digit numbers, any top-level inventory
+# variables, and the current defined storage pool (volume only).
+
+#eseries_storage_pool_configuration:
+# - name: # Name or name scheme (see above) for the storage pool.
+# state: # Specifies whether the storage pool should exist (present, absent). When removing an existing storage array all of the
+ # volumes must be defined with state=absent.
+# raid_level # Volume group raid level. Choices: raid0, raid1, raid5, raid6, raidDiskPool (Default: raidDiskPool)
+# secure_pool: no # Default for storage pool drive security. This flag will enable the security at rest feature. There must be sufficient FDE
+ # or FIPS security capable drives. Type: boolean
+# criteria_drive_count: # Default storage pool drive count.
+# reserve_drive_count: # Default reserve drive count for drive reconstruction for storage pools using dynamic disk pool and the raid level must be
+ # set for raidDiskPool.
+# criteria_min_usable_capacity: # Default minimum required capacity for storage pools.
+# criteria_drive_type: # Default drive type for storage pools. Choices: hdd, ssd
+# criteria_size_unit: gb # Default unit size for all storage pool related sizing. Choices: bytes, b, kb, mb, gb, tb, pb, eb, zb, yb
+# criteria_drive_min_size: # Default minimum drive size for storage pools.
+# criteria_drive_require_da: # Default for whether storage pools are required to have data assurance (DA) compatible drives. Type: boolean
+# criteria_drive_require_fde: # Default for whether storage pools are required to have drive security compatible drives. Type: boolean
+# remove_volumes: # Default policy for deleting volumes prior to removing storage pools.
+# erase_secured_drives: # Default policy for erasing the content drives during create and delete storage pool operations. Type: boolean
+# common_volume_configuration: # Any option that can be specified at the volume level can be generalized here at the storage pool level. This is useful when
+ # all volumes share common configuration definitions.
+# volumes: # List of volumes associated the storage pool.
+# - state: # Specifies whether the volume should exist (present, absent)
+# name: # (required) Name or name scheme (see above) for the volume(s) to be created in the storage pool(s)
+# host: # host or host group for the volume should be mapped to.
+# host_type: # Only required when using something other than Linux kernel 3.10 or later with DM-MP (Linux DM-MP),
+ # non-clustered Windows (Windows), or the storage system default host type is incorrect. Common definitions below:
+ # - AIX MPIO: The Advanced Interactive Executive (AIX) OS and the native MPIO driver
+ # - AVT 4M: Silicon Graphics, Inc. (SGI) proprietary multipath driver; refer to the SGI installation documentation for more information
+ # - HP-UX: The HP-UX OS with native multipath driver
+ # - Linux ATTO: The Linux OS and the ATTO Technology, Inc. driver (must use ATTO FC HBAs)
+ # - Linux DM-MP: The Linux OS and the native DM-MP driver
+ # - Linux Pathmanager: The Linux OS and the SGI proprietary multipath driver; refer to the SGI installation documentation for more information
+ # - Mac: The Mac OS and the ATTO Technology, Inc. driver
+ # - ONTAP: FlexArray
+ # - Solaris 11 or later: The Solaris 11 or later OS and the native MPxIO driver
+ # - Solaris 10 or earlier: The Solaris 10 or earlier OS and the native MPxIO driver
+ # - SVC: IBM SAN Volume Controller
+ # - VMware: ESXi OS
+ # - Windows: Windows Server OS and Windows MPIO with a DSM driver
+ # - Windows Clustered: Clustered Windows Server OS and Windows MPIO with a DSM driver
+ # - Windows ATTO: Windows OS and the ATTO Technology, Inc. driver
+# size: # Size of the volume or presented size of the thinly provisioned volume.
+# size_unit: # Unit size for the size, thin_volume_repo_size, and thin_volume_max_repo_size
+ # Choices: bytes, b, kb, mb, gb, tb, pb, eb, zb, yb
+# segment_size_kb: # Indicates the amount of data stored on a drive before moving on to the next drive in the volume group. Does not apply to pool volumes.
+# thin_provision: # Whether volumes should be thinly provisioned.
+# thin_volume_repo_size: # Actually allocated space for thinly provisioned volumes.
+# thin_volume_max_repo_size: # Maximum allocated space allowed for thinly provisioned volumes.
+# thin_volume_expansion_policy: # Thin volume expansion policy. Choices: automatic, manual
+# thin_volume_growth_alert_threshold: # Thin volume growth alert threshold; this is the threshold for when the thin volume expansion
+ # policy will be enacted. Allowable values are between and including 10% and 99%
+# ssd_cache_enabled: # Enables ssd cache which will enable the volume to use an existing SSD cache on the storage array.
+# data_assurance_enabled: # Enables whether data assurance(DA) is required to be enabled.
+# read_cache_enable: # Enables read caching which will cache all read requests.
+# read_ahead_enable: # Enables read ahead caching; this is good for sequential workloads to cache subsequent blocks.
+# write_cache_enable: # Enables write caching which will cache all writes.
+# workload_name: # Name of the volume's workload. This can be defined using the metadata option or, if already defined, specify one already
+ # created on the storage array.
+# metadata: # Dictionary containing arbitrary entries normally used for defining the volume(s) workload.
+# wait_for_initialization: # Whether volume creation with wait for initialization to complete
+
+
+# Initiator-Target Protocol Variable Defaults
+# -------------------------------------------
+# Note that the following commands produce a unique list of IQNs or WWNs of the interfaces used, line separated.
+eseries_initiator_protocol: # This variable defines which protocol the storage array will use. Choices: iscsi, fc, sas, ib_iser, ib_srp, nvme_ib, nvme_roce
+eseries_initiator_command:
+ fc:
+ linux: "systool -c fc_host -v | grep port_name | cut -d '\"' -f 2 | cut -d 'x' -f 2 | sort | uniq"
+ windows: "(Get-InitiatorPort | Where-Object -P ConnectionType -EQ 'Fibre Channel' | Select-Object -Property PortAddress |
+ Format-Table -AutoSize -HideTableHeaders | Out-String).trim()"
+ iscsi:
+ linux: "grep -o iqn.* /etc/iscsi/initiatorname.iscsi"
+ windows: "(get-initiatorPort | select-object -property nodeaddress | sort-object | get-unique | ft -autoSize | out-string -stream |
+ select-string iqn | out-string).trim()"
+ sas:
+ # NetApp IMT for SAS attached E-Series SAN hosts recommends adding all possible SAS addresses with the base address
+ # starting at 0, and the last address ending in 3 for single port HBAs, or 7 for dual port HBAs. Since determining
+ # single vs . dual port HBAs adds complexity, we always add all 8 possible permutations of the SAS address.
+ linux: "systool -c scsi_host -v | grep host_sas_address | cut -d '\"' -f 2 | cut -d 'x' -f 2 | sort | uniq"
+ windows: "(Get-InitiatorPort | Where-Object -P ConnectionType -EQ 'SAS' | Select-Object -Property PortAddress |
+ Format-Table -AutoSize -HideTableHeaders | Out-String).trim()"
+ ib_iser:
+ linux: "grep -o iqn.* /etc/iscsi/initiatorname.iscsi"
+ windows: "" # add windows command for determining host iqn address(es)
+ ib_srp:
+ linux: "ibstat -p"
+ windows: "" # add windows command for determining host guid
+ nvme_ib:
+ linux: "cat /etc/nvme/hostnqn"
+ windows: "" # add windows command for determining host nqn address(es)
+ nvme_roce:
+ linux: "cat /etc/nvme/hostnqn"
+ windows: "" # add windows command for determining host nqn address(es)
+
+eseries_remove_all_configuration: False # WARNING!!! This flag will force any storage pool, volume, host, hostgroup and mapping to be absent. Choices: True, False \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/meta/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/meta/main.yml
new file mode 100644
index 00000000..78685845
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/meta/main.yml
@@ -0,0 +1,19 @@
+galaxy_info:
+ author: Nathan Swartz (@ndswartz)
+ description: Manages NetApp E-Series storage system's interfaces, storage pools, volumes, hosts, hostgroups, and volume mappings.
+ company: NetApp, Inc
+ license: BSD-3 Clause
+ platforms:
+ min_ansible_version: 2.9
+ galaxy_tags:
+ - netapp
+ - eseries
+ - storage
+ - iscsi
+ - sas
+ - fc
+ - infiniband
+ - ib
+ - nvme
+
+dependencies: []
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/initiator.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/initiator.yml
new file mode 100644
index 00000000..7bd3e4d7
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/initiator.yml
@@ -0,0 +1,109 @@
+---
+# netapp_eseries_iscsi/tasks/initiator.yml
+
+- name: Collect facts on the storage array
+ na_santricity_facts:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ register: storage_array_facts
+ when: eseries_host_object is defined
+
+- name: Organize current and expected hostgroups
+ set_fact:
+ current_storage_array_hostgroups: |-
+ {%- set current_hostgroup={} %}
+ {%- for group in storage_array_facts["storage_array_facts"]["netapp_host_groups"] %}
+ {%- if current_hostgroup.update( {group["name"]: []} ) %}{%- endif %}
+ {%- for host in storage_array_facts["storage_array_facts"]["netapp_hosts"] %}
+ {%- if group["id"] == host["group_id"] and current_hostgroup[group["name"]].append(host["name"]) %}{%- endif %}
+ {%- endfor %}
+ {%- endfor %}
+ {{ current_hostgroup }}
+ hostgroups: |-
+ {%- set hostgroups=[] %}
+ {%- for host in eseries_host_object %}
+ {%- if "group" in (host.keys()|list) and host["group"] not in hostgroups and
+ ("state" not in (host.keys()|list) or host["state"] == "present") and hostgroups.append(host["group"]) %}
+ {%- endif %}
+ {%- endfor %}
+ {{ hostgroups }}
+ when: eseries_host_object is defined
+
+- name: Configure inventory-defined hostgroup definitions
+ na_santricity_hostgroup:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ state: "{{ eseries_remove_all_configuration_state | default('present') }}"
+ name: "{{ item }}"
+ hosts: "{{ current_storage_array_hostgroups[item] | default(omit) }}"
+ loop: "{{ lookup('list', hostgroups) }}"
+ when: eseries_host_object is defined and (hostgroups | length > 0)
+
+- name: Configure inventory-defined host definitions
+ na_santricity_host:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ state: "{{ eseries_remove_all_configuration_state | default(item['state'] | default(omit)) }}"
+ name: "{{ item['name'] }}"
+ ports: "{{ item['ports'] | default(omit) }}"
+ force_port: "{{ item['force_port'] | default(eseries_host_force_port | default(omit)) }}"
+ group: "{{ item['group'] | default(eseries_host_group | default(omit)) }}"
+ host_type: "{{ item['host_type_index'] | default(item['host_type'] | default(eseries_host_type_index | default(omit))) }}"
+ loop: "{{ lookup('list', eseries_host_object) }}"
+ when: eseries_host_object is defined
+
+- name: Collect facts on the storage array
+ na_santricity_facts:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ register: storage_array_facts
+ when: eseries_host_object is defined
+
+- name: Update the current storage array hostgroups
+ set_fact:
+ current_storage_array_hostgroups: |-
+ {%- set current_hostgroup={} %}
+ {%- for group in storage_array_facts["storage_array_facts"]["netapp_host_groups"] %}
+ {%- if current_hostgroup.update( {group["name"]: []} ) %}{%- endif %}
+ {%- for host in storage_array_facts["storage_array_facts"]["netapp_hosts"] %}
+ {%- if group["id"] == host["group_id"] and current_hostgroup[group["name"]].append(host["name"]) %}{%- endif %}
+ {%- endfor %}
+ {%- endfor %}
+ {{ current_hostgroup }}
+ when: eseries_host_object is defined
+
+- name: Create list of unused hostgroups
+ set_fact:
+ unused_hostgroups: |-
+ {%- set unused_hostgroup = [] %}
+ {%- for current_host_name in (current_storage_array_hostgroups.keys()|list) %}
+ {%- if (current_storage_array_hostgroups[current_host_name]|length==0) and unused_hostgroup.append(current_host_name) %}{%- endif %}
+ {%- endfor %}
+ {{ unused_hostgroup }}
+ when: eseries_host_object is defined
+
+- name: Configure inventory-defined hostgroup definitions
+ na_santricity_hostgroup:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ state: absent
+ name: "{{ item }}"
+ hosts: "{{ current_storage_array_hostgroups[item] | default(omit) }}"
+ loop: "{{ lookup('list', unused_hostgroups) }}"
+ when: "eseries_host_object is defined and (unused_hostgroups|length>0) and eseries_host_remove_unused_hostgroup is
+ defined and eseries_host_remove_unused_hostgroup"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface.yml
new file mode 100644
index 00000000..9139e6ff
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface.yml
@@ -0,0 +1,27 @@
+- name: Configure controllers iSCSI interface ports
+ import_tasks: interface/iscsi.yml
+ delegate_to: localhost
+ when: eseries_initiator_protocol == "iscsi"
+ tags:
+ - iscsi
+
+- name: Configure controllers InfiniBand iSER interface ports
+ import_tasks: interface/ib_iser.yml
+ delegate_to: localhost
+ when: eseries_initiator_protocol == "ib_iser"
+ tags:
+ - ib_iser
+
+- name: Configure controllers NVMe interface ports over InfiniBand
+ import_tasks: interface/nvme_ib.yml
+ delegate_to: localhost
+ when: eseries_initiator_protocol == "nvme_ib"
+ tags:
+ - nvme_ib
+
+- name: Configure controllers NVMe interface ports on RoCE
+ import_tasks: interface/nvme_roce.yml
+ delegate_to: localhost
+ when: eseries_initiator_protocol == "nvme_roce"
+ tags:
+ - nvme_roce
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface/ib_iser.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface/ib_iser.yml
new file mode 100644
index 00000000..eece8972
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface/ib_iser.yml
@@ -0,0 +1,29 @@
+- name: Configure controller A inventory-defined controller port definitions for InfiniBand iSER
+ na_santricity_ib_iser_interface:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ controller: A
+ channel: "{{ channel + 1 }}"
+ address: "{{ item }}"
+ loop: "{{ lookup('list', eseries_controller_ib_iser_port['controller_a']) }}"
+ loop_control:
+ index_var: channel
+ when: eseries_controller_ib_iser_port is defined and eseries_controller_ib_iser_port['controller_a'] is defined
+
+- name: Configure controller B inventory-defined controller port definitions for InfiniBand iSER
+ na_santricity_ib_iser_interface:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ controller: B
+ channel: "{{ channel + 1 }}"
+ address: "{{ item }}"
+ loop: "{{ lookup('list', eseries_controller_ib_iser_port['controller_b']) }}"
+ loop_control:
+ index_var: channel
+ when: eseries_controller_ib_iser_port is defined and eseries_controller_ib_iser_port['controller_b'] is defined
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface/iscsi.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface/iscsi.yml
new file mode 100644
index 00000000..733dfcba
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface/iscsi.yml
@@ -0,0 +1,54 @@
+- name: Configure controller A inventory-defined controller port definitions for iSCSI
+ na_santricity_iscsi_interface:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ state: "{{ item['state'] | default(eseries_controller_iscsi_port_state | default(omit)) }}"
+ port: "{{ port + 1 }}"
+ controller: A
+ config_method: "{{ item['config_method'] | default(eseries_controller_iscsi_port_config_method | default(omit)) }}"
+ address: "{{ item['address'] | default(omit) }}"
+ gateway: "{{ item['gateway'] | default(eseries_controller_iscsi_port_gateway | default(omit)) }}"
+ subnet_mask: "{{ item['subnet_mask'] | default(eseries_controller_iscsi_port_subnet_mask | default(omit)) }}"
+ mtu: "{{ item['mtu'] | default(eseries_controller_iscsi_port_mtu | default(omit)) }}"
+ loop: "{{ lookup('list', eseries_controller_iscsi_port['controller_a']) }}"
+ loop_control:
+ index_var: port
+ when: eseries_controller_iscsi_port is defined and eseries_controller_iscsi_port['controller_a'] is defined
+
+- name: Configure controller B inventory-defined controller port definitions for iSCSI
+ na_santricity_iscsi_interface:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ state: "{{ item['state'] | default(eseries_controller_iscsi_port_state | default(omit)) }}"
+ port: "{{ port + 1 }}"
+ controller: B
+ config_method: "{{ item['config_method'] | default(eseries_controller_iscsi_port_config_method | default(omit)) }}"
+ address: "{{ item['address'] | default(omit) }}"
+ gateway: "{{ item['gateway'] | default(eseries_controller_iscsi_port_gateway | default(omit)) }}"
+ subnet_mask: "{{ item['subnet_mask'] | default(eseries_controller_iscsi_port_subnet_mask | default(omit)) }}"
+ mtu: "{{ item['mtu'] | default(eseries_controller_iscsi_port_mtu | default(omit)) }}"
+ loop: "{{ lookup('list', eseries_controller_iscsi_port['controller_b']) }}"
+ loop_control:
+ index_var: port
+ when: eseries_controller_iscsi_port is defined and eseries_controller_iscsi_port['controller_b'] is defined
+
+- name: Configure iSCSI discovery parameters
+ na_santricity_iscsi_target:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ name: "{{ eseries_iscsi_target_name | default(omit) }}"
+ chap_secret: "{%- if eseries_iscsi_target_chap_secret_update %}{{ eseries_iscsi_target_chap_secret }}{%- endif %}"
+ ping: "{{ eseries_iscsi_target_ping | default(omit) }}"
+ unnamed_discovery: "{{ eseries_iscsi_target_unnamed_discovery | default(omit) }}"
+ when: ((eseries_iscsi_target_chap_secret is defined and eseries_iscsi_target_chap_secret_update) or
+ eseries_iscsi_target_name is defined or eseries_iscsi_target_ping is defined or
+ eseries_iscsi_target_unnamed_discovery is defined)
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface/nvme_ib.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface/nvme_ib.yml
new file mode 100644
index 00000000..d1f33afa
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface/nvme_ib.yml
@@ -0,0 +1,29 @@
+- name: Configure controller A inventory-defined controller port definitions for NVMe interface ports over InfiniBand
+ na_santricity_nvme_interface:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ controller: A
+ channel: "{{ channel + 1 }}"
+ address: "{{ item }}"
+ loop: "{{ lookup('list', eseries_controller_nvme_ib_port['controller_a']) }}"
+ loop_control:
+ index_var: channel
+ when: eseries_controller_nvme_ib_port is defined and eseries_controller_nvme_ib_port['controller_a'] is defined
+
+- name: Configure controller B inventory-defined controller port definitions for NVMe interface ports over InfiniBand
+ na_santricity_nvme_interface:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ controller: B
+ channel: "{{ channel + 1 }}"
+ address: "{{ item }}"
+ loop: "{{ lookup('list', eseries_controller_nvme_ib_port['controller_b']) }}"
+ loop_control:
+ index_var: channel
+ when: eseries_controller_nvme_ib_port is defined and eseries_controller_nvme_ib_port['controller_b'] is defined
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface/nvme_roce.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface/nvme_roce.yml
new file mode 100644
index 00000000..8dd711fe
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/interface/nvme_roce.yml
@@ -0,0 +1,41 @@
+- name: Configure controller A inventory-defined controller port definitions for NVMe interface ports on RoCE
+ na_santricity_nvme_interface:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ state: "{{ item['state'] | default(eseries_controller_nvme_roce_port_state | default(omit)) }}"
+ controller: A
+ channel: "{{ channel + 1 }}"
+ config_method: "{{ item['config_method'] | default(eseries_controller_nvme_roce_port_config_method | default(omit)) }}"
+ address: "{{ item['address'] | default(omit) }}"
+ gateway: "{{ item['gateway'] | default(eseries_controller_nvme_roce_port_gateway | default(omit)) }}"
+ subnet_mask: "{{ item['subnet_mask'] | default(eseries_controller_nvme_roce_port_subnet_mask | default(omit)) }}"
+ mtu: "{{ item['mtu'] | default(eseries_controller_nvme_roce_port_mtu | default(omit)) }}"
+ speed: "{{ item['speed'] | default(eseries_controller_nvme_roce_port_speed | default(omit)) }}"
+ loop: "{{ lookup('list', eseries_controller_nvme_roce_port['controller_a']) }}"
+ loop_control:
+ index_var: channel
+ when: eseries_controller_nvme_roce_port is defined and eseries_controller_nvme_roce_port['controller_a'] is defined
+
+- name: Configure controller B inventory-defined controller port definitions for NVMe interface ports on RoCE
+ na_santricity_nvme_interface:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ state: "{{ item['state'] | default(eseries_controller_nvme_roce_port_state | default(omit)) }}"
+ controller: B
+ channel: "{{ channel + 1 }}"
+ config_method: "{{ item['config_method'] | default(eseries_controller_nvme_roce_port_config_method | default(omit)) }}"
+ address: "{{ item['address'] | default(omit) }}"
+ gateway: "{{ item['gateway'] | default(eseries_controller_nvme_roce_port_gateway | default(omit)) }}"
+ subnet_mask: "{{ item['subnet_mask'] | default(eseries_controller_nvme_roce_port_subnet_mask | default(omit)) }}"
+ mtu: "{{ item['mtu'] | default(eseries_controller_nvme_roce_port_mtu | default(omit)) }}"
+ speed: "{{ item['speed'] | default(eseries_controller_nvme_roce_port_speed | default(omit)) }}"
+ loop: "{{ lookup('list', eseries_controller_nvme_roce_port['controller_b']) }}"
+ loop_control:
+ index_var: channel
+ when: eseries_controller_nvme_roce_port is defined and eseries_controller_nvme_roce_port['controller_b'] is defined
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/lun_mapping.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/lun_mapping.yml
new file mode 100644
index 00000000..9db1c05b
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/lun_mapping.yml
@@ -0,0 +1,119 @@
+- name: Collect facts on the storage array
+ na_santricity_facts:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ register: storage_array_facts
+
+- name: Collect volume host and host group list
+ set_fact:
+ hosts: "{{ lookup('netapp_eseries.santricity.santricity_host', hostvars[inventory_hostname]) }}"
+
+- name: Retrieve required details and map luns
+ block:
+ - name: Collect host/host group networking information
+ setup:
+ gather_subset: min
+ register: hosts_info
+ delegate_to: "{{ item }}"
+ delegate_facts: True
+ loop: "{{ lookup('list', (hosts['expected_hosts'].keys() | list)) }}"
+
+ - name: Collect Ansible defined host and host group interface information
+ set_fact:
+ host_interface_ansible: |-
+ {%- set host_interfaces = [] -%}
+ {%- for host in (hosts['expected_hosts'].keys() | list) -%}
+ {%- if "eseries_iscsi_iqn" in (hostvars[host].keys() | list) -%}
+ {%- if host_interfaces.append({"item": host, "stdout_lines": [hostvars[host]["eseries_iscsi_iqn"]]}) -%}{%- endif -%}
+ {%- endif -%}
+ {%- endfor -%}
+ {{ host_interfaces }}
+
+ - name: Collect host and host group interface information for Linux
+ shell: "{{ eseries_initiator_command[eseries_initiator_protocol]['linux'] }}"
+ register: host_interface_linux
+ delegate_to: "{{ item }}"
+ become: True
+ changed_when: False
+ failed_when: "host_interface_linux.rc != 0"
+ when: 'eseries_initiator_protocol in ["iscsi", "ib_iser"] and "eseries_iscsi_iqn" not in (hostvars[item].keys() | list) and hostvars[item]["ansible_facts"]["system"] == "Linux"'
+ loop: "{{ lookup('list', (hosts['expected_hosts'].keys() | list)) }}"
+
+ - name: Collect host and host group interface information for Windows
+ win_shell: "{{ eseries_initiator_command[eseries_initiator_protocol]['windows'] }}"
+ register: host_interface_windows
+ delegate_to: "{{ item }}"
+ changed_when: False
+ failed_when: "host_interface_windows.rc != 0"
+ when: 'eseries_initiator_protocol in ["iscsi", "ib_iser"] and "eseries_iscsi_iqn" not in (hostvars[item].keys() | list) and hostvars[item]["ansible_facts"]["os_family"] == "Windows"'
+ loop: "{{ lookup('list', (hosts['expected_hosts'].keys() | list)) }}"
+
+ - name: Aggregate host/host group interface information
+ set_fact:
+ host_interface: |-
+ {%- set host_interfaces = [] -%}
+ {%- for interface in host_interface_ansible + host_interface_linux['results'] + host_interface_windows['results'] -%}
+ {%- if "skipped" not in (interface.keys() | list) or interface["skipped"] == False -%}
+ {%- if host_interfaces.append(interface) -%}{%- endif -%}
+ {%- endif -%}
+ {%- endfor -%}
+ {{- host_interfaces -}}
+
+ - name: Organize host/host group interface information and update hosts variable
+ set_fact:
+ hosts: "{{ lookup('netapp_eseries.santricity.santricity_host_detail',
+ hosts, hosts_info=hosts_info['results'], host_interface_ports=host_interface, protocol=eseries_initiator_protocol) }}"
+
+ - name: Create all hosts objects on the storage array
+ na_santricity_host:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ state: "{{ eseries_remove_all_configuration_state | default(hosts['expected_hosts'][item]['state'] | default(omit)) }}"
+ name: "{{ hosts['expected_hosts'][item]['sanitized_hostname'] }}"
+ ports: "{{ hosts['expected_hosts'][item]['ports'] }}"
+ force_port: "{{ eseries_host_force_port | default(omit) }}"
+ host_type: "{{ hosts['expected_hosts'][item]['host_type'] | default(omit) }}"
+ loop: "{{ lookup('list', (hosts['expected_hosts'].keys()|list)) }}"
+
+ - name: Create all required host groups on the storage array
+ na_santricity_hostgroup:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ state: "{{ eseries_remove_all_configuration_state | default('present') }}"
+ name: "{{ item }}"
+ hosts: "{{ hosts['host_groups'][item] | default(omit) }}"
+ loop: "{{ lookup('list', (hosts['host_groups'].keys()|list)) }}"
+
+ - name: Update facts on the storage array
+ na_santricity_facts:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ register: storage_array_facts
+ when: eseries_remove_all_configuration_state is not defined or eseries_remove_all_configuration_state == False
+
+ - name: Map hosts to there respective volume
+ na_santricity_lun_mapping:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ state: present
+ volume: "{{ item['volume'] }}"
+ target: "{{ item['target'] | default(eseries_lun_mapping_host) }}"
+ lun: "{{ item['lun'] }}"
+ loop: "{{ lookup('netapp_eseries.santricity.santricity_lun_mapping', storage_array_facts,
+ volumes=lookup('netapp_eseries.santricity.santricity_volume', hostvars[inventory_hostname]), wantlist=True) }}"
+ when: hosts != []
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/main.yml
new file mode 100644
index 00000000..bea64768
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/main.yml
@@ -0,0 +1,50 @@
+- name: Set current storage system credentials
+ include_role:
+ name: netapp_eseries.santricity.nar_santricity_common
+ tasks_from: build_info.yml
+ when: current_eseries_api_url is not defined
+ tags:
+ - always
+
+- name: Override all inventory configuration states.
+ set_fact:
+ eseries_remove_all_configuration_state: absent
+ when: eseries_remove_all_configuration is defined and eseries_remove_all_configuration == True
+ tags:
+ - always
+
+- name: Configure NetApp E-Series storage system disk pool configuration
+ import_tasks: storage_pool_present.yml
+ delegate_to: localhost
+ tags:
+ - storage_pools
+
+- name: Configure NetApp E-Series storage system volume configuration
+ import_tasks: volume.yml
+ delegate_to: localhost
+ tags:
+ - volumes
+
+- name: Configure NetApp E-Series storage system disk pool configuration
+ import_tasks: storage_pool_absent.yml
+ delegate_to: localhost
+ tags:
+ - storage_pools
+
+- name: Configure storage system's hosts/hostgroups initiators
+ import_tasks: initiator.yml
+ delegate_to: localhost
+ tags:
+ - initiators
+
+- name: Map storage system's volumes to host objects
+ import_tasks: lun_mapping.yml
+ delegate_to: localhost
+ tags:
+ - lun_mappings
+
+- name: Configure controllers interfaces
+ import_tasks: interface.yml
+ delegate_to: localhost
+ tags:
+ - interface
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/storage_pool_absent.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/storage_pool_absent.yml
new file mode 100644
index 00000000..d20e5dfe
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/storage_pool_absent.yml
@@ -0,0 +1,22 @@
+- name: Configure NetApp E-Series storage system disk pool configuration
+ na_santricity_storagepool:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ state: "{{ item['state'] | default(eseries_storage_pool_state | default(omit)) }}"
+ name: "{{ item['name'] | default(omit) }}"
+ raid_level: "{{ item['raid_level'] | default(eseries_storage_pool_raid_level | default(omit)) }}"
+ secure_pool: "{{ item['secure_pool'] | default(eseries_storage_pool_secure_pool | default(omit)) }}"
+ criteria_drive_count: "{{ item['criteria_drive_count'] | default(eseries_storage_pool_criteria_drive_count | default(omit)) }}"
+ reserve_drive_count: "{{ item['reserve_drive_count'] | default(eseries_storage_pool_reserve_drive_count | default(omit)) }}"
+ criteria_min_usable_capacity: "{{ item['criteria_min_usable_capacity'] | default(eseries_storage_pool_criteria_min_usable_capacity | default(omit)) }}"
+ criteria_drive_type: "{{ item['criteria_drive_type'] | default(eseries_storage_pool_criteria_drive_type | default(omit)) }}"
+ criteria_size_unit: "{{ item['criteria_size_unit'] | default(eseries_storage_pool_criteria_size_unit | default(omit)) }}"
+ criteria_drive_min_size: "{{ item['criteria_drive_min_size'] | default(eseries_storage_pool_criteria_drive_min_size | default(omit)) }}"
+ criteria_drive_require_da: "{{ item['criteria_drive_require_da'] | default(eseries_storage_pool_criteria_drive_require_da | default(omit)) }}"
+ criteria_drive_require_fde: "{{ item['criteria_drive_require_fde'] | default(eseries_storage_pool_criteria_drive_require_fde | default(omit)) }}"
+ remove_volumes: "{{ item['remove_volumes'] | default(eseries_storage_pool_remove_volumes | default(omit)) }}"
+ erase_secured_drives: "{{ item['erase_secured_drives'] | default(eseries_storage_pool_erase_secured_drives | default(omit)) }}"
+ loop: "{{ query('netapp_eseries.santricity.santricity_storage_pool', hostvars[inventory_hostname], state='absent') }}"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/storage_pool_present.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/storage_pool_present.yml
new file mode 100644
index 00000000..d51e147d
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/storage_pool_present.yml
@@ -0,0 +1,23 @@
+- name: Configure NetApp E-Series storage system disk pool configuration
+ na_santricity_storagepool:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ state: "{{ eseries_remove_all_configuration_state | default(item['state'] | default(eseries_storage_pool_state | default(omit))) }}"
+ name: "{{ item['name'] | default(omit) }}"
+ raid_level: "{{ item['raid_level'] | default(eseries_storage_pool_raid_level | default(omit)) }}"
+ secure_pool: "{{ item['secure_pool'] | default(eseries_storage_pool_secure_pool | default(omit)) }}"
+ criteria_drive_count: "{{ item['criteria_drive_count'] | default(eseries_storage_pool_criteria_drive_count | default(omit)) }}"
+ reserve_drive_count: "{{ item['reserve_drive_count'] | default(eseries_storage_pool_reserve_drive_count | default(omit)) }}"
+ criteria_min_usable_capacity: "{{ item['criteria_min_usable_capacity'] | default(eseries_storage_pool_criteria_min_usable_capacity | default(omit)) }}"
+ criteria_drive_type: "{{ item['criteria_drive_type'] | default(eseries_storage_pool_criteria_drive_type | default(omit)) }}"
+ criteria_size_unit: "{{ item['criteria_size_unit'] | default(eseries_storage_pool_criteria_size_unit | default(omit)) }}"
+ criteria_drive_min_size: "{{ item['criteria_drive_min_size'] | default(eseries_storage_pool_criteria_drive_min_size | default(omit)) }}"
+ criteria_drive_require_da: "{{ item['criteria_drive_require_da'] | default(eseries_storage_pool_criteria_drive_require_da | default(omit)) }}"
+ criteria_drive_require_fde: "{{ item['criteria_drive_require_fde'] | default(eseries_storage_pool_criteria_drive_require_fde | default(omit)) }}"
+ remove_volumes: "{{ item['remove_volumes'] | default(eseries_storage_pool_remove_volumes | default(omit)) }}"
+ erase_secured_drives: "{{ item['erase_secured_drives'] | default(eseries_storage_pool_erase_secured_drives | default(omit)) }}"
+ loop: "{{ query('netapp_eseries.santricity.santricity_storage_pool', hostvars[inventory_hostname], state='present') }}"
+ when: eseries_remove_all_configuration is not defined or eseries_remove_all_configuration == False
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/volume.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/volume.yml
new file mode 100644
index 00000000..ee6d79c1
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_host/tasks/volume.yml
@@ -0,0 +1,31 @@
+- name: Configure Netapp E-Series storage system disk pool volume configuration
+ na_santricity_volume:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ state: "{{ eseries_remove_all_configuration_state | default(item['state'] | default(eseries_volume_state | default('present'))) }}"
+ name: "{{ item['name'] }}"
+ storage_pool_name: "{{ item['storage_pool_name'] }}"
+ size: "{{ item['size'] | default(eseries_volume_size | default(omit)) }}"
+ size_unit: "{{ item['size_unit'] | default(eseries_volume_size_unit | default(omit)) }}"
+ segment_size_kb: "{{ item['segment_size_kb'] | default(eseries_volume_segment_size_kb | default(omit)) }}"
+ owning_controller: "{{ item['owning_controller'] | default(eseries_volume_owning_controller | default(omit)) }}"
+ thin_provision: "{{ item['thin_provision'] | default(eseries_volume_thin_provision | default(omit)) }}"
+ thin_volume_repo_size: "{{ item['thin_volume_repo_size'] | default(eseries_volume_thin_volume_repo_size | default(omit)) }}"
+ thin_volume_max_repo_size: "{{ item['thin_volume_max_repo_size'] | default(eseries_volume_thin_volume_max_repo_size | default(omit)) }}"
+ thin_volume_expansion_policy: "{{ item['thin_volume_expansion_policy'] | default(eseries_volume_thin_volume_expansion_policy | default(omit)) }}"
+ thin_volume_growth_alert_threshold: "{{ item['thin_volume_growth_alert_threshold'] | default(eseries_volume_thin_volume_growth_alert_threshold | default(omit)) }}"
+ ssd_cache_enabled: "{{ item['ssd_cache_enabled'] | default(eseries_volume_ssd_cache_enabled | default(omit)) }}"
+ data_assurance_enabled: "{{ item['data_assurance_enabled'] | default(eseries_volume_data_assurance_enabled | default(omit)) }}"
+ read_cache_enable: "{{ item['read_cache_enable'] | default(eseries_volume_read_cache_enable | default(omit)) }}"
+ read_ahead_enable: "{{ item['read_ahead_enable'] | default(eseries_volume_read_ahead_enable | default(omit)) }}"
+ write_cache_enable: "{{ item['write_cache_enable'] | default(eseries_volume_write_cache_enable | default(omit)) }}"
+ cache_without_batteries: "{{ item['cache_without_batteries'] | default(eseries_volume_cache_without_batteries | default(omit)) }}"
+ wait_for_initialization: "{{ item['wait_for_initialization'] | default(eseries_volume_wait_for_initialization | default(omit)) }}"
+ workload_name: "{{ item['workload_name'] | default(eseries_volume_workload_name | default(omit)) }}"
+ workload_metadata: "{{ item['workload_metadata'] | default(item['metadata'] | default(eseries_volume_workload_metadata | default(eseries_volume_metadata | default(omit)))) }}"
+ volume_metadata: "{{ item['volume_metadata'] | default(eseries_volume_volume_metadata | default(omit)) }}"
+ when: eseries_storage_pool_configuration is defined
+ loop: "{{ query('netapp_eseries.santricity.santricity_volume', hostvars[inventory_hostname]) }}"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/.travis.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/.travis.yml
new file mode 100644
index 00000000..36bbf620
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/.travis.yml
@@ -0,0 +1,29 @@
+---
+language: python
+python: "2.7"
+
+# Use the new container infrastructure
+sudo: false
+
+# Install ansible
+addons:
+ apt:
+ packages:
+ - python-pip
+
+install:
+ # Install ansible
+ - pip install ansible
+
+ # Check ansible version
+ - ansible --version
+
+ # Create ansible.cfg with correct roles_path
+ - printf '[defaults]\nroles_path=../' >ansible.cfg
+
+script:
+ # Basic role syntax check
+ - ansible-playbook tests/test.yml -i tests/inventory --syntax-check
+
+notifications:
+ webhooks: https://galaxy.ansible.com/api/v1/notifications/ \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/README.md b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/README.md
new file mode 100644
index 00000000..f54ae2ee
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/README.md
@@ -0,0 +1,273 @@
+nar_santricity_management
+=========
+ Manages NetApp E-Series storage system's name, passwords, management interfaces, alerts, syslog, auditlog, asup, ldap, certificates, drive firmware and controller firmware.
+
+Requirements
+------------
+ - Ansible 2.9 or later
+ - NetApp E-Series E2800 platform or newer or NetApp E-Series SANtricity Web Services Proxy configured for older E-Series storage systems.
+
+Example Playbook
+----------------
+ - hosts: eseries_storage_systems
+ gather_facts: false
+ collection:
+ - netapp_eseries.santricity
+ tasks:
+ - name: Ensure NetApp E-Series storage system is properly configured
+ import_role:
+ name: nar_santricity_management
+
+Example Storage System Inventory File (Discover storage system with proxy)
+-------------------------------------
+ eseries_system_serial: "012345678901" # Be sure to quote if the serial is all numbers and begins with zero.
+ eseries_system_password: admin_password
+ eseries_proxy_api_url: https://192.168.1.100:8443/devmgr/v2/
+ eseries_proxy_api_password: admin_password
+ eseries_subnet: 192.168.1.0/24
+ eseries_prefer_embedded: false # Overrides the default behavior of using Web Services Proxy when eseries_proxy_api_url is defined. This will only effect storage systems that have Embedded Web Services.
+ eseries_validate_certs: false
+
+ eseries_system_name: my_eseries_array
+ eseries_system_cache_block_size: 128
+ eseries_system_cache_flush_threshold: 90
+ eseries_system_autoload_balance: enabled
+ eseries_system_host_connectivity_reporting: enabled
+ eseries_system_default_host_type: Linux DM-MP
+
+ eseries_management_interfaces:
+ config_method: static
+ subnet_mask: 255.255.255.0
+ gateway: 192.168.1.1
+ dns_config_method: static
+ dns_address: 192.168.1.253
+ dns_address_backup: 192.168.1.254
+ ssh: true
+ ntp_config_method: static
+ ntp_address: 192.168.1.200
+ ntp_address_backup: 192.168.1.201
+ controller_a:
+ - address: 192.168.1.100
+ - address: 192.168.1.101
+ controller_b:
+ - address: 192.168.1.102
+ - address: 192.168.1.103
+
+ eseries_ldap_state: present
+ eseries_ldap_bind_username:
+ eseries_ldap_bind_password:
+ eseries_ldap_server:
+ eseries_ldap_search_base:
+ eseries_ldap_role_mappings:
+ ".*":
+ - storage.admin
+ - storage.monitor
+ - support.admin
+ - security.admin
+
+ eseries_client_certificate_certificates:
+ - /path/to/client_certificate.crt
+
+ eseries_firmware_firmware: "/path/to/firmware.dlp"
+ eseries_firmware_nvsram: "/path/to/nvsram.dlp"
+ eseries_drive_firmware_firmware_list:
+ - "/path/to/drive_firmware.dlp"
+
+ eseries_asup_state: enabled
+ eseries_asup_active: true
+ eseries_asup_days: [sunday, saturday]
+ eseries_asup_start: 17
+ eseries_asup_end: 24
+ eseries_asup_validate: false
+ eseries_asup_method: email
+ eseries_asup_email:
+ server: smtp.example.com
+ sender: noreply@example.com
+
+ eseries_syslog_state: present
+ eseries_syslog_address: 192.168.1.150
+ eseries_syslog_protocol: udp
+ eseries_syslog_port: 514
+ eseries_alert_syslog_servers:
+ - "address": 192.168.1.150
+ "port": 514
+
+Example Storage System Inventory File (Without storage system discovery)
+-------------------------------------
+ eseries_system_api_url: https://192.168.1.200:8443/devmgr/v2/
+ eseries_system_password: admin_password
+ eseries_validate_certs: false
+
+ (...) # Same as the previous example
+
+Role Variables
+--------------
+**Note that when values are specified below, they indicate the default value.**
+
+ # Web Services Embedded information
+ eseries_subnet: # Network subnet to search for the storage system specified in CIDR form. Example: 192.168.1.0/24
+ eseries_system_serial: # Storage system serial number. Be sure to quote if the serial is all numbers and begins with zero. (This is located on a label at the top-left towards the front on the device)
+ eseries_system_addresses: # Storage system management IP addresses. Only required when eseries_system_serial or eseries_system_api_url are not defined. When not specified, addresses will be populated with eseries_management_interfaces controller addresses.
+ eseries_system_api_url: # Url for the storage system's for embedded web services rest api. Example: https://192.168.10.100/devmgr/v2
+ eseries_system_username: admin # Username for the storage system's for embedded web services rest api
+ eseries_system_password: # Password for the storage system's for embedded web services rest api and when the admin password has not been set eseries_system_password will be used to set it.
+ eseries_proxy_ssid: # Arbitrary string for the proxy to represent the storage system. eseries_system_serial will be used when not defined.
+ eseries_template_api_url: # Template for the web services api url. Default: https://0.0.0.0:8443/devmgr/v2/
+ eseries_prefer_embedded: false # Overrides the default behavior of using Web Services Proxy when eseries_proxy_api_url is defined. This will only effect storage systems that have Embedded Web Services.
+ eseries_validate_certs: true # Indicates Whether SSL certificates should be verified. Used for both embedded and proxy. Choices: true, false
+
+ # Web Services Proxy information
+ Note: eseries_proxy_* variables are required to discover storage systems prior to SANtricity OS version 11.60.2.
+ eseries_proxy_api_url: # Url for the storage system's for proxy web services rest api. Example: https://192.168.10.100/devmgr/v2
+ eseries_proxy_api_username: # Username for the storage system's for proxy web services rest api.
+ eseries_proxy_api_password: # Password for the storage system's for proxy web services rest api and when the admin password has not been set eseries_proxy_api_password will be used to set it.
+
+ # Global storage system information
+ eseries_system_name: # Name of the storage system.
+ eseries_system_cache_block_size: # Cache block size
+ eseries_system_cache_flush_threshold: # Unwritten data will be flushed when exceeds this threshold
+ eseries_system_autoload_balance: # Whether automatic load balancing should be enabled. Choices: enabled, disabled
+ eseries_system_host_connectivity_reporting: # Whether host connectivity reporting should be enabled. Choices: enabled, disabled
+ eseries_system_default_host_type: # Only required when using something other than Linux kernel 3.10 or later with DM-MP (Linux DM-MP),
+ # non-clustered Windows (Windows), or the storage system default host type is incorrect. Common definitions below:
+ # - AIX MPIO: The Advanced Interactive Executive (AIX) OS and the native MPIO driver
+ # - AVT 4M: Silicon Graphics, Inc. (SGI) proprietary multipath driver; refer to the SGI installation documentation for more information
+ # - HP-UX: The HP-UX OS with native multipath driver
+ # - Linux ATTO: The Linux OS and the ATTO Technology, Inc. driver (must use ATTO FC HBAs)
+ # - Linux DM-MP: The Linux OS and the native DM-MP driver
+ # - Linux Pathmanager: The Linux OS and the SGI proprietary multipath driver; refer to the SGI installation documentation for more information
+ # - Mac: The Mac OS and the ATTO Technology, Inc. driver
+ # - ONTAP: FlexArray
+ # - Solaris 11 or later: The Solaris 11 or later OS and the native MPxIO driver
+ # - Solaris 10 or earlier: The Solaris 10 or earlier OS and the native MPxIO driver
+ # - SVC: IBM SAN Volume Controller
+ # - VMware: ESXi OS
+ # - Windows: Windows Server OS and Windows MPIO with a DSM driver
+ # - Windows Clustered: Clustered Windows Server OS and Windows MPIO with a DSM driver
+ # - Windows ATTO: Windows OS and the ATTO Technology, Inc. driver
+
+ # Role-based username passwords
+ eseries_system_monitor_password: # Storage system monitor username password
+ eseries_system_security_password: # Storage system security username password
+ eseries_system_storage_password: # Storage system storage username password
+ eseries_system_support_password: # Storage system support username password
+
+ # Storage management interface defaults
+ Note: eseries_management_* variables have the lowest priority and will be overwritten by those found in eseries_management_interfaces; use these to defined host group defaults.
+ eseries_management_config_method: # Default config method for all management interfaces. Choices: static, dhcp
+ eseries_management_subnet_mask: # Default subnet mask for all management interfaces
+ eseries_management_gateway_mask: # Default gateway for all management interfaces
+ eseries_management_dns_config_method: # Default DNS config method for all management interfaces
+ eseries_management_dns_address: # Default primary DNS address for all management interfaces
+ eseries_management_dns_address_backup: # Default backup DNS address for all management interfaces
+ eseries_management_ntp_config_method: # Default NTP config method for all management interfaces
+ eseries_management_ntp_address: # Default primary NTP address for all management interfaces
+ eseries_management_ntp_address_backup: # Default backup NTP address for all management interfaces
+ eseries_management_ssh: # Default SSH access for all management interfaces. Choices: true, false
+ eseries_management_interfaces:
+ config_method: # Config method for all management interfaces. Choices: static, dhcp
+ subnet_mask: # Subnet mask for all management interfaces
+ gateway_mask: # Gateway for all management interfaces
+ dns_config_method: # DNS config method for all management interfaces
+ dns_address: # Primary DNS address for all management interfaces
+ dns_address_backup: # Backup DNS address for all management interfaces
+ ntp_config_method: # NTP config method for all management interfaces
+ ntp_address: # Primary NTP address for all management interfaces
+ ntp_address_backup: # Backup NTP address for all management interfaces
+ ssh: # SSH access for all management interfaces. Choices: true, false
+ controller_a: # List of controller A ports
+ - address: # IPv4 address for controller A
+ config_method: # Config method for controller A. Choices: static, dhcp
+ subnet_mask: # Subnet mask for controller A
+ gateway: # Gateway for controller A
+ dns_config_method: # DNS config method for controller A
+ dns_address: # Primary DNS address for controller A
+ dns_address_backup: # Backup DNS address for controller A
+ ntp_config_method: # NTP config method for controller A
+ ntp_address: # Primary NTP address for controller A
+ ntp_address_backup: # Backup NTP address for controller A
+ ssh: # SSH access for controller A. Choices: true, false
+ controller_b: # List of controller B ports
+ - (...) # Same as for controller A but for controller B.
+
+ # Alerts configuration defaults
+ eseries_alerts_state: # Whether to enable storage system alerts. Choices: enabled, disabled
+ eseries_alerts_contact: # This allows owner to specify free-form contact information such as email or phone number.
+ eseries_alerts_recipients: # List containing e-mails that should be sent notifications when alerts are issued.
+ eseries_alerts_sender: # Sender email. This does not necessarily need to be a valid e-mail.
+ eseries_alerts_server: # Fully qualified domain name, IPv4 address, or IPv6 address of the mail server.
+ eseries_alerts_test: false # When changes are made to the storage system alert configuration a test e-mail will be sent. Choices: true, false
+ eseries_alert_syslog_servers: # List of dictionaries where each dictionary contains a syslog server entry. [{"address": <syslog_address>, "port": 514}]
+ eseries_alert_syslog_test: false # When changes are made to the alerts syslog servers configuration a test message will be sent to them. Choices: true, false
+
+ # LDAP configuration defaults
+ eseries_ldap_state: # Whether LDAP should be configured
+ eseries_ldap_identifier: # The user attributes that should be considered for the group to role mapping
+ eseries_ldap_user_attribute: # Attribute used to the provided username during authentication.
+ eseries_ldap_bind_username: # User account that will be used for querying the LDAP server.
+ eseries_ldap_bind_password: # Password for the bind user account
+ eseries_ldap_server: # LDAP server URL.
+ eseries_ldap_search_base: # Search base used for find user's group membership
+ eseries_ldap_role_mappings: # Dictionary of user groups, each containing the list of access roles.
+ # Role choices: storage.admin - allows users full read/writes access to storage objects and operations.
+ # storage.monitor - allows users read-only access to storage objects and operations.
+ # storage.admin - allows users access to hardware, diagnostic information, major event logs,
+ # and other critical support-related functionality, but not the sorage configuration.
+ # security.admin - allows users access to authentication/authorization configuration, as
+ # well as the audit log configuration, adn certification management.
+
+ # Drive firmware defaults
+ eseries_drive_firmware_firmware_list: # Local path list for drive firmware.
+ eseries_drive_firmware_wait_for_completion: # Forces drive firmware upgrades to wait for all associated tasks to complete. Choices: true, false
+ eseries_drive_firmware_ignore_inaccessible_drives: # Forces drive firmware upgrades to ignore any inaccessible drives. Choices: true, false
+ eseries_drive_firmware_upgrade_drives_online: # Forces drive firmware upgrades to be performed while I/Os are accepted. Choices: true, false
+
+ # Controller firmware defaults
+ eseries_firmware_nvsram: # Local path for NVSRAM file.
+ eseries_firmware_firmware: # Local path for controller firmware file.
+ eseries_firmware_wait_for_completion: # Forces controller firmware upgrade to wait until upgrade has completed before continuing. Choices: true, false
+ eseries_firmware_clear_mel_events: # Forces firmware upgrade to be attempted regardless of the health check results. Choices: true, false
+
+ # Auto-Support configuration defaults
+ eseries_asup_state: # Whether auto support (ASUP) should be enabled. Choices: enabled, disabled
+ eseries_asup_active: # Enables active monitoring which allows NetApp support personnel to request support data to resolve issues. Choices: true, false
+ eseries_asup_days: # List of days of the week. Choices: monday, tuesday, wednesday, thursday, friday, saturday, sunday
+ eseries_asup_start: # Hour of the day(s) to start ASUP bundle transmissions. Start time must be less than end time. Choices: 0-23
+ eseries_asup_end: # Hour of the day(s) to end ASUP bundle transmissions. Start time must be less than end time. Choices: 1-24
+ eseries_asup_method: # ASUP delivery method. Choices https, http, email (default: https)
+ eseries_asup_routing_type: # ASUP delivery routing type for https or http. Choices: direct, proxy, script (default: direct)
+ eseries_asup_proxy: # ASUP proxy delivery method information.
+ host: # ASUP proxy host IP address or FQDN. When eseries_asup_routing_type==proxy this must be specified.
+ port: # ASUP proxy host port. When eseries_asup_routing_type==proxy this must be specified.
+ script: # ASUP proxy host script.
+ eseries_asup_email: # ASUP email delivery configuration information
+ server: # ASUP email server
+ sender: # ASUP email sender
+ test_recipient: # ASUP configuration mail test recipient
+ eseries_maintenance_duration: # Duration in hours (1-72) the ASUP maintenance mode will be active
+ eseries_maintenance_emails: # List of email addresses for maintenance notifications
+ eseries_asup_validate: # Verify ASUP configuration prior to applying changes
+
+ # Audit-log configuration defaults
+ eseries_auditlog_enforce_policy: # Whether to make audit-log policy changes. Choices: true, false
+ eseries_auditlog_force: # Forces audit-log to delete log messages when fullness threshold has been exceeded. Applicable when eseries_auditlog_full_policy=preventSystemAccess. Choices: true, false
+ eseries_auditlog_full_policy: # Policy for what to do when record limit has been reached. Choices: overWrite, preventSystemAccess
+ eseries_auditlog_log_level: # Filters logs based on the specified level. Choices: all, writeOnly
+ eseries_auditlog_max_records: # Maximum number of audit-log messages retained. Choices: 100-50000.
+ eseries_auditlog_threshold: # Memory full percentage threshold that audit-log will start issuing warning messages. Choices: 60-90
+
+ # Syslog configuration defaults
+ eseries_syslog_state: # Whether syslog servers should be added or removed from storage system. Choices: present, absent
+ eseries_syslog_address: # Syslog server IPv4 address or fully qualified hostname.
+ eseries_syslog_test: # Whether a test messages should be sent to syslog server when added to the storage system. Choices: true, false
+ eseries_syslog_protocol: # Protocol to be used when transmitting log messages to syslog server. Choices: udp, tc, tls
+ eseries_syslog_port: # Port to be used when transmitting log messages to syslog server.
+ eseries_syslog_components: # List of components log to syslog server. Choices: auditLog, (others may become available)
+
+License
+-------
+ BSD-3-Clause
+
+Author Information
+------------------
+ Nathan Swartz (@ndswartz)
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/defaults/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/defaults/main.yml
new file mode 100644
index 00000000..db9145af
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/defaults/main.yml
@@ -0,0 +1,195 @@
+# Storage system specific credentials
+# -----------------------------------
+eseries_system_username: admin # Storage system username. Default: admin
+#eseries_system_password: # Storage system admin password.
+#eseries_validate_certs: # Whether the SSL certificates should be verified. (boolean)
+#eseries_system_subnet: # IPv4 search range for discovering E-Series storage. Must be in CIDR form.
+#eseries_system_serial: # Storage system chassis serial number. This is used to automatically discover the system.
+#eseries_system_addresses: # (list) Controller address(es) for the storage system. Only applicable for proxy web services.
+#eseries_system_tags: # Meta tags to associate to the storage system. Only applicable for proxy web services.
+#eseries_system_password: # Required when adding storage systems to SANtricity Web Services Proxy.
+#eseries_system_minimum_password_length: # Minimum required password length
+
+# SANtricity Web Services Proxy specific variables
+# ------------------------------------------------
+#eseries_proxy_ssid: # Storage array identifier. This value will be 1 when interacting with the embedded web services,
+#eseries_proxy_api_url: # Url for the web services proxy rest api. Example: https://192.168.10.100:8443/devmgr/v2
+#eseries_proxy_api_username: # Username for the web services proxy rest api.
+#eseries_proxy_api_password: # Password for the web services proxy rest api.
+#eseries_proxy_current_api_password: # This is for changing the password for the proxy.
+#eseries_proxy_api_validate_certs: # Whether the SSL certificates should be verified. (boolean)
+#eseries_proxy_minimum_password_length: # Minimum required proxy password length
+
+#eseries_proxy_discovery_subnet: # IPv4 search range for discovering E-Series storage. Must be in CIDR form.
+#eseries_proxy_accept_certifications: # Force automatic acceptance of all storage system's certificate
+#eseries_proxy_default_system_tags: # Default meta tags to associate with all storage systems
+#eseries_proxy_default_password: # Default password to associate with all storage systems
+#eseries_proxy_systems: # List of storage system information which defines which systems should be added to proxy web services.
+ # Automatically populated from storage system's inventory when not defined.
+ # See na_santricity_proxy_systems for more details.
+
+# Storage proxy non-admin passwords
+# ---------------------------------
+#eseries_proxy_monitor_password: # Proxy monitor username password
+#eseries_proxy_security_password: # Proxy security username password
+#eseries_proxy_storage_password: # Proxy storage username password
+#eseries_proxy_support_password: # Proxy support username password
+
+# Storage system non-admin passwords
+# ----------------------------------
+#eseries_system_monitor_password: # Storage system monitor username password
+#eseries_system_security_password: # Storage system security username password
+#eseries_system_storage_password: # Storage system storage username password
+#eseries_system_support_password: # Storage system support username password
+
+# Storage system defaults
+# -----------------------------
+#eseries_system_name: # Name of the storage system.
+#eseries_system_cache_block_size: # Cache block size
+#eseries_system_cache_flush_threshold: # Unwritten data will be flushed when exceeds this threshold
+#eseries_system_autoload_balance: # Whether automatic load balancing should be enabled. Choices: enabled, disabled
+#eseries_system_host_connectivity_reporting: # Whether host connectivity reporting should be enabled. Choices: enabled, disabled
+#eseries_system_default_host_type: # Only required when using something other than Linux kernel 3.10 or later with DM-MP (Linux DM-MP),
+ # non-clustered Windows (Windows), or the storage system default host type is incorrect. Common definitions below:
+ # - AIX MPIO: The Advanced Interactive Executive (AIX) OS and the native MPIO driver
+ # - AVT 4M: Silicon Graphics, Inc. (SGI) proprietary multipath driver; refer to the SGI installation documentation for more information
+ # - HP-UX: The HP-UX OS with native multipath driver
+ # - Linux ATTO: The Linux OS and the ATTO Technology, Inc. driver (must use ATTO FC HBAs)
+ # - Linux DM-MP: The Linux OS and the native DM-MP driver
+ # - Linux Pathmanager: The Linux OS and the SGI proprietary multipath driver; refer to the SGI installation documentation for more information
+ # - Mac: The Mac OS and the ATTO Technology, Inc. driver
+ # - ONTAP: FlexArray
+ # - Solaris 11 or later: The Solaris 11 or later OS and the native MPxIO driver
+ # - Solaris 10 or earlier: The Solaris 10 or earlier OS and the native MPxIO driver
+ # - SVC: IBM SAN Volume Controller
+ # - VMware: ESXi OS
+ # - Windows: Windows Server OS and Windows MPIO with a DSM driver
+ # - Windows Clustered: Clustered Windows Server OS and Windows MPIO with a DSM driver
+ # - Windows ATTO: Windows OS and the ATTO Technology, Inc. driver
+
+# Storage system SSL certificates
+# -------------------------------
+#eseries_client_certificate_certificates # Dictionary containing the SSL certificate file paths. The key will be used as the alias.
+#eseries_client_certificate_absent_certificates # List of aliases to remove from the storage array's trust store.
+
+# Storage management interface defaults
+# -------------------------------------
+#eseries_management_config_method:
+#eseries_management_subnet_mask:
+#eseries_management_gateway_mask:
+#eseries_management_dns_config_method:
+#eseries_management_dns_address:
+#eseries_management_dns_address_backup:
+#eseries_management_ntp_config_method:
+#eseries_management_ntp_address:
+#eseries_management_ntp_address_backup:
+#eseries_management_ssh:
+#eseries_management_interfaces:
+# config_method:
+# subnet_mask:
+# gateway_mask:
+# dns_config_method:
+# dns_address:
+# dns_address_backup:
+# ntp_config_method:
+# ntp_address:
+# ntp_address_backup:
+# ssh
+# controller_a:
+# config_method:
+# address:
+# subnet_mask:
+# gateway:
+# dns_config_method:
+# dns_address:
+# dns_address_backup:
+# ntp_config_method:
+# ntp_address:
+# ntp_address_backup:
+# ssh:
+# - (...)
+# controller_b:
+# - (...)
+# - (...)
+
+# Alerts configuration defaults
+# -----------------------------
+#eseries_alerts_state: # Whether to enable storage system alerts. Choices: enabled, disabled
+#eseries_alerts_contact: # This allows owner to specify free-form contact information such as email or phone number.
+#eseries_alerts_recipients: # List containing e-mails that should be sent notifications when alerts are issued.
+#eseries_alerts_sender: # Sender email. This does not necessarily need to be a valid e-mail.
+#eseries_alerts_server: # Fully qualified domain name, IPv4 address, or IPv6 address of the mail server.
+#eseries_alert_syslog_servers: # List of dictionaries where each dictionary contains a syslog server entry. [{"address": <syslog_address>, "port": 514}]
+eseries_alerts_test: false # When changes are made to the storage system alert configuration a test e-mail will be sent. Choices: true, false
+eseries_alert_syslog_test: false # When changes are made to the alerts syslog servers configuration a test message will be sent to them. Choices: true, false
+
+# LDAP configuration defaults
+# ---------------------------
+#eseries_ldap_state: # Whether LDAP should be configured
+#eseries_ldap_identifier: memberOf # The user attributes that should be considered for the group to role mapping
+#eseries_ldap_user_attribute: sAMAccountName # Attribute used to the provided username during authentication.
+#eseries_ldap_bind_username: # User account that will be used for querying the LDAP server.
+#eseries_ldap_bind_password: # Password for the bind user account
+#eseries_ldap_server: # LDAP server URL.
+#eseries_ldap_search_base: # Search base used for find user's group membership
+#eseries_ldap_role_mappings: # Dictionary of user groups, each containing the list of access roles.
+ # Role choices: storage.admin - allows users full read/writes access to storage objects and operations.
+ # storage.monitor - allows users read-only access to storage objects and operations.
+ # storage.admin - allows users access to hardware, diagnostic information, major event logs, and
+ # other critical support-related functionality, but not the sorage configuration.
+ # security.admin - allows users access to authentication/authorization configuration, as well as
+ # the audit log configuration, adn certification management.
+
+# Drive firmware defaults
+# -----------------------
+#eseries_drive_firmware_firmware_list: # Local path list for drive firmware.
+eseries_drive_firmware_wait_for_completion: true # Forces drive firmware upgrades to wait for all associated tasks to complete. Choices: true, false
+eseries_drive_firmware_ignore_inaccessible_drives: false # Forces drive firmware upgrades to ignore any inaccessible drives. Choices: true, false
+eseries_drive_firmware_upgrade_drives_online: true # Forces drive firmware upgrades to be performed while I/Os are accepted. Choices: true, false
+
+# Controller firmware defaults
+# ----------------------------
+#eseries_firmware_nvsram: # Local path for NVSRAM file.
+#eseries_firmware_firmware: # Local path for controller firmware file.
+eseries_firmware_wait_for_completion: true # Forces controller firmware upgrade to wait until upgrade has completed before continuing. Choices: true, false
+eseries_firmware_ignore_mel_events: false # Forces firmware upgrade to be attempted regardless of the health check results. Choices: true, false
+
+# ASUP configuration defaults
+# ---------------------------
+#eseries_asup_state: # Whether auto support (ASUP) should be enabled. Choices: enabled, disabled
+eseries_asup_active: true # Enables active monitoring which allows NetApp support personnel to request support data to resolve issues. Choices: true, false
+#eseries_asup_days: # List of days of the week. Choices: monday, tuesday, wednesday, thursday, friday, saturday, sunday
+eseries_asup_start: 0 # Hour of the day(s) to start ASUP bundle transmissions. Start time must be less than end time. Choices: 0-23
+eseries_asup_end: 24 # Hour of the day(s) to end ASUP bundle transmissions. Start time must be less than end time. Choices: 1-24
+#eseries_asup_method: # ASUP delivery method. Choices https, http, email (default: https)
+#eseries_asup_routing_type: # ASUP delivery routing type for https or http. Choices: direct, proxy, script (default: direct)
+#eseries_asup_proxy: # ASUP proxy delivery method information.
+# host: # ASUP proxy host IP address or FQDN. When eseries_asup_routing_type==proxy this must be specified.
+# port: # ASUP proxy host port. When eseries_asup_routing_type==proxy this must be specified.
+# script: # ASUP proxy host script.
+#eseries_asup_email: # ASUP email delivery configuration information
+# server: # ASUP email server
+# sender: # ASUP email sender
+# test_recipient: # ASUP configuration mail test recipient
+#eseries_maintenance_duration: # Duration in hours (1-72) the ASUP maintenance mode will be active
+#eseries_maintenance_emails: # List of email addresses for maintenance notifications
+#eseries_asup_validate: # Verify ASUP configuration prior to applying changes.
+
+# Audit-log configuration defaults
+# --------------------------------
+eseries_auditlog_enforce_policy: false # Whether to make audit-log policy changes. Choices: true, false
+eseries_auditlog_force: false # Forces audit-log to delete log messages when fullness threshold has been exceeded.
+ # Applicable when eseries_auditlog_full_policy=preventSystemAccess. Choices: true, false
+eseries_auditlog_full_policy: overWrite # Policy for what to do when record limit has been reached. Choices: overWrite, preventSystemAccess
+eseries_auditlog_log_level: writeOnly # Filters logs based on the specified level. Choices: all, writeOnly
+eseries_auditlog_max_records: 50000 # Maximum number of audit-log messages retained. Choices: 100-50000.
+eseries_auditlog_threshold: 90 # Memory full percentage threshold that audit-log will start issuing warning messages. Choices: 60-90
+
+# Syslog configuration defaults
+# -----------------------------
+#eseries_syslog_state: # Whether syslog servers should be added or removed from storage system. Choices: present, absent
+#eseries_syslog_address: # Syslog server IPv4 address or fully qualified hostname.
+eseries_syslog_test: false # Whether a test messages should be sent to syslog server when added to the storage system. Choices: true, false
+eseries_syslog_protocol: udp # Protocol to be used when transmitting log messages to syslog server. Choices: udp, tc, tls
+eseries_syslog_port: 514 # Port to be used when transmitting log messages to syslog server.
+eseries_syslog_components: ["auditLog"] # List of components log to syslog server. Choices: auditLog, (others may be available)
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/meta/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/meta/main.yml
new file mode 100644
index 00000000..91437f9d
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/meta/main.yml
@@ -0,0 +1,13 @@
+galaxy_info:
+ author: Nathan Swartz (@ndswartz)
+ description: Manages NetApp E-Series storage system's firmware, management interfaces, security, system, and logging configuration.
+ company: NetApp, Inc
+ license: BSD-3 Clause
+ platforms: []
+ min_ansible_version: 2.9
+ galaxy_tags:
+ - netapp
+ - eseries
+ - storage
+
+dependencies: [] \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/firmware.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/firmware.yml
new file mode 100644
index 00000000..99c7b6a5
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/firmware.yml
@@ -0,0 +1,78 @@
+- name: Upload required drive, nvsram, and firmware files to Web Services Proxy.
+ block:
+ - name: Collect expected firmware file lists
+ set_fact:
+ eseries_proxy_drive_firmware: |-
+ {%- set drive_firmware = [] %}
+ {%- for host in ansible_play_hosts_all %}
+ {%- if hostvars[host]["current_eseries_api_is_proxy"] and "eseries_drive_firmware_firmware_list" in hostvars[host] %}
+ {%- if drive_firmware.extend(hostvars[host]["eseries_drive_firmware_firmware_list"]) %}{%- endif %}
+ {%- endif %}
+ {%- endfor %}
+ {{ drive_firmware | list }}
+ eseries_proxy_nvsram: |-
+ {%- set nvsram = [] %}
+ {%- for host in ansible_play_hosts_all %}
+ {%- if hostvars[host]["current_eseries_api_is_proxy"] and "eseries_firmware_nvsram" in hostvars[host] %}
+ {%- if nvsram.append(hostvars[host]["eseries_firmware_nvsram"]) %}{%- endif %}
+ {%- endif %}
+ {%- endfor %}
+ {{ nvsram | list }}
+ eseries_proxy_firmware: |-
+ {%- set firmware = [] %}
+ {%- for host in ansible_play_hosts_all %}
+ {%- if hostvars[host]["current_eseries_api_is_proxy"] and "eseries_firmware_firmware" in hostvars[host] %}
+ {%- if firmware.append(hostvars[host]["eseries_firmware_firmware"]) %}{%- endif %}
+ {%- endif %}
+ {%- endfor %}
+ {{ firmware | list }}
+
+ - name: Ensure SANtricity Web Services Proxy has the expected drive firmware
+ na_santricity_proxy_drive_firmware_upload:
+ api_url: "{{ eseries_proxy_api_url }}"
+ api_username: "{{ eseries_proxy_api_username }}"
+ api_password: "{{ eseries_proxy_api_password }}"
+ validate_certs: "{{ eseries_validate_certs | default(omit) }}"
+ firmware: "{{ eseries_proxy_drive_firmware | default(omit) }}"
+ when: eseries_proxy_drive_firmware != []
+ - name: Ensure SANtricity Web Services Proxy has the expected controller firmware and NVSRAM
+ na_santricity_proxy_firmware_upload:
+ api_url: "{{ eseries_proxy_api_url }}"
+ api_username: "{{ eseries_proxy_api_username }}"
+ api_password: "{{ eseries_proxy_api_password }}"
+ validate_certs: "{{ eseries_validate_certs | default(omit) }}"
+ firmware: "{{ eseries_proxy_firmware | default(omit) + eseries_proxy_nvsram | default(omit)}}"
+ when: eseries_proxy_nvsram != [] or eseries_proxy_firmware != []
+ run_once: true
+
+- name: Ensure drive firmware is properly configured
+ na_santricity_drive_firmware:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ firmware: "{{ eseries_drive_firmware_firmware_list }}"
+ wait_for_completion: "{{ eseries_drive_firmware_wait_for_completion | default(true) }}"
+ ignore_inaccessible_drives: "{{ eseries_drive_firmware_ignore_inaccessible_drives | default(omit) }}"
+ upgrade_drives_online: "{{ eseries_drive_firmware_upgrade_drives_online | default(omit) }}"
+ when: eseries_drive_firmware_firmware_list is defined
+ tags:
+ - firmware
+ - drive_firmware
+
+- name: Ensure controller firmware is properly configured
+ na_santricity_firmware:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ nvsram: "{{ eseries_firmware_nvsram | default('') }}"
+ firmware: "{{ eseries_firmware_firmware | default('') }}"
+ wait_for_completion: "{{ eseries_firmware_wait_for_completion | default(true) }}"
+ clear_mel_events: "{{ eseries_firmware_ignore_mel_events | default(omit) }}"
+ when: eseries_firmware_nvsram is defined or eseries_firmware_firmware is defined
+ tags:
+ - firmware
+ - controller_firmware
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/interface.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/interface.yml
new file mode 100644
index 00000000..00335895
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/interface.yml
@@ -0,0 +1,117 @@
+- name: Determine individual management interface information.
+ set_fact:
+ interface_a1: "{{ eseries_management_interfaces['controller_a'][0] | default(omit) }}"
+ interface_a2: "{{ eseries_management_interfaces['controller_a'][1] | default(omit) }}"
+ interface_b1: "{{ eseries_management_interfaces['controller_b'][0] | default(omit) }}"
+ interface_b2: "{{ eseries_management_interfaces['controller_b'][1] | default(omit) }}"
+
+- name: Ensure the management interface (controller A, port 1) have been configure.
+ block:
+ - na_santricity_mgmt_interface:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ controller: A
+ port: 1
+ address: "{{ interface_a1['address'] | default(omit) }}"
+ config_method: "{{ interface_a1['config_method'] | default(eseries_management_interfaces['config_method'] | default(eseries_management_config_method | default(omit))) }}"
+ subnet_mask: "{{ interface_a1['subnet_mask'] | default(eseries_management_interfaces['subnet_mask'] | default(eseries_management_subnet_mask | default(omit))) }}"
+ gateway: "{{ interface_a1['gateway'] | default(eseries_management_interfaces['gateway'] | default(eseries_management_gateway_mask | default(omit))) }}"
+ dns_config_method: "{{ interface_a1['dns_config_method'] | default(eseries_management_interfaces['dns_config_method'] | default(eseries_management_dns_config_method | default(omit))) }}"
+ dns_address: "{{ interface_a1['dns_address'] | default(eseries_management_interfaces['dns_address'] | default(eseries_management_dns_address | default(omit))) }}"
+ dns_address_backup: "{{ interface_a1['dns_address_backup'] | default(eseries_management_interfaces['dns_address_backup'] | default(eseries_management_dns_address_backup | default(omit))) }}"
+ ntp_config_method: "{{ interface_a1['ntp_config_method'] | default(eseries_management_interfaces['ntp_config_method'] | default(eseries_management_ntp_config_method | default(omit))) }}"
+ ntp_address: "{{ interface_a1['ntp_address'] | default(eseries_management_interfaces['ntp_address'] | default(eseries_management_ntp_address | default(omit))) }}"
+ ntp_address_backup: "{{ interface_a1['ntp_address_backup'] | default(eseries_management_interfaces['ntp_address_backup'] | default(eseries_management_ntp_address_backup | default(omit))) }}"
+ ssh: "{{ interface_a1['ssh'] | default(eseries_management_interfaces['ssh'] | default(eseries_management_ssh | default(omit))) }}"
+ register: current_management_urls_a1
+ - name: Update Web Services URL
+ set_fact:
+ current_eseries_api_url: "{{ current_management_urls_a1['available_embedded_api_urls'][0] | default(current_eseries_api_url) }}"
+ when: interface_a1 is defined and interface_a1
+
+- name: Ensure the management interface (controller A, port 2) have been configure.
+ block:
+ - name: Ensure the management interface (controller A, port 2) have been configure.
+ na_santricity_mgmt_interface:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ controller: A
+ port: 2
+ address: "{{ interface_a2['address'] | default(omit) }}"
+ config_method: "{{ interface_a2['config_method'] | default(eseries_management_interfaces['config_method'] | default(eseries_management_config_method | default(omit))) }}"
+ subnet_mask: "{{ interface_a2['subnet_mask'] | default(eseries_management_interfaces['subnet_mask'] | default(eseries_management_subnet_mask | default(omit))) }}"
+ gateway: "{{ interface_a2['gateway'] | default(eseries_management_interfaces['gateway'] | default(eseries_management_gateway_mask | default(omit))) }}"
+ dns_config_method: "{{ interface_a2['dns_config_method'] | default(eseries_management_interfaces['dns_config_method'] | default(eseries_management_dns_config_method | default(omit))) }}"
+ dns_address: "{{ interface_a2['dns_address'] | default(eseries_management_interfaces['dns_address'] | default(eseries_management_dns_address | default(omit))) }}"
+ dns_address_backup: "{{ interface_a2['dns_address_backup'] | default(eseries_management_interfaces['dns_address_backup'] | default(eseries_management_dns_address_backup | default(omit))) }}"
+ ntp_config_method: "{{ interface_a2['ntp_config_method'] | default(eseries_management_interfaces['ntp_config_method'] | default(eseries_management_ntp_config_method | default(omit))) }}"
+ ntp_address: "{{ interface_a2['ntp_address'] | default(eseries_management_interfaces['ntp_address'] | default(eseries_management_ntp_address | default(omit))) }}"
+ ntp_address_backup: "{{ interface_a2['ntp_address_backup'] | default(eseries_management_interfaces['ntp_address_backup'] | default(eseries_management_ntp_address_backup | default(omit))) }}"
+ ssh: "{{ interface_a2['ssh'] | default(eseries_management_interfaces['ssh'] | default(eseries_management_ssh | default(omit))) }}"
+ register: current_management_urls_a2
+ - name: Try backup Web Services REST API url.
+ set_fact:
+ current_eseries_api_url: "{{ current_management_urls_a2['available_embedded_api_urls'][0] | default(current_eseries_api_url) }}"
+ when: interface_a2 is defined and interface_a2
+
+- name: Ensure the management interface (controller B, port 1) have been configure.
+ block:
+ - name: Ensure the management interface (controller B, port 1) have been configure.
+ na_santricity_mgmt_interface:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ controller: B
+ port: 1
+ address: "{{ interface_b1['address'] | default(omit) }}"
+ config_method: "{{ interface_b1['config_method'] | default(eseries_management_interfaces['config_method'] | default(eseries_management_config_method | default(omit))) }}"
+ subnet_mask: "{{ interface_b1['subnet_mask'] | default(eseries_management_interfaces['subnet_mask'] | default(eseries_management_subnet_mask | default(omit))) }}"
+ gateway: "{{ interface_b1['gateway'] | default(eseries_management_interfaces['gateway'] | default(eseries_management_gateway_mask | default(omit))) }}"
+ dns_config_method: "{{ interface_b1['dns_config_method'] | default(eseries_management_interfaces['dns_config_method'] | default(eseries_management_dns_config_method | default(omit))) }}"
+ dns_address: "{{ interface_b1['dns_address'] | default(eseries_management_interfaces['dns_address'] | default(eseries_management_dns_address | default(omit))) }}"
+ dns_address_backup: "{{ interface_b1['dns_address_backup'] | default(eseries_management_interfaces['dns_address_backup'] | default(eseries_management_dns_address_backup | default(omit))) }}"
+ ntp_config_method: "{{ interface_b1['ntp_config_method'] | default(eseries_management_interfaces['ntp_config_method'] | default(eseries_management_ntp_config_method | default(omit))) }}"
+ ntp_address: "{{ interface_b1['ntp_address'] | default(eseries_management_interfaces['ntp_address'] | default(eseries_management_ntp_address | default(omit))) }}"
+ ntp_address_backup: "{{ interface_b1['ntp_address_backup'] | default(eseries_management_interfaces['ntp_address_backup'] | default(eseries_management_ntp_address_backup | default(omit))) }}"
+ ssh: "{{ interface_b1['ssh'] | default(eseries_management_interfaces['ssh'] | default(eseries_management_ssh | default(omit))) }}"
+ register: current_management_urls_b1
+ - name: Try backup Web Services REST API url.
+ set_fact:
+ current_eseries_api_url: "{{ current_management_urls_b1['available_embedded_api_urls'][0] | default(current_eseries_api_url) }}"
+ when: interface_b1 is defined and interface_b1
+
+- name: Ensure the management interface (controller B, port 2) have been configure.
+ block:
+ - name: Ensure the management interface (controller B, port 2) have been configure.
+ na_santricity_mgmt_interface:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ controller: B
+ port: 2
+ address: "{{ interface_b2['address'] | default(omit) }}"
+ config_method: "{{ interface_b2['config_method'] | default(eseries_management_interfaces['config_method'] | default(eseries_management_config_method | default(omit))) }}"
+ subnet_mask: "{{ interface_b2['subnet_mask'] | default(eseries_management_interfaces['subnet_mask'] | default(eseries_management_subnet_mask | default(omit))) }}"
+ gateway: "{{ interface_b2['gateway'] | default(eseries_management_interfaces['gateway'] | default(eseries_management_gateway_mask | default(omit))) }}"
+ dns_config_method: "{{ interface_b2['dns_config_method'] | default(eseries_management_interfaces['dns_config_method'] | default(eseries_management_dns_config_method | default(omit))) }}"
+ dns_address: "{{ interface_b2['dns_address'] | default(eseries_management_interfaces['dns_address'] | default(eseries_management_dns_address | default(omit))) }}"
+ dns_address_backup: "{{ interface_b2['dns_address_backup'] | default(eseries_management_interfaces['dns_address_backup'] | default(eseries_management_dns_address_backup | default(omit))) }}"
+ ntp_config_method: "{{ interface_b2['ntp_config_method'] | default(eseries_management_interfaces['ntp_config_method'] | default(eseries_management_ntp_config_method | default(omit))) }}"
+ ntp_address: "{{ interface_b2['ntp_address'] | default(eseries_management_interfaces['ntp_address'] | default(eseries_management_ntp_address | default(omit))) }}"
+ ntp_address_backup: "{{ interface_b2['ntp_address_backup'] | default(eseries_management_interfaces['ntp_address_backup'] | default(eseries_management_ntp_address_backup | default(omit))) }}"
+ ssh: "{{ interface_b2['ssh'] | default(eseries_management_interfaces['ssh'] | default(eseries_management_ssh | default(omit))) }}"
+ register: current_management_urls_b2
+ - name: Try backup Web Services REST API url.
+ set_fact:
+ current_eseries_api_url: "{{ current_management_urls_b2['available_embedded_api_urls'][0] | default(current_eseries_api_url) }}"
+ when: interface_b2 is defined and interface_b2
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/logging.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/logging.yml
new file mode 100644
index 00000000..05f752e8
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/logging.yml
@@ -0,0 +1,90 @@
+- name: Ensure ASUP configuration
+ na_santricity_asup:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ eseries_validate_certs | default(omit) }}"
+ state: "{{ eseries_asup_state }}"
+ active: "{{ eseries_asup_active | default(omit)}}"
+ days: "{{ eseries_asup_days | default(omit) }}"
+ start: "{{ eseries_asup_start | default(omit) }}"
+ end: "{{ eseries_asup_end | default(omit) }}"
+ method: "{{ eseries_asup_method | default(omit) }}"
+ routing_type: "{{ eseries_asup_routing_type | default(omit) }}"
+ proxy: "{{ eseries_asup_proxy | default(omit) }}"
+ email: "{{ eseries_asup_email | default(omit) }}"
+ maintenance_duration: "{{ eseries_maintenance_duration | default(omit) }}"
+ maintenance_emails: "{{ eseries_maintenance_emails | default(omit) }}"
+ validate: "{{ eseries_asup_validate | default(omit) }}"
+ when: eseries_asup_state is defined
+ tags:
+ - logging
+ - asup
+
+- name: Ensure alerts have been configured
+ na_santricity_alerts:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ eseries_validate_certs | default(omit) }}"
+ state: "{{ eseries_alerts_state }}"
+ contact: "{{ eseries_alerts_contact | default(omit) }}"
+ recipients: "{{ eseries_alerts_recipients | default(omit) }}"
+ sender: "{{ eseries_alerts_sender| default(omit) }}"
+ server: "{{ eseries_alerts_server | default(omit) }}"
+ test: "{{ eseries_alerts_test | default(omit) }}"
+ when: eseries_alerts_state is defined
+ tags:
+ - logging
+ - alerts
+
+- name: Ensure auditLog configuration
+ na_santricity_auditlog:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ eseries_validate_certs | default(omit) }}"
+ force: "{{ eseries_auditlog_force | default(omit) }}"
+ full_policy: "{{ eseries_auditlog_full_policy | default(omit) }}"
+ log_level: "{{ eseries_auditlog_log_level | default(omit) }}"
+ max_records: "{{ eseries_auditlog_max_records | default(omit) }}"
+ threshold: "{{ eseries_auditlog_threshold | default(omit) }}"
+ when: eseries_auditlog_enforce_policy
+ tags:
+ - logging
+ - auditlog
+
+- name: Ensure components are configured to be sent to the approriate syslog servers
+ na_santricity_syslog:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ eseries_validate_certs | default(omit) }}"
+ state: "{{ eseries_syslog_state }}"
+ address: "{{ eseries_syslog_address }}"
+ test: "{{ eseries_syslog_test | default(omit) }}"
+ protocol: "{{ eseries_syslog_protocol | default(omit) }}"
+ port: "{{ eseries_syslog_port | default(omit) }}"
+ components: "{{ eseries_syslog_components | default(omit) }}"
+ when: eseries_syslog_state is defined and eseries_syslog_address is defined
+ tags:
+ - logging
+ - syslog
+
+- name: Ensure alerts are configured to be sent to the approriate syslog servers
+ na_santricity_alerts_syslog:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ eseries_validate_certs | default(omit) }}"
+ servers: "{{ eseries_alert_syslog_servers | default(omit) }}"
+ test: "{{ eseries_alert_syslog_test | default(omit) }}"
+ when: eseries_syslog_state is defined and eseries_syslog_address is defined
+ tags:
+ - logging
+ - syslog \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/main.yml
new file mode 100644
index 00000000..dedf7d90
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/main.yml
@@ -0,0 +1,36 @@
+- name: Set current storage system credentials
+ include_role:
+ name: netapp_eseries.santricity.nar_santricity_common
+ tasks_from: build_info.yml
+ when: current_eseries_api_url is not defined
+ tags:
+ - always
+
+- name: Ensure security settings are configured
+ import_tasks: security.yml
+ delegate_to: localhost
+ ignore_errors: true
+
+- name: Ensure management interfaces are configured
+ import_tasks: interface.yml
+ delegate_to: localhost
+ tags:
+ - interface
+ - ntp
+ - dns
+ - ssh
+
+- name: Ensure all global system settings are configured
+ import_tasks: system.yml
+ delegate_to: localhost
+ ignore_errors: true
+
+- name: Ensure event logging has been configured
+ import_tasks: logging.yml
+ delegate_to: localhost
+ ignore_errors: true
+
+- name: Ensure drive and controller firmware are correct
+ import_tasks: firmware.yml
+ delegate_to: localhost
+ ignore_errors: true
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/security.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/security.yml
new file mode 100644
index 00000000..e0adfa85
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/security.yml
@@ -0,0 +1,66 @@
+- name: Ensure admin password has been set
+ na_santricity_auth:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ current_admin_password: "{{ eseries_system_password }}"
+ user: admin
+ password: "{{ eseries_system_password }}"
+ when: eseries_system_password is defined
+ tags:
+ - always
+
+- name: Ensure non-admin passwords have been set
+ na_santricity_auth:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ current_admin_password: "{{ eseries_system_password }}"
+ user: "{{ item['key'] }}"
+ password: "{{ item['value'] }}"
+ loop: "{{ lookup('dict', non_admin_user_authentication, wantlist=True) }}"
+ vars:
+ non_admin_user_authentication: |-
+ {%- set non_admin_list = {} %}
+ {%- if eseries_system_monitor_password is defined and eseries_system_monitor_password and non_admin_list.update({"monitor": eseries_system_monitor_password})%}{%- endif %}
+ {%- if eseries_system_security_password is defined and eseries_system_security_password and non_admin_list.update({"security": eseries_system_security_password})%}{%- endif %}
+ {%- if eseries_system_storage_password is defined and eseries_system_storage_password and non_admin_list.update({"storage": eseries_system_storage_password})%}{%- endif %}
+ {%- if eseries_system_support_password is defined and eseries_system_support_password and non_admin_list.update({"support": eseries_system_support_password})%}{%- endif %}
+ {{ non_admin_list }}
+
+- name: Ensure client-side certificates are installed
+ na_santricity_client_certificate:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ certificates: "{{ eseries_client_certificate_certificates }}"
+ when: eseries_client_certificate_certificates is defined
+ tags:
+ - security
+ - certificates
+
+- name: Ensure LDAP has been configured
+ na_santricity_ldap:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ state: "{{ eseries_ldap_state }}"
+ identifier: "{{ eseries_ldap_identifier | default(omit) }}"
+ server_url: "{{ eseries_ldap_server | default(omit) }}"
+ bind_user: "{{ eseries_ldap_bind_username | default(omit) }}"
+ bind_password: "{{ eseries_ldap_bind_password | default(omit) }}"
+ search_base: "{{ eseries_ldap_search_base | default(omit) }}"
+ user_attribute: "{{ eseries_ldap_user_attribute | default(omit) }}"
+ role_mappings: "{{ eseries_ldap_role_mappings | default(omit) }}"
+ when: eseries_ldap_state is defined
+ tags:
+ - security
+ - ldap \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/system.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/system.yml
new file mode 100644
index 00000000..920d94fd
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/roles/nar_santricity_management/tasks/system.yml
@@ -0,0 +1,16 @@
+- name: Ensure storage array has the correct array globals
+ na_santricity_global:
+ ssid: "{{ current_eseries_ssid }}"
+ api_url: "{{ current_eseries_api_url }}"
+ api_username: "{{ current_eseries_api_username }}"
+ api_password: "{{ current_eseries_api_password }}"
+ validate_certs: "{{ current_eseries_validate_certs | default(omit) }}"
+ name: "{{ eseries_system_name | default(omit) }}"
+ cache_block_size: "{{ eseries_system_cache_block_size | default(omit) }}"
+ cache_flush_threshold: "{{ eseries_system_cache_flush_threshold | default(omit) }}"
+ automatic_load_balancing: "{{ eseries_system_autoload_balance | default(omit) }}"
+ host_connectivity_reporting: "{{ eseries_system_host_connectivity_reporting | default(omit) }}"
+ default_host_type: "{{ eseries_system_default_host_type | default(omit)}}"
+ when: eseries_system_name is defined
+ tags:
+ - system
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/integration_config.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/integration_config.yml
new file mode 100644
index 00000000..8292ee42
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/integration_config.yml
@@ -0,0 +1,32 @@
+# url and credentials - all santricity modules will use this information
+ssid: "1"
+base_url: https://192.168.1.100:8443/devmgr/v2/
+username: admin
+password: adminPass
+validate_cert: false
+
+# proxy url and credentials - modules that require special api testing will use this information
+proxy_ssid: "10"
+proxy_legacy_ssid: "20"
+proxy_base_url: https://192.168.1.200:8443/devmgr/v2/
+proxy_username: admin
+proxy_password: ""
+proxy_validate_cert: false
+
+# na_santricity_auth module variable requirements in addition to both embedded and proxy credentials
+expected_serial_with_proxy_legacy: "711214012345"
+expected_serial_with_proxy_embedded: "021633012345"
+expected_serial_without_proxy: "021628012345"
+proxy_discover_subnet: 192.168.1.0/24
+systems:
+ - ssid: 10 # should match proxy_ssid above
+ addresses: ["192.168.1.110"]
+ - ssid: 20 # should match proxy_legacy_ssid above
+ addresses: ["192.168.1.120"]
+
+
+# na_santricity_ldap module variable requirements
+#bind_user: "CN=bind_user,OU=accounts,DC=test,DC=example,DC=com"
+#bind_password: "bind_password"
+#server_url: "ldap://test.example.com:389"
+#search_base: "OU=users,DC=test,DC=example,DC=com"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_alerts/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_alerts/tasks/main.yml
new file mode 100644
index 00000000..a5463ea8
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_alerts/tasks/main.yml
@@ -0,0 +1,117 @@
+# Test code for the na_santricity_alerts module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+
+- name: NetApp Test ASUP module
+ set_fact:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+
+- name: Disable alerts
+ na_santricity_alerts:
+ <<: *creds
+ state: disabled
+- name: Get the current device alerts
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/device-alerts"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ register: current_state
+- name: Determine whether the current state is expected
+ assert:
+ that: "{{ not current_state['json']['alertingEnabled'] }}"
+ msg: "Failed to disable alerts!"
+
+- name: Set the initial alerting settings (changed, check_mode)
+ na_santricity_alerts:
+ <<: *creds
+ state: enabled
+ server: mail.example.com
+ sender: noreply@example.com
+ recipients:
+ - noreply@example.com
+ register: result
+ check_mode: true
+- name: Get the current device alerts
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/device-alerts"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ register: current_state
+- name: Determine whether the current state is expected
+ assert:
+ that: "{{ result['changed'] and not current_state['json']['alertingEnabled'] }}"
+ msg: "Failed to disable alerts!"
+
+- name: Set the initial alerting settings (changed)
+ na_santricity_alerts:
+ <<: *creds
+ state: enabled
+ server: mail.example.com
+ sender: noreply@example.com
+ recipients:
+ - noreply@example.com
+ register: result
+- name: Get the current device alerts
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/device-alerts"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ register: current_state
+- name: Determine whether the current state is expected
+ assert:
+ that: "{{ result['changed'] and current_state['json']['alertingEnabled'] and
+ current_state['json']['emailServerAddress'] == 'mail.example.com' and
+ current_state['json']['emailSenderAddress'] == 'noreply@example.com' and
+ current_state['json']['recipientEmailAddresses'] == ['noreply@example.com'] }}"
+ msg: "Failed to enable alerts!"
+
+- name: Set to different alerting settings (changed)
+ na_santricity_alerts:
+ <<: *creds
+ state: enabled
+ server: mail2.example.com
+ sender: noreply2@example.com
+ recipients:
+ - noreply@example.com
+ - noreply2@example.com
+ register: result
+- name: Get the current device alerts
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/device-alerts"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ register: current_state
+- name: Determine whether the current state is expected
+ assert:
+ that: "{{ result['changed'] and current_state['json']['alertingEnabled'] and
+ current_state['json']['emailServerAddress'] == 'mail2.example.com' and
+ current_state['json']['emailSenderAddress'] == 'noreply2@example.com' and
+ (current_state['json']['recipientEmailAddresses'] == ['noreply@example.com', 'noreply2@example.com'] or
+ current_state['json']['recipientEmailAddresses'] == ['noreply2@example.com', 'noreply@example.com']) }}"
+ msg: "Failed to enable alerts!"
+
+- name: Disable alerts again (changed)
+ na_santricity_alerts:
+ <<: *creds
+ state: disabled
+ register: result
+- name: Get the current device alerts
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/device-alerts"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ register: current_state
+- name: Determine whether the current state is expected
+ assert:
+ that: "{{ result['changed'] and not current_state['json']['alertingEnabled'] }}"
+ msg: "Failed to disable alerts!"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_alerts_syslog/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_alerts_syslog/tasks/main.yml
new file mode 100644
index 00000000..34de206e
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_alerts_syslog/tasks/main.yml
@@ -0,0 +1,112 @@
+# Test code for the na_santricity_alerts_syslog module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+
+- name: Set facts for na_santricity_alerts_syslog module's intergration test.
+ set_fact:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+
+- name: Delete all alert syslog servers
+ na_santricity_alerts_syslog:
+ <<: *creds
+
+- name: Add alert syslog servers (change, check_mode)
+ na_santricity_alerts_syslog:
+ <<: *creds
+ servers:
+ - address: "192.168.1.100"
+ - address: "192.168.2.100"
+ port: 514
+ - address: "192.168.3.100"
+ port: 1000
+ check_mode: true
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Add alert syslog servers (change)
+ na_santricity_alerts_syslog:
+ <<: *creds
+ servers:
+ - address: "192.168.1.100"
+ - address: "192.168.2.100"
+ port: 514
+ - address: "192.168.3.100"
+ port: 1000
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Add alert syslog servers (no change)
+ na_santricity_alerts_syslog:
+ <<: *creds
+ test: true
+ servers:
+ - address: "192.168.1.100"
+ - address: "192.168.2.100"
+ port: 514
+ - address: "192.168.3.100"
+ port: 1000
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Remove one alert syslog server (change)
+ na_santricity_alerts_syslog:
+ <<: *creds
+ test: true
+ servers:
+ - address: "192.168.2.100"
+ port: 514
+ - address: "192.168.3.100"
+ port: 1000
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Add one alert syslog server (change)
+ na_santricity_alerts_syslog:
+ <<: *creds
+ test: true
+ servers:
+ - address: "192.168.1.100"
+ - address: "192.168.2.100"
+ port: 514
+ - address: "192.168.3.100"
+ port: 1000
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Delete all alert syslog servers (change)
+ na_santricity_alerts_syslog:
+ <<: *creds
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Delete all alert syslog servers (no change)
+ na_santricity_alerts_syslog:
+ <<: *creds
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_asup/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_asup/tasks/main.yml
new file mode 100644
index 00000000..fd66149f
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_asup/tasks/main.yml
@@ -0,0 +1,287 @@
+# Test code for the na_santricity_asup module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+- name: Set credential facts
+ set_fact:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+
+- name: Enable auto-support using default values
+ na_santricity_asup:
+ <<: *creds
+- name: Collect auto-support state information from the array
+ uri:
+ url: "{{ base_url }}device-asup"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+- name: Validate auto-support expected default state
+ assert:
+ that: "{{ current.json.asupEnabled and
+ current.json.onDemandEnabled and
+ current.json.remoteDiagsEnabled and
+ current.json.schedule.dailyMinTime == 0 and
+ current.json.schedule.dailyMaxTime == 1439 }}"
+ msg: "Unexpected auto-support state"
+- name: Validate auto-support schedule
+ assert:
+ that: "{{ item in current.json.schedule.daysOfWeek }}"
+ msg: "{{ item }} is missing from the schedule"
+ loop: "{{ lookup('list', ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']) }}"
+
+- name: Disable auto-support
+ na_santricity_asup:
+ <<: *creds
+ state: disabled
+- name: Collect auto-support state information from the array
+ uri:
+ url: "{{ base_url }}device-asup"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+- name: Validate auto-support is disabled
+ assert:
+ that: "{{ not current.json.asupEnabled }}"
+ msg: "Auto-support failed to be disabled"
+
+- name: Enable auto-support using specific values
+ na_santricity_asup:
+ <<: *creds
+ state: enabled
+ active: true
+ start: 22
+ end: 24
+ days:
+ - friday
+ - saturday
+- name: Collect auto-support state information from the array
+ uri:
+ url: "{{ base_url }}device-asup"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+- name: Validate auto-support expected state
+ assert:
+ that: "{{ current.json.asupEnabled and
+ current.json.onDemandEnabled and
+ current.json.remoteDiagsEnabled and
+ current.json.schedule.dailyMinTime == (22 * 60) and
+ current.json.schedule.dailyMaxTime == (24 * 60 - 1) }}"
+ msg: "Unexpected auto-support state"
+- name: Validate auto-support schedule
+ assert:
+ that: "{{ item in current.json.schedule.daysOfWeek }}"
+ msg: "{{ item }} is missing from the schedule"
+ loop: "{{ lookup('list', ['friday', 'saturday']) }}"
+
+- name: Auto auto-support schedule
+ na_santricity_asup:
+ <<: *creds
+ state: enabled
+ active: true
+ start: 0
+ end: 5
+ days:
+ - monday
+ - thursday
+ - sunday
+- name: Collect auto-support state information from the array
+ uri:
+ url: "{{ base_url }}device-asup"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+- name: Validate auto-support expected state
+ assert:
+ that: "{{ current.json.asupEnabled and
+ current.json.onDemandEnabled and
+ current.json.remoteDiagsEnabled and
+ current.json.schedule.dailyMinTime == (0 * 60) and
+ current.json.schedule.dailyMaxTime == (5 * 60) }}"
+ msg: "Unexpected auto-support state"
+- name: Validate auto-support schedule
+ assert:
+ that: "{{ item in current.json.schedule.daysOfWeek }}"
+ msg: "{{ item }} is missing from the schedule"
+ loop: "{{ lookup('list', ['monday', 'thursday', 'sunday']) }}"
+
+- name: Repeat auto-support schedule change to verify idempotency
+ na_santricity_asup:
+ <<: *creds
+ state: enabled
+ active: true
+ start: 0
+ end: 5
+ days:
+ - monday
+ - thursday
+ - sunday
+ register: result
+- name: Collect auto-support state information from the array
+ uri:
+ url: "{{ base_url }}device-asup"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+- name: Validate auto-support expected state
+ assert:
+ that: "{{ current.json.asupEnabled and
+ current.json.onDemandEnabled and
+ current.json.remoteDiagsEnabled and
+ current.json.schedule.dailyMinTime == (0 * 60) and
+ current.json.schedule.dailyMaxTime == (5 * 60) }}"
+ msg: "Unexpected auto-support state"
+- name: Validate auto-support schedule
+ assert:
+ that: "{{ item in current.json.schedule.daysOfWeek }}"
+ msg: "{{ item }} is missing from the schedule"
+ loop: "{{ lookup('list', ['monday', 'thursday', 'sunday']) }}"
+- name: Validate change was not detected
+ assert:
+ that: "{{ not result.changed }}"
+ msg: "Invalid change was detected"
+
+- name: Auto auto-support schedule
+ na_santricity_asup:
+ <<: *creds
+ state: enabled
+ active: false
+ start: 0
+ end: 5
+ days:
+ - monday
+ - thursday
+ - sunday
+- name: Collect auto-support state information from the array
+ uri:
+ url: "{{ base_url }}device-asup"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+- name: Validate auto-support expected state
+ assert:
+ that: "{{ current.json.asupEnabled and not current.json.onDemandEnabled and not current.json.remoteDiagsEnabled }}"
+ msg: "Unexpected auto-support state"
+
+- name: Auto auto-support direct delivery method
+ na_santricity_asup:
+ <<: *creds
+ state: enabled
+ method: http
+ routing_type: direct
+- name: Collect auto-support state information from the array
+ uri:
+ url: "{{ base_url }}device-asup"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+- name: Validate auto-support expected state
+ assert:
+ that: "{{ current['json']['delivery']['method'] == 'http' }}"
+ msg: "Delievery method should be http!"
+
+- name: Auto auto-support direct delivery method
+ na_santricity_asup:
+ <<: *creds
+ state: enabled
+ method: https
+ routing_type: direct
+- name: Collect auto-support state information from the array
+ uri:
+ url: "{{ base_url }}device-asup"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+- name: Validate auto-support expected state
+ assert:
+ that: "{{ current['json']['delivery']['method'] == 'https' }}"
+ msg: "Delievery method should be https!"
+
+- name: Auto auto-support proxy delivery method
+ na_santricity_asup:
+ <<: *creds
+ state: enabled
+ method: https
+ routing_type: proxy
+ proxy:
+ host: 192.168.1.1
+ port: 1000
+- name: Collect auto-support state information from the array
+ uri:
+ url: "{{ base_url }}device-asup"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+- name: Validate auto-support expected state
+ assert:
+ that: "{{ current['json']['delivery']['method'] == 'https' and
+ current['json']['delivery']['proxyHost'] == '192.168.1.1' and
+ current['json']['delivery']['proxyPort'] == 1000 }}"
+ msg: "Delievery method should be https-proxy-host!"
+
+- name: Auto auto-support proxy-script delivery method
+ na_santricity_asup:
+ <<: *creds
+ state: enabled
+ method: https
+ routing_type: script
+ proxy:
+ script: autosupport_script.sh
+- name: Collect auto-support state information from the array
+ uri:
+ url: "{{ base_url }}device-asup"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+- name: Validate auto-support expected state
+ assert:
+ that: "{{ current['json']['delivery']['method'] == 'https' and
+ current['json']['delivery']['proxyScript'] == 'autosupport_script.sh' }}"
+ msg: "Delievery method should be https-proxy-script!"
+
+- name: Auto auto-support email delivery method
+ na_santricity_asup:
+ <<: *creds
+ state: enabled
+ method: email
+ email:
+ server: server@example.com
+ sender: noreply@example.com
+- name: Collect auto-support state information from the array
+ uri:
+ url: "{{ base_url }}device-asup"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+- name: Validate auto-support expected state
+ assert:
+ that: "{{ current['json']['delivery']['method'] == 'smtp' and
+ current['json']['delivery']['mailRelayServer'] == 'server@example.com' and
+ current['json']['delivery']['mailSenderAddress'] == 'noreply@example.com' }}"
+ msg: "Delievery method should be email!"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_auditlog/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_auditlog/tasks/main.yml
new file mode 100644
index 00000000..424ba2e5
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_auditlog/tasks/main.yml
@@ -0,0 +1,220 @@
+# Test code for the na_santricity_auditlog module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+
+# Note: If audit-log is full then clear it before testing, since it can result in expect 422, symbol errors.
+- name: Set credential facts
+ set_fact:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ proxy_credentials: &proxy_creds
+ ssid: "PROXY"
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "{{ proxy_password }}"
+ validate_certs: "{{ proxy_validate_cert }}"
+ proxy_embedded_credentials: &proxy_embedded_creds
+ ssid: "{{ proxy_ssid }}"
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "{{ proxy_password }}"
+ validate_certs: "{{ proxy_validate_cert }}"
+
+- name: Set audit log settings to the defaults
+ na_santricity_auditlog:
+ <<: *creds
+- name: Retrieve current auditlog config settings
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/audit-log/config"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: config
+- name: Validate change status
+ assert:
+ that: "{{ config['json']['auditLogMaxRecords'] == 50000 and
+ config['json']['auditLogLevel'] == 'writeOnly' and
+ config['json']['auditLogFullPolicy'] == 'overWrite' and
+ config['json']['auditLogWarningThresholdPct'] == 90 }}"
+ msg: "Config settings are not correct!"
+
+- name: Change audit log settings. (change, check_mode)
+ na_santricity_auditlog:
+ <<: *creds
+ max_records: 50000
+ log_level: all
+ full_policy: preventSystemAccess
+ threshold: 60
+ register: result
+ check_mode: true
+- name: Retrieve current auditlog config settings
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/audit-log/config"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: config
+- name: Validate change status
+ assert:
+ that: "{{ result['changed'] and config['json']['auditLogMaxRecords'] == 50000 and
+ config['json']['auditLogLevel'] == 'writeOnly' and
+ config['json']['auditLogFullPolicy'] == 'overWrite' and
+ config['json']['auditLogWarningThresholdPct'] == 90 }}"
+ msg: "Config settings are not correct!"
+
+- name: Change audit log settings. (change)
+ na_santricity_auditlog:
+ <<: *creds
+ max_records: 10000
+ log_level: all
+ full_policy: preventSystemAccess
+ threshold: 60
+ register: result
+- name: Retrieve current auditlog config settings
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/audit-log/config"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: config
+- name: Validate change status
+ assert:
+ that: "{{ result['changed'] and config['json']['auditLogMaxRecords'] == 10000 and
+ config['json']['auditLogLevel'] == 'all' and
+ config['json']['auditLogFullPolicy'] == 'preventSystemAccess' and
+ config['json']['auditLogWarningThresholdPct'] == 60 }}"
+ msg: "Config settings are not correct!"
+
+- name: Set audit log settings to the defaults (proxy)
+ na_santricity_auditlog:
+ <<: *proxy_creds
+- name: Retrieve current auditlog config settings
+ uri:
+ url: "{{ proxy_base_url }}audit-log/config"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: config
+- name: Validate change status
+ assert:
+ that: "{{ config['json']['auditLogMaxRecords'] == 50000 and
+ config['json']['auditLogLevel'] == 'writeOnly' and
+ config['json']['auditLogFullPolicy'] == 'overWrite' and
+ config['json']['auditLogWarningThresholdPct'] == 90 }}"
+ msg: "Config settings are not correct!"
+
+- name: Change audit log settings. (proxy) (change, check_mode)
+ na_santricity_auditlog:
+ <<: *proxy_creds
+ max_records: 50000
+ log_level: all
+ full_policy: preventSystemAccess
+ threshold: 60
+ register: result
+ check_mode: true
+- name: Retrieve current auditlog config settings
+ uri:
+ url: "{{ proxy_base_url }}audit-log/config"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: config
+- name: Validate change status
+ assert:
+ that: "{{ result['changed'] and config['json']['auditLogMaxRecords'] == 50000 and
+ config['json']['auditLogLevel'] == 'writeOnly' and
+ config['json']['auditLogFullPolicy'] == 'overWrite' and
+ config['json']['auditLogWarningThresholdPct'] == 90 }}"
+ msg: "Config settings are not correct!"
+
+- name: Change audit log settings. (proxy) (change)
+ na_santricity_auditlog:
+ <<: *proxy_creds
+ max_records: 10000
+ log_level: all
+ full_policy: preventSystemAccess
+ threshold: 60
+ register: result
+- name: Retrieve current auditlog config settings
+ uri:
+ url: "{{ proxy_base_url }}audit-log/config"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: config
+- name: Validate change status
+ assert:
+ that: "{{ result['changed'] and config['json']['auditLogMaxRecords'] == 10000 and
+ config['json']['auditLogLevel'] == 'all' and
+ config['json']['auditLogFullPolicy'] == 'preventSystemAccess' and
+ config['json']['auditLogWarningThresholdPct'] == 60 }}"
+ msg: "Config settings are not correct!"
+
+- name: Set audit log settings to the defaults (proxy)
+ na_santricity_auditlog:
+ <<: *proxy_embedded_creds
+- name: Retrieve current auditlog config settings
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/audit-log/config"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: config
+- name: Validate change status
+ assert:
+ that: "{{ config['json']['auditLogMaxRecords'] == 50000 and
+ config['json']['auditLogLevel'] == 'writeOnly' and
+ config['json']['auditLogFullPolicy'] == 'overWrite' and
+ config['json']['auditLogWarningThresholdPct'] == 90 }}"
+ msg: "Config settings are not correct!"
+
+- name: Change audit log settings. (proxy) (change, check_mode)
+ na_santricity_auditlog:
+ <<: *proxy_embedded_creds
+ max_records: 50000
+ log_level: all
+ full_policy: preventSystemAccess
+ threshold: 60
+ register: result
+ check_mode: true
+- name: Retrieve current auditlog config settings
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/audit-log/config"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: config
+- name: Validate change status
+ assert:
+ that: "{{ result['changed'] and config['json']['auditLogMaxRecords'] == 50000 and
+ config['json']['auditLogLevel'] == 'writeOnly' and
+ config['json']['auditLogFullPolicy'] == 'overWrite' and
+ config['json']['auditLogWarningThresholdPct'] == 90 }}"
+ msg: "Config settings are not correct!"
+
+- name: Change audit log settings. (proxy) (change)
+ na_santricity_auditlog:
+ <<: *proxy_embedded_creds
+ max_records: 10000
+ log_level: all
+ full_policy: preventSystemAccess
+ threshold: 60
+ register: result
+- name: Retrieve current auditlog config settings
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/audit-log/config"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: config
+- name: Validate change status
+ assert:
+ that: "{{ result['changed'] and config['json']['auditLogMaxRecords'] == 10000 and
+ config['json']['auditLogLevel'] == 'all' and
+ config['json']['auditLogFullPolicy'] == 'preventSystemAccess' and
+ config['json']['auditLogWarningThresholdPct'] == 60 }}"
+ msg: "Config settings are not correct!"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_auth/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_auth/tasks/main.yml
new file mode 100644
index 00000000..12c55252
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_auth/tasks/main.yml
@@ -0,0 +1,170 @@
+# Test code for the na_santricity_alerts module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+
+# Clear embedded, legacy, and passwords before executing integration tests!
+
+- name: Set initial credential variables
+ set_fact:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ proxy_credentials: &proxy_creds
+ ssid: "{{ proxy_ssid }}"
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "{{ proxy_password }}"
+ validate_certs: "{{ proxy_validate_cert }}"
+ proxy_legacy_credentials: &proxy_legacy_creds
+ ssid: "{{ proxy_legacy_ssid }}"
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "{{ proxy_password }}"
+ validate_certs: "{{ proxy_validate_cert }}"
+
+# TODO: series of tests for embedded
+# Validate admin passwords are updated regardless of supplied api_password and current_admin_password options
+- name: Set storage system's initial admin password (embedded, changed)
+ na_santricity_auth:
+ <<: *creds
+ minimum_password_length: 8
+ password: infiniti
+ user: admin
+
+- name: Set storage system's non-admin passwords (embedded, changed)
+ na_santricity_auth:
+ <<: *creds
+ password: "{{ item }}_password"
+ user: "{{ item }}"
+ ignore_errors: true
+ loop: ["monitor", "support", "security", "storage"]
+
+- name: Set storage system's initial admin password (embedded, changed)
+ na_santricity_auth:
+ <<: *creds
+ minimum_password_length: 0
+ password: ""
+ user: admin
+
+- name: Set storage system's initial admin password (embedded, changed)
+ na_santricity_auth:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: ""
+ validate_certs: "{{ validate_cert }}"
+ minimum_password_length: 8
+
+- name: Set proxy's initial password (proxy, changed)
+ na_santricity_auth:
+ ssid: proxy
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "{{ proxy_password }}"
+ validate_certs: "{{ proxy_validate_cert }}"
+ password: infiniti
+ user: admin
+
+# # TODO: series of tests for proxy
+- name: Add storage systems to proxy without passwords
+ na_santricity_proxy_systems:
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "{{ proxy_password }}"
+ validate_certs: "{{ proxy_validate_cert }}"
+ subnet_mask: "{{ proxy_discover_subnet }}"
+ systems: "{{ systems }}"
+ password: ""
+
+# Validate proxy system's admin passwords are updated regardless of current_admin_password options
+- name: Set storage system's initial password (proxy system with embedded, changed)
+ na_santricity_auth:
+ <<: *proxy_creds
+ minimum_password_length: 8
+ current_admin_password: "" # THIS NEEDS TO MATCH STORAGE SYSTEM'S STORED-PASSWORD
+ password: infiniti
+ user: admin
+
+- name: Set storage system's initial password (proxy system without embedded, changed)
+ na_santricity_auth:
+ <<: *proxy_legacy_creds
+ minimum_password_length: 8
+ current_admin_password: "" # THIS NEEDS TO MATCH LEGACY STORAGE SYSTEM'S STORED-PASSWORD
+ password: infiniti
+ user: admin
+
+- pause: seconds=10
+
+- name: Set storage system's initial password (proxy system with embedded, changed)
+ na_santricity_auth:
+ ssid: "10"
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "{{ proxy_password }}"
+ validate_certs: "{{ proxy_validate_cert }}"
+ current_admin_password: infiniti # THIS NEEDS TO MATCH STORAGE SYSTEM'S STORED-PASSWORD
+ password: "{{ item }}_password"
+ user: "{{ item }}"
+ loop: ["monitor", "support", "security", "storage"]
+
+- name: Set storage system's initial password (proxy system with embedded, changed)
+ na_santricity_auth:
+ ssid: "10"
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "{{ proxy_password }}"
+ validate_certs: "{{ proxy_validate_cert }}"
+ current_admin_password: infiniti # THIS NEEDS TO MATCH STORAGE SYSTEM'S STORED-PASSWORD
+ minimum_password_length: 0
+ password: ""
+ user: admin
+
+- name: Set storage system's initial password (proxy system without embedded, changed)
+ na_santricity_auth:
+ ssid: "20"
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "{{ proxy_password }}"
+ validate_certs: "{{ proxy_validate_cert }}"
+ current_admin_password: infiniti # THIS NEEDS TO MATCH STORAGE SYSTEM'S STORED-PASSWORD
+ password: ""
+ user: admin
+
+- name: Set storage system's initial password (proxy system without embedded, changed)
+ na_santricity_auth:
+ ssid: proxy
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "{{ proxy_password }}"
+ validate_certs: "{{ proxy_validate_cert }}"
+ minimum_password_length: 0
+ password: ""
+ user: admin
+
+- name: Set storage system's initial password (proxy system with embedded, changed)
+ na_santricity_auth:
+ ssid: Proxy
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "" # THIS NEEDS TO MATCH PROXY'S PASSWORD
+ validate_certs: "{{ proxy_validate_cert }}"
+ minimum_password_length: 8
+
+- name: Set storage system's initial password (proxy system with embedded, changed)
+ na_santricity_auth:
+ ssid: "10"
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "" # THIS NEEDS TO MATCH PROXY'S PASSWORD
+ validate_certs: "{{ proxy_validate_cert }}"
+ minimum_password_length: 8
+
+- name: Remove storage system from proxy
+ na_santricity_proxy_systems:
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "" # THIS NEEDS TO MATCH PROXY'S PASSWORD
+ validate_certs: "{{ proxy_validate_cert }}"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_client_certificate/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_client_certificate/tasks/main.yml
new file mode 100644
index 00000000..9f3964d9
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_client_certificate/tasks/main.yml
@@ -0,0 +1,55 @@
+# Test code for the na_santricity_alerts module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+- name: Set credential facts
+ set_fact:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ certificates:
+ - "/home/swartzn/ExampleRootCA.crt"
+ - "/home/swartzn/ExampleIssuingCA.crt"
+ - "/home/swartzn/ExampleClient.crt"
+
+- name: Remove certificates
+ na_santricity_client_certificate:
+ <<: *creds
+
+- name: Upload certificate (changed, check_mode)
+ na_santricity_client_certificate:
+ <<: *creds
+ certificates: "{{ certificates }}"
+ register: result
+ check_mode: true
+- assert:
+ that: "{{ result['changed'] }}"
+ msg: "Failed to upload certificates to storage array."
+
+- name: Upload certificate (changed)
+ na_santricity_client_certificate:
+ <<: *creds
+ certificates: "{{ certificates }}"
+ register: result
+- assert:
+ that: "{{ result['changed'] }}"
+ msg: "Failed to upload certificates to storage array."
+
+- name: Repeat upload certificate (no change)
+ na_santricity_client_certificate:
+ <<: *creds
+ certificates: "{{ certificates }}"
+ register: result
+- assert:
+ that: "{{ not result['changed'] }}"
+ msg: "Failed not to make any changes."
+
+- name: Remove certificates
+ na_santricity_client_certificate:
+ <<: *creds
+ register: result
+- assert:
+ that: "{{ result['changed'] }}"
+ msg: "Failed to remove uploaded certificates" \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_discover/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_discover/tasks/main.yml
new file mode 100644
index 00000000..38c18f97
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_discover/tasks/main.yml
@@ -0,0 +1,64 @@
+# Test code for the na_santricity_discover module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+
+- name: Discover storage systems using SANtricity Web Services Proxy
+ na_santricity_discover:
+ proxy_url: "{{ proxy_base_url }}"
+ proxy_username: "{{ proxy_username }}"
+ proxy_password: "{{ proxy_password }}"
+ proxy_validate_certs: "{{ proxy_validate_cert }}"
+ subnet_mask: "{{ proxy_discover_subnet }}"
+ prefer_embedded: false
+ register: systems
+- name: find storage system
+ set_fact:
+ api_url: |-
+ {%- for system_serial in (systems["systems_found"].keys() | list) -%}
+ {%- if system_serial == expected_serial_with_proxy_legacy %}
+ {{- systems["systems_found"][system_serial]["api_urls"][0] -}}
+ {%- endif -%}
+ {%- endfor -%}
+- name: Verify storage system is found
+ fail:
+ msg: "Storage system was not discovered"
+ when: api_url == "" or api_url != proxy_base_url
+
+- name: Discover storage systems using SANtricity Web Services Proxy with a preference for embedded url
+ na_santricity_discover:
+ proxy_url: "{{ proxy_base_url }}"
+ proxy_username: "{{ proxy_username }}"
+ proxy_password: "{{ proxy_password }}"
+ proxy_validate_certs: "{{ proxy_validate_cert }}"
+ subnet_mask: "{{ proxy_discover_subnet }}"
+ prefer_embedded: true
+ register: systems
+- name: find storage system
+ set_fact:
+ api_url: |-
+ {%- for system_serial in (systems["systems_found"].keys() | list) -%}
+ {%- if system_serial == expected_serial_with_proxy_embedded %}
+ {{- systems["systems_found"][system_serial]["api_urls"][0] -}}
+ {%- endif -%}
+ {%- endfor -%}
+- name: Verify storage system is found
+ fail:
+ msg: "Storage system was not discovered"
+ when: api_url == "" or api_url == proxy_base_url
+
+- name: Discover storage systems not using SANtricity Web Services Proxy (requires SANtricity version 11.60.2 or later)
+ na_santricity_discover:
+ subnet_mask: "{{ proxy_discover_subnet }}"
+ register: systems
+- name: find storage system
+ set_fact:
+ api_url: |-
+ {%- for system_serial in (systems["systems_found"].keys() | list) -%}
+ {%- if system_serial == expected_serial_without_proxy %}
+ {{- systems["systems_found"][system_serial]["api_urls"][0] -}}
+ {%- endif -%}
+ {%- endfor -%}
+- name: Verify storage system is found
+ fail:
+ msg: "Storage system was not discovered"
+ when: api_url == ""
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_drive_firmware/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_drive_firmware/tasks/main.yml
new file mode 100644
index 00000000..5559691d
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_drive_firmware/tasks/main.yml
@@ -0,0 +1,185 @@
+# Test code for the na_santricity_drive_firmware module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+
+# Existing symbol issue: occasionally symbol will return 422 which causes Ansible to fail; however the drive firmware download will complete.
+# Work-around: Remove all storage provisioning before commencing test.
+
+- name: Set necessary credentials and other facts.
+ set_fact:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ firmware:
+ downgrade:
+ list:
+ - "/home/swartzn/Downloads/drive firmware/D_PX04SVQ160_DOWNGRADE_MS00toMSB6_801.dlp"
+ - "/home/swartzn/Downloads/drive firmware/D_ST1200MM0017_DNGRADE_MS02toMS00_6600_802.dlp"
+ check:
+ - firmware: "D_PX04SVQ160_DOWNGRADE_MS00toMSB6_801.dlp"
+ drive: "PX04SVQ160"
+ version: "MSB6"
+ - firmware: "D_ST1200MM0017_DNGRADE_MS02toMS00_6600_802.dlp"
+ drive: "ST1200MM0017"
+ version: "MS00"
+ upgrade:
+ list:
+ - "/home/swartzn/Downloads/drive firmware/D_PX04SVQ160_30603183_MS00_6600_001.dlp"
+ - "/home/swartzn/Downloads/drive firmware/D_ST1200MM0017_30602214_MS02_5600_002.dlp"
+ check:
+ - firmware: "D_PX04SVQ160_30603183_MS00_6600_001.dlp"
+ drive: "PX04SVQ160"
+ version: "MS00"
+ - firmware: "D_ST1200MM0017_30602214_MS02_5600_002.dlp"
+ drive: "ST1200MM0017"
+ version: "MS02"
+
+- name: Set drive firmware (baseline, maybe change)
+ netapp_e_drive_firmware:
+ <<: *creds
+ firmware: "{{ firmware['downgrade']['list'] }}"
+ wait_for_completion: true
+ ignore_inaccessible_drives: true
+ upgrade_drives_online: false
+ register: drive_firmware
+- pause: seconds=5
+- name: Retrieve current firmware version
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/drives"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: current_drive_firmware
+- name: Check if drive firmware is the expected versions
+ assert:
+ that: "{{ (item['productID'].strip() not in [firmware['downgrade']['check'][0]['drive'], firmware['downgrade']['check'][1]['drive']]) or
+ (firmware['downgrade']['check'][0]['drive'] == item['productID'].strip() and
+ firmware['downgrade']['check'][0]['version'] == item['softwareVersion']) or
+ (firmware['downgrade']['check'][1]['drive'] == item['productID'].strip() and
+ firmware['downgrade']['check'][1]['version'] == item['softwareVersion']) }}"
+ msg: "Drive firmware failed to update all drives"
+ loop: "{{ lookup('list', current_drive_firmware['json']) }}"
+
+- name: Set drive firmware (upgrade, change-checkmode)
+ netapp_e_drive_firmware:
+ <<: *creds
+ firmware: "{{ firmware['upgrade']['list'] }}"
+ wait_for_completion: true
+ ignore_inaccessible_drives: true
+ upgrade_drives_online: false
+ register: drive_firmware
+ check_mode: true
+- pause: seconds=5
+- name: Retrieve current firmware version
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/drives"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: current_drive_firmware
+- name: Validate change status
+ assert:
+ that: "{{ drive_firmware.changed }}"
+ msg: "Change status is incorrect."
+- name: Check if drive firmware is the expected versions
+ assert:
+ that: "{{ (item['productID'].strip() not in [firmware['downgrade']['check'][0]['drive'], firmware['downgrade']['check'][1]['drive']]) or
+ (firmware['downgrade']['check'][0]['drive'] == item['productID'].strip() and
+ firmware['downgrade']['check'][0]['version'] == item['softwareVersion']) or
+ (firmware['downgrade']['check'][1]['drive'] == item['productID'].strip() and
+ firmware['downgrade']['check'][1]['version'] == item['softwareVersion']) }}"
+ msg: "Drive firmware failed to update all drives"
+ loop: "{{ lookup('list', current_drive_firmware['json']) }}"
+
+- name: Set drive firmware (upgrade, change)
+ netapp_e_drive_firmware:
+ <<: *creds
+ firmware: "{{ firmware['upgrade']['list'] }}"
+ wait_for_completion: true
+ ignore_inaccessible_drives: true
+ upgrade_drives_online: false
+ register: drive_firmware
+- pause: seconds=5
+- name: Retrieve current firmware version
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/drives"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: current_drive_firmware
+- name: Validate change status
+ assert:
+ that: "{{ drive_firmware.changed }}"
+ msg: "Change status is incorrect."
+- name: Check if drive firmware is the expected versions
+ assert:
+ that: "{{ (item['productID'].strip() not in [firmware['downgrade']['check'][0]['drive'], firmware['downgrade']['check'][1]['drive']]) or
+ (firmware['upgrade']['check'][0]['drive'] == item['productID'].strip() and
+ firmware['upgrade']['check'][0]['version'] == item['softwareVersion']) or
+ (firmware['upgrade']['check'][1]['drive'] == item['productID'].strip() and
+ firmware['upgrade']['check'][1]['version'] == item['softwareVersion']) }}"
+ msg: "Drive firmware failed to update all drives"
+ loop: "{{ lookup('list', current_drive_firmware['json']) }}"
+
+- name: Set drive firmware (upgrade, no change)
+ netapp_e_drive_firmware:
+ <<: *creds
+ firmware: "{{ firmware['upgrade']['list'] }}"
+ wait_for_completion: true
+ ignore_inaccessible_drives: true
+ upgrade_drives_online: false
+ register: drive_firmware
+- pause: seconds=5
+- name: Retrieve current firmware version
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/drives"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: current_drive_firmware
+- name: Validate change status
+ assert:
+ that: "{{ not drive_firmware.changed }}"
+ msg: "Change status is incorrect."
+- name: Check if drive firmware is the expected versions
+ assert:
+ that: "{{ (item['productID'].strip() not in [firmware['downgrade']['check'][0]['drive'], firmware['downgrade']['check'][1]['drive']]) or
+ (firmware['upgrade']['check'][0]['drive'] == item['productID'].strip() and
+ firmware['upgrade']['check'][0]['version'] == item['softwareVersion']) or
+ (firmware['upgrade']['check'][1]['drive'] == item['productID'].strip() and
+ firmware['upgrade']['check'][1]['version'] == item['softwareVersion']) }}"
+ msg: "Drive firmware failed to update all drives"
+ loop: "{{ lookup('list', current_drive_firmware['json']) }}"
+
+- name: Set drive firmware (downgrade, change)
+ netapp_e_drive_firmware:
+ <<: *creds
+ firmware: "{{ firmware['downgrade']['list'] }}"
+ wait_for_completion: true
+ ignore_inaccessible_drives: true
+ upgrade_drives_online: false
+ register: drive_firmware
+- pause: seconds=5
+- name: Retrieve current firmware version
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/drives"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: current_drive_firmware
+- name: Validate change status
+ assert:
+ that: "{{ drive_firmware.changed }}"
+ msg: "Change status is incorrect."
+- name: Check if drive firmware is the expected versions
+ assert:
+ that: "{{ (item['productID'].strip() not in [firmware['downgrade']['check'][0]['drive'], firmware['downgrade']['check'][1]['drive']]) or
+ (firmware['downgrade']['check'][0]['drive'] == item['productID'].strip() and
+ firmware['downgrade']['check'][0]['version'] == item['softwareVersion']) or
+ (firmware['downgrade']['check'][1]['drive'] == item['productID'].strip() and
+ firmware['downgrade']['check'][1]['version'] == item['softwareVersion']) }}"
+ msg: "Drive firmware failed to update all drives"
+ loop: "{{ lookup('list', current_drive_firmware['json']) }}"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_facts/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_facts/tasks/main.yml
new file mode 100644
index 00000000..14cc43c6
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_facts/tasks/main.yml
@@ -0,0 +1,19 @@
+# Test code for the na_santricity_facts module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+
+- name: Retrieve facts from SANtricity Web Services Embedded
+ na_santricity_facts:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+
+- name: Retrieve facts from SANtricity Web Services Proxy
+ na_santricity_facts:
+ ssid: "{{ proxy_ssid }}"
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "{{ proxy_password }}"
+ validate_certs: "{{ proxy_validate_cert }}" \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_firmware/tasks/firmware_legacy_tests.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_firmware/tasks/firmware_legacy_tests.yml
new file mode 100644
index 00000000..6aff714c
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_firmware/tasks/firmware_legacy_tests.yml
@@ -0,0 +1,128 @@
+# Test code for the na_santricity_firmware module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+
+# TODO: MUST BE DOWNGRADE BEFORE EXECUTING INTEGRATION TO RCB_11.40.3R2_280x_5c7d81b3.dlp and N280X-842834-D02.dlp
+# loadControllerFirmware_MT swartzn@10.113.1.250 /home/swartzn/Downloads/RCB_11.40.3R2_280x_5c7d81b3.dlp /home/swartzn/Downloads/N280X-842834-D02.dlp
+
+# This integration test will validate upgrade functionality for firmware-only, firmware-and-nvsram, and check mode.
+- name: Set credentials and other facts
+ set_fact:
+ proxy_credentials: &proxy_creds
+ ssid: "{{ proxy_legacy_ssid }}"
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "{{ proxy_password }}"
+ validate_certs: "{{ proxy_validate_cert }}"
+ path: "/home/swartzn/Downloads/"
+ upgrades:
+ - firmware: "RC_08405000_m3_e10_840_5600.dlp"
+ nvsram: "N5600-840834-D03.dlp"
+ expected_firmware_version: "08.40.50.00"
+ expected_nvsram_version: "N5600-840834-D03"
+ - firmware: "RC_08403000_m3_e10_840_5600.dlp"
+ nvsram: "N5600-840834-D03.dlp"
+ expected_firmware_version: "08.40.30.00"
+ expected_nvsram_version: "N5600-840834-D03"
+
+- name: Perform firmware upgrade using the Web Services Proxy (changed, firmware)
+ na_santricity_firmware:
+ <<: *proxy_creds
+ nvsram: "{{ path }}{{ upgrades[1]['nvsram'] }}"
+ firmware: "{{ path }}{{ upgrades[1]['firmware'] }}"
+ wait_for_completion: true
+ clear_mel_events: true
+ register: results
+- name: Retrieve current firmware version
+ uri:
+ url: "{{ proxy_base_url }}storage-systems/{{ proxy_legacy_ssid }}/graph/xpath-filter?query=/sa/saData/fwVersion"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: current_firmware
+- name: Retrieve current nvsram version
+ uri:
+ url: "{{ proxy_base_url }}storage-systems/{{ proxy_legacy_ssid }}/graph/xpath-filter?query=/sa/saData/nvsramVersion"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: current_nvsram
+- name: Verify current firmware version
+ assert:
+ that: "{{ current_firmware['json'][0] == upgrades[1]['expected_firmware_version'] }}"
+ msg: "Failed to change the firmware version."
+- name: Verify current nvsram version
+ assert:
+ that: "{{ current_nvsram['json'][0] == upgrades[1]['expected_nvsram_version'] }}"
+ msg: "Failed to change the nvsram version."
+
+- name: Perform firmware upgrade using the Web Services Proxy (check_mode, changed, firmware)
+ na_santricity_firmware:
+ <<: *proxy_creds
+ nvsram: "{{ path }}{{ upgrades[0]['nvsram'] }}"
+ firmware: "{{ path }}{{ upgrades[0]['firmware'] }}"
+ wait_for_completion: true
+ clear_mel_events: true
+ register: results
+ check_mode: true
+- name: Retrieve current firmware version
+ uri:
+ url: "{{ proxy_base_url }}storage-systems/{{ proxy_legacy_ssid }}/graph/xpath-filter?query=/sa/saData/fwVersion"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: current_firmware
+- name: Retrieve current nvsram version
+ uri:
+ url: "{{ proxy_base_url }}storage-systems/{{ proxy_legacy_ssid }}/graph/xpath-filter?query=/sa/saData/nvsramVersion"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: current_nvsram
+- name: Verify change status
+ assert:
+ that: "{{ results.changed == True }}"
+ msg: "Failed to return changed."
+- name: Verify current firmware version
+ assert:
+ that: "{{ current_firmware['json'][0] == upgrades[1]['expected_firmware_version'] }}"
+ msg: "Failed to change the firmware version."
+- name: Verify current nvsram version
+ assert:
+ that: "{{ current_nvsram['json'][0] == upgrades[1]['expected_nvsram_version'] }}"
+ msg: "Failed to change the nvsram version."
+
+- name: Perform firmware upgrade using the Web Services Proxy (changed, firmware)
+ na_santricity_firmware:
+ <<: *proxy_creds
+ nvsram: "{{ path }}{{ upgrades[0]['nvsram'] }}"
+ firmware: "{{ path }}{{ upgrades[0]['firmware'] }}"
+ wait_for_completion: true
+ clear_mel_events: true
+ register: results
+- name: Retrieve current firmware version
+ uri:
+ url: "{{ proxy_base_url }}storage-systems/{{ proxy_legacy_ssid }}/graph/xpath-filter?query=/sa/saData/fwVersion"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: current_firmware
+- name: Retrieve current nvsram version
+ uri:
+ url: "{{ proxy_base_url }}storage-systems/{{ proxy_legacy_ssid }}/graph/xpath-filter?query=/sa/saData/nvsramVersion"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: current_nvsram
+- name: Verify change status
+ assert:
+ that: "{{ results.changed == True }}"
+ msg: "Failed to return changed."
+- name: Verify current firmware version
+ assert:
+ that: "{{ current_firmware['json'][0] == upgrades[0]['expected_firmware_version'] }}"
+ msg: "Failed to change the firmware version."
+- name: Verify current nvsram version
+ assert:
+ that: "{{ current_nvsram['json'][0] == upgrades[0]['expected_nvsram_version'] }}"
+ msg: "Failed to change the nvsram version."
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_firmware/tasks/firmware_tests.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_firmware/tasks/firmware_tests.yml
new file mode 100644
index 00000000..99827e1b
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_firmware/tasks/firmware_tests.yml
@@ -0,0 +1,320 @@
+# Test code for the na_santricity_firmware module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+
+# TODO: MUST BE DOWNGRADE BEFORE EXECUTING INTEGRATION TO RCB_11.40.3R2_280x_5c7d81b3.dlp and N280X-842834-D02.dlp
+# loadControllerFirmware_MT swartzn@10.113.1.250 /home/swartzn/Downloads/RCB_11.40.3R2_280x_5c7d81b3.dlp /home/swartzn/Downloads/N280X-842834-D02.dlp
+
+# This integration test will validate upgrade functionality for firmware-only, firmware-and-nvsram, and check mode.
+- name: Set credentials and other facts
+ set_fact:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ proxy_credentials: &proxy_creds
+ ssid: "{{ proxy_ssid }}"
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "{{ proxy_password }}"
+ validate_certs: "{{ proxy_validate_cert }}"
+ path: "/home/swartzn/Downloads/"
+ upgrades:
+ - firmware: "RCB_11.40.3R2_280x_5c7d81b3.dlp"
+ nvsram: "N280X-842834-D02.dlp"
+ expected_firmware_version: "08.42.30.05"
+ expected_nvsram_version: "N280X-842834-D02"
+ - firmware: "RCB_11.40.5_280x_5ceef00e.dlp"
+ nvsram: "N280X-842834-D02.dlp"
+ expected_firmware_version: "08.42.50.00"
+ expected_nvsram_version: "N280X-842834-D02"
+ - firmware: "RCB_11.50.2_280x_5ce8501f.dlp"
+ nvsram: "N280X-852834-D02.dlp"
+ expected_firmware_version: "08.52.00.00"
+ expected_nvsram_version: "N280X-852834-D02"
+
+- name: Perform firmware upgrade using the Web Services REST API (checkmode-no change, firmware only)
+ na_santricity_firmware:
+ <<: *creds
+ nvsram: "{{ path }}{{ upgrades[0]['nvsram'] }}"
+ firmware: "{{ path }}{{ upgrades[0]['firmware'] }}"
+ wait_for_completion: true
+ clear_mel_events: true
+ check_mode: true
+ register: results
+- name: Retrieve current firmware version
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/graph/xpath-filter?query=/sa/saData/fwVersion"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: current_firmware
+- name: Retrieve current nvsram version
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/graph/xpath-filter?query=/sa/saData/nvsramVersion"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: current_nvsram
+- name: Verify change status
+ assert:
+ that: "{{ results.changed == False }}"
+ msg: "Failed to return unchanged."
+- name: Verify current firmware version
+ assert:
+ that: "{{ current_firmware['json'][0] == upgrades[0]['expected_firmware_version'] }}"
+ msg: "Unexpected firmware version."
+- name: Verify current nvsram version
+ assert:
+ that: "{{ current_nvsram['json'][0] == upgrades[0]['expected_nvsram_version'] }}"
+ msg: "Unexpected nvsram version."
+
+- name: Perform firmware upgrade using the Web Services REST API (no change, firmware only)
+ na_santricity_firmware:
+ <<: *creds
+ nvsram: "{{ path }}{{ upgrades[0]['nvsram'] }}"
+ firmware: "{{ path }}{{ upgrades[0]['firmware'] }}"
+ wait_for_completion: true
+ clear_mel_events: true
+ register: results
+- name: Retrieve current firmware version
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/graph/xpath-filter?query=/sa/saData/fwVersion"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: current_firmware
+- name: Retrieve current nvsram version
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/graph/xpath-filter?query=/sa/saData/nvsramVersion"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: current_nvsram
+- name: Verify change status
+ assert:
+ that: "{{ results.changed == False }}"
+ msg: "Failed to return changed."
+- name: Verify current firmware version
+ assert:
+ that: "{{ current_firmware['json'][0] == upgrades[0]['expected_firmware_version'] }}"
+ msg: "Unexpected firmware version."
+- name: Verify current nvsram version
+ assert:
+ that: "{{ current_nvsram['json'][0] == upgrades[0]['expected_nvsram_version'] }}"
+ msg: "Unexpected nvsram version."
+
+- name: Perform firmware upgrade using the Web Services REST API (checkmode-change, firmware)
+ na_santricity_firmware:
+ <<: *creds
+ nvsram: "{{ path }}{{ upgrades[1]['nvsram'] }}"
+ firmware: "{{ path }}{{ upgrades[1]['firmware'] }}"
+ wait_for_completion: true
+ clear_mel_events: true
+ register: results
+ check_mode: true
+- name: Retrieve current firmware version
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/graph/xpath-filter?query=/sa/saData/fwVersion"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: current_firmware
+- name: Retrieve current nvsram version
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/graph/xpath-filter?query=/sa/saData/nvsramVersion"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: current_nvsram
+- name: Verify change status
+ assert:
+ that: "{{ results.changed == True }}"
+ msg: "Failed to return changed."
+- name: Verify current firmware version
+ assert:
+ that: "{{ current_firmware['json'][0] == upgrades[0]['expected_firmware_version'] }}"
+ msg: "Unexpected firmware version."
+- name: Verify current nvsram version
+ assert:
+ that: "{{ current_nvsram['json'][0] == upgrades[0]['expected_nvsram_version'] }}"
+ msg: "Unexpected nvsram version."
+
+- name: Perform firmware upgrade using the Web Services REST API (change, firmware)
+ na_santricity_firmware:
+ <<: *creds
+ nvsram: "{{ path }}{{ upgrades[1]['nvsram'] }}"
+ firmware: "{{ path }}{{ upgrades[1]['firmware'] }}"
+ wait_for_completion: true
+ clear_mel_events: true
+ register: results
+- name: Retrieve current firmware version
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/graph/xpath-filter?query=/sa/saData/fwVersion"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: current_firmware
+- name: Retrieve current nvsram version
+ uri:
+ url: "{{ base_url }}storage-systems/{{ ssid }}/graph/xpath-filter?query=/sa/saData/nvsramVersion"
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: no
+ register: current_nvsram
+- name: Verify change status
+ assert:
+ that: "{{ results.changed == True }}"
+ msg: "Failed to return changed."
+- name: Verify current firmware version
+ assert:
+ that: "{{ current_firmware['json'][0] == upgrades[1]['expected_firmware_version'] }}"
+ msg: "Unexpected firmware version. {{ current_firmware['json'][0] }} != {{ upgrades[1]['expected_firmware_version'] }}"
+- name: Verify current nvsram version
+ assert:
+ that: "{{ current_nvsram['json'][0] == upgrades[1]['expected_nvsram_version'] }}"
+ msg: "Unexpected nvsram version. {{ current_nvsram['json'][0] }} != {{ upgrades[1]['expected_nvsram_version'] }}"
+
+- name: Perform firmware upgrade using the Web Services Proxy (changed, firmware)
+ na_santricity_firmware:
+ <<: *proxy_creds
+ nvsram: "{{ path }}{{ upgrades[0]['nvsram'] }}"
+ firmware: "{{ path }}{{ upgrades[0]['firmware'] }}"
+ wait_for_completion: true
+ clear_mel_events: true
+ register: results
+- name: Retrieve current firmware version
+ uri:
+ url: "{{ proxy_base_url }}storage-systems/{{ proxy_ssid }}/graph/xpath-filter?query=/sa/saData/fwVersion"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: current_firmware
+- name: Retrieve current nvsram version
+ uri:
+ url: "{{ proxy_base_url }}storage-systems/{{ proxy_ssid }}/graph/xpath-filter?query=/sa/saData/nvsramVersion"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: current_nvsram
+- name: Verify change status
+ assert:
+ that: "{{ results.changed == True }}"
+ msg: "Failed to return changed."
+- name: Verify current firmware version
+ assert:
+ that: "{{ current_firmware['json'][0] == upgrades[0]['expected_firmware_version'] }}"
+ msg: "Failed to change the firmware version."
+- name: Verify current nvsram version
+ assert:
+ that: "{{ current_nvsram['json'][0] == upgrades[0]['expected_nvsram_version'] }}"
+ msg: "Failed to change the nvsram version."
+
+- name: Perform firmware upgrade using the Web Services REST API (checkmode-unchanged, firmware)
+ na_santricity_firmware:
+ <<: *proxy_creds
+ nvsram: "{{ path }}{{ upgrades[0]['nvsram'] }}"
+ firmware: "{{ path }}{{ upgrades[0]['firmware'] }}"
+ wait_for_completion: true
+ clear_mel_events: true
+ check_mode: true
+ register: results
+- name: Retrieve current firmware version
+ uri:
+ url: "{{ proxy_base_url }}storage-systems/{{ proxy_ssid }}/graph/xpath-filter?query=/sa/saData/fwVersion"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: current_firmware
+- name: Retrieve current nvsram version
+ uri:
+ url: "{{ proxy_base_url }}storage-systems/{{ proxy_ssid }}/graph/xpath-filter?query=/sa/saData/nvsramVersion"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: current_nvsram
+- name: Verify change status
+ assert:
+ that: "{{ results.changed == False }}"
+ msg: "Failed to return unchanged."
+- name: Verify current firmware version
+ assert:
+ that: "{{ current_firmware['json'][0] == upgrades[0]['expected_firmware_version'] }}"
+ msg: "Failed to change the firmware version."
+- name: Verify current nvsram version
+ assert:
+ that: "{{ current_nvsram['json'][0] == upgrades[0]['expected_nvsram_version'] }}"
+ msg: "Failed to change the nvsram version."
+
+- name: Perform firmware upgrade using the Web Services REST API (checkmode-change, firmware and nvsram)
+ na_santricity_firmware:
+ <<: *proxy_creds
+ nvsram: "{{ path }}{{ upgrades[2]['nvsram'] }}"
+ firmware: "{{ path }}{{ upgrades[2]['firmware'] }}"
+ wait_for_completion: true
+ clear_mel_events: true
+ check_mode: true
+ register: results
+- name: Retrieve current firmware version
+ uri:
+ url: "{{ proxy_base_url }}storage-systems/{{ proxy_ssid }}/graph/xpath-filter?query=/sa/saData/fwVersion"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: current_firmware
+- name: Retrieve current nvsram version
+ uri:
+ url: "{{ proxy_base_url }}storage-systems/{{ proxy_ssid }}/graph/xpath-filter?query=/sa/saData/nvsramVersion"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: current_nvsram
+- name: Verify change status
+ assert:
+ that: "{{ results.changed == True }}"
+ msg: "Failed to return changed."
+- name: Verify current firmware version
+ assert:
+ that: "{{ current_firmware['json'][0] == upgrades[0]['expected_firmware_version'] }}"
+ msg: "Failed to change the firmware version."
+- name: Verify current nvsram version
+ assert:
+ that: "{{ current_nvsram['json'][0] == upgrades[0]['expected_nvsram_version'] }}"
+ msg: "Failed to change the nvsram version."
+
+- name: Perform firmware upgrade using the Web Services REST API (changed, firmware and nvsram)
+ na_santricity_firmware:
+ <<: *proxy_creds
+ nvsram: "{{ path }}{{ upgrades[2]['nvsram'] }}"
+ firmware: "{{ path }}{{ upgrades[2]['firmware'] }}"
+ wait_for_completion: true
+ clear_mel_events: true
+ register: results
+- name: Retrieve current firmware version
+ uri:
+ url: "{{ proxy_base_url }}storage-systems/{{ proxy_ssid }}/graph/xpath-filter?query=/sa/saData/fwVersion"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: current_firmware
+- name: Retrieve current nvsram version
+ uri:
+ url: "{{ proxy_base_url }}storage-systems/{{ proxy_ssid }}/graph/xpath-filter?query=/sa/saData/nvsramVersion"
+ user: "{{ proxy_username }}"
+ password: "{{ proxy_password }}"
+ validate_certs: no
+ register: current_nvsram
+- name: Verify change status
+ assert:
+ that: "{{ results.changed == True }}"
+ msg: "Failed to return changed."
+- name: Verify current firmware version
+ assert:
+ that: "{{ current_firmware['json'][0] == upgrades[2]['expected_firmware_version'] }}"
+ msg: "Failed to change the firmware version."
+- name: Verify current nvsram version
+ assert:
+ that: "{{ current_nvsram['json'][0] == upgrades[2]['expected_nvsram_version'] }}"
+ msg: "Failed to change the nvsram version."
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_firmware/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_firmware/tasks/main.yml
new file mode 100644
index 00000000..15edc520
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_firmware/tasks/main.yml
@@ -0,0 +1,2 @@
+- include_tasks: firmware_tests.yml
+- include_tasks: firmware_legacy_tests.yml
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_global/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_global/tasks/main.yml
new file mode 100644
index 00000000..9d6e6df9
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_global/tasks/main.yml
@@ -0,0 +1,185 @@
+# Test code for the nac_sancticity_global module.
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+- include_vars: "../../integration_config.yml"
+
+- name: Set initial global settings
+ na_santricity_global:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ name: arrayname01
+ cache_block_size: 32768
+ cache_flush_threshold: 80
+ automatic_load_balancing: disabled
+ host_connectivity_reporting: disabled
+ default_host_type: linux dm-mp
+- name: Retrieve the current array graph
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url }}storage-systems/{{ ssid }}/graph/xpath-filter?query=/sa"
+ register: graph
+- name: Validate initial global settings
+ assert:
+ that: "{{ graph['json'][0]['saData']['storageArrayLabel'] == 'arrayname01' and
+ graph['json'][0]['cache']['cacheBlkSize'] == 32768 and
+ graph['json'][0]['cache']['demandFlushThreshold'] == 80 and
+ not graph['json'][0]['autoLoadBalancingEnabled'] and
+ not graph['json'][0]['hostConnectivityReportingEnabled'] and
+ graph['json'][0]['defaultHostTypeIndex'] == 28 }}"
+ msg: "Failed to set initial global settings"
+
+- name: Repeat initial global settings
+ na_santricity_global:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ name: arrayname01
+ cache_block_size: 32768
+ cache_flush_threshold: 80
+ automatic_load_balancing: disabled
+ host_connectivity_reporting: disabled
+ default_host_type: linux dm-mp
+ register: result
+- name: Retrieve the current array graph
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url }}storage-systems/{{ ssid }}/graph/xpath-filter?query=/sa"
+ register: graph
+- name: Validate initial global settings
+ assert:
+ that: "{{ not result.changed and
+ graph['json'][0]['saData']['storageArrayLabel'] == 'arrayname01' and
+ graph['json'][0]['cache']['cacheBlkSize'] == 32768 and
+ graph['json'][0]['cache']['demandFlushThreshold'] == 80 and
+ not graph['json'][0]['autoLoadBalancingEnabled'] and
+ not graph['json'][0]['hostConnectivityReportingEnabled'] and
+ graph['json'][0]['defaultHostTypeIndex'] == 28 }}"
+ msg: "Failed to set initial global settings"
+
+- name: Change global settings (check-mode)
+ na_santricity_global:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ name: arrayname02
+ cache_block_size: 8192
+ cache_flush_threshold: 60
+ automatic_load_balancing: disabled
+ host_connectivity_reporting: disabled
+ default_host_type: windows
+ check_mode: true
+ register: result
+- name: Retrieve the current array graph
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url }}storage-systems/{{ ssid }}/graph/xpath-filter?query=/sa"
+ register: graph
+- name: Validate initial global settings
+ assert:
+ that: "{{ result.changed and
+ graph['json'][0]['saData']['storageArrayLabel'] == 'arrayname01' and
+ graph['json'][0]['cache']['cacheBlkSize'] == 32768 and
+ graph['json'][0]['cache']['demandFlushThreshold'] == 80 and
+ not graph['json'][0]['autoLoadBalancingEnabled'] and
+ not graph['json'][0]['hostConnectivityReportingEnabled'] and
+ graph['json'][0]['defaultHostTypeIndex'] == 28 }}"
+ msg: "Failed to set initial global settings"
+
+- name: Change global settings
+ na_santricity_global:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ name: arrayname02
+ cache_block_size: 8192
+ cache_flush_threshold: 60
+ automatic_load_balancing: disabled
+ host_connectivity_reporting: disabled
+ default_host_type: windows
+ register: result
+- name: Retrieve the current array graph
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url }}storage-systems/{{ ssid }}/graph/xpath-filter?query=/sa"
+ register: graph
+- name: Validate initial global settings
+ assert:
+ that: "{{ result.changed and
+ graph['json'][0]['saData']['storageArrayLabel'] == 'arrayname02' and
+ graph['json'][0]['cache']['cacheBlkSize'] == 8192 and
+ graph['json'][0]['cache']['demandFlushThreshold'] == 60 and
+ not graph['json'][0]['autoLoadBalancingEnabled'] and
+ not graph['json'][0]['hostConnectivityReportingEnabled'] and
+ graph['json'][0]['defaultHostTypeIndex'] == 1 }}"
+ msg: "Failed to set initial global settings"
+
+- name: Turn on autoload balancing which should force enable host connection reporting
+ na_santricity_global:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ automatic_load_balancing: enabled
+ register: result
+- name: Retrieve the current array graph
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url }}storage-systems/{{ ssid }}/graph/xpath-filter?query=/sa"
+ register: graph
+- name: Validate initial global settings
+ assert:
+ that: "{{ result.changed and
+ graph['json'][0]['saData']['storageArrayLabel'] == 'arrayname02' and
+ graph['json'][0]['cache']['cacheBlkSize'] == 8192 and
+ graph['json'][0]['cache']['demandFlushThreshold'] == 60 and
+ graph['json'][0]['autoLoadBalancingEnabled'] and
+ graph['json'][0]['hostConnectivityReportingEnabled'] and
+ graph['json'][0]['defaultHostTypeIndex'] == 1 }}"
+ msg: "Failed to set initial global settings"
+
+- name: Change array name only
+ na_santricity_global:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ name: arrayname03
+ register: result
+- name: Retrieve the current array graph
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url }}storage-systems/{{ ssid }}/graph/xpath-filter?query=/sa"
+ register: graph
+- name: Validate initial global settings
+ assert:
+ that: "{{ result.changed and
+ graph['json'][0]['saData']['storageArrayLabel'] == 'arrayname03' and
+ graph['json'][0]['cache']['cacheBlkSize'] == 8192 and
+ graph['json'][0]['cache']['demandFlushThreshold'] == 60 and
+ graph['json'][0]['autoLoadBalancingEnabled'] and
+ graph['json'][0]['hostConnectivityReportingEnabled'] and
+ graph['json'][0]['defaultHostTypeIndex'] == 1 }}"
+ msg: "Failed to set initial global settings"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_host/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_host/tasks/main.yml
new file mode 100644
index 00000000..16ae2263
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_host/tasks/main.yml
@@ -0,0 +1,189 @@
+# Test code for the na_santricity_host module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+- name: Set facts for na_santricity_host module's intergration test.
+ set_fact:
+ vars:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+
+- name: Create iSCSI host
+ na_santricity_host:
+ <<: *creds
+ name: windows_iscsi_host
+ host_type: Windows
+ ports:
+ - type: iscsi
+ label: iscsi_p1
+ port: iqn.windows.host.com.1
+ - type: iscsi
+ label: iscsi_p2
+ port: iqn.windows.host.com.2
+
+- name: Create FC host
+ na_santricity_host:
+ <<: *creds
+ name: linux_fc_host
+ host_type: Linux dm-mp
+ ports:
+ - type: fc
+ label: fc_p1
+ port: "0x1122334455667788"
+ - type: fc
+ label: fc_p2
+ port: "01:23:45:67:89:1a:bc:de"
+
+- name: (Repeat) Create iSCSI host (no change)
+ na_santricity_host:
+ <<: *creds
+ name: windows_iscsi_host
+ host_type: Windows
+ ports:
+ - type: iscsi
+ label: iscsi_p1
+ port: iqn.windows.host.com.1
+ - type: iscsi
+ label: iscsi_p2
+ port: iqn.windows.host.com.2
+ register: results
+- name: Verify no changes were made
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Create FC host (no change)
+ na_santricity_host:
+ <<: *creds
+ name: linux_fc_host
+ host_type: Linux dm-mp
+ ports:
+ - type: fc
+ label: fc_p1
+ port: "0x1122334455667788"
+ - type: fc
+ label: fc_p2
+ port: "01:23:45:67:89:1a:bc:de"
+ register: results
+- name: Verify no changes were made
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Create FC host with an used port (change, check_mode)
+ na_santricity_host:
+ <<: *creds
+ name: linux_fc2_host
+ host_type: Linux dm-mp
+ force_port: true
+ ports:
+ - type: fc
+ label: fc2_p1
+ port: "0x1122334455667788"
+ check_mode: true
+ register: results
+- name: Verify changes were made
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Create FC host with an used port (change)
+ na_santricity_host:
+ <<: *creds
+ name: linux_fc2_host
+ host_type: Linux dm-mp
+ force_port: true
+ ports:
+ - type: fc
+ label: fc2_p1
+ port: "0x1122334455667788"
+ register: results
+- name: Verify changes were made
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Create FC host with an used port (no change)
+ na_santricity_host:
+ <<: *creds
+ name: linux_fc2_host
+ host_type: Linux dm-mp
+ force_port: true
+ ports:
+ - type: fc
+ label: fc2_p1
+ port: "0x1122334455667788"
+ register: results
+- name: Verify no changes were made
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Delete iSCSI host (changed)
+ na_santricity_host:
+ <<: *creds
+ state: absent
+ name: windows_iscsi_host
+ register: results
+- name: Verify changes were made
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Delete FC host (changed)
+ na_santricity_host:
+ <<: *creds
+ state: absent
+ name: linux_fc_host
+ register: results
+- name: Verify changes were made
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Delete second FC host (changed)
+ na_santricity_host:
+ <<: *creds
+ state: absent
+ name: linux_fc2_host
+ register: results
+- name: Verify changes were made
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Delete iSCSI host (no change)
+ na_santricity_host:
+ <<: *creds
+ state: absent
+ name: windows_iscsi_host
+ register: results
+- name: Verify no changes were made
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Delete second FC host (no change)
+ na_santricity_host:
+ <<: *creds
+ state: absent
+ name: linux_fc_host
+ register: results
+- name: Verify no changes were made
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Delete FC host (no change)
+ na_santricity_host:
+ <<: *creds
+ state: absent
+ name: linux_fc2_host
+ register: results
+- name: Verify no changes were made
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_hostgroup/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_hostgroup/tasks/main.yml
new file mode 100644
index 00000000..8a2af77d
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_hostgroup/tasks/main.yml
@@ -0,0 +1,137 @@
+# Test code for the na_santricity_hostgroup module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+- name: Set facts for na_santricity_host module's intergration test.
+ set_fact:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+
+- name: Setup hosts for the groups
+ block:
+ - name: Create iSCSI host
+ na_santricity_host:
+ <<: *creds
+ name: windows_iscsi_host
+ host_type: Windows
+ ports:
+ - type: iscsi
+ label: iscsi_p1
+ port: iqn.windows.host.com.1
+ - type: iscsi
+ label: iscsi_p2
+ port: iqn.windows.host.com.2
+ - name: Create FC host
+ na_santricity_host:
+ <<: *creds
+ name: linux_fc_host
+ host_type: Linux dm-mp
+ ports:
+ - type: fc
+ label: fc_p1
+ port: "0x1122334455667788"
+ - type: fc
+ label: fc_p2
+ port: "01:23:45:67:89:1a:bc:de"
+
+- name: Create host group and add one host (change)
+ na_santricity_hostgroup:
+ <<: *creds
+ name: hostgroup_test
+ hosts:
+ - windows_iscsi_host
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Create host group and add one host (no change)
+ na_santricity_hostgroup:
+ <<: *creds
+ name: hostgroup_test
+ hosts:
+ - windows_iscsi_host
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Add one host (change, check_mode)
+ na_santricity_hostgroup:
+ <<: *creds
+ name: hostgroup_test
+ hosts:
+ - windows_iscsi_host
+ - linux_fc_host
+ register: results
+ check_mode: true
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Add one host (change, check_mode)
+ na_santricity_hostgroup:
+ <<: *creds
+ name: hostgroup_test
+ hosts:
+ - windows_iscsi_host
+ - linux_fc_host
+ register: results
+ check_mode: true
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Add one host (change)
+ na_santricity_hostgroup:
+ <<: *creds
+ name: hostgroup_test
+ hosts:
+ - windows_iscsi_host
+ - linux_fc_host
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Remove one host (change)
+ na_santricity_hostgroup:
+ <<: *creds
+ name: hostgroup_test
+ hosts:
+ - linux_fc_host
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Delete host group (change)
+ na_santricity_hostgroup:
+ <<: *creds
+ state: absent
+ name: hostgroup_test
+
+- name: Delete hosts for the groups
+ block:
+ - name: Delete iSCSI host
+ na_santricity_host:
+ <<: *creds
+ state: absent
+ name: windows_iscsi_host
+ register: results
+
+ - name: Delete FC host
+ na_santricity_host:
+ <<: *creds
+ state: absent
+ name: linux_fc_host
+ register: results
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_ib_iser_interface/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_ib_iser_interface/tasks/main.yml
new file mode 100644
index 00000000..d2d8142b
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_ib_iser_interface/tasks/main.yml
@@ -0,0 +1,88 @@
+# Test code for the na_santricity_ib_iser_interface module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+- name: Set facts for na_santricity_ib_iser_interface module test
+ set_fact:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ interface_a1_ip: &a1_ip 192.168.1.101
+ interface_a2_ip: &a2_ip 192.168.2.101
+
+- name: Set the initial ib_iser interfaces
+ na_santricity_ib_iser_interface:
+ <<: *creds
+ controller: "{{ item[0] }}"
+ channel: "{{ item[1] }}"
+ address: "{{ item[2] }}"
+ loop:
+ - ["A", "1", *a1_ip]
+ - ["B", "1", *a2_ip]
+
+- name: Repeat the initial ib_iser interfaces (no change)
+ na_santricity_ib_iser_interface:
+ <<: *creds
+ controller: "{{ item[0] }}"
+ channel: "{{ item[1] }}"
+ address: "{{ item[2] }}"
+ register: results
+ loop:
+ - ["A", "1", *a1_ip]
+ - ["B", "1", *a2_ip]
+- name: Verify no changes were made
+ assert:
+ that: "{{ not item['changed'] }}"
+ msg: "Unexpected results!"
+ loop: "{{ lookup('list', results['results']) }}"
+
+- name: Change the initial ib_iser interfaces (changed, check_mode)
+ na_santricity_ib_iser_interface:
+ <<: *creds
+ controller: "{{ item[0] }}"
+ channel: "{{ item[1] }}"
+ address: "{{ item[2] }}"
+ register: results
+ loop:
+ - ["A", "1", "192.168.3.230"]
+ - ["B", "1", "192.168.3.231"]
+ check_mode: true
+- name: Verify no changes were made
+ assert:
+ that: "{{ item['changed'] }}"
+ msg: "Unexpected results!"
+ loop: "{{ lookup('list', results['results']) }}"
+
+- name: Change the initial ib_iser interfaces (changed)
+ na_santricity_ib_iser_interface:
+ <<: *creds
+ controller: "{{ item[0] }}"
+ channel: "{{ item[1] }}"
+ address: "{{ item[2] }}"
+ register: results
+ loop:
+ - ["A", "1", "192.168.3.230"]
+ - ["B", "1", "192.168.3.231"]
+- name: Verify no changes were made
+ assert:
+ that: "{{ item['changed'] }}"
+ msg: "Unexpected results!"
+ loop: "{{ lookup('list', results['results']) }}"
+
+- name: Revert to the initial ib_iser interfaces (changed)
+ na_santricity_ib_iser_interface:
+ <<: *creds
+ controller: "{{ item[0] }}"
+ channel: "{{ item[1] }}"
+ address: "{{ item[2] }}"
+ register: results
+ loop:
+ - ["A", "1", *a1_ip]
+ - ["B", "1", *a2_ip]
+- name: Verify no changes were made
+ assert:
+ that: "{{ item['changed'] }}"
+ msg: "Unexpected results!"
+ loop: "{{ lookup('list', results['results']) }}" \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_iscsi_interface/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_iscsi_interface/tasks/main.yml
new file mode 100644
index 00000000..38b6faba
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_iscsi_interface/tasks/main.yml
@@ -0,0 +1,115 @@
+# Test code for the na_santricity_iscsi_interface module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+- name: Set facts for na_santricity_iscsi_interface module's intergration test.
+ set_fact:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+
+- name: Set controller iSCSI interfaces to DHCP
+ na_santricity_iscsi_interface:
+ <<: *creds
+ controller: "{{ item }}"
+ port: 1
+ config_method: dhcp
+ mtu: 1500
+ loop: ["A", "B"]
+
+- name: Set controller A iSCSI interface to static (change, check_mode)
+ na_santricity_iscsi_interface:
+ <<: *creds
+ controller: A
+ port: 1
+ config_method: static
+ address: 192.168.1.100
+ subnet_mask: 255.255.255.0
+ gateway: 192.168.1.1
+ mtu: 1500
+ check_mode: true
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Set controller A iSCSI interface to static (change)
+ na_santricity_iscsi_interface:
+ <<: *creds
+ controller: A
+ port: 1
+ config_method: static
+ address: 192.168.1.100
+ subnet_mask: 255.255.255.0
+ gateway: 192.168.1.1
+ mtu: 1500
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Set controller A iSCSI interface to static (no change)
+ na_santricity_iscsi_interface:
+ <<: *creds
+ controller: A
+ port: 1
+ config_method: static
+ address: 192.168.1.100
+ subnet_mask: 255.255.255.0
+ gateway: 192.168.1.1
+ mtu: 1500
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Set controller B iSCSI interface to static (change)
+ na_santricity_iscsi_interface:
+ <<: *creds
+ controller: B
+ port: 1
+ config_method: static
+ address: 192.168.1.200
+ subnet_mask: 255.255.255.0
+ gateway: 192.168.1.1
+ mtu: 1500
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Set controller A iSCSI interface MTU to 9000 (change)
+ na_santricity_iscsi_interface:
+ <<: *creds
+ controller: A
+ port: 1
+ config_method: static
+ address: 192.168.1.100
+ subnet_mask: 255.255.255.0
+ gateway: 192.168.1.1
+ mtu: 9000
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Set controller iSCSI interfaces to DHCP
+ na_santricity_iscsi_interface:
+ <<: *creds
+ controller: "{{ item }}"
+ port: 1
+ config_method: dhcp
+ mtu: 1500
+ loop: ["A", "B"]
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_iscsi_target/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_iscsi_target/tasks/main.yml
new file mode 100644
index 00000000..b259ec87
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_iscsi_target/tasks/main.yml
@@ -0,0 +1,81 @@
+# Test code for the na_santricity_iscsi_target module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+- name: Set facts for na_santricity_iscsi_target module's intergration test.
+ set_fact:
+ vars:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+
+- name: Set initial iSCSI target state
+ na_santricity_iscsi_target:
+ <<: *creds
+ name: eseries_storage_iscsi_target
+ ping: false
+ unnamed_discovery: false
+ chap_secret: "chappySecret"
+
+- name: Clear chap secret
+ na_santricity_iscsi_target:
+ <<: *creds
+ name: eseries_storage_iscsi_target
+ ping: false
+ unnamed_discovery: false
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Make iSCSI target pingable (change, check_mode)
+ na_santricity_iscsi_target:
+ <<: *creds
+ name: eseries_storage_iscsi_target
+ ping: true
+ unnamed_discovery: false
+ check_mode: true
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Make iSCSI target pingable (change)
+ na_santricity_iscsi_target:
+ <<: *creds
+ name: eseries_storage_iscsi_target
+ ping: true
+ unnamed_discovery: false
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Make iSCSI target pingable (no change)
+ na_santricity_iscsi_target:
+ <<: *creds
+ name: eseries_storage_iscsi_target
+ ping: true
+ unnamed_discovery: false
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Make iSCSI target discoverable (change)
+ na_santricity_iscsi_target:
+ <<: *creds
+ name: eseries_storage_iscsi_target
+ ping: true
+ unnamed_discovery: true
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_ldap/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_ldap/tasks/main.yml
new file mode 100644
index 00000000..b7b57df1
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_ldap/tasks/main.yml
@@ -0,0 +1,104 @@
+# Test code for the nac_sancticity_ldap module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+- include_vars: "../../integration_config.yml"
+
+- set_fact:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ ldap_info: &info
+ bind_user: "{{ bind_user }}"
+ bind_password: "{{ bind_password }}"
+ server_url: "{{ server_url }}"
+ search_base: "{{ search_base }}"
+ role_mappings:
+ - ".*":
+ - storage.admin
+ - security.admin
+ - support.admin
+ - storage.monitor
+ - ".*":
+ - storage.monitor
+
+- name: Delete default LDAP domain
+ na_santricity_ldap:
+ <<: *creds
+ state: disabled
+
+- name: Delete default LDAP domain
+ na_santricity_ldap:
+ <<: *creds
+ state: disabled
+ register: results
+- name: Verify LDAP changes
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Define a default LDAP domain, utilizing defaults where possible (changed, check_mode)
+ na_santricity_ldap:
+ <<: *creds
+ <<: *info
+ state: present
+ identifier: test1
+ role_mappings: "{{ role_mappings[0] }}"
+ check_mode: true
+ register: results
+- name: Verify LDAP changes
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Define a default LDAP domain, utilizing defaults where possible (changed)
+ na_santricity_ldap:
+ <<: *creds
+ <<: *info
+ state: present
+ identifier: test1
+ role_mappings: "{{ role_mappings[0] }}"
+ register: results
+- name: Verify LDAP changes
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Define a default LDAP domain, utilizing defaults where possible (no change)
+ na_santricity_ldap:
+ <<: *creds
+ <<: *info
+ state: present
+ identifier: test1
+ role_mappings: "{{ role_mappings[0] }}"
+ register: results
+- name: Verify LDAP changes
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Define a default LDAP domain, utilizing defaults where possible (change)
+ na_santricity_ldap:
+ <<: *creds
+ <<: *info
+ state: present
+ identifier: test1
+ role_mappings: "{{ role_mappings[1] }}"
+ register: results
+- name: Verify LDAP changes
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Delete default LDAP domain
+ na_santricity_ldap:
+ <<: *creds
+ state: absent
+ identifier: test1
+ register: results
+- name: Verify LDAP changes
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!" \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_lun_mapping/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_lun_mapping/tasks/main.yml
new file mode 100644
index 00000000..37955fbd
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_lun_mapping/tasks/main.yml
@@ -0,0 +1,318 @@
+# Test code for the na_santricity_lun_mapping module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+- name: Set facts for na_santricity_host module's intergration test.
+ set_fact:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+
+# ****************************************************
+# *** Setup test hosts, storage pools, and volumes ***
+# ****************************************************
+- name: Create host for host mapping
+ na_santricity_host:
+ <<: *creds
+ state: present
+ name: test_host_mapping_host
+ host_type: 27
+- na_santricity_host:
+ <<: *creds
+ state: present
+ name: test_host1
+ host_type: 27
+- na_santricity_host:
+ <<: *creds
+ state: present
+ name: test_host2
+ host_type: 27
+- name: Create storage pool for host mapping
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: test_host_mapping_storage_pool
+ raid_level: raid0
+ criteria_min_usable_capacity: 1
+- name: Create volume for host mapping
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: test_host_mapping_volume
+ storage_pool_name: test_host_mapping_storage_pool
+ size: 1
+- name: Create volume for host mapping
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: test_host_mapping_volume2
+ storage_pool_name: test_host_mapping_storage_pool
+ size: 1
+
+# **********************************************
+# *** Create new lun between host and volume ***
+# **********************************************
+- name: Create na_santricity_lun_mapping
+ na_santricity_lun_mapping:
+ <<: *creds
+ state: present
+ target: test_host_mapping_host
+ volume: test_host_mapping_volume
+ register: result
+
+- name: Verify lun mapping
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/graph/xpath-filter?query=//volume[name='test_host_mapping_volume']"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+
+- assert:
+ that: "{{ item['mapped'] }}"
+ msg: "Lun failed to be created."
+ loop: "{{ lookup('list', current.json)}}"
+
+# QUICK VERIFICATION OF MISMATCHING TARGET/TARGET_TYPE - GOOD
+#- name: Create na_santricity_lun_mapping
+# na_santricity_lun_mapping:
+# <<: *creds
+# state: present
+# target: test_host_mapping_host
+# volume: test_host_mapping_volume
+# lun: 100
+# target_type: group
+# register: result
+#
+#- pause: seconds=30
+# **************************************************************
+# *** Repeat previous lun creation play and verify unchanged ***
+# **************************************************************
+- name: Repeat lun creation
+ na_santricity_lun_mapping:
+ <<: *creds
+ state: present
+ target: test_host_mapping_host
+ volume: test_host_mapping_volume
+ register: result
+
+- name: Verify lun mapping
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/graph/xpath-filter?query=//volume[name='test_host_mapping_volume']"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+
+- assert:
+ that: "{{ item['mapped'] and result.changed==False }}"
+ msg: "Lun failed to be unchanged."
+ loop: "{{ lookup('list', current.json)}}"
+
+# ****************************************************************
+# *** Move existing lun to default target and verify unchanged ***
+# ****************************************************************
+- name: Move lun to default target
+ na_santricity_lun_mapping:
+ <<: *creds
+ state: present
+ volume: test_host_mapping_volume
+ register: result
+
+- name: Verify lun mapping
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/graph/xpath-filter?query=//volume[name='test_host_mapping_volume']"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+
+- assert:
+ that: "{{ item['mapped'] }}"
+ msg: "Lun failed to be created."
+ loop: "{{ lookup('list', current.json)}}"
+
+# *****************************************************************
+# *** Move existing lun to specific target and verify unchanged ***
+# *****************************************************************
+- name: Move lun to default target
+ na_santricity_lun_mapping:
+ <<: *creds
+ state: present
+ target: test_host_mapping_host
+ volume: test_host_mapping_volume
+ register: result
+
+- name: Verify lun mapping
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/graph/xpath-filter?query=//volume[name='test_host_mapping_volume']"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+
+- assert:
+ that: "{{ item['mapped'] }}"
+ msg: "Lun failed to be created."
+ loop: "{{ lookup('list', current.json)}}"
+
+# *******************************************
+# *** Modify a volume mapping's lun value ***
+# *******************************************
+- name: Change volume mapping's lun value
+ na_santricity_lun_mapping:
+ <<: *creds
+ state: present
+ target: test_host_mapping_host
+ volume: test_host_mapping_volume
+ lun: 100
+ register: result
+
+- pause: seconds=15
+
+- name: Verify lun mapping
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/graph/xpath-filter?query=//volume[name='test_host_mapping_volume']"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+
+- assert:
+ that: "{{ result.changed }}"
+ msg: "Lun failed to be unchanged."
+ loop: "{{ lookup('list', current.json)}}"
+
+- name: Verify mapping fails when lun already in use on existing host object
+ na_santricity_lun_mapping:
+ <<: *creds
+ state: present
+ target: test_host_mapping_host
+ volume: test_host_mapping_volume2
+ lun: 100
+ register: result
+ ignore_errors: True
+
+- pause: seconds=15
+
+- assert:
+ that: "{{ not result.changed }}"
+ msg: "Lun succeeded when it should have failed."
+ loop: "{{ lookup('list', current.json)}}"
+
+- name: Verify mapping succeeds when the same lun is used on multiple host objects.
+ na_santricity_lun_mapping:
+ <<: *creds
+ state: present
+ target: test_host1
+ volume: test_host_mapping_volume2
+ lun: 100
+ register: result
+
+- pause: seconds=15
+
+- assert:
+ that: "{{ result.changed }}"
+ msg: "Lun failed to be unchanged."
+ loop: "{{ lookup('list', current.json)}}"
+
+# *************************************************************************************************
+# *** Verify that exact mapping details but different lun results in an unchanged configuration ***
+# *************************************************************************************************
+- name: Verify that exact mapping details but different lun results in an unchanged configuration
+ na_santricity_lun_mapping:
+ <<: *creds
+ state: absent
+ target: test_host_mapping_host
+ volume: test_host_mapping_volume
+ lun: 99
+ register: result
+
+- name: Verify lun mapping
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/graph/xpath-filter?query=//volume[name='test_host_mapping_volume']"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+
+- assert:
+ that: "{{ item['mapped'] and not result.changed }}"
+ msg: "Lun failed to be unchanged."
+ loop: "{{ lookup('list', current.json)}}"
+
+# ********************************
+# *** Delete newly created lun ***
+# ********************************
+- name: Delete lun creation
+ na_santricity_lun_mapping:
+ <<: *creds
+ state: absent
+ target: test_host_mapping_host
+ volume: test_host_mapping_volume
+ register: result
+
+- name: Verify lun mapping
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/graph/xpath-filter?query=//volume[name='test_host_mapping_volume']"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current
+
+- assert:
+ that: "{{ not item['mapped'] }}"
+ msg: "Lun failed to be created."
+ loop: "{{ lookup('list', current.json)}}"
+
+# ********************************************************
+# *** Tear down test hosts, storage pools, and volumes ***
+# ********************************************************
+- name: Delete volume for host mapping
+ na_santricity_volume:
+ <<: *creds
+ state: absent
+ name: test_host_mapping_volume
+ storage_pool_name: test_host_mapping_storage_pool
+ size: 1
+- name: Delete volume for host mapping
+ na_santricity_volume:
+ <<: *creds
+ state: absent
+ name: test_host_mapping_volume2
+ storage_pool_name: test_host_mapping_storage_pool
+ size: 1
+- name: Delete storage pool for host mapping
+ na_santricity_storagepool:
+ <<: *creds
+ state: absent
+ name: test_host_mapping_storage_pool
+ raid_level: raid0
+ criteria_min_usable_capacity: 1
+- name: Delete host for host mapping
+ na_santricity_host:
+ <<: *creds
+ state: absent
+ name: test_host_mapping_host
+ host_type_index: 27
+- name: Delete host for host mapping
+ na_santricity_host:
+ <<: *creds
+ state: absent
+ name: test_host2
+ host_type_index: 27
+- name: Delete host for host mapping
+ na_santricity_host:
+ <<: *creds
+ state: absent
+ name: test_host1
+ host_type_index: 27 \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_mgmt_interface/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_mgmt_interface/tasks/main.yml
new file mode 100644
index 00000000..15aebf4f
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_mgmt_interface/tasks/main.yml
@@ -0,0 +1,383 @@
+# Test code for the nac_sancticity_mgmt_interface module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+
+# Note: Ensure controller B has IPv6 enabled, otherwise the first task will fail.
+
+- include_vars: "../../integration_config.yml"
+
+- set_fact:
+ controller_a: '070000000000000000000001'
+ controller_b: '070000000000000000000002'
+ original_channel_a1_info: &channel_a1_info
+ state: enabled
+ address: 10.113.1.192
+ subnet_mask: 255.255.255.0
+ gateway: 10.113.1.1
+ config_method: static
+ dns_config_method: static
+ dns_address: 10.193.0.250
+ dns_address_backup: 10.192.0.250
+ ntp_config_method: static
+ ntp_address: 216.239.35.0
+ ntp_address_backup: 216.239.35.4
+ ssh: true
+ original_channel_b1_info: &channel_b1_info
+ state: enabled
+ address: 10.113.1.193
+ subnet_mask: 255.255.255.0
+ gateway: 10.113.1.1
+ config_method: static
+ dns_config_method: static
+ dns_address: 10.193.0.250
+ dns_address_backup: 10.192.0.250
+ ntp_config_method: static
+ ntp_address: 216.239.35.0
+ ntp_address_backup: 216.239.35.4
+ ssh: true
+ address_info_list: &test_info
+ address: 10.113.1.251
+ subnet_mask: 255.255.255.0
+ gateway: 10.113.1.1
+
+- name: Set controller A port 1 to dhcp
+ netapp_eseries.santricity.na_santricity_mgmt_interface:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ controller: A
+ port: "1"
+ config_method: dhcp
+- name: Retrieve the current management interfaces
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url.replace(original_channel_a1_info['address'], original_channel_b1_info['address']) }}storage-systems/{{ ssid }}/configuration/ethernet-interfaces"
+ register: interfaces
+- name: Validate controller A port 1 is set to dhcp
+ assert:
+ that: "{{ (item['controllerRef'] != controller_a or item['channel'] != 1) or item['ipv4AddressConfigMethod'] == 'configDhcp' }}"
+ msg: "Failed to set controller A port 1 to dhcp!"
+ loop: "{{ lookup('list', interfaces['json']) }}"
+
+- name: Restore controller A port 1 to static
+ netapp_eseries.santricity.na_santricity_mgmt_interface:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url.replace(original_channel_a1_info['address'], original_channel_b1_info['address']) }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ controller: A
+ port: "1"
+ <<: *channel_a1_info
+
+- name: Disable controller B port 1
+ netapp_eseries.santricity.na_santricity_mgmt_interface:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ state: "disabled"
+ port: "1"
+ controller: B
+
+- name: Set controller B port 1 to dhcp
+ netapp_eseries.santricity.na_santricity_mgmt_interface:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ state: "enabled"
+ port: "1"
+ controller: B
+ config_method: dhcp
+- name: Retrieve the current management interfaces
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url }}storage-systems/{{ ssid }}/configuration/ethernet-interfaces"
+ register: interfaces
+- name: Validate controller B port 1 is set to dhcp
+ assert:
+ that: "{{ (item['controllerRef'] != controller_b or item['channel'] != 1) or item['ipv4AddressConfigMethod'] == 'configDhcp' }}"
+ msg: "Failed to set controller B port 1 to dhcp!"
+ loop: "{{ lookup('list', interfaces['json']) }}"
+
+- name: Set controller B port 1 to static ip address (changed, check_mode)
+ netapp_eseries.santricity.na_santricity_mgmt_interface:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ state: "enabled"
+ port: "1"
+ controller: B
+ config_method: static
+ <<: *test_info
+ check_mode: true
+ register: result
+- name: Retrieve the current management interfaces
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url }}storage-systems/{{ ssid }}/configuration/ethernet-interfaces"
+ register: interfaces
+- name: Validate controller B port 1 is set to static ip address
+ assert:
+ that: "{{ result['changed'] and
+ ((item['controllerRef'] != controller_b or item['channel'] != 1) or
+ item['ipv4AddressConfigMethod'] == 'configDhcp') }}"
+ msg: "Failed to set controller B port 1 to static ip address!"
+ loop: "{{ lookup('list', interfaces['json']) }}"
+
+- name: Set controller B port 1 to static ip address (changed)
+ netapp_eseries.santricity.na_santricity_mgmt_interface:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ state: "enabled"
+ port: "1"
+ controller: B
+ config_method: static
+ <<: *test_info
+ register: result
+- name: Retrieve the current management interfaces
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url }}storage-systems/{{ ssid }}/configuration/ethernet-interfaces"
+ register: interfaces
+- name: Validate controller B port 1 is set to static ip address
+ assert:
+ that: "{{ result['changed'] and
+ ((item['controllerRef'] != controller_b or item['channel'] != 1) or
+ (item['ipv4AddressConfigMethod'] == 'configStatic' and
+ item['ipv4Address'] == address_info_list['address'] and
+ item['ipv4SubnetMask'] == address_info_list['subnet_mask'] and
+ item['ipv4GatewayAddress'] == address_info_list['gateway'])) }}"
+ msg: "Failed to set controller B port 1 to static ip address!"
+ loop: "{{ lookup('list', interfaces['json']) }}"
+
+- name: set controller B port 1 dns setting to dhcp
+ netapp_eseries.santricity.na_santricity_mgmt_interface:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ state: "enabled"
+ port: "1"
+ controller: B
+ config_method: static
+ <<: *test_info
+ dns_config_method: dhcp
+- name: Retrieve the current management interfaces
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url }}storage-systems/{{ ssid }}/configuration/ethernet-interfaces"
+ register: interfaces
+- name: Validate controller B port 1 is set to dhcp
+ assert:
+ that: "{{ ((item['controllerRef'] != controller_b or item['channel'] != 1) or
+ item['dnsProperties']['acquisitionProperties']['dnsAcquisitionType'] == 'dhcp') }}"
+ msg: "Failed to set controller B port 1 dns setting to dhcp!"
+ loop: "{{ lookup('list', interfaces['json']) }}"
+
+- name: set controller B port 1 dns is set to static (changed)
+ netapp_eseries.santricity.na_santricity_mgmt_interface:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ state: "enabled"
+ port: "1"
+ controller: B
+ config_method: static
+ <<: *test_info
+ dns_config_method: static
+ dns_address: 192.168.1.1
+ dns_address_backup: 192.168.1.2
+ register: result
+- name: Retrieve the current management interfaces
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url }}storage-systems/{{ ssid }}/configuration/ethernet-interfaces"
+ register: interfaces
+- name: Validate controller B port 1 dns is set to static
+ assert:
+ that: "{{ result['changed'] and
+ ((item['controllerRef'] != controller_b or item['channel'] != 1) or
+ (item['dnsProperties']['acquisitionProperties']['dnsAcquisitionType'] == 'stat') and
+ item['dnsProperties']['acquisitionProperties']['dnsServers'][0]['addressType'] == 'ipv4' and
+ item['dnsProperties']['acquisitionProperties']['dnsServers'][0]['ipv4Address'] == '192.168.1.1' and
+ item['dnsProperties']['acquisitionProperties']['dnsServers'][1]['addressType'] == 'ipv4' and
+ item['dnsProperties']['acquisitionProperties']['dnsServers'][1]['ipv4Address'] == '192.168.1.2') }}"
+ msg: "Failed to set controller B port 1 dns setting to static!"
+ loop: "{{ lookup('list', interfaces['json']) }}"
+
+- name: disable controller B port 1 ntp settings (changed)
+ netapp_eseries.santricity.na_santricity_mgmt_interface:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ state: "enabled"
+ port: "1"
+ controller: B
+ config_method: static
+ <<: *test_info
+ ntp_config_method: disabled
+- name: Retrieve the current management interfaces
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url }}storage-systems/{{ ssid }}/configuration/ethernet-interfaces"
+ register: interfaces
+- name: Validate controller B port 1 is set to dhcp
+ assert:
+ that: "{{ (item['controllerRef'] != controller_b or item['channel'] != 1) or
+ item['ntpProperties']['acquisitionProperties']['ntpAcquisitionType'] == 'disabled' }}"
+ msg: "Failed to disable controller B port 1 ntp!"
+ loop: "{{ lookup('list', interfaces['json']) }}"
+
+- name: set controller B port 1 ntp setting to dhcp (changed)
+ netapp_eseries.santricity.na_santricity_mgmt_interface:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ state: "enabled"
+ port: "1"
+ controller: B
+ config_method: static
+ <<: *test_info
+ ntp_config_method: dhcp
+ register: result
+- name: Retrieve the current management interfaces
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url }}storage-systems/{{ ssid }}/configuration/ethernet-interfaces"
+ register: interfaces
+- name: Validate controller B port 1 is set to dhcp
+ assert:
+ that: "{{ result['changed'] and
+ ((item['controllerRef'] != controller_b or item['channel'] != 1) or
+ item['ntpProperties']['acquisitionProperties']['ntpAcquisitionType'] == 'dhcp') }}"
+ msg: "Failed to set controller B port 1 ntp setting to dhcp!"
+ loop: "{{ lookup('list', interfaces['json']) }}"
+
+- name: set controller B port 1 ntp setting to static (changed)
+ netapp_eseries.santricity.na_santricity_mgmt_interface:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ state: "enabled"
+ port: "1"
+ controller: B
+ config_method: static
+ <<: *test_info
+ ntp_config_method: static
+ ntp_address: 192.168.1.1
+ ntp_address_backup: 192.168.1.2
+ register: result
+- name: Retrieve the current management interfaces
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url }}storage-systems/{{ ssid }}/configuration/ethernet-interfaces"
+ register: interfaces
+- name: Validate controller B port 1 is set to static
+ assert:
+ that: "{{ result['changed'] and
+ ((item['controllerRef'] != controller_b or item['channel'] != 1) or
+ (item['ntpProperties']['acquisitionProperties']['ntpAcquisitionType'] == 'stat') and
+ item['ntpProperties']['acquisitionProperties']['ntpServers'][0]['addrType'] == 'ipvx' and
+ item['ntpProperties']['acquisitionProperties']['ntpServers'][0]['ipvxAddress']['addressType'] == 'ipv4' and
+ item['ntpProperties']['acquisitionProperties']['ntpServers'][0]['ipvxAddress']['ipv4Address'] == '192.168.1.1' and
+ item['ntpProperties']['acquisitionProperties']['ntpServers'][1]['addrType'] == 'ipvx' and
+ item['ntpProperties']['acquisitionProperties']['ntpServers'][1]['ipvxAddress']['addressType'] == 'ipv4' and
+ item['ntpProperties']['acquisitionProperties']['ntpServers'][1]['ipvxAddress']['ipv4Address'] == '192.168.1.2') }}"
+ msg: "Failed to set controller B port 1 ntp setting to static!"
+ loop: "{{ lookup('list', interfaces['json']) }}"
+
+- name: disable controller B ssh
+ netapp_eseries.santricity.na_santricity_mgmt_interface:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ port: "1"
+ controller: B
+ ssh: false
+- name: Retrieve the current management interfaces
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url }}storage-systems/{{ ssid }}/controllers"
+ register: controllers
+- name: Validate controller B ssh is enabled
+ assert:
+ that: "{{ item['controllerRef'] != controller_b or not item['networkSettings']['remoteAccessEnabled'] }}"
+ msg: "Failed to disable controller B ssh!"
+ loop: "{{ lookup('list', controllers['json']) }}"
+
+- name: enable controller B ssh (changed)
+ netapp_eseries.santricity.na_santricity_mgmt_interface:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ port: "1"
+ controller: B
+ ssh: true
+ register: result
+- name: Retrieve the current management interfaces
+ uri:
+ user: "{{ username }}"
+ password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ url: "{{ base_url }}storage-systems/{{ ssid }}/controllers"
+ register: controllers
+- name: Validate controller B ssh is enabled
+ assert:
+ that: "{{ result['changed'] and (item['controllerRef'] != controller_b or item['networkSettings']['remoteAccessEnabled']) }}"
+ msg: "Failed to set controller B port 1 ntp setting to static!"
+ loop: "{{ lookup('list', controllers['json']) }}"
+
+- name: Restore controller B port 1 settings
+ netapp_eseries.santricity.na_santricity_mgmt_interface:
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+ port: "1"
+ controller: B
+ <<: *channel_b1_info
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_nvme_interface/tasks/ib.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_nvme_interface/tasks/ib.yml
new file mode 100644
index 00000000..260f3d7f
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_nvme_interface/tasks/ib.yml
@@ -0,0 +1,88 @@
+# Test code for the na_santricity_nvme_interface module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+- name: Set facts for na_santricity_nvme_interface module test
+ set_fact:
+ credentials: &creds
+ ssid: 1
+ api_url: https://192.168.1.100:8443/devmgr/v2/
+ api_username: admin
+ api_password: adminpassword
+ validate_certs: false
+ interface_a1_ip: 192.168.1.1
+ interface_b1_ip: 192.168.2.1
+
+- name: Set the initial nvme interfaces
+ na_santricity_nvme_interface:
+ <<: *creds
+ controller: "{{ item[0] }}"
+ channel: "{{ item[1] }}"
+ address: "{{ item[2] }}"
+ loop:
+ - ["A", "1", "{{ interface_a1_ip }}"]
+ - ["B", "1", "{{ interface_b1_ip }}"]
+
+- name: Repeat the initial nvme interfaces (no change)
+ na_santricity_nvme_interface:
+ <<: *creds
+ controller: "{{ item[0] }}"
+ channel: "{{ item[1] }}"
+ address: "{{ item[2] }}"
+ register: results
+ loop:
+ - ["A", "1", "{{ interface_a1_ip }}"]
+ - ["B", "1", "{{ interface_b1_ip }}"]
+- name: Verify no changes were made
+ assert:
+ that: "{{ not item['changed'] }}"
+ msg: "Unexpected results!"
+ loop: "{{ lookup('list', results['results']) }}"
+
+- name: Change the initial nvme interfaces (changed, check_mode)
+ na_santricity_nvme_interface:
+ <<: *creds
+ controller: "{{ item[0] }}"
+ channel: "{{ item[1] }}"
+ address: "{{ item[2] }}"
+ register: results
+ loop:
+ - ["A", "1", "192.168.3.230"]
+ - ["B", "1", "192.168.3.231"]
+ check_mode: true
+- name: Verify no changes were made
+ assert:
+ that: "{{ item['changed'] }}"
+ msg: "Unexpected results!"
+ loop: "{{ lookup('list', results['results']) }}"
+
+- name: Change the initial nvme interfaces (changed)
+ na_santricity_nvme_interface:
+ <<: *creds
+ controller: "{{ item[0] }}"
+ channel: "{{ item[1] }}"
+ address: "{{ item[2] }}"
+ register: results
+ loop:
+ - ["A", "1", "192.168.3.230"]
+ - ["B", "1", "192.168.3.231"]
+- name: Verify no changes were made
+ assert:
+ that: "{{ item['changed'] }}"
+ msg: "Unexpected results!"
+ loop: "{{ lookup('list', results['results']) }}"
+
+- name: Revert to the initial nvme interfaces (changed)
+ na_santricity_nvme_interface:
+ <<: *creds
+ controller: "{{ item[0] }}"
+ channel: "{{ item[1] }}"
+ address: "{{ item[2] }}"
+ register: results
+ loop:
+ - ["A", "1", "{{ interface_a1_ip }}"]
+ - ["B", "1", "{{ interface_b1_ip }}"]
+- name: Verify no changes were made
+ assert:
+ that: "{{ item['changed'] }}"
+ msg: "Unexpected results!"
+ loop: "{{ lookup('list', results['results']) }}" \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_nvme_interface/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_nvme_interface/tasks/main.yml
new file mode 100644
index 00000000..82f5ba16
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_nvme_interface/tasks/main.yml
@@ -0,0 +1,2 @@
+- include_tasks: ib.yml
+- include_tasks: roce.yml
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_nvme_interface/tasks/roce.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_nvme_interface/tasks/roce.yml
new file mode 100644
index 00000000..70bfe55d
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_nvme_interface/tasks/roce.yml
@@ -0,0 +1,105 @@
+# Test code for the na_santricity_nvme_interface module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+- name: Set facts for na_santricity_nvme_interface module test
+ set_fact:
+ credentials: &creds
+ ssid: 1
+ api_url: https://192.168.1.100:8443/devmgr/v2/
+ api_username: admin
+ api_password: adminpassword
+ validate_certs: false
+ original_interface: &iface
+ address: 192.168.131.101
+ subnet_mask: 255.255.255.0
+ gateway: 0.0.0.0
+
+- name: Ensure NVMeoF interfaces are properly configured.
+ na_santricity_nvme_interface:
+ <<: *creds
+ controller: A
+ channel: 1
+ config_method: dhcp
+ mtu: 9000
+ speed: 25
+
+- name: Ensure NVMeoF interfaces are properly configured (no change).
+ na_santricity_nvme_interface:
+ <<: *creds
+ controller: A
+ channel: 1
+ config_method: dhcp
+ mtu: 9000
+ speed: 25
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Ensure NVMe interfaces are properly configured. (change, check_mode)
+ na_santricity_nvme_interface:
+ <<: *creds
+ controller: A
+ channel: 1
+ config_method: static
+ address: 192.168.130.200
+ subnet_mask: 255.255.254.0
+ gateway: 192.168.130.1
+ mtu: 1500
+ speed: auto
+ check_mode: true
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Ensure NVMe interfaces are properly configured. (change)
+ na_santricity_nvme_interface:
+ <<: *creds
+ controller: A
+ channel: 1
+ config_method: static
+ address: 192.168.130.200
+ subnet_mask: 255.255.254.0
+ gateway: 192.168.130.1
+ mtu: 1500
+ speed: auto
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Ensure NVMe interfaces are properly configured. (no change)
+ na_santricity_nvme_interface:
+ <<: *creds
+ controller: A
+ channel: 1
+ config_method: static
+ address: 192.168.130.200
+ subnet_mask: 255.255.254.0
+ gateway: 192.168.130.1
+ mtu: 1500
+ speed: auto
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Ensure NVMeoF interfaces are properly configured. (change)
+ na_santricity_nvme_interface:
+ <<: *creds
+ <<: *iface
+ controller: A
+ channel: 1
+ config_method: static
+ mtu: 1500
+ speed: auto
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_proxy_drive_firmware_upload/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_proxy_drive_firmware_upload/tasks/main.yml
new file mode 100644
index 00000000..c261abff
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_proxy_drive_firmware_upload/tasks/main.yml
@@ -0,0 +1,65 @@
+# Test code for the na_santricity_proxy_drive_firmware_upload module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+- name: Test na_santricity_proxy_drive_firmware_upload module
+ set_fact:
+ credentials: &creds
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "{{ proxy_password }}"
+ validate_certs: "{{ proxy_validate_cert }}"
+ firmware:
+ - /home/swartzn/Downloads/drive firmware/D_PX04SVQ160_30603182_MS00_5600_001.dlp
+ - /home/swartzn/Downloads/drive firmware/D_PX04SVQ160_30603299_MSB6_224C_705.dlp
+
+- name: Clear any existing proxy drive firmware
+ na_santricity_proxy_drive_firmware_upload:
+ <<: *creds
+
+- name: Clear any existing proxy drive firmware (no changed)
+ na_santricity_proxy_drive_firmware_upload:
+ <<: *creds
+ register: results
+- name: Verify all drive firmware has been removed
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Drive firmware exists!"
+
+- name: Add drive firmware to proxy (changed, check_mode)
+ na_santricity_proxy_drive_firmware_upload:
+ <<: *creds
+ firmware: "{{ firmware }}"
+ register: results
+ check_mode: true
+- name: Verify drive firmware has been added
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Drive firmware exists!"
+
+- name: Add drive firmware to proxy (changed)
+ na_santricity_proxy_drive_firmware_upload:
+ <<: *creds
+ firmware: "{{ firmware }}"
+ register: results
+- name: Verify drive firmware has been added
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Drive firmware exists!"
+
+- name: Remove drive firmware to proxy (changed)
+ na_santricity_proxy_drive_firmware_upload:
+ <<: *creds
+ register: results
+- name: Verify drive firmware has been added
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Drive firmware exists!"
+
+- name: Remove drive firmware to proxy (no changed)
+ na_santricity_proxy_drive_firmware_upload:
+ <<: *creds
+ register: results
+- name: Verify drive firmware has been added
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Drive firmware exists!"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_proxy_firmware_upload/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_proxy_firmware_upload/tasks/main.yml
new file mode 100644
index 00000000..d4b9f02d
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_proxy_firmware_upload/tasks/main.yml
@@ -0,0 +1,65 @@
+# Test code for the na_santricity_proxy_firmware_upload module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+- name: Test na_santricity_proxy_firmware_upload module
+ set_fact:
+ credentials: &creds
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "{{ proxy_password }}"
+ validate_certs: "{{ proxy_validate_cert }}"
+ firmware:
+ - /home/swartzn/Downloads/N5600-840834-D03.dlp
+ - /home/swartzn/Downloads/RC_08405000_m3_e10_840_5600.dlp
+
+- name: Clear any existing proxy drive firmware
+ na_santricity_proxy_firmware_upload:
+ <<: *creds
+
+- name: Clear any existing proxy drive firmware (no changed)
+ na_santricity_proxy_firmware_upload:
+ <<: *creds
+ register: results
+- name: Verify all drive firmware has been removed
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Drive firmware exists!"
+
+- name: Add drive firmware to proxy (changed, check_mode)
+ na_santricity_proxy_firmware_upload:
+ <<: *creds
+ firmware: "{{ firmware }}"
+ register: results
+ check_mode: true
+- name: Verify drive firmware has been added
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Drive firmware exists!"
+
+- name: Add drive firmware to proxy (changed)
+ na_santricity_proxy_firmware_upload:
+ <<: *creds
+ firmware: "{{ firmware }}"
+ register: results
+- name: Verify drive firmware has been added
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Drive firmware exists!"
+
+- name: Remove drive firmware to proxy (changed)
+ na_santricity_proxy_firmware_upload:
+ <<: *creds
+ register: results
+- name: Verify drive firmware has been added
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Drive firmware exists!"
+
+- name: Remove drive firmware to proxy (no changed)
+ na_santricity_proxy_firmware_upload:
+ <<: *creds
+ register: results
+- name: Verify drive firmware has been added
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Drive firmware exists!"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_proxy_systems/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_proxy_systems/tasks/main.yml
new file mode 100644
index 00000000..1475cda9
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_proxy_systems/tasks/main.yml
@@ -0,0 +1,160 @@
+# Test code for the na_santricity_proxy_systems module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+
+# NOTE: Running this test back-to-back can result in a 10 minute lock-out
+
+- name: Test na_santricity_proxy_firmware_upload module
+ set_fact:
+ credentials: &creds
+ api_url: "{{ proxy_base_url }}"
+ api_username: "{{ proxy_username }}"
+ api_password: "{{ proxy_password }}"
+ validate_certs: "{{ proxy_validate_cert }}"
+ subnet: 192.168.1.10/24
+ small_subnet: 192.168.1.10/31 # Be sure to know the systems included in this subnet since they will be discovered and not specified.
+ systems:
+ - ssid: "10"
+ serial: "021633035190"
+ password: "password"
+ - ssid: "20"
+ serial: "711214000794"
+ password: "password"
+
+- name: Ensure no systems have been added.
+ na_santricity_proxy_systems:
+ <<: *creds
+
+- name: Add multiple systems using serial numbers and a common password (change, check_mode)
+ na_santricity_proxy_systems:
+ <<: *creds
+ subnet_mask: "{{ subnet }}"
+ password: "{{ systems[0]['password'] }}"
+ systems: |-
+ {%- set output=[] %}
+ {%- for system in systems %}
+ {%- if output.append({"serial": system["serial"]}) %}{%- endif %}
+ {%- endfor %}
+ {{ output }}
+ check_mode: true
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Add multiple systems using serial numbers and a common password (change)
+ na_santricity_proxy_systems:
+ <<: *creds
+ subnet_mask: "{{ subnet }}"
+ password: "{{ systems[0]['password'] }}"
+ systems: |-
+ {%- set output=[] %}
+ {%- for system in systems %}
+ {%- if output.append({"serial": system["serial"]}) %}{%- endif %}
+ {%- endfor %}
+ {{ output }}
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Add multiple systems using serial numbers and a common password (no change)
+ na_santricity_proxy_systems:
+ <<: *creds
+ subnet_mask: "{{ subnet }}"
+ password: "{{ systems[0]['password'] }}"
+ systems: |-
+ {%- set output=[] %}
+ {%- for system in systems %}
+ {%- if output.append({"serial": system["serial"]}) %}{%- endif %}
+ {%- endfor %}
+ {{ output }}
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Remove all systems. (change)
+ na_santricity_proxy_systems:
+ <<: *creds
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Add multiple systems using serial numbers (change, check_mode)
+ na_santricity_proxy_systems:
+ <<: *creds
+ subnet_mask: "{{ subnet }}"
+ systems: "{{ systems }}"
+ check_mode: true
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Add multiple systems using serial numbers (change)
+ na_santricity_proxy_systems:
+ <<: *creds
+ subnet_mask: "{{ subnet }}"
+ systems: "{{ systems }}"
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Add multiple systems using serial numbers (no change)
+ na_santricity_proxy_systems:
+ <<: *creds
+ subnet_mask: "{{ subnet }}"
+ systems: "{{ systems }}"
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Remove all systems. (change)
+ na_santricity_proxy_systems:
+ <<: *creds
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Add any other available system on the subnet (change)
+ na_santricity_proxy_systems:
+ <<: *creds
+ subnet_mask: "{{ small_subnet }}"
+ add_discovered_systems: true
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Remove all systems. (change, check_mode)
+ na_santricity_proxy_systems:
+ <<: *creds
+ register: results
+ check_mode: true
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Remove all systems. (change)
+ na_santricity_proxy_systems:
+ <<: *creds
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_storagepool/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_storagepool/tasks/main.yml
new file mode 100644
index 00000000..664df595
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_storagepool/tasks/main.yml
@@ -0,0 +1,1038 @@
+# Test code for the na_santricity_storagepool module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+#
+# Raid levels tested: raid0, raid1, raid5, raid6, disk pool
+# Actions covered: create w/capacity, create w/drive count, repeat create (no changes), extend w/capacity,
+# extend w/drive count, delete, migrate raid levels (raid0->raid6, 1->5, 5->1, 6->0),
+# secure pool for raid0, erasing drives on creation, erasing drives on deletion,
+# setting reserve drive count for ddp,
+
+- name: Set facts for na_santricity_host module's intergration test.
+ set_fact:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+
+# Ensure that test starts without storage pools
+- name: Remove simple storage pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: absent
+ erase_secured_drives: yes
+ name: "{{ item }}"
+ loop:
+ - raid0_storage
+ - raid1_storage
+ - raid5_storage
+ - raid6_storage
+ - raidDiskPool_storage
+
+# Raid0
+# Create, rerun, extend, and modify raid level.
+- name: Create simple storage pool using raid0.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid0_storage
+ criteria_min_usable_capacity: 1400
+ raid_level: raid0
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ item.raidLevel == 'raid0' and (item.totalRaidedSpace | int) >= 1503238553600 }}"
+ msg: "raid0 storage pool failed to be created."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raid0_storage`]') }}"
+
+- name: (Repeat) Create simple storage pool using raid0.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid0_storage
+ criteria_min_usable_capacity: 1400
+ criteria_size_unit: gb
+ raid_level: raid0
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ not results.changed and item.raidLevel == 'raid0' and (item.totalRaidedSpace | int) >= 1503238553600 }}"
+ msg: "raid0 storage pool failed not to be modified."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raid0_storage`]') }}"
+
+- name: Extend storage pool to 2400gb minimum usable capacity.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid0_storage
+ criteria_min_usable_capacity: 2400
+ criteria_size_unit: gb
+ raid_level: raid0
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ item.raidLevel == 'raid0' and (item.totalRaidedSpace | int) >= 2576980377600 }}"
+ msg: "raid0 storage pool using raid0 failed to be extended to a minimum of 2400gb."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raid0_storage`]') }}"
+
+- name: Expand simple storage pool using raid0.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid0_storage
+ criteria_drive_count: 6
+ raid_level: raid0
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/drives"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_drives
+- assert:
+ that: "{{ results.raidLevel == 'raid0' and
+ (current_drives.json | json_query(count_query) | length) == 6 }}"
+ msg: "raid0 storage pool failed to be extended to 6 drives."
+ vars:
+ count_query: "[?currentVolumeGroupRef=='{{ results.volumeGroupRef }}'].currentVolumeGroupRef"
+
+- name: Migrate raid0 storage pool to raid6.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid0_storage
+ criteria_drive_count: 6
+ raid_level: raid6
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/drives"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_drives
+- assert:
+ that: "{{ results.raidLevel == 'raid6' and
+ (current_drives.json | json_query(count_query) | length) == 6 }}"
+ msg: "raid0 storage pool failed to migrate to raid6"
+ vars:
+ count_query: "[?currentVolumeGroupRef=='{{ results.volumeGroupRef }}'].currentVolumeGroupRef"
+
+- name: Remove simple storage pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: absent
+ name: "{{ item }}"
+ loop:
+ - raid0_storage
+
+
+# Raid1
+# Create, rerun, extend, and modify raid level.
+- name: Create simple storage pool using raid1.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid1_storage
+ criteria_min_usable_capacity: 1400
+ criteria_size_unit: gb
+ raid_level: raid1
+ register: results
+- pause: seconds=5
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ item.raidLevel == 'raid1' and (item.totalRaidedSpace | int) >= 1503238553600 }}"
+ msg: "raid1 storage pool failed to be created."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raid1_storage`]') }}"
+
+- name: (Repeat) Create simple storage pool using raid1.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid1_storage
+ criteria_min_usable_capacity: 1400
+ criteria_size_unit: gb
+ raid_level: raid1
+ register: results
+- pause: seconds=5
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ not results.changed and item.raidLevel == 'raid1' and (item.totalRaidedSpace | int) >= 1503238553600 }}"
+ msg: "raid1 storage pool failed not to be modified."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raid1_storage`]') }}"
+
+- name: Expand simple storage pool using raid1.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid1_storage
+ criteria_drive_count: 6
+ raid_level: raid1
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/drives"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_drives
+- assert:
+ that: "{{ results.raidLevel == 'raid1' and
+ (current_drives.json | json_query(count_query) | length) == 6 }}"
+ msg: "raid1 storage pool failed to be extended."
+ vars:
+ count_query: "[?currentVolumeGroupRef=='{{ results.volumeGroupRef }}'].currentVolumeGroupRef"
+
+- name: Migrate raid1 storage pool to raid5
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid1_storage
+ criteria_drive_count: 6
+ raid_level: raid5
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/drives"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_drives
+- assert:
+ that: "{{ results.raidLevel == 'raid5' and
+ (current_drives.json | json_query(count_query) | length) == 6 }}"
+ msg: "raid1 storage pool failed to migrate to raid5."
+ vars:
+ count_query: "[?currentVolumeGroupRef=='{{ results.volumeGroupRef }}'].currentVolumeGroupRef"
+
+- name: Remove simple storage pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: absent
+ name: "{{ item }}"
+ loop:
+ - raid1_storage
+
+
+# Raid5
+# Create, rerun, extend, and modify raid level.
+- name: Create simple storage pool using raid5.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid5_storage
+ criteria_drive_count: 6
+ raid_level: raid5
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/drives"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_drives
+- assert:
+ that: "{{ results.raidLevel == 'raid5' and
+ (current_drives.json | json_query(count_query) | length) == 6 }}"
+ msg: "raid5 storage pool failed to be created."
+ vars:
+ count_query: "[?currentVolumeGroupRef=='{{ results.volumeGroupRef }}'].currentVolumeGroupRef"
+
+- name: (Rerun) Create simple storage pool using raid5.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid5_storage
+ criteria_drive_count: 6
+ raid_level: raid5
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/drives"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_drives
+- assert:
+ that: "{{ not results.changed and results.raidLevel == 'raid5' and
+ (current_drives.json | json_query(count_query) | length) == 6 }}"
+ msg: "raid5 storage pool failed not to be modified."
+ vars:
+ count_query: "[?currentVolumeGroupRef=='{{ results.volumeGroupRef }}'].currentVolumeGroupRef"
+
+- name: Expand simple storage pool using raid5.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid5_storage
+ criteria_drive_count: 8
+ raid_level: raid5
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/drives"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_drives
+- assert:
+ that: "{{ results.raidLevel == 'raid5' and
+ (current_drives.json | json_query(count_query) | length) == 8}}"
+ msg: "raid5 storage pool failed to be modified to 8 drives."
+ vars:
+ count_query: "[?currentVolumeGroupRef=='{{ results.volumeGroupRef }}'].currentVolumeGroupRef"
+
+- name: Migrate raid5 storage pool to raid1
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid5_storage
+ criteria_drive_count: 8
+ raid_level: raid1
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/drives"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_drives
+- assert:
+ that: "{{ results.raidLevel == 'raid1' and
+ (current_drives.json | json_query(count_query) | length) == 8}}"
+ msg: "raid5 storage pool failed to migrate to raid1."
+ vars:
+ count_query: "[?currentVolumeGroupRef=='{{ results.volumeGroupRef }}'].currentVolumeGroupRef"
+
+- name: Remove simple storage pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: absent
+ name: "{{ item }}"
+ loop:
+ - raid5_storage
+
+
+# raid6
+# Create, rerun, extend, and modify raid level.
+- name: Create simple storage pool using raid6.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid6_storage
+ criteria_drive_count: 5
+ raid_level: raid6
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/drives"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_drives
+- assert:
+ that: "{{ results.raidLevel == 'raid6' and
+ (current_drives.json | json_query(count_query) | length) == 5}}"
+ msg: "raid6 storage pool failed to be created with 5 drives."
+ vars:
+ count_query: "[?currentVolumeGroupRef=='{{ results.volumeGroupRef }}'].currentVolumeGroupRef"
+
+- name: Extend simple storage pool using raid6.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid6_storage
+ criteria_min_usable_capacity: 3.4
+ criteria_size_unit: tb
+ raid_level: raid6
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ item.raidLevel == 'raid6' and (item.totalRaidedSpace | int) >= 3738339534438 }}"
+ msg: "raid6 storage pool failed to be extended to a minimum of 3.4tb."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raid6_storage`]') }}"
+
+- name: Migrate rai6 storage pool to raid0
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid6_storage
+ criteria_min_usable_capacity: 3.4
+ criteria_size_unit: tb
+ raid_level: raid0
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ item.raidLevel == 'raid0' and (item.totalRaidedSpace | int) >= 3738339534438 }}"
+ msg: "raid6 storage pool failed to migrate to raid0."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raid6_storage`]') }}"
+
+- name: Remove simple storage pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: absent
+ name: "{{ item }}"
+ loop:
+ - raid6_storage
+
+# raidDiskPool
+# Create, rerun, extend, and modify raid level.
+- name: Create simple storage pool using raidDiskPool.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raidDiskPool_storage
+ criteria_min_usable_capacity: 2300
+ criteria_size_unit: gb
+ raid_level: raidDiskPool
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ item.raidLevel == 'raidDiskPool' and (item.totalRaidedSpace | int) >= 2469606195200 }}"
+ msg: "Simple storage pool failed to be created."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raidDiskPool_storage`]') }}"
+
+- name: Rerun simple storage pool creation.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raidDiskPool_storage
+ criteria_min_usable_capacity: 2300
+ criteria_size_unit: gb
+ raid_level: raidDiskPool
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was not modified
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ not results.changed and item.raidLevel == 'raidDiskPool' and (item.totalRaidedSpace | int) >= 2469606195200 }}"
+ msg: "Simple storage pool failed not to be modified."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raidDiskPool_storage`]') }}"
+
+- name: Extend simple storage pool to a minimum usable capacity of 3000gb
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raidDiskPool_storage
+ criteria_min_usable_capacity: 3000
+ criteria_size_unit: gb
+ raid_level: raidDiskPool
+ register: results
+- name: Verify storage pool was extended
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ item.raidLevel == 'raidDiskPool' and (item.totalRaidedSpace | int) >= 3221225472000 }}"
+ msg: "Simple storage pool failed to be extended."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raidDiskPool_storage`]') }}"
+
+- name: Extend simple storage pool.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raidDiskPool_storage
+ criteria_drive_count: 12
+ raid_level: raidDiskPool
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/drives"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_drives
+- assert:
+ that: "{{ results.raidLevel == 'raidDiskPool' and
+ (current_drives.json | json_query(count_query) | length) == 12}}"
+ msg: "raidDiskPool storage pool failed to be extended with 12 drives."
+ vars:
+ count_query: "[?currentVolumeGroupRef=='{{ results.volumeGroupRef }}'].currentVolumeGroupRef"
+
+- name: Remove simple storage pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: absent
+ name: raidDiskPool_storage
+ register: results
+
+
+# raid0 secured
+- name: Create simple storage pool using raid0.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid0_storage
+ criteria_min_usable_capacity: 1400
+ secure_pool: yes
+ erase_secured_drives: yes
+ raid_level: raid0
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ item.raidLevel == 'raid0' and (item.totalRaidedSpace | int) >= 1503238553600 and
+ item.securityType == 'enabled' }}"
+ msg: "raid0 storage pool failed to be created."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raid0_storage`]') }}"
+
+- name: (Repeat) Create simple storage pool using raid0.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid0_storage
+ criteria_min_usable_capacity: 1400
+ criteria_size_unit: gb
+ secure_pool: yes
+ erase_secured_drives: yes
+ raid_level: raid0
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ not results.changed and item.raidLevel == 'raid0' and (item.totalRaidedSpace | int) >= 1503238553600 and
+ item.securityType == 'enabled' }}"
+ msg: "raid0 storage pool failed not to be modified."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raid0_storage`]') }}"
+
+- name: Extend storage pool to 2400gb minimum usable capacity.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid0_storage
+ criteria_min_usable_capacity: 2400
+ criteria_size_unit: gb
+ secure_pool: yes
+ erase_secured_drives: yes
+ raid_level: raid0
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ item.raidLevel == 'raid0' and (item.totalRaidedSpace | int) >= 2576980377600 and
+ item.securityType == 'enabled' }}"
+ msg: "raid0 storage pool using raid0 failed to be extended to a minimum of 2400gb."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raid0_storage`]') }}"
+
+- name: Expand simple storage pool using raid0.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid0_storage
+ criteria_drive_count: 6
+ secure_pool: yes
+ erase_secured_drives: yes
+ raid_level: raid0
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/drives"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_drives
+- assert:
+ that: "{{ results.raidLevel == 'raid0' and results.securityType == 'enabled' and
+ (current_drives.json | json_query(count_query) | length) == 6 }}"
+ msg: "raid0 storage pool failed to be extended to 6 drives."
+ vars:
+ count_query: "[?currentVolumeGroupRef=='{{ results.volumeGroupRef }}'].currentVolumeGroupRef"
+
+- name: Migrate raid0 storage pool to raid6.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raid0_storage
+ criteria_drive_count: 6
+ secure_pool: yes
+ erase_secured_drives: yes
+ raid_level: raid6
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/drives"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_drives
+- assert:
+ that: "{{ results.raidLevel == 'raid6' and results.securityType == 'enabled' and
+ (current_drives.json | json_query(count_query) | length) == 6 }}"
+ msg: "raid0 storage pool failed to migrate to raid6"
+ vars:
+ count_query: "[?currentVolumeGroupRef=='{{ results.volumeGroupRef }}'].currentVolumeGroupRef"
+
+- name: Remove simple storage pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: absent
+ name: "{{ item }}"
+ erase_secured_drives: yes
+ loop:
+ - raid0_storage
+
+
+# raidDiskPool secured
+- name: Create simple storage pool using raidDiskPool.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raidDiskPool_storage
+ criteria_min_usable_capacity: 2300
+ criteria_size_unit: gb
+ secure_pool: yes
+ erase_secured_drives: yes
+ raid_level: raidDiskPool
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ item.raidLevel == 'raidDiskPool' and (item.totalRaidedSpace | int) >= 2469606195200 and
+ item.securityType == 'enabled' }}"
+ msg: "Simple storage pool failed to be created."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raidDiskPool_storage`]') }}"
+
+- name: Rerun simple storage pool creation.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raidDiskPool_storage
+ criteria_min_usable_capacity: 2300
+ criteria_size_unit: gb
+ secure_pool: yes
+ erase_secured_drives: yes
+ raid_level: raidDiskPool
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was not modified
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ not results.changed and item.raidLevel == 'raidDiskPool' and (item.totalRaidedSpace | int) >= 2469606195200 and
+ item.securityType == 'enabled' }}"
+ msg: "Simple storage pool failed not to be modified."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raidDiskPool_storage`]') }}"
+
+- name: Extend simple storage pool to a minimum usable capacity of 3000gb
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raidDiskPool_storage
+ criteria_min_usable_capacity: 3000
+ criteria_size_unit: gb
+ secure_pool: yes
+ erase_secured_drives: yes
+ raid_level: raidDiskPool
+ register: results
+- name: Verify storage pool was extended
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ item.raidLevel == 'raidDiskPool' and (item.totalRaidedSpace | int) >= 3221225472000 and
+ item.securityType == 'enabled' }}"
+ msg: "Simple storage pool failed to be extended."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raidDiskPool_storage`]') }}"
+
+- name: Extend simple storage pool.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raidDiskPool_storage
+ criteria_drive_count: 12
+ secure_pool: yes
+ erase_secured_drives: yes
+ raid_level: raidDiskPool
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/drives"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_drives
+- assert:
+ that: "{{ results.raidLevel == 'raidDiskPool' and results.securityType == 'enabled' and
+ (current_drives.json | json_query(count_query) | length) == 12 }}"
+ msg: "raidDiskPool storage pool failed to be extended with 12 drives."
+ vars:
+ count_query: "[?currentVolumeGroupRef=='{{ results.volumeGroupRef }}'].currentVolumeGroupRef"
+
+- name: Remove simple storage pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: absent
+ name: raidDiskPool_storage
+ register: results
+
+
+# raidDiskPool set reserve drive count
+- name: Create simple storage pool using raidDiskPool.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raidDiskPool_storage
+ criteria_drive_count: 11
+ reserve_drive_count: 1
+ secure_pool: yes
+ erase_secured_drives: yes
+ raid_level: raidDiskPool
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ item.raidLevel == 'raidDiskPool' and
+ item.volumeGroupData.diskPoolData.reconstructionReservedDriveCount == 1 and
+ item.securityType == 'enabled' }}"
+ msg: "Simple storage pool failed to be created."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raidDiskPool_storage`]') }}"
+
+- name: Change disk pool reserve drive count.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raidDiskPool_storage
+ criteria_drive_count: 12
+ reserve_drive_count: 2
+ secure_pool: yes
+ erase_secured_drives: yes
+ raid_level: raidDiskPool
+ register: results
+- pause: seconds=30
+- name: Verify storage pool was not modified
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ item.raidLevel == 'raidDiskPool' and
+ item.volumeGroupData.diskPoolData.reconstructionReservedDriveCount == 2 and
+ item.securityType == 'enabled' }}"
+ msg: "Simple storage pool failed not to be modified."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raidDiskPool_storage`]') }}"
+
+# erase drives on storage pool deletion
+- name: Remove simple storage pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: absent
+ name: raidDiskPool_storage
+ erase_secured_drives: yes
+ register: results
+
+- name: Create simple storage pool using raidDiskPool with capacity and reserve count specified.
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: raidDiskPool_storage
+ criteria_min_usable_capacity: 8000
+ criteria_size_unit: gb
+ reserve_drive_count: 2
+ secure_pool: yes
+ erase_secured_drives: yes
+ raid_level: raidDiskPool
+ register: results
+- pause: seconds=5
+- name: Verify storage pool was created
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/storage-pools"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ body_format: json
+ validate_certs: no
+ register: current_storage_pools
+- assert:
+ that: "{{ item.raidLevel == 'raidDiskPool' and
+ (item.totalRaidedSpace | int) >= 3221225472000 and
+ item.volumeGroupData.diskPoolData.reconstructionReservedDriveCount == 2 and
+ item.securityType == 'enabled' }}"
+ msg: "Simple storage pool failed to be created."
+ loop: "{{ lookup('list', storage_pools, wantList=True) }}"
+ vars:
+ storage_pools: "{{ current_storage_pools | json_query('json[?name==`raidDiskPool_storage`]') }}"
+
+- name: Integration cleanup
+ na_santricity_storagepool:
+ <<: *creds
+ state: absent
+ name: raidDiskPool_storage
+ erase_secured_drives: yes
+ register: results
+- na_santricity_storagepool:
+ <<: *creds
+ state: absent
+ name: raidDiskPool_storage
+ register: results
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_syslog/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_syslog/tasks/main.yml
new file mode 100644
index 00000000..79830c3d
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_syslog/tasks/main.yml
@@ -0,0 +1,127 @@
+# Test code for the na_santricity_syslog module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+- name: Set facts for na_santricity_iscsi_target module's intergration test.
+ set_fact:
+ vars:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+
+- name: Add initial syslog server settings (changed)
+ na_santricity_syslog:
+ <<: *creds
+ address: 192.168.1.100
+ port: 514
+ protocol: udp
+ components: ["auditLog"]
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Configure initial syslog server settings (no change)
+ na_santricity_syslog:
+ <<: *creds
+ address: 192.168.1.100
+ port: 514
+ protocol: udp
+ components: ["auditLog"]
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ not results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Add another syslog server settings with different protocol (changed)
+ na_santricity_syslog:
+ <<: *creds
+ address: 192.168.1.100
+ port: 514
+ protocol: tcp
+ components: ["auditLog"]
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Add another syslog server settings with different port (changed)
+ na_santricity_syslog:
+ <<: *creds
+ address: 192.168.1.100
+ port: 123
+ protocol: tcp
+ components: ["auditLog"]
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Add another syslog server address (change, check_mode)
+ na_santricity_syslog:
+ <<: *creds
+ address: 192.168.1.200
+ port: 514
+ protocol: tcp
+ components: ["auditLog"]
+ check_mode: true
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: (Repeat) Add another syslog server address (change)
+ na_santricity_syslog:
+ <<: *creds
+ address: 192.168.1.200
+ port: 514
+ protocol: tcp
+ components: ["auditLog"]
+ register: results
+- name: Verify results
+ assert:
+ that: "{{ results['changed'] }}"
+ msg: "Unexpected results!"
+
+- name: Diasable syslog server (change)
+ na_santricity_syslog:
+ <<: *creds
+ state: absent
+ address: 192.168.1.100
+ port: 514
+ protocol: udp
+ components: ["auditLog"]
+
+- name: Diasable syslog server (change)
+ na_santricity_syslog:
+ <<: *creds
+ state: absent
+ address: 192.168.1.100
+ port: 514
+ protocol: tcp
+ components: ["auditLog"]
+
+- name: Diasable syslog server (change)
+ na_santricity_syslog:
+ <<: *creds
+ state: absent
+ address: 192.168.1.100
+ port: 123
+ protocol: tcp
+ components: ["auditLog"]
+
+- name: Diasable syslog server (change)
+ na_santricity_syslog:
+ <<: *creds
+ state: absent
+ address: 192.168.1.200
+ port: 1514
+ protocol: tcp
+ components: ["auditLog"]
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_volume/tasks/main.yml b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_volume/tasks/main.yml
new file mode 100644
index 00000000..fe6d91d3
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/integration/targets/na_santricity_volume/tasks/main.yml
@@ -0,0 +1,768 @@
+# Test code for the na_santricity_volume module
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+- name: Set facts for na_santricity_host module's intergration test.
+ set_fact:
+ credentials: &creds
+ ssid: "{{ ssid }}"
+ api_url: "{{ base_url }}"
+ api_username: "{{ username }}"
+ api_password: "{{ password }}"
+ validate_certs: "{{ validate_cert }}"
+
+# test setup
+- name: Delete raid 0 storage pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: absent
+ name: "{{ item }}"
+ loop:
+ - storage_pool
+ - storage_pool2
+ - storage_pool3
+
+# Thick volume testing: create, delete, expand, change properties (read/write cache), expand and change properties,
+- name: Create raid 0 storage pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: storage_pool
+ criteria_min_usable_capacity: 5
+ criteria_size_unit: tb
+ erase_secured_drives: yes
+ raid_level: raid0
+
+- name: Delete volume in raid 0 storage pool
+ na_santricity_volume:
+ <<: *creds
+ state: absent
+ name: volume
+
+- name: Create volume in raid 0 storage pool
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: volume
+ storage_pool_name: storage_pool
+ size: 100
+ size_unit: gb
+ register: results
+- pause: seconds=15
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ results.changed and item.name == 'volume' and not item.thinProvisioned and
+ item.capacity == '107374182400' and item.segmentSize == 131072}}"
+ msg: "Failed to create volume"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`volume`]') }}"
+
+- name: Re-execute volume creation in raid 0 storage pool
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: volume
+ storage_pool_name: storage_pool
+ size: 100
+ size_unit: gb
+ register: results
+- pause: seconds=15
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ not results.changed and item.name == 'volume' and not item.thinProvisioned and
+ item.capacity == '107374182400' and item.segmentSize == 131072}}"
+ msg: "Failed to create volume"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`volume`]') }}"
+
+- name: Update volume size
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: volume
+ storage_pool_name: storage_pool
+ size: 200
+ size_unit: gb
+ register: results
+- pause: seconds=15
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ results.changed and item.name == 'volume' and not item.thinProvisioned and
+ item.capacity == '214748364800' and item.segmentSize == 131072}}"
+ msg: "Failed to create volume"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`volume`]') }}"
+
+- pause: seconds=15
+
+- name: Update volume properties
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: volume
+ storage_pool_name: storage_pool
+ size: 200
+ size_unit: gb
+ write_cache_enable: true
+ read_cache_enable: false
+ register: results
+- pause: seconds=15
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ results.changed and item.name == 'volume' and not item.thinProvisioned and
+ item.capacity == '214748364800' and item.segmentSize == 131072 and
+ not item.cacheSettings.readCacheEnable and item.cacheSettings.writeCacheEnable}}"
+ msg: "Failed to create volume"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`volume`]') }}"
+
+- name: Update volume properties and expand storage capabilities
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: volume
+ storage_pool_name: storage_pool
+ size: 300
+ size_unit: gb
+ write_cache_enable: false
+ read_cache_enable: true
+ register: results
+- pause: seconds=15
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ results.changed and item.name == 'volume' and not item.thinProvisioned and
+ item.capacity == '322122547200' and item.segmentSize == 131072 and
+ item.cacheSettings.readCacheEnable and not item.cacheSettings.writeCacheEnable}}"
+ msg: "Failed to create volume"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`volume`]') }}"
+
+# Workload tagging testing: create, utilize existing (name only, name with same attributes), modify attributes
+- name: Add workload tag (change, new workload tag)
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: volume
+ storage_pool_name: storage_pool
+ size: 300
+ size_unit: gb
+ write_cache_enable: false
+ read_cache_enable: true
+ workload_name: volume_tag
+ metadata:
+ volume_tag_key: volume_tag_value
+ register: results
+- pause: seconds=15
+- name: Validate volume workload changes
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ results.changed and item.name == 'volume' and not item.thinProvisioned and
+ item.capacity == '322122547200' and item.segmentSize == 131072 and
+ item.cacheSettings.readCacheEnable and not item.cacheSettings.writeCacheEnable and
+ {'key': 'volumeTypeId', 'value': 'volume'} in item.metadata }}"
+ msg: "Failed to modify volume metadata!"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`volume`]') }}"
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/workloads"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: workload_tags
+- assert:
+ that: "{{ item.name == 'volume_tag' and
+ {'key': 'volume_tag_key', 'value': 'volume_tag_value'} in item.workloadAttributes }}"
+ msg: "Workload tag failed to be created!"
+ loop: "{{ lookup('list', volume_tag_id, wantList=True) }}"
+ vars:
+ volume_tag_id: "{{ workload_tags | json_query('json[?name==`volume_tag`]') }}"
+
+- name: Repeat add workload tag (no change)
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: volume
+ storage_pool_name: storage_pool
+ size: 300
+ size_unit: gb
+ write_cache_enable: false
+ read_cache_enable: true
+ workload_name: volume_tag
+ metadata:
+ volume_tag_key: volume_tag_value
+ register: results
+- pause: seconds=15
+- name: Validate volume workload changes
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ not results.changed and item.name == 'volume' and not item.thinProvisioned and
+ item.capacity == '322122547200' and item.segmentSize == 131072 and
+ item.cacheSettings.readCacheEnable and not item.cacheSettings.writeCacheEnable and
+ {'key': 'volumeTypeId', 'value': 'volume'} in item.metadata }}"
+ msg: "Failed to not modify volume metadata!"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`volume`]') }}"
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/workloads"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: workload_tags
+- assert:
+ that: "{{ item.name == 'volume_tag' and
+ {'key': 'volume_tag_key', 'value': 'volume_tag_value'} in item.workloadAttributes }}"
+ msg: "Workload tag failed not to be changed"
+ loop: "{{ lookup('list', volume_tag_id, wantList=True) }}"
+ vars:
+ volume_tag_id: "{{ workload_tags | json_query('json[?name==`volume_tag`]') }}"
+
+- name: Workload tag (no change, just using workload_name)
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: volume
+ storage_pool_name: storage_pool
+ size: 300
+ size_unit: gb
+ write_cache_enable: false
+ read_cache_enable: true
+ workload_name: volume_tag
+ register: results
+- pause: seconds=15
+- name: Validate volume workload changes
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ not results.changed and item.name == 'volume' and not item.thinProvisioned and
+ item.capacity == '322122547200' and item.segmentSize == 131072 and
+ item.cacheSettings.readCacheEnable and not item.cacheSettings.writeCacheEnable and
+ {'key': 'volumeTypeId', 'value': 'volume'} in item.metadata }}"
+ msg: "Failed to not modify volume metadata!"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`volume`]') }}"
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/workloads"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: workload_tags
+- assert:
+ that: "{{ item.name == 'volume_tag' and
+ {'key': 'volume_tag_key', 'value': 'volume_tag_value'} in item.workloadAttributes }}"
+ msg: "Workload tag failed to not be modified!"
+ loop: "{{ lookup('list', volume_tag_id, wantList=True) }}"
+ vars:
+ volume_tag_id: "{{ workload_tags | json_query('json[?name==`volume_tag`]') }}"
+
+- name: Add workload tag (change, new attributes)
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: volume
+ storage_pool_name: storage_pool
+ size: 300
+ size_unit: gb
+ write_cache_enable: false
+ read_cache_enable: true
+ workload_name: volume_tag
+ metadata:
+ volume_tag_key2: volume_tag_value2
+ register: results
+- pause: seconds=15
+- name: Validate volume workload changes
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ results.changed and item.name == 'volume' and not item.thinProvisioned and
+ item.capacity == '322122547200' and item.segmentSize == 131072 and
+ item.cacheSettings.readCacheEnable and not item.cacheSettings.writeCacheEnable and
+ {'key': 'volumeTypeId', 'value': 'volume'} in item.metadata }}"
+ msg: "Failed to not modify volume metadata!"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`volume`]') }}"
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/workloads"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: workload_tags
+- assert:
+ that: "{{ item.name == 'volume_tag' and
+ {'key': 'volume_tag_key2', 'value': 'volume_tag_value2'} in item.workloadAttributes }}"
+ msg: "Workload tag failed to be updated!"
+ loop: "{{ lookup('list', volume_tag_id, wantList=True) }}"
+ vars:
+ volume_tag_id: "{{ workload_tags | json_query('json[?name==`volume_tag`]') }}"
+
+- name: Remove workload tag from volume (change)
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: volume
+ storage_pool_name: storage_pool
+ size: 300
+ size_unit: gb
+ write_cache_enable: false
+ read_cache_enable: true
+ register: results
+- pause: seconds=15
+- name: Validate volume workload changes
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ results.changed and item.name == 'volume' and not item.thinProvisioned and
+ item.capacity == '322122547200' and item.segmentSize == 131072 and
+ item.cacheSettings.readCacheEnable and not item.cacheSettings.writeCacheEnable and
+ item.metadata == []}}"
+ msg: "Failed to not modify volume metadata!"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`volume`]') }}"
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/workloads"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: workload_tags
+- assert:
+ that: "{{ item.name == 'volume_tag' and
+ {'key': 'volume_tag_key2', 'value': 'volume_tag_value2'} in item.workloadAttributes }}"
+ msg: "Workload tag failed to be updated!"
+ loop: "{{ lookup('list', volume_tag_id, wantList=True) }}"
+ vars:
+ volume_tag_id: "{{ workload_tags | json_query('json[?name==`volume_tag`]') }}"
+
+- name: Delete workload tag
+ uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/workloads"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: workload_tags
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/workloads/{{ item }}"
+ method: DELETE
+ status_code: 204
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ loop: "{{ lookup('list', volume_tag_id, wantList=True) }}"
+ vars:
+ volume_tag_id: "{{ workload_tags | json_query('json[?name==`volume_tag`].id') }}"
+
+- name: Delete raid 0 storage pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: absent
+ name: storage_pool
+
+
+# *** Thin volume testing (May not work with simulator) ***
+- name: Create dynamic disk pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: storage_pool
+ criteria_min_usable_capacity: 2
+ criteria_size_unit: tb
+
+- name: Create thin volume
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: thin_volume
+ storage_pool_name: storage_pool
+ size: 131072
+ size_unit: gb
+ thin_provision: true
+ thin_volume_repo_size: 32
+ thin_volume_max_repo_size: 1024
+ register: results
+- pause: seconds=15
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/thin-volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ results.changed and item.name == 'thin_volume' and item.thinProvisioned and
+ item.capacity == '140737488355328' and item.initialProvisionedCapacity == '34359738368' and
+ item.provisionedCapacityQuota == '1099511627776' and item.expansionPolicy == 'automatic' }}"
+ msg: "Failed to create volume"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`thin_volume`]') }}"
+
+- name: (Rerun) Create thin volume
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: thin_volume
+ storage_pool_name: storage_pool
+ size: 131072
+ size_unit: gb
+ thin_provision: true
+ thin_volume_repo_size: 32
+ thin_volume_max_repo_size: 1024
+ register: results
+- pause: seconds=15
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/thin-volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ not results.changed and item.name == 'thin_volume' and item.thinProvisioned and
+ item.capacity == '140737488355328' and item.initialProvisionedCapacity == '34359738368' and
+ item.provisionedCapacityQuota == '1099511627776' and item.expansionPolicy == 'automatic' }}"
+ msg: "Failed to create volume"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`thin_volume`]') }}"
+
+
+- name: Expand thin volume's virtual size
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: thin_volume
+ storage_pool_name: storage_pool
+ size: 262144
+ size_unit: gb
+ thin_provision: true
+ thin_volume_repo_size: 32
+ thin_volume_max_repo_size: 1024
+ register: results
+- pause: seconds=15
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/thin-volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ results.changed and item.name == 'thin_volume' and item.thinProvisioned and
+ item.capacity == '281474976710656' and item.initialProvisionedCapacity == '34359738368' and
+ item.provisionedCapacityQuota == '1099511627776' and item.expansionPolicy == 'automatic' }}"
+ msg: "Failed to create volume"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`thin_volume`]') }}"
+
+
+- name: Expand thin volume's maximum repository size
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: thin_volume
+ storage_pool_name: storage_pool
+ size: 262144
+ size_unit: gb
+ thin_provision: true
+ thin_volume_repo_size: 32
+ thin_volume_max_repo_size: 2048
+ register: results
+- pause: seconds=15
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/thin-volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ results.changed and item.name == 'thin_volume' and item.thinProvisioned and
+ item.capacity == '281474976710656' and item.initialProvisionedCapacity == '34359738368' and
+ item.provisionedCapacityQuota == '2199023255552' and item.expansionPolicy == 'automatic' }}"
+ msg: "Failed to create volume"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`thin_volume`]') }}"
+
+- name: Create dynamic disk pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: storage_pool2
+ criteria_min_usable_capacity: 2
+ criteria_size_unit: tb
+- pause: seconds=15
+
+- name: Create second thin volume with manual expansion policy
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: thin_volume2
+ storage_pool_name: storage_pool2
+ size_unit: gb
+ size: 131072
+ thin_provision: true
+ thin_volume_repo_size: 32
+ thin_volume_max_repo_size: 32
+ thin_volume_expansion_policy: manual
+ register: results
+- pause: seconds=15
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/thin-volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ results.changed and item.name == 'thin_volume2' and item.thinProvisioned and
+ item.capacity == '140737488355328' and item.initialProvisionedCapacity == '34359738368' and
+ item.currentProvisionedCapacity == '34359738368' and item.expansionPolicy == 'manual' }}"
+ msg: "Failed to create volume"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`thin_volume2`]') }}"
+
+
+- name: Create second thin volume with manual expansion policy
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: thin_volume2
+ storage_pool_name: storage_pool2
+ size_unit: gb
+ size: 131072
+ thin_provision: true
+ thin_volume_repo_size: 288
+ thin_volume_max_repo_size: 288
+ thin_volume_expansion_policy: manual
+ register: results
+- pause: seconds=15
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/thin-volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ results.changed and item.name == 'thin_volume2' and item.thinProvisioned and
+ item.capacity == '140737488355328' and item.initialProvisionedCapacity == '34359738368' and
+ item.currentProvisionedCapacity == '309237645312' and item.expansionPolicy == 'manual' }}"
+ msg: "Failed to create volume"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`thin_volume2`]') }}"
+
+- name: Modify second thin volume to use automatic expansion policy
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: thin_volume2
+ storage_pool_name: storage_pool2
+ size_unit: gb
+ size: 131072
+ thin_provision: true
+ thin_volume_repo_size: 288
+ thin_volume_max_repo_size: 288
+ thin_volume_expansion_policy: automatic
+ register: results
+- pause: seconds=15
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/thin-volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ results.changed and item.name == 'thin_volume2' and item.thinProvisioned and
+ item.capacity == '140737488355328' and item.initialProvisionedCapacity == '34359738368' and
+ item.currentProvisionedCapacity == '309237645312' and item.expansionPolicy == 'automatic' }}"
+ msg: "Failed to create volume"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`thin_volume2`]') }}"
+
+- name: Delete raid 0 storage pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: absent
+ name: "{{ item }}"
+ loop:
+ - storage_pool
+ - storage_pool2
+
+- name: Create raid 0 storage pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: storage_pool
+ criteria_min_usable_capacity: 5
+ criteria_size_unit: tb
+ erase_secured_drives: yes
+ raid_level: raid0
+
+# Thick volume expansion testing: wait and don't wait for operation to complete
+- name: Create raid 6 storage pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: present
+ name: storage_pool3
+ criteria_min_usable_capacity: 5
+ criteria_size_unit: tb
+ erase_secured_drives: yes
+ raid_level: raid6
+
+- name: Delete volume in raid 6 storage pool
+ na_santricity_volume:
+ <<: *creds
+ state: absent
+ name: volume
+
+- name: Create volume in raid 0 storage pool for expansion testing
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: volume
+ storage_pool_name: storage_pool3
+ size: 1
+ size_unit: gb
+ register: results
+- pause: seconds=10
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- assert:
+ that: "{{ results.changed and item.name == 'volume' and not item.thinProvisioned and
+ item.capacity == '1073741824' and item.segmentSize == 131072}}"
+ msg: "Failed to create volume"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`volume`]') }}"
+
+- name: Modify volume in raid 0 storage pool and wait for expansion testing
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: volume
+ storage_pool_name: storage_pool3
+ size: 10
+ size_unit: gb
+ wait_for_initialization: True
+ register: results
+- pause: seconds=10
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/volumes/{{ volume[0]['id'] }}/expand"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: expansion_state
+ vars:
+ volume: "{{ current | json_query('json[?name==`volume`]') }}"
+- assert:
+ that: "{{ results.changed and item.name == 'volume' and not item.thinProvisioned and
+ item.capacity == '10737418240' and item.segmentSize == 131072 and
+ expansion_state['json']['action'] == 'none'}}"
+ msg: "Volume expansion test failed."
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`volume`]') }}"
+
+- name: Modify volume in raid 0 storage pool and don't wait for expansion testing
+ na_santricity_volume:
+ <<: *creds
+ state: present
+ name: volume
+ storage_pool_name: storage_pool3
+ size: 100
+ size_unit: gb
+ wait_for_initialization: False
+ register: results
+- pause: seconds=10
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/volumes"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: current
+- uri:
+ url: "{{ credentials.api_url }}storage-systems/{{ credentials.ssid }}/volumes/{{ volume[0]['id'] }}/expand"
+ user: "{{ credentials.api_username }}"
+ password: "{{ credentials.api_password }}"
+ validate_certs: no
+ register: expansion_state
+ vars:
+ volume: "{{ current | json_query('json[?name==`volume`]') }}"
+- assert:
+ that: "{{ results.changed and item.name == 'volume' and not item.thinProvisioned and
+ item.capacity == '107374182400' and item.segmentSize == 131072 and expansion_state['json']['action'] != 'none'}}"
+ msg: "Failed to create volume"
+ loop: "{{ lookup('list', volume, wantList=True) }}"
+ vars:
+ volume: "{{ current | json_query('json[?name==`volume`]') }}"
+
+- name: Delete raid 0 storage pool
+ na_santricity_storagepool:
+ <<: *creds
+ state: absent
+ name: "{{ item }}"
+ loop:
+ - storage_pool3 \ No newline at end of file
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_alerts.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_alerts.py
new file mode 100644
index 00000000..3510e510
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_alerts.py
@@ -0,0 +1,194 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_alerts import NetAppESeriesAlerts
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class AlertsTest(ModuleTestCase):
+ REQUIRED_PARAMS = {
+ 'api_username': 'rw',
+ 'api_password': 'password',
+ 'api_url': 'http://localhost',
+ 'ssid': '1',
+ 'state': 'disabled'
+ }
+ REQ_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_alerts.NetAppESeriesAlerts.request'
+
+ def _set_args(self, **kwargs):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if kwargs is not None:
+ module_args.update(kwargs)
+ set_module_args(module_args)
+
+ def _validate_args(self, **kwargs):
+ self._set_args(**kwargs)
+ NetAppESeriesAlerts()
+
+ def test_validation_disable(self):
+ """Ensure a default configuration succeeds"""
+ self._validate_args()
+
+ def test_validation_enable(self):
+ """Ensure a typical, default configuration succeeds"""
+ self._validate_args(state='enabled', server='localhost', sender='x@y.z', recipients=['a@b.c'])
+
+ def test_validation_fail_required(self):
+ """Ensure we fail on missing configuration"""
+
+ # Missing recipients
+ with self.assertRaises(AnsibleFailJson):
+ self._validate_args(state='enabled', server='localhost', sender='x@y.z')
+ NetAppESeriesAlerts()
+
+ # Missing sender
+ with self.assertRaises(AnsibleFailJson):
+ self._validate_args(state='enabled', server='localhost', recipients=['a@b.c'])
+ NetAppESeriesAlerts()
+
+ # Missing server
+ with self.assertRaises(AnsibleFailJson):
+ self._validate_args(state='enabled', sender='x@y.z', recipients=['a@b.c'])
+
+ def test_validation_fail(self):
+ # Empty recipients
+ with self.assertRaises(AnsibleFailJson):
+ self._validate_args(state='enabled', server='localhost', sender='x@y.z', recipients=[])
+
+ # Bad sender
+ with self.assertRaises(AnsibleFailJson):
+ self._validate_args(state='enabled', server='localhost', sender='y.z', recipients=['a@b.c'])
+
+ def test_get_configuration(self):
+ """Validate retrieving the current configuration"""
+ self._set_args(state='enabled', server='localhost', sender='x@y.z', recipients=['a@b.c'])
+
+ expected = 'result'
+ alerts = NetAppESeriesAlerts()
+ alerts.is_proxy = lambda: False
+ alerts.is_embedded_available = lambda: False
+
+ # Expecting an update
+ with mock.patch(self.REQ_FUNC, return_value=(200, expected)) as req:
+ actual = alerts.get_configuration()
+ self.assertEquals(expected, actual)
+ self.assertEquals(req.call_count, 1)
+
+ def test_update_configuration(self):
+ """Validate updating the configuration"""
+ initial = dict(alertingEnabled=True,
+ emailServerAddress='localhost',
+ sendAdditionalContactInformation=True,
+ additionalContactInformation='None',
+ emailSenderAddress='x@y.z',
+ recipientEmailAddresses=['x@y.z']
+ )
+
+ args = dict(state='enabled', server=initial['emailServerAddress'], sender=initial['emailSenderAddress'],
+ contact=initial['additionalContactInformation'], recipients=initial['recipientEmailAddresses'])
+
+ self._set_args(**args)
+
+ alerts = NetAppESeriesAlerts()
+ alerts.is_proxy = lambda: False
+ alerts.is_embedded_available = lambda: False
+
+ # Ensure when trigger updates when each relevant field is changed
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)) as req:
+ with mock.patch.object(alerts, 'get_configuration', return_value=initial):
+ update = alerts.update_configuration()
+ self.assertFalse(update)
+
+ alerts.sender = 'a@b.c'
+ update = alerts.update_configuration()
+ self.assertTrue(update)
+ self._set_args(**args)
+
+ alerts.recipients = ['a@b.c']
+ update = alerts.update_configuration()
+ self.assertTrue(update)
+ self._set_args(**args)
+
+ alerts.contact = 'abc'
+ update = alerts.update_configuration()
+ self.assertTrue(update)
+ self._set_args(**args)
+
+ alerts.server = 'abc'
+ update = alerts.update_configuration()
+ self.assertTrue(update)
+
+ def test_send_test_email_check(self):
+ """Ensure we handle check_mode correctly"""
+ self._set_args(test=True)
+ alerts = NetAppESeriesAlerts()
+ alerts.check_mode = True
+ with mock.patch(self.REQ_FUNC) as req:
+ with mock.patch.object(alerts, 'update_configuration', return_value=True):
+ alerts.send_test_email()
+ self.assertFalse(req.called)
+
+ def test_send_test_email(self):
+ """Ensure we send a test email if test=True"""
+ self._set_args(test=True)
+ alerts = NetAppESeriesAlerts()
+ alerts.is_proxy = lambda: False
+ alerts.is_embedded_available = lambda: False
+
+ with mock.patch(self.REQ_FUNC, return_value=(200, dict(response='emailSentOK'))) as req:
+ alerts.send_test_email()
+ self.assertTrue(req.called)
+
+ def test_send_test_email_fail(self):
+ """Ensure we fail if the test returned a failure status"""
+ self._set_args(test=True)
+ alerts = NetAppESeriesAlerts()
+ alerts.is_proxy = lambda: False
+ alerts.is_embedded_available = lambda: False
+
+ ret_msg = 'fail'
+ with self.assertRaisesRegexp(AnsibleFailJson, ret_msg):
+ with mock.patch(self.REQ_FUNC, return_value=(200, dict(response=ret_msg))) as req:
+ alerts.send_test_email()
+ self.assertTrue(req.called)
+
+ def test_send_test_email_fail_connection(self):
+ """Ensure we fail cleanly if we hit a connection failure"""
+ self._set_args(test=True)
+ alerts = NetAppESeriesAlerts()
+ alerts.is_proxy = lambda: False
+ alerts.is_embedded_available = lambda: False
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"failed to send"):
+ with mock.patch(self.REQ_FUNC, side_effect=Exception) as req:
+ alerts.send_test_email()
+ self.assertTrue(req.called)
+
+ def test_update(self):
+ # Ensure that when test is enabled and alerting is enabled, we run the test
+ self._set_args(state='enabled', server='localhost', sender='x@y.z', recipients=['a@b.c'], test=True)
+ alerts = NetAppESeriesAlerts()
+ with self.assertRaisesRegexp(AnsibleExitJson, r"enabled"):
+ with mock.patch.object(alerts, 'update_configuration', return_value=True):
+ with mock.patch.object(alerts, 'send_test_email') as test:
+ alerts.update()
+ self.assertTrue(test.called)
+
+ # Ensure we don't run a test when changed=False
+ with self.assertRaisesRegexp(AnsibleExitJson, r"enabled"):
+ with mock.patch.object(alerts, 'update_configuration', return_value=False):
+ with mock.patch.object(alerts, 'send_test_email') as test:
+ alerts.update()
+ self.assertFalse(test.called)
+
+ # Ensure that test is not called when we have alerting disabled
+ self._set_args(state='disabled')
+ alerts = NetAppESeriesAlerts()
+ with self.assertRaisesRegexp(AnsibleExitJson, r"disabled"):
+ with mock.patch.object(alerts, 'update_configuration', return_value=True):
+ with mock.patch.object(alerts, 'send_test_email') as test:
+ alerts.update()
+ self.assertFalse(test.called)
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_alerts_syslog.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_alerts_syslog.py
new file mode 100644
index 00000000..758c7c21
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_alerts_syslog.py
@@ -0,0 +1,151 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_alerts_syslog import NetAppESeriesAlertsSyslog
+from units.modules.utils import AnsibleFailJson, AnsibleExitJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class NetAppESeriesAlertSyslogTest(ModuleTestCase):
+ REQUIRED_PARAMS = {
+ "api_username": "rw",
+ "api_password": "password",
+ "api_url": "http://localhost",
+ }
+ REQ_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_alerts_syslog.NetAppESeriesAlertsSyslog.request'
+ BASE_REQ_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity.request'
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_valid_options_pass(self):
+ """Validate valid options."""
+ options_list = [{"servers": []},
+ {"servers": [{"address": "192.168.1.100"}]},
+ {"servers": [{"address": "192.168.1.100", "port": 1000}]},
+ {"servers": [{"address": "192.168.1.100"}, {"address": "192.168.1.200", "port": 1000}, {"address": "192.168.1.300", "port": 2000}]},
+ {"servers": [{"address": "192.168.1.101"}, {"address": "192.168.1.102"}, {"address": "192.168.1.103"},
+ {"address": "192.168.1.104"}, {"address": "192.168.1.105"}]}]
+
+ for options in options_list:
+ self._set_args(options)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ syslog = NetAppESeriesAlertsSyslog()
+ for options in options_list:
+ self._set_args(options)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": True})]):
+ syslog = NetAppESeriesAlertsSyslog()
+
+ def test_invalid_options_fail(self):
+ """Validate exceptions are thrown when invalid options are provided."""
+ options_list = [{"servers": [{"address": "192.168.1.100"}, {"address": "192.168.1.200"}, {"address": "192.168.1.300"},
+ {"address": "192.168.1.101"}, {"address": "192.168.1.102"}, {"address": "192.168.1.103"}]}]
+
+ for options in options_list:
+ self._set_args(options)
+ with self.assertRaisesRegexp(AnsibleFailJson, "Maximum number of syslog servers is 5!"):
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ syslog = NetAppESeriesAlertsSyslog()
+
+ def test_change_required_pass(self):
+ """Validate is_change_required properly reports true."""
+ options_list = [{"servers": []},
+ {"servers": [{"address": "192.168.1.100"}]},
+ {"servers": [{"address": "192.168.1.100", "port": 1000}]},
+ {"servers": [{"address": "192.168.1.100"}, {"address": "192.168.1.200", "port": 1000}, {"address": "192.168.1.300", "port": 2000}]},
+ {"servers": [{"address": "192.168.1.101"}, {"address": "192.168.1.102"}, {"address": "192.168.1.103"},
+ {"address": "192.168.1.104"}, {"address": "192.168.1.105"}]}]
+ current_config_list = [{"syslogReceivers": [{"serverName": "192.168.1.100", "portNumber": 514}]},
+ {"syslogReceivers": [{"serverName": "192.168.1.100", "portNumber": 1000}]},
+ {"syslogReceivers": [{"serverName": "192.168.1.101", "portNumber": 1000}]},
+ {"syslogReceivers": [{"serverName": "192.168.1.100", "portNumber": 514}]},
+ {"syslogReceivers": [{"serverName": "192.168.1.100", "portNumber": 514}]}]
+
+ for index in range(5):
+ self._set_args(options_list[index])
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ syslog = NetAppESeriesAlertsSyslog()
+ syslog.get_current_configuration = lambda: current_config_list[index]
+ self.assertTrue(syslog.is_change_required())
+
+ def test_get_current_configuration_fail(self):
+ """Verify get_current_configuration throws expected exception."""
+ self._set_args({"servers": []})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ syslog = NetAppESeriesAlertsSyslog()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve syslog configuration!"):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ syslog.get_current_configuration()
+
+ def test_no_change_required_pass(self):
+ """Validate is_change_required properly reports false."""
+ options_list = [{"servers": []},
+ {"servers": [{"address": "192.168.1.100"}]},
+ {"servers": [{"address": "192.168.1.101", "port": 1000}, {"address": "192.168.1.100", "port": 514}]}]
+ current_config_list = [{"syslogReceivers": []},
+ {"syslogReceivers": [{"serverName": "192.168.1.100", "portNumber": 514}]},
+ {"syslogReceivers": [{"serverName": "192.168.1.100", "portNumber": 514}, {"serverName": "192.168.1.101", "portNumber": 1000}]}]
+
+ for index in range(3):
+ self._set_args(options_list[index])
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ syslog = NetAppESeriesAlertsSyslog()
+ syslog.get_current_configuration = lambda: current_config_list[index]
+ self.assertFalse(syslog.is_change_required())
+
+ def test_request_body_pass(self):
+ """Verify request body is properly formatted."""
+ options_list = [{"servers": []},
+ {"servers": [{"address": "192.168.1.100"}]},
+ {"servers": [{"address": "192.168.1.101", "port": 1000}, {"address": "192.168.1.100", "port": 514}]}]
+ expected_config_list = [{"syslogReceivers": [], "defaultFacility": 3, "defaultTag": "StorageArray"},
+ {"syslogReceivers": [{"serverName": "192.168.1.100", "portNumber": 514}], "defaultFacility": 3, "defaultTag": "StorageArray"},
+ {"syslogReceivers": [{"serverName": "192.168.1.101", "portNumber": 1000}, {"serverName": "192.168.1.100", "portNumber": 514}],
+ "defaultFacility": 3, "defaultTag": "StorageArray"}]
+
+ for index in range(3):
+ self._set_args(options_list[index])
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ syslog = NetAppESeriesAlertsSyslog()
+ self.assertEqual(syslog.make_request_body(), expected_config_list[index])
+
+ def test_test_configuration_fail(self):
+ """Verify get_current_configuration throws expected exception."""
+ self._set_args({"servers": []})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ syslog = NetAppESeriesAlertsSyslog()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to send test message!"):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ syslog.test_configuration()
+
+ def test_update_pass(self):
+ """Verify update method successfully completes."""
+ self._set_args({"test": True, "servers": [{"address": "192.168.1.100"}]})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ syslog = NetAppESeriesAlertsSyslog()
+ syslog.is_change_required = lambda: True
+ syslog.make_request_body = lambda: {}
+ self.test_configuration = lambda: None
+
+ with self.assertRaises(AnsibleExitJson):
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ syslog.update()
+
+ def tests_update_fail(self):
+ """Verify update method throws expected exceptions."""
+ self._set_args({"servers": []})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ syslog = NetAppESeriesAlertsSyslog()
+ syslog.is_change_required = lambda: True
+ syslog.make_request_body = lambda: {}
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to add syslog server!"):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ syslog.update()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_asup.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_asup.py
new file mode 100644
index 00000000..84c05d59
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_asup.py
@@ -0,0 +1,318 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import time
+from units.compat import mock
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_asup import NetAppESeriesAsup
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+
+
+class AsupTest(ModuleTestCase):
+ REQUIRED_PARAMS = {
+ "api_username": "rw",
+ "api_password": "password",
+ "api_url": "http://localhost",
+ "ssid": "1",
+ }
+
+ REQ_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_asup.NetAppESeriesAsup.request"
+ BASE_REQ_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity.request'
+ TIME_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_asup.time.time"
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_valid_options_pass(self):
+ """Validate valid options."""
+ options_list = [
+ {"state": "disabled", "active": False},
+ {"state": "enabled", "active": False, "start": 20, "end": 24, "days": ["saturday", "sunday"],
+ "method": "email", "email": {"server": "192.168.1.100", "sender": "noreply@netapp.com"}},
+ {"state": "enabled", "active": False, "start": 20, "end": 24, "days": ["saturday", "sunday"],
+ "method": "https", "routing_type": "direct"},
+ {"state": "enabled", "active": False, "start": 20, "end": 24, "days": ["saturday", "sunday"],
+ "method": "https", "routing_type": "proxy", "proxy": {"host": "192.168.1.100", "port": 1234}},
+ {"state": "enabled", "active": False, "start": 20, "end": 24, "days": ["saturday", "sunday"],
+ "method": "https", "routing_type": "script", "proxy": {"script": "/path/to/proxy/script.sh"}},
+ {"state": "maintenance_enabled", "maintenance_duration": 24, "maintenance_emails": ["janey@netapp.com", "joe@netapp.com"]},
+ {"state": "maintenance_disabled"}
+ ]
+
+ for options in options_list:
+ self._set_args(options)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ for options in options_list:
+ self._set_args(options)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": True})]):
+ asup = NetAppESeriesAsup()
+
+ def test_invalid_options_fail(self):
+ """Verify invalid options throw expected exceptions."""
+ options_list = [
+ {"state": "enabled", "active": False, "start": 24, "end": 23, "days": ["saturday", "sunday"],
+ "method": "email", "email": {"server": "192.168.1.100", "sender": "noreply@netapp.com"}},
+ {"state": "enabled", "active": False, "start": -1, "end": 23, "days": ["saturday", "sunday"],
+ "method": "email", "email": {"server": "192.168.1.100", "sender": "noreply@netapp.com"}},
+ {"state": "enabled", "active": False, "start": 20, "end": 25, "days": ["saturday", "sunday"],
+ "method": "email", "email": {"server": "192.168.1.100", "sender": "noreply@netapp.com"}},
+ {"state": "enabled", "active": False, "start": 20, "end": 24, "days": ["not_a_day", "sunday"],
+ "method": "https", "routing_type": "direct"},
+ {"state": "maintenance_enabled", "maintenance_duration": 0, "maintenance_emails": ["janey@netapp.com", "joe@netapp.com"]},
+ {"state": "maintenance_enabled", "maintenance_duration": 73, "maintenance_emails": ["janey@netapp.com", "joe@netapp.com"]},
+ ]
+
+ for options in options_list:
+ self._set_args(options)
+ with self.assertRaises(AnsibleFailJson):
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+
+ def test_get_configuration_fail(self):
+ """Verify get_configuration method throws expected exceptions."""
+ self._set_args({"state": "disabled", "active": False})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve ASUP configuration!"):
+ asup.get_configuration()
+ self._set_args({"state": "disabled", "active": False})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ with mock.patch(self.REQ_FUNC, return_value=(200, {"asupCapable": False, "onDemandCapable": True})):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve ASUP configuration!"):
+ asup.get_configuration()
+ self._set_args({"state": "disabled", "active": False})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ with mock.patch(self.REQ_FUNC, return_value=(200, {"asupCapable": True, "onDemandCapable": False})):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve ASUP configuration!"):
+ asup.get_configuration()
+ self._set_args({"state": "disabled", "active": False})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ with mock.patch(self.REQ_FUNC, return_value=(200, {"asupCapable": False, "onDemandCapable": False})):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve ASUP configuration!"):
+ asup.get_configuration()
+
+ def test_in_maintenance_mode_pass(self):
+ """Verify whether asup is in maintenance mode successful."""
+ self._set_args({"state": "disabled", "active": False})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ with mock.patch(self.REQ_FUNC, return_value=(200, [{"key": "ansible_asup_maintenance_stop_time", "value": str(time.time() + 10000)}])):
+ self.assertTrue(asup.in_maintenance_mode())
+
+ self._set_args({"state": "disabled", "active": False})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ with mock.patch(self.REQ_FUNC, return_value=(200, [{"key": "ansible_asup_maintenance_email_list", "value": "janey@netapp.com,joe@netapp.com"},
+ {"key": "ansible_asup_maintenance_stop_time", "value": str(time.time() - 1)}])):
+ self.assertFalse(asup.in_maintenance_mode())
+
+ def test_in_maintenance_mode_fail(self):
+ """Verify that in_maintenance mode throws expected exceptions."""
+ self._set_args({"state": "disabled", "active": False})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve maintenance windows information!"):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ asup.in_maintenance_mode()
+
+ def test_update_configuration_pass(self):
+ """Verify that update_configuration completes successfully."""
+ asup_config = [{"asupCapable": True,
+ "onDemandCapable": True,
+ "asupEnabled": True,
+ "onDemandEnabled": True,
+ "remoteDiagsEnabled": True,
+ "delivery": {"method": "smtp",
+ "routingType": "none",
+ "proxyHost": None,
+ "proxyPort": 0,
+ "proxyUserName": None,
+ "proxyPassword": None,
+ "proxyScript": None,
+ "mailRelayServer": "server@example.com",
+ "mailSenderAddress": "noreply@example.com"},
+ "destinationAddress": "autosupport@netapp.com",
+ "schedule": {"dailyMinTime": 0,
+ "dailyMaxTime": 1439,
+ "weeklyMinTime": 0,
+ "weeklyMaxTime": 1439,
+ "daysOfWeek": ["sunday", "monday", "tuesday"]}},
+ {"asupCapable": True,
+ "onDemandCapable": True,
+ "asupEnabled": True,
+ "onDemandEnabled": False,
+ "remoteDiagsEnabled": False,
+ "delivery": {
+ "method": "https",
+ "routingType": "proxyServer",
+ "proxyHost": "192.168.1.100",
+ "proxyPort": 1234,
+ "proxyUserName": None,
+ "proxyPassword": None,
+ "proxyScript": None,
+ "mailRelayServer": None,
+ "mailSenderAddress": None
+ },
+ "destinationAddress": "https://support.netapp.com/put/AsupPut/",
+ "schedule": {
+ "dailyMinTime": 1200,
+ "dailyMaxTime": 1439,
+ "weeklyMinTime": 0,
+ "weeklyMaxTime": 1439,
+ "daysOfWeek": ["sunday", "saturday"]}},
+ {"asupCapable": True,
+ "onDemandCapable": True,
+ "asupEnabled": True,
+ "onDemandEnabled": False,
+ "remoteDiagsEnabled": False,
+ "delivery": {
+ "method": "https",
+ "routingType": "proxyScript",
+ "proxyHost": None,
+ "proxyPort": 0,
+ "proxyUserName": None,
+ "proxyPassword": None,
+ "proxyScript": "/home/user/path/to/script.sh",
+ "mailRelayServer": None,
+ "mailSenderAddress": None
+ },
+ "destinationAddress": "https://support.netapp.com/put/AsupPut/",
+ "schedule": {
+ "dailyMinTime": 0,
+ "dailyMaxTime": 420,
+ "weeklyMinTime": 0,
+ "weeklyMaxTime": 1439,
+ "daysOfWeek": ["monday", "tuesday", "wednesday", "thursday", "friday"]}}]
+ options_list = [{"state": "disabled", "active": False},
+ {"state": "enabled", "active": False, "start": 20, "end": 24, "days": ["saturday"],
+ "method": "email", "email": {"server": "192.168.1.100", "sender": "noreply@netapp.com"}},
+ {"state": "enabled", "active": False, "start": 20, "end": 24, "days": ["sunday"],
+ "method": "https", "routing_type": "direct"},
+ {"state": "enabled", "active": False, "start": 20, "end": 24, "days": ["saturday", "sunday"],
+ "method": "https", "routing_type": "proxy", "proxy": {"host": "192.168.1.100", "port": 1234}},
+ {"state": "enabled", "active": False, "start": 20, "end": 24, "days": ["saturday", "sunday"],
+ "method": "https", "routing_type": "script", "proxy": {"script": "/path/to/proxy/script.sh"}},
+ {"state": "maintenance_enabled", "maintenance_duration": 24, "maintenance_emails": ["janey@netapp.com", "joe@netapp.com"]},
+ {"state": "maintenance_disabled"}]
+
+ for index, options in enumerate(options_list):
+ self._set_args(options)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ asup.get_configuration = lambda: asup_config[index % 3]
+ asup.in_maintenance_mode = lambda: False
+
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ asup.update_configuration()
+
+ def test_update_configuration_fail(self):
+ """Verify that update_configuration throws expected exceptions."""
+ asup_config = {"asupCapable": True,
+ "onDemandCapable": True,
+ "asupEnabled": True,
+ "onDemandEnabled": True,
+ "remoteDiagsEnabled": True,
+ "delivery": {"method": "smtp",
+ "routingType": "none",
+ "proxyHost": None,
+ "proxyPort": 0,
+ "proxyUserName": None,
+ "proxyPassword": None,
+ "proxyScript": None,
+ "mailRelayServer": "server@example.com",
+ "mailSenderAddress": "noreply@example.com"},
+ "destinationAddress": "autosupport@netapp.com",
+ "schedule": {"dailyMinTime": 0,
+ "dailyMaxTime": 1439,
+ "weeklyMinTime": 0,
+ "weeklyMaxTime": 1439,
+ "daysOfWeek": ["sunday", "monday", "tuesday"]}}
+
+ # Exceptions for state=="enabled" or state=="disabled"
+ self._set_args({"state": "enabled", "active": False, "start": 20, "end": 24, "days": ["saturday"],
+ "method": "email", "email": {"server": "192.168.1.100", "sender": "noreply@netapp.com"}})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ asup.get_configuration = lambda: asup_config
+ asup.in_maintenance_mode = lambda: False
+ asup.validate = lambda: True
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to validate ASUP configuration!"):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ asup.update_configuration()
+ self._set_args({"state": "disabled", "active": False})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ asup.get_configuration = lambda: asup_config
+ asup.in_maintenance_mode = lambda: False
+ asup.validate = lambda: False
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to change ASUP configuration!"):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ asup.update_configuration()
+
+ # Exceptions for state=="maintenance enabled"
+ self._set_args({"state": "maintenance_enabled", "maintenance_duration": 24, "maintenance_emails": ["janey@netapp.com", "joe@netapp.com"]})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ asup.get_configuration = lambda: {"asupEnabled": False}
+ asup.in_maintenance_mode = lambda: False
+ with self.assertRaisesRegexp(AnsibleFailJson, "AutoSupport must be enabled before enabling or disabling maintenance mode."):
+ asup.update_configuration()
+ self._set_args({"state": "maintenance_enabled", "maintenance_duration": 24, "maintenance_emails": ["janey@netapp.com", "joe@netapp.com"]})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ asup.get_configuration = lambda: {"asupEnabled": True}
+ asup.in_maintenance_mode = lambda: False
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to enabled ASUP maintenance window."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ asup.update_configuration()
+ self._set_args({"state": "maintenance_enabled", "maintenance_duration": 24, "maintenance_emails": ["janey@netapp.com", "joe@netapp.com"]})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ asup.get_configuration = lambda: {"asupEnabled": True}
+ asup.in_maintenance_mode = lambda: False
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to store maintenance information."):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, None), Exception()]):
+ asup.update_configuration()
+ self._set_args({"state": "maintenance_enabled", "maintenance_duration": 24, "maintenance_emails": ["janey@netapp.com", "joe@netapp.com"]})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ asup.get_configuration = lambda: {"asupEnabled": True}
+ asup.in_maintenance_mode = lambda: False
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to store maintenance information."):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, None), (200, None), Exception()]):
+ asup.update_configuration()
+
+ # Exceptions for state=="maintenance disabled"
+ self._set_args({"state": "maintenance_disabled"})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ asup.get_configuration = lambda: {"asupEnabled": True}
+ asup.in_maintenance_mode = lambda: True
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to disable ASUP maintenance window."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ asup.update_configuration()
+ self._set_args({"state": "maintenance_disabled"})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ asup.get_configuration = lambda: {"asupEnabled": True}
+ asup.in_maintenance_mode = lambda: True
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to store maintenance information."):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, None), Exception()]):
+ asup.update_configuration()
+ self._set_args({"state": "maintenance_disabled"})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ asup = NetAppESeriesAsup()
+ asup.get_configuration = lambda: {"asupEnabled": True}
+ asup.in_maintenance_mode = lambda: True
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to store maintenance information."):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, None), (200, None), Exception()]):
+ asup.update_configuration()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_auditlog.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_auditlog.py
new file mode 100644
index 00000000..1cb57068
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_auditlog.py
@@ -0,0 +1,205 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_auditlog import NetAppESeriesAuditLog
+from units.modules.utils import AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class NetAppESeriesAuditLogTests(ModuleTestCase):
+ REQUIRED_PARAMS = {'api_username': 'rw',
+ 'api_password': 'password',
+ 'api_url': 'http://localhost',
+ 'ssid': '1'}
+ REQ_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_auditlog.NetAppESeriesAuditLog.request'
+ BASE_REQ_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity.request'
+ MAX_RECORDS_MAXIMUM = 50000
+ MAX_RECORDS_MINIMUM = 100
+
+ def _set_args(self, **kwargs):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if kwargs is not None:
+ module_args.update(kwargs)
+ set_module_args(module_args)
+
+ def test_max_records_argument_pass(self):
+ """Verify NetAppESeriesAuditLog argument's max_records and threshold upper and lower boundaries."""
+ initial = {"max_records": 1000,
+ "log_level": "writeOnly",
+ "full_policy": "overWrite",
+ "threshold": 90}
+ max_records_set = (self.MAX_RECORDS_MINIMUM, 25000, self.MAX_RECORDS_MAXIMUM)
+
+ for max_records in max_records_set:
+ initial["max_records"] = max_records
+ self._set_args(**initial)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ audit_log = NetAppESeriesAuditLog()
+ self.assertTrue(audit_log.max_records == max_records)
+
+ def test_max_records_argument_fail(self):
+ """Verify NetAppESeriesAuditLog arument's max_records and threshold upper and lower boundaries."""
+ initial = {"max_records": 1000,
+ "log_level": "writeOnly",
+ "full_policy": "overWrite",
+ "threshold": 90}
+ max_records_set = (self.MAX_RECORDS_MINIMUM - 1, self.MAX_RECORDS_MAXIMUM + 1)
+
+ for max_records in max_records_set:
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Audit-log max_records count must be between 100 and 50000"):
+ initial["max_records"] = max_records
+ self._set_args(**initial)
+ NetAppESeriesAuditLog()
+
+ def test_threshold_argument_pass(self):
+ """Verify NetAppESeriesAuditLog argument's max_records and threshold upper and lower boundaries."""
+ initial = {"max_records": 1000,
+ "log_level": "writeOnly",
+ "full_policy": "overWrite",
+ "threshold": 90}
+ threshold_set = (60, 75, 90)
+
+ for threshold in threshold_set:
+ initial["threshold"] = threshold
+ self._set_args(**initial)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ audit_log = NetAppESeriesAuditLog()
+ self.assertTrue(audit_log.threshold == threshold)
+
+ def test_threshold_argument_fail(self):
+ """Verify NetAppESeriesAuditLog arument's max_records and threshold upper and lower boundaries."""
+ initial = {"max_records": 1000,
+ "log_level": "writeOnly",
+ "full_policy": "overWrite",
+ "threshold": 90}
+ threshold_set = (59, 91)
+
+ for threshold in threshold_set:
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Audit-log percent threshold must be between 60 and 90"):
+ initial["threshold"] = threshold
+ self._set_args(**initial)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ NetAppESeriesAuditLog()
+
+ def test_get_configuration_pass(self):
+ """Validate get configuration does not throw exception when normal request is returned."""
+ initial = {"max_records": 1000,
+ "log_level": "writeOnly",
+ "full_policy": "overWrite",
+ "threshold": 90}
+ expected = {"auditLogMaxRecords": 1000,
+ "auditLogLevel": "writeOnly",
+ "auditLogFullPolicy": "overWrite",
+ "auditLogWarningThresholdPct": 90}
+
+ self._set_args(**initial)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ audit_log = NetAppESeriesAuditLog()
+
+ with mock.patch(self.REQ_FUNC, return_value=(200, expected)):
+ body = audit_log.get_configuration()
+ self.assertTrue(body == expected)
+
+ def test_get_configuration_fail(self):
+ """Verify AnsibleJsonFail exception is thrown."""
+ initial = {"max_records": 1000,
+ "log_level": "writeOnly",
+ "full_policy": "overWrite",
+ "threshold": 90}
+
+ self._set_args(**initial)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ audit_log = NetAppESeriesAuditLog()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to retrieve the audit-log configuration!"):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ audit_log.get_configuration()
+
+ def test_build_configuration_pass(self):
+ """Validate configuration changes will force an update."""
+ response = {"auditLogMaxRecords": 1000,
+ "auditLogLevel": "writeOnly",
+ "auditLogFullPolicy": "overWrite",
+ "auditLogWarningThresholdPct": 90}
+ initial = {"max_records": 1000,
+ "log_level": "writeOnly",
+ "full_policy": "overWrite",
+ "threshold": 90}
+ changes = [{"max_records": 50000},
+ {"log_level": "all"},
+ {"full_policy": "preventSystemAccess"},
+ {"threshold": 75}]
+
+ for change in changes:
+ initial_with_changes = initial.copy()
+ initial_with_changes.update(change)
+ self._set_args(**initial_with_changes)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ audit_log = NetAppESeriesAuditLog()
+
+ with mock.patch(self.REQ_FUNC, return_value=(200, response)):
+ update = audit_log.build_configuration()
+ self.assertTrue(update)
+
+ def test_delete_log_messages_fail(self):
+ """Verify AnsibleJsonFail exception is thrown."""
+ initial = {"max_records": 1000,
+ "log_level": "writeOnly",
+ "full_policy": "overWrite",
+ "threshold": 90}
+
+ self._set_args(**initial)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ audit_log = NetAppESeriesAuditLog()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to delete audit-log messages!"):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ audit_log.delete_log_messages()
+
+ def test_update_configuration_delete_pass(self):
+ """Verify 422 and force successfully returns True."""
+ body = {"auditLogMaxRecords": 1000,
+ "auditLogLevel": "writeOnly",
+ "auditLogFullPolicy": "overWrite",
+ "auditLogWarningThresholdPct": 90}
+ initial = {"max_records": 2000,
+ "log_level": "writeOnly",
+ "full_policy": "overWrite",
+ "threshold": 90,
+ "force": True}
+
+ self._set_args(**initial)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ audit_log = NetAppESeriesAuditLog()
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, body),
+ (422, {u"invalidFieldsIfKnown": None,
+ u"errorMessage": u"Configuration change...",
+ u"localizedMessage": u"Configuration change...",
+ u"retcode": u"auditLogImmediateFullCondition",
+ u"codeType": u"devicemgrerror"}),
+ (200, None),
+ (200, None)]):
+ self.assertTrue(audit_log.update_configuration())
+
+ def test_update_configuration_delete_skip_fail(self):
+ """Verify 422 and no force results in AnsibleJsonFail exception."""
+ body = {"auditLogMaxRecords": 1000,
+ "auditLogLevel": "writeOnly",
+ "auditLogFullPolicy": "overWrite",
+ "auditLogWarningThresholdPct": 90}
+ initial = {"max_records": 2000,
+ "log_level": "writeOnly",
+ "full_policy": "overWrite",
+ "threshold": 90,
+ "force": False}
+
+ self._set_args(**initial)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ audit_log = NetAppESeriesAuditLog()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to update audit-log configuration!"):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, body), Exception(422, {"errorMessage": "error"}),
+ (200, None), (200, None)]):
+ audit_log.update_configuration()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_auth.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_auth.py
new file mode 100644
index 00000000..305d6028
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_auth.py
@@ -0,0 +1,488 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_auth import NetAppESeriesAuth
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class AuthTest(ModuleTestCase):
+ REQUIRED_PARAMS = {"api_username": "admin", "api_password": "password", "api_url": "http://localhost", "ssid": "1"}
+ REQ_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_auth.NetAppESeriesAuth.request"
+ SLEEP_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_auth.sleep"
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_minimum_password_length_change_required_pass(self):
+ """Verify minimum_password_length_change_required returns expected values."""
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ with mock.patch(self.REQ_FUNC, return_value=(200, {"adminPasswordSet": False, "minimumPasswordLength": 8})):
+ self.assertFalse(auth.minimum_password_length_change_required())
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass", "minimum_password_length": 7})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ with mock.patch(self.REQ_FUNC, return_value=(200, {"adminPasswordSet": False, "minimumPasswordLength": 8})):
+ self.assertTrue(auth.minimum_password_length_change_required())
+
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ with mock.patch(self.REQ_FUNC, return_value=(200, {"adminPasswordSet": False, "minimumPasswordLength": 8})):
+ self.assertFalse(auth.minimum_password_length_change_required())
+
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: True
+ with mock.patch(self.REQ_FUNC, return_value=(200, {"adminPasswordSet": False, "minimumPasswordLength": 8})):
+ self.assertFalse(auth.minimum_password_length_change_required())
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass", "minimum_password_length": 7})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: True
+ with mock.patch(self.REQ_FUNC, return_value=(200, {"adminPasswordSet": False, "minimumPasswordLength": 8})):
+ self.assertTrue(auth.minimum_password_length_change_required())
+
+ self._set_args({"ssid": "1", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: False
+ auth.is_embedded_available = lambda: True
+ with mock.patch(self.REQ_FUNC, return_value=(200, {"adminPasswordSet": False, "minimumPasswordLength": 8})):
+ self.assertFalse(auth.minimum_password_length_change_required())
+ self._set_args({"ssid": "1", "user": "admin", "password": "adminpass", "minimum_password_length": 7})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: False
+ auth.is_embedded_available = lambda: True
+ with mock.patch(self.REQ_FUNC, return_value=(200, {"adminPasswordSet": False, "minimumPasswordLength": 8})):
+ self.assertTrue(auth.minimum_password_length_change_required())
+
+ def test_minimum_password_length_change_required_fail(self):
+ """Verify minimum_password_length_change_required throws expected exceptions."""
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass", "minimum_password_length": 10})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: False
+ with self.assertRaisesRegexp(AnsibleFailJson, "Password does not meet the length requirement"):
+ with mock.patch(self.REQ_FUNC, return_value=(200, {"adminPasswordSet": False, "minimumPasswordLength": 8})):
+ auth.minimum_password_length_change_required()
+
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: False
+ with self.assertRaisesRegexp(AnsibleFailJson, "Password does not meet the length requirement"):
+ with mock.patch(self.REQ_FUNC, return_value=(200, {"adminPasswordSet": True, "minimumPasswordLength": 10})):
+ auth.minimum_password_length_change_required()
+
+ def test_update_minimum_password_length_pass(self):
+ """Verify update_minimum_password_length returns expected values."""
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ auth.is_admin_password_set = True
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ auth.update_minimum_password_length()
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ auth.is_admin_password_set = False
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ auth.update_minimum_password_length()
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ auth.is_admin_password_set = False
+ with mock.patch(self.REQ_FUNC, side_effect=[Exception(), (200, None)]):
+ auth.update_minimum_password_length()
+
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: True
+ auth.is_admin_password_set = True
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ auth.update_minimum_password_length()
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: True
+ auth.is_admin_password_set = False
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ auth.update_minimum_password_length()
+
+ self._set_args({"ssid": "1", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: False
+ auth.is_embedded_available = lambda: True
+ auth.is_admin_password_set = True
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ auth.update_minimum_password_length()
+ self._set_args({"ssid": "1", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: False
+ auth.is_embedded_available = lambda: True
+ auth.is_admin_password_set = False
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ auth.update_minimum_password_length()
+
+ def test_update_minimum_password_length_fail(self):
+ """Verify update_minimum_password_length throws expected exceptions."""
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ auth.is_admin_password_set = False
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to set minimum password length."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ auth.update_minimum_password_length()
+
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: True
+ auth.is_admin_password_set = False
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to set minimum password length."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ auth.update_minimum_password_length()
+
+ self._set_args({"ssid": "1", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: False
+ auth.is_embedded_available = lambda: True
+ auth.is_admin_password_set = False
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to set minimum password length."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ auth.update_minimum_password_length()
+
+ def test_logout_system_pass(self):
+ """Verify logout_system returns expected values."""
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ with mock.patch(self.REQ_FUNC, return_value=(204, None)):
+ auth.logout_system()
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ with mock.patch(self.REQ_FUNC, return_value=(204, None)):
+ auth.logout_system()
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: True
+ with mock.patch(self.REQ_FUNC, return_value=(204, None)):
+ auth.logout_system()
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass", "minimum_password_length": 8})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: False
+ auth.is_embedded_available = lambda: True
+ with mock.patch(self.REQ_FUNC, return_value=(204, None)):
+ auth.logout_system()
+
+ def test_password_change_required_pass(self):
+ """Verify password_change_required returns expected values."""
+ self._set_args({"ssid": "Proxy", "user": "admin"})
+ auth = NetAppESeriesAuth()
+ self.assertFalse(auth.password_change_required())
+
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": False})]):
+ self.assertTrue(auth.password_change_required())
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: True
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": False})]):
+ self.assertTrue(auth.password_change_required())
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": False})]):
+ self.assertTrue(auth.password_change_required())
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: False
+ auth.is_embedded_available = lambda: True
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": False})]):
+ self.assertTrue(auth.password_change_required())
+
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ auth.logout_system = lambda: None
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": True}), (200, None)]):
+ self.assertFalse(auth.password_change_required())
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": True}), (401, None)]):
+ self.assertTrue(auth.password_change_required())
+
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: True
+ auth.logout_system = lambda: None
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": True}), (200, None)]):
+ self.assertFalse(auth.password_change_required())
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": True}), (401, None)]):
+ self.assertTrue(auth.password_change_required())
+
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ auth.logout_system = lambda: None
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": True}), (200, {"isValidPassword": True})]):
+ self.assertFalse(auth.password_change_required())
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": True}), (200, {"isValidPassword": False})]):
+ self.assertTrue(auth.password_change_required())
+
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: False
+ auth.is_embedded_available = lambda: True
+ auth.logout_system = lambda: None
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": True}), (200, None)]):
+ self.assertFalse(auth.password_change_required())
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": True}), (401, None)]):
+ self.assertTrue(auth.password_change_required())
+
+ def test_password_change_required_fail(self):
+ """Verify password_change_required throws expected exceptions."""
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ auth.logout_system = lambda: None
+ with self.assertRaisesRegexp(AnsibleFailJson, "SAML enabled! SAML disables default role based login."):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": True}), (422, None)]):
+ auth.password_change_required()
+
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ auth.logout_system = lambda: None
+ auth.is_web_services_version_met = lambda x: True
+ with self.assertRaisesRegexp(AnsibleFailJson, "For platforms before E2800 use SANtricity Web Services Proxy 4.1 or later!"):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": True}), (404, None)]):
+ self.assertFalse(auth.password_change_required())
+ auth.is_web_services_version_met = lambda x: False
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to validate stored password!"):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": True}), (404, None)]):
+ self.assertFalse(auth.password_change_required())
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to validate stored password!"):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": True}), (422, None)]):
+ self.assertFalse(auth.password_change_required())
+
+ self._set_args({"ssid": "10", "user": "monitor", "password": "monitorpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ auth.logout_system = lambda: None
+ auth.is_web_services_version_met = lambda x: True
+ with self.assertRaisesRegexp(AnsibleFailJson, "Role based login not available! Only storage system password can be set for storage systems prior to"):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, {"minimumPasswordLength": 8, "adminPasswordSet": True})]):
+ self.assertFalse(auth.password_change_required())
+
+ def test_set_array_admin_password_pass(self):
+ """Verify set_array_admin_password results."""
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, None)]):
+ auth.set_array_admin_password()
+ with mock.patch(self.REQ_FUNC, side_effect=[Exception(), (200, None)]):
+ auth.set_array_admin_password()
+
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, None)]):
+ auth.set_array_admin_password()
+ auth.is_embedded_available = lambda: True
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, None)]):
+ auth.set_array_admin_password()
+
+ self._set_args({"ssid": "1", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: False
+ auth.is_embedded_available = lambda: True
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ auth.set_array_admin_password()
+
+ def test_set_array_admin_password_fail(self):
+ """Verify set_array_admin_password throws expected exceptions."""
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to set proxy's admin password."):
+ with mock.patch(self.REQ_FUNC, side_effect=[Exception(), Exception()]):
+ auth.set_array_admin_password()
+
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to set storage system's admin password."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ auth.set_array_admin_password()
+
+ self._set_args({"ssid": "1", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: False
+ auth.is_embedded_available = lambda: True
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to set embedded storage system's admin password."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ auth.set_array_admin_password()
+
+ def test_set_array_password_pass(self):
+ """Verify set_array_password results."""
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ auth.is_admin_password_set = True
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ auth.set_array_password()
+
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: True
+ auth.is_admin_password_set = True
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ auth.set_array_password()
+
+ self._set_args({"ssid": "1", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: False
+ auth.is_embedded_available = lambda: True
+ auth.is_admin_password_set = True
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ auth.set_array_password()
+
+ def test_set_array_password_fail(self):
+ """Verify set_array_password throws expected exceptions."""
+ self._set_args({"ssid": "Proxy", "user": "monitor", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ auth.is_admin_password_set = False
+ with self.assertRaisesRegexp(AnsibleFailJson, "Admin password not set! Set admin password before changing non-admin user passwords."):
+ auth.set_array_password()
+
+ self._set_args({"ssid": "Proxy", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: False
+ auth.is_admin_password_set = True
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to set proxy password."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ auth.set_array_password()
+
+ self._set_args({"ssid": "10", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: True
+ auth.is_embedded_available = lambda: True
+ auth.is_admin_password_set = True
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to set embedded user password."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ auth.set_array_password()
+
+ self._set_args({"ssid": "1", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_proxy = lambda: False
+ auth.is_embedded_available = lambda: True
+ auth.is_admin_password_set = True
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to set embedded user password."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ auth.set_array_password()
+
+ def test_apply_pass(self):
+ """Verify apply results."""
+ self._set_args({"ssid": "1", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_admin_password_set = True
+ auth.password_change_required = lambda: True
+ auth.minimum_password_length_change_required = lambda: True
+ auth.update_minimum_password_length = lambda: None
+ auth.set_array_admin_password = lambda: None
+ auth.set_array_password = lambda: None
+ with self.assertRaisesRegexp(AnsibleExitJson, "'admin' password and required password length has been changed."):
+ auth.apply()
+
+ self._set_args({"ssid": "1", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_admin_password_set = False
+ auth.password_change_required = lambda: True
+ auth.minimum_password_length_change_required = lambda: True
+ auth.update_minimum_password_length = lambda: None
+ auth.set_array_admin_password = lambda: None
+ auth.set_array_password = lambda: None
+ with self.assertRaisesRegexp(AnsibleExitJson, "'admin' password and required password length has been changed."):
+ auth.apply()
+
+ self._set_args({"ssid": "1", "user": "monitor", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_admin_password_set = True
+ auth.password_change_required = lambda: True
+ auth.minimum_password_length_change_required = lambda: True
+ auth.update_minimum_password_length = lambda: None
+ auth.set_array_admin_password = lambda: None
+ auth.set_array_password = lambda: None
+ with self.assertRaisesRegexp(AnsibleExitJson, "'monitor' password and required password length has been changed."):
+ auth.apply()
+
+ self._set_args({"ssid": "1", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_admin_password_set = True
+ auth.password_change_required = lambda: True
+ auth.minimum_password_length_change_required = lambda: False
+ auth.update_minimum_password_length = lambda: None
+ auth.set_array_admin_password = lambda: None
+ auth.set_array_password = lambda: None
+ with self.assertRaisesRegexp(AnsibleExitJson, "'admin' password has been changed."):
+ auth.apply()
+
+ self._set_args({"ssid": "1", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_admin_password_set = True
+ auth.password_change_required = lambda: False
+ auth.minimum_password_length_change_required = lambda: True
+ auth.update_minimum_password_length = lambda: None
+ auth.set_array_admin_password = lambda: None
+ auth.set_array_password = lambda: None
+ with self.assertRaisesRegexp(AnsibleExitJson, "Required password length has been changed."):
+ auth.apply()
+
+ self._set_args({"ssid": "1", "user": "admin", "password": "adminpass"})
+ auth = NetAppESeriesAuth()
+ auth.is_admin_password_set = True
+ auth.password_change_required = lambda: False
+ auth.minimum_password_length_change_required = lambda: False
+ auth.update_minimum_password_length = lambda: None
+ auth.set_array_admin_password = lambda: None
+ auth.set_array_password = lambda: None
+ with self.assertRaisesRegexp(AnsibleExitJson, "No changes have been made."):
+ auth.apply()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_client_certificate.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_client_certificate.py
new file mode 100644
index 00000000..9541aeb8
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_client_certificate.py
@@ -0,0 +1,373 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import datetime
+import os
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_client_certificate import NetAppESeriesClientCertificate
+from units.modules.utils import AnsibleFailJson, AnsibleExitJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class NetAppESeriesClientCertificateTest(ModuleTestCase):
+
+ REQUIRED_PARAMS = {"api_username": "username",
+ "api_password": "password",
+ "api_url": "https://localhost:8443/devmgr/v2",
+ "ssid": "1", "validate_certs": "no"}
+
+ REQUEST_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_client_certificate.NetAppESeriesClientCertificate.request"
+ LOAD_PEM_X509_CERTIFICATE = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_client_certificate.x509.load_pem_x509_certificate"
+ LOAD_DER_X509_CERTIFICATE = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_client_certificate.x509.load_der_x509_certificate"
+ BASE_REQUEST_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity.request"
+
+ CERTIFICATE_PATH = "certificate.crt"
+ CERTIFICATE_CONTENT = """Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 1 (0x1)
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C=AU, ST=Florida, L=Palm City, O=Internet Widgits Pty Ltd
+ Validity
+ Not Before: Apr 1 19:30:07 2019 GMT
+ Not After : Mar 29 19:30:07 2029 GMT
+ Subject: C=AU, ST=Florida, O=Internet Widgits Pty Ltd, CN=test.example.com
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ Public-Key: (2048 bit)
+ Modulus:
+ 00:ad:64:b5:4c:40:bb:0f:03:e8:2d:a3:76:af:14:
+ 49:b8:06:4a:f9:48:9b:ad:f2:69:55:42:b0:49:de:
+ cd:10:c3:37:71:1a:f8:e1:5e:88:61:b3:c3:0f:7a:
+ 3b:3e:eb:47:d3:7b:02:f9:40:6d:11:e9:c6:d0:05:
+ 3c:ab:d2:51:97:a3:c9:5d:e4:31:89:85:28:dd:96:
+ 75:c7:18:87:0e:a4:26:cb:bc:6d:2f:47:74:89:10:
+ a0:40:5c:39:4e:c2:52:bc:72:25:6c:30:48:dc:50:
+ 4e:c7:10:68:7f:96:ef:14:78:05:b3:53:5a:91:2a:
+ 8f:b0:5d:75:f0:85:b7:34:6f:78:43:44:a6:3c:4d:
+ 87:56:d0:fb:cf:53:de:50:f8:a7:70:89:68:52:83:
+ 87:32:70:da:cc:3f:d5:ae:f8:b4:8f:d9:de:40:b7:
+ 9a:15:c3:83:4b:62:73:d3:a9:e6:fe:2e:4a:33:7f:
+ 13:76:10:d5:d4:04:18:44:9c:b7:a8:17:3f:fe:4b:
+ 5d:d4:92:5e:9f:95:64:77:ef:1c:01:09:6a:a3:29:
+ 33:08:10:fa:5b:1c:ab:45:16:9d:ee:93:0b:90:d4:
+ ea:cf:0e:13:c8:73:d2:29:00:fa:c1:10:ed:20:66:
+ 4f:f5:a5:cf:8d:4e:2a:8e:4a:f2:8e:59:f1:a5:b6:
+ f5:87
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Basic Constraints:
+ CA:FALSE
+ Netscape Comment:
+ OpenSSL Generated Certificate
+ X509v3 Subject Key Identifier:
+ 08:21:10:B9:3E:A5:AF:63:02:88:F3:9D:77:74:FC:BB:AE:A0:BE:6F
+ X509v3 Authority Key Identifier:
+ keyid:B8:CC:D9:8C:03:C6:06:C3:C4:22:DD:04:64:70:79:0C:93:3F:5C:E8
+
+ Signature Algorithm: sha256WithRSAEncryption
+ 5b:9f:d8:f5:74:e0:66:56:99:62:d8:6f:c0:15:d9:fc:4f:8b:
+ 3d:ab:7a:a5:e0:55:49:62:fc:1f:d3:d1:71:4a:55:e9:a2:03:
+ 7b:57:8f:f2:e4:5b:9c:17:9e:e9:fe:4e:20:a7:48:87:e9:e8:
+ 80:e9:89:3c:4a:94:a2:68:6d:6d:b0:53:e3:9f:a5:dc:b9:cb:
+ 21:c3:b0:9f:1b:e1:32:8b:e3:cb:df:ba:32:bb:f4:fd:ef:83:
+ 9e:64:be:c4:37:4e:c2:90:65:60:3e:19:17:57:7f:59:9c:3d:
+ 8a:4b:4d:c6:42:ad:c4:98:d3:e1:88:74:3d:67:8b:6e:fd:85:
+ 1a:d0:ba:52:bc:24:bd:9e:74:82:d6:5f:8f:c7:2d:d8:04:b9:
+ fa:bd:e7:ef:5b:cf:d4:28:bf:c0:9a:6b:0c:7b:b7:3a:95:91:
+ 1c:f3:ad:5b:ce:48:cf:fa:c1:6e:82:f2:df:bd:ba:51:8e:00:
+ fb:86:b1:a6:a9:6a:5e:e4:e4:17:a2:35:b5:3c:fa:b1:4f:8d:
+ b7:24:53:0f:63:ac:16:f5:91:a0:15:e9:59:cd:59:55:28:a3:
+ d9:c0:70:74:30:5b:01:2a:e4:25:44:36:dd:74:f1:4a:3c:c3:
+ ad:52:51:c1:c7:79:7a:d7:21:23:a0:b6:55:c4:0d:27:40:10:
+ 4f:9c:db:04:f8:37:5a:4b:a1:9b:f2:78:b3:63:1a:c5:e3:6a:
+ a8:6d:c9:d5:73:41:91:c0:49:2c:72:32:43:73:f2:15:3e:c1:
+ 31:5d:91:b9:04:c1:78:a8:4e:cf:34:90:ee:05:f9:e5:ee:21:
+ 4c:1b:ae:55:fd:d8:c9:39:91:4c:5e:61:d9:72:10:a4:24:6a:
+ 20:c6:ad:44:0c:81:7a:ca:d5:fc:1c:6a:bf:52:9d:87:13:47:
+ dd:79:9e:6f:6e:03:be:06:7a:87:c9:5f:2d:f8:9f:c6:44:e6:
+ 05:c0:cd:28:17:2c:09:28:50:2b:12:39:ff:86:85:71:6b:f0:
+ cd:0f:4d:54:89:de:88:ee:fb:e8:e3:ba:45:97:9e:67:d6:ae:
+ 38:54:86:79:ca:fe:99:b4:20:25:d2:30:aa:3a:62:95:0f:dd:
+ 42:00:18:88:c7:1f:42:07:1d:dd:9c:42:c4:2f:56:c5:50:b1:
+ cd:6d:b9:36:df:9f:5d:f5:77:b3:cd:e4:b8:62:ed:2b:50:d0:
+ 0b:a2:31:0c:ae:20:8c:b4:0a:83:1f:20:3f:6c:d6:c7:bc:b6:
+ 84:ae:60:6e:69:2b:cb:01:22:55:a4:e5:3e:62:34:bd:20:f8:
+ 12:13:6f:25:8d:49:88:74:ba:61:51:bc:bc:8a:c6:fb:02:31:
+ ce:5b:85:df:55:d0:55:9b
+-----BEGIN CERTIFICATE-----
+MIIEqTCCApGgAwIBAgIBATANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTEQ
+MA4GA1UECAwHRmxvcmlkYTESMBAGA1UEBwwJUGFsbSBDaXR5MSEwHwYDVQQKDBhJ
+bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTkwNDAxMTkzMDA3WhcNMjkwMzI5
+MTkzMDA3WjBdMQswCQYDVQQGEwJBVTEQMA4GA1UECAwHRmxvcmlkYTEhMB8GA1UE
+CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRkwFwYDVQQDDBB0ZXN0LmV4YW1w
+bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArWS1TEC7DwPo
+LaN2rxRJuAZK+UibrfJpVUKwSd7NEMM3cRr44V6IYbPDD3o7PutH03sC+UBtEenG
+0AU8q9JRl6PJXeQxiYUo3ZZ1xxiHDqQmy7xtL0d0iRCgQFw5TsJSvHIlbDBI3FBO
+xxBof5bvFHgFs1NakSqPsF118IW3NG94Q0SmPE2HVtD7z1PeUPincIloUoOHMnDa
+zD/Vrvi0j9neQLeaFcODS2Jz06nm/i5KM38TdhDV1AQYRJy3qBc//ktd1JJen5Vk
+d+8cAQlqoykzCBD6WxyrRRad7pMLkNTqzw4TyHPSKQD6wRDtIGZP9aXPjU4qjkry
+jlnxpbb1hwIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVu
+U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUCCEQuT6lr2MCiPOd
+d3T8u66gvm8wHwYDVR0jBBgwFoAUuMzZjAPGBsPEIt0EZHB5DJM/XOgwDQYJKoZI
+hvcNAQELBQADggIBAFuf2PV04GZWmWLYb8AV2fxPiz2reqXgVUli/B/T0XFKVemi
+A3tXj/LkW5wXnun+TiCnSIfp6IDpiTxKlKJobW2wU+Ofpdy5yyHDsJ8b4TKL48vf
+ujK79P3vg55kvsQ3TsKQZWA+GRdXf1mcPYpLTcZCrcSY0+GIdD1ni279hRrQulK8
+JL2edILWX4/HLdgEufq95+9bz9Qov8Caawx7tzqVkRzzrVvOSM/6wW6C8t+9ulGO
+APuGsaapal7k5BeiNbU8+rFPjbckUw9jrBb1kaAV6VnNWVUoo9nAcHQwWwEq5CVE
+Nt108Uo8w61SUcHHeXrXISOgtlXEDSdAEE+c2wT4N1pLoZvyeLNjGsXjaqhtydVz
+QZHASSxyMkNz8hU+wTFdkbkEwXioTs80kO4F+eXuIUwbrlX92Mk5kUxeYdlyEKQk
+aiDGrUQMgXrK1fwcar9SnYcTR915nm9uA74GeofJXy34n8ZE5gXAzSgXLAkoUCsS
+Of+GhXFr8M0PTVSJ3oju++jjukWXnmfWrjhUhnnK/pm0ICXSMKo6YpUP3UIAGIjH
+H0IHHd2cQsQvVsVQsc1tuTbfn131d7PN5Lhi7StQ0AuiMQyuIIy0CoMfID9s1se8
+toSuYG5pK8sBIlWk5T5iNL0g+BITbyWNSYh0umFRvLyKxvsCMc5bhd9V0FWb
+-----END CERTIFICATE-----"""
+ #
+ # {'expire_date': datetime.datetime(2029, 3, 29, 19, 30, 7),
+ # 'issuer_dn': [u'AU', u'Florida', u'Palm City', u'Internet Widgits Pty Ltd'],
+ # 'start_date': datetime.datetime(2019, 4, 1, 19, 30, 7),
+ # 'subject_dn': [u'AU', u'Florida', u'Internet Widgits Pty Ltd', u'test.example.com']})
+ #
+ CERTIFICATE_FINGERPRINT = b"4cb68a8039a54b2f5fbe4c55dabb92464a0149a9fce64eb779fd3211c482e44e"
+ GET_CERTIFICATE_RESPONSE_OLD = [
+ {"alias": "f869e886-4262-42de-87a6-8f99fc3e6272",
+ "subjectDN": "CN=test.example.com, O=Internet Widgits Pty Ltd, ST=Florida, C=AU",
+ "issuerDN": "O=Internet Widgits Pty Ltd, L=Palm City, ST=Florida, C=AU",
+ "start": "2019-04-01T19:30:07.000+0000", "expire": "2029-03-29T19:30:07.000+0000", "isUserInstalled": True},
+ {"alias": "ca2", "subjectDN": "sdn2", "issuerDN": "idn2",
+ "start": "2019-04-02T13:07:30.516Z", "expire": "2019-04-02T13:07:30.516Z", "isUserInstalled": False},
+ {"alias": "ca3", "subjectDN": "sdn3", "issuerDN": "idn3",
+ "start": "2019-04-02T13:07:30.516Z", "expire": "2019-04-02T13:07:30.516Z", "isUserInstalled": False},
+ {"alias": "ca4", "subjectDN": "sdn4", "issuerDN": "idn4",
+ "start": "2019-04-02T13:07:30.516Z", "expire": "2019-04-02T13:07:30.516Z", "isUserInstalled": False}]
+ GET_CERTIFICATE_RESPONSE = [
+ {'alias': 'alias1', 'expire': '2019-04-02T13:46:04.285Z', 'isKeyEntry': True, 'isUserInstalled': True,
+ 'issuerDN': 'string', 'issuerRdns': [{'attributes': [{'name': 'string', 'value': 'string'}]}],
+ 'sha256Fingerprint': b'4cb68a8039a54b2f5fbe4c55dabb92464a0149a9fce64eb779fd3211c482e44e',
+ 'shaFingerprint': b'4cb68a8039a54b2f5fbe4c55dabb92464a0149a9fce64eb779fd3211c482e44e',
+ 'start': '2019-04-02T13:46:04.285Z', 'status': 'trusted', 'subjectDN': 'string',
+ 'subjectRdns': [{'attributes': [{'name': 'string', 'value': 'string'}]}], 'truststore': True, 'type': 'selfSigned'},
+ {"alias": "alias1", "shaFingerprint": CERTIFICATE_FINGERPRINT, "sha256Fingerprint": CERTIFICATE_FINGERPRINT,
+ "subjectDN": "string", "subjectRdns": [{"attributes": [{"name": "string", "value": "string"}]}],
+ "issuerDN": "string", "issuerRdns": [{"attributes": [{"name": "string", "value": "string"}]}],
+ "start": "2019-04-02T13:46:04.285Z", "expire": "2019-04-02T13:46:04.285Z", "status": "trusted",
+ "truststore": True, "isUserInstalled": True, "isKeyEntry": True, "type": "selfSigned"},
+ {"alias": "alias1", "shaFingerprint": "123412341234", "sha256Fingerprint": "4567345673456",
+ "subjectDN": "string", "subjectRdns": [{"attributes": [{"name": "string", "value": "string"}]}],
+ "issuerDN": "string", "issuerRdns": [{"attributes": [{"name": "string", "value": "string"}]}],
+ "start": "2019-04-02T13:46:04.285Z", "expire": "2019-04-02T13:46:04.285Z", "status": "trusted",
+ "truststore": True, "isUserInstalled": True, "isKeyEntry": True, "type": "selfSigned"}
+ ]
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ if not os.path.exists(self.CERTIFICATE_PATH):
+ with open(self.CERTIFICATE_PATH, "w") as fh:
+ fh.write(self.CERTIFICATE_CONTENT)
+
+ def test_init_url_path_prefix(self):
+ """Verify url path prefix for both embedded and proxy scenarios."""
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ self.assertEquals(certificate.url_path_prefix, "")
+
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": True})]):
+ certificate = NetAppESeriesClientCertificate()
+ self.assertEquals(certificate.url_path_prefix, "storage-systems/1/forward/devmgr/v2/")
+
+ self._set_args({"ssid": "0", "certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": True})]):
+ certificate = NetAppESeriesClientCertificate()
+ self.assertEquals(certificate.url_path_prefix, "")
+
+ self._set_args({"ssid": "PROXY", "certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": True})]):
+ certificate = NetAppESeriesClientCertificate()
+ self.assertEquals(certificate.url_path_prefix, "")
+
+ def test_certificate_info_pass(self):
+ """Determine whether certificate_info returns expected results."""
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ self.assertEquals(certificate.certificate_info(self.CERTIFICATE_PATH),
+ {"start_date": datetime.datetime(2019, 4, 1, 19, 30, 7),
+ "expire_date": datetime.datetime(2029, 3, 29, 19, 30, 7),
+ "subject_dn": ["AU", "Florida", "Internet Widgits Pty Ltd", "test.example.com"],
+ "issuer_dn": ["AU", "Florida", "Palm City", "Internet Widgits Pty Ltd"]})
+
+ def test_certificate_info_fail(self):
+ """Determine wehther certificate_info throws expected exceptions."""
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to load certificate."):
+ with mock.patch(self.LOAD_PEM_X509_CERTIFICATE, side_effect=Exception()):
+ with mock.patch(self.LOAD_DER_X509_CERTIFICATE, side_effect=Exception()):
+ certificate.certificate_info(self.CERTIFICATE_PATH)
+
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to open certificate file or invalid certificate object type."):
+ with mock.patch(self.LOAD_PEM_X509_CERTIFICATE, return_value=None):
+ certificate.certificate_info(self.CERTIFICATE_PATH)
+
+ def test_certificate_fingerprint_pass(self):
+ """Determine whether certificate_fingerprint returns expected results."""
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ self.assertEquals(certificate.certificate_fingerprint(self.CERTIFICATE_PATH), "4cb68a8039a54b2f5fbe4c55dabb92464a0149a9fce64eb779fd3211c482e44e")
+
+ def test_certificate_fingerprint_fail(self):
+ """Determine whether certificate_fingerprint throws expected exceptions."""
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to determine certificate fingerprint."):
+ with mock.patch(self.LOAD_PEM_X509_CERTIFICATE, side_effect=Exception()):
+ with mock.patch(self.LOAD_DER_X509_CERTIFICATE, side_effect=Exception()):
+ certificate.certificate_fingerprint(self.CERTIFICATE_PATH)
+
+ def test_determine_changes_pass(self):
+ """Determine whether determine_changes successful return expected results."""
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, self.GET_CERTIFICATE_RESPONSE)):
+ certificate.determine_changes()
+ self.assertEquals(certificate.add_certificates, ["certificate.crt"])
+ # self.assertEquals(certificate.remove_certificates, [])
+
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(404, None), (200, self.GET_CERTIFICATE_RESPONSE_OLD)]):
+ certificate.determine_changes()
+ self.assertEquals(certificate.add_certificates, [])
+ # self.assertEquals(certificate.remove_certificates, [])
+
+ self._set_args({"certificates": []})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(404, None), (200, self.GET_CERTIFICATE_RESPONSE_OLD)]):
+ certificate.determine_changes()
+ self.assertEquals(certificate.add_certificates, [])
+ self.assertEquals(certificate.remove_certificates, [self.GET_CERTIFICATE_RESPONSE_OLD[0]])
+
+ def test_determine_changes_fail(self):
+ """Determine whether determine_changes throws expected exceptions."""
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to retrieve remote server certificates."):
+ with mock.patch(self.REQUEST_FUNC, return_value=(300, [])):
+ certificate.determine_changes()
+
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to retrieve remote server certificates."):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(404, None), (300, [])]):
+ certificate.determine_changes()
+
+ def test_upload_certificate_pass(self):
+ """Validate upload_certificate successfully completes"""
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, [])):
+ certificate.upload_certificate(self.CERTIFICATE_PATH)
+
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(404, None), (200, [])]):
+ certificate.upload_certificate(self.CERTIFICATE_PATH)
+
+ def test_upload_certificate_fail(self):
+ """Validate upload_certificate successfully completes"""
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to upload certificate."):
+ with mock.patch(self.REQUEST_FUNC, return_value=(300, [])):
+ certificate.upload_certificate(self.CERTIFICATE_PATH)
+
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to upload certificate."):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(404, None), (300, [])]):
+ certificate.upload_certificate(self.CERTIFICATE_PATH)
+
+ def test_delete_certificate_pass(self):
+ """Validate delete_certificate successfully completes"""
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, [])):
+ certificate.delete_certificate({"alias": "alias1"})
+
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(404, None), (200, [])]):
+ certificate.delete_certificate({"alias": "alias1"})
+
+ def test_delete_certificate_fail(self):
+ """Validate delete_certificate successfully completes"""
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to delete certificate."):
+ with mock.patch(self.REQUEST_FUNC, return_value=(300, [])):
+ certificate.delete_certificate({"alias": "alias1"})
+
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to delete certificate."):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(404, None), (300, [])]):
+ certificate.delete_certificate({"alias": "alias1"})
+
+ def test_apply_pass(self):
+ """Verify apply functions as expected."""
+ self._set_args({"certificates": [self.CERTIFICATE_PATH]})
+ with mock.patch(self.BASE_REQUEST_FUNC, side_effect=[(200, {"version": "03.00.0000.0000"}), (200, {"runningAsProxy": False})]):
+ certificate = NetAppESeriesClientCertificate()
+ certificate.determine_changes = lambda: None
+ certificate.delete_certificate = lambda x: None
+ certificate.upload_certificate = lambda x: None
+
+ certificate.remove_certificates = []
+ certificate.add_certificates = []
+ certificate.module.check_mode = False
+ with self.assertRaises(AnsibleExitJson):
+ certificate.apply()
+
+ certificate.remove_certificates = []
+ certificate.add_certificates = []
+ certificate.module.check_mode = True
+ with self.assertRaises(AnsibleExitJson):
+ certificate.apply()
+
+ certificate.remove_certificates = [True]
+ certificate.add_certificates = []
+ certificate.module.check_mode = False
+ with self.assertRaises(AnsibleExitJson):
+ certificate.apply()
+
+ certificate.remove_certificates = []
+ certificate.add_certificates = [True]
+ certificate.module.check_mode = False
+ with self.assertRaises(AnsibleExitJson):
+ certificate.apply()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_discover.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_discover.py
new file mode 100644
index 00000000..5dc390ed
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_discover.py
@@ -0,0 +1,168 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_discover import NetAppESeriesDiscover
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class AlertsTest(ModuleTestCase):
+ REQUIRED_PARAMS = {"subnet_mask": "192.168.1.0/24"}
+ BASE_REQ_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_discover.request'
+ SLEEP_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_discover.sleep'
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_valid_options_pass(self):
+ """Verify constructor accepts valid options."""
+ options_list = [{"ports": [1, 8443]},
+ {"ports": [8080, 65535]},
+ {"ports": [8443], "proxy_url": "https://192.168.1.1:8443/devmgr/v2/", "proxy_username": "admin", "proxy_password": "adminpass"},
+ {"ports": [8443], "proxy_url": "https://192.168.1.1:8443/devmgr/v2/", "proxy_username": "admin", "proxy_password": "adminpass",
+ "prefer_embedded": True},
+ {"ports": [8443], "proxy_url": "https://192.168.1.1:8443/devmgr/v2/", "proxy_username": "admin", "proxy_password": "adminpass",
+ "prefer_embedded": False},
+ {"ports": [8443], "proxy_url": "https://192.168.1.1:8443/devmgr/v2/", "proxy_username": "admin", "proxy_password": "adminpass",
+ "proxy_validate_certs": True},
+ {"ports": [8443], "proxy_url": "https://192.168.1.1:8443/devmgr/v2/", "proxy_username": "admin", "proxy_password": "adminpass",
+ "proxy_validate_certs": False}]
+
+ for options in options_list:
+ self._set_args(options)
+ discover = NetAppESeriesDiscover()
+
+ def test_valid_options_fail(self):
+ """Verify constructor throws expected exceptions."""
+ options_list = [{"ports": [0, 8443]}, {"ports": [8080, 65536]}, {"ports": [8080, "port"]}, {"ports": [8080, -10]}, {"ports": [8080, 70000]}]
+
+ for options in options_list:
+ self._set_args(options)
+ with self.assertRaisesRegexp(AnsibleFailJson, "Invalid port! Ports must be positive numbers between 0 and 65536."):
+ discover = NetAppESeriesDiscover()
+
+ def test_check_ip_address_pass(self):
+ """Verify check_ip_address successfully completes."""
+ self._set_args()
+ with mock.patch(self.BASE_REQ_FUNC, return_value=(200, {"chassisSerialNumber": "012345678901", "storageArrayLabel": "array_label"})):
+ discover = NetAppESeriesDiscover()
+ discover.check_ip_address(discover.systems_found, "192.168.1.100")
+ self.assertEqual(discover.systems_found, {"012345678901": {"api_urls": ["https://192.168.1.100:8443/devmgr/v2/storage-systems/1/"],
+ "label": "array_label", "addresses": [], "proxy_required": False}})
+
+ self._set_args({"ports": [8080, 8443]})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(404, None), (401, None), (200, {"sa": {"saData": {"chassisSerialNumber": "012345678901",
+ "storageArrayLabel": "array_label"}}})]):
+ discover = NetAppESeriesDiscover()
+ discover.check_ip_address(discover.systems_found, "192.168.1.101")
+ self.assertEqual(discover.systems_found, {"012345678901": {"api_urls": ["https://192.168.1.101:8443/devmgr/v2/storage-systems/1/"],
+ "label": "array_label", "addresses": [], "proxy_required": False}})
+
+ def test_no_proxy_discover_pass(self):
+ """Verify no_proxy_discover completes successfully."""
+ self._set_args()
+ discover = NetAppESeriesDiscover()
+ discover.check_ip_address = lambda: None
+ discover.no_proxy_discover()
+
+ def test_verify_proxy_service_pass(self):
+ """Verify verify_proxy_service completes successfully."""
+ self._set_args({"proxy_url": "https://192.168.1.200", "proxy_username": "admin", "proxy_password": "adminpass"})
+ discover = NetAppESeriesDiscover()
+ with mock.patch(self.BASE_REQ_FUNC, return_value=(200, {"runningAsProxy": True})):
+ discover.verify_proxy_service()
+
+ def test_verify_proxy_service_fail(self):
+ """Verify verify_proxy_service throws expected exception."""
+ self._set_args({"proxy_url": "https://192.168.1.200", "proxy_username": "admin", "proxy_password": "adminpass"})
+ discover = NetAppESeriesDiscover()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Web Services is not running as a proxy!"):
+ with mock.patch(self.BASE_REQ_FUNC, return_value=(200, {"runningAsProxy": False})):
+ discover.verify_proxy_service()
+
+ self._set_args({"proxy_url": "https://192.168.1.200", "proxy_username": "admin", "proxy_password": "adminpass"})
+ discover = NetAppESeriesDiscover()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Proxy is not available! Check proxy_url."):
+ with mock.patch(self.BASE_REQ_FUNC, return_value=Exception()):
+ discover.verify_proxy_service()
+
+ def test_test_systems_found_pass(self):
+ """Verify test_systems_found adds to systems_found dictionary."""
+ self._set_args({"proxy_url": "https://192.168.1.200", "proxy_username": "admin", "proxy_password": "adminpass", "prefer_embedded": True})
+ discover = NetAppESeriesDiscover()
+ with mock.patch(self.BASE_REQ_FUNC, return_value=(200, {"runningAsProxy": True})):
+ discover.test_systems_found(discover.systems_found, "012345678901", "array_label", ["192.168.1.100", "192.168.1.102"])
+ self.assertEqual(discover.systems_found, {"012345678901": {"api_urls": ["https://192.168.1.100:8443/devmgr/v2/",
+ "https://192.168.1.102:8443/devmgr/v2/"],
+ "label": "array_label",
+ "addresses": ["192.168.1.100", "192.168.1.102"],
+ "proxy_required": False}})
+
+ def test_proxy_discover_pass(self):
+ """Verify proxy_discover completes successfully."""
+ self._set_args({"subnet_mask": "192.168.1.0/30", "proxy_url": "https://192.168.1.200", "proxy_username": "admin", "proxy_password": "adminpass"})
+ discover = NetAppESeriesDiscover()
+ discover.verify_proxy_service = lambda: None
+ with mock.patch(self.SLEEP_FUNC, return_value=None):
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"requestId": "1"}), (200, {"discoverProcessRunning": True}),
+ (200, {"discoverProcessRunning": False,
+ "storageSystems": [{"controllers": [{"ipAddresses": ["192.168.1.100", "192.168.1.102"]}],
+ "supportedManagementPorts": ["https"], "serialNumber": "012345678901",
+ "label": "array_label"}]})]):
+ discover.proxy_discover()
+
+ self._set_args({"subnet_mask": "192.168.1.0/30", "proxy_url": "https://192.168.1.200", "proxy_username": "admin", "proxy_password": "adminpass"})
+ discover = NetAppESeriesDiscover()
+ discover.verify_proxy_service = lambda: None
+ with mock.patch(self.SLEEP_FUNC, return_value=None):
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"requestId": "1"}), (200, {"discoverProcessRunning": True}),
+ (200, {"discoverProcessRunning": False,
+ "storageSystems": [{"controllers": [{"ipAddresses": ["192.168.1.100", "192.168.1.102"]}],
+ "supportedManagementPorts": [], "serialNumber": "012345678901",
+ "label": "array_label"}]})]):
+ discover.proxy_discover()
+
+ def test_proxy_discover_fail(self):
+ """Verify proxy_discover throws expected exceptions."""
+ self._set_args({"subnet_mask": "192.168.1.0/30", "proxy_url": "https://192.168.1.200", "proxy_username": "admin", "proxy_password": "adminpass"})
+ discover = NetAppESeriesDiscover()
+ discover.verify_proxy_service = lambda: None
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to initiate array discovery."):
+ with mock.patch(self.SLEEP_FUNC, return_value=None):
+ with mock.patch(self.BASE_REQ_FUNC, return_value=Exception()):
+ discover.proxy_discover()
+
+ self._set_args({"subnet_mask": "192.168.1.0/30", "proxy_url": "https://192.168.1.200", "proxy_username": "admin", "proxy_password": "adminpass"})
+ discover = NetAppESeriesDiscover()
+ discover.verify_proxy_service = lambda: None
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to get the discovery results."):
+ with mock.patch(self.SLEEP_FUNC, return_value=None):
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"requestId": "1"}), Exception()]):
+ discover.proxy_discover()
+
+ self._set_args({"subnet_mask": "192.168.1.0/30", "proxy_url": "https://192.168.1.200", "proxy_username": "admin", "proxy_password": "adminpass"})
+ discover = NetAppESeriesDiscover()
+ discover.verify_proxy_service = lambda: None
+ with self.assertRaisesRegexp(AnsibleFailJson, "Timeout waiting for array discovery process."):
+ with mock.patch(self.SLEEP_FUNC, return_value=None):
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"requestId": "1"})] + [(200, {"discoverProcessRunning": True})] * 300):
+ discover.proxy_discover()
+
+ def test_discover_pass(self):
+ """Verify discover successfully completes."""
+ self._set_args({"subnet_mask": "192.168.1.0/30", "proxy_url": "https://192.168.1.200", "proxy_username": "admin", "proxy_password": "adminpass"})
+ discover = NetAppESeriesDiscover()
+ discover.proxy_discover = lambda: None
+ with self.assertRaisesRegexp(AnsibleExitJson, "Discover process complete."):
+ discover.discover()
+
+ self._set_args()
+ discover = NetAppESeriesDiscover()
+ discover.no_proxy_discover = lambda: None
+ with self.assertRaisesRegexp(AnsibleExitJson, "Discover process complete."):
+ discover.discover()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_drive_firmware.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_drive_firmware.py
new file mode 100644
index 00000000..b59bd0ac
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_drive_firmware.py
@@ -0,0 +1,212 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_drive_firmware import NetAppESeriesDriveFirmware
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class HostTest(ModuleTestCase):
+ REQUIRED_PARAMS = {"api_username": "rw",
+ "api_password": "password",
+ "api_url": "http://localhost",
+ "ssid": "1"}
+
+ REQUEST_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_drive_firmware.NetAppESeriesDriveFirmware.request"
+ CREATE_MULTIPART_FORMDATA_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_drive_firmware.create_multipart_formdata"
+ SLEEP_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_drive_firmware.sleep"
+ UPGRADE_LIST_RESPONSE = ({"filename": "test_drive_firmware_1",
+ "driveRefList": ["010000005000C5007EDE4ECF0000000000000000",
+ "010000005000C5007EDF9AAB0000000000000000",
+ "010000005000C5007EDBE3C70000000000000000"]},
+ {"filename": "test_drive_firmware_2",
+ "driveRefList": ["010000005000C5007EDE4ECF0000000000000001",
+ "010000005000C5007EDF9AAB0000000000000001",
+ "010000005000C5007EDBE3C70000000000000001"]})
+
+ FIRMWARE_DRIVES_RESPONSE = {"compatibilities": [
+ {"filename": "test_drive_firmware_1",
+ "firmwareVersion": "MS02",
+ "supportedFirmwareVersions": ["MSB6", "MSB8", "MS00", "MS02"],
+ "compatibleDrives": [{"driveRef": "010000005000C5007EDE4ECF0000000000000000", "onlineUpgradeCapable": True},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000000", "onlineUpgradeCapable": True},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000000", "onlineUpgradeCapable": True}]},
+ {"filename": "test_drive_firmware_2",
+ "firmwareVersion": "MS01",
+ "supportedFirmwareVersions": ["MSB8", "MS00", "MS01"],
+ "compatibleDrives": [{"driveRef": "010000005000C5007EDE4ECF0000000000000001", "onlineUpgradeCapable": True},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000001", "onlineUpgradeCapable": False},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000001", "onlineUpgradeCapable": True}]}]}
+
+ def _set_args(self, args):
+ module_args = self.REQUIRED_PARAMS.copy()
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_upload_firmware(self):
+ """Verify exception is thrown"""
+ self._set_args({"firmware": ["path_to_test_drive_firmware_1", "path_to_test_drive_firmware_2"]})
+ firmware_object = NetAppESeriesDriveFirmware()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to upload drive firmware"):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ with mock.patch(self.CREATE_MULTIPART_FORMDATA_FUNC, return_value=("", {})):
+ firmware_object.upload_firmware()
+
+ def test_upgrade_list_pass(self):
+ """Verify upgrade_list method pass"""
+ side_effects = [(200, self.FIRMWARE_DRIVES_RESPONSE),
+ (200, {"offline": False, "available": True, "firmwareVersion": "MS00"}),
+ (200, {"offline": False, "available": True, "firmwareVersion": "MS01"}),
+ (200, {"offline": False, "available": True, "firmwareVersion": "MS02"})]
+ self._set_args({"firmware": ["path/to/test_drive_firmware_1"]})
+ firmware_object = NetAppESeriesDriveFirmware()
+ with mock.patch(self.REQUEST_FUNC, side_effect=side_effects):
+ self.assertEqual(firmware_object.upgrade_list(), [{"driveRefList": ["010000005000C5007EDE4ECF0000000000000000",
+ "010000005000C5007EDF9AAB0000000000000000"],
+ "filename": "test_drive_firmware_1"}])
+
+ side_effects = [(200, self.FIRMWARE_DRIVES_RESPONSE),
+ (200, {"offline": False, "available": True, "firmwareVersion": "MS02"}),
+ (200, {"offline": False, "available": True, "firmwareVersion": "MS02"}),
+ (200, {"offline": False, "available": True, "firmwareVersion": "MS02"})]
+ self._set_args({"firmware": ["path/to/test_drive_firmware_1"]})
+ firmware_object = NetAppESeriesDriveFirmware()
+ with mock.patch(self.REQUEST_FUNC, side_effect=side_effects):
+ self.assertEqual(firmware_object.upgrade_list(), [])
+
+ def test_upgrade_list_fail(self):
+ """Verify upgrade_list method throws expected exceptions."""
+ self._set_args({"firmware": ["path_to_test_drive_firmware_1"]})
+ firmware_object = NetAppESeriesDriveFirmware()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to complete compatibility and health check."):
+ with mock.patch(self.REQUEST_FUNC, response=Exception()):
+ firmware_object.upgrade_list()
+
+ side_effects = [(200, self.FIRMWARE_DRIVES_RESPONSE),
+ (200, {"offline": False, "available": True, "firmwareVersion": "MS01"}),
+ (200, {"offline": False, "available": True, "firmwareVersion": "MS00"}),
+ Exception()]
+ self._set_args({"firmware": ["path/to/test_drive_firmware_1"]})
+ firmware_object = NetAppESeriesDriveFirmware()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve drive information."):
+ with mock.patch(self.REQUEST_FUNC, side_effect=side_effects):
+ firmware_object.upgrade_list()
+
+ side_effects = [(200, self.FIRMWARE_DRIVES_RESPONSE),
+ (200, {"offline": False, "available": True, "firmwareVersion": "MS01"}),
+ (200, {"offline": False, "available": True, "firmwareVersion": "MS00"}),
+ (200, {"offline": False, "available": True, "firmwareVersion": "MS00"})]
+ self._set_args({"firmware": ["path/to/test_drive_firmware_2"], "upgrade_drives_online": True})
+ firmware_object = NetAppESeriesDriveFirmware()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Drive is not capable of online upgrade."):
+ with mock.patch(self.REQUEST_FUNC, side_effect=side_effects):
+ firmware_object.upgrade_list()
+
+ def test_wait_for_upgrade_completion_pass(self):
+ """Verify function waits for okay status."""
+ self._set_args({"firmware": ["path/to/test_drive_firmware_1", "path/to/test_drive_firmware_2"], "wait_for_completion": True})
+ firmware_object = NetAppESeriesDriveFirmware()
+ firmware_object.upgrade_drives_online = True
+ firmware_object.upgrade_list = lambda: self.UPGRADE_LIST_RESPONSE
+ with mock.patch(self.SLEEP_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[
+ (200, {"driveStatus": [{"driveRef": "010000005000C5007EDE4ECF0000000000000000", "status": "inProgress"},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000000", "status": "okay"},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000000", "status": "okay"},
+ {"driveRef": "010000005000C5007EDE4ECF0000000000000001", "status": "okay"},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000001", "status": "okay"},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000001", "status": "okay"}]}),
+ (200, {"driveStatus": [{"driveRef": "010000005000C5007EDE4ECF0000000000000000", "status": "okay"},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000000", "status": "inProgressRecon"},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000000", "status": "okay"},
+ {"driveRef": "010000005000C5007EDE4ECF0000000000000001", "status": "okay"},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000001", "status": "okay"},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000001", "status": "okay"}]}),
+ (200, {"driveStatus": [{"driveRef": "010000005000C5007EDE4ECF0000000000000000", "status": "okay"},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000000", "status": "okay"},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000000", "status": "pending"},
+ {"driveRef": "010000005000C5007EDE4ECF0000000000000001", "status": "okay"},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000001", "status": "okay"},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000001", "status": "okay"}]}),
+ (200, {"driveStatus": [{"driveRef": "010000005000C5007EDE4ECF0000000000000000", "status": "okay"},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000000", "status": "okay"},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000000", "status": "okay"},
+ {"driveRef": "010000005000C5007EDE4ECF0000000000000001", "status": "notAttempted"},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000001", "status": "okay"},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000001", "status": "okay"}]}),
+ (200, {"driveStatus": [{"driveRef": "010000005000C5007EDE4ECF0000000000000000", "status": "okay"},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000000", "status": "okay"},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000000", "status": "okay"},
+ {"driveRef": "010000005000C5007EDE4ECF0000000000000001", "status": "okay"},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000001", "status": "okay"},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000001", "status": "okay"}]})]):
+ firmware_object.wait_for_upgrade_completion()
+
+ def test_wait_for_upgrade_completion_fail(self):
+ """Verify wait for upgrade completion exceptions."""
+ self._set_args({"firmware": ["path/to/test_drive_firmware_1", "path/to/test_drive_firmware_2"], "wait_for_completion": True})
+ firmware_object = NetAppESeriesDriveFirmware()
+ firmware_object.upgrade_drives_online = True
+ firmware_object.upgrade_list = lambda: self.UPGRADE_LIST_RESPONSE
+ firmware_object.WAIT_TIMEOUT_SEC = 5
+ response = (200, {"driveStatus": [{"driveRef": "010000005000C5007EDE4ECF0000000000000000", "status": "inProgress"},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000000", "status": "inProgressRecon"},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000000", "status": "pending"},
+ {"driveRef": "010000005000C5007EDE4ECF0000000000000001", "status": "notAttempted"},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000001", "status": "okay"},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000001", "status": "okay"}]})
+ with self.assertRaisesRegexp(AnsibleFailJson, "Timed out waiting for drive firmware upgrade."):
+ with mock.patch(self.SLEEP_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, return_value=response):
+ firmware_object.wait_for_upgrade_completion()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve drive status."):
+ with mock.patch(self.SLEEP_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ firmware_object.wait_for_upgrade_completion()
+
+ response = (200, {"driveStatus": [{"driveRef": "010000005000C5007EDE4ECF0000000000000000", "status": "_UNDEFINED"},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000000", "status": "inProgressRecon"},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000000", "status": "pending"},
+ {"driveRef": "010000005000C5007EDE4ECF0000000000000001", "status": "notAttempted"},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000001", "status": "okay"},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000001", "status": "okay"}]})
+ with self.assertRaisesRegexp(AnsibleFailJson, "Drive firmware upgrade failed."):
+ with mock.patch(self.SLEEP_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, return_value=response):
+ firmware_object.wait_for_upgrade_completion()
+
+ def test_upgrade_pass(self):
+ """Verify upgrade upgrade in progress variable properly reports."""
+ self._set_args({"firmware": ["path/to/test_drive_firmware_1", "path/to/test_drive_firmware_2"], "wait_for_completion": False})
+ firmware_object = NetAppESeriesDriveFirmware()
+ firmware_object.upgrade_drives_online = True
+ firmware_object.upgrade_list = lambda: {}
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, {})):
+ firmware_object.upgrade()
+ self.assertTrue(firmware_object.upgrade_in_progress)
+
+ self._set_args({"firmware": ["path_to_test_drive_firmware_1", "path_to_test_drive_firmware_2"], "wait_for_completion": True})
+ firmware_object = NetAppESeriesDriveFirmware()
+ firmware_object.upgrade_drives_online = True
+ firmware_object.upgrade_list = lambda: self.UPGRADE_LIST_RESPONSE
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(200, {}),
+ (200, {"driveStatus": [{"driveRef": "010000005000C5007EDE4ECF0000000000000000", "status": "okay"},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000000", "status": "okay"},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000000", "status": "okay"},
+ {"driveRef": "010000005000C5007EDE4ECF0000000000000001", "status": "okay"},
+ {"driveRef": "010000005000C5007EDF9AAB0000000000000001", "status": "okay"},
+ {"driveRef": "010000005000C5007EDBE3C70000000000000001", "status": "okay"}]})]):
+ firmware_object.upgrade()
+ self.assertFalse(firmware_object.upgrade_in_progress)
+
+ def test_upgrade_fail(self):
+ """Verify upgrade method exceptions."""
+ self._set_args({"firmware": ["path_to_test_drive_firmware_1", "path_to_test_drive_firmware_2"]})
+ firmware_object = NetAppESeriesDriveFirmware()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to upgrade drive firmware."):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ firmware_object.upgrade()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_facts.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_facts.py
new file mode 100644
index 00000000..d3d09427
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_facts.py
@@ -0,0 +1,470 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_facts import Facts
+from units.modules.utils import AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class FactsTest(ModuleTestCase):
+ REQUIRED_PARAMS = {
+ 'api_username': 'rw',
+ 'api_password': 'password',
+ 'api_url': 'http://localhost',
+ 'ssid': '1'
+ }
+ REQUEST_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_facts.Facts.request'
+ GET_CONTROLLERS_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_facts.Facts.get_controllers'
+ WORKLOAD_RESPONSE = [{"id": "4200000001000000000000000000000000000000", "name": "beegfs_metadata",
+ "workloadAttributes": [{"key": "profileId", "value": "ansible_workload_1"}]},
+ {"id": "4200000002000000000000000000000000000000", "name": "other_workload_1",
+ "workloadAttributes": [{"key": "profileId", "value": "Other_1"}]}]
+ GRAPH_RESPONSE = {
+ "sa": {"saData": {"storageArrayLabel": "ictm0718s01c1", "saId": {"worldWideName": "600A098000A4B28D000000005CF10481"}, "fwVersion": "08.42.30.05",
+ "chassisSerialNumber": "021633035190"},
+ "featureParameters": {"cacheBlockSizes": [4096, 8192, 16384, 32768],
+ "supportedSegSizes": [32768, 65536, 131072, 262144, 524288, 495616, 655360, 1982464]},
+ "capabilities": ["autoCodeSync", "autoLunTransfer", "subLunsAllowed", "stagedDownload", "mixedDriveTypes", "bundleMigration", "raid6",
+ "performanceTier", "secureVolume", "protectionInformation", "ssdSupport", "driveSlotLimit", "flashReadCache",
+ "storagePoolsType2", "totalNumberOfArvmMirrorsPerArray", "totalNumberOfPitsPerArray", "totalNumberOfThinVolumesPerArray"],
+ "premiumFeatures": [],
+ "hostSpecificVals": [{"hostType": "FactoryDefault", "index": 0}, {"hostType": "W2KNETNCL", "index": 1}, {"hostPortType": "W2KNETCL", "index": 8},
+ {"hostType": "LnxTPGSALUA_SF", "index": 27}, {"hostType": "LnxDHALUA", "index": 28}]}, "controller": [
+ {"active": True, "quiesced": False, "status": "optimal", "controllerRef": "070000000000000000000001",
+ "physicalLocation": {"trayRef": "0E00000000000000000000000000000000000000", "slot": 1,
+ "locationParent": {"refType": "generic", "controllerRef": None, "symbolRef": "0000000000000000000000000000000000000000",
+ "typedReference": None}, "locationPosition": 1, "label": "A"}, "manufacturer": "NETAPP ",
+ "manufacturerDate": "1474675200", "appVersion": "08.42.30.05", "bootVersion": "08.42.30.05", "productID": "INF-01-00 ",
+ "productRevLevel": "0842", "serialNumber": "021619039162 ", "boardID": "2806", "cacheMemorySize": 3328, "processorMemorySize": 1278,
+ "hostInterfaces": [{"interfaceType": "iscsi", "fibre": None, "ib": None,
+ "iscsi": {"channel": 1, "channelPortRef": "1F00010001010000000000000000000000000000", "tcpListenPort": 3260,
+ "ipv4Enabled": True, "ipv4Data": {"ipv4Address": "0.0.0.0", "ipv4AddressConfigMethod": "configStatic",
+ "ipv4OutboundPacketPriority": {"isEnabled": False, "value": 1},
+ "ipv4VlanId": {"isEnabled": False, "value": 1},
+ "ipv4AddressData": {"configState": "configured", "ipv4Address": "10.10.11.110",
+ "ipv4SubnetMask": "255.255.255.0",
+ "ipv4GatewayAddress": "0.0.0.0"}},
+ "interfaceData": {"type": "ethernet", "ethernetData": {
+ "partData": {"vendorName": "QLogic Corporation", "partNumber": "83xx", "revisionNumber": "5.5.31.511",
+ "serialNumber": "00a098a4b28f"}, "macAddress": "00A098A4B293", "fullDuplex": True,
+ "maximumFramePayloadSize": 9000, "currentInterfaceSpeed": "speed10gig", "maximumInterfaceSpeed": "speed10gig",
+ "linkStatus": "up", "supportedInterfaceSpeeds": ["speed1gig", "speed10gig"], "autoconfigSupport": False,
+ "copperCableDiagnosticsSupport": False}, "infinibandData": None},
+ "interfaceRef": "2201020000000000000000000000000000000000", "ipv6Enabled": True,
+ "ipv6Data": {"ipv6LocalAddresses": [
+ {"address": "FE8000000000000002A098FFFEA4B293",
+ "addressState": {"addressType": "typeInterface", "interfaceAddressState": "configured",
+ "routerAddressState": "__UNDEFINED"}}], "ipv6RoutableAddresses": [
+ {"address": "00000000000000000000000000000000",
+ "addressState": {"addressType": "typeInterface", "interfaceAddressState": "unconfigured",
+ "routerAddressState": "__UNDEFINED"}},
+ {"address": "00000000000000000000000000000000",
+ "addressState": {"addressType": "typeInterface", "interfaceAddressState": "unconfigured",
+ "routerAddressState": "__UNDEFINED"}}],
+ "ipv6PortRouterAddress": {"address": "00000000000000000000000000000000",
+ "addressState": {"addressType": "typeRouter", "interfaceAddressState": "__UNDEFINED",
+ "routerAddressState": "unknown"}},
+ "ipv6AddressConfigMethod": "configStateless", "ipv6OutboundPacketPriority": {"isEnabled": False, "value": 1},
+ "ipv6VlanId": {"isEnabled": False, "value": 1}, "ipv6HopLimit": 64, "ipv6NdReachableTime": 30000,
+ "ipv6NdRetransmitTime": 1000, "ipv6NdStaleTimeout": 30000, "ipv6DuplicateAddressDetectionAttempts": 1},
+ "physicalLocation": {"trayRef": "0000000000000000000000000000000000000000", "slot": 0,
+ "locationParent": {"refType": "generic", "controllerRef": None,
+ "symbolRef": "0000000000000000000000000000000000000000",
+ "typedReference": None}, "locationPosition": 0, "label": ""},
+ "protectionInformationCapable": True, "isIPv6Capable": True, "oneWayMaxRate": "1230000000",
+ "bidirectionalMaxRate": "2120000000", "iqn": "iqn.1992-08.com.netapp:2806.600a098000a4b28d000000005cf10481",
+ "controllerId": "070000000000000000000001",
+ "addressId": "iqn.1992-08.com.netapp:2806.600a098000a4b28d000000005cf10481",
+ "niceAddressId": "iqn.1992-08.com.netapp:2806.600a098000a4b28d000000005cf10481",
+ "interfaceId": "2201020000000000000000000000000000000000", "id": "2201020000000000000000000000000000000000"},
+ "sas": None, "sata": None, "scsi": None}],
+ "driveInterfaces": [
+ {"interfaceType": "sas", "fibre": None, "ib": None, "iscsi": None,
+ "sas": {"channel": 1, "currentInterfaceSpeed": "speed12gig", "maximumInterfaceSpeed": "speed12gig", "part": "LSISAS3008",
+ "revision": 172688896, "isDegraded": False,
+ "iocPort": {
+ "parent": {"type": "controller", "controller": "070000000000000000000001", "drive": None, "expander": None, "hostBoardRef": None},
+ "attachedDevice": {"channel": 1, "channelType": "driveside",
+ "sasAttachedDeviceData": {"type": "expander", "alternateController": None, "drive": None,
+ "expander": "2000000000000000000000630001000000000000",
+ "remoteHostPortAddress": None,
+ "localController": None, "physicalLocation": None}}, "state": "optimal",
+ "miswireType": "None", "channelPortRef": "1F01000001010000000000000000000000000000",
+ "sasPhys": [{"phyIdentifier": 4, "isOperational": True}, {"phyIdentifier": 5, "isOperational": True},
+ {"phyIdentifier": 6, "isOperational": True}, {"phyIdentifier": 7, "isOperational": True}],
+ "portTypeData": {"portType": "endDevice", "portIdentifier": "500A098A4B28D004", "routingType": "__UNDEFINED"},
+ "portMode": "internal",
+ "domainNumber": 1, "attachedChannelPortRef": "0000000000000000000000000000000000000000", "discoveryStatus": 0},
+ "interfaceRef": "2201000000000000000000000000000000000000",
+ "physicalLocation": {"trayRef": "0000000000000000000000000000000000000000", "slot": 0,
+ "locationParent": {"refType": "generic", "controllerRef": None,
+ "symbolRef": "0000000000000000000000000000000000000000", "typedReference": None},
+ "locationPosition": 0, "label": ""}, "protectionInformationCapable": True, "oneWayMaxRate": "4400000000",
+ "bidirectionalMaxRate": "8400000000", "controllerId": None, "addressId": "500A098A4B28D004", "niceAddressId": "500A098A4B28D004",
+ "interfaceId": "2201000000000000000000000000000000000000", "basePortAddress": "500A098A4B28D00",
+ "id": "2201000000000000000000000000000000000000"}, "sata": None, "scsi": None}],
+ "netInterfaces": [{"interfaceType": "ethernet",
+ "ethernet": {"interfaceName": "wan0", "channel": 1, "speed": 1000, "ip": 175178176, "alias": "ictm0718s01c1-a",
+ "macAddr": "00A098A4B28D", "gatewayIp": 175177985, "subnetMask": -256, "bootpUsed": False, "rloginEnabled": True,
+ "reserved1": "0000000000000000", "setupError": False, "reserved2": "",
+ "interfaceRef": "2800070000000000000000000001000000000000", "linkStatus": "up", "ipv4Enabled": True,
+ "ipv4Address": "10.113.1.192", "ipv4SubnetMask": "255.255.255.0", "ipv4AddressConfigMethod": "configStatic",
+ "ipv6Enabled": False, "ipv6LocalAddress": {"address": "00000000000000000000000000000000",
+ "addressState": {"addressType": "typeInterface",
+ "interfaceAddressState": "configured",
+ "routerAddressState": "__UNDEFINED"}},
+ "ipv6PortStaticRoutableAddress": {"address": "00000000000000000000000000000000",
+ "addressState": {"addressType": "typeInterface",
+ "interfaceAddressState": "__UNDEFINED",
+ "routerAddressState": "__UNDEFINED"}},
+ "ipv6PortRoutableAddresses": [], "ipv6AddressConfigMethod": "configStatic", "fullDuplex": True,
+ "supportedSpeedSettings": ["speedAutoNegotiated", "speed10MbitHalfDuplex", "speed10MbitFullDuplex",
+ "speed100MbitHalfDuplex", "speed100MbitFullDuplex", "speed1000MbitFullDuplex"],
+ "configuredSpeedSetting": "speedAutoNegotiated", "currentSpeed": "speed1gig",
+ "physicalLocation": {"trayRef": "0E00000000000000000000000000000000000000", "slot": 0,
+ "locationParent": {"refType": "controller", "controllerRef": "070000000000000000000001",
+ "symbolRef": None, "typedReference": None}, "locationPosition": 1,
+ "label": "P1"}, "ipv4GatewayAddress": "10.113.1.1",
+ "controllerRef": "070000000000000000000001", "controllerSlot": 1,
+ "dnsProperties": {
+ "acquisitionProperties": {"dnsAcquisitionType": "stat",
+ "dnsServers": [
+ {"addressType": "ipv4", "ipv4Address": "10.193.0.250", "ipv6Address": None},
+ {"addressType": "ipv4", "ipv4Address": "10.192.0.250", "ipv6Address": None}]},
+ "dhcpAcquiredDnsServers": []},
+ "ntpProperties": {
+ "acquisitionProperties": {"ntpAcquisitionType": "stat", "ntpServers": [
+ {"addrType": "ipvx", "domainName": None,
+ "ipvxAddress": {"addressType": "ipv4", "ipv4Address": "216.239.35.0", "ipv6Address": None}},
+ {"addrType": "ipvx", "domainName": None,
+ "ipvxAddress": {"addressType": "ipv4", "ipv4Address": "216.239.35.4", "ipv6Address": None}}]},
+ "dhcpAcquiredNtpServers": []},
+ "id": "2800070000000000000000000001000000000000"}}],
+ "inventory": [], "reserved1": "000000000000000000000000", "reserved2": "", "hostBoardID": "None", "physicalCacheMemorySize": 4864,
+ "readyToRemove": False, "boardSubmodelID": "319", "submodelSupported": True, "oemPartNumber": "E2800A-8GB", "partNumber": "111-02829+C0 ",
+ "rtrAttributes": {"cruType": "dedicated", "parentCru": None, "rtrAttributeData": {"hasReadyToRemoveIndicator": False, "readyToRemove": False}},
+ "bootTime": "1563988406", "modelName": "2806",
+ "networkSettings": {"ipv4DefaultRouterAddress": "10.113.1.1",
+ "ipv6DefaultRouterAddress": {"address": "00000000000000000000000000000000",
+ "addressState": {"addressType": "typeInterface",
+ "interfaceAddressState": "__UNDEFINED", "routerAddressState": "__UNDEFINED"}},
+ "ipv6CandidateDefaultRouterAddresses": [],
+ "remoteAccessEnabled": True,
+ "dnsProperties": {"acquisitionProperties": {"dnsAcquisitionType": "stat",
+ "dnsServers": [
+ {"addressType": "ipv4", "ipv4Address": "10.193.0.250", "ipv6Address": None},
+ {"addressType": "ipv4", "ipv4Address": "10.192.0.250", "ipv6Address": None}]},
+ "dhcpAcquiredDnsServers": []},
+ "ntpProperties": {
+ "acquisitionProperties": {
+ "ntpAcquisitionType": "stat", "ntpServers": [
+ {"addrType": "ipvx", "domainName": None,
+ "ipvxAddress": {"addressType": "ipv4", "ipv4Address": "216.239.35.0", "ipv6Address": None}},
+ {"addrType": "ipvx", "domainName": None,
+ "ipvxAddress": {"addressType": "ipv4", "ipv4Address": "216.239.35.4", "ipv6Address": None}}]},
+ "dhcpAcquiredNtpServers": []}},
+ "repairPolicy": {"removalData": {"removalMethod": "__UNDEFINED", "rtrAttributes": None}, "replacementMethod": "__UNDEFINED"},
+ "flashCacheMemorySize": 419430400, "ctrlIocDumpData": {"iocDumpNeedsRetrieved": False, "iocDumpTag": 0, "timeStamp": "0"},
+ "locateInProgress": False, "hasTrayIdentityIndicator": False, "controllerErrorMode": "notInErrorMode",
+ "codeVersions": [{"codeModule": "raid", "versionString": "08.42.30.05"}, {"codeModule": "hypervisor", "versionString": "08.42.30.05"},
+ {"codeModule": "management", "versionString": "11.42.0000.0026"}, {"codeModule": "iom", "versionString": "11.42.0G00.0001"},
+ {"codeModule": "bundle", "versionString": "08.42.30.05"}, {"codeModule": "bundleDisplay", "versionString": "11.40.3R2"}],
+ "id": "070000000000000000000001"}],
+ "drive": [{"offline": False, "hotSpare": False, "invalidDriveData": False, "available": True, "pfa": False,
+ "driveRef": "0100000050000396AC882ED10000000000000000", "status": "optimal", "cause": "None",
+ "interfaceType": {"driveType": "sas", "fibre": None,
+ "sas": {"deviceName": "50000396AC882ED1",
+ "drivePortAddresses": [{"channel": 2, "portIdentifier": "50000396AC882ED3"},
+ {"channel": 1, "portIdentifier": "50000396AC882ED2"}]},
+ "scsi": None},
+ "physicalLocation": {"trayRef": "0E00000000000000000000000000000000000000", "slot": 6,
+ "locationParent": {"refType": "genericTyped", "controllerRef": None, "symbolRef": None,
+ "typedReference": {"componentType": "tray",
+ "symbolRef": "0E00000000000000000000000000000000000000"}},
+ "locationPosition": 6, "label": "5"}, "manufacturer": "TOSHIBA ",
+ "manufacturerDate": "1447200000", "productID": "PX04SVQ160 ", "serialNumber": "Y530A001T5MD", "softwareVersion": "MSB6", "blkSize": 512,
+ "usableCapacity": "1599784443904", "rawCapacity": "1600321314816", "worldWideName": "50000396AC882ED10000000000000000",
+ "currentVolumeGroupRef": "0000000000000000000000000000000000000000", "sparedForDriveRef": "0000000000000000000000000000000000000000",
+ "mirrorDrive": "0000000000000000000000000000000000000000", "nonRedundantAccess": False, "workingChannel": -1, "volumeGroupIndex": -1,
+ "currentSpeed": "speed12gig", "maxSpeed": "speed12gig", "uncertified": False, "hasDegradedChannel": False, "degradedChannels": [],
+ "phyDriveType": "sas", "spindleSpeed": 0, "rtrAttributes": {"cruType": "dedicated", "parentCru": None,
+ "rtrAttributeData": {"hasReadyToRemoveIndicator": False,
+ "readyToRemove": False}}, "reserved": "",
+ "phyDriveTypeData": {"phyDriveType": "sas", "sataDriveAttributes": None}, "pfaReason": "None", "bypassSource": [],
+ "repairPolicy": {"removalData": {"removalMethod": "self", "rtrAttributes": {"hasReadyToRemoveIndicator": False, "readyToRemove": False}},
+ "replacementMethod": "self"}, "fdeCapable": True, "fdeEnabled": False, "fdeLocked": False,
+ "lockKeyID": "0000000000000000000000000000000000000000",
+ "ssdWearLife": {"averageEraseCountPercent": 18, "spareBlocksRemainingPercent": 91, "isWearLifeMonitoringSupported": True,
+ "percentEnduranceUsed": 18}, "driveMediaType": "ssd", "fpgaVersion": "",
+ "protectionInformationCapabilities": {"protectionInformationCapable": True, "protectionType": "type2Protection"},
+ "protectionInformationCapable": False, "protectionType": "type0Protection", "interposerPresent": False,
+ "interposerRef": "0000000000000000000000000000000000000000", "currentCommandAgingTimeout": 6, "defaultCommandAgingTimeout": 6,
+ "driveTemperature": {"currentTemp": 25, "refTemp": 64}, "blkSizePhysical": 4096, "lowestAlignedLBA": "0", "removed": False,
+ "locateInProgress": False, "fipsCapable": False, "firmwareVersion": "MSB6", "lockKeyIDValue": None,
+ "id": "0100000050000396AC882ED10000000000000000"},
+ {"offline": False, "hotSpare": False, "invalidDriveData": False, "available": True, "pfa": False,
+ "driveRef": "0100000050000396AC882EDD0000000000000000", "status": "optimal", "cause": "None",
+ "interfaceType": {"driveType": "sas", "fibre": None,
+ "sas": {"deviceName": "50000396AC882EDD",
+ "drivePortAddresses": [{"channel": 2, "portIdentifier": "50000396AC882EDF"},
+ {"channel": 1, "portIdentifier": "50000396AC882EDE"}]},
+ "scsi": None},
+ "physicalLocation": {"trayRef": "0E00000000000000000000000000000000000000", "slot": 8,
+ "locationParent": {"refType": "genericTyped", "controllerRef": None, "symbolRef": None,
+ "typedReference": {"componentType": "tray",
+ "symbolRef": "0E00000000000000000000000000000000000000"}},
+ "locationPosition": 8, "label": "7"}, "manufacturer": "TOSHIBA ",
+ "manufacturerDate": "1447200000", "productID": "PX04SVQ160 ", "serialNumber": "Y530A004T5MD", "softwareVersion": "MSB6", "blkSize": 512,
+ "usableCapacity": "1599784443904", "rawCapacity": "1600321314816", "worldWideName": "50000396AC882EDD0000000000000000",
+ "currentVolumeGroupRef": "0000000000000000000000000000000000000000", "sparedForDriveRef": "0000000000000000000000000000000000000000",
+ "mirrorDrive": "0000000000000000000000000000000000000000", "nonRedundantAccess": False, "workingChannel": -1, "volumeGroupIndex": -1,
+ "currentSpeed": "speed12gig", "maxSpeed": "speed12gig", "uncertified": False, "hasDegradedChannel": False, "degradedChannels": [],
+ "phyDriveType": "sas", "spindleSpeed": 0, "rtrAttributes": {"cruType": "dedicated", "parentCru": None,
+ "rtrAttributeData": {"hasReadyToRemoveIndicator": False,
+ "readyToRemove": False}}, "reserved": "",
+ "phyDriveTypeData": {"phyDriveType": "sas", "sataDriveAttributes": None}, "pfaReason": "None", "bypassSource": [],
+ "repairPolicy": {"removalData": {"removalMethod": "self", "rtrAttributes": {"hasReadyToRemoveIndicator": False, "readyToRemove": False}},
+ "replacementMethod": "self"}, "fdeCapable": True, "fdeEnabled": False, "fdeLocked": False,
+ "lockKeyID": "0000000000000000000000000000000000000000",
+ "ssdWearLife": {"averageEraseCountPercent": 18, "spareBlocksRemainingPercent": 91, "isWearLifeMonitoringSupported": True,
+ "percentEnduranceUsed": 18}, "driveMediaType": "ssd", "fpgaVersion": "",
+ "protectionInformationCapabilities": {"protectionInformationCapable": True, "protectionType": "type2Protection"},
+ "protectionInformationCapable": False, "protectionType": "type0Protection", "interposerPresent": False,
+ "interposerRef": "0000000000000000000000000000000000000000", "currentCommandAgingTimeout": 6, "defaultCommandAgingTimeout": 6,
+ "driveTemperature": {"currentTemp": 25, "refTemp": 64}, "blkSizePhysical": 4096, "lowestAlignedLBA": "0", "removed": False,
+ "locateInProgress": False, "fipsCapable": False, "firmwareVersion": "MSB6", "lockKeyIDValue": None,
+ "id": "0100000050000396AC882EDD0000000000000000"}],
+ "volumeGroup": [
+ {"sequenceNum": 1, "offline": False, "raidLevel": "raid6", "worldWideName": "600A098000A4B9D10000380A5D4AAC3C",
+ "volumeGroupRef": "04000000600A098000A4B9D10000380A5D4AAC3C", "reserved1": "000000000000000000000000", "reserved2": "",
+ "trayLossProtection": False, "label": "beegfs_storage_vg", "state": "complete", "spindleSpeedMatch": True, "spindleSpeed": 10500,
+ "isInaccessible": False, "securityType": "capable", "drawerLossProtection": False, "protectionInformationCapable": False,
+ "protectionInformationCapabilities": {"protectionInformationCapable": True, "protectionType": "type2Protection"},
+ "volumeGroupData": {"type": "unknown", "diskPoolData": None},
+ "usage": "standard", "driveBlockFormat": "allNative", "reservedSpaceAllocated": False, "securityLevel": "fde", "usedSpace": "1099511627776",
+ "totalRaidedSpace": "9597654597632",
+ "extents": [{"sectorOffset": "268435456", "rawCapacity": "8498142969856", "raidLevel": "raid6",
+ "volumeGroupRef": "04000000600A098000A4B9D10000380A5D4AAC3C", "freeExtentRef": "03000000600A098000A4B9D10000380A5D4AAC3C",
+ "reserved1": "000000000000000000000000", "reserved2": ""}],
+ "largestFreeExtentSize": "8498142969856", "raidStatus": "optimal", "freeSpace": "8498142969856", "drivePhysicalType": "sas",
+ "driveMediaType": "hdd", "normalizedSpindleSpeed": "spindleSpeed10k", "diskPool": False,
+ "id": "04000000600A098000A4B9D10000380A5D4AAC3C", "name": "beegfs_storage_vg"}], "volume": [
+ {"offline": False, "extremeProtection": False, "volumeHandle": 0, "raidLevel": "raid6", "sectorOffset": "0",
+ "worldWideName": "600A098000A4B28D00003E435D4AAC54", "label": "beegfs_storage_01_1", "blkSize": 512, "capacity": "1099511627776",
+ "reconPriority": 1, "segmentSize": 131072, "action": "None",
+ "cache": {"cwob": False, "enterpriseCacheDump": False, "mirrorActive": True, "mirrorEnable": True, "readCacheActive": False,
+ "readCacheEnable": False, "writeCacheActive": True, "writeCacheEnable": True, "cacheFlushModifier": "flush10Sec",
+ "readAheadMultiplier": 1}, "mediaScan": {"enable": True, "parityValidationEnable": True},
+ "volumeRef": "02000000600A098000A4B28D00003E435D4AAC54", "status": "optimal", "volumeGroupRef": "04000000600A098000A4B9D10000380A5D4AAC3C",
+ "currentManager": "070000000000000000000001", "preferredManager": "070000000000000000000001",
+ "perms": {"mapToLUN": True, "snapShot": True, "format": True, "reconfigure": True, "mirrorPrimary": True, "mirrorSecondary": True,
+ "copySource": True, "copyTarget": True, "readable": True, "writable": True, "rollback": True, "mirrorSync": True, "newImage": True,
+ "allowDVE": True, "allowDSS": True, "concatVolumeMember": False, "flashReadCache": True, "asyncMirrorPrimary": True,
+ "asyncMirrorSecondary": True, "pitGroup": True, "cacheParametersChangeable": True, "allowThinManualExpansion": False,
+ "allowThinGrowthParametersChange": False},
+ "mgmtClientAttribute": 0, "dssPreallocEnabled": False, "dssMaxSegmentSize": 0, "preReadRedundancyCheckEnabled": False,
+ "protectionInformationCapable": False, "protectionType": "type0Protection", "applicationTagOwned": True,
+ "repairedBlockCount": 0, "extendedUniqueIdentifier": "", "cacheMirroringValidateProtectionInformation": False,
+ "expectedProtectionInformationAppTag": 0, "volumeUse": "standardVolume", "volumeFull": False, "volumeCopyTarget": False, "volumeCopySource": False,
+ "pitBaseVolume": False, "asyncMirrorTarget": False, "asyncMirrorSource": False, "remoteMirrorSource": False, "remoteMirrorTarget": False,
+ "diskPool": False, "flashCached": False, "increasingBy": "0", "metadata": [], "dataAssurance": False, "objectType": "volume",
+ "listOfMappings": [
+ {"lunMappingRef": "88000000A1010000000000000000000000000000", "lun": 1, "ssid": 0, "perms": 15,
+ "volumeRef": "02000000600A098000A4B28D00003E435D4AAC54", "type": "host", "mapRef": "84000000600A098000A4B28D00303D065D430118",
+ "id": "88000000A1010000000000000000000000000000"}],
+ "mapped": True, "currentControllerId": "070000000000000000000001",
+ "cacheSettings": {"cwob": False, "enterpriseCacheDump": False, "mirrorActive": True, "mirrorEnable": True, "readCacheActive": False,
+ "readCacheEnable": False, "writeCacheActive": True, "writeCacheEnable": True, "cacheFlushModifier": "flush10Sec",
+ "readAheadMultiplier": 1},
+ "thinProvisioned": False, "preferredControllerId": "070000000000000000000001", "totalSizeInBytes": "1099511627776", "onlineVolumeCopy": False,
+ "wwn": "600A098000A4B28D00003E435D4AAC54", "name": "beegfs_storage_01_1", "id": "02000000600A098000A4B28D00003E435D4AAC54"}],
+ "storagePoolBundle": {"cluster": [], "host": [
+ {"hostRef": "84000000600A098000A4B28D00303D005D430107", "clusterRef": "0000000000000000000000000000000000000000", "label": "test",
+ "isSAControlled": False, "confirmLUNMappingCreation": False, "hostTypeIndex": 28, "protectionInformationCapableAccessMethod": True,
+ "isLargeBlockFormatHost": False, "isLun0Restricted": False, "ports": [],
+ "initiators": [
+ {"initiatorRef": "89000000600A098000A4B9D1003037005D4300F5",
+ "nodeName": {"ioInterfaceType": "iscsi", "iscsiNodeName": "iqn.iscsi_tests1", "remoteNodeWWN": None, "nvmeNodeName": None},
+ "alias": {"ioInterfaceType": "iscsi", "iscsiAlias": ""}, "label": "iscsi_test1",
+ "configuredAuthMethods": {"authMethodData": [{"authMethod": "None", "chapSecret": None}]},
+ "hostRef": "84000000600A098000A4B28D00303D005D430107", "initiatorInactive": False, "id": "89000000600A098000A4B9D1003037005D4300F5"}],
+ "hostSidePorts": [{"type": "iscsi", "address": "iqn.iscsi_tests1", "label": "iscsi_test1"}],
+ "id": "84000000600A098000A4B28D00303D005D430107", "name": "test"},
+ {"hostRef": "84000000600A098000A4B9D1003037035D4300F8", "clusterRef": "0000000000000000000000000000000000000000", "label": "test2",
+ "isSAControlled": True, "confirmLUNMappingCreation": False, "hostTypeIndex": 28, "protectionInformationCapableAccessMethod": True,
+ "isLargeBlockFormatHost": False, "isLun0Restricted": False, "ports": [],
+ "initiators": [
+ {"initiatorRef": "89000000600A098000A4B9D1003037075D4300F9",
+ "nodeName": {"ioInterfaceType": "iscsi", "iscsiNodeName": "iqn.iscsi_tests2", "remoteNodeWWN": None, "nvmeNodeName": None},
+ "alias": {"ioInterfaceType": "iscsi", "iscsiAlias": ""}, "label": "iscsi_test2",
+ "configuredAuthMethods": {"authMethodData": [{"authMethod": "None", "chapSecret": None}]},
+ "hostRef": "84000000600A098000A4B9D1003037035D4300F8", "initiatorInactive": False, "id": "89000000600A098000A4B9D1003037075D4300F9"}],
+ "hostSidePorts": [{"type": "iscsi", "address": "iqn.iscsi_tests2", "label": "iscsi_test2"}],
+ "id": "84000000600A098000A4B9D1003037035D4300F8", "name": "test2"},
+ {"hostRef": "84000000600A098000A4B28D00303D065D430118", "clusterRef": "0000000000000000000000000000000000000000", "label": "beegfs_storage1",
+ "isSAControlled": False, "confirmLUNMappingCreation": False, "hostTypeIndex": 28, "protectionInformationCapableAccessMethod": True,
+ "isLargeBlockFormatHost": False, "isLun0Restricted": False, "ports": [],
+ "initiators": [
+ {"initiatorRef": "89000000600A098000A4B28D00303CF55D4300E3",
+ "nodeName": {"ioInterfaceType": "iscsi", "iscsiNodeName": "iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818", "remoteNodeWWN": None,
+ "nvmeNodeName": None}, "alias": {"ioInterfaceType": "iscsi", "iscsiAlias": ""}, "label": "beegfs_storage1_iscsi_0",
+ "configuredAuthMethods": {"authMethodData": [{"authMethod": "None", "chapSecret": None}]},
+ "hostRef": "84000000600A098000A4B28D00303D065D430118", "initiatorInactive": False, "id": "89000000600A098000A4B28D00303CF55D4300E3"}],
+ "hostSidePorts": [{"type": "iscsi", "address": "iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818", "label": "beegfs_storage1_iscsi_0"}],
+ "id": "84000000600A098000A4B28D00303D065D430118", "name": "beegfs_storage1"},
+ {"hostRef": "84000000600A098000A4B9D10030370B5D430109", "clusterRef": "0000000000000000000000000000000000000000", "label": "beegfs_metadata1",
+ "isSAControlled": False, "confirmLUNMappingCreation": False, "hostTypeIndex": 28, "protectionInformationCapableAccessMethod": True,
+ "isLargeBlockFormatHost": False, "isLun0Restricted": False, "ports": [],
+ "initiators": [
+ {"initiatorRef": "89000000600A098000A4B28D00303CFC5D4300F7",
+ "nodeName": {"ioInterfaceType": "iscsi", "iscsiNodeName": "iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8", "remoteNodeWWN": None,
+ "nvmeNodeName": None}, "alias": {"ioInterfaceType": "iscsi", "iscsiAlias": ""}, "label": "beegfs_metadata1_iscsi_0",
+ "configuredAuthMethods": {"authMethodData": [{"authMethod": "None", "chapSecret": None}]},
+ "hostRef": "84000000600A098000A4B9D10030370B5D430109", "initiatorInactive": False, "id": "89000000600A098000A4B28D00303CFC5D4300F7"}],
+ "hostSidePorts": [{"type": "iscsi", "address": "iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8", "label": "beegfs_metadata1_iscsi_0"}],
+ "id": "84000000600A098000A4B9D10030370B5D430109", "name": "beegfs_metadata1"}], "lunMapping": [
+ {"lunMappingRef": "8800000000000000000000000000000000000000", "lun": 7, "ssid": 16384, "perms": 15,
+ "volumeRef": "21000000600A098000A4B28D000027EC5CF10481", "type": "all", "mapRef": "0000000000000000000000000000000000000000",
+ "id": "8800000000000000000000000000000000000000"},
+ {"lunMappingRef": "880000008B010000000000000000000000000000", "lun": 7, "ssid": 16384, "perms": 15,
+ "volumeRef": "21000000600A098000A4B28D000027EC5CF10481", "type": "host", "mapRef": "84000000600A098000A4B28D00303D065D430118",
+ "id": "880000008B010000000000000000000000000000"},
+ {"lunMappingRef": "8800000090010000000000000000000000000000", "lun": 7, "ssid": 16384, "perms": 15,
+ "volumeRef": "21000000600A098000A4B28D000027EC5CF10481", "type": "host", "mapRef": "84000000600A098000A4B9D10030370B5D430109",
+ "id": "8800000090010000000000000000000000000000"},
+ {"lunMappingRef": "8800000092010000000000000000000000000000", "lun": 7, "ssid": 16384, "perms": 15,
+ "volumeRef": "21000000600A098000A4B28D000027EC5CF10481", "type": "host", "mapRef": "84000000600A098000A4B28D00303D005D430107",
+ "id": "8800000092010000000000000000000000000000"}, {"lunMappingRef": "88000000A1010000000000000000000000000000", "lun": 1, "ssid": 0, "perms": 15,
+ "volumeRef": "02000000600A098000A4B28D00003E435D4AAC54", "type": "host",
+ "mapRef": "84000000600A098000A4B28D00303D065D430118",
+ "id": "88000000A1010000000000000000000000000000"}]}, "highLevelVolBundle": {"pit": []}}
+
+ EXPECTED_GET_ARRAY_FACTS = {'facts_from_proxy': False,
+ 'netapp_controllers': [{'name': 'A', 'serial': '021619039162', 'status': 'optimal'}],
+ 'netapp_default_hostgroup_access_volume_lun': 7,
+ 'netapp_disks': [
+ {'available': True, 'firmware_version': 'MSB6', 'id': '0100000050000396AC882ED10000000000000000', 'media_type': 'ssd',
+ 'product_id': 'PX04SVQ160 ', 'serial_number': 'Y530A001T5MD', 'status': 'optimal',
+ 'tray_ref': '0E00000000000000000000000000000000000000', 'usable_bytes': '1599784443904'},
+ {'available': True, 'firmware_version': 'MSB6', 'id': '0100000050000396AC882EDD0000000000000000', 'media_type': 'ssd',
+ 'product_id': 'PX04SVQ160 ', 'serial_number': 'Y530A004T5MD', 'status': 'optimal',
+ 'tray_ref': '0E00000000000000000000000000000000000000', 'usable_bytes': '1599784443904'}],
+ 'netapp_driveside_interfaces': [{'controller': 'A', 'interface_speed': '12g', 'interface_type': 'sas'}],
+ 'netapp_enabled_features': ['autoCodeSync', 'autoLunTransfer', 'bundleMigration', 'driveSlotLimit', 'flashReadCache',
+ 'mixedDriveTypes', 'performanceTier', 'protectionInformation', 'raid6', 'secureVolume',
+ 'ssdSupport', 'stagedDownload', 'storagePoolsType2', 'subLunsAllowed',
+ 'totalNumberOfArvmMirrorsPerArray', 'totalNumberOfPitsPerArray',
+ 'totalNumberOfThinVolumesPerArray'],
+ 'netapp_host_groups': [{'hosts': ['test',
+ 'test2',
+ 'beegfs_storage1',
+ 'beegfs_metadata1'],
+ 'id': '0000000000000000000000000000000000000000',
+ 'name': 'default_hostgroup'}],
+ 'netapp_host_types': [{'index': 0, 'type': 'FactoryDefault'}, {'index': 1, 'type': 'W2KNETNCL'},
+ {'index': 27, 'type': 'LnxTPGSALUA_SF'}, {'index': 28, 'type': 'LnxDHALUA'}],
+ 'netapp_hosts': [
+ {'group_id': '0000000000000000000000000000000000000000', 'host_type_index': 28,
+ 'hosts_reference': '84000000600A098000A4B28D00303D005D430107',
+ 'id': '84000000600A098000A4B28D00303D005D430107', 'name': 'test',
+ 'ports': [{'address': 'iqn.iscsi_tests1', 'label': 'iscsi_test1', 'type': 'iscsi'}]},
+ {'group_id': '0000000000000000000000000000000000000000', 'host_type_index': 28,
+ 'hosts_reference': '84000000600A098000A4B9D1003037035D4300F8',
+ 'id': '84000000600A098000A4B9D1003037035D4300F8', 'name': 'test2',
+ 'ports': [{'address': 'iqn.iscsi_tests2', 'label': 'iscsi_test2', 'type': 'iscsi'}]},
+ {'group_id': '0000000000000000000000000000000000000000', 'host_type_index': 28,
+ 'hosts_reference': '84000000600A098000A4B28D00303D065D430118',
+ 'id': '84000000600A098000A4B28D00303D065D430118', 'name': 'beegfs_storage1',
+ 'ports': [{'address': 'iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818', 'label': 'beegfs_storage1_iscsi_0',
+ 'type': 'iscsi'}]},
+ {'group_id': '0000000000000000000000000000000000000000', 'host_type_index': 28,
+ 'hosts_reference': '84000000600A098000A4B9D10030370B5D430109',
+ 'id': '84000000600A098000A4B9D10030370B5D430109', 'name': 'beegfs_metadata1',
+ 'ports': [{'address': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8', 'label': 'beegfs_metadata1_iscsi_0',
+ 'type': 'iscsi'}]}],
+ 'netapp_hostside_interfaces': [{'fc': [], 'ib': [],
+ 'iscsi': [
+ {'controller': 'A', 'current_interface_speed': '10g', 'ipv4_address': '10.10.11.110',
+ 'ipv4_enabled': True,
+ 'ipv4_gateway': '0.0.0.0', 'ipv4_subnet_mask': '255.255.255.0', 'ipv6_enabled': True,
+ 'iqn': 'iqn.1992-08.com.netapp:2806.600a098000a4b28d000000005cf10481', 'link_status': 'up',
+ 'mtu': 9000,
+ 'supported_interface_speeds': ['1g', '10g']}], 'sas': []}],
+ 'netapp_luns_by_target': {'beegfs_metadata1': [],
+ 'beegfs_storage1': [('beegfs_storage_01_1', 1)],
+ 'default_hostgroup': [('beegfs_storage_01_1', 1)],
+ 'test': [],
+ 'test2': []},
+ 'netapp_management_interfaces': [
+ {'alias': 'ictm0718s01c1-a', 'channel': 1, 'controller': 'A', 'dns_config_method': 'stat',
+ 'dns_servers': [{'addressType': 'ipv4', 'ipv4Address': '10.193.0.250', 'ipv6Address': None},
+ {'addressType': 'ipv4', 'ipv4Address': '10.192.0.250', 'ipv6Address': None}],
+ 'ipv4_address': '10.113.1.192',
+ 'ipv4_address_config_method': 'static', 'ipv4_enabled': True, 'ipv4_gateway': '10.113.1.1',
+ 'ipv4_subnet_mask': '255.255.255.0', 'ipv6_enabled': False, 'link_status': 'up',
+ 'mac_address': '00A098A4B28D', 'name': 'wan0', 'ntp_config_method': 'stat',
+ 'ntp_servers': [
+ {'addrType': 'ipvx', 'domainName': None,
+ 'ipvxAddress': {'addressType': 'ipv4', 'ipv4Address': '216.239.35.0', 'ipv6Address': None}},
+ {'addrType': 'ipvx', 'domainName': None,
+ 'ipvxAddress': {'addressType': 'ipv4', 'ipv4Address': '216.239.35.4', 'ipv6Address': None}}],
+ 'remote_ssh_access': True}],
+ 'netapp_storage_array': {'cache_block_sizes': [4096, 8192, 16384, 32768], 'chassis_serial': '021633035190',
+ 'firmware': '08.42.30.05', 'name': 'ictm0718s01c1',
+ 'segment_sizes': [32768, 65536, 131072, 262144, 524288, 495616, 655360, 1982464],
+ 'wwn': '600A098000A4B28D000000005CF10481'},
+ 'netapp_storage_pools': [
+ {'available_capacity': '8498142969856', 'id': '04000000600A098000A4B9D10000380A5D4AAC3C', 'name': 'beegfs_storage_vg',
+ 'total_capacity': '9597654597632', 'used_capacity': '1099511627776'}],
+ 'netapp_volumes': [
+ {'capacity': '1099511627776', 'id': '02000000600A098000A4B28D00003E435D4AAC54', 'is_thin_provisioned': False,
+ 'name': 'beegfs_storage_01_1', 'parent_storage_pool_id': '04000000600A098000A4B9D10000380A5D4AAC3C', 'workload': []}],
+ 'netapp_volumes_by_initiators': {'beegfs_metadata1': [],
+ 'beegfs_storage1': [{'id': '02000000600A098000A4B28D00003E435D4AAC54',
+ 'meta_data': {},
+ 'name': 'beegfs_storage_01_1',
+ 'raid_level': 'raid6',
+ 'segment_size_kb': 128,
+ 'stripe_count': -2,
+ 'workload_name': '',
+ 'wwn': '600A098000A4B28D00003E435D4AAC54'}],
+
+ 'test': [], 'test2': []},
+ 'netapp_workload_tags': [
+ {'attributes': [{'key': 'profileId', 'value': 'ansible_workload_1'}], 'id': '4200000001000000000000000000000000000000',
+ 'name': 'beegfs_metadata'},
+ {'attributes': [{'key': 'profileId', 'value': 'Other_1'}], 'id': '4200000002000000000000000000000000000000',
+ 'name': 'other_workload_1'}], 'snapshot_images': [], 'ssid': '1'}
+
+ def _set_args(self, **kwargs):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if kwargs is not None:
+ module_args.update(kwargs)
+ set_module_args(module_args)
+
+ def test_get_controllers_pass(self):
+ """Verify get_controllers returns the expected results."""
+ self._set_args()
+ facts = Facts()
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, ["070000000000000000000002", "070000000000000000000001"])):
+ self.assertEqual(facts.get_controllers(), {"070000000000000000000001": "A", "070000000000000000000002": "B"})
+
+ def test_get_controllers_fail(self):
+ """Verify get_controllers throws the expected exceptions."""
+ self._set_args()
+ facts = Facts()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve controller list!"):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ facts.get_controllers()
+
+ def test_get_array_facts_pass(self):
+ """Verify get_array_facts method returns expected results."""
+ self.maxDiff = None
+ self._set_args()
+ facts = Facts()
+ facts.is_embedded = lambda: True
+ with mock.patch(self.GET_CONTROLLERS_FUNC, return_value={"070000000000000000000001": "A", "070000000000000000000002": "B"}):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(200, self.GRAPH_RESPONSE), (200, self.WORKLOAD_RESPONSE)]):
+ self.assertEquals(facts.get_array_facts(), self.EXPECTED_GET_ARRAY_FACTS)
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_firmware.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_firmware.py
new file mode 100644
index 00000000..8c786d63
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_firmware.py
@@ -0,0 +1,494 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils import six
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_firmware import NetAppESeriesFirmware
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat.mock import patch, mock_open
+
+if six.PY2:
+ builtin_path = "__builtin__.open"
+else:
+ builtin_path = "builtins.open"
+
+
+def mock_open_with_iter(*args, **kwargs):
+ mock = mock_open(*args, **kwargs)
+
+ if six.PY2:
+ mock.return_value.__iter__ = lambda x: iter(x.readline, "")
+ else:
+ mock.return_value.__iter__ = lambda x: x
+ mock.return_value.__next__ = lambda x: iter(x.readline, "")
+ return mock
+
+
+class FirmwareTest(ModuleTestCase):
+ REQUIRED_PARAMS = {"api_username": "username",
+ "api_password": "password",
+ "api_url": "http://localhost/devmgr/v2",
+ "ssid": "1",
+ "validate_certs": "no"}
+ REQUEST_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_firmware.NetAppESeriesFirmware.request"
+ BASE_REQUEST_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_firmware.request"
+ CREATE_MULTIPART_FORMDATA_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_firmware.create_multipart_formdata"
+ SLEEP_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_firmware.sleep"
+ BUNDLE_HEADER = b'combined_content\x00\x00\x00\x04\x00\x00\x07\xf8#Engenio Downloadable Package\n#Tue Jun 04 11:46:48 CDT 2019\ncheckList=compatibleBoard' \
+ b'Map,compatibleSubmodelMap,compatibleFirmwareMap,fileManifest\ncompatibleSubmodelMap=261|true,262|true,263|true,264|true,276|true,277|t' \
+ b'rue,278|true,282|true,300|true,301|true,302|true,318|true,319|true,320|true,321|true,322|true,323|true,324|true,325|true,326|true,328|t' \
+ b'rue,329|true,330|true,331|true,332|true,333|true,338|true,339|true,340|true,341|true,342|true,343|true,344|true,345|true,346|true,347|t' \
+ b'rue,356|true,357|true,390|true\nnonDisplayableAttributeList=512\ndisplayableAttributeList=FILENAME|RCB_11.40.5_280x_5ceef00e.dlp,VERSI' \
+ b'ON|11.40.5\ndacStoreLimit=512\nfileManifest=metadata.tar|metadata|08.42.50.00.000|c04275f98fc2f07bd63126fc57cb0569|bundle|10240,084250' \
+ b'00_m3_e30_842_root.img|linux|08.42.50.00|367c5216e5c4b15b904a025bff69f039|linux|1342177280,RC_08425000_m3_e30_842_280x.img|linux_cfw|0' \
+ b'8.42.50.00|e6589b0a50b29ff34b34d3ced8ae3ccb|eos|1073741824,msw.img|sam|11.42.0000.0028|ef3ee5589ab4a019a3e6f83768364aa1|linux|41943040' \
+ b'0,iom.img|iom|11.42.0G00.0003|9bb740f8d3a4e62a0f2da2ec83c254c4|linux|8177664\nmanagementVersionList=devmgr.v1142api8.Manager\ncompatib' \
+ b'leFirmwareMap=08.30.*.*|true,08.30.*.30|false,08.30.*.31|false,08.30.*.32|false,08.30.*.33|false,08.30.*.34|false,08.30.*.35|false,08.' \
+ b'30.*.36|false,08.30.*.37|false,08.30.*.38|false,08.30.*.39|false,08.40.*.*|true,08.40.*.30|false,08.40.*.31|false,08.40.*.32|false,08.4' \
+ b'0.*.33|false,08.40.*.34|false,08.40.*.35|false,08.40.*.36|false,08.40.*.37|false,08.40.*.38|false,08.40.*.39|false,08.41.*.*|true,08.4' \
+ b'1.*.30|false,08.41.*.31|false,08.41.*.32|false,08.41.*.33|false,08.41.*.34|false,08.41.*.35|false,08.41.*.36|false,08.41.*.37|false,08' \
+ b'.41.*.38|false,08.41.*.39|false,08.42.*.*|true,08.42.*.30|false,08.42.*.31|false,08.42.*.32|false,08.42.*.33|false,08.42.*.34|false,08' \
+ b'.42.*.35|false,08.42.*.36|false,08.42.*.37|false,08.42.*.38|false,08.42.*.39|false\nversion=08.42.50.00.000\ntype=tar\nversionTag=comb' \
+ b'ined_content\n'
+
+ NVSRAM_HEADER = b'nvsram \x00\x00\x00\x01\x00\x00\x00\xa0\x00\x00\x00\x04280X\x00\x00\x00\x00\x00\x00\x00\x032801 2804 2806 \x00\x00' \
+ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1bArapaho controller, 8.52 FW\x00\x00\x001dual controller configuration, with cac' \
+ b'he battery\x07\x81A\x08Config\x00\x00\x0008.52.00.00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\xdc\xaf\x00\x00' \
+ b'\x94\xc1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00\x00 2801 2804 2806 \x00\x00\x00\x00\x00' \
+ b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
+ b'\x00\x00\x00\x00\x00\x00Board\n .Board Name = "NetApp RAID Controller"\n .NVSRAM Configuration Number' \
+ b' = "N280X-852834-D02"\n\nUserCfg\n .Enable Synchronous Negotiation = 0x00 \n'
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_is_firmware_bundled_pass(self):
+ """Determine whether firmware file is bundled."""
+ self._set_args({"firmware": "test.dlp", "nvsram": "test.dlp"})
+ with patch(builtin_path, mock_open(read_data=b"firmwarexxxxxxxx")) as mock_file:
+ firmware = NetAppESeriesFirmware()
+ self.assertEqual(firmware.is_firmware_bundled(), False)
+
+ self._set_args({"firmware": "test.dlp", "nvsram": "test.dlp"})
+ with patch(builtin_path, mock_open(read_data=self.BUNDLE_HEADER[:16])) as mock_file:
+ firmware = NetAppESeriesFirmware()
+ self.assertEqual(firmware.is_firmware_bundled(), True)
+
+ def test_is_firmware_bundles_fail(self):
+ """Verify non-firmware fails."""
+ self._set_args({"firmware": "test.dlp", "nvsram": "test.dlp"})
+ with patch(builtin_path, mock_open(read_data=b"xxxxxxxxxxxxxxxx")) as mock_file:
+ firmware = NetAppESeriesFirmware()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Firmware file is invalid."):
+ firmware.is_firmware_bundled()
+
+ def test_firmware_version(self):
+ """Verify correct firmware version is returned."""
+ self._set_args({"firmware": "test.dlp", "nvsram": "test.dlp"})
+ firmware = NetAppESeriesFirmware()
+ firmware.is_firmware_bundled = lambda: True
+ with patch(builtin_path, mock_open_with_iter(read_data=self.BUNDLE_HEADER)) as mock_file:
+ self.assertEqual(firmware.firmware_version(), b"11.40.5")
+
+ def test_nvsram_version(self):
+ """Verify correct nvsram version is returned."""
+ self._set_args({"firmware": "test.dlp", "nvsram": "test.dlp"})
+ firmware = NetAppESeriesFirmware()
+
+ with patch(builtin_path, mock_open_with_iter(read_data=self.NVSRAM_HEADER)) as mock_file:
+ self.assertEqual(firmware.nvsram_version(), b"N280X-852834-D02")
+
+ def test_check_system_health_pass(self):
+ """Validate check_system_health method."""
+ self._set_args({"firmware": "test.dlp", "nvsram": "test.dlp"})
+ firmware = NetAppESeriesFirmware()
+ with patch(self.REQUEST_FUNC, return_value=(200, {"successful": True})):
+ firmware.check_system_health()
+
+ def test_check_system_health_fail(self):
+ """Validate check_system_health method throws proper exceptions."""
+ self._set_args({"firmware": "test.dlp", "nvsram": "test.dlp"})
+ firmware = NetAppESeriesFirmware()
+ with patch(self.SLEEP_FUNC, return_value=None):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Health check failed!"):
+ with patch(self.REQUEST_FUNC, return_value=(404, Exception())):
+ firmware.check_system_health()
+
+ def test_embedded_check_nvsram_compatibility_pass(self):
+ """Verify embedded nvsram compatibility."""
+ self._set_args({"firmware": "test.dlp", "nvsram": "test.dlp"})
+ firmware = NetAppESeriesFirmware()
+ with patch(self.CREATE_MULTIPART_FORMDATA_FUNC, return_value=("", {})):
+ with patch(self.REQUEST_FUNC, return_value=(200, {"signatureTestingPassed": True,
+ "fileCompatible": True,
+ "versionContents": [{"module": "nvsram",
+ "bundledVersion": "N280X-842834-D02",
+ "onboardVersion": "N280X-842834-D02"}]})):
+ firmware.embedded_check_nvsram_compatibility()
+
+ def test_embedded_check_nvsram_compatibility_fail(self):
+ """Verify embedded nvsram compatibility fails with expected exceptions."""
+ self._set_args({"firmware": "test.dlp", "nvsram": "test.dlp"})
+ firmware = NetAppESeriesFirmware()
+
+ with patch(self.CREATE_MULTIPART_FORMDATA_FUNC, return_value=("", {})):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve NVSRAM compatibility results."):
+ with patch(self.REQUEST_FUNC, return_value=Exception()):
+ firmware.embedded_check_nvsram_compatibility()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Invalid NVSRAM file."):
+ with patch(self.REQUEST_FUNC, return_value=(200, {"signatureTestingPassed": False,
+ "fileCompatible": False,
+ "versionContents": [{"module": "nvsram",
+ "bundledVersion": "N280X-842834-D02",
+ "onboardVersion": "N280X-842834-D02"}]})):
+ firmware.embedded_check_nvsram_compatibility()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Incompatible NVSRAM file."):
+ with patch(self.REQUEST_FUNC, return_value=(200, {"signatureTestingPassed": True,
+ "fileCompatible": False,
+ "versionContents": [{"module": "nvsram",
+ "bundledVersion": "N280X-842834-D02",
+ "onboardVersion": "N280X-842834-D02"}]})):
+ firmware.embedded_check_nvsram_compatibility()
+
+ def test_embedded_check_firmware_compatibility_pass(self):
+ """Verify embedded firmware compatibility."""
+ self._set_args({"firmware": "test.dlp", "nvsram": "test.dlp"})
+ firmware = NetAppESeriesFirmware()
+
+ with patch(self.CREATE_MULTIPART_FORMDATA_FUNC, return_value=("", {})):
+ with patch(self.REQUEST_FUNC, return_value=(200, {
+ "signatureTestingPassed": True,
+ "fileCompatible": True,
+ "versionContents": [
+ {"module": "bundle", "bundledVersion": "08.42.50.00.000", "onboardVersion": "08.42.30.05"},
+ {"module": "bundleDisplay", "bundledVersion": "11.40.5", "onboardVersion": "11.40.3R2"},
+ {"module": "hypervisor", "bundledVersion": "08.42.50.00", "onboardVersion": "08.42.30.05"},
+ {"module": "raid", "bundledVersion": "08.42.50.00", "onboardVersion": "08.42.30.05"},
+ {"module": "management", "bundledVersion": "11.42.0000.0028", "onboardVersion": "11.42.0000.0026"},
+ {"module": "iom", "bundledVersion": "11.42.0G00.0003", "onboardVersion": "11.42.0G00.0001"}]})):
+ firmware.embedded_check_bundle_compatibility()
+
+ def test_embedded_check_firmware_compatibility_fail(self):
+ """Verify embedded firmware compatibility fails with expected exceptions."""
+ self._set_args({"firmware": "test.dlp", "nvsram": "test.dlp"})
+ firmware = NetAppESeriesFirmware()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve bundle compatibility results."):
+ with patch(self.CREATE_MULTIPART_FORMDATA_FUNC, return_value=("", {})):
+ with patch(self.REQUEST_FUNC, return_value=Exception()):
+ firmware.embedded_check_bundle_compatibility()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Invalid firmware bundle file."):
+ with patch(self.CREATE_MULTIPART_FORMDATA_FUNC, return_value=("", {})):
+ with patch(self.REQUEST_FUNC, return_value=(200, {
+ "signatureTestingPassed": False,
+ "fileCompatible": True,
+ "versionContents": [
+ {"module": "bundle", "bundledVersion": "08.42.50.00.000", "onboardVersion": "08.42.30.05"},
+ {"module": "bundleDisplay", "bundledVersion": "11.40.5", "onboardVersion": "11.40.3R2"},
+ {"module": "hypervisor", "bundledVersion": "08.42.50.00", "onboardVersion": "08.42.30.05"},
+ {"module": "raid", "bundledVersion": "08.42.50.00", "onboardVersion": "08.42.30.05"},
+ {"module": "management", "bundledVersion": "11.42.0000.0028", "onboardVersion": "11.42.0000.0026"},
+ {"module": "iom", "bundledVersion": "11.42.0G00.0003", "onboardVersion": "11.42.0G00.0001"}]})):
+ firmware.embedded_check_bundle_compatibility()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Incompatible firmware bundle file."):
+ with patch(self.CREATE_MULTIPART_FORMDATA_FUNC, return_value=("", {})):
+ with patch(self.REQUEST_FUNC, return_value=(200, {
+ "signatureTestingPassed": True,
+ "fileCompatible": False,
+ "versionContents": [
+ {"module": "bundle", "bundledVersion": "08.42.50.00.000", "onboardVersion": "08.42.30.05"},
+ {"module": "bundleDisplay", "bundledVersion": "11.40.5", "onboardVersion": "11.40.3R2"},
+ {"module": "hypervisor", "bundledVersion": "08.42.50.00", "onboardVersion": "08.42.30.05"},
+ {"module": "raid", "bundledVersion": "08.42.50.00", "onboardVersion": "08.42.30.05"},
+ {"module": "management", "bundledVersion": "11.42.0000.0028", "onboardVersion": "11.42.0000.0026"},
+ {"module": "iom", "bundledVersion": "11.42.0G00.0003", "onboardVersion": "11.42.0G00.0001"}]})):
+ firmware.embedded_check_bundle_compatibility()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Downgrades are not permitted."):
+ with patch(self.CREATE_MULTIPART_FORMDATA_FUNC, return_value=("", {})):
+ with patch(self.REQUEST_FUNC, return_value=(200, {
+ "signatureTestingPassed": True,
+ "fileCompatible": True,
+ "versionContents": [
+ {"module": "bundle", "bundledVersion": "08.42.00.00.000", "onboardVersion": "08.50.30.05"},
+ {"module": "bundleDisplay", "bundledVersion": "11.40.5", "onboardVersion": "11.40.3R2"},
+ {"module": "hypervisor", "bundledVersion": "08.42.50.00", "onboardVersion": "08.42.30.05"},
+ {"module": "raid", "bundledVersion": "08.42.50.00", "onboardVersion": "08.42.30.05"},
+ {"module": "management", "bundledVersion": "11.42.0000.0028", "onboardVersion": "11.42.0000.0026"},
+ {"module": "iom", "bundledVersion": "11.42.0G00.0003", "onboardVersion": "11.42.0G00.0001"}]})):
+ firmware.embedded_check_bundle_compatibility()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Downgrades are not permitted."):
+ with patch(self.CREATE_MULTIPART_FORMDATA_FUNC, return_value=("", {})):
+ with patch(self.REQUEST_FUNC, return_value=(200, {
+ "signatureTestingPassed": True,
+ "fileCompatible": True,
+ "versionContents": [
+ {"module": "bundle", "bundledVersion": "08.42.00.00.000", "onboardVersion": "09.20.30.05"},
+ {"module": "bundleDisplay", "bundledVersion": "11.40.5", "onboardVersion": "11.40.3R2"},
+ {"module": "hypervisor", "bundledVersion": "08.42.50.00", "onboardVersion": "08.42.30.05"},
+ {"module": "raid", "bundledVersion": "08.42.50.00", "onboardVersion": "08.42.30.05"},
+ {"module": "management", "bundledVersion": "11.42.0000.0028", "onboardVersion": "11.42.0000.0026"},
+ {"module": "iom", "bundledVersion": "11.42.0G00.0003", "onboardVersion": "11.42.0G00.0001"}]})):
+ firmware.embedded_check_bundle_compatibility()
+
+ def test_wait_for_web_services_pass(self):
+ """Verify controller reboot wait succeeds."""
+ self._set_args({"firmware": "test.dlp", "nvsram": "test.dlp"})
+ firmware = NetAppESeriesFirmware()
+ firmware.firmware_version = lambda: b"08.42.30.05"
+ firmware.nvsram_version = lambda: b"N280X-842834-D02"
+ firmware.is_firmware_bundled = lambda: False
+ with patch(self.SLEEP_FUNC, return_value=None):
+ with patch(self.REQUEST_FUNC, side_effect=[(200, ["08.42.30.05"]), (200, ["N280X-842834-D02"]), (200, {"status": "optimal"})]):
+ firmware.wait_for_web_services()
+
+ def test_wait_for_web_services_fail(self):
+ """Verify controller reboot wait throws expected exceptions"""
+ self._set_args({"firmware": "test.dlp", "nvsram": "test.dlp"})
+ firmware = NetAppESeriesFirmware()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Timeout waiting for Santricity Web Services."):
+ with patch(self.SLEEP_FUNC, return_value=None):
+ with patch(self.BASE_REQUEST_FUNC, return_value=Exception()):
+ firmware.wait_for_web_services()
+
+ def test_check_nvsram_compatibility_pass(self):
+ """Verify proxy nvsram compatibility."""
+ self._set_args({"firmware": "test.dlp", "nvsram": "test_nvsram.dlp"})
+ firmware = NetAppESeriesFirmware()
+ with patch(self.SLEEP_FUNC, return_value=None):
+ with patch(self.REQUEST_FUNC, side_effect=[(200, {"requestId": 1}),
+ (200, {"checkRunning": True}),
+ (200, {"checkRunning": False,
+ "results": [{"nvsramFiles": [{"filename": "test_nvsram.dlp"}]}]})]):
+ firmware.proxy_check_nvsram_compatibility()
+
+ def test_check_nvsram_compatibility_fail(self):
+ """Verify proxy nvsram compatibility throws expected exceptions."""
+ self._set_args({"firmware": "test.dlp", "nvsram": "test_nvsram.dlp"})
+ firmware = NetAppESeriesFirmware()
+ with patch(self.SLEEP_FUNC, return_value=None):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to receive NVSRAM compatibility information."):
+ with patch(self.REQUEST_FUNC, return_value=Exception()):
+ firmware.proxy_check_nvsram_compatibility()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve NVSRAM status update from proxy."):
+ with patch(self.REQUEST_FUNC, side_effect=[(200, {"requestId": 1}), Exception()]):
+ firmware.proxy_check_nvsram_compatibility()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "NVSRAM is not compatible."):
+ with patch(self.REQUEST_FUNC, side_effect=[(200, {"requestId": 1}),
+ (200, {"checkRunning": True}),
+ (200, {"checkRunning": False,
+ "results": [{"nvsramFiles": [{"filename": "not_test_nvsram.dlp"}]}]})]):
+ firmware.proxy_check_nvsram_compatibility()
+
+ def test_proxy_check_firmware_compatibility_pass(self):
+ """Verify proxy firmware compatibility."""
+ self._set_args({"firmware": "test_firmware.dlp", "nvsram": "test_nvsram.dlp"})
+ firmware = NetAppESeriesFirmware()
+ with patch(self.SLEEP_FUNC, return_value=None):
+ with patch(self.REQUEST_FUNC, side_effect=[(200, {"requestId": 1}),
+ (200, {"checkRunning": True}),
+ (200, {"checkRunning": False,
+ "results": [{"cfwFiles": [{"filename": "test_firmware.dlp"}]}]})]):
+ firmware.proxy_check_firmware_compatibility()
+
+ def test_proxy_check_firmware_compatibility_fail(self):
+ """Verify proxy firmware compatibility throws expected exceptions."""
+ self._set_args({"firmware": "test_firmware.dlp", "nvsram": "test_nvsram.dlp"})
+ firmware = NetAppESeriesFirmware()
+
+ with patch(self.SLEEP_FUNC, return_value=None):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to receive firmware compatibility information."):
+ with patch(self.REQUEST_FUNC, return_value=Exception()):
+ firmware.proxy_check_firmware_compatibility()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve firmware status update from proxy."):
+ with patch(self.REQUEST_FUNC, side_effect=[(200, {"requestId": 1}), Exception()]):
+ firmware.proxy_check_firmware_compatibility(retries=0)
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Firmware bundle is not compatible."):
+ with patch(self.REQUEST_FUNC, side_effect=[(200, {"requestId": 1}),
+ (200, {"checkRunning": True}),
+ (200, {"checkRunning": False, "results": [{"cfwFiles": [{"filename": "not_test_firmware.dlp"}]}]})]):
+ firmware.proxy_check_firmware_compatibility(retries=0)
+
+ def test_proxy_upload_and_check_compatibility_pass(self):
+ """Verify proxy_upload_and_check_compatibility"""
+ self._set_args({"firmware": "test_firmware.dlp", "nvsram": "test_nvsram.dlp"})
+ firmware = NetAppESeriesFirmware()
+ firmware.proxy_check_nvsram_compatibility = lambda: None
+ firmware.proxy_check_firmware_compatibility = lambda: None
+ with patch(self.CREATE_MULTIPART_FORMDATA_FUNC, return_value=("headers", "data")):
+ with patch(self.REQUEST_FUNC, side_effect=[(200, [{"version": "XX.XX.XX.XX", "filename": "test"},
+ {"version": "XXXXXXXXXX", "filename": "test.dlp"}]),
+ (200, None), (200, None)]):
+ firmware.proxy_upload_and_check_compatibility()
+
+ with patch(self.REQUEST_FUNC, side_effect=[(200, [{"version": "XX.XX.XX.XX", "filename": "test"},
+ {"version": "test_nvsram", "filename": "test_nvsram.dlp"},
+ {"version": "test", "filename": "test.dlp"},
+ {"filename": "test_firmware.dlp", "version": "test_firmware"}]),
+ (200, None), (200, None)]):
+ firmware.proxy_upload_and_check_compatibility()
+
+ def test_proxy_upload_and_check_compatibility_fail(self):
+ """Verify proxy_upload_and_check_compatibility throws expected exceptions."""
+ self._set_args({"firmware": "test_firmware.dlp", "nvsram": "test_nvsram.dlp"})
+ firmware = NetAppESeriesFirmware()
+ firmware.proxy_check_nvsram_compatibility = lambda: None
+ firmware.proxy_check_firmware_compatibility = lambda: None
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve existing firmware files."):
+ with patch(self.CREATE_MULTIPART_FORMDATA_FUNC, return_value=("headers", "data")):
+ with patch(self.REQUEST_FUNC, return_value=Exception()):
+ firmware.proxy_upload_and_check_compatibility()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to upload NVSRAM file."):
+ with patch(self.CREATE_MULTIPART_FORMDATA_FUNC, return_value=("headers", "data")):
+ with patch(self.REQUEST_FUNC, side_effect=[(200, [{"version": "XX.XX.XX.XX", "filename": "test"},
+ {"version": "XXXXXXXXXX", "filename": "test.dlp"},
+ {"filename": "test_firmware.dlp", "version": "test_firmware"}]),
+ Exception()]):
+ firmware.proxy_upload_and_check_compatibility()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to upload firmware bundle file."):
+ with patch(self.CREATE_MULTIPART_FORMDATA_FUNC, return_value=("headers", "data")):
+ with patch(self.REQUEST_FUNC, side_effect=[(200, [{"version": "XX.XX.XX.XX", "filename": "test"},
+ {"version": "test_nvsram", "filename": "test_nvsram.dlp"},
+ {"version": "XXXXXXXXXX", "filename": "test.dlp"}]),
+ Exception()]):
+ firmware.proxy_upload_and_check_compatibility()
+
+ def test_proxy_check_upgrade_required_pass(self):
+ """Verify proxy_check_upgrade_required."""
+ self._set_args({"firmware": "test_firmware.dlp", "nvsram": "test_nvsram.dlp"})
+ firmware = NetAppESeriesFirmware()
+ firmware.firmware_version = lambda: b"08.42.50.00"
+ firmware.nvsram_version = lambda: b"nvsram_version"
+ with patch(self.REQUEST_FUNC, side_effect=[(200, [{"versionString": "08.42.50.00"}]), (200, ["nvsram_version"])]):
+ firmware.is_firmware_bundled = lambda: True
+ firmware.proxy_check_upgrade_required()
+ self.assertFalse(firmware.upgrade_required)
+
+ with patch(self.REQUEST_FUNC, side_effect=[(200, ["08.42.50.00"]), (200, ["nvsram_version"])]):
+ firmware.is_firmware_bundled = lambda: False
+ firmware.proxy_check_upgrade_required()
+ self.assertFalse(firmware.upgrade_required)
+
+ self._set_args({"firmware": "test_firmware.dlp", "nvsram": "test_nvsram.dlp"})
+ firmware = NetAppESeriesFirmware()
+ firmware.firmware_version = lambda: b"08.42.50.00"
+ firmware.nvsram_version = lambda: b"not_nvsram_version"
+ with patch(self.REQUEST_FUNC, side_effect=[(200, [{"versionString": "08.42.50.00"}]), (200, ["nvsram_version"])]):
+ firmware.is_firmware_bundled = lambda: True
+ firmware.proxy_check_upgrade_required()
+ self.assertTrue(firmware.upgrade_required)
+
+ with patch(self.REQUEST_FUNC, side_effect=[(200, ["08.42.50.00"]), (200, ["nvsram_version"])]):
+ firmware.is_firmware_bundled = lambda: False
+ firmware.proxy_check_upgrade_required()
+ self.assertTrue(firmware.upgrade_required)
+
+ self._set_args({"firmware": "test_firmware.dlp", "nvsram": "test_nvsram.dlp"})
+ firmware = NetAppESeriesFirmware()
+ firmware.firmware_version = lambda: b"08.52.00.00"
+ firmware.nvsram_version = lambda: b"nvsram_version"
+ with patch(self.REQUEST_FUNC, side_effect=[(200, [{"versionString": "08.42.50.00"}]), (200, ["nvsram_version"])]):
+ firmware.is_firmware_bundled = lambda: True
+ firmware.proxy_check_upgrade_required()
+ self.assertTrue(firmware.upgrade_required)
+
+ with patch(self.REQUEST_FUNC, side_effect=[(200, ["08.42.50.00"]), (200, ["nvsram_version"])]):
+ firmware.is_firmware_bundled = lambda: False
+ firmware.proxy_check_upgrade_required()
+ self.assertTrue(firmware.upgrade_required)
+
+ self._set_args({"firmware": "test_firmware.dlp", "nvsram": "test_nvsram.dlp"})
+ firmware = NetAppESeriesFirmware()
+ firmware.firmware_version = lambda: b"08.52.00.00"
+ firmware.nvsram_version = lambda: b"not_nvsram_version"
+ with patch(self.REQUEST_FUNC, side_effect=[(200, [{"versionString": "08.42.50.00"}]), (200, ["nvsram_version"])]):
+ firmware.is_firmware_bundled = lambda: True
+ firmware.proxy_check_upgrade_required()
+ self.assertTrue(firmware.upgrade_required)
+
+ with patch(self.REQUEST_FUNC, side_effect=[(200, ["08.42.50.00"]), (200, ["nvsram_version"])]):
+ firmware.is_firmware_bundled = lambda: False
+ firmware.proxy_check_upgrade_required()
+ self.assertTrue(firmware.upgrade_required)
+
+ def test_proxy_check_upgrade_required_fail(self):
+ """Verify proxy_check_upgrade_required throws expected exceptions."""
+ self._set_args({"firmware": "test_firmware.dlp", "nvsram": "test_nvsram.dlp"})
+ firmware = NetAppESeriesFirmware()
+
+ firmware.firmware_version = lambda: b"08.42.50.00"
+ firmware.nvsram_version = lambda: b"not_nvsram_version"
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve controller firmware information."):
+ with patch(self.REQUEST_FUNC, return_value=Exception()):
+ firmware.proxy_check_upgrade_required()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve storage system's NVSRAM version."):
+ with patch(self.REQUEST_FUNC, side_effect=[(200, [{"versionString": "08.42.50.00"}]), Exception()]):
+ firmware.is_firmware_bundled = lambda: True
+ firmware.proxy_check_upgrade_required()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve storage system's NVSRAM version."):
+ with patch(self.REQUEST_FUNC, side_effect=[(200, ["08.42.50.00"]), Exception()]):
+ firmware.is_firmware_bundled = lambda: False
+ firmware.proxy_check_upgrade_required()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Downgrades are not permitted."):
+ with patch(self.REQUEST_FUNC, side_effect=[(200, [{"versionString": "08.42.50.00"}]), (200, ["nvsram_version"])]):
+ firmware.firmware_version = lambda: b"08.40.00.00"
+ firmware.nvsram_version = lambda: "nvsram_version"
+ firmware.is_firmware_bundled = lambda: True
+ firmware.proxy_check_upgrade_required()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Downgrades are not permitted."):
+ with patch(self.REQUEST_FUNC, side_effect=[(200, ["08.42.50.00"]), (200, ["nvsram_version"])]):
+ firmware.is_firmware_bundled = lambda: False
+ firmware.proxy_check_upgrade_required()
+
+ def test_proxy_wait_for_upgrade_pass(self):
+ """Verify proxy_wait_for_upgrade."""
+ with patch(self.SLEEP_FUNC, return_value=None):
+ self._set_args({"firmware": "test_firmware.dlp", "nvsram": "expected_nvsram.dlp"})
+ firmware = NetAppESeriesFirmware()
+
+ with patch(self.REQUEST_FUNC, side_effect=[(200, {"running": True}),
+ (200, {"running": False, "activationCompletionTime": "completion_time"})]):
+ firmware.proxy_wait_for_upgrade()
+
+ def test_proxy_wait_for_upgrade_fail(self):
+ """Verify proxy_wait_for_upgrade throws expected exceptions."""
+ with patch(self.SLEEP_FUNC, return_value=None):
+ self._set_args({"firmware": "test_firmware.dlp", "nvsram": "test_nvsram.dlp"})
+ firmware = NetAppESeriesFirmware()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to complete upgrade."):
+ with patch(self.REQUEST_FUNC, return_value=(200, {"running": False, "activationCompletionTime": None})):
+ firmware.proxy_wait_for_upgrade()
+
+ def test_proxy_upgrade_fail(self):
+ """Verify proxy_upgrade throws expected exceptions."""
+ self._set_args({"firmware": "test_firmware.dlp", "nvsram": "test_nvsram.dlp"})
+ firmware = NetAppESeriesFirmware()
+
+ firmware.is_firmware_bundled = lambda: True
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to initiate firmware upgrade."):
+ with patch(self.REQUEST_FUNC, return_value=Exception()):
+ firmware.proxy_upgrade()
+
+ firmware.is_firmware_bundled = lambda: False
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to initiate firmware upgrade."):
+ with patch(self.REQUEST_FUNC, return_value=Exception()):
+ firmware.proxy_upgrade()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_global.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_global.py
new file mode 100644
index 00000000..44ba8f4a
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_global.py
@@ -0,0 +1,494 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_global import NetAppESeriesGlobalSettings
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat.mock import patch, mock_open
+
+
+class GlobalSettingsTest(ModuleTestCase):
+ REQUIRED_PARAMS = {
+ 'api_username': 'rw',
+ 'api_password': 'password',
+ 'api_url': 'http://localhost',
+ 'ssid': '1',
+ }
+ REQ_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_global.NetAppESeriesGlobalSettings.request'
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_init_pass(self):
+ """Verify module instantiates successfully."""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 80, "default_host_type": "linux dm-mp", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 80, "default_host_type": "linux dm-mp", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 80, "default_host_type": "linux dm-mp", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+
+ def test_init_fail(self):
+ """Verify module fails when autoload is enabled but host connectivity reporting is not."""
+ self._set_args({"automatic_load_balancing": "enabled", "host_connectivity_reporting": "disabled"})
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Option automatic_load_balancing requires host_connectivity_reporting to be enabled."):
+ instance = NetAppESeriesGlobalSettings()
+
+ def test_get_current_configuration_pass(self):
+ """Ensure get_current_configuration method succeeds."""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 80, "default_host_type": "linux dm-mp", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ with patch(self.REQ_FUNC, side_effect=[(200, {"productCapabilities": [], "featureParameters": {"cacheBlockSizes": []}}), (200, []),
+ (200, [{"defaultHostTypeIndex": 28, "cache": {"cacheBlkSize": 32768, "demandFlushThreshold": 90}}]),
+ (200, {"autoLoadBalancingEnabled": True, "hostConnectivityReportingEnabled": True, "name": "array1"})]):
+ self.assertEqual(instance.get_current_configuration(), {"autoload_capable": False, "autoload_enabled": True, "cache_block_size_options": [],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 90},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {}, "name": 'array1'})
+
+ def test_get_current_configuration_fail(self):
+ """Ensure exceptions are thrown when current configuration requests fail."""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 80, "default_host_type": "linux dm-mp", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to retrieve storage array capabilities."):
+ with patch(self.REQ_FUNC, side_effect=[Exception()]):
+ instance.get_current_configuration()
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 80, "default_host_type": "linux dm-mp", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to retrieve storage array host options."):
+ with patch(self.REQ_FUNC, side_effect=[(200, {"productCapabilities": [], "featureParameters": {"cacheBlockSizes": []}}), Exception()]):
+ instance.get_current_configuration()
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 80, "default_host_type": "linux dm-mp", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to retrieve cache settings."):
+ with patch(self.REQ_FUNC, side_effect=[(200, {"productCapabilities": [], "featureParameters": {"cacheBlockSizes": []}}), (200, []), Exception()]):
+ instance.get_current_configuration()
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 80, "default_host_type": "linux dm-mp", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to determine current configuration."):
+ with patch(self.REQ_FUNC, side_effect=[(200, {"productCapabilities": [], "featureParameters": {"cacheBlockSizes": []}}), (200, []),
+ (200, [{"defaultHostTypeIndex": 28, "cache": {"cacheBlkSize": 32768, "demandFlushThreshold": 90}}]),
+ Exception()]):
+ instance.get_current_configuration()
+
+ def test_cache_block_size_pass(self):
+ """Verify cache_block_size passes successfully."""
+ self._set_args({"cache_flush_threshold": 80, "default_host_type": "linux dm-mp", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": False, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 90},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {}, "name": 'array1'}
+ self.assertFalse(instance.change_cache_block_size_required())
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 80, "default_host_type": "linux dm-mp", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": False, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 90},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {}, "name": 'array1'}
+ self.assertFalse(instance.change_cache_block_size_required())
+
+ self._set_args({"cache_block_size": 16384, "cache_flush_threshold": 80, "default_host_type": "linux dm-mp", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": False, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 90},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {}, "name": 'array1'}
+ self.assertTrue(instance.change_cache_block_size_required())
+
+ def test_cache_block_size_fail(self):
+ """Verify cache_block_size throws expected exceptions."""
+ self._set_args({"cache_block_size": 16384, "cache_flush_threshold": 80, "default_host_type": "linux dm-mp", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": False, "autoload_enabled": True, "cache_block_size_options": [32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 90},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {}, "name": 'array1'}
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Invalid cache block size."):
+ self.assertTrue(instance.change_cache_block_size_required())
+
+ def test_change_cache_flush_threshold_required_pass(self):
+ """Verify change_cache_block_size_required passes successfully."""
+ self._set_args({"cache_block_size": 32768, "default_host_type": "linux dm-mp", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": False, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {}, "name": 'array1'}
+ self.assertFalse(instance.change_cache_flush_threshold_required())
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 80, "default_host_type": "linux dm-mp", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": False, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {}, "name": 'array1'}
+ self.assertFalse(instance.change_cache_flush_threshold_required())
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "linux dm-mp", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": False, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {}, "name": 'array1'}
+ self.assertTrue(instance.change_cache_flush_threshold_required())
+
+ def test_change_cache_flush_threshold_required_fail(self):
+ """Verify change_cache_block_size_required throws expected exceptions."""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 100, "default_host_type": "linux dm-mp", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": False, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {}, "name": 'array1'}
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Invalid cache flushing threshold, it must be equal to or between 0 and 100."):
+ instance.change_cache_flush_threshold_required()
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 0, "default_host_type": "linux dm-mp", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": False, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {}, "name": 'array1'}
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Invalid cache flushing threshold, it must be equal to or between 0 and 100."):
+ instance.change_cache_flush_threshold_required()
+
+ def test_change_host_type_required_pass(self):
+ """Verify change_host_type_required passes successfully."""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": False, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ self.assertFalse(instance.change_host_type_required())
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Linux DM-MP", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": False, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ self.assertFalse(instance.change_host_type_required())
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": False, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ self.assertTrue(instance.change_host_type_required())
+
+ def test_change_host_type_required_fail(self):
+ """Verify change_host_type_required throws expected exceptions"""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "NotAHostType", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": False, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Invalid host type index!"):
+ self.assertTrue(instance.change_host_type_required())
+
+ def test_change_autoload_enabled_required_pass(self):
+ """Verify change_autoload_enabled_required passes successfully."""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ self.assertFalse(instance.change_autoload_enabled_required())
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ self.assertFalse(instance.change_autoload_enabled_required())
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ self.assertTrue(instance.change_autoload_enabled_required())
+
+ def test_change_autoload_enabled_required_fail(self):
+ """Verify change_autoload_enabled_required throws expected exceptions"""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "NotAHostType", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": False, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Automatic load balancing is not available."):
+ self.assertTrue(instance.change_autoload_enabled_required())
+
+ def test_change_host_connectivity_reporting_enabled_required_pass(self):
+ """Verify change_host_connectivity_reporting_enabled_required passes successfully."""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ self.assertFalse(instance.change_host_connectivity_reporting_enabled_required())
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "enabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ self.assertFalse(instance.change_host_connectivity_reporting_enabled_required())
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ self.assertTrue(instance.change_host_connectivity_reporting_enabled_required())
+
+ def test_change_name_required_pass(self):
+ """Verify change_name_required passes successfully."""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ self.assertFalse(instance.change_name_required())
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled", "name": "array1"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ self.assertFalse(instance.change_name_required())
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled", "name": "array2"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ self.assertTrue(instance.change_name_required())
+
+ def test_change_name_required_fail(self):
+ """Verify change_name_required throws expected exceptions"""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "NotAHostType", "automatic_load_balancing": "enabled",
+ "host_connectivity_reporting": "enabled", "name": "A" * 31})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": False, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ with self.assertRaisesRegexp(AnsibleFailJson, r"The provided name is invalid, it must be less than or equal to 30 characters in length."):
+ self.assertTrue(instance.change_name_required())
+
+ def test_update_cache_settings_pass(self):
+ """Verify update_cache_settings passes successfully."""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled", "name": "array2"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ with patch(self.REQ_FUNC, return_value=(200, None)):
+ instance.update_cache_settings()
+
+ def test_update_cache_settings_fail(self):
+ """Verify update_cache_settings throws expected exceptions"""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled", "name": "array2"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to set cache settings."):
+ with patch(self.REQ_FUNC, return_value=Exception()):
+ instance.update_cache_settings()
+
+ def test_update_host_type_pass(self):
+ """Verify update_host_type passes successfully."""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled", "name": "array2"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ with patch(self.REQ_FUNC, return_value=(200, None)):
+ instance.update_host_type()
+
+ def test_update_host_type_fail(self):
+ """Verify update_host_type throws expected exceptions"""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled", "name": "array2"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to set default host type."):
+ with patch(self.REQ_FUNC, return_value=Exception()):
+ instance.update_host_type()
+
+ def test_update_autoload_pass(self):
+ """Verify update_autoload passes successfully."""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled", "name": "array2"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ with patch(self.REQ_FUNC, return_value=(200, None)):
+ instance.update_autoload()
+
+ def test_update_autoload_fail(self):
+ """Verify update_autoload throws expected exceptions"""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled", "name": "array2"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to set automatic load balancing state."):
+ with patch(self.REQ_FUNC, return_value=Exception()):
+ instance.update_autoload()
+
+ def test_update_host_connectivity_reporting_enabled_pass(self):
+ """Verify update_host_connectivity_reporting_enabled passes successfully."""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled", "name": "array2"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ with patch(self.REQ_FUNC, return_value=(200, None)):
+ instance.update_host_connectivity_reporting_enabled()
+
+ def test_update_host_connectivity_reporting_enabled_fail(self):
+ """Verify update_host_connectivity_reporting_enabled throws expected exceptions"""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled", "name": "array2"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to enable host connectivity reporting."):
+ with patch(self.REQ_FUNC, return_value=Exception()):
+ instance.update_host_connectivity_reporting_enabled()
+
+ def test_update_name_pass(self):
+ """Verify update_name passes successfully."""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled", "name": "array2"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ with patch(self.REQ_FUNC, return_value=(200, None)):
+ instance.update_name()
+
+ def test_update_name_fail(self):
+ """Verify update_name throws expected exceptions"""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled", "name": "array2"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.get_current_configuration = lambda: {"autoload_capable": True, "autoload_enabled": True, "cache_block_size_options": [16384, 32768],
+ "cache_settings": {"cache_block_size": 32768, "cache_flush_threshold": 80},
+ "default_host_type_index": 28, "host_connectivity_reporting_enabled": True,
+ "host_type_options": {"windows": 1, "linux": 28}, "name": 'array1'}
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to set the storage array name!"):
+ with patch(self.REQ_FUNC, return_value=Exception()):
+ instance.update_name()
+
+ def test_update_pass(self):
+ """Verify update passes successfully."""
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled", "name": "array2"})
+ instance = NetAppESeriesGlobalSettings()
+
+ instance.change_autoload_enabled_required = lambda: False
+ instance.change_cache_block_size_required = lambda: False
+ instance.change_cache_flush_threshold_required = lambda: False
+ instance.change_host_type_required = lambda: False
+ instance.change_name_required = lambda: False
+ instance.change_host_connectivity_reporting_enabled_required = lambda: False
+ with self.assertRaisesRegexp(AnsibleExitJson, r"'changed': False"):
+ with patch(self.REQ_FUNC, side_effect=[(200, {"productCapabilities": [], "featureParameters": {"cacheBlockSizes": []}}), (200, []),
+ (200, [{"defaultHostTypeIndex": 28, "cache": {"cacheBlkSize": 32768, "demandFlushThreshold": 90}}]),
+ (200, {"autoLoadBalancingEnabled": True, "hostConnectivityReportingEnabled": True, "name": "array1"})] * 2):
+ instance.update()
+
+ self._set_args({"cache_block_size": 32768, "cache_flush_threshold": 90, "default_host_type": "Windows", "automatic_load_balancing": "disabled",
+ "host_connectivity_reporting": "disabled", "name": "array2"})
+ instance = NetAppESeriesGlobalSettings()
+ instance.change_autoload_enabled_required = lambda: True
+ instance.change_cache_block_size_required = lambda: False
+ instance.change_cache_flush_threshold_required = lambda: False
+ instance.change_host_type_required = lambda: False
+ instance.change_name_required = lambda: False
+ instance.change_host_connectivity_reporting_enabled_required = lambda: False
+ instance.update_autoload = lambda: None
+ with self.assertRaisesRegexp(AnsibleExitJson, r"'changed': True"):
+ with patch(self.REQ_FUNC, side_effect=[(200, {"productCapabilities": [], "featureParameters": {"cacheBlockSizes": []}}), (200, []),
+ (200, [{"defaultHostTypeIndex": 28, "cache": {"cacheBlkSize": 32768, "demandFlushThreshold": 90}}]),
+ (200, {"autoLoadBalancingEnabled": True, "hostConnectivityReportingEnabled": True, "name": "array1"})] * 2):
+ instance.update()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_host.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_host.py
new file mode 100644
index 00000000..646010ff
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_host.py
@@ -0,0 +1,434 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_host import NetAppESeriesHost
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class HostTest(ModuleTestCase):
+ REQUIRED_PARAMS = {
+ 'api_username': 'rw',
+ 'api_password': 'password',
+ 'api_url': 'http://localhost',
+ 'ssid': '1',
+ 'name': '1',
+ }
+ HOST = {
+ 'name': '1',
+ 'hostRef': '123',
+ 'label': '1',
+ 'id': '0' * 30,
+ 'clusterRef': 40 * '0',
+ 'hostTypeIndex': 28,
+ 'hostSidePorts': [],
+ 'initiators': [],
+ 'ports': [],
+ }
+ HOST_ALT = {
+ 'name': '2',
+ 'label': '2',
+ 'id': '1' * 30,
+ 'clusterRef': '1',
+ 'hostSidePorts': [],
+ 'initiators': [],
+ 'ports': [],
+ }
+ EXISTING_HOSTS = [
+ {"hostRef": "84000000600A098000A4B28D00303D065D430118", "clusterRef": "0000000000000000000000000000000000000000", "label": "Beegfs_storage1",
+ "hostTypeIndex": 28, "ports": [], "initiators": [{"initiatorRef": "89000000600A098000A4B28D00303CF55D4300E3",
+ "nodeName": {"ioInterfaceType": "iscsi",
+ "iscsiNodeName": "iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818",
+ "remoteNodeWWN": None, "nvmeNodeName": None},
+ "alias": {"ioInterfaceType": "iscsi", "iscsiAlias": ""}, "label": "beegfs_storage1_iscsi_0",
+ "hostRef": "84000000600A098000A4B28D00303D065D430118",
+ "id": "89000000600A098000A4B28D00303CF55D4300E3"}],
+ "hostSidePorts": [{"type": "iscsi", "address": "iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818", "label": "beegfs_storage1_iscsi_0"}],
+ "id": "84000000600A098000A4B28D00303D065D430118", "name": "beegfs_storage1"},
+ {"hostRef": "84000000600A098000A4B9D10030370B5D430109", "clusterRef": "0000000000000000000000000000000000000000", "label": "beegfs_metadata1",
+ "hostTypeIndex": 28, "ports": [], "initiators": [{"initiatorRef": "89000000600A098000A4B28D00303CFC5D4300F7",
+ "nodeName": {"ioInterfaceType": "iscsi",
+ "iscsiNodeName": "iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8",
+ "remoteNodeWWN": None, "nvmeNodeName": None},
+ "alias": {"ioInterfaceType": "iscsi", "iscsiAlias": ""}, "label": "beegfs_metadata1_iscsi_0",
+ "hostRef": "84000000600A098000A4B9D10030370B5D430109",
+ "id": "89000000600A098000A4B28D00303CFC5D4300F7"}],
+ "hostSidePorts": [{"type": "iscsi", "address": "iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8", "label": "beegfs_metadata1_iscsi_0"}],
+ "id": "84000000600A098000A4B9D10030370B5D430109", "name": "beegfs_metadata1"},
+ {"hostRef": "84000000600A098000A4B9D10030370B5D430109", "clusterRef": "85000000600A098000A4B9D1003637135D483DEB", "label": "beegfs_metadata2",
+ "hostTypeIndex": 28, "ports": [], "initiators": [{"initiatorRef": "89000000600A098000A4B28D00303CFC5D4300F7",
+ "nodeName": {"ioInterfaceType": "iscsi",
+ "iscsiNodeName": "iqn.used_elsewhere",
+ "remoteNodeWWN": None, "nvmeNodeName": None},
+ "alias": {"ioInterfaceType": "iscsi", "iscsiAlias": ""}, "label": "beegfs_metadata2_iscsi_0",
+ "hostRef": "84000000600A098000A4B9D10030370B5D430109",
+ "id": "89000000600A098000A4B28D00303CFC5D4300F7"}],
+ "hostSidePorts": [{"type": "iscsi", "address": "iqn.used_elsewhere", "label": "beegfs_metadata2_iscsi_0"}],
+ "id": "84000000600A098000A4B9D10030370B5D430120", "name": "beegfs_metadata2"}]
+ HOST_GROUPS = [{"clusterRef": "85000000600A098000A4B9D1003637135D483DEB", "label": "test_group", "isSAControlled": False,
+ "confirmLUNMappingCreation": False, "protectionInformationCapableAccessMethod": True, "isLun0Restricted": False,
+ "id": "85000000600A098000A4B9D1003637135D483DEB", "name": "test_group"}]
+ HOST_TYPES = [{"name": "FactoryDefault", "index": 0, "code": "FactoryDefault"},
+ {"name": "Windows 2000/Server 2003/Server 2008 Non-Clustered", "index": 1, "code": "W2KNETNCL"},
+ {"name": "Solaris", "index": 2, "code": "SOL"},
+ {"name": "Linux", "index": 6, "code": "LNX"},
+ {"name": "LnxALUA", "index": 7, "code": "LnxALUA"},
+ {"name": "Windows 2000/Server 2003/Server 2008 Clustered", "index": 8, "code": "W2KNETCL"},
+ {"name": "LnxTPGSALUA_SF", "index": 27, "code": "LnxTPGSALUA_SF"},
+ {"name": "LnxDHALUA", "index": 28, "code": "LnxDHALUA"}]
+ REQ_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_host.NetAppESeriesHost.request'
+
+ def _set_args(self, args):
+ module_args = self.REQUIRED_PARAMS.copy()
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_host_exists_pass(self):
+ """Verify host_exists produces expected results."""
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)):
+ self._set_args({'state': 'present', 'name': 'new_host', 'host_type': 'linux dm-mp', 'force_port': False,
+ 'ports': [{'label': 'new_host_port_1', 'type': 'fc', 'port': '0x08ef08ef08ef08ef'}]})
+ host = NetAppESeriesHost()
+ self.assertFalse(host.host_exists)
+
+ self._set_args({'state': 'present', 'name': 'does_not_exist', 'host_type': 'linux dm-mp',
+ 'ports': [{'label': 'beegfs_storage1_iscsi_0', 'type': 'iscsi',
+ 'port': 'iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818'}]})
+ host = NetAppESeriesHost()
+ self.assertFalse(host.host_exists)
+
+ self._set_args({'state': 'present', 'name': 'beegfs_storage1', 'host_type': 'linux dm-mp',
+ 'ports': [{'label': 'beegfs_storage1_iscsi_0', 'type': 'iscsi', 'port': 'iqn.differentiqn.org'}]})
+ host = NetAppESeriesHost()
+ self.assertTrue(host.host_exists)
+
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': True,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_0', 'type': 'iscsi',
+ 'port': 'iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818'}]})
+ host = NetAppESeriesHost()
+ self.assertTrue(host.host_exists)
+
+ def test_host_exists_fail(self):
+ """Verify host_exists produces expected exceptions."""
+ self._set_args({'state': 'present', 'host_type': 'linux dm-mp', 'ports': [{'label': 'abc', 'type': 'iscsi', 'port': 'iqn:0'}]})
+ host = NetAppESeriesHost()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to determine host existence."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ exists = host.host_exists
+
+ def test_needs_update_pass(self):
+ """Verify needs_update produces expected results."""
+ # No changes
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp',
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_0', 'type': 'iscsi',
+ 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]})
+ host = NetAppESeriesHost()
+ exists = host.host_exists
+ self.assertFalse(host.needs_update)
+
+ # Change host type
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'windows', 'force_port': False,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi', 'port': 'iqn.not_used'}]})
+ host = NetAppESeriesHost()
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+
+ # Add port to host
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi', 'port': 'iqn.not_used'}]})
+ host = NetAppESeriesHost()
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+
+ # Change port name
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_2', 'type': 'iscsi',
+ 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]})
+ host = NetAppESeriesHost()
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+
+ # take port from another host by force
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': True,
+ 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi',
+ 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]})
+ host = NetAppESeriesHost()
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+
+ def test_needs_update_fail(self):
+ """Verify needs_update produces expected exceptions."""
+ with self.assertRaisesRegexp(AnsibleFailJson, "is associated with a different host."):
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False,
+ 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi',
+ 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]})
+ host = NetAppESeriesHost()
+ exists = host.host_exists
+ host.needs_update
+
+ def test_valid_host_type_pass(self):
+ """Validate the available host types."""
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.HOST_TYPES)):
+ self._set_args({'state': 'present', 'host_type': '0'})
+ host = NetAppESeriesHost()
+ self.assertTrue(host.valid_host_type)
+ self._set_args({'state': 'present', 'host_type': '28'})
+ host = NetAppESeriesHost()
+ self.assertTrue(host.valid_host_type)
+ self._set_args({'state': 'present', 'host_type': 'windows'})
+ host = NetAppESeriesHost()
+ self.assertTrue(host.valid_host_type)
+ self._set_args({'state': 'present', 'host_type': 'linux dm-mp'})
+ host = NetAppESeriesHost()
+ self.assertTrue(host.valid_host_type)
+
+ def test_valid_host_type_fail(self):
+ """Validate the available host types."""
+ with self.assertRaisesRegexp(AnsibleFailJson, "host_type must be either a host type name or host type index found integer the documentation"):
+ self._set_args({'state': 'present', 'host_type': 'non-host-type'})
+ host = NetAppESeriesHost()
+
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.HOST_TYPES)):
+ with self.assertRaisesRegexp(AnsibleFailJson, "There is no host type with index"):
+ self._set_args({'state': 'present', 'host_type': '4'})
+ host = NetAppESeriesHost()
+ valid = host.valid_host_type
+
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to get host types."):
+ self._set_args({'state': 'present', 'host_type': '4'})
+ host = NetAppESeriesHost()
+ valid = host.valid_host_type
+
+ def test_assigned_host_ports_pass(self):
+ """Verify assigned_host_ports gives expected results."""
+
+ # Add an unused port to host
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi', 'port': 'iqn.not_used'}]})
+ host = NetAppESeriesHost()
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+ self.assertEquals(host.assigned_host_ports(), {})
+
+ # Change port name (force)
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': True,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_2', 'type': 'iscsi',
+ 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]})
+ host = NetAppESeriesHost()
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+ self.assertEquals(host.assigned_host_ports(), {'84000000600A098000A4B9D10030370B5D430109': ['89000000600A098000A4B28D00303CFC5D4300F7']})
+
+ # Change port type
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': True,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'fc', 'port': '08:ef:7e:24:52:a0'}]})
+ host = NetAppESeriesHost()
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+ self.assertEquals(host.assigned_host_ports(), {})
+
+ # take port from another host by force
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': True,
+ 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi', 'port': 'iqn.used_elsewhere'}]})
+ host = NetAppESeriesHost()
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+ self.assertEquals(host.assigned_host_ports(), {'84000000600A098000A4B9D10030370B5D430109': ['89000000600A098000A4B28D00303CFC5D4300F7']})
+
+ # take port from another host by force
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.EXISTING_HOSTS), (200, {})]):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': True,
+ 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi', 'port': 'iqn.used_elsewhere'}]})
+ host = NetAppESeriesHost()
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+ self.assertEquals(host.assigned_host_ports(apply_unassigning=True),
+ {'84000000600A098000A4B9D10030370B5D430109': ['89000000600A098000A4B28D00303CFC5D4300F7']})
+
+ def test_assigned_host_ports_fail(self):
+ """Verify assigned_host_ports gives expected exceptions."""
+ # take port from another
+ with self.assertRaisesRegexp(AnsibleFailJson, "There are no host ports available OR there are not enough unassigned host ports"):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.EXISTING_HOSTS)]):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_2', 'type': 'iscsi',
+ 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]})
+ host = NetAppESeriesHost()
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+ host.assigned_host_ports(apply_unassigning=True)
+
+ # take port from another host and fail because force == False
+ with self.assertRaisesRegexp(AnsibleFailJson, "There are no host ports available OR there are not enough unassigned host ports"):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.EXISTING_HOSTS)]):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False,
+ 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi', 'port': 'iqn.used_elsewhere'}]})
+ host = NetAppESeriesHost()
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+ host.assigned_host_ports(apply_unassigning=True)
+
+ # take port from another host and fail because force == False
+ with self.assertRaisesRegexp(AnsibleFailJson, "There are no host ports available OR there are not enough unassigned host ports"):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.EXISTING_HOSTS)]):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata3', 'host_type': 'linux dm-mp', 'force_port': False,
+ 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi', 'port': 'iqn.used_elsewhere'}]})
+ host = NetAppESeriesHost()
+ exists = host.host_exists
+ host.assigned_host_ports(apply_unassigning=True)
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to unassign host port."):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.EXISTING_HOSTS), Exception()]):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': True,
+ 'ports': [{'label': 'beegfs_metadata2_iscsi_0', 'type': 'iscsi', 'port': 'iqn.used_elsewhere'}]})
+ host = NetAppESeriesHost()
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+ host.assigned_host_ports(apply_unassigning=True)
+
+ def test_update_host_pass(self):
+ """Verify update_host produces expected results."""
+ # Change host type
+ with self.assertRaises(AnsibleExitJson):
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'windows', 'force_port': True,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi',
+ 'port': 'iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818'}]})
+ host = NetAppESeriesHost()
+ host.build_success_payload = lambda x: {}
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+ host.update_host()
+
+ # Change port iqn
+ with self.assertRaises(AnsibleExitJson):
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi', 'port': 'iqn.not_used'}]})
+ host = NetAppESeriesHost()
+ host.build_success_payload = lambda x: {}
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+ host.update_host()
+
+ # Change port type to fc
+ with self.assertRaises(AnsibleExitJson):
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'fc', 'port': '0x08ef08ef08ef08ef'}]})
+ host = NetAppESeriesHost()
+ host.build_success_payload = lambda x: {}
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+ host.update_host()
+
+ # Change port name
+ with self.assertRaises(AnsibleExitJson):
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.EXISTING_HOSTS)):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'windows', 'force_port': True,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_12', 'type': 'iscsi',
+ 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]})
+ host = NetAppESeriesHost()
+ host.build_success_payload = lambda x: {}
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+ host.update_host()
+
+ def test_update_host_fail(self):
+ """Verify update_host produces expected exceptions."""
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to update host."):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.EXISTING_HOSTS), Exception()]):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'windows', 'force_port': False,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_0', 'type': 'iscsi',
+ 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]})
+ host = NetAppESeriesHost()
+ host.build_success_payload = lambda x: {}
+ exists = host.host_exists
+ self.assertTrue(host.needs_update)
+ host.update_host()
+
+ def test_create_host_pass(self):
+ """Verify create_host produces expected results."""
+ def _assigned_host_ports(apply_unassigning=False):
+ return None
+
+ with self.assertRaises(AnsibleExitJson):
+ with mock.patch(self.REQ_FUNC, return_value=(200, {'id': '84000000600A098000A4B9D10030370B5D430109'})):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'windows', 'force_port': True,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi',
+ 'port': 'iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818'}]})
+ host = NetAppESeriesHost()
+ with mock.patch(self.REQ_FUNC, return_value=(200, [])):
+ host.assigned_host_ports = _assigned_host_ports
+ host.build_success_payload = lambda x: {}
+ host.create_host()
+
+ def test_create_host_fail(self):
+ """Verify create_host produces expected exceptions."""
+ def _assigned_host_ports(apply_unassigning=False):
+ return None
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to create host."):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, []), Exception()]):
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'windows', 'force_port': True,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi',
+ 'port': 'iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818'}]})
+ host = NetAppESeriesHost()
+ host.assigned_host_ports = _assigned_host_ports
+ host.build_success_payload = lambda x: {}
+ host.create_host()
+
+ with self.assertRaisesRegexp(AnsibleExitJson, "Host already exists."):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.EXISTING_HOSTS)]):
+ self._set_args({'state': 'present', 'name': 'beegfs_storage1', 'host_type': 'linux dm-mp', 'force_port': True,
+ 'ports': [{'label': 'beegfs_storage1_iscsi_0', 'type': 'iscsi',
+ 'port': 'iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818'}]})
+ host = NetAppESeriesHost()
+ host.assigned_host_ports = _assigned_host_ports
+ host.build_success_payload = lambda x: {}
+ host.create_host()
+
+ def test_remove_host_pass(self):
+ """Verify remove_host produces expected results."""
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ self._set_args({'state': 'absent', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_0', 'type': 'iscsi',
+ 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]})
+ host = NetAppESeriesHost()
+ host.host_obj = {"id": "84000000600A098000A4B9D10030370B5D430109"}
+ host.remove_host()
+
+ def test_remove_host_fail(self):
+ """Verify remove_host produces expected exceptions."""
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to remove host."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ self._set_args({'state': 'absent', 'name': 'beegfs_metadata1', 'host_type': 'linux dm-mp', 'force_port': False,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_0', 'type': 'iscsi',
+ 'port': 'iqn.1993-08.org.debian.beegfs-metadata:01:69e4efdf30b8'}]})
+ host = NetAppESeriesHost()
+ host.host_obj = {"id": "84000000600A098000A4B9D10030370B5D430109"}
+ host.remove_host()
+
+ def test_build_success_payload(self):
+ """Validate success payload."""
+ def _assigned_host_ports(apply_unassigning=False):
+ return None
+
+ self._set_args({'state': 'present', 'name': 'beegfs_metadata1', 'host_type': 'windows', 'force_port': True,
+ 'ports': [{'label': 'beegfs_metadata1_iscsi_1', 'type': 'iscsi', 'port': 'iqn.1993-08.org.debian.beegfs-storage1:01:b0621126818'}]})
+ host = NetAppESeriesHost()
+ self.assertEquals(host.build_success_payload(), {'api_url': 'http://localhost/', 'ssid': '1'})
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_hostgroup.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_hostgroup.py
new file mode 100644
index 00000000..6cecf0e8
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_hostgroup.py
@@ -0,0 +1,140 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_hostgroup import NetAppESeriesHostGroup
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class HostTest(ModuleTestCase):
+ REQUIRED_PARAMS = {"api_username": "rw",
+ "api_password": "password",
+ "api_url": "http://localhost",
+ "ssid": "1"}
+ REQ_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_hostgroup.NetAppESeriesHostGroup.request"
+ HOSTS_GET_RESPONSE = [
+ {"hostRef": "84000000600A098000A4B28D0030102E5C3DFC0F",
+ "clusterRef": "85000000600A098000A4B28D0036102C5C3DFC08", "id": "84000000600A098000A4B28D0030102E5C3DFC0F",
+ "name": "host1"},
+ {"hostRef": "84000000600A098000A4B28D003010315C3DFC11",
+ "clusterRef": "85000000600A098000A4B9D100360F765C3DFC1C", "id": "84000000600A098000A4B28D003010315C3DFC11",
+ "name": "host2"},
+ {"hostRef": "84000000600A098000A4B28D003010345C3DFC14",
+ "clusterRef": "85000000600A098000A4B9D100360F765C3DFC1C", "id": "84000000600A098000A4B28D003010345C3DFC14",
+ "name": "host3"}]
+ HOSTGROUPS_GET_RESPONSE = [
+ {"clusterRef": "85000000600A098000A4B28D0036102C5C3DFC08", "id": "85000000600A098000A4B28D0036102C5C3DFC08",
+ "name": "group1"},
+ {"clusterRef": "85000000600A098000A4B9D100360F765C3DFC1C", "id": "85000000600A098000A4B9D100360F765C3DFC1C",
+ "name": "group2"},
+ {"clusterRef": "85000000600A098000A4B9D100360F775C3DFC1E", "id": "85000000600A098000A4B9D100360F775C3DFC1E",
+ "name": "group3"}]
+
+ def _set_args(self, args):
+ self.module_args = self.REQUIRED_PARAMS.copy()
+ self.module_args.update(args)
+ set_module_args(self.module_args)
+
+ def test_hosts_fail(self):
+ """Ensure that the host property method fails when self.request throws an exception."""
+ self._set_args({"state": "present", "name": "hostgroup1", "hosts": ["host1", "host2"]})
+ hostgroup_object = NetAppESeriesHostGroup()
+ with self.assertRaises(AnsibleFailJson):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ hosts = hostgroup_object.hosts
+
+ self._set_args({"state": "present", "name": "hostgroup1", "hosts": ["host1", "host2"]})
+ hostgroup_object = NetAppESeriesHostGroup()
+ with mock.patch(self.REQ_FUNC, return_value=(200, [])):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Expected host does not exist"):
+ hosts = hostgroup_object.hosts
+
+ def test_hosts_pass(self):
+ """Evaluate hosts property method for valid returned data structure."""
+ expected_host_list = ['84000000600A098000A4B28D003010315C3DFC11', '84000000600A098000A4B28D0030102E5C3DFC0F']
+ for hostgroup_hosts in [["host1", "host2"], ["84000000600A098000A4B28D0030102E5C3DFC0F",
+ "84000000600A098000A4B28D003010315C3DFC11"]]:
+ self._set_args({"state": "present", "name": "hostgroup1", "hosts": hostgroup_hosts})
+ hostgroup_object = NetAppESeriesHostGroup()
+
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.HOSTS_GET_RESPONSE)):
+ for item in hostgroup_object.hosts:
+ self.assertTrue(item in expected_host_list)
+
+ # Create hostgroup with no hosts
+ self._set_args({"state": "present", "name": "hostgroup1"})
+ hostgroup_object = NetAppESeriesHostGroup()
+ with mock.patch(self.REQ_FUNC, return_value=(200, [])):
+ self.assertEqual(hostgroup_object.hosts, [])
+
+ def test_host_groups_fail(self):
+ """Ensure that the host_groups property method fails when self.request throws an exception."""
+ self._set_args({"state": "present", "name": "hostgroup1", "hosts": ["host1", "host2"]})
+ hostgroup_object = NetAppESeriesHostGroup()
+ with self.assertRaises(AnsibleFailJson):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ host_groups = hostgroup_object.host_groups
+
+ def test_host_groups_pass(self):
+ """Evaluate host_groups property method for valid return data structure."""
+ expected_groups = [
+ {'hosts': ['84000000600A098000A4B28D0030102E5C3DFC0F'], 'id': '85000000600A098000A4B28D0036102C5C3DFC08',
+ 'name': 'group1'},
+ {'hosts': ['84000000600A098000A4B28D003010315C3DFC11', '84000000600A098000A4B28D003010345C3DFC14'],
+ 'id': '85000000600A098000A4B9D100360F765C3DFC1C', 'name': 'group2'},
+ {'hosts': [], 'id': '85000000600A098000A4B9D100360F775C3DFC1E', 'name': 'group3'}]
+
+ self._set_args({"state": "present", "name": "hostgroup1", "hosts": ["host1", "host2"]})
+ hostgroup_object = NetAppESeriesHostGroup()
+
+ with mock.patch(self.REQ_FUNC,
+ side_effect=[(200, self.HOSTGROUPS_GET_RESPONSE), (200, self.HOSTS_GET_RESPONSE)]):
+ self.assertEqual(hostgroup_object.host_groups, expected_groups)
+
+ @mock.patch.object(NetAppESeriesHostGroup, "host_groups")
+ @mock.patch.object(NetAppESeriesHostGroup, "hosts")
+ @mock.patch.object(NetAppESeriesHostGroup, "create_host_group")
+ @mock.patch.object(NetAppESeriesHostGroup, "update_host_group")
+ @mock.patch.object(NetAppESeriesHostGroup, "delete_host_group")
+ def test_apply_pass(self, fake_delete_host_group, fake_update_host_group, fake_create_host_group, fake_hosts,
+ fake_host_groups):
+ """Apply desired host group state to the storage array."""
+ hosts_response = ['84000000600A098000A4B28D003010315C3DFC11', '84000000600A098000A4B28D0030102E5C3DFC0F']
+ host_groups_response = [
+ {'hosts': ['84000000600A098000A4B28D0030102E5C3DFC0F'], 'id': '85000000600A098000A4B28D0036102C5C3DFC08',
+ 'name': 'group1'},
+ {'hosts': ['84000000600A098000A4B28D003010315C3DFC11', '84000000600A098000A4B28D003010345C3DFC14'],
+ 'id': '85000000600A098000A4B9D100360F765C3DFC1C', 'name': 'group2'},
+ {'hosts': [], 'id': '85000000600A098000A4B9D100360F775C3DFC1E', 'name': 'group3'}]
+
+ fake_host_groups.return_value = host_groups_response
+ fake_hosts.return_value = hosts_response
+ fake_create_host_group.return_value = lambda x: "Host group created!"
+ fake_update_host_group.return_value = lambda x: "Host group updated!"
+ fake_delete_host_group.return_value = lambda x: "Host group deleted!"
+
+ # Test create new host group
+ self._set_args({"state": "present", "name": "hostgroup1", "hosts": ["host1", "host2"]})
+ hostgroup_object = NetAppESeriesHostGroup()
+ with self.assertRaises(AnsibleExitJson):
+ hostgroup_object.apply()
+
+ # Test make no changes to existing host group
+ self._set_args({"state": "present", "name": "group1", "hosts": ["host1"]})
+ hostgroup_object = NetAppESeriesHostGroup()
+ with self.assertRaises(AnsibleExitJson):
+ hostgroup_object.apply()
+
+ # Test add host to existing host group
+ self._set_args({"state": "present", "name": "group1", "hosts": ["host1", "host2"]})
+ hostgroup_object = NetAppESeriesHostGroup()
+ with self.assertRaises(AnsibleExitJson):
+ hostgroup_object.apply()
+
+ # Test delete existing host group
+ self._set_args({"state": "absent", "name": "group1"})
+ hostgroup_object = NetAppESeriesHostGroup()
+ with self.assertRaises(AnsibleExitJson):
+ hostgroup_object.apply()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_ib_iser_interface.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_ib_iser_interface.py
new file mode 100644
index 00000000..d2eca39f
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_ib_iser_interface.py
@@ -0,0 +1,159 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_ib_iser_interface import NetAppESeriesIbIserInterface
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class NvmeInterfaceTest(ModuleTestCase):
+ REQUIRED_PARAMS = {"api_username": "rw",
+ "api_password": "password",
+ "api_url": "http://localhost",
+ "ssid": "1",
+ "controller": "A",
+ "channel": 1}
+
+ REQ_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_ib_iser_interface.NetAppESeriesIbIserInterface.request"
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_invalid_options_fail(self):
+ """Verify invalid options fail."""
+ options_list = [{"address": "nonaddress@somewhere.com"},
+ {"address": "192.168.100.1000"},
+ {"address": "1192.168.100.100"}]
+
+ for options in options_list:
+ self._set_args(options)
+ with self.assertRaisesRegexp(AnsibleFailJson, "An invalid ip address was provided for address."):
+ iface = NetAppESeriesIbIserInterface()
+
+ def test_get_interfaces_pass(self):
+ """Verify get_interfaces method passes."""
+ self._set_args({"address": "192.168.100.100"})
+ iface = NetAppESeriesIbIserInterface()
+ with mock.patch(self.REQ_FUNC, return_value=(200, [{"interfaceType": "iscsi", "iscsi": {"interfaceData": {"type": "infiniband",
+ "infinibandData": {"isIser": True}}}},
+ {"interfaceType": "iscsi", "iscsi": {"interfaceData": {"type": "infiniband",
+ "infinibandData": {"isIser": True}}}},
+ {"interfaceType": "fc", "fc": {}}])):
+ self.assertEquals(iface.get_interfaces(),
+ [{'interfaceType': 'iscsi', 'iscsi': {'interfaceData': {'type': 'infiniband', 'infinibandData': {'isIser': True}}}},
+ {'interfaceType': 'iscsi', 'iscsi': {'interfaceData': {'type': 'infiniband', 'infinibandData': {'isIser': True}}}}])
+
+ def test_get_interfaces_fails(self):
+ """Verify get_interfaces method throws expected exceptions."""
+ self._set_args({"address": "192.168.100.100"})
+ iface = NetAppESeriesIbIserInterface()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve defined host interfaces."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ iface.get_interfaces()
+
+ self._set_args({"address": "192.168.100.100"})
+ iface = NetAppESeriesIbIserInterface()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to detect any InfiniBand iSER interfaces!"):
+ with mock.patch(self.REQ_FUNC, return_value=(200, [{"interfaceType": "eth", "eth": {"interfaceData": {"type": "ethernet",
+ "infinibandData": {"isIser": False}}}},
+ {"interfaceType": "iscsi", "iscsi": {"interfaceData": {"type": "infiniband",
+ "infinibandData": {"isIser": False}}}},
+ {"interfaceType": "fc", "fc": {}}])):
+ iface.get_interfaces()
+
+ def test_get_ib_link_status_pass(self):
+ """Verify expected data structure."""
+ self._set_args({"address": "192.168.100.100"})
+ iface = NetAppESeriesIbIserInterface()
+ with mock.patch(self.REQ_FUNC, return_value=(200, {"ibPorts": [{"channelPortRef": 1, "linkState": "active"},
+ {"channelPortRef": 2, "linkState": "down"},
+ {"channelPortRef": 3, "linkState": "down"},
+ {"channelPortRef": 4, "linkState": "active"}]})):
+ self.assertEquals(iface.get_ib_link_status(), {1: 'active', 2: 'down', 3: 'down', 4: 'active'})
+
+ def test_get_ib_link_status_fail(self):
+ """Verify expected exception is thrown."""
+ self._set_args({"address": "192.168.100.100"})
+ iface = NetAppESeriesIbIserInterface()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve ib link status information!"):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ statuses = iface.get_ib_link_status()
+
+ def test_is_change_required_pass(self):
+ """Verify is_change_required method returns expected values."""
+ self._set_args({"address": "192.168.100.100"})
+ iface = NetAppESeriesIbIserInterface()
+ iface.get_target_interface = lambda: {"iscsi": {"ipv4Data": {"ipv4AddressData": {"ipv4Address": "192.168.1.1"}}}}
+ self.assertTrue(iface.is_change_required())
+
+ self._set_args({"address": "192.168.100.100"})
+ iface = NetAppESeriesIbIserInterface()
+ iface.get_target_interface = lambda: {"iscsi": {"ipv4Data": {"ipv4AddressData": {"ipv4Address": "192.168.100.100"}}}}
+ self.assertFalse(iface.is_change_required())
+
+ def test_make_request_body_pass(self):
+ """Verify expected request body."""
+ self._set_args({"address": "192.168.100.100"})
+ iface = NetAppESeriesIbIserInterface()
+ iface.get_target_interface = lambda: {"iscsi": {"id": "1234", "ipv4Data": {"ipv4AddressData": {"ipv4Address": "192.168.1.1"}}}}
+ self.assertEquals(iface.make_request_body(), {"iscsiInterface": "1234",
+ "settings": {"tcpListenPort": [],
+ "ipv4Address": ["192.168.100.100"],
+ "ipv4SubnetMask": [],
+ "ipv4GatewayAddress": [],
+ "ipv4AddressConfigMethod": [],
+ "maximumFramePayloadSize": [],
+ "ipv4VlanId": [],
+ "ipv4OutboundPacketPriority": [],
+ "ipv4Enabled": [],
+ "ipv6Enabled": [],
+ "ipv6LocalAddresses": [],
+ "ipv6RoutableAddresses": [],
+ "ipv6PortRouterAddress": [],
+ "ipv6AddressConfigMethod": [],
+ "ipv6OutboundPacketPriority": [],
+ "ipv6VlanId": [],
+ "ipv6HopLimit": [],
+ "ipv6NdReachableTime": [],
+ "ipv6NdRetransmitTime": [],
+ "ipv6NdStaleTimeout": [],
+ "ipv6DuplicateAddressDetectionAttempts": [],
+ "maximumInterfaceSpeed": []}})
+
+ def test_update_pass(self):
+ """Verify update method behavior."""
+ self._set_args({"address": "192.168.100.100"})
+ iface = NetAppESeriesIbIserInterface()
+ iface.is_change_required = lambda: False
+ with self.assertRaisesRegexp(AnsibleExitJson, "No changes were required."):
+ iface.update()
+
+ self._set_args({"address": "192.168.100.100"})
+ iface = NetAppESeriesIbIserInterface()
+ iface.is_change_required = lambda: True
+ iface.check_mode = True
+ with self.assertRaisesRegexp(AnsibleExitJson, "No changes were required."):
+ iface.update()
+
+ self._set_args({"address": "192.168.100.100"})
+ iface = NetAppESeriesIbIserInterface()
+ iface.is_change_required = lambda: True
+ iface.make_request_body = lambda: {}
+ with self.assertRaisesRegexp(AnsibleExitJson, "The interface settings have been updated."):
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ iface.update()
+
+ def test_update_fail(self):
+ """Verify exceptions are thrown."""
+ self._set_args({"address": "192.168.100.100"})
+ iface = NetAppESeriesIbIserInterface()
+ iface.is_change_required = lambda: True
+ iface.make_request_body = lambda: {}
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to modify the interface!"):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ iface.update()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_iscsi_interface.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_iscsi_interface.py
new file mode 100644
index 00000000..de9617e6
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_iscsi_interface.py
@@ -0,0 +1,239 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_iscsi_interface import NetAppESeriesIscsiInterface
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class IscsiInterfaceTest(ModuleTestCase):
+ REQUIRED_PARAMS = {
+ 'api_username': 'rw',
+ 'api_password': 'password',
+ 'api_url': 'http://localhost',
+ 'ssid': '1',
+ 'state': 'disabled',
+ 'port': 1,
+ 'controller': 'A',
+ }
+ REQ_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_iscsi_interface.NetAppESeriesIscsiInterface.request'
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_validate_params(self):
+ """Ensure we can pass valid parameters to the module"""
+ # Provide a range of valid values for each
+ for controller in ['A', 'B']:
+ for i in range(1, 10):
+ for mtu in [1500, 2500, 9000]:
+ self._set_args(dict(
+ state='disabled',
+ port=i,
+ controller=controller,
+ mtu=mtu,
+ ))
+ iface = NetAppESeriesIscsiInterface()
+
+ def test_invalid_params(self):
+ """Ensure that our input validation catches invalid parameters"""
+
+ # Currently a 'C' controller is invalid
+ self._set_args(dict(
+ state='disabled',
+ port=1,
+ controller="C",
+ ))
+ with self.assertRaises(AnsibleFailJson) as result:
+ iface = NetAppESeriesIscsiInterface()
+
+ # Each of these mtu values are invalid
+ for mtu in [500, 1499, 9001]:
+ self._set_args({
+ 'state': 'disabled',
+ 'port': 1,
+ 'controller': 'A',
+ 'mtu': mtu
+ })
+ with self.assertRaises(AnsibleFailJson) as result:
+ iface = NetAppESeriesIscsiInterface()
+
+ def test_interfaces(self):
+ """Validate that we are processing the interface list properly"""
+ self._set_args()
+ interfaces = [{"interfaceType": "iscsi", "iscsi": {"interfaceData": {"type": "ethernet"}}},
+ {"interfaceType": "iscsi", "iscsi": {"interfaceData": {"type": "ethernet"}}},
+ {"interfaceType": "fc", "iscsi": {"interfaceData": {"type": "ethernet"}}}]
+
+ # Ensure we filter out anything without an interfaceType of iscsi
+ expected = [iface for iface in interfaces if iface['interfaceType'] == 'iscsi']
+
+ # We expect a single call to the API: retrieve the list of interfaces from the objectGraph.
+ with mock.patch(self.REQ_FUNC, return_value=(200, interfaces)):
+ iface = NetAppESeriesIscsiInterface()
+ interfaces = iface.interfaces
+ self.assertEquals(interfaces, expected)
+
+ def test_interfaces_fail(self):
+ """Ensure we fail gracefully on an error to retrieve the interfaces"""
+ self._set_args()
+
+ with self.assertRaises(AnsibleFailJson) as result:
+ # Simulate a failed call to the API
+ with mock.patch(self.REQ_FUNC, side_effect=Exception("Failure")):
+ iface = NetAppESeriesIscsiInterface()
+ interfaces = iface.interfaces
+
+ def test_get_target_interface_bad_port(self):
+ """Ensure we fail correctly when a bad port is provided"""
+ self._set_args()
+
+ interfaces = [{"iscsi": {"port": 1, "controllerId": "1"}}]
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Invalid controller.*?iSCSI port."):
+ with mock.patch.object(NetAppESeriesIscsiInterface, 'interfaces', return_value=interfaces):
+ iface = NetAppESeriesIscsiInterface()
+ interfaces = iface.get_target_interface()
+
+ def test_make_update_body_dhcp(self):
+ """Ensure the update body generates correctly for a transition from static to dhcp"""
+ self._set_args(dict(state='enabled',
+ config_method='dhcp')
+ )
+
+ iface = {"iscsi": {"id": 1,
+ "ipv4Enabled": False,
+ "ipv4Data": {"ipv4AddressData": {"ipv4Address": "0.0.0.0",
+ "ipv4SubnetMask": "0.0.0.0",
+ "ipv4GatewayAddress": "0.0.0.0"},
+ "ipv4AddressConfigMethod": "configStatic"},
+ "interfaceData": {"ethernetData": {"maximumFramePayloadSize": 1500}}}}
+
+ # Test a transition from static to dhcp
+ inst = NetAppESeriesIscsiInterface()
+ update, body = inst.make_update_body(iface)
+ self.assertTrue(update, msg="An update was expected!")
+ self.assertEquals(body['settings']['ipv4Enabled'][0], True)
+ self.assertEquals(body['settings']['ipv4AddressConfigMethod'][0], 'configDhcp')
+
+ def test_make_update_body_static(self):
+ """Ensure the update body generates correctly for a transition from dhcp to static"""
+ iface = {"iscsi": {"id": 1,
+ "ipv4Enabled": False,
+ "ipv4Data": {"ipv4AddressData": {"ipv4Address": "0.0.0.0",
+ "ipv4SubnetMask": "0.0.0.0",
+ "ipv4GatewayAddress": "0.0.0.0"},
+ "ipv4AddressConfigMethod": "configDhcp"},
+ "interfaceData": {"ethernetData": {"maximumFramePayloadSize": 1500}}}}
+
+ self._set_args(dict(state='enabled',
+ config_method='static',
+ address='10.10.10.10',
+ subnet_mask='255.255.255.0',
+ gateway='1.1.1.1'))
+
+ inst = NetAppESeriesIscsiInterface()
+ update, body = inst.make_update_body(iface)
+ self.assertTrue(update, msg="An update was expected!")
+ self.assertEquals(body['settings']['ipv4Enabled'][0], True)
+ self.assertEquals(body['settings']['ipv4AddressConfigMethod'][0], 'configStatic')
+ self.assertEquals(body['settings']['ipv4Address'][0], '10.10.10.10')
+ self.assertEquals(body['settings']['ipv4SubnetMask'][0], '255.255.255.0')
+ self.assertEquals(body['settings']['ipv4GatewayAddress'][0], '1.1.1.1')
+
+ CONTROLLERS = dict(A='1', B='2')
+
+ def test_update_bad_controller(self):
+ """Ensure a bad controller fails gracefully"""
+ self._set_args(dict(controller='B'))
+
+ inst = NetAppESeriesIscsiInterface()
+ with self.assertRaises(AnsibleFailJson) as result:
+ with mock.patch.object(inst, 'get_controllers', return_value=dict(A='1')) as get_controllers:
+ inst.update()
+
+ @mock.patch.object(NetAppESeriesIscsiInterface, 'get_controllers', return_value=CONTROLLERS)
+ def test_update(self, get_controllers):
+ """Validate the good path"""
+ self._set_args()
+
+ inst = NetAppESeriesIscsiInterface()
+ with self.assertRaises(AnsibleExitJson):
+ with mock.patch(self.REQ_FUNC, return_value=(200, "")) as request:
+ with mock.patch.object(inst, 'get_target_interface', side_effect=[{}, mock.MagicMock()]):
+ with mock.patch.object(inst, 'make_update_body', return_value=(True, {})):
+ inst.update()
+ request.assert_called_once()
+
+ @mock.patch.object(NetAppESeriesIscsiInterface, 'get_controllers', return_value=CONTROLLERS)
+ def test_update_not_required(self, get_controllers):
+ """Ensure we don't trigger the update if one isn't required or if check mode is enabled"""
+ self._set_args()
+
+ # make_update_body will report that no change is required, so we should see no call to the API.
+ inst = NetAppESeriesIscsiInterface()
+ with self.assertRaises(AnsibleExitJson) as result:
+ with mock.patch(self.REQ_FUNC, return_value=(200, "")) as request:
+ with mock.patch.object(inst, 'get_target_interface', side_effect=[{}, mock.MagicMock()]):
+ with mock.patch.object(inst, 'make_update_body', return_value=(False, {})):
+ inst.update()
+ request.assert_not_called()
+ self.assertFalse(result.exception.args[0]['changed'], msg="No change was expected.")
+
+ # Since check_mode is enabled, we will run everything normally, but not make a request to the API
+ # to perform the actual change.
+ inst = NetAppESeriesIscsiInterface()
+ inst.check_mode = True
+ with self.assertRaises(AnsibleExitJson) as result:
+ with mock.patch(self.REQ_FUNC, return_value=(200, "")) as request:
+ with mock.patch.object(inst, 'get_target_interface', side_effect=[{}, mock.MagicMock()]):
+ with mock.patch.object(inst, 'make_update_body', return_value=(True, {})):
+ inst.update()
+ request.assert_not_called()
+ self.assertTrue(result.exception.args[0]['changed'], msg="A change was expected.")
+
+ @mock.patch.object(NetAppESeriesIscsiInterface, 'get_controllers', return_value=CONTROLLERS)
+ def test_update_fail_busy(self, get_controllers):
+ """Ensure we fail correctly on receiving a busy response from the API."""
+ self._set_args()
+
+ inst = NetAppESeriesIscsiInterface()
+ with self.assertRaisesRegexp(AnsibleFailJson, r".*?busy.*") as result:
+ with mock.patch(self.REQ_FUNC, return_value=(422, dict(retcode="3"))) as request:
+ with mock.patch.object(inst, 'get_target_interface', side_effect=[{}, mock.MagicMock()]):
+ with mock.patch.object(inst, 'make_update_body', return_value=(True, {})):
+ inst.update()
+ request.assert_called_once()
+
+ @mock.patch.object(NetAppESeriesIscsiInterface, 'get_controllers', return_value=CONTROLLERS)
+ @mock.patch.object(NetAppESeriesIscsiInterface, 'make_update_body', return_value=(True, {}))
+ def test_update_fail(self, get_controllers, make_body):
+ """Ensure we fail correctly on receiving a normal failure from the API."""
+ self._set_args()
+
+ inst = NetAppESeriesIscsiInterface()
+ # Test a 422 error with a non-busy status
+ with self.assertRaisesRegexp(AnsibleFailJson, r".*?Failed to modify.*") as result:
+ with mock.patch(self.REQ_FUNC, return_value=(422, mock.MagicMock())) as request:
+ with mock.patch.object(inst, 'get_target_interface', side_effect=[{}, mock.MagicMock()]):
+ inst.update()
+ request.assert_called_once()
+
+ # Test a 401 (authentication) error
+ with self.assertRaisesRegexp(AnsibleFailJson, r".*?Failed to modify.*") as result:
+ with mock.patch(self.REQ_FUNC, return_value=(401, mock.MagicMock())) as request:
+ with mock.patch.object(inst, 'get_target_interface', side_effect=[{}, mock.MagicMock()]):
+ inst.update()
+ request.assert_called_once()
+
+ # Test with a connection failure
+ with self.assertRaisesRegexp(AnsibleFailJson, r".*?Connection failure.*") as result:
+ with mock.patch(self.REQ_FUNC, side_effect=Exception()) as request:
+ with mock.patch.object(inst, 'get_target_interface', side_effect=[{}, mock.MagicMock()]):
+ inst.update()
+ request.assert_called_once()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_iscsi_target.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_iscsi_target.py
new file mode 100644
index 00000000..93ccafe4
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_iscsi_target.py
@@ -0,0 +1,188 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_iscsi_target import NetAppESeriesIscsiTarget
+from units.modules.utils import AnsibleFailJson, AnsibleExitJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class IscsiTargetTest(ModuleTestCase):
+ REQUIRED_PARAMS = {"api_username": "admin", "api_password": "adminpassword", "api_url": "http://localhost", "ssid": "1", "name": "abc"}
+ CHAP_SAMPLE = "a" * 14
+ REQ_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_iscsi_target.NetAppESeriesIscsiTarget.request"
+ TARGET_REQUEST_RESPONSE = [{"targetRef": "90000000600A098000A4B28D00334A065DA9D747",
+ "nodeName": {"ioInterfaceType": "iscsi",
+ "iscsiNodeName": "iqn.1992-08.com.netapp:2806.600a098000a4b28d000000005da9d744",
+ "remoteNodeWWN": None, "nvmeNodeName": None},
+ "alias": {"ioInterfaceType": "iscsi",
+ "iscsiAlias": "target_name"},
+ "configuredAuthMethods": {"authMethodData": [{"authMethod": "none",
+ "chapSecret": None}]},
+ "portals": [{"groupTag": 2,
+ "ipAddress": {"addressType": "ipv4",
+ "ipv4Address": "10.10.10.110",
+ "ipv6Address": None},
+ "tcpListenPort": 3260},
+ {"groupTag": 2,
+ "ipAddress": {"addressType": "ipv6",
+ "ipv4Address": None,
+ "ipv6Address": "FE8000000000000002A098FFFEA4B9D7"},
+ "tcpListenPort": 3260},
+ {"groupTag": 2,
+ "ipAddress": {"addressType": "ipv4",
+ "ipv4Address": "10.10.10.112",
+ "ipv6Address": None},
+ "tcpListenPort": 3260},
+ {"groupTag": 1, "ipAddress": {"addressType": "ipv4",
+ "ipv4Address": "10.10.11.110",
+ "ipv6Address": None},
+ "tcpListenPort": 3260},
+ {"groupTag": 1,
+ "ipAddress": {"addressType": "ipv6",
+ "ipv4Address": None,
+ "ipv6Address": "FE8000000000000002A098FFFEA4B293"},
+ "tcpListenPort": 3260},
+ {"groupTag": 1,
+ "ipAddress": {"addressType": "ipv4",
+ "ipv4Address": "10.10.11.112",
+ "ipv6Address": None},
+ "tcpListenPort": 3260}]}]
+ ISCSI_ENTRY_DATA_RESPONSE = [{"icmpPingResponseEnabled": False,
+ "unnamedDiscoverySessionsEnabled": False,
+ "isnsServerTcpListenPort": 0,
+ "ipv4IsnsServerAddressConfigMethod": "configDhcp",
+ "ipv4IsnsServerAddress": "0.0.0.0",
+ "ipv6IsnsServerAddressConfigMethod": "configStatic",
+ "ipv6IsnsServerAddress": "00000000000000000000000000000000",
+ "isnsRegistrationState": "__UNDEFINED",
+ "isnsServerRegistrationEnabled": False,
+ "hostPortsConfiguredDHCP": False}]
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_validate_params(self):
+ """Ensure we can pass valid parameters to the module"""
+ for i in range(12, 57):
+ secret = 'a' * i
+ self._set_args(dict(chap=secret))
+ tgt = NetAppESeriesIscsiTarget()
+
+ def test_invalid_chap_secret(self):
+ for secret in [11 * 'a', 58 * 'a']:
+ with self.assertRaisesRegexp(AnsibleFailJson, r'.*?CHAP secret is not valid.*') as result:
+ self._set_args(dict(chap=secret))
+ tgt = NetAppESeriesIscsiTarget()
+
+ def test_target_pass(self):
+ """Ensure target property returns the expected data structure."""
+ expected_response = {"alias": "target_name", "chap": False, "iqn": "iqn.1992-08.com.netapp:2806.600a098000a4b28d000000005da9d744",
+ "ping": False, "unnamed_discovery": False}
+
+ self._set_args({"name": "target_name", "ping": True, "unnamed_discovery": True})
+ iscsi_target = NetAppESeriesIscsiTarget()
+
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.TARGET_REQUEST_RESPONSE), (200, self.ISCSI_ENTRY_DATA_RESPONSE)]):
+ self.assertEquals(iscsi_target.target, expected_response)
+
+ def test_target_fail(self):
+ """Ensure target property returns the expected data structure."""
+ self._set_args({"name": "target_name", "ping": True, "unnamed_discovery": True})
+ iscsi_target = NetAppESeriesIscsiTarget()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to retrieve the iSCSI target information."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ result = iscsi_target.target
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to retrieve the iSCSI target information."):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.TARGET_REQUEST_RESPONSE), Exception()]):
+ result = iscsi_target.target
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"This storage-system does not appear to have iSCSI interfaces."):
+ with mock.patch(self.REQ_FUNC, return_value=(200, [])):
+ result = iscsi_target.target
+
+ def test_apply_iscsi_settings_pass(self):
+ """Ensure apply_iscsi_settings succeeds properly."""
+ self._set_args({"name": "not_target_name"})
+ iscsi_target = NetAppESeriesIscsiTarget()
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.TARGET_REQUEST_RESPONSE), (200, self.ISCSI_ENTRY_DATA_RESPONSE), (200, [])]):
+ self.assertTrue(iscsi_target.apply_iscsi_settings())
+
+ self._set_args({"name": "target_name"})
+ iscsi_target = NetAppESeriesIscsiTarget()
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.TARGET_REQUEST_RESPONSE), (200, self.ISCSI_ENTRY_DATA_RESPONSE), (200, [])]):
+ self.assertFalse(iscsi_target.apply_iscsi_settings())
+
+ def test_apply_iscsi_settings_fail(self):
+ """Ensure apply_iscsi_settings fails properly."""
+ self._set_args({"name": "not_target_name"})
+ iscsi_target = NetAppESeriesIscsiTarget()
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to update the iSCSI target settings."):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.TARGET_REQUEST_RESPONSE), (200, self.ISCSI_ENTRY_DATA_RESPONSE), Exception()]):
+ self.assertTrue(iscsi_target.apply_iscsi_settings())
+
+ def test_apply_target_changes_pass(self):
+ """Ensure apply_iscsi_settings succeeds properly."""
+ self._set_args({"name": "target_name", "ping": True, "unnamed_discovery": True})
+ iscsi_target = NetAppESeriesIscsiTarget()
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.TARGET_REQUEST_RESPONSE), (200, self.ISCSI_ENTRY_DATA_RESPONSE), (200, [])]):
+ self.assertTrue(iscsi_target.apply_target_changes())
+
+ self._set_args({"name": "target_name", "ping": False, "unnamed_discovery": True})
+ iscsi_target = NetAppESeriesIscsiTarget()
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.TARGET_REQUEST_RESPONSE), (200, self.ISCSI_ENTRY_DATA_RESPONSE), (200, [])]):
+ self.assertTrue(iscsi_target.apply_target_changes())
+
+ self._set_args({"name": "target_name", "ping": True, "unnamed_discovery": False})
+ iscsi_target = NetAppESeriesIscsiTarget()
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.TARGET_REQUEST_RESPONSE), (200, self.ISCSI_ENTRY_DATA_RESPONSE), (200, [])]):
+ self.assertTrue(iscsi_target.apply_target_changes())
+
+ self._set_args({"name": "target_name", "ping": False, "unnamed_discovery": False})
+ iscsi_target = NetAppESeriesIscsiTarget()
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.TARGET_REQUEST_RESPONSE), (200, self.ISCSI_ENTRY_DATA_RESPONSE), (200, [])]):
+ self.assertFalse(iscsi_target.apply_target_changes())
+
+ def test_apply_target_changes_fail(self):
+ """Ensure apply_iscsi_settings fails properly."""
+ self._set_args({"name": "target_name", "ping": True, "unnamed_discovery": True})
+ iscsi_target = NetAppESeriesIscsiTarget()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to update the iSCSI target settings."):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.TARGET_REQUEST_RESPONSE), (200, self.ISCSI_ENTRY_DATA_RESPONSE), Exception()]):
+ iscsi_target.apply_target_changes()
+
+ def test_update_pass(self):
+ """Ensure update successfully exists."""
+ self._set_args({"name": "target_name", "ping": True, "unnamed_discovery": True})
+ iscsi_target = NetAppESeriesIscsiTarget()
+
+ iscsi_target.apply_iscsi_settings = lambda: True
+ iscsi_target.apply_target_changes = lambda: True
+ with self.assertRaisesRegexp(AnsibleExitJson, r"\'changed\': True"):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.TARGET_REQUEST_RESPONSE), (200, self.ISCSI_ENTRY_DATA_RESPONSE)]):
+ iscsi_target.update()
+
+ iscsi_target.apply_iscsi_settings = lambda: False
+ iscsi_target.apply_target_changes = lambda: True
+ with self.assertRaisesRegexp(AnsibleExitJson, r"\'changed\': True"):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.TARGET_REQUEST_RESPONSE), (200, self.ISCSI_ENTRY_DATA_RESPONSE)]):
+ iscsi_target.update()
+
+ iscsi_target.apply_iscsi_settings = lambda: True
+ iscsi_target.apply_target_changes = lambda: False
+ with self.assertRaisesRegexp(AnsibleExitJson, r"\'changed\': True"):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.TARGET_REQUEST_RESPONSE), (200, self.ISCSI_ENTRY_DATA_RESPONSE)]):
+ iscsi_target.update()
+
+ iscsi_target.apply_iscsi_settings = lambda: False
+ iscsi_target.apply_target_changes = lambda: False
+ with self.assertRaisesRegexp(AnsibleExitJson, r"\'changed\': False"):
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, self.TARGET_REQUEST_RESPONSE), (200, self.ISCSI_ENTRY_DATA_RESPONSE)]):
+ iscsi_target.update()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_ldap.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_ldap.py
new file mode 100644
index 00000000..69bf2674
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_ldap.py
@@ -0,0 +1,371 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_ldap import NetAppESeriesLdap
+from units.modules.utils import ModuleTestCase, set_module_args, AnsibleFailJson, AnsibleExitJson
+from units.compat import mock
+
+
+class LdapTest(ModuleTestCase):
+ REQUIRED_PARAMS = {
+ "api_username": "admin",
+ "api_password": "password",
+ "api_url": "http://localhost",
+ "ssid": "1"}
+ REQ_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_ldap.NetAppESeriesLdap.request"
+ BASE_REQ_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity.request"
+
+ GET_DOMAINS = {"version": "3",
+ "ldapDomains": [{"id": "test1",
+ "bindLookupUser": {"password": "***", "user": "CN=cn,OU=accounts,DC=test1,DC=example,DC=com"},
+ "groupAttributes": ["memberOf"],
+ "ldapUrl": "ldap://test.example.com:389",
+ "names": ["test.example.com"],
+ "roleMapCollection": [{"groupRegex": ".*", "ignoreCase": False, "name": "storage.monitor"}],
+ "searchBase": "OU=accounts,DC=test,DC=example,DC=com",
+ "userAttribute": "sAMAccountName"},
+ {"id": "test2",
+ "bindLookupUser": {"password": "***", "user": "CN=cn,OU=accounts,DC=test2,DC=example,DC=com"},
+ "groupAttributes": ["memberOf"],
+ "ldapUrl": "ldap://test2.example.com:389",
+ "names": ["test2.example.com"],
+ "roleMapCollection": [{"groupRegex": ".*", "ignoreCase": False, "name": "storage.admin"},
+ {"groupRegex": ".*", "ignoreCase": False, "name": "support.admin"},
+ {"groupRegex": ".*", "ignoreCase": False, "name": "security.admin"},
+ {"groupRegex": ".*", "ignoreCase": False, "name": "storage.monitor"}],
+ "searchBase": "OU=accounts,DC=test2,DC=example,DC=com",
+ "userAttribute": "sAMAccountName"}]}
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_valid_options_pass(self):
+ """Verify valid options."""
+ options_list = [{"state": "disabled"},
+ {"state": "absent", "identifier": "test_domain"},
+ {"state": "present", "identifier": "test_domain", "server_url": "ldap://test.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com"},
+ {"state": "present", "identifier": "test_domain", "server_url": "ldap://test.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com", "bind_user": "admin", "bind_password": "adminpass"},
+ {"state": "present", "identifier": "test_domain", "server_url": "ldap://test.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com", "bind_user": "admin", "bind_password": "adminpass",
+ "names": ["name1", "name2"], "group_attributes": ["group_attr1", "group_attr1"], "user_attribute": "user_attr"}]
+
+ for options in options_list:
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ self._set_args(options)
+ ldap = NetAppESeriesLdap()
+ for options in options_list:
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": False})]):
+ self._set_args(options)
+ ldap = NetAppESeriesLdap()
+
+ def test_get_domain_pass(self):
+ """Verify get_domain returns expected data structure."""
+ options = {"state": "present", "identifier": "test_domain", "server_url": "ldap://test.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com", "bind_user": "admin", "bind_password": "adminpass",
+ "names": ["name1", "name2"], "group_attributes": ["group_attr1", "group_attr1"], "user_attribute": "user_attr"}
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.GET_DOMAINS)):
+ self._set_args(options)
+ ldap = NetAppESeriesLdap()
+ self.assertEquals(ldap.get_domains(), self.GET_DOMAINS["ldapDomains"])
+
+ def test_get_domain_fail(self):
+ """Verify get_domain throws expected exceptions."""
+ options = {"state": "present", "identifier": "test_domain", "server_url": "ldap://test.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com", "bind_user": "admin", "bind_password": "adminpass",
+ "names": ["name1", "name2"], "group_attributes": ["group_attr1", "group_attr1"], "user_attribute": "user_attr"}
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve current LDAP configuration."):
+ self._set_args(options)
+ ldap = NetAppESeriesLdap()
+ ldap.get_domains()
+
+ def test_build_request_body_pass(self):
+ """Verify build_request_body builds expected data structure."""
+ options_list = [{"state": "present", "identifier": "test_domain", "server_url": "ldap://test.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com"},
+ {"state": "present", "identifier": "test_domain", "server_url": "ldap://test.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com", "bind_user": "admin", "bind_password": "adminpass"},
+ {"state": "present", "identifier": "test_domain", "server_url": "ldap://test.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com", "bind_user": "admin", "bind_password": "adminpass",
+ "names": ["name1", "name2"], "group_attributes": ["group_attr1", "group_attr1"], "user_attribute": "user_attr"}]
+ expectation_list = [{'id': 'test_domain', 'groupAttributes': ['memberOf'], 'ldapUrl': 'ldap://test.example.com:389', 'names': ['test.example.com'],
+ 'roleMapCollection': [], 'searchBase': 'ou=accounts,DC=test,DC=example,DC=com', 'userAttribute': 'sAMAccountName'},
+ {'id': 'test_domain', 'groupAttributes': ['memberOf'], 'ldapUrl': 'ldap://test.example.com:389', 'names': ['test.example.com'],
+ 'roleMapCollection': [], 'searchBase': 'ou=accounts,DC=test,DC=example,DC=com', 'userAttribute': 'sAMAccountName',
+ 'bindLookupUser': {'password': 'adminpass', 'user': 'admin'}},
+ {'id': 'test_domain', 'groupAttributes': ['group_attr1', 'group_attr1'], 'ldapUrl': 'ldap://test.example.com:389',
+ 'names': ['name1', 'name2'], 'roleMapCollection': [], 'searchBase': 'ou=accounts,DC=test,DC=example,DC=com',
+ 'userAttribute': 'user_attr', 'bindLookupUser': {'password': 'adminpass', 'user': 'admin'}}]
+ for index in range(len(options_list)):
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ self._set_args(options_list[index])
+ ldap = NetAppESeriesLdap()
+ ldap.build_request_body()
+ self.assertEquals(ldap.body, expectation_list[index])
+
+ def test_are_changes_required_pass(self):
+ """Verify build_request_body builds expected data structure."""
+ options_list = [{"state": "present", "identifier": "test_domain", "server_url": "ldap://test.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com"},
+ {"state": "present", "identifier": "test_domain", "server_url": "ldap://test.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com", "bind_user": "admin", "bind_password": "adminpass"},
+ {"state": "present", "identifier": "test_domain", "server_url": "ldap://test.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com", "bind_user": "admin", "bind_password": "adminpass",
+ "names": ["name1", "name2"], "group_attributes": ["group_attr1", "group_attr1"], "user_attribute": "user_attr"}]
+
+ for index in range(len(options_list)):
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ self._set_args(options_list[index])
+ ldap = NetAppESeriesLdap()
+ ldap.get_domains = lambda: self.GET_DOMAINS["ldapDomains"]
+ self.assertTrue(ldap.are_changes_required())
+
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ self._set_args({"state": "disabled"})
+ ldap = NetAppESeriesLdap()
+ ldap.get_domains = lambda: self.GET_DOMAINS["ldapDomains"]
+ self.assertTrue(ldap.are_changes_required())
+ self.assertEquals(ldap.existing_domain_ids, ["test1", "test2"])
+
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ self._set_args({"state": "absent", "identifier": "test_domain"})
+ ldap = NetAppESeriesLdap()
+ ldap.get_domains = lambda: self.GET_DOMAINS["ldapDomains"]
+ self.assertFalse(ldap.are_changes_required())
+
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ self._set_args({"state": "present", "identifier": "test2", "server_url": "ldap://test2.example.com:389",
+ "search_base": "ou=accounts,DC=test2,DC=example,DC=com",
+ "bind_user": "CN=cn,OU=accounts,DC=test2,DC=example,DC=com", "bind_password": "adminpass",
+ "role_mappings": {".*": ["storage.admin", "support.admin", "security.admin", "storage.monitor"]},
+ "names": ["test2.example.com"], "group_attributes": ["memberOf"], "user_attribute": "sAMAccountName"})
+ ldap = NetAppESeriesLdap()
+ ldap.build_request_body()
+ ldap.get_domains = lambda: self.GET_DOMAINS["ldapDomains"]
+ ldap.add_domain = lambda temporary, skip_test: {"id": "ANSIBLE_TMP_DOMAIN"}
+
+ with mock.patch(self.REQ_FUNC, return_value=(200, [{"id": "test2", "result": {"authenticationTestResult": "ok"}},
+ {"id": "ANSIBLE_TMP_DOMAIN", "result": {"authenticationTestResult": "ok"}}])):
+ self.assertFalse(ldap.are_changes_required())
+
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ self._set_args({"state": "present", "identifier": "test2", "server_url": "ldap://test2.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com",
+ "bind_user": "CN=cn,OU=accounts,DC=test2,DC=example,DC=com", "bind_password": "adminpass",
+ "role_mappings": {".*": ["storage.admin", "support.admin", "security.admin", "storage.monitor"]},
+ "names": ["test2.example.com"], "group_attributes": ["memberOf"], "user_attribute": "sAMAccountName"})
+ ldap = NetAppESeriesLdap()
+ ldap.build_request_body()
+ ldap.get_domains = lambda: self.GET_DOMAINS["ldapDomains"]
+ ldap.add_domain = lambda temporary, skip_test: {"id": "ANSIBLE_TMP_DOMAIN"}
+
+ with mock.patch(self.REQ_FUNC, return_value=(200, [{"id": "test2", "result": {"authenticationTestResult": "fail"}},
+ {"id": "ANSIBLE_TMP_DOMAIN", "result": {"authenticationTestResult": "ok"}}])):
+ self.assertTrue(ldap.are_changes_required())
+
+ def test_are_changes_required_fail(self):
+ """Verify are_changes_required throws expected exception."""
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ self._set_args({"state": "present", "identifier": "test2", "server_url": "ldap://test2.example.com:389",
+ "search_base": "ou=accounts,DC=test2,DC=example,DC=com",
+ "bind_user": "CN=cn,OU=accounts,DC=test2,DC=example,DC=com", "bind_password": "adminpass",
+ "role_mappings": {".*": ["storage.admin", "support.admin", "security.admin", "storage.monitor"]},
+ "names": ["test2.example.com"], "group_attributes": ["memberOf"], "user_attribute": "sAMAccountName"})
+ ldap = NetAppESeriesLdap()
+ ldap.build_request_body()
+ ldap.get_domains = lambda: self.GET_DOMAINS["ldapDomains"]
+ ldap.add_domain = lambda temporary, skip_test: {"id": "ANSIBLE_TMP_DOMAIN"}
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to authenticate bind credentials!"):
+ with mock.patch(self.REQ_FUNC, return_value=(200, [{"id": "test2", "result": {"authenticationTestResult": "fail"}},
+ {"id": "ANSIBLE_TMP_DOMAIN", "result": {"authenticationTestResult": "fail"}}])):
+ ldap.are_changes_required()
+
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ self._set_args({"state": "present", "identifier": "test2", "server_url": "ldap://test2.example.com:389",
+ "search_base": "ou=accounts,DC=test2,DC=example,DC=com",
+ "bind_user": "CN=cn,OU=accounts,DC=test2,DC=example,DC=com", "bind_password": "adminpass",
+ "role_mappings": {".*": ["storage.admin", "support.admin", "security.admin", "storage.monitor"]},
+ "names": ["test2.example.com"], "group_attributes": ["memberOf"], "user_attribute": "sAMAccountName"})
+ ldap = NetAppESeriesLdap()
+ ldap.build_request_body()
+ ldap.get_domains = lambda: self.GET_DOMAINS["ldapDomains"]
+ ldap.add_domain = lambda temporary, skip_test: {"id": "ANSIBLE_TMP_DOMAIN"}
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to authenticate bind credentials!"):
+ with mock.patch(self.REQ_FUNC, return_value=(200, [{"id": "test2", "result": {"authenticationTestResult": "ok"}},
+ {"id": "ANSIBLE_TMP_DOMAIN", "result": {"authenticationTestResult": "fail"}}])):
+ ldap.are_changes_required()
+
+ def test_add_domain_pass(self):
+ """Verify add_domain returns expected data."""
+ self._set_args({"state": "present", "identifier": "test2", "server_url": "ldap://test2.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com",
+ "bind_user": "CN=cn,OU=accounts,DC=test2,DC=example,DC=com", "bind_password": "adminpass",
+ "role_mappings": {".*": ["storage.admin", "support.admin", "security.admin", "storage.monitor"]},
+ "names": ["test2.example.com"], "group_attributes": ["memberOf"], "user_attribute": "sAMAccountName"})
+
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ ldap = NetAppESeriesLdap()
+ ldap.build_request_body()
+ with mock.patch(self.REQ_FUNC, return_value=(200, {"ldapDomains": [{"id": "test2"}]})):
+ self.assertEquals(ldap.add_domain(), {"id": "test2"})
+
+ def test_add_domain_fail(self):
+ """Verify add_domain returns expected data."""
+ self._set_args({"state": "present", "identifier": "test2", "server_url": "ldap://test2.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com",
+ "bind_user": "CN=cn,OU=accounts,DC=test2,DC=example,DC=com", "bind_password": "adminpass",
+ "role_mappings": {".*": ["storage.admin", "support.admin", "security.admin", "storage.monitor"]},
+ "names": ["test2.example.com"], "group_attributes": ["memberOf"], "user_attribute": "sAMAccountName"})
+
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ ldap = NetAppESeriesLdap()
+ ldap.build_request_body()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to create LDAP domain."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ ldap.add_domain()
+
+ def test_update_domain_pass(self):
+ """Verify update_domain returns expected data."""
+ self._set_args({"state": "present", "identifier": "test2", "server_url": "ldap://test2.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com",
+ "bind_user": "CN=cn,OU=accounts,DC=test2,DC=example,DC=com", "bind_password": "adminpass",
+ "role_mappings": {".*": ["storage.admin", "support.admin", "security.admin", "storage.monitor"]},
+ "names": ["test2.example.com"], "group_attributes": ["memberOf"], "user_attribute": "sAMAccountName"})
+
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ ldap = NetAppESeriesLdap()
+ ldap.build_request_body()
+ ldap.domain = {"id": "test2"}
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ ldap.update_domain()
+
+ def test_update_domain_fail(self):
+ """Verify update_domain returns expected data."""
+ self._set_args({"state": "present", "identifier": "test2", "server_url": "ldap://test2.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com",
+ "bind_user": "CN=cn,OU=accounts,DC=test2,DC=example,DC=com", "bind_password": "adminpass",
+ "role_mappings": {".*": ["storage.admin", "support.admin", "security.admin", "storage.monitor"]},
+ "names": ["test2.example.com"], "group_attributes": ["memberOf"], "user_attribute": "sAMAccountName"})
+
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ ldap = NetAppESeriesLdap()
+ ldap.build_request_body()
+ ldap.domain = {"id": "test2"}
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to update LDAP domain."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ ldap.update_domain()
+
+ def test_delete_domain_pass(self):
+ """Verify delete_domain returns expected data."""
+ self._set_args({"state": "present", "identifier": "test2", "server_url": "ldap://test2.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com",
+ "bind_user": "CN=cn,OU=accounts,DC=test2,DC=example,DC=com", "bind_password": "adminpass",
+ "role_mappings": {".*": ["storage.admin", "support.admin", "security.admin", "storage.monitor"]},
+ "names": ["test2.example.com"], "group_attributes": ["memberOf"], "user_attribute": "sAMAccountName"})
+
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ ldap = NetAppESeriesLdap()
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ ldap.delete_domain("test2")
+
+ def test_delete_domain_fail(self):
+ """Verify delete_domain returns expected data."""
+ self._set_args({"state": "present", "identifier": "test2", "server_url": "ldap://test2.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com",
+ "bind_user": "CN=cn,OU=accounts,DC=test2,DC=example,DC=com", "bind_password": "adminpass",
+ "role_mappings": {".*": ["storage.admin", "support.admin", "security.admin", "storage.monitor"]},
+ "names": ["test2.example.com"], "group_attributes": ["memberOf"], "user_attribute": "sAMAccountName"})
+
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ ldap = NetAppESeriesLdap()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to delete LDAP domain."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ ldap.delete_domain("test2")
+
+ def test_disable_domains_pass(self):
+ """Verify disable_domains completes successfully."""
+ self._set_args({"state": "present", "identifier": "test2", "server_url": "ldap://test2.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com",
+ "bind_user": "CN=cn,OU=accounts,DC=test2,DC=example,DC=com", "bind_password": "adminpass",
+ "role_mappings": {".*": ["storage.admin", "support.admin", "security.admin", "storage.monitor"]},
+ "names": ["test2.example.com"], "group_attributes": ["memberOf"], "user_attribute": "sAMAccountName"})
+
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ ldap = NetAppESeriesLdap()
+ ldap.delete_domain = lambda x: None
+ ldap.existing_domain_ids = ["id1", "id2", "id3"]
+ ldap.disable_domains()
+
+ def test_apply_pass(self):
+ """Verify apply exits as expected."""
+ self._set_args({"state": "present", "identifier": "test2", "server_url": "ldap://test2.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com",
+ "bind_user": "CN=cn,OU=accounts,DC=test2,DC=example,DC=com", "bind_password": "adminpass",
+ "role_mappings": {".*": ["storage.admin", "support.admin", "security.admin", "storage.monitor"]},
+ "names": ["test2.example.com"], "group_attributes": ["memberOf"], "user_attribute": "sAMAccountName"})
+
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ ldap = NetAppESeriesLdap()
+ ldap.build_request_body = lambda: None
+ ldap.are_changes_required = lambda: False
+ with self.assertRaisesRegexp(AnsibleExitJson, "No changes have been made to the LDAP configuration."):
+ ldap.apply()
+
+ self._set_args({"state": "present", "identifier": "test2", "server_url": "ldap://test2.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com",
+ "bind_user": "CN=cn,OU=accounts,DC=test2,DC=example,DC=com", "bind_password": "adminpass",
+ "role_mappings": {".*": ["storage.admin", "support.admin", "security.admin", "storage.monitor"]},
+ "names": ["test2.example.com"], "group_attributes": ["memberOf"], "user_attribute": "sAMAccountName"})
+
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ ldap = NetAppESeriesLdap()
+ ldap.build_request_body = lambda: None
+ ldap.are_changes_required = lambda: True
+ ldap.add_domain = lambda: None
+ ldap.domain = {}
+ with self.assertRaisesRegexp(AnsibleExitJson, "LDAP domain has been added."):
+ ldap.apply()
+
+ self._set_args({"state": "present", "identifier": "test2", "server_url": "ldap://test2.example.com:389",
+ "search_base": "ou=accounts,DC=test,DC=example,DC=com",
+ "bind_user": "CN=cn,OU=accounts,DC=test2,DC=example,DC=com", "bind_password": "adminpass",
+ "role_mappings": {".*": ["storage.admin", "support.admin", "security.admin", "storage.monitor"]},
+ "names": ["test2.example.com"], "group_attributes": ["memberOf"], "user_attribute": "sAMAccountName"})
+
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ ldap = NetAppESeriesLdap()
+ ldap.build_request_body = lambda: None
+ ldap.are_changes_required = lambda: True
+ ldap.update_domain = lambda: None
+ ldap.domain = {"id": "test"}
+ with self.assertRaisesRegexp(AnsibleExitJson, "LDAP domain has been updated."):
+ ldap.apply()
+
+ self._set_args({"state": "absent", "identifier": "test2"})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ ldap = NetAppESeriesLdap()
+ ldap.build_request_body = lambda: None
+ ldap.are_changes_required = lambda: True
+ ldap.delete_domain = lambda x: None
+ ldap.domain = {"id": "test"}
+ with self.assertRaisesRegexp(AnsibleExitJson, "LDAP domain has been removed."):
+ ldap.apply()
+
+ self._set_args({"state": "disabled"})
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.10.0000.0001"}), (200, {"runningAsProxy": True})]):
+ ldap = NetAppESeriesLdap()
+ ldap.build_request_body = lambda: None
+ ldap.are_changes_required = lambda: True
+ ldap.disable_domain = lambda: None
+ ldap.domain = {"id": "test"}
+ with self.assertRaisesRegexp(AnsibleExitJson, "All LDAP domains have been removed."):
+ ldap.apply()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_lun_mapping.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_lun_mapping.py
new file mode 100644
index 00000000..ed44e0de
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_lun_mapping.py
@@ -0,0 +1,196 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_lun_mapping import NetAppESeriesLunMapping
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class NetAppLunMappingTest(ModuleTestCase):
+ REQUIRED_PARAMS = {"api_username": "rw",
+ "api_password": "password",
+ "api_url": "http://localhost",
+ "ssid": "1"}
+
+ REQ_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_lun_mapping.NetAppESeriesLunMapping.request"
+ GRAPH_RESPONSE = {"storagePoolBundle": {"host": [{"name": "host1", "hostRef": "1"},
+ {"name": "host2", "hostRef": "2"},
+ {"name": "host3", "hostRef": "3"}],
+ "cluster": [{"name": "hostgroup1", "clusterRef": "10"},
+ {"name": "hostgroup2", "clusterRef": "20"},
+ {"name": "hostgroup3", "clusterRef": "30"}],
+ "lunMapping": [{"volumeRef": "100", "mapRef": "1", "lunMappingRef": "100001", "lun": 5},
+ {"volumeRef": "200", "mapRef": "2", "lunMappingRef": "200001", "lun": 3},
+ {"volumeRef": "1000", "mapRef": "10", "lunMappingRef": "300001", "lun": 6},
+ {"volumeRef": "2000", "mapRef": "20", "lunMappingRef": "400001", "lun": 4}]},
+ "volume": [{"name": "volume1", "volumeRef": "100", "listOfMappings": [{"lun": 5}]},
+ {"name": "volume2", "volumeRef": "200", "listOfMappings": [{"lun": 3}]},
+ {"name": "volume3", "volumeRef": "300", "listOfMappings": []}],
+ "highLevelVolBundle": {"thinVolume": [{"name": "thin_volume1", "volumeRef": "1000", "listOfMappings": [{"lun": 6}]},
+ {"name": "thin_volume2", "volumeRef": "2000", "listOfMappings": [{"lun": 4}]},
+ {"name": "thin_volume3", "volumeRef": "3000", "listOfMappings": []}]},
+ "sa": {"accessVolume": {"name": "access_volume", "accessVolumeRef": "10000"}}}
+ MAPPING_INFO = {"lun_mapping": [{"volume_reference": "100", "map_reference": "1", "lun_mapping_reference": "100001", "lun": 5},
+ {"volume_reference": "200", "map_reference": "2", "lun_mapping_reference": "200001", "lun": 3},
+ {"volume_reference": "1000", "map_reference": "10", "lun_mapping_reference": "300001", "lun": 6},
+ {"volume_reference": "2000", "map_reference": "20", "lun_mapping_reference": "400001", "lun": 4}],
+ "volume_by_reference": {"100": "volume1", "200": "volume2", "300": "volume3", "1000": "thin_volume1", "2000": "thin_volume2",
+ "3000": "thin_volume3", "10000": "access_volume"},
+ "volume_by_name": {"volume1": "100", "volume2": "200", "volume3": "300", "thin_volume1": "1000", "thin_volume2": "2000",
+ "thin_volume3": "3000", "access_volume": "10000"},
+ "lun_by_name": {"volume1": 5, "volume2": 3, "thin_volume1": 6, "thin_volume2": 4},
+ "target_by_reference": {"1": "host1", "2": "host2", "3": "host3", "10": "hostgroup1", "20": "hostgroup2", "30": "hostgroup3",
+ "0000000000000000000000000000000000000000": "DEFAULT_HOSTGROUP"},
+ "target_by_name": {"host1": "1", "host2": "2", "host3": "3", "hostgroup1": "10", "hostgroup2": "20", "hostgroup3": "30",
+ "DEFAULT_HOSTGROUP": "0000000000000000000000000000000000000000"},
+ "target_type_by_name": {"host1": "host", "host2": "host", "host3": "host", "hostgroup1": "group", "hostgroup2": "group",
+ "hostgroup3": "group", "DEFAULT_HOSTGROUP": "group"}}
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_update_mapping_info_pass(self):
+ """Verify update_mapping_info method creates the correct data structure."""
+ options = {"target": "host1", "volume": "volume1"}
+ self._set_args(options)
+ mapping = NetAppESeriesLunMapping()
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.GRAPH_RESPONSE)):
+ mapping.update_mapping_info()
+ print("%s" % mapping.mapping_info)
+ self.assertEquals(mapping.mapping_info, self.MAPPING_INFO)
+
+ def test_update_mapping_info_fail(self):
+ """Verify update_mapping_info throws the expected exceptions."""
+ response = {"storagePoolBundle": {"host": [{"name": "host1", "hostRef": "1"},
+ {"name": "host2", "hostRef": "2"},
+ {"name": "host3", "hostRef": "3"}],
+ "cluster": [{"name": "host1", "clusterRef": "10"},
+ {"name": "hostgroup2", "clusterRef": "20"},
+ {"name": "hostgroup3", "clusterRef": "30"}]}}
+ options = {"target": "host1", "volume": "volume1"}
+ self._set_args(options)
+ mapping = NetAppESeriesLunMapping()
+ with mock.patch(self.REQ_FUNC, return_value=(200, response)):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Ambiguous target type: target name is used for both host and group targets!"):
+ mapping.update_mapping_info()
+
+ def test_get_lun_mapping_pass(self):
+ """Verify get_lun_mapping method creates the correct data structure."""
+ options = {"target": "host1", "volume": "volume1"}
+ self._set_args(options)
+ mapping = NetAppESeriesLunMapping()
+ mapping.update_mapping_info = lambda: None
+ mapping.mapping_info = self.MAPPING_INFO
+ self.assertEquals(mapping.get_lun_mapping(), (True, "100001", 5))
+
+ options = {"target": "host1", "volume": "volume1", "lun": 5}
+ self._set_args(options)
+ mapping = NetAppESeriesLunMapping()
+ mapping.update_mapping_info = lambda: None
+ mapping.mapping_info = self.MAPPING_INFO
+ self.assertEquals(mapping.get_lun_mapping(), (True, "100001", 5))
+
+ options = {"target": "host1", "volume": "volume3", "lun": 10}
+ self._set_args(options)
+ mapping = NetAppESeriesLunMapping()
+ mapping.update_mapping_info = lambda: None
+ mapping.mapping_info = self.MAPPING_INFO
+ self.assertEquals(mapping.get_lun_mapping(), (False, None, None))
+
+ def test_get_lun_mapping_fail(self):
+ """Verify get_lun_mapping throws the expected exceptions."""
+ options = {"target": "host1", "volume": "volume3", "lun": 5}
+ self._set_args(options)
+ mapping = NetAppESeriesLunMapping()
+ mapping.update_mapping_info = lambda: None
+ mapping.mapping_info = self.MAPPING_INFO
+ with self.assertRaisesRegexp(AnsibleFailJson, "Option lun value is already in use for target!"):
+ mapping.get_lun_mapping()
+
+ options = {"target": "host10", "volume": "volume3"}
+ self._set_args(options)
+ mapping = NetAppESeriesLunMapping()
+ mapping.update_mapping_info = lambda: None
+ mapping.mapping_info = self.MAPPING_INFO
+ with self.assertRaisesRegexp(AnsibleFailJson, "Target does not exist."):
+ mapping.get_lun_mapping()
+
+ options = {"target": "host1", "volume": "volume10"}
+ self._set_args(options)
+ mapping = NetAppESeriesLunMapping()
+ mapping.update_mapping_info = lambda: None
+ mapping.mapping_info = self.MAPPING_INFO
+ with self.assertRaisesRegexp(AnsibleFailJson, "Volume does not exist."):
+ mapping.get_lun_mapping()
+
+ def test_update_pass(self):
+ """Verify update method creates the correct data structure."""
+ options = {"target": "host1", "volume": "volume1"}
+ self._set_args(options)
+ mapping = NetAppESeriesLunMapping()
+ mapping.update_mapping_info = lambda: None
+ mapping.mapping_info = self.MAPPING_INFO
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ with self.assertRaises(AnsibleExitJson):
+ mapping.update()
+
+ options = {"target": "host1", "volume": "volume1", "lun": 5}
+ self._set_args(options)
+ mapping = NetAppESeriesLunMapping()
+ mapping.update_mapping_info = lambda: None
+ mapping.mapping_info = self.MAPPING_INFO
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ with self.assertRaises(AnsibleExitJson):
+ mapping.update()
+
+ options = {"target": "host1", "volume": "volume3", "lun": 10}
+ self._set_args(options)
+ mapping = NetAppESeriesLunMapping()
+ mapping.update_mapping_info = lambda: None
+ mapping.mapping_info = self.MAPPING_INFO
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ with self.assertRaises(AnsibleExitJson):
+ mapping.update()
+
+ options = {"target": "host1", "volume": "volume1", "lun": 10}
+ self._set_args(options)
+ mapping = NetAppESeriesLunMapping()
+ mapping.update_mapping_info = lambda: None
+ mapping.mapping_info = self.MAPPING_INFO
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to update storage array lun mapping."):
+ mapping.update()
+
+ def test_update_fail(self):
+ """Verify update throws the expected exceptions."""
+ options = {"target": "host3", "volume": "volume3"}
+ self._set_args(options)
+ mapping = NetAppESeriesLunMapping()
+ mapping.update_mapping_info = lambda: None
+ mapping.mapping_info = self.MAPPING_INFO
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to update storage array lun mapping."):
+ mapping.update()
+
+ options = {"state": "absent", "target": "host1", "volume": "volume1"}
+ self._set_args(options)
+ mapping = NetAppESeriesLunMapping()
+ mapping.update_mapping_info = lambda: None
+ mapping.mapping_info = self.MAPPING_INFO
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to update storage array lun mapping."):
+ mapping.update()
+
+ options = {"target": "host3", "volume": "volume3", "lun": 15}
+ self._set_args(options)
+ mapping = NetAppESeriesLunMapping()
+ mapping.update_mapping_info = lambda: None
+ mapping.mapping_info = self.MAPPING_INFO
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to update storage array lun mapping."):
+ mapping.update()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_mgmt_interface.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_mgmt_interface.py
new file mode 100644
index 00000000..7c35d40d
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_mgmt_interface.py
@@ -0,0 +1,513 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_mgmt_interface import NetAppESeriesMgmtInterface
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class MgmtInterfaceTest(ModuleTestCase):
+ REQUIRED_PARAMS = {
+ 'api_username': 'rw',
+ 'api_password': 'password',
+ 'api_url': 'http://localhost',
+ 'ssid': '1',
+ }
+
+ TEST_DATA = [
+ {"controllerRef": "070000000000000000000001",
+ "controllerSlot": 1,
+ "interfaceName": "wan0",
+ "interfaceRef": "2800070000000000000000000001000000000000",
+ "channel": 1,
+ "alias": "creG1g-AP-a",
+ "ipv4Enabled": True,
+ "ipv4Address": "10.1.1.10",
+ "linkStatus": "up",
+ "ipv4SubnetMask": "255.255.255.0",
+ "ipv4AddressConfigMethod": "configStatic",
+ "ipv4GatewayAddress": "10.1.1.1",
+ "ipv6Enabled": False,
+ "physicalLocation": {"slot": 0},
+ "dnsProperties": {"acquisitionProperties": {"dnsAcquisitionType": "stat",
+ "dnsServers": [{"addressType": "ipv4",
+ "ipv4Address": "10.1.0.250"},
+ {"addressType": "ipv4",
+ "ipv4Address": "10.10.0.20"}]},
+ "dhcpAcquiredDnsServers": []},
+ "ntpProperties": {"acquisitionProperties": {"ntpAcquisitionType": "disabled",
+ "ntpServers": None},
+ "dhcpAcquiredNtpServers": []}},
+ {"controllerRef": "070000000000000000000001",
+ "controllerSlot": 1,
+ "interfaceName": "wan1",
+ "interfaceRef": "2800070000000000000000000001000000000000",
+ "channel": 2,
+ "alias": "creG1g-AP-a",
+ "ipv4Enabled": True,
+ "linkStatus": "down",
+ "ipv4Address": "0.0.0.0",
+ "ipv4SubnetMask": "0.0.0.0",
+ "ipv4AddressConfigMethod": "configDhcp",
+ "ipv4GatewayAddress": "10.1.1.1",
+ "ipv6Enabled": False,
+ "physicalLocation": {"slot": 1},
+ "dnsProperties": {"acquisitionProperties": {"dnsAcquisitionType": "stat",
+ "dnsServers": [{"addressType": "ipv4",
+ "ipv4Address": "10.1.0.250",
+ "ipv6Address": None},
+ {"addressType": "ipv4",
+ "ipv4Address": "10.10.0.20",
+ "ipv6Address": None}]},
+ "dhcpAcquiredDnsServers": []},
+ "ntpProperties": {"acquisitionProperties": {"ntpAcquisitionType": "disabled",
+ "ntpServers": None},
+ "dhcpAcquiredNtpServers": []}},
+ {"controllerRef": "070000000000000000000002",
+ "controllerSlot": 2,
+ "interfaceName": "wan0",
+ "interfaceRef": "2800070000000000000000000001000000000000",
+ "channel": 1,
+ "alias": "creG1g-AP-b",
+ "ipv4Enabled": True,
+ "ipv4Address": "0.0.0.0",
+ "linkStatus": "down",
+ "ipv4SubnetMask": "0.0.0.0",
+ "ipv4AddressConfigMethod": "configDhcp",
+ "ipv4GatewayAddress": "10.1.1.1",
+ "ipv6Enabled": False,
+ "physicalLocation": {"slot": 0},
+ "dnsProperties": {"acquisitionProperties": {"dnsAcquisitionType": "stat",
+ "dnsServers": [{"addressType": "ipv4",
+ "ipv4Address": "10.1.0.250",
+ "ipv6Address": None}]},
+ "dhcpAcquiredDnsServers": []},
+ "ntpProperties": {"acquisitionProperties": {"ntpAcquisitionType": "stat",
+ "ntpServers": [{"addrType": "ipvx",
+ "domainName": None,
+ "ipvxAddress": {"addressType": "ipv4",
+ "ipv4Address": "10.13.1.5",
+ "ipv6Address": None}},
+ {"addrType": "ipvx",
+ "domainName": None,
+ "ipvxAddress": {"addressType": "ipv4",
+ "ipv4Address": "10.15.1.8",
+ "ipv6Address": None}}]},
+ "dhcpAcquiredNtpServers": []}},
+ {"controllerRef": "070000000000000000000002",
+ "controllerSlot": 2,
+ "interfaceName": "wan1",
+ "interfaceRef": "2801070000000000000000000001000000000000",
+ "channel": 2,
+ "alias": "creG1g-AP-b",
+ "ipv4Enabled": True,
+ "ipv4Address": "0.0.0.0",
+ "linkStatus": "down",
+ "ipv4SubnetMask": "0.0.0.0",
+ "ipv4AddressConfigMethod": "configDhcp",
+ "ipv4GatewayAddress": "10.1.1.1",
+ "ipv6Enabled": False,
+ "physicalLocation": {"slot": 1},
+ "dnsProperties": {"acquisitionProperties": {"dnsAcquisitionType": "stat",
+ "dnsServers": [{"addressType": "ipv4",
+ "ipv4Address": "10.19.1.2",
+ "ipv6Address": None}]},
+ "dhcpAcquiredDnsServers": []},
+ "ntpProperties": {"acquisitionProperties": {"ntpAcquisitionType": "stat",
+ "ntpServers": [{"addrType": "ipvx",
+ "domainName": None,
+ "ipvxAddress": {"addressType": "ipv4",
+ "ipv4Address": "10.13.1.5",
+ "ipv6Address": None}},
+ {"addrType": "ipvx",
+ "domainName": None,
+ "ipvxAddress": {"addressType": "ipv4",
+ "ipv4Address": "10.15.1.18",
+ "ipv6Address": None}}]},
+ "dhcpAcquiredNtpServers": []}}]
+
+ REQ_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_mgmt_interface.NetAppESeriesMgmtInterface.request'
+ TIME_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_mgmt_interface.sleep'
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_get_controllers_pass(self):
+ """Verify dictionary return from get_controllers."""
+ initial = {
+ "state": "enabled",
+ "controller": "A",
+ "port": "1",
+ "address": "192.168.1.1",
+ "subnet_mask": "255.255.255.1",
+ "config_method": "static"}
+ controller_request = [
+ {"physicalLocation": {"slot": 2},
+ "controllerRef": "070000000000000000000002",
+ "networkSettings": {"remoteAccessEnabled": True}},
+ {"physicalLocation": {"slot": 1},
+ "controllerRef": "070000000000000000000001",
+ "networkSettings": {"remoteAccessEnabled": False}}]
+ expected = {
+ 'A': {'controllerRef': '070000000000000000000001',
+ 'controllerSlot': 1, 'ssh': False},
+ 'B': {'controllerRef': '070000000000000000000002',
+ 'controllerSlot': 2, 'ssh': True}}
+
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+
+ with mock.patch(self.REQ_FUNC, return_value=(200, controller_request)):
+ response = mgmt_interface.get_controllers()
+ self.assertTrue(response == expected)
+
+ def test_controller_property_fail(self):
+ """Verify controllers endpoint request failure causes AnsibleFailJson exception."""
+ initial = {
+ "state": "enabled",
+ "controller": "A",
+ "port": "1",
+ "address": "192.168.1.1",
+ "subnet_mask": "255.255.255.1",
+ "config_method": "static"}
+ controller_request = [
+ {"physicalLocation": {"slot": 2},
+ "controllerRef": "070000000000000000000002",
+ "networkSettings": {"remoteAccessEnabled": True}},
+ {"physicalLocation": {"slot": 1},
+ "controllerRef": "070000000000000000000001",
+ "networkSettings": {"remoteAccessEnabled": False}}]
+ expected = {
+ 'A': {'controllerRef': '070000000000000000000001',
+ 'controllerSlot': 1, 'ssh': False},
+ 'B': {'controllerRef': '070000000000000000000002',
+ 'controllerSlot': 2, 'ssh': True}}
+
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to retrieve the controller settings."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception):
+ response = mgmt_interface.get_controllers()
+
+ def test_update_target_interface_info_pass(self):
+ """Verify return value from interface property."""
+ initial = {
+ "state": "enabled",
+ "controller": "A",
+ "port": "1",
+ "address": "192.168.1.1",
+ "subnet_mask": "255.255.255.0",
+ "config_method": "static"}
+ get_controller = {"A": {"controllerSlot": 1, "controllerRef": "070000000000000000000001", "ssh": False},
+ "B": {"controllerSlot": 2, "controllerRef": "070000000000000000000002", "ssh": True}}
+ expected = {"channel": 1, "link_status": "up", "enabled": True, "address": "10.1.1.10", "gateway": "10.1.1.1", "subnet_mask": "255.255.255.0",
+ "dns_config_method": "stat",
+ "dns_servers": [{"addressType": "ipv4", "ipv4Address": "10.1.0.250"}, {"addressType": "ipv4", "ipv4Address": "10.10.0.20"}],
+ "ntp_config_method": "disabled", "ntp_servers": None, "config_method": "configStatic", "controllerRef": "070000000000000000000001",
+ "controllerSlot": 1, "ipv6_enabled": False, "id": "2800070000000000000000000001000000000000", "ssh": False}
+
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.get_controllers = lambda: get_controller
+
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.TEST_DATA)):
+ mgmt_interface.update_target_interface_info()
+ self.assertEquals(mgmt_interface.interface_info, expected)
+
+ def test_interface_property_request_exception_fail(self):
+ """Verify ethernet-interfaces endpoint request failure results in AnsibleFailJson exception."""
+ initial = {
+ "state": "enabled",
+ "controller": "A",
+ "port": "1",
+ "address": "192.168.1.1",
+ "subnet_mask": "255.255.255.0",
+ "config_method": "static"}
+ get_controller = {"A": {"controllerSlot": 1, "controllerRef": "070000000000000000000001", "ssh": False},
+ "B": {"controllerSlot": 2, "controllerRef": "070000000000000000000002", "ssh": True}}
+
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.get_controllers = lambda: get_controller
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to retrieve defined management interfaces."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ mgmt_interface.update_target_interface_info()
+
+ def test_update_target_interface_info_fail(self):
+ """Verify return value from update_target_interface_info method."""
+ initial = {
+ "state": "enabled",
+ "controller": "A",
+ "port": "3",
+ "address": "192.168.1.1",
+ "subnet_mask": "255.255.255.1",
+ "config_method": "static"}
+ get_controller = {"A": {"controllerSlot": 1, "controllerRef": "070000000000000000000001", "ssh": False},
+ "B": {"controllerSlot": 2, "controllerRef": "070000000000000000000002", "ssh": True}}
+
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.get_controllers = lambda: get_controller
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Invalid port number! Controller .*? ports:"):
+ with mock.patch(self.REQ_FUNC, return_value=(200, self.TEST_DATA)):
+ mgmt_interface.update_target_interface_info()
+
+ def test_update_body_enable_interface_setting_pass(self):
+ """Validate update_body_enable_interface_setting updates properly."""
+ initial = {"state": "enabled", "controller": "A", "port": "1", "address": "192.168.1.1", "subnet_mask": "255.255.255.1", "config_method": "static"}
+ interface_info = {"channel": 1, "link_status": "up", "enabled": True, "address": "10.1.1.10", "gateway": "10.1.1.1",
+ "subnet_mask": "255.255.255.0",
+ "dns_config_method": "stat",
+ "dns_servers": [{"addressType": "ipv4", "ipv4Address": "10.1.0.250"},
+ {"addressType": "ipv4", "ipv4Address": "10.10.0.20"}],
+ "ntp_config_method": "disabled", "ntp_servers": None, "config_method": "configStatic",
+ "controllerRef": "070000000000000000000001",
+ "controllerSlot": 1, "ipv6_enabled": True, "id": "2800070000000000000000000001000000000000", "ssh": False}
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.interface_info = interface_info
+ change_required = mgmt_interface.update_body_enable_interface_setting()
+ self.assertFalse(change_required)
+ self.assertTrue("ipv4Enabled" in mgmt_interface.body and mgmt_interface.body["ipv4Enabled"])
+
+ initial = {"state": "disabled", "controller": "A", "port": "1", "address": "192.168.1.1", "subnet_mask": "255.255.255.1", "config_method": "static"}
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.interface_info = interface_info
+ change_required = mgmt_interface.update_body_enable_interface_setting()
+ self.assertTrue(change_required)
+ self.assertTrue("ipv4Enabled" in mgmt_interface.body and not mgmt_interface.body["ipv4Enabled"])
+
+ def test_update_body_enable_interface_setting_fail(self):
+ """Validate update_body_enable_interface_setting throws expected exception"""
+ initial = {"state": "disabled", "controller": "A", "port": "1", "address": "192.168.1.1", "subnet_mask": "255.255.255.1", "config_method": "static"}
+ interface_info = {"channel": 1, "link_status": "up", "enabled": True, "address": "10.1.1.10", "gateway": "10.1.1.1",
+ "subnet_mask": "255.255.255.0",
+ "dns_config_method": "stat",
+ "dns_servers": [{"addressType": "ipv4", "ipv4Address": "10.1.0.250"},
+ {"addressType": "ipv4", "ipv4Address": "10.10.0.20"}],
+ "ntp_config_method": "disabled", "ntp_servers": None, "config_method": "configStatic",
+ "controllerRef": "070000000000000000000001",
+ "controllerSlot": 1, "ipv6_enabled": False, "id": "2800070000000000000000000001000000000000", "ssh": False}
+
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.interface_info = interface_info
+ with self.assertRaisesRegexp(AnsibleFailJson, "Either IPv4 or IPv6 must be enabled."):
+ mgmt_interface.update_body_enable_interface_setting()
+
+ def test_update_body_interface_settings_fail(self):
+ """Validate update_body_interface_settings throws expected exception"""
+ initial = {"state": "enabled", "controller": "A", "port": "1", "address": "192.168.1.1", "subnet_mask": "255.255.255.1", "config_method": "static"}
+ interface_info = {"channel": 1, "link_status": "up", "enabled": True, "address": "10.1.1.10", "gateway": "10.1.1.1",
+ "subnet_mask": "255.255.255.0",
+ "dns_config_method": "stat",
+ "dns_servers": [{"addressType": "ipv4", "ipv4Address": "10.1.0.250"},
+ {"addressType": "ipv4", "ipv4Address": "10.10.0.20"}],
+ "ntp_config_method": "disabled", "ntp_servers": None, "config_method": "configStatic",
+ "controllerRef": "070000000000000000000001",
+ "controllerSlot": 1, "ipv6_enabled": False, "id": "2800070000000000000000000001000000000000", "ssh": False}
+
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.interface_info = interface_info
+ self.assertTrue(mgmt_interface.update_body_interface_settings())
+ self.assertEquals(mgmt_interface.body, {"ipv4AddressConfigMethod": "configStatic", "ipv4Address": "192.168.1.1", "ipv4SubnetMask": "255.255.255.1"})
+
+ initial = {"state": "enabled", "controller": "A", "port": "1", "address": "192.168.1.100", "subnet_mask": "255.255.255.1", "gateway": "192.168.1.1",
+ "config_method": "static"}
+ interface_info = {"channel": 1, "link_status": "up", "enabled": True, "address": "10.1.1.10", "gateway": "10.1.1.1",
+ "subnet_mask": "255.255.255.0",
+ "dns_config_method": "stat",
+ "dns_servers": [{"addressType": "ipv4", "ipv4Address": "10.1.0.250"},
+ {"addressType": "ipv4", "ipv4Address": "10.10.0.20"}],
+ "ntp_config_method": "disabled", "ntp_servers": None, "config_method": "configStatic",
+ "controllerRef": "070000000000000000000001",
+ "controllerSlot": 1, "ipv6_enabled": False, "id": "2800070000000000000000000001000000000000", "ssh": False}
+
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.interface_info = interface_info
+ self.assertTrue(mgmt_interface.update_body_interface_settings())
+ self.assertEquals(mgmt_interface.body, {"ipv4AddressConfigMethod": "configStatic", "ipv4Address": "192.168.1.100", "ipv4SubnetMask": "255.255.255.1",
+ "ipv4GatewayAddress": "192.168.1.1"})
+
+ initial = {"state": "enabled", "controller": "A", "port": "1", "config_method": "dhcp"}
+ interface_info = {"channel": 1, "link_status": "up", "enabled": True, "address": "10.1.1.10", "gateway": "10.1.1.1",
+ "subnet_mask": "255.255.255.0",
+ "dns_config_method": "stat",
+ "dns_servers": [{"addressType": "ipv4", "ipv4Address": "10.1.0.250"},
+ {"addressType": "ipv4", "ipv4Address": "10.10.0.20"}],
+ "ntp_config_method": "disabled", "ntp_servers": None, "config_method": "configStatic",
+ "controllerRef": "070000000000000000000001",
+ "controllerSlot": 1, "ipv6_enabled": False, "id": "2800070000000000000000000001000000000000", "ssh": False}
+
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.interface_info = interface_info
+ self.assertTrue(mgmt_interface.update_body_interface_settings())
+ self.assertEquals(mgmt_interface.body, {"ipv4AddressConfigMethod": "configDhcp"})
+
+ initial = {"state": "enabled", "controller": "A", "port": "1", "config_method": "dhcp"}
+ interface_info = {"channel": 1, "link_status": "up", "enabled": True, "address": "10.1.1.10", "gateway": "10.1.1.1",
+ "subnet_mask": "255.255.255.0",
+ "dns_config_method": "stat",
+ "dns_servers": [{"addressType": "ipv4", "ipv4Address": "10.1.0.250"},
+ {"addressType": "ipv4", "ipv4Address": "10.10.0.20"}],
+ "ntp_config_method": "disabled", "ntp_servers": None, "config_method": "configDhcp",
+ "controllerRef": "070000000000000000000001",
+ "controllerSlot": 1, "ipv6_enabled": False, "id": "2800070000000000000000000001000000000000", "ssh": False}
+
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.interface_info = interface_info
+ self.assertFalse(mgmt_interface.update_body_interface_settings())
+ self.assertEquals(mgmt_interface.body, {"ipv4AddressConfigMethod": "configDhcp"})
+
+ def test_update_body_dns_server_settings_pass(self):
+ """Validate update_body_dns_server_settings throws expected exception"""
+ interface_info = {"channel": 1, "link_status": "up", "enabled": True, "address": "10.1.1.10", "gateway": "10.1.1.1",
+ "subnet_mask": "255.255.255.0",
+ "dns_config_method": "stat",
+ "dns_servers": [{"addressType": "ipv4", "ipv4Address": "10.1.0.250"},
+ {"addressType": "ipv4", "ipv4Address": "10.10.0.20"}],
+ "ntp_config_method": "disabled", "ntp_servers": None, "config_method": "configStatic",
+ "controllerRef": "070000000000000000000001",
+ "controllerSlot": 1, "ipv6_enabled": False, "id": "2800070000000000000000000001000000000000", "ssh": False}
+
+ initial = {"state": "enabled", "controller": "A", "port": "1", "dns_config_method": "dhcp"}
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.interface_info = interface_info
+ self.assertTrue(mgmt_interface.update_body_dns_server_settings())
+ self.assertEquals(mgmt_interface.body, {"dnsAcquisitionDescriptor": {"dnsAcquisitionType": "dhcp"}})
+
+ initial = {"state": "enabled", "controller": "A", "port": "1", "dns_config_method": "static", "dns_address": "192.168.1.100"}
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.interface_info = interface_info
+ self.assertTrue(mgmt_interface.update_body_dns_server_settings())
+ self.assertEquals(mgmt_interface.body, {"dnsAcquisitionDescriptor": {"dnsAcquisitionType": "stat",
+ "dnsServers": [{"addressType": "ipv4", "ipv4Address": "192.168.1.100"}]}})
+
+ initial = {"state": "enabled", "controller": "A", "port": "1", "dns_config_method": "static", "dns_address": "192.168.1.100",
+ "dns_address_backup": "192.168.1.102"}
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.interface_info = interface_info
+ self.assertTrue(mgmt_interface.update_body_dns_server_settings())
+ self.assertEquals(mgmt_interface.body, {"dnsAcquisitionDescriptor": {"dnsAcquisitionType": "stat",
+ "dnsServers": [{"addressType": "ipv4", "ipv4Address": "192.168.1.100"},
+ {"addressType": "ipv4", "ipv4Address": "192.168.1.102"}]}})
+
+ def test_update_body_ntp_server_settings_pass(self):
+ """Validate update_body_ntp_server_settings throws expected exception"""
+ interface_info = {"channel": 1, "link_status": "up", "enabled": True, "address": "10.1.1.10", "gateway": "10.1.1.1",
+ "subnet_mask": "255.255.255.0",
+ "dns_config_method": "stat",
+ "dns_servers": [{"addressType": "ipv4", "ipv4Address": "10.1.0.250"},
+ {"addressType": "ipv4", "ipv4Address": "10.10.0.20"}],
+ "ntp_config_method": "dhcp", "ntp_servers": None, "config_method": "configStatic",
+ "controllerRef": "070000000000000000000001",
+ "controllerSlot": 1, "ipv6_enabled": False, "id": "2800070000000000000000000001000000000000", "ssh": False}
+
+ initial = {"state": "enabled", "controller": "A", "port": "1", "ntp_config_method": "disabled"}
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.interface_info = interface_info
+ self.assertTrue(mgmt_interface.update_body_ntp_server_settings())
+ self.assertEquals(mgmt_interface.body, {"ntpAcquisitionDescriptor": {"ntpAcquisitionType": "disabled"}})
+
+ initial = {"state": "enabled", "controller": "A", "port": "1", "ntp_config_method": "dhcp"}
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.interface_info = interface_info
+ self.assertFalse(mgmt_interface.update_body_ntp_server_settings())
+ self.assertEquals(mgmt_interface.body, {"ntpAcquisitionDescriptor": {"ntpAcquisitionType": "dhcp"}})
+
+ initial = {"state": "enabled", "controller": "A", "port": "1", "ntp_config_method": "static", "ntp_address": "192.168.1.200"}
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.interface_info = interface_info
+ self.assertTrue(mgmt_interface.update_body_ntp_server_settings())
+ self.assertEquals(mgmt_interface.body, {"ntpAcquisitionDescriptor": {
+ "ntpAcquisitionType": "stat", "ntpServers": [{"addrType": "ipvx", "ipvxAddress": {"addressType": "ipv4", "ipv4Address": "192.168.1.200"}}]}})
+
+ initial = {"state": "enabled", "controller": "A", "port": "1", "ntp_config_method": "static", "ntp_address": "192.168.1.200",
+ "ntp_address_backup": "192.168.1.202"}
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.interface_info = interface_info
+ self.assertTrue(mgmt_interface.update_body_ntp_server_settings())
+ self.assertEquals(mgmt_interface.body, {"ntpAcquisitionDescriptor": {
+ "ntpAcquisitionType": "stat", "ntpServers": [{"addrType": "ipvx", "ipvxAddress": {"addressType": "ipv4", "ipv4Address": "192.168.1.200"}},
+ {"addrType": "ipvx", "ipvxAddress": {"addressType": "ipv4", "ipv4Address": "192.168.1.202"}}]}})
+
+ def test_update_body_ssh_setting_pass(self):
+ """Validate update_body_ssh_setting throws expected exception"""
+ interface_info = {"channel": 1, "link_status": "up", "enabled": True, "address": "10.1.1.10", "gateway": "10.1.1.1",
+ "subnet_mask": "255.255.255.0",
+ "dns_config_method": "stat",
+ "dns_servers": [{"addressType": "ipv4", "ipv4Address": "10.1.0.250"},
+ {"addressType": "ipv4", "ipv4Address": "10.10.0.20"}],
+ "ntp_config_method": "disabled", "ntp_servers": None, "config_method": "configStatic",
+ "controllerRef": "070000000000000000000001",
+ "controllerSlot": 1, "ipv6_enabled": False, "id": "2800070000000000000000000001000000000000", "ssh": False}
+
+ initial = {"state": "enabled", "controller": "A", "port": "1", "config_method": "dhcp", "ssh": True}
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.interface_info = interface_info
+ self.assertTrue(mgmt_interface.update_body_ssh_setting())
+ self.assertEquals(mgmt_interface.body, {"enableRemoteAccess": True})
+
+ initial = {"state": "enabled", "controller": "A", "port": "1", "config_method": "dhcp", "ssh": False}
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.interface_info = interface_info
+ self.assertFalse(mgmt_interface.update_body_ssh_setting())
+ self.assertEquals(mgmt_interface.body, {"enableRemoteAccess": False})
+
+ def test_update_url_pass(self):
+ """Verify update_url returns expected url."""
+ initial = {"state": "enabled", "controller": "A", "port": "1", "config_method": "dhcp", "ssh": False}
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.url = "https://192.168.1.100:8443/devmgr/v2/"
+ mgmt_interface.alt_interface_addresses = ["192.168.1.102"]
+ mgmt_interface.update_url()
+ self.assertTrue(mgmt_interface.url, "https://192.168.1.102:8443/devmgr/v2/")
+
+ def test_update_pass(self):
+ """Verify update successfully completes."""
+ initial = {"state": "enabled", "controller": "A", "port": "1", "config_method": "dhcp", "ssh": False}
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.update_request_body = lambda: False
+ mgmt_interface.is_embedded = lambda: False
+ mgmt_interface.use_alternate_address = False
+ with self.assertRaisesRegexp(AnsibleExitJson, "No changes are required."):
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ mgmt_interface.update()
+
+ def update_request_body():
+ update_request_body.value = not update_request_body.value
+ return update_request_body.value
+ update_request_body.value = False
+
+ initial = {"state": "enabled", "controller": "A", "port": "1", "config_method": "dhcp", "ssh": False}
+ self._set_args(initial)
+ mgmt_interface = NetAppESeriesMgmtInterface()
+ mgmt_interface.update_request_body = update_request_body
+ mgmt_interface.is_embedded = lambda: True
+ mgmt_interface.use_alternate_address = False
+ with self.assertRaisesRegexp(AnsibleExitJson, "The interface settings have been updated."):
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ mgmt_interface.update()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_nvme_interface.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_nvme_interface.py
new file mode 100644
index 00000000..aee149f0
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_nvme_interface.py
@@ -0,0 +1,220 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_nvme_interface import NetAppESeriesNvmeInterface
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class NvmeInterfaceTest(ModuleTestCase):
+ REQUIRED_PARAMS = {"api_username": "rw",
+ "api_password": "password",
+ "api_url": "http://localhost",
+ "ssid": "1",
+ "state": "enabled",
+ "controller": "A",
+ "channel": 1}
+
+ REQ_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_nvme_interface.NetAppESeriesNvmeInterface.request"
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_valid_options_pass(self):
+ """Verify valid options."""
+ valid_option_list = [{"state": "enabled", "config_method": "static", "address": "192.168.1.100", "subnet_mask": "255.255.255.0",
+ "gateway": "192.168.1.1", "mtu": 1500},
+ {"address": "192.168.1.100"},
+ {"state": "enabled", "config_method": "dhcp", "mtu": 1500},
+ {"state": "disabled"}]
+
+ for option in valid_option_list:
+ self._set_args(option)
+ nvme = NetAppESeriesNvmeInterface()
+
+ def test_invalid_options_fail(self):
+ """Verify invalid options throw expected exceptions."""
+ invalid_option_list = [{"state": "enabled", "config_method": "static", "address": "1920.168.1.100", "subnet_mask": "255.255.255.0",
+ "gateway": "192.168.1.1", "mtu": 1500},
+ {"state": "enabled", "config_method": "static", "address": "192.168.1.100", "subnet_mask": "255.2550.255.0",
+ "gateway": "192.168.1.1", "mtu": 1500},
+ {"state": "enabled", "config_method": "static", "address": "192.168.1.100", "subnet_mask": "255.255.255.0",
+ "gateway": "192.168..100", "mtu": 1500},
+ {"state": "enabled", "config_method": "static", "address": "192.168.1.100", "subnet_mask": "2550.255.255.0",
+ "gateway": "192.168.1.1000", "mtu": 1500}]
+
+ for option in invalid_option_list:
+ self._set_args(option)
+ with self.assertRaises(AnsibleFailJson):
+ nvme = NetAppESeriesNvmeInterface()
+
+ def test_get_nvmeof_interfaces_pass(self):
+ """Verify get_nvmeof_interfaces method returns the expected list of interface values."""
+ options = {"address": "192.168.1.100"}
+ response = [{"controllerRef": "070000000000000000000001", "interfaceRef": "2201020000000000000000000000000000000000",
+ "ioInterfaceTypeData": {"interfaceType": "ib",
+ "ib": {"interfaceRef": "2201020000000000000000000000000000000000", "channel": 1, "linkState": "up"}},
+ "commandProtocolPropertiesList": {"commandProtocolProperties": [
+ {"commandProtocol": "nvme", "nvmeProperties": {"commandSet": "nvmeof", "nvmeofProperties": {
+ "provider": "providerInfiniband", "ibProperties": {"ipAddressData": {
+ "addressType": "ipv4", "ipv4Data": {"configState": "configured", "ipv4Address": "192.168.1.100"}}}}}}]}}]
+ self._set_args(options)
+ nvme = NetAppESeriesNvmeInterface()
+ with mock.patch(self.REQ_FUNC, return_value=(200, response)):
+ self.assertEquals(nvme.get_nvmeof_interfaces(), [
+ {"properties": {"provider": "providerInfiniband", "ibProperties": {
+ "ipAddressData": {"addressType": "ipv4",
+ "ipv4Data": {"configState": "configured", "ipv4Address": "192.168.1.100"}}}},
+ "reference": "2201020000000000000000000000000000000000", "channel": 1, "interface_type": "ib",
+ "interface": {"interfaceRef": "2201020000000000000000000000000000000000", "channel": 1,
+ "linkState": "up"}, "controller_id": "070000000000000000000001",
+ "link_status": "up"}])
+
+ def test_get_nvmeof_interfaces_fail(self):
+ """Verify get_nvmeof_interfaces method throws the expected exceptions."""
+ options = {"address": "192.168.1.100"}
+ self._set_args(options)
+ nvme = NetAppESeriesNvmeInterface()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve defined host interfaces."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ nvme.get_nvmeof_interfaces()
+
+ def test_get_target_interface_pass(self):
+ """Verify get_target_interface returns the expected interface."""
+ # options = {"state": "enabled", "config_method": "static", "address": "192.168.1.100", "subnet_mask": "255.255.255.0",
+ # "gateway": "192.168.1.1", "mtu": 1500}
+ options = {"address": "192.168.1.200"}
+ self._set_args(options)
+ nvme = NetAppESeriesNvmeInterface()
+ nvme.get_nvmeof_interfaces = lambda: [
+ {"properties": {"provider": "providerInfiniband", "ibProperties": {
+ "ipAddressData": {"addressType": "ipv4",
+ "ipv4Data": {"configState": "configured", "ipv4Address": "192.168.1.100"}}}},
+ "reference": "2201020000000000000000000000000000000000", "channel": 5,
+ "interface_type": {"interfaceRef": "2201020000000000000000000000000000000000", "channel": 5,
+ "linkState": "up"}, "controller_id": "070000000000000000000001",
+ "link_status": "up"},
+ {"properties": {"provider": "providerInfiniband", "ibProperties": {
+ "ipAddressData": {"addressType": "ipv4",
+ "ipv4Data": {"configState": "configured", "ipv4Address": "192.168.2.100"}}}},
+ "reference": "2201030000000000000000000000000000000000", "channel": 4,
+ "interface_type": {"interfaceRef": "2201030000000000000000000000000000000000", "channel": 4,
+ "linkState": "up"}, "controller_id": "070000000000000000000001",
+ "link_status": "up"},
+ {"properties": {"provider": "providerInfiniband", "ibProperties": {
+ "ipAddressData": {"addressType": "ipv4",
+ "ipv4Data": {"configState": "configured", "ipv4Address": "192.168.3.100"}}}},
+ "reference": "2201040000000000000000000000000000000000", "channel": 6,
+ "interface_type": {"interfaceRef": "2201040000000000000000000000000000000000", "channel": 6,
+ "linkState": "down"}, "controller_id": "070000000000000000000001",
+ "link_status": "up"}]
+ nvme.get_controllers = lambda: {"A": "070000000000000000000001", "B": "070000000000000000000002"}
+ self.assertEqual(nvme.get_target_interface(), {
+ "properties": {"provider": "providerInfiniband", "ibProperties": {
+ "ipAddressData": {"addressType": "ipv4",
+ "ipv4Data": {"configState": "configured", "ipv4Address": "192.168.2.100"}}}},
+ "reference": "2201030000000000000000000000000000000000", "channel": 4,
+ "interface_type": {"interfaceRef": "2201030000000000000000000000000000000000", "channel": 4,
+ "linkState": "up"}, "controller_id": "070000000000000000000001",
+ "link_status": "up"})
+
+ def test_get_target_interface_fail(self):
+ """Verify get_target_interface method throws the expected exceptions."""
+ options = {"address": "192.168.1.200", "channel": "0"}
+ self._set_args(options)
+ nvme = NetAppESeriesNvmeInterface()
+ nvme.get_nvmeof_interfaces = lambda: [
+ {"properties": {"provider": "providerInfiniband", "ibProperties": {
+ "ipAddressData": {"addressType": "ipv4",
+ "ipv4Data": {"configState": "configured", "ipv4Address": "192.168.1.100"}}}},
+ "reference": "2201020000000000000000000000000000000000", "channel": 5,
+ "interface_type": {"interfaceRef": "2201020000000000000000000000000000000000", "channel": 5,
+ "linkState": "up"}, "controller_id": "070000000000000000000001",
+ "link_status": "up"}]
+ nvme.get_controllers = lambda: {"A": "070000000000000000000001", "B": "070000000000000000000002"}
+ with self.assertRaisesRegexp(AnsibleFailJson, "Invalid controller .*? NVMe channel."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ nvme.get_target_interface()
+
+ options = {"address": "192.168.1.200", "channel": "2"}
+ self._set_args(options)
+ nvme = NetAppESeriesNvmeInterface()
+ nvme.get_nvmeof_interfaces = lambda: [
+ {"properties": {"provider": "providerInfiniband", "ibProperties": {
+ "ipAddressData": {"addressType": "ipv4",
+ "ipv4Data": {"configState": "configured", "ipv4Address": "192.168.1.100"}}}},
+ "reference": "2201020000000000000000000000000000000000", "channel": 5,
+ "interface_type": {"interfaceRef": "2201020000000000000000000000000000000000", "channel": 5,
+ "linkState": "up"}, "controller_id": "070000000000000000000001",
+ "link_status": "up"}]
+ nvme.get_controllers = lambda: {"A": "070000000000000000000001", "B": "070000000000000000000002"}
+ with self.assertRaisesRegexp(AnsibleFailJson, "Invalid controller .*? NVMe channel."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ nvme.get_target_interface()
+
+ def test_update_pass(self):
+ """Verify update successfully completes"""
+ # options = {"state": "enabled", "config_method": "static", "address": "192.168.1.100", "subnet_mask": "255.255.255.0",
+ # "gateway": "192.168.1.1", "mtu": 1500}
+ options = {"address": "192.168.1.200"}
+ iface = {"properties": {"provider": "providerInfiniband",
+ "ibProperties": {"ipAddressData": {"addressType": "ipv4",
+ "ipv4Data": {"configState": "configured", "ipv4Address": "192.168.1.100"}}}},
+ "reference": "2201020000000000000000000000000000000000", "channel": 5, "interface_type": "ib", "controllerRef": "070000000000000000000001",
+ "link_status": "up"}
+ self._set_args(options)
+ nvme = NetAppESeriesNvmeInterface()
+ nvme.get_target_interface = lambda: iface
+ with self.assertRaisesRegexp(AnsibleExitJson, "NVMeoF interface settings have been updated."):
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ nvme.update()
+
+ options = {"address": "192.168.1.200"}
+ iface = {"properties": {"provider": "providerInfiniband",
+ "ibProperties": {"ipAddressData": {"addressType": "ipv4",
+ "ipv4Data": {"configState": "configured", "ipv4Address": "192.168.1.100"}}}},
+ "reference": "2201020000000000000000000000000000000000", "channel": 5, "interface_type": "ib", "controllerRef": "070000000000000000000001",
+ "link_status": "up"}
+ self._set_args(options)
+ nvme = NetAppESeriesNvmeInterface()
+ nvme.module.check_mode = True
+ nvme.get_target_interface = lambda: iface
+ with self.assertRaisesRegexp(AnsibleExitJson, "No changes have been made."):
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ nvme.update()
+
+ options = {"address": "192.168.1.100"}
+ iface = {"properties": {"provider": "providerInfiniband",
+ "ibProperties": {"ipAddressData": {"addressType": "ipv4",
+ "ipv4Data": {"configState": "configured", "ipv4Address": "192.168.1.100"}}}},
+ "reference": "2201020000000000000000000000000000000000", "channel": 5, "interface_type": "ib", "controllerRef": "070000000000000000000001",
+ "link_status": "up"}
+ self._set_args(options)
+ nvme = NetAppESeriesNvmeInterface()
+ nvme.get_target_interface = lambda: iface
+
+ with self.assertRaisesRegexp(AnsibleExitJson, "No changes have been made."):
+ with mock.patch(self.REQ_FUNC, return_value=(200, None)):
+ nvme.update()
+
+ def test_update_fail(self):
+ """Verify update throws expected exception."""
+ # options = {"state": "enabled", "config_method": "static", "address": "192.168.1.100", "subnet_mask": "255.255.255.0",
+ # "gateway": "192.168.1.1", "mtu": 1500}
+ options = {"address": "192.168.1.200"}
+ iface = {"properties": {"provider": "providerInfiniband",
+ "ibProperties": {"ipAddressData": {"addressType": "ipv4",
+ "ipv4Data": {"configState": "configured", "ipv4Address": "192.168.1.100"}}}},
+ "reference": "2201020000000000000000000000000000000000", "channel": 5, "interface_type": "ib", "controllerRef": "070000000000000000000001",
+ "link_status": "up"}
+ self._set_args(options)
+ nvme = NetAppESeriesNvmeInterface()
+ nvme.get_target_interface = lambda: iface
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to configure interface."):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ nvme.update()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_proxy_drive_firmware_upload.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_proxy_drive_firmware_upload.py
new file mode 100644
index 00000000..a527b291
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_proxy_drive_firmware_upload.py
@@ -0,0 +1,137 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_proxy_drive_firmware_upload import NetAppESeriesProxyDriveFirmwareUpload
+from units.compat.mock import patch, mock_open
+
+
+class StoragePoolTest(ModuleTestCase):
+ REQUIRED_PARAMS = {"api_username": "username",
+ "api_password": "password",
+ "api_url": "http://localhost/devmgr/v2",
+ "validate_certs": "no"}
+
+ REQUEST_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_proxy_drive_firmware_upload." \
+ "NetAppESeriesProxyDriveFirmwareUpload.request"
+ CREATE_MULTIPART_FORMDATA_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules." \
+ "na_santricity_proxy_drive_firmware_upload.create_multipart_formdata"
+ OS_PATH_EXISTS_FUNC = "os.path.exists"
+ OS_PATH_ISDIR_FUNC = "os.path.isdir"
+ OS_LISTDIR_FUNC = "os.listdir"
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_determine_file_paths_pass(self):
+ """Ensure determine_file_paths method succeeds when all files exist."""
+ self._set_args({"firmware": ["/path/to/firmware1.dlp", "/path/to/firmware/directory"]})
+ firmware = NetAppESeriesProxyDriveFirmwareUpload()
+
+ with patch(self.OS_PATH_EXISTS_FUNC, return_value=True):
+ with patch(self.OS_PATH_ISDIR_FUNC, side_effect=[False, True]):
+ with patch(self.OS_LISTDIR_FUNC, return_value=["firmware2.dlp", "firmware3.dlp"]):
+ firmware.determine_file_paths()
+ self.assertEqual(firmware.files, {"firmware1.dlp": "/path/to/firmware1.dlp",
+ "firmware2.dlp": "/path/to/firmware/directory/firmware2.dlp",
+ "firmware3.dlp": "/path/to/firmware/directory/firmware3.dlp"})
+
+ def test_determine_file_paths_fail(self):
+ """Ensure determine_file_paths method throws expected exception."""
+ self._set_args({"firmware": ["/path/to/firmware1.dlp", "/path/to/firmware/directory"]})
+ firmware = NetAppESeriesProxyDriveFirmwareUpload()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Drive firmware file does not exist!"):
+ with patch(self.OS_PATH_EXISTS_FUNC, side_effect=[True, False]):
+ firmware.determine_file_paths()
+
+ def test_determine_changes_pass(self):
+ """Determine whether determine_changes returns expected results."""
+ self._set_args({"firmware": ["/path/to/firmware1.dlp", "/path/to/firmware/directory"]})
+ firmware = NetAppESeriesProxyDriveFirmwareUpload()
+ firmware.files = {"firmware1.dlp": "/path/to/firmware1.dlp",
+ "firmware2.dlp": "/path/to/firmware/directory/firmware2.dlp",
+ "firmware3.dlp": "/path/to/firmware/directory/firmware3.dlp"}
+
+ with patch(self.REQUEST_FUNC, return_value=(200, [{"fileName": "firmware1.dlp"}, {"fileName": "firmware3.dlp"}, {"fileName": "firmware4.dlp"}])):
+ firmware.determine_changes()
+
+ self.assertEqual(firmware.add_files, ["firmware2.dlp"])
+ self.assertEqual(firmware.remove_files, ["firmware4.dlp"])
+
+ def test_determine_changes_fail(self):
+ """Ensure class constructor fails when file does not exist."""
+ self._set_args({"firmware": ["/path/to/firmware1.dlp", "/path/to/firmware/directory"]})
+ firmware = NetAppESeriesProxyDriveFirmwareUpload()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to retrieve proxy drive firmware file list."):
+ with patch(self.REQUEST_FUNC, return_value=Exception()):
+ firmware.determine_changes()
+
+ def test_upload_files_pass(self):
+ """Ensure upload_files method successfully passes."""
+ self._set_args({"firmware": ["/path/to/firmware1.dlp", "/path/to/firmware/directory"]})
+ firmware = NetAppESeriesProxyDriveFirmwareUpload()
+ firmware.files = {"firmware1.dlp": "/path/to/firmware1.dlp",
+ "firmware2.dlp": "/path/to/firmware/directory/firmware2.dlp",
+ "firmware3.dlp": "/path/to/firmware/directory/firmware3.dlp"}
+ firmware.add_files = ["firmware1.dlp", "firmware2.dlp"]
+
+ with patch(self.CREATE_MULTIPART_FORMDATA_FUNC, return_value=(None, None)):
+ with patch(self.REQUEST_FUNC, return_value=(200, None)):
+ firmware.upload_files()
+
+ def test_delete_files_pass(self):
+ """Ensure delete_files completes as expected."""
+ self._set_args({"firmware": ["/path/to/firmware1.dlp", "/path/to/firmware/directory"]})
+ firmware = NetAppESeriesProxyDriveFirmwareUpload()
+ firmware.remove_files = ["firmware1.dlp", "firmware2.dlp"]
+
+ with patch(self.REQUEST_FUNC, return_value=(204, None)):
+ firmware.delete_files()
+
+ def test_apply_pass(self):
+ """Ensure that the apply method behaves as expected."""
+ self._set_args({"firmware": ["/path/to/firmware1.dlp", "/path/to/firmware/directory"]})
+ firmware = NetAppESeriesProxyDriveFirmwareUpload()
+ firmware.files = {"firmware1.dlp": "/path/to/firmware1.dlp",
+ "firmware2.dlp": "/path/to/firmware/directory/firmware2.dlp",
+ "firmware3.dlp": "/path/to/firmware/directory/firmware3.dlp"}
+ firmware.module.check_mode = True
+ firmware.is_proxy = lambda: True
+ firmware.determine_file_paths = lambda: None
+ firmware.determine_changes = lambda: None
+
+ firmware.add_files = ["firmware1.dlp", "firmware2.dlp"]
+ firmware.remove_files = ["firmware3.dlp", "firmware4.dlp"]
+ with self.assertRaisesRegexp(AnsibleExitJson, r"'changed': True"):
+ firmware.apply()
+
+ firmware.add_files = ["firmware1.dlp", "firmware2.dlp"]
+ firmware.remove_files = []
+ with self.assertRaisesRegexp(AnsibleExitJson, r"'changed': True"):
+ firmware.apply()
+
+ firmware.add_files = []
+ firmware.remove_files = ["firmware3.dlp", "firmware4.dlp"]
+ with self.assertRaisesRegexp(AnsibleExitJson, r"'changed': True"):
+ firmware.apply()
+
+ firmware.add_files = []
+ firmware.remove_files = []
+ with self.assertRaisesRegexp(AnsibleExitJson, r"'changed': False"):
+ firmware.apply()
+
+ def test_apply_fail(self):
+ """Ensure that the apply method fails when not executing against the proxy."""
+ self._set_args({"firmware": ["/path/to/firmware1.dlp", "/path/to/firmware/directory"]})
+ firmware = NetAppESeriesProxyDriveFirmwareUpload()
+ firmware.is_proxy = lambda: False
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Module can only be executed against SANtricity Web Services Proxy."):
+ firmware.apply()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_proxy_firmware_upload.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_proxy_firmware_upload.py
new file mode 100644
index 00000000..72ccd671
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_proxy_firmware_upload.py
@@ -0,0 +1,136 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_proxy_firmware_upload import NetAppESeriesProxyFirmwareUpload
+from units.compat.mock import patch, mock_open
+
+
+class StoragePoolTest(ModuleTestCase):
+ REQUIRED_PARAMS = {"api_username": "username",
+ "api_password": "password",
+ "api_url": "http://localhost/devmgr/v2",
+ "validate_certs": "no"}
+
+ REQUEST_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_proxy_firmware_upload.NetAppESeriesProxyFirmwareUpload.request"
+ CREATE_MULTIPART_FORMDATA_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules." \
+ "na_santricity_proxy_firmware_upload.create_multipart_formdata"
+ OS_PATH_EXISTS_FUNC = "os.path.exists"
+ OS_PATH_ISDIR_FUNC = "os.path.isdir"
+ OS_LISTDIR_FUNC = "os.listdir"
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_determine_file_paths_pass(self):
+ """Ensure determine_file_paths method succeeds when all files exist."""
+ self._set_args({"firmware": ["/path/to/firmware1.dlp", "/path/to/firmware/directory"]})
+ firmware = NetAppESeriesProxyFirmwareUpload()
+
+ with patch(self.OS_PATH_EXISTS_FUNC, return_value=True):
+ with patch(self.OS_PATH_ISDIR_FUNC, side_effect=[False, True]):
+ with patch(self.OS_LISTDIR_FUNC, return_value=["firmware2.dlp", "firmware3.dlp"]):
+ firmware.determine_file_paths()
+ self.assertEqual(firmware.files, {"firmware1.dlp": "/path/to/firmware1.dlp",
+ "firmware2.dlp": "/path/to/firmware/directory/firmware2.dlp",
+ "firmware3.dlp": "/path/to/firmware/directory/firmware3.dlp"})
+
+ def test_determine_file_paths_fail(self):
+ """Ensure determine_file_paths method throws expected exception."""
+ self._set_args({"firmware": ["/path/to/firmware1.dlp", "/path/to/firmware/directory"]})
+ firmware = NetAppESeriesProxyFirmwareUpload()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Drive firmware file does not exist!"):
+ with patch(self.OS_PATH_EXISTS_FUNC, side_effect=[True, False]):
+ firmware.determine_file_paths()
+
+ def test_determine_changes_pass(self):
+ """Determine whether determine_changes returns expected results."""
+ self._set_args({"firmware": ["/path/to/firmware1.dlp", "/path/to/firmware/directory"]})
+ firmware = NetAppESeriesProxyFirmwareUpload()
+ firmware.files = {"firmware1.dlp": "/path/to/firmware1.dlp",
+ "firmware2.dlp": "/path/to/firmware/directory/firmware2.dlp",
+ "firmware3.dlp": "/path/to/firmware/directory/firmware3.dlp"}
+
+ with patch(self.REQUEST_FUNC, return_value=(200, [{"filename": "firmware1.dlp"}, {"filename": "firmware3.dlp"}, {"filename": "firmware4.dlp"}])):
+ firmware.determine_changes()
+
+ self.assertEqual(firmware.add_files, ["firmware2.dlp"])
+ self.assertEqual(firmware.remove_files, ["firmware4.dlp"])
+
+ def test_determine_changes_fail(self):
+ """Ensure class constructor fails when file does not exist."""
+ self._set_args({"firmware": ["/path/to/firmware1.dlp", "/path/to/firmware/directory"]})
+ firmware = NetAppESeriesProxyFirmwareUpload()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to retrieve current firmware file listing."):
+ with patch(self.REQUEST_FUNC, return_value=Exception()):
+ firmware.determine_changes()
+
+ def test_upload_files_pass(self):
+ """Ensure upload_files method successfully passes."""
+ self._set_args({"firmware": ["/path/to/firmware1.dlp", "/path/to/firmware/directory"]})
+ firmware = NetAppESeriesProxyFirmwareUpload()
+ firmware.files = {"firmware1.dlp": "/path/to/firmware1.dlp",
+ "firmware2.dlp": "/path/to/firmware/directory/firmware2.dlp",
+ "firmware3.dlp": "/path/to/firmware/directory/firmware3.dlp"}
+ firmware.add_files = ["firmware1.dlp", "firmware2.dlp"]
+
+ with patch(self.CREATE_MULTIPART_FORMDATA_FUNC, return_value=(None, None)):
+ with patch(self.REQUEST_FUNC, return_value=(200, None)):
+ firmware.upload_files()
+
+ def test_delete_files_pass(self):
+ """Ensure delete_files completes as expected."""
+ self._set_args({"firmware": ["/path/to/firmware1.dlp", "/path/to/firmware/directory"]})
+ firmware = NetAppESeriesProxyFirmwareUpload()
+ firmware.remove_files = ["firmware1.dlp", "firmware2.dlp"]
+
+ with patch(self.REQUEST_FUNC, return_value=(204, None)):
+ firmware.delete_files()
+
+ def test_apply_pass(self):
+ """Ensure that the apply method behaves as expected."""
+ self._set_args({"firmware": ["/path/to/firmware1.dlp", "/path/to/firmware/directory"]})
+ firmware = NetAppESeriesProxyFirmwareUpload()
+ firmware.files = {"firmware1.dlp": "/path/to/firmware1.dlp",
+ "firmware2.dlp": "/path/to/firmware/directory/firmware2.dlp",
+ "firmware3.dlp": "/path/to/firmware/directory/firmware3.dlp"}
+ firmware.module.check_mode = True
+ firmware.is_proxy = lambda: True
+ firmware.determine_file_paths = lambda: None
+ firmware.determine_changes = lambda: None
+
+ firmware.add_files = ["firmware1.dlp", "firmware2.dlp"]
+ firmware.remove_files = ["firmware3.dlp", "firmware4.dlp"]
+ with self.assertRaisesRegexp(AnsibleExitJson, r"'changed': True"):
+ firmware.apply()
+
+ firmware.add_files = ["firmware1.dlp", "firmware2.dlp"]
+ firmware.remove_files = []
+ with self.assertRaisesRegexp(AnsibleExitJson, r"'changed': True"):
+ firmware.apply()
+
+ firmware.add_files = []
+ firmware.remove_files = ["firmware3.dlp", "firmware4.dlp"]
+ with self.assertRaisesRegexp(AnsibleExitJson, r"'changed': True"):
+ firmware.apply()
+
+ firmware.add_files = []
+ firmware.remove_files = []
+ with self.assertRaisesRegexp(AnsibleExitJson, r"'changed': False"):
+ firmware.apply()
+
+ def test_apply_fail(self):
+ """Ensure that the apply method fails when not executing against the proxy."""
+ self._set_args({"firmware": ["/path/to/firmware1.dlp", "/path/to/firmware/directory"]})
+ firmware = NetAppESeriesProxyFirmwareUpload()
+ firmware.is_proxy = lambda: False
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"Module can only be executed against SANtricity Web Services Proxy."):
+ firmware.apply()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_proxy_systems.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_proxy_systems.py
new file mode 100644
index 00000000..31e07820
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_proxy_systems.py
@@ -0,0 +1,497 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils import six
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_proxy_systems import NetAppESeriesProxySystems
+from units.compat import mock
+
+
+class StoragePoolTest(ModuleTestCase):
+ REQUIRED_PARAMS = {"api_username": "username",
+ "api_password": "password",
+ "api_url": "http://localhost/devmgr/v2",
+ "validate_certs": "no"}
+
+ REQUEST_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_proxy_systems.NetAppESeriesProxySystems.request"
+ _REQUEST_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_proxy_systems.NetAppESeriesProxySystems._request"
+ TIME_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_proxy_systems.sleep"
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_valid_options_pass(self):
+ """Verify valid options."""
+ options_list = [{"password": "password", "systems": [{"ssid": "10", "serial": "021633035190"},
+ {"addresses": ["192.168.1.100"]},
+ {"serial": "021628016299"}]},
+ {"password": "password", "systems": ["021178889999", "022348016297", "021625436296"]},
+ {"password": "password", "systems": []}, {}]
+
+ for options in options_list:
+ self._set_args(options)
+ systems = NetAppESeriesProxySystems()
+
+ self._set_args(options_list[0])
+ systems = NetAppESeriesProxySystems()
+ self.assertEquals(systems.systems, [
+ {"ssid": "10", "serial": "021633035190", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": [], "embedded_available": None, "accept_certificate": False, "current_info": {}, "changes": {},
+ "updated_required": False, "failed": False, "discovered": False},
+ {"ssid": "192.168.1.100", "serial": "", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.100"], "embedded_available": None, "accept_certificate": False, "current_info": {},
+ "changes": {}, "updated_required": False, "failed": False, "discovered": False},
+ {"ssid": "021628016299", "serial": "021628016299", "password": "password", "password_valid": None, "password_set": None,
+ "stored_password_valid": None, "meta_tags": [], "controller_addresses": [], "embedded_available": None, "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": False}])
+
+ def test_invalid_options_fail(self):
+ """Verify invalid systems option throws expected exception."""
+ self._set_args({"password": "password", "systems": [[]]})
+ with self.assertRaisesRegexp(AnsibleFailJson, "Invalid system! All systems must either be a simple serial number or a dictionary."):
+ systems = NetAppESeriesProxySystems()
+
+ def test_discover_array_pass(self):
+ """Verify the discover_array method."""
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ response = {"discoverProcessRunning": False, "storageSystems": [{"serialNumber": "1", "ipAddresses": ["192.168.1.5", "192.168.1.6"],
+ "supportedManagementPorts": ["https", "symbol"]},
+ {"serialNumber": "2", "ipAddresses": ["192.168.1.15", "192.168.1.16"],
+ "supportedManagementPorts": ["symbol"]},
+ {"serialNumber": "3", "ipAddresses": ["192.168.1.25", "192.168.1.26"],
+ "supportedManagementPorts": ["https", "symbol"]},
+ {"serialNumber": "4", "ipAddresses": ["192.168.1.35", "192.168.1.36"],
+ "supportedManagementPorts": ["symbol"]}]}
+ systems = NetAppESeriesProxySystems()
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(200, {"requestId": "1"}), (200, {"discoverProcessRunning": True}), (200, response)]):
+ systems.discover_array()
+ self.assertEquals(systems.systems, [
+ {"ssid": "1", "serial": "1", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.5", "192.168.1.6"], "embedded_available": True, "accept_certificate": True,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True},
+ {"ssid": "192.168.1.36", "serial": "", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.35", "192.168.1.36"], "embedded_available": False, "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True},
+ {"ssid": "2", "serial": "2", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.15", "192.168.1.16"], "embedded_available": False, "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True}])
+
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24", "add_discovered_systems": True})
+ response = {"discoverProcessRunning": False, "storageSystems": [{"serialNumber": "1", "ipAddresses": ["192.168.1.5", "192.168.1.6"],
+ "supportedManagementPorts": ["https", "symbol"]},
+ {"serialNumber": "2", "ipAddresses": ["192.168.1.15", "192.168.1.16"],
+ "supportedManagementPorts": ["symbol"]},
+ {"serialNumber": "3", "ipAddresses": ["192.168.1.25", "192.168.1.26"],
+ "supportedManagementPorts": ["https", "symbol"]},
+ {"serialNumber": "4", "ipAddresses": ["192.168.1.35", "192.168.1.36"],
+ "supportedManagementPorts": ["symbol"]}]}
+ systems = NetAppESeriesProxySystems()
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(200, {"requestId": "1"}), (200, {"discoverProcessRunning": True}), (200, response)]):
+ systems.discover_array()
+ self.assertEquals(systems.systems, [
+ {"ssid": "1", "serial": "1", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.5", "192.168.1.6"], "embedded_available": True, "accept_certificate": True,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True},
+ {"ssid": "2", "serial": "2", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.15", "192.168.1.16"], "embedded_available": False, "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True},
+ {"ssid": "3", "serial": "3", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.25", "192.168.1.26"], "embedded_available": True, "accept_certificate": True,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True},
+ {"ssid": "4", "serial": "4", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.35", "192.168.1.36"], "embedded_available": False, "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True}])
+
+ def test_discover_array_fail(self):
+ """Verify discover_array method throws expected exceptions."""
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24", "add_discovered_systems": True})
+ systems = NetAppESeriesProxySystems()
+ with self.assertRaisesRegex(AnsibleFailJson, "Failed to initiate array discovery."):
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ systems.discover_array()
+
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24", "add_discovered_systems": True})
+ systems = NetAppESeriesProxySystems()
+ with self.assertRaisesRegex(AnsibleFailJson, "Failed to get the discovery results."):
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(200, {"requestId": "1"}), Exception()]):
+ systems.discover_array()
+
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24", "add_discovered_systems": True})
+ systems = NetAppESeriesProxySystems()
+ with self.assertRaisesRegex(AnsibleFailJson, "Timeout waiting for array discovery process."):
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(200, {"requestId": "1"})] + [(200, {"discoverProcessRunning": True})] * 1000):
+ systems.discover_array()
+
+ def test_update_storage_systems_info_pass(self):
+ """Verify update_storage_systems_info method performs correctly."""
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.systems = [
+ {"ssid": "1", "serial": "1", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.5", "192.168.1.6"], "embedded_available": True, "accept_certificate": True,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True},
+ {"ssid": "192.168.1.36", "serial": "", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.35", "192.168.1.36"], "embedded_available": False, "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True},
+ {"ssid": "2", "serial": "2", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.15", "192.168.1.16"], "embedded_available": False, "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True}]
+
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, [{"id": "1", "passwordStatus": "valid", "metaTags": []},
+ {"id": "5", "passwordStatus": "valid", "metaTags": []}])):
+ systems.update_storage_systems_info()
+ self.assertEquals(systems.systems_to_remove, ["5"])
+ self.assertEquals(systems.systems_to_add, [
+ {"ssid": "192.168.1.36", "serial": "", "password": "password", "password_valid": None, "password_set": None,
+ "stored_password_valid": None, "meta_tags": [], "controller_addresses": ["192.168.1.35", "192.168.1.36"], "embedded_available": False,
+ "accept_certificate": False, "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True},
+ {"ssid": "2", "serial": "2", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.15", "192.168.1.16"], "embedded_available": False, "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True}])
+
+ def test_update_storage_systems_info_fail(self):
+ """Verify update_storage_systems_info throws expected exceptions."""
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.systems = [
+ {"ssid": "1", "serial": "1", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.5", "192.168.1.6"], "embedded_available": True, "accept_certificate": True,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True},
+ {"ssid": "192.168.1.36", "serial": "", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.35", "192.168.1.36"], "embedded_available": False, "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True},
+ {"ssid": "2", "serial": "2", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.15", "192.168.1.16"], "embedded_available": False, "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True}]
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve storage systems."):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ systems.update_storage_systems_info()
+
+ def test_set_password_pass(self):
+ """Verify set_password completes as expected."""
+ system = {"ssid": "1", "serial": "1", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.5", "192.168.1.6"], "embedded_available": True, "accept_certificate": True,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True}
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self._REQUEST_FUNC, return_value=(200, None)):
+ systems.set_password(system)
+ self.assertFalse(system["password_set"])
+
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self._REQUEST_FUNC, return_value=(401, None)):
+ systems.set_password(system)
+ self.assertTrue(system["password_set"])
+
+ def test_set_password_fail(self):
+ """Verify set_password throws expected exceptions."""
+ system = {"ssid": "1", "serial": "1", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.5", "192.168.1.6"], "embedded_available": True, "accept_certificate": True,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True}
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self._REQUEST_FUNC, return_value=Exception()):
+ systems.set_password(system)
+ self.assertTrue(system["failed"])
+
+ system = {"ssid": "1", "serial": "1", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.5", "192.168.1.6"], "embedded_available": True, "accept_certificate": True,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True}
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self._REQUEST_FUNC, side_effect=[(200, None), Exception(), Exception(), Exception()]):
+ systems.set_password(system)
+ self.assertTrue(system["failed"])
+
+ def test_update_system_changes_pass(self):
+ """Verify system changes."""
+ system = {"ssid": "1", "serial": "1", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.5", "192.168.1.6"], "embedded_available": True, "accept_certificate": True,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True}
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.update_system_changes(system)
+ self.assertEquals(system["changes"], {})
+
+ system = {"ssid": "1", "serial": "1", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.5", "192.168.1.6"], "embedded_available": True, "accept_certificate": True,
+ "current_info": {"managementPaths": ["192.168.1.25", "192.168.1.6"], "metaTags": [],
+ "controllers": [{"certificateStatus": "trusted"}, {"certificateStatus": "trusted"}]},
+ "changes": {}, "updated_required": False, "failed": False, "discovered": True}
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.update_system_changes(system)
+ self.assertEquals(system["changes"], {"controllerAddresses": ["192.168.1.5", "192.168.1.6"]})
+
+ system = {"ssid": "1", "serial": "1", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.5", "192.168.1.6"], "embedded_available": True, "accept_certificate": True,
+ "current_info": {"managementPaths": ["192.168.1.5", "192.168.1.6"], "metaTags": [], "ip1": "192.168.1.5", "ip2": "192.168.1.6",
+ "controllers": [{"certificateStatus": "trusted"}, {"certificateStatus": "unknown"}]},
+ "changes": {}, "updated_required": False, "failed": False, "discovered": True}
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.update_system_changes(system)
+ self.assertEquals(system["changes"], {"acceptCertificate": True})
+
+ system = {"ssid": "1", "serial": "1", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.5", "192.168.1.6"], "embedded_available": True, "accept_certificate": True,
+ "current_info": {"managementPaths": ["192.168.1.5", "192.168.1.6"], "metaTags": [{"key": "key", "value": "1"}], "ip1": "192.168.1.5",
+ "ip2": "192.168.1.6",
+ "controllers": [{"certificateStatus": "trusted"}, {"certificateStatus": "trusted"}]},
+ "changes": {}, "updated_required": False, "failed": False, "discovered": True}
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.update_system_changes(system)
+ self.assertEquals(system["changes"], {"removeAllTags": True})
+
+ system = {"ssid": "1", "serial": "1", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [{"key": "key", "value": "1"}], "controller_addresses": ["192.168.1.5", "192.168.1.6"], "embedded_available": True,
+ "accept_certificate": True,
+ "current_info": {"managementPaths": ["192.168.1.5", "192.168.1.6"], "metaTags": [], "ip1": "192.168.1.5", "ip2": "192.168.1.6",
+ "controllers": [{"certificateStatus": "trusted"}, {"certificateStatus": "trusted"}]},
+ "changes": {}, "updated_required": False, "failed": False, "discovered": True}
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.update_system_changes(system)
+ self.assertEquals(system["changes"], {"metaTags": [{"key": "key", "value": "1"}]})
+
+ def test_add_system_pass(self):
+ """Validate add_system method."""
+ system = {"ssid": "1", "serial": "1", "password": "password", "meta_tags": [{"key": "key", "value": "1"}],
+ "controller_addresses": ["192.168.1.5", "192.168.1.6"], "accept_certificate": True}
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.set_password = lambda x: None
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(200, None), (200, None)]):
+ systems.add_system(system)
+
+ system = {"ssid": "1", "serial": "1", "password": "password", "meta_tags": [],
+ "controller_addresses": ["192.168.1.5", "192.168.1.6"], "accept_certificate": False}
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.set_password = lambda x: None
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(200, None), (200, None)]):
+ systems.add_system(system)
+
+ # Test warning situations, tests should still succeed
+ system = {"ssid": "1", "serial": "1", "password": "password", "meta_tags": [{"key": "key", "value": "1"}],
+ "controller_addresses": ["192.168.1.5", "192.168.1.6"], "accept_certificate": True}
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.set_password = lambda x: None
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[Exception(), Exception()]):
+ systems.add_system(system)
+
+ system = {"ssid": "1", "serial": "1", "password": "password", "meta_tags": [{"key": "key", "value": "1"}],
+ "controller_addresses": ["192.168.1.5", "192.168.1.6"], "accept_certificate": True}
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.set_password = lambda x: None
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(200, None), Exception()]):
+ systems.add_system(system)
+
+ def test_update_system_pass(self):
+ """Validate update_system method."""
+ system = {"ssid": "1", "changes": {}}
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.set_password = lambda x: None
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, None)):
+ systems.update_system(system)
+
+ system = {"ssid": "1", "changes": {}}
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.set_password = lambda x: None
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ systems.update_system(system)
+
+ def test_remove_system_pass(self):
+ """Validate remove_system method."""
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.set_password = lambda x: None
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, None)):
+ systems.remove_system("1")
+
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24",
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.set_password = lambda x: None
+ with mock.patch(self.TIME_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ systems.remove_system("1")
+
+ def test_apply_pass(self):
+ """Validate apply method."""
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24", "add_discovered_systems": False,
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.is_embedded = lambda: False
+ systems.discover_array = lambda: None
+ systems.update_storage_systems_info = lambda: None
+ systems.update_system_changes = lambda x: None
+ systems.remove_system = lambda x: None
+ systems.add_system = lambda x: None
+ systems.update_system = lambda x: None
+ systems.systems = [{"ssid": "1", "serial": "1", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.5", "192.168.1.6"], "embedded_available": True, "accept_certificate": True,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True},
+ {"ssid": "192.168.1.36", "serial": "", "password": "password", "password_valid": None, "password_set": None,
+ "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.35", "192.168.1.36"], "embedded_available": False, "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True},
+ {"ssid": "2", "serial": "2", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.15", "192.168.1.16"], "embedded_available": False, "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True}]
+ systems.systems_to_remove = ["5"]
+ systems.systems_to_add = [{"ssid": "192.168.1.36", "serial": "", "password": "password", "password_valid": None, "password_set": None,
+ "stored_password_valid": None, "meta_tags": [], "controller_addresses": ["192.168.1.35", "192.168.1.36"],
+ "embedded_available": False,
+ "accept_certificate": False, "current_info": {}, "changes": {}, "updated_required": False, "failed": False,
+ "discovered": True},
+ {"ssid": "2", "serial": "2", "password": "password", "password_valid": None, "password_set": None,
+ "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.15", "192.168.1.16"], "embedded_available": False,
+ "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True}]
+ systems.systems_to_update = [{"ssid": "192.168.1.36", "serial": "", "password": "password", "password_valid": None, "password_set": None,
+ "stored_password_valid": None, "meta_tags": [], "controller_addresses": ["192.168.1.35", "192.168.1.36"],
+ "embedded_available": False,
+ "accept_certificate": False, "current_info": {}, "changes": {}, "updated_required": False, "failed": False,
+ "discovered": True},
+ {"ssid": "2", "serial": "2", "password": "password", "password_valid": None, "password_set": None,
+ "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.15", "192.168.1.16"], "embedded_available": False,
+ "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True}]
+ with self.assertRaisesRegexp(AnsibleExitJson, "systems added.*?systems updated.*?system removed"):
+ systems.apply()
+
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24", "add_discovered_systems": False,
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.is_embedded = lambda: False
+ systems.discover_array = lambda: None
+ systems.update_storage_systems_info = lambda: None
+ systems.update_system_changes = lambda x: None
+ systems.remove_system = lambda x: None
+ systems.add_system = lambda x: None
+ systems.update_system = lambda x: None
+ systems.systems = [{"ssid": "1", "serial": "1", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.5", "192.168.1.6"], "embedded_available": True, "accept_certificate": True,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True},
+ {"ssid": "192.168.1.36", "serial": "", "password": "password", "password_valid": None, "password_set": None,
+ "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.35", "192.168.1.36"], "embedded_available": False, "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True},
+ {"ssid": "2", "serial": "2", "password": "password", "password_valid": None, "password_set": None, "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.15", "192.168.1.16"], "embedded_available": False, "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True}]
+ systems.systems_to_remove = ["5"]
+ systems.systems_to_add = [{"ssid": "192.168.1.36", "serial": "", "password": "password", "password_valid": None, "password_set": None,
+ "stored_password_valid": None, "meta_tags": [], "controller_addresses": ["192.168.1.35", "192.168.1.36"],
+ "embedded_available": False,
+ "accept_certificate": False, "current_info": {}, "changes": {}, "updated_required": False, "failed": False,
+ "discovered": True},
+ {"ssid": "2", "serial": "2", "password": "password", "password_valid": None, "password_set": None,
+ "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.15", "192.168.1.16"], "embedded_available": False,
+ "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True}]
+ systems.systems_to_update = [{"ssid": "192.168.1.36", "serial": "", "password": "password", "password_valid": None, "password_set": None,
+ "stored_password_valid": None, "meta_tags": [], "controller_addresses": ["192.168.1.35", "192.168.1.36"],
+ "embedded_available": False,
+ "accept_certificate": False, "current_info": {}, "changes": {}, "updated_required": False, "failed": False,
+ "discovered": True},
+ {"ssid": "2", "serial": "2", "password": "password", "password_valid": None, "password_set": None,
+ "stored_password_valid": None,
+ "meta_tags": [], "controller_addresses": ["192.168.1.15", "192.168.1.16"], "embedded_available": False,
+ "accept_certificate": False,
+ "current_info": {}, "changes": {}, "updated_required": False, "failed": False, "discovered": True}]
+ systems.undiscovered_systems = ["5", "6"]
+ with self.assertRaises(AnsibleFailJson):
+ systems.apply()
+
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24", "add_discovered_systems": False,
+ "systems": []})
+ systems = NetAppESeriesProxySystems()
+ systems.is_embedded = lambda: False
+ systems.discover_array = lambda: None
+ systems.update_storage_systems_info = lambda: None
+ systems.update_system_changes = lambda x: None
+ systems.remove_system = lambda x: None
+ systems.add_system = lambda x: None
+ systems.systems = []
+ systems.systems_to_remove = []
+ systems.systems_to_add = []
+ systems.systems_to_update = []
+ with self.assertRaisesRegexp(AnsibleExitJson, "No changes were made."):
+ systems.apply()
+
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24", "add_discovered_systems": False,
+ "systems": []})
+ systems = NetAppESeriesProxySystems()
+ systems.is_embedded = lambda: False
+ systems.discover_array = lambda: None
+ systems.update_storage_systems_info = lambda: None
+ systems.update_system_changes = lambda x: None
+ systems.remove_system = lambda x: None
+ systems.add_system = lambda x: None
+ systems.systems = []
+ systems.systems_to_remove = []
+ systems.systems_to_add = []
+ systems.undiscovered_systems = ["5", "6"]
+ with self.assertRaises(AnsibleFailJson):
+ systems.apply()
+
+ def test_apply_fail(self):
+ """Validate apply method throws expected exceptions."""
+ self._set_args({"password": "password", "subnet_mask": "192.168.1.0/24", "add_discovered_systems": False,
+ "systems": [{"ssid": "1", "serial": "1"}, {"addresses": ["192.168.1.36"]}, {"serial": "2"}, {"serial": "5"}]})
+ systems = NetAppESeriesProxySystems()
+ systems.is_embedded = lambda: True
+ with self.assertRaisesRegexp(AnsibleFailJson, "Cannot add/remove storage systems to SANtricity Web Services Embedded instance."):
+ systems.apply()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_storagepool.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_storagepool.py
new file mode 100644
index 00000000..181e983e
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_storagepool.py
@@ -0,0 +1,715 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_storagepool import NetAppESeriesStoragePool
+from units.compat.mock import patch, PropertyMock
+
+
+class StoragePoolTest(ModuleTestCase):
+ REQUIRED_PARAMS = {"api_username": "username",
+ "api_password": "password",
+ "api_url": "http://localhost/devmgr/v2",
+ "ssid": "1",
+ "validate_certs": "no"}
+
+ STORAGE_POOL_DATA = [{"raidLevel": "raidDiskPool", "volumeGroupRef": "04000000600A098000A4B28D000017805C7BD4D8",
+ "securityType": "capable",
+ "protectionInformationCapabilities": {"protectionInformationCapable": True,
+ "protectionType": "type2Protection"},
+ "volumeGroupData": {"diskPoolData": {"reconstructionReservedDriveCount": 2}},
+ "totalRaidedSpace": "2735894167552", "name": "pool",
+ "id": "04000000600A098000A4B28D000017805C7BD4D8", "driveMediaType": "hdd"}]
+ DRIVES_DATA = [{'available': True, 'currentVolumeGroupRef': '0000000000000000000000000000000000000000',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551ED1FF0000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': False, 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551EB1930000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': False, 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551EAAE30000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': False, 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551ECB1F0000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': False, 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551EB2930000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': False, 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551ECB0B0000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': False, 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551EC6C70000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': False, 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551E9BA70000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': False, 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551ED7CF0000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': False, 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551ECB0F0000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': False, 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551E72870000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': False, 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551E9DBB0000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': False, 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551EAC230000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': False, 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551EA0BB0000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': False, 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551EAC4B0000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': True, 'currentVolumeGroupRef': '0000000000000000000000000000000000000000',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551E7F2B0000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': True, 'currentVolumeGroupRef': '0000000000000000000000000000000000000000',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551EC9270000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': True, 'currentVolumeGroupRef': '0000000000000000000000000000000000000000',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551EC97F0000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': True, 'currentVolumeGroupRef': '0000000000000000000000000000000000000000',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551ECBFF0000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': True, 'currentVolumeGroupRef': '0000000000000000000000000000000000000000',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551E9ED30000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': True, 'currentVolumeGroupRef': '0000000000000000000000000000000000000000',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551EA4CF0000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': True, 'currentVolumeGroupRef': '0000000000000000000000000000000000000000',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551EA29F0000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': True, 'currentVolumeGroupRef': '0000000000000000000000000000000000000000',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551ECDFB0000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': True, 'currentVolumeGroupRef': '0000000000000000000000000000000000000000',
+ 'driveMediaType': 'hdd', 'id': '010000005000C500551E99230000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': True, 'currentVolumeGroupRef': '0000000000000000000000000000000000000000',
+ 'driveMediaType': 'ssd', 'id': '010000005000C500551E9ED31000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': True, 'currentVolumeGroupRef': '0000000000000000000000000000000000000000',
+ 'driveMediaType': 'ssd', 'id': '010000005000C500551EA4CF2000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': True, 'currentVolumeGroupRef': '0000000000000000000000000000000000000000',
+ 'driveMediaType': 'ssd', 'id': '010000005000C500551EA29F3000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': True, 'currentVolumeGroupRef': '0000000000000000000000000000000000000000',
+ 'driveMediaType': 'ssd', 'id': '010000005000C500551ECDFB4000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sas', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'},
+ {'available': True, 'currentVolumeGroupRef': '0000000000000000000000000000000000000000',
+ 'driveMediaType': 'ssd', 'id': '010000005000C500551E99235000000000000000', 'fdeCapable': True,
+ 'hotSpare': False, 'invalidDriveData': False, 'nonRedundantAccess': False, 'pfa': False,
+ 'phyDriveType': 'sata', 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'rawCapacity': '300000000000', 'removed': False, 'status': 'optimal', 'uncertified': False,
+ 'usableCapacity': '299463129088'}]
+ RAID6_CANDIDATE_DRIVES = {"volumeCandidate": [
+ {"raidLevel": "raid6", "trayLossProtection": False, "rawSize": "898389368832", "usableSize": "898388459520",
+ "driveCount": 5, "freeExtentRef": "0000000000000000000000000000000000000000", "driveRefList": {
+ "driveRef": ["010000005000C500551E7F2B0000000000000000", "010000005000C500551EC9270000000000000000",
+ "010000005000C500551EC97F0000000000000000", "010000005000C500551ECBFF0000000000000000",
+ "010000005000C500551E9ED30000000000000000"]}, "candidateSelectionType": "count",
+ "spindleSpeedMatch": True, "spindleSpeed": 10000, "phyDriveType": "sas", "dssPreallocEnabled": False,
+ "securityType": "capable", "drawerLossProtection": False, "driveMediaType": "hdd",
+ "protectionInformationCapable": False,
+ "protectionInformationCapabilities": {"protectionInformationCapable": True,
+ "protectionType": "type2Protection"},
+ "volumeCandidateData": {"type": "traditional", "diskPoolVolumeCandidateData": None},
+ "driveBlockFormat": "allNative", "allocateReservedSpace": False, "securityLevel": "fde"},
+ {"raidLevel": "raid6", "trayLossProtection": False, "rawSize": "1197852491776", "usableSize": "1197851279360",
+ "driveCount": 6, "freeExtentRef": "0000000000000000000000000000000000000000", "driveRefList": {
+ "driveRef": ["010000005000C500551E7F2B0000000000000000", "010000005000C500551EC9270000000000000000",
+ "010000005000C500551EC97F0000000000000000", "010000005000C500551ECBFF0000000000000000",
+ "010000005000C500551E9ED30000000000000000", "010000005000C500551EA4CF0000000000000000"]},
+ "candidateSelectionType": "count", "spindleSpeedMatch": True, "spindleSpeed": 10000, "phyDriveType": "sas",
+ "dssPreallocEnabled": False, "securityType": "capable", "drawerLossProtection": False, "driveMediaType": "hdd",
+ "protectionInformationCapable": False,
+ "protectionInformationCapabilities": {"protectionInformationCapable": True,
+ "protectionType": "type2Protection"},
+ "volumeCandidateData": {"type": "traditional", "diskPoolVolumeCandidateData": None},
+ "driveBlockFormat": "allNative", "allocateReservedSpace": False, "securityLevel": "fde"},
+ {"raidLevel": "raid6", "trayLossProtection": False, "rawSize": "1497315614720", "usableSize": "1497314099200",
+ "driveCount": 7, "freeExtentRef": "0000000000000000000000000000000000000000", "driveRefList": {
+ "driveRef": ["010000005000C500551E7F2B0000000000000000", "010000005000C500551EC9270000000000000000",
+ "010000005000C500551EC97F0000000000000000", "010000005000C500551ECBFF0000000000000000",
+ "010000005000C500551E9ED30000000000000000", "010000005000C500551EA4CF0000000000000000",
+ "010000005000C500551ED1FF0000000000000000"]}, "candidateSelectionType": "count",
+ "spindleSpeedMatch": True, "spindleSpeed": 10000, "phyDriveType": "sas", "dssPreallocEnabled": False,
+ "securityType": "capable", "drawerLossProtection": False, "driveMediaType": "hdd",
+ "protectionInformationCapable": False,
+ "protectionInformationCapabilities": {"protectionInformationCapable": True,
+ "protectionType": "type2Protection"},
+ "volumeCandidateData": {"type": "traditional", "diskPoolVolumeCandidateData": None},
+ "driveBlockFormat": "allNative", "allocateReservedSpace": False, "securityLevel": "fde"},
+ {"raidLevel": "raid6", "trayLossProtection": False, "rawSize": "1796778737664", "usableSize": "1796776919040",
+ "driveCount": 8, "freeExtentRef": "0000000000000000000000000000000000000000", "driveRefList": {
+ "driveRef": ["010000005000C500551E7F2B0000000000000000", "010000005000C500551EC9270000000000000000",
+ "010000005000C500551EC97F0000000000000000", "010000005000C500551ECBFF0000000000000000",
+ "010000005000C500551E9ED30000000000000000", "010000005000C500551EA4CF0000000000000000",
+ "010000005000C500551ED1FF0000000000000000", "010000005000C500551EA29F0000000000000000"]},
+ "candidateSelectionType": "count", "spindleSpeedMatch": True, "spindleSpeed": 10000, "phyDriveType": "sas",
+ "dssPreallocEnabled": False, "securityType": "capable", "drawerLossProtection": False, "driveMediaType": "hdd",
+ "protectionInformationCapable": False,
+ "protectionInformationCapabilities": {"protectionInformationCapable": True,
+ "protectionType": "type2Protection"},
+ "volumeCandidateData": {"type": "traditional", "diskPoolVolumeCandidateData": None},
+ "driveBlockFormat": "allNative", "allocateReservedSpace": False, "securityLevel": "fde"},
+ {"raidLevel": "raid6", "trayLossProtection": False, "rawSize": "2096241860608", "usableSize": "2096239738880",
+ "driveCount": 9, "freeExtentRef": "0000000000000000000000000000000000000000", "driveRefList": {
+ "driveRef": ["010000005000C500551E7F2B0000000000000000", "010000005000C500551EC9270000000000000000",
+ "010000005000C500551EC97F0000000000000000", "010000005000C500551ECBFF0000000000000000",
+ "010000005000C500551E9ED30000000000000000", "010000005000C500551EA4CF0000000000000000",
+ "010000005000C500551ED1FF0000000000000000", "010000005000C500551EA29F0000000000000000",
+ "010000005000C500551ECDFB0000000000000000"]}, "candidateSelectionType": "count",
+ "spindleSpeedMatch": True, "spindleSpeed": 10000, "phyDriveType": "sas", "dssPreallocEnabled": False,
+ "securityType": "capable", "drawerLossProtection": False, "driveMediaType": "hdd",
+ "protectionInformationCapable": False,
+ "protectionInformationCapabilities": {"protectionInformationCapable": True,
+ "protectionType": "type2Protection"},
+ "volumeCandidateData": {"type": "traditional", "diskPoolVolumeCandidateData": None},
+ "driveBlockFormat": "allNative", "allocateReservedSpace": False, "securityLevel": "fde"},
+ {"raidLevel": "raid6", "trayLossProtection": False, "rawSize": "2395704983552", "usableSize": "2395702558720",
+ "driveCount": 10, "freeExtentRef": "0000000000000000000000000000000000000000", "driveRefList": {
+ "driveRef": ["010000005000C500551E7F2B0000000000000000", "010000005000C500551EC9270000000000000000",
+ "010000005000C500551EC97F0000000000000000", "010000005000C500551ECBFF0000000000000000",
+ "010000005000C500551E9ED30000000000000000", "010000005000C500551EA4CF0000000000000000",
+ "010000005000C500551ED1FF0000000000000000", "010000005000C500551EA29F0000000000000000",
+ "010000005000C500551ECDFB0000000000000000", "010000005000C500551E99230000000000000000"]},
+ "candidateSelectionType": "count", "spindleSpeedMatch": True, "spindleSpeed": 10000, "phyDriveType": "sas",
+ "dssPreallocEnabled": False, "securityType": "capable", "drawerLossProtection": False, "driveMediaType": "hdd",
+ "protectionInformationCapable": False,
+ "protectionInformationCapabilities": {"protectionInformationCapable": True,
+ "protectionType": "type2Protection"},
+ "volumeCandidateData": {"type": "traditional", "diskPoolVolumeCandidateData": None},
+ "driveBlockFormat": "allNative", "allocateReservedSpace": False, "securityLevel": "fde"}], "returnCode": "ok"}
+ EXPANSION_DDP_DRIVES_LIST = ["010000005000C500551ED1FF0000000000000000", "010000005000C500551E7F2B0000000000000000",
+ "010000005000C500551EC9270000000000000000", "010000005000C500551EC97F0000000000000000",
+ "010000005000C500551ECBFF0000000000000000", "010000005000C500551E9ED30000000000000000",
+ "010000005000C500551EA4CF0000000000000000", "010000005000C500551EA29F0000000000000000",
+ "010000005000C500551ECDFB0000000000000000", "010000005000C500551E99230000000000000000",
+ "010000005000C500551E9ED31000000000000000", "010000005000C500551EA4CF2000000000000000",
+ "010000005000C500551EA29F3000000000000000", "010000005000C500551ECDFB4000000000000000",
+ "010000005000C500551E99235000000000000000"]
+ EXPANSION_DDP_DRIVE_DATA = {"returnCode": "ok", "candidates": [
+ {"drives": ["010000005000C500551E7F2B0000000000000000"], "trayLossProtection": False, "wastedCapacity": "0",
+ "spindleSpeedMatch": True, "drawerLossProtection": False, "usableCapacity": "299463129088",
+ "driveBlockFormat": "allNative"},
+ {"drives": ["010000005000C500551E7F2B0000000000000000", "010000005000C500551E99230000000000000000"],
+ "trayLossProtection": False, "wastedCapacity": "0", "spindleSpeedMatch": True, "drawerLossProtection": False,
+ "usableCapacity": "598926258176", "driveBlockFormat": "allNative"},
+ {"drives": ["010000005000C500551E7F2B0000000000000000", "010000005000C500551E99230000000000000000",
+ "010000005000C500551E9ED30000000000000000"], "trayLossProtection": False, "wastedCapacity": "0",
+ "spindleSpeedMatch": True, "drawerLossProtection": False, "usableCapacity": "898389387264",
+ "driveBlockFormat": "allNative"},
+ {"drives": ["010000005000C500551E7F2B0000000000000000", "010000005000C500551E99230000000000000000",
+ "010000005000C500551E9ED30000000000000000", "010000005000C500551EA29F0000000000000000"],
+ "trayLossProtection": False, "wastedCapacity": "0", "spindleSpeedMatch": True, "drawerLossProtection": False,
+ "usableCapacity": "1197852516352", "driveBlockFormat": "allNative"},
+ {"drives": ["010000005000C500551E7F2B0000000000000000", "010000005000C500551E99230000000000000000",
+ "010000005000C500551E9ED30000000000000000", "010000005000C500551EA29F0000000000000000",
+ "010000005000C500551EA4CF0000000000000000"], "trayLossProtection": False, "wastedCapacity": "0",
+ "spindleSpeedMatch": True, "drawerLossProtection": False, "usableCapacity": "1497315645440",
+ "driveBlockFormat": "allNative"},
+ {"drives": ["010000005000C500551E7F2B0000000000000000", "010000005000C500551E99230000000000000000",
+ "010000005000C500551E9ED30000000000000000", "010000005000C500551EA29F0000000000000000",
+ "010000005000C500551EA4CF0000000000000000", "010000005000C500551EC9270000000000000000"],
+ "trayLossProtection": False, "wastedCapacity": "0", "spindleSpeedMatch": True, "drawerLossProtection": False,
+ "usableCapacity": "1796778774528", "driveBlockFormat": "allNative"},
+ {"drives": ["010000005000C500551E7F2B0000000000000000", "010000005000C500551E99230000000000000000",
+ "010000005000C500551E9ED30000000000000000", "010000005000C500551EA29F0000000000000000",
+ "010000005000C500551EA4CF0000000000000000", "010000005000C500551EC9270000000000000000",
+ "010000005000C500551EC97F0000000000000000"], "trayLossProtection": False, "wastedCapacity": "0",
+ "spindleSpeedMatch": True, "drawerLossProtection": False, "usableCapacity": "2096241903616",
+ "driveBlockFormat": "allNative"},
+ {"drives": ["010000005000C500551E7F2B0000000000000000", "010000005000C500551E99230000000000000000",
+ "010000005000C500551E9ED30000000000000000", "010000005000C500551EA29F0000000000000000",
+ "010000005000C500551EA4CF0000000000000000", "010000005000C500551EC9270000000000000000",
+ "010000005000C500551EC97F0000000000000000", "010000005000C500551ECBFF0000000000000000"],
+ "trayLossProtection": False, "wastedCapacity": "0", "spindleSpeedMatch": True, "drawerLossProtection": False,
+ "usableCapacity": "2395705032704", "driveBlockFormat": "allNative"},
+ {"drives": ["010000005000C500551E7F2B0000000000000000", "010000005000C500551E99230000000000000000",
+ "010000005000C500551E9ED30000000000000000", "010000005000C500551EA29F0000000000000000",
+ "010000005000C500551EA4CF0000000000000000", "010000005000C500551EC9270000000000000000",
+ "010000005000C500551EC97F0000000000000000", "010000005000C500551ECBFF0000000000000000",
+ "010000005000C500551ECDFB0000000000000000"], "trayLossProtection": False, "wastedCapacity": "0",
+ "spindleSpeedMatch": True, "drawerLossProtection": False, "usableCapacity": "2695168161792",
+ "driveBlockFormat": "allNative"},
+ {"drives": ["010000005000C500551E7F2B0000000000000000", "010000005000C500551E99230000000000000000",
+ "010000005000C500551E9ED30000000000000000", "010000005000C500551EA29F0000000000000000",
+ "010000005000C500551EA4CF0000000000000000", "010000005000C500551EC9270000000000000000",
+ "010000005000C500551EC97F0000000000000000", "010000005000C500551ECBFF0000000000000000",
+ "010000005000C500551ECDFB0000000000000000", "010000005000C500551ED1FF0000000000000000"],
+ "trayLossProtection": False, "wastedCapacity": "0", "spindleSpeedMatch": True, "drawerLossProtection": False,
+ "usableCapacity": "2994631290880", "driveBlockFormat": "allNative"}]}
+
+ REQUEST_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_storagepool.request"
+ NETAPP_REQUEST_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity.NetAppESeriesModule.request"
+ DRIVES_PROPERTY = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_storagepool.NetAppESeriesStoragePool.drives"
+ STORAGE_POOL_PROPERTY = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_storagepool.NetAppESeriesStoragePool.storage_pool"
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def _initialize_dummy_instance(self, alt_args=None):
+ """Initialize a dummy instance of NetAppESeriesStoragePool for the purpose of testing individual methods."""
+ args = {"state": "absent", "name": "storage_pool"}
+ if alt_args:
+ args.update(alt_args)
+ self._set_args(args)
+ return NetAppESeriesStoragePool()
+
+ def test_drives_fail(self):
+ """Verify exception is thrown."""
+
+ with patch(self.NETAPP_REQUEST_FUNC) as netapp_request:
+ netapp_request.return_value = Exception()
+ storagepool = self._initialize_dummy_instance()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to fetch disk drives."):
+ drives = storagepool.drives
+
+ def test_available_drives(self):
+ """Verify all drives returned are available"""
+ with patch(self.DRIVES_PROPERTY, new_callable=PropertyMock) as drives:
+ drives.return_value = self.DRIVES_DATA
+
+ storagepool = self._initialize_dummy_instance()
+ self.assertEqual(storagepool.available_drives,
+ ['010000005000C500551ED1FF0000000000000000', '010000005000C500551E7F2B0000000000000000',
+ '010000005000C500551EC9270000000000000000', '010000005000C500551EC97F0000000000000000',
+ '010000005000C500551ECBFF0000000000000000', '010000005000C500551E9ED30000000000000000',
+ '010000005000C500551EA4CF0000000000000000', '010000005000C500551EA29F0000000000000000',
+ '010000005000C500551ECDFB0000000000000000', '010000005000C500551E99230000000000000000',
+ '010000005000C500551E9ED31000000000000000', '010000005000C500551EA4CF2000000000000000',
+ '010000005000C500551EA29F3000000000000000', '010000005000C500551ECDFB4000000000000000',
+ '010000005000C500551E99235000000000000000'])
+
+ def test_available_drive_types(self):
+ """Verify all drive types are returned in most common first order."""
+ with patch(self.DRIVES_PROPERTY, new_callable=PropertyMock) as drives:
+ drives.return_value = self.DRIVES_DATA
+
+ storagepool = self._initialize_dummy_instance()
+ self.assertEqual(storagepool.available_drive_types[0], "hdd")
+ self.assertEqual(storagepool.available_drive_types[1], "ssd")
+
+ def test_available_drive_interface_types(self):
+ """Verify all interface types are returned in most common first order."""
+ with patch(self.DRIVES_PROPERTY, new_callable=PropertyMock) as drives:
+ drives.return_value = self.DRIVES_DATA
+
+ storagepool = self._initialize_dummy_instance()
+ self.assertEqual(storagepool.available_drive_interface_types[0], "sas")
+ self.assertEqual(storagepool.available_drive_interface_types[1], "sata")
+
+ def test_storage_pool_drives(self):
+ """Verify storage pool drive collection."""
+ with patch(self.DRIVES_PROPERTY, new_callable=PropertyMock) as drives:
+ drives.return_value = self.DRIVES_DATA
+
+ storagepool = self._initialize_dummy_instance(
+ {"state": "present", "name": "pool", "criteria_drive_count": "12", "raid_level": "raidDiskPool"})
+ storagepool.pool_detail = self.STORAGE_POOL_DATA[0]
+ self.assertEqual(storagepool.storage_pool_drives, [
+ {'available': False, 'pfa': False, 'driveMediaType': 'hdd', 'uncertified': False,
+ 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'}, 'fdeCapable': True,
+ 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8', 'invalidDriveData': False,
+ 'nonRedundantAccess': False, 'hotSpare': False, 'status': 'optimal', 'rawCapacity': '300000000000',
+ 'usableCapacity': '299463129088', 'phyDriveType': 'sas', 'removed': False,
+ 'id': '010000005000C500551EB1930000000000000000'},
+ {'available': False, 'pfa': False, 'driveMediaType': 'hdd', 'uncertified': False,
+ 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'}, 'fdeCapable': True,
+ 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8', 'invalidDriveData': False,
+ 'nonRedundantAccess': False, 'hotSpare': False, 'status': 'optimal', 'rawCapacity': '300000000000',
+ 'usableCapacity': '299463129088', 'phyDriveType': 'sas', 'removed': False,
+ 'id': '010000005000C500551EAAE30000000000000000'},
+ {'available': False, 'pfa': False, 'driveMediaType': 'hdd', 'uncertified': False,
+ 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'}, 'fdeCapable': True,
+ 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8', 'invalidDriveData': False,
+ 'nonRedundantAccess': False, 'hotSpare': False, 'status': 'optimal', 'rawCapacity': '300000000000',
+ 'usableCapacity': '299463129088', 'phyDriveType': 'sas', 'removed': False,
+ 'id': '010000005000C500551ECB1F0000000000000000'},
+ {'available': False, 'pfa': False, 'driveMediaType': 'hdd', 'uncertified': False,
+ 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'}, 'fdeCapable': True,
+ 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8', 'invalidDriveData': False,
+ 'nonRedundantAccess': False, 'hotSpare': False, 'status': 'optimal', 'rawCapacity': '300000000000',
+ 'usableCapacity': '299463129088', 'phyDriveType': 'sas', 'removed': False,
+ 'id': '010000005000C500551EB2930000000000000000'},
+ {'available': False, 'pfa': False, 'driveMediaType': 'hdd', 'uncertified': False,
+ 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'}, 'fdeCapable': True,
+ 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8', 'invalidDriveData': False,
+ 'nonRedundantAccess': False, 'hotSpare': False, 'status': 'optimal', 'rawCapacity': '300000000000',
+ 'usableCapacity': '299463129088', 'phyDriveType': 'sas', 'removed': False,
+ 'id': '010000005000C500551ECB0B0000000000000000'},
+ {'available': False, 'pfa': False, 'driveMediaType': 'hdd', 'uncertified': False,
+ 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'}, 'fdeCapable': True,
+ 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8', 'invalidDriveData': False,
+ 'nonRedundantAccess': False, 'hotSpare': False, 'status': 'optimal', 'rawCapacity': '300000000000',
+ 'usableCapacity': '299463129088', 'phyDriveType': 'sas', 'removed': False,
+ 'id': '010000005000C500551EC6C70000000000000000'},
+ {'available': False, 'pfa': False, 'driveMediaType': 'hdd', 'uncertified': False,
+ 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'}, 'fdeCapable': True,
+ 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8', 'invalidDriveData': False,
+ 'nonRedundantAccess': False, 'hotSpare': False, 'status': 'optimal', 'rawCapacity': '300000000000',
+ 'usableCapacity': '299463129088', 'phyDriveType': 'sas', 'removed': False,
+ 'id': '010000005000C500551E9BA70000000000000000'},
+ {'available': False, 'pfa': False, 'driveMediaType': 'hdd', 'uncertified': False,
+ 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'}, 'fdeCapable': True,
+ 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8', 'invalidDriveData': False,
+ 'nonRedundantAccess': False, 'hotSpare': False, 'status': 'optimal', 'rawCapacity': '300000000000',
+ 'usableCapacity': '299463129088', 'phyDriveType': 'sas', 'removed': False,
+ 'id': '010000005000C500551ED7CF0000000000000000'},
+ {'available': False, 'pfa': False, 'driveMediaType': 'hdd', 'uncertified': False,
+ 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'}, 'fdeCapable': True,
+ 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8', 'invalidDriveData': False,
+ 'nonRedundantAccess': False, 'hotSpare': False, 'status': 'optimal', 'rawCapacity': '300000000000',
+ 'usableCapacity': '299463129088', 'phyDriveType': 'sas', 'removed': False,
+ 'id': '010000005000C500551ECB0F0000000000000000'},
+ {'available': False, 'pfa': False, 'driveMediaType': 'hdd', 'uncertified': False,
+ 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'}, 'fdeCapable': True,
+ 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8', 'invalidDriveData': False,
+ 'nonRedundantAccess': False, 'hotSpare': False, 'status': 'optimal', 'rawCapacity': '300000000000',
+ 'usableCapacity': '299463129088', 'phyDriveType': 'sas', 'removed': False,
+ 'id': '010000005000C500551E72870000000000000000'},
+ {'available': False, 'pfa': False, 'driveMediaType': 'hdd', 'uncertified': False,
+ 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'}, 'fdeCapable': True,
+ 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8', 'invalidDriveData': False,
+ 'nonRedundantAccess': False, 'hotSpare': False, 'status': 'optimal', 'rawCapacity': '300000000000',
+ 'usableCapacity': '299463129088', 'phyDriveType': 'sas', 'removed': False,
+ 'id': '010000005000C500551E9DBB0000000000000000'},
+ {'available': False, 'pfa': False, 'driveMediaType': 'hdd', 'uncertified': False,
+ 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'}, 'fdeCapable': True,
+ 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8', 'invalidDriveData': False,
+ 'nonRedundantAccess': False, 'hotSpare': False, 'status': 'optimal', 'rawCapacity': '300000000000',
+ 'usableCapacity': '299463129088', 'phyDriveType': 'sas', 'removed': False,
+ 'id': '010000005000C500551EAC230000000000000000'},
+ {'available': False, 'pfa': False, 'driveMediaType': 'hdd', 'uncertified': False,
+ 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'}, 'fdeCapable': True,
+ 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8', 'invalidDriveData': False,
+ 'nonRedundantAccess': False, 'hotSpare': False, 'status': 'optimal', 'rawCapacity': '300000000000',
+ 'usableCapacity': '299463129088', 'phyDriveType': 'sas', 'removed': False,
+ 'id': '010000005000C500551EA0BB0000000000000000'},
+ {'available': False, 'pfa': False, 'driveMediaType': 'hdd', 'uncertified': False,
+ 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'}, 'fdeCapable': True,
+ 'currentVolumeGroupRef': '04000000600A098000A4B28D000017805C7BD4D8', 'invalidDriveData': False,
+ 'nonRedundantAccess': False, 'hotSpare': False, 'status': 'optimal', 'rawCapacity': '300000000000',
+ 'usableCapacity': '299463129088', 'phyDriveType': 'sas', 'removed': False,
+ 'id': '010000005000C500551EAC4B0000000000000000'}])
+
+ def test_get_ddp_capacity(self):
+ """Evaluate returned capacity from get_ddp_capacity method."""
+ with patch(self.DRIVES_PROPERTY, new_callable=PropertyMock) as drives:
+ drives.return_value = self.DRIVES_DATA
+
+ storagepool = self._initialize_dummy_instance(
+ {"state": "present", "name": "pool", "criteria_drive_count": "12", "raid_level": "raidDiskPool"})
+ storagepool.pool_detail = self.STORAGE_POOL_DATA[0]
+ self.assertAlmostEqual(storagepool.get_ddp_capacity(self.EXPANSION_DDP_DRIVES_LIST), 6038680353645,
+ places=-2) # Allows for python version/architecture computational differences
+
+ def test_get_candidate_drives(self):
+ """Verify correct candidate list is returned."""
+ with patch(self.NETAPP_REQUEST_FUNC) as netapp_request:
+ netapp_request.return_value = (200, self.RAID6_CANDIDATE_DRIVES)
+ with patch(self.DRIVES_PROPERTY, new_callable=PropertyMock) as drives:
+ drives.return_value = self.DRIVES_DATA
+
+ storagepool = self._initialize_dummy_instance(
+ {"state": "present", "name": "raid6_vg", "criteria_drive_count": "6", "raid_level": "raid6"})
+ self.assertEqual(storagepool.get_candidate_drives(),
+ {'candidateSelectionType': 'count', 'driveMediaType': 'hdd',
+ 'protectionInformationCapabilities': {'protectionInformationCapable': True,
+ 'protectionType': 'type2Protection'},
+ 'dssPreallocEnabled': False, 'phyDriveType': 'sas', 'allocateReservedSpace': False,
+ 'trayLossProtection': False, 'raidLevel': 'raid6', 'spindleSpeed': 10000,
+ 'securityType': 'capable', 'securityLevel': 'fde', 'spindleSpeedMatch': True,
+ 'driveBlockFormat': 'allNative', 'protectionInformationCapable': False,
+ 'freeExtentRef': '0000000000000000000000000000000000000000', 'driveCount': 6,
+ 'driveRefList': {'driveRef': ['010000005000C500551E7F2B0000000000000000',
+ '010000005000C500551EC9270000000000000000',
+ '010000005000C500551EC97F0000000000000000',
+ '010000005000C500551ECBFF0000000000000000',
+ '010000005000C500551E9ED30000000000000000',
+ '010000005000C500551EA4CF0000000000000000']},
+ 'rawSize': '1197852491776', 'usableSize': '1197851279360',
+ 'drawerLossProtection': False,
+ 'volumeCandidateData': {'type': 'traditional', 'diskPoolVolumeCandidateData': None}})
+
+ def test_get_expansion_candidate_drives(self):
+ """Verify correct drive list is returned"""
+ with patch(self.NETAPP_REQUEST_FUNC) as netapp_request:
+ netapp_request.return_value = (200, self.EXPANSION_DDP_DRIVE_DATA)
+ with patch(self.DRIVES_PROPERTY, new_callable=PropertyMock) as drives:
+ drives.return_value = self.DRIVES_DATA
+
+ storagepool = self._initialize_dummy_instance(
+ {"state": "present", "name": "pool", "criteria_drive_count": "20", "raid_level": "raidDiskPool"})
+ storagepool.pool_detail = self.STORAGE_POOL_DATA[0]
+ self.assertEqual(storagepool.get_expansion_candidate_drives(), [
+ {'drawerLossProtection': False, 'trayLossProtection': False,
+ 'drives': ['010000005000C500551E7F2B0000000000000000', '010000005000C500551E99230000000000000000',
+ '010000005000C500551E9ED30000000000000000', '010000005000C500551EA29F0000000000000000',
+ '010000005000C500551EA4CF0000000000000000', '010000005000C500551EC9270000000000000000'],
+ 'spindleSpeedMatch': True, 'driveBlockFormat': 'allNative', 'usableCapacity': '1796778774528',
+ 'wastedCapacity': '0'}])
+
+ def test_get_maximum_reserve_drive_count(self):
+ """Ensure maximum reserve drive count is accurately calculated."""
+ with patch(self.NETAPP_REQUEST_FUNC) as netapp_request:
+ netapp_request.return_value = (200, self.EXPANSION_DDP_DRIVE_DATA)
+ with patch(self.DRIVES_PROPERTY, new_callable=PropertyMock) as drives:
+ drives.return_value = self.DRIVES_DATA
+
+ storagepool = self._initialize_dummy_instance(
+ {"state": "present", "name": "pool", "criteria_drive_count": "20", "raid_level": "raidDiskPool"})
+ storagepool.pool_detail = self.STORAGE_POOL_DATA[0]
+ self.assertEqual(storagepool.get_maximum_reserve_drive_count(), 5)
+
+ def test_apply_check_mode_unchange(self):
+ """Verify that the changes are appropriately determined."""
+ # Absent storage pool required to be absent
+ with self.assertRaisesRegexp(AnsibleExitJson, "'changed': False"):
+ with patch(self.DRIVES_PROPERTY, new_callable=PropertyMock) as drives:
+ drives.return_value = self.DRIVES_DATA
+ with patch(self.STORAGE_POOL_PROPERTY, new_callable=PropertyMock) as storage_pool:
+ storage_pool.return_value = {}
+ storagepool = self._initialize_dummy_instance(
+ {"state": "absent", "name": "not-a-pool", "erase_secured_drives": False,
+ "criteria_drive_count": "14", "raid_level": "raidDiskPool"})
+ storagepool.module.check_mode = True
+ storagepool.is_drive_count_valid = lambda x: True
+ storagepool.apply()
+
+ # Present storage pool with no changes
+ with self.assertRaisesRegexp(AnsibleExitJson, "'changed': False"):
+ with patch(self.DRIVES_PROPERTY, new_callable=PropertyMock) as drives:
+ drives.return_value = self.DRIVES_DATA
+ with patch(self.STORAGE_POOL_PROPERTY, new_callable=PropertyMock) as storage_pool:
+ storage_pool.return_value = self.STORAGE_POOL_DATA[0]
+ storagepool = self._initialize_dummy_instance(
+ {"state": "present", "name": "pool", "erase_secured_drives": False,
+ "criteria_drive_count": "14", "raid_level": "raidDiskPool"})
+ storagepool.module.check_mode = True
+ storagepool.is_drive_count_valid = lambda x: True
+ storagepool.apply()
+
+ def test_apply_check_mode_change(self):
+ """Verify that the changes are appropriately determined."""
+ # Remove absent storage pool
+ with self.assertRaisesRegexp(AnsibleExitJson, "'changed': True"):
+ with patch(self.DRIVES_PROPERTY, new_callable=PropertyMock) as drives:
+ drives.return_value = self.DRIVES_DATA
+ with patch(self.STORAGE_POOL_PROPERTY, new_callable=PropertyMock) as storage_pool:
+ storage_pool.return_value = self.STORAGE_POOL_DATA[0]
+ storagepool = self._initialize_dummy_instance(
+ {"state": "absent", "name": "pool", "erase_secured_drives": False, "criteria_drive_count": "14",
+ "raid_level": "raidDiskPool"})
+ storagepool.module.check_mode = True
+ storagepool.is_drive_count_valid = lambda x: True
+ storagepool.apply()
+
+ # Expand present storage pool
+ with self.assertRaisesRegexp(AnsibleExitJson, "'changed': True"):
+ with patch(self.DRIVES_PROPERTY, new_callable=PropertyMock) as drives:
+ drives.return_value = self.DRIVES_DATA
+ with patch(self.STORAGE_POOL_PROPERTY, new_callable=PropertyMock) as storage_pool:
+ storage_pool.return_value = self.STORAGE_POOL_DATA[0]
+ storagepool = self._initialize_dummy_instance(
+ {"state": "present", "name": "pool", "erase_secured_drives": False,
+ "criteria_drive_count": "15", "raid_level": "raidDiskPool"})
+ storagepool.module.check_mode = True
+ storagepool.is_drive_count_valid = lambda x: True
+ storagepool.expand_storage_pool = lambda check_mode: (True, 100)
+ storagepool.migrate_raid_level = lambda check_mode: False
+ storagepool.secure_storage_pool = lambda check_mode: False
+ storagepool.set_reserve_drive_count = lambda check_mode: False
+ storagepool.apply()
+
+ # Migrate present storage pool raid level
+ with self.assertRaisesRegexp(AnsibleExitJson, "'changed': True"):
+ with patch(self.DRIVES_PROPERTY, new_callable=PropertyMock) as drives:
+ drives.return_value = self.DRIVES_DATA
+ with patch(self.STORAGE_POOL_PROPERTY, new_callable=PropertyMock) as storage_pool:
+ storage_pool.return_value = self.STORAGE_POOL_DATA[0]
+ storagepool = self._initialize_dummy_instance(
+ {"state": "present", "name": "pool", "erase_secured_drives": False,
+ "criteria_drive_count": "15", "raid_level": "raidDiskPool"})
+ storagepool.module.check_mode = True
+ storagepool.is_drive_count_valid = lambda x: True
+ storagepool.expand_storage_pool = lambda check_mode: (False, 0)
+ storagepool.migrate_raid_level = lambda check_mode: True
+ storagepool.secure_storage_pool = lambda check_mode: False
+ storagepool.set_reserve_drive_count = lambda check_mode: False
+ storagepool.apply()
+
+ # Secure present storage pool
+ with self.assertRaisesRegexp(AnsibleExitJson, "'changed': True"):
+ with patch(self.DRIVES_PROPERTY, new_callable=PropertyMock) as drives:
+ drives.return_value = self.DRIVES_DATA
+ with patch(self.STORAGE_POOL_PROPERTY, new_callable=PropertyMock) as storage_pool:
+ storage_pool.return_value = self.STORAGE_POOL_DATA[0]
+ storagepool = self._initialize_dummy_instance(
+ {"state": "present", "name": "pool", "erase_secured_drives": False,
+ "criteria_drive_count": "15", "raid_level": "raidDiskPool"})
+ storagepool.module.check_mode = True
+ storagepool.is_drive_count_valid = lambda x: True
+ storagepool.expand_storage_pool = lambda check_mode: (False, 0)
+ storagepool.migrate_raid_level = lambda check_mode: False
+ storagepool.secure_storage_pool = lambda check_mode: True
+ storagepool.set_reserve_drive_count = lambda check_mode: False
+ storagepool.apply()
+
+ # Change present storage pool reserve drive count
+ with self.assertRaisesRegexp(AnsibleExitJson, "'changed': True"):
+ with patch(self.DRIVES_PROPERTY, new_callable=PropertyMock) as drives:
+ drives.return_value = self.DRIVES_DATA
+ with patch(self.STORAGE_POOL_PROPERTY, new_callable=PropertyMock) as storage_pool:
+ storage_pool.return_value = self.STORAGE_POOL_DATA[0]
+ storagepool = self._initialize_dummy_instance(
+ {"state": "present", "name": "pool", "erase_secured_drives": False,
+ "criteria_drive_count": "15", "raid_level": "raidDiskPool"})
+ storagepool.module.check_mode = True
+ storagepool.is_drive_count_valid = lambda x: True
+ storagepool.expand_storage_pool = lambda check_mode: (False, 0)
+ storagepool.migrate_raid_level = lambda check_mode: False
+ storagepool.secure_storage_pool = lambda check_mode: False
+ storagepool.set_reserve_drive_count = lambda check_mode: True
+ storagepool.apply()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_syslog.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_syslog.py
new file mode 100644
index 00000000..b36278bf
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_syslog.py
@@ -0,0 +1,128 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_syslog import NetAppESeriesSyslog
+from units.modules.utils import AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class AsupTest(ModuleTestCase):
+ REQUIRED_PARAMS = {
+ "api_username": "rw",
+ "api_password": "password",
+ "api_url": "http://localhost",
+ }
+ REQ_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_syslog.NetAppESeriesSyslog.request'
+ BASE_REQ_FUNC = 'ansible_collections.netapp_eseries.santricity.plugins.module_utils.santricity.request'
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_test_configuration_fail(self):
+ """Validate test_configuration fails when request exception is thrown."""
+ initial = {"state": "present",
+ "ssid": "1",
+ "address": "192.168.1.1",
+ "port": "514",
+ "protocol": "udp",
+ "components": ["auditLog"]}
+ self._set_args(initial)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ syslog = NetAppESeriesSyslog()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, r"We failed to send test message!"):
+ with mock.patch(self.REQ_FUNC, return_value=Exception()):
+ syslog.test_configuration(self.REQUIRED_PARAMS)
+
+ def test_update_configuration_record_match_pass(self):
+ """Verify existing syslog server record match does not issue update request."""
+ initial = {"state": "present",
+ "ssid": "1",
+ "address": "192.168.1.1",
+ "port": "514",
+ "protocol": "udp",
+ "components": ["auditLog"]}
+ expected = [{"id": "123456",
+ "serverAddress": "192.168.1.1",
+ "port": 514,
+ "protocol": "udp",
+ "components": [{"type": "auditLog"}]}]
+
+ self._set_args(initial)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ syslog = NetAppESeriesSyslog()
+
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, expected), (200, None)]):
+ updated = syslog.update_configuration()
+ self.assertFalse(updated)
+
+ def test_update_configuration_record_partial_match_pass(self):
+ """Verify existing syslog server record partial match results in an update request."""
+ initial = {"state": "present",
+ "ssid": "1",
+ "address": "192.168.1.1",
+ "port": "514",
+ "protocol": "tcp",
+ "components": ["auditLog"]}
+ expected = [{"id": "123456",
+ "serverAddress": "192.168.1.1",
+ "port": 514,
+ "protocol": "udp",
+ "components": [{"type": "auditLog"}]}]
+
+ self._set_args(initial)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ syslog = NetAppESeriesSyslog()
+
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, expected), (200, None)]):
+ updated = syslog.update_configuration()
+ self.assertTrue(updated)
+
+ def test_update_configuration_record_no_match_pass(self):
+ """Verify existing syslog server record partial match results in an update request."""
+ initial = {"state": "present",
+ "ssid": "1",
+ "address": "192.168.1.1",
+ "port": "514",
+ "protocol": "tcp",
+ "components": ["auditLog"]}
+ expected = [{"id": "123456",
+ "serverAddress": "192.168.1.100",
+ "port": 514,
+ "protocol": "udp",
+ "components": [{"type": "auditLog"}]}]
+
+ self._set_args(initial)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ syslog = NetAppESeriesSyslog()
+
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, expected), (200, dict(id=1234))]):
+ updated = syslog.update_configuration()
+ self.assertTrue(updated)
+
+ def test_update_configuration_record_no_match_defaults_pass(self):
+ """Verify existing syslog server record partial match results in an update request."""
+ initial = {"state": "present",
+ "ssid": "1",
+ "address": "192.168.1.1",
+ "port": "514",
+ "protocol": "tcp",
+ "components": ["auditLog"]}
+ expected = [{"id": "123456",
+ "serverAddress": "192.168.1.100",
+ "port": 514,
+ "protocol": "udp",
+ "components": [{"type": "auditLog"}]}]
+
+ self._set_args(initial)
+ with mock.patch(self.BASE_REQ_FUNC, side_effect=[(200, {"version": "04.00.00.00"}), (200, {"runningAsProxy": False})]):
+ syslog = NetAppESeriesSyslog()
+
+ with mock.patch(self.REQ_FUNC, side_effect=[(200, expected), (200, dict(id=1234))]):
+ updated = syslog.update_configuration()
+ self.assertTrue(updated)
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_volume.py b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_volume.py
new file mode 100644
index 00000000..4bf547b3
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/tests/unit/modules/test_na_santricity_volume.py
@@ -0,0 +1,864 @@
+# (c) 2020, NetApp, Inc
+# BSD-3 Clause (see COPYING or https://opensource.org/licenses/BSD-3-Clause)
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_volume import NetAppESeriesVolume
+from units.modules.utils import AnsibleFailJson, ModuleTestCase, set_module_args
+from units.compat import mock
+
+
+class NetAppESeriesVolumeTest(ModuleTestCase):
+ REQUIRED_PARAMS = {"api_username": "username",
+ "api_password": "password",
+ "api_url": "http://localhost/devmgr/v2",
+ "ssid": "1",
+ "validate_certs": "no"}
+
+ THIN_VOLUME_RESPONSE = [{"capacity": "1288490188800",
+ "volumeRef": "3A000000600A098000A4B28D000010475C405428",
+ "status": "optimal",
+ "protectionType": "type1Protection",
+ "maxVirtualCapacity": "281474976710656",
+ "initialProvisionedCapacity": "4294967296",
+ "currentProvisionedCapacity": "4294967296",
+ "provisionedCapacityQuota": "1305670057984",
+ "growthAlertThreshold": 85,
+ "expansionPolicy": "automatic",
+ "flashCached": False,
+ "metadata": [{"key": "workloadId", "value": "4200000001000000000000000000000000000000"},
+ {"key": "volumeTypeId", "value": "volume"}],
+ "dataAssurance": True,
+ "segmentSize": 131072,
+ "diskPool": True,
+ "listOfMappings": [],
+ "mapped": False,
+ "currentControllerId": "070000000000000000000001",
+ "cacheSettings": {"readCacheEnable": True, "writeCacheEnable": True,
+ "readAheadMultiplier": 0},
+ "name": "thin_volume",
+ "id": "3A000000600A098000A4B28D000010475C405428"}]
+ VOLUME_GET_RESPONSE = [{"offline": False,
+ "raidLevel": "raid6",
+ "capacity": "214748364800",
+ "reconPriority": 1,
+ "segmentSize": 131072,
+ "volumeRef": "02000000600A098000A4B9D100000F095C2F7F31",
+ "status": "optimal",
+ "protectionInformationCapable": False,
+ "protectionType": "type0Protection",
+ "diskPool": True,
+ "flashCached": False,
+ "metadata": [{"key": "workloadId", "value": "4200000002000000000000000000000000000000"},
+ {"key": "volumeTypeId", "value": "Clare"}],
+ "dataAssurance": False,
+ "currentControllerId": "070000000000000000000002",
+ "cacheSettings": {"readCacheEnable": True, "writeCacheEnable": False,
+ "readAheadMultiplier": 0},
+ "thinProvisioned": False,
+ "totalSizeInBytes": "214748364800",
+ "name": "Matthew",
+ "id": "02000000600A098000A4B9D100000F095C2F7F31"},
+ {"offline": False,
+ "raidLevel": "raid6",
+ "capacity": "107374182400",
+ "reconPriority": 1,
+ "segmentSize": 131072,
+ "volumeRef": "02000000600A098000A4B28D00000FBE5C2F7F26",
+ "status": "optimal",
+ "protectionInformationCapable": False,
+ "protectionType": "type0Protection",
+ "diskPool": True,
+ "flashCached": False,
+ "metadata": [{"key": "workloadId", "value": "4200000002000000000000000000000000000000"},
+ {"key": "volumeTypeId", "value": "Samantha"}],
+ "dataAssurance": False,
+ "currentControllerId": "070000000000000000000001",
+ "cacheSettings": {"readCacheEnable": True, "writeCacheEnable": False,
+ "readAheadMultiplier": 0},
+ "thinProvisioned": False,
+ "totalSizeInBytes": "107374182400",
+ "name": "Samantha",
+ "id": "02000000600A098000A4B28D00000FBE5C2F7F26"},
+ {"offline": False,
+ "raidLevel": "raid6",
+ "capacity": "107374182400",
+ "segmentSize": 131072,
+ "volumeRef": "02000000600A098000A4B9D100000F0B5C2F7F40",
+ "status": "optimal",
+ "protectionInformationCapable": False,
+ "protectionType": "type0Protection",
+ "volumeGroupRef": "04000000600A098000A4B9D100000F085C2F7F26",
+ "diskPool": True,
+ "flashCached": False,
+ "metadata": [{"key": "workloadId", "value": "4200000002000000000000000000000000000000"},
+ {"key": "volumeTypeId", "value": "Micah"}],
+ "dataAssurance": False,
+ "currentControllerId": "070000000000000000000002",
+ "cacheSettings": {"readCacheEnable": True, "writeCacheEnable": False,
+ "readAheadMultiplier": 0},
+ "thinProvisioned": False,
+ "totalSizeInBytes": "107374182400",
+ "name": "Micah",
+ "id": "02000000600A098000A4B9D100000F0B5C2F7F40"}]
+ STORAGE_POOL_GET_RESPONSE = [{"offline": False,
+ "raidLevel": "raidDiskPool",
+ "volumeGroupRef": "04000000600A",
+ "securityType": "capable",
+ "protectionInformationCapable": False,
+ "protectionInformationCapabilities": {"protectionInformationCapable": True,
+ "protectionType": "type2Protection"},
+ "volumeGroupData": {"type": "diskPool",
+ "diskPoolData": {"reconstructionReservedDriveCount": 1,
+ "reconstructionReservedAmt": "296889614336",
+ "reconstructionReservedDriveCountCurrent": 1,
+ "poolUtilizationWarningThreshold": 0,
+ "poolUtilizationCriticalThreshold": 85,
+ "poolUtilizationState": "utilizationOptimal",
+ "unusableCapacity": "0",
+ "degradedReconstructPriority": "high",
+ "criticalReconstructPriority": "highest",
+ "backgroundOperationPriority": "low",
+ "allocGranularity": "4294967296"}},
+ "reservedSpaceAllocated": False,
+ "securityLevel": "fde",
+ "usedSpace": "863288426496",
+ "totalRaidedSpace": "2276332666880",
+ "raidStatus": "optimal",
+ "freeSpace": "1413044240384",
+ "drivePhysicalType": "sas",
+ "driveMediaType": "hdd",
+ "diskPool": True,
+ "id": "04000000600A098000A4B9D100000F085C2F7F26",
+ "name": "employee_data_storage_pool"},
+ {"offline": False,
+ "raidLevel": "raid1",
+ "volumeGroupRef": "04000000600A098000A4B28D00000FBD5C2F7F19",
+ "state": "complete",
+ "securityType": "capable",
+ "drawerLossProtection": False,
+ "protectionInformationCapable": False,
+ "protectionInformationCapabilities": {"protectionInformationCapable": True,
+ "protectionType": "type2Protection"},
+ "volumeGroupData": {"type": "unknown", "diskPoolData": None},
+ "reservedSpaceAllocated": False,
+ "securityLevel": "fde",
+ "usedSpace": "322122547200",
+ "totalRaidedSpace": "598926258176",
+ "raidStatus": "optimal",
+ "freeSpace": "276803710976",
+ "drivePhysicalType": "sas",
+ "driveMediaType": "hdd",
+ "diskPool": False,
+ "id": "04000000600A098000A4B28D00000FBD5C2F7F19",
+ "name": "database_storage_pool"}]
+
+ GET_LONG_LIVED_OPERATION_RESPONSE = [
+ {"returnCode": "ok",
+ "longLivedOpsProgress": [
+ {"volAction": "initializing", "reconstruct": None, "volExpansion": None, "volAndCapExpansion": None,
+ "init": {"volumeRef": "02000000600A098000A4B9D1000037315D494C6F", "pending": False, "percentComplete": 1, "timeToCompletion": 20},
+ "format": None, "volCreation": None, "volDeletion": None},
+ {"volAction": "initializing", "reconstruct": None, "volExpansion": None, "volAndCapExpansion": None,
+ "init": {"volumeRef": "02000000600A098000A4B28D00003D2C5D494C87", "pending": False, "percentComplete": 0, "timeToCompletion": 18},
+ "volCreation": None, "volDeletion": None}]},
+ {"returnCode": "ok",
+ "longLivedOpsProgress": [
+ {"volAction": "complete", "reconstruct": None, "volExpansion": None, "volAndCapExpansion": None,
+ "init": {"volumeRef": "02000000600A098000A4B9D1000037315D494C6F", "pending": False, "percentComplete": 1, "timeToCompletion": 20},
+ "format": None, "volCreation": None, "volDeletion": None},
+ {"volAction": "initializing", "reconstruct": None, "volExpansion": None, "volAndCapExpansion": None,
+ "init": {"volumeRef": "02000000600A098000A4B28D00003D2C5D494C87", "pending": False, "percentComplete": 0, "timeToCompletion": 18},
+ "volCreation": None, "volDeletion": None}]},
+ {"returnCode": "ok",
+ "longLivedOpsProgress": [
+ {"volAction": "initializing", "reconstruct": None, "volExpansion": None, "volAndCapExpansion": None,
+ "init": {"volumeRef": "02000000600A098000A4B9D1000037315D494C6F", "pending": False, "percentComplete": 1, "timeToCompletion": 20},
+ "format": None, "volCreation": None, "volDeletion": None},
+ {"volAction": "complete", "reconstruct": None, "volExpansion": None, "volAndCapExpansion": None,
+ "init": {"volumeRef": "02000000600A098000A4B28D00003D2C5D494C87", "pending": False, "percentComplete": 0, "timeToCompletion": 18},
+ "volCreation": None, "volDeletion": None}]},
+ {"returnCode": "ok",
+ "longLivedOpsProgress": [
+ {"volAction": "complete", "reconstruct": None, "volExpansion": None, "volAndCapExpansion": None,
+ "init": {"volumeRef": "02000000600A098000A4B9D1000037315D494C6F", "pending": False, "percentComplete": 1, "timeToCompletion": 20},
+ "format": None, "volCreation": None, "volDeletion": None},
+ {"volAction": "complete", "reconstruct": None, "volExpansion": None, "volAndCapExpansion": None,
+ "init": {"volumeRef": "02000000600A098000A4B28D00003D2C5D494C87", "pending": False, "percentComplete": 0, "timeToCompletion": 18},
+ "volCreation": None, "volDeletion": None}]}]
+
+ WORKLOAD_GET_RESPONSE = [{"id": "4200000001000000000000000000000000000000", "name": "general_workload_1",
+ "workloadAttributes": [{"key": "profileId", "value": "Other_1"}]},
+ {"id": "4200000002000000000000000000000000000000", "name": "employee_data",
+ "workloadAttributes": [{"key": "use", "value": "EmployeeData"},
+ {"key": "location", "value": "ICT"},
+ {"key": "private", "value": "public"},
+ {"key": "profileId", "value": "ansible_workload_1"}]},
+ {"id": "4200000003000000000000000000000000000000", "name": "customer_database",
+ "workloadAttributes": [{"key": "use", "value": "customer_information"},
+ {"key": "location", "value": "global"},
+ {"key": "profileId", "value": "ansible_workload_2"}]},
+ {"id": "4200000004000000000000000000000000000000", "name": "product_database",
+ "workloadAttributes": [{"key": "use", "value": "production_information"},
+ {"key": "security", "value": "private"},
+ {"key": "location", "value": "global"},
+ {"key": "profileId", "value": "ansible_workload_4"}]}]
+
+ REQUEST_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_volume.NetAppESeriesVolume.request"
+ GET_VOLUME_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_volume.NetAppESeriesVolume.get_volume"
+ SLEEP_FUNC = "ansible_collections.netapp_eseries.santricity.plugins.modules.na_santricity_volume.time.sleep"
+
+ def _set_args(self, args=None):
+ module_args = self.REQUIRED_PARAMS.copy()
+ if args is not None:
+ module_args.update(args)
+ set_module_args(module_args)
+
+ def test_module_arguments_pass(self):
+ """Ensure valid arguments successful create a class instance."""
+ arg_sets = [{"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 100, "size_unit": "tb",
+ "thin_provision": True, "thin_volume_repo_size": 64, "thin_volume_max_repo_size": 1000,
+ "thin_volume_growth_alert_threshold": 10},
+ {"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 100, "size_unit": "gb",
+ "thin_provision": True, "thin_volume_repo_size": 64, "thin_volume_max_repo_size": 1024,
+ "thin_volume_growth_alert_threshold": 99},
+ {"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 100, "size_unit": "gb",
+ "thin_provision": True, "thin_volume_repo_size": 64},
+ {"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 100, "size_unit": "kb",
+ "thin_provision": True, "thin_volume_repo_size": 64, "thin_volume_max_repo_size": 67108864}]
+
+ # validate size normalization
+ for arg_set in arg_sets:
+ self._set_args(arg_set)
+ volume_object = NetAppESeriesVolume()
+
+ self.assertEqual(volume_object.size_b, volume_object.convert_to_aligned_bytes(arg_set["size"]))
+ self.assertEqual(volume_object.thin_volume_repo_size_b, volume_object.convert_to_aligned_bytes(arg_set["thin_volume_repo_size"]))
+ self.assertEqual(volume_object.thin_volume_expansion_policy, "automatic")
+ if "thin_volume_max_repo_size" not in arg_set.keys():
+ self.assertEqual(volume_object.thin_volume_max_repo_size_b, volume_object.convert_to_aligned_bytes(arg_set["size"]))
+ else:
+ self.assertEqual(volume_object.thin_volume_max_repo_size_b,
+ volume_object.convert_to_aligned_bytes(arg_set["thin_volume_max_repo_size"]))
+
+ # validate metadata form
+ self._set_args(
+ {"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 10, "workload_name": "workload1",
+ "metadata": {"availability": "public", "security": "low"}})
+ volume_object = NetAppESeriesVolume()
+ for entry in volume_object.metadata:
+ self.assertTrue(entry in [{'value': 'low', 'key': 'security'}, {'value': 'public', 'key': 'availability'}])
+
+ def test_module_arguments_fail(self):
+ """Ensure invalid arguments values do not create a class instance."""
+ arg_sets = [{"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 100, "size_unit": "tb",
+ "thin_provision": True, "thin_volume_repo_size": 260},
+ {"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 10000, "size_unit": "tb",
+ "thin_provision": True, "thin_volume_repo_size": 64, "thin_volume_max_repo_size": 10},
+ {"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 10000, "size_unit": "gb",
+ "thin_provision": True, "thin_volume_repo_size": 64, "thin_volume_max_repo_size": 1000,
+ "thin_volume_growth_alert_threshold": 9},
+ {"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 10000, "size_unit": "gb",
+ "thin_provision": True, "thin_volume_repo_size": 64, "thin_volume_max_repo_size": 1000,
+ "thin_volume_growth_alert_threshold": 100}]
+
+ for arg_set in arg_sets:
+ with self.assertRaises(AnsibleFailJson):
+ self._set_args(arg_set)
+ print(arg_set)
+ volume_object = NetAppESeriesVolume()
+
+ def test_get_volume_pass(self):
+ """Evaluate the get_volume method."""
+ with mock.patch(self.REQUEST_FUNC,
+ side_effect=[(200, self.VOLUME_GET_RESPONSE), (200, self.THIN_VOLUME_RESPONSE)]):
+ self._set_args({"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100})
+ volume_object = NetAppESeriesVolume()
+ self.assertEqual(volume_object.get_volume(),
+ [entry for entry in self.VOLUME_GET_RESPONSE if entry["name"] == "Matthew"][0])
+
+ with mock.patch(self.REQUEST_FUNC,
+ side_effect=[(200, self.VOLUME_GET_RESPONSE), (200, self.THIN_VOLUME_RESPONSE)]):
+ self._set_args({"state": "present", "name": "NotAVolume", "storage_pool_name": "pool", "size": 100})
+ volume_object = NetAppESeriesVolume()
+ self.assertEqual(volume_object.get_volume(), {})
+
+ def test_get_volume_fail(self):
+ """Evaluate the get_volume exception paths."""
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to obtain list of thick volumes."):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ self._set_args({"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100})
+ volume_object = NetAppESeriesVolume()
+ volume_object.get_volume()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to obtain list of thin volumes."):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(200, self.VOLUME_GET_RESPONSE), Exception()]):
+ self._set_args({"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100})
+ volume_object = NetAppESeriesVolume()
+ volume_object.get_volume()
+
+ def tests_wait_for_volume_availability_pass(self):
+ """Ensure wait_for_volume_availability completes as expected."""
+ self._set_args({"state": "present", "name": "NewVolume", "storage_pool_name": "employee_data_storage_pool", "size": 100,
+ "wait_for_initialization": True})
+ volume_object = NetAppESeriesVolume()
+ with mock.patch(self.SLEEP_FUNC, return_value=None):
+ with mock.patch(self.GET_VOLUME_FUNC, side_effect=[False, False, True]):
+ volume_object.wait_for_volume_availability()
+
+ def tests_wait_for_volume_availability_fail(self):
+ """Ensure wait_for_volume_availability throws the expected exceptions."""
+ self._set_args({"state": "present", "name": "NewVolume", "storage_pool_name": "employee_data_storage_pool", "size": 100,
+ "wait_for_initialization": True})
+ volume_object = NetAppESeriesVolume()
+ volume_object.get_volume = lambda: False
+ with self.assertRaisesRegexp(AnsibleFailJson, "Timed out waiting for the volume"):
+ with mock.patch(self.SLEEP_FUNC, return_value=None):
+ volume_object.wait_for_volume_availability()
+
+ def tests_wait_for_volume_action_pass(self):
+ """Ensure wait_for_volume_action completes as expected."""
+ self._set_args({"state": "present", "name": "NewVolume", "storage_pool_name": "employee_data_storage_pool", "size": 100,
+ "wait_for_initialization": True})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"id": "02000000600A098000A4B9D1000037315D494C6F",
+ "storageVolumeRef": "02000000600A098000A4B9D1000037315DXXXXXX"}
+ with mock.patch(self.SLEEP_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(200, self.GET_LONG_LIVED_OPERATION_RESPONSE[0]),
+ (200, self.GET_LONG_LIVED_OPERATION_RESPONSE[1]),
+ (200, self.GET_LONG_LIVED_OPERATION_RESPONSE[2]),
+ (200, self.GET_LONG_LIVED_OPERATION_RESPONSE[3])]):
+ volume_object.wait_for_volume_action()
+
+ self._set_args({"state": "present", "name": "NewVolume", "storage_pool_name": "employee_data_storage_pool", "size": 100,
+ "wait_for_initialization": True})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"id": "02000000600A098000A4B9D1000037315DXXXXXX",
+ "storageVolumeRef": "02000000600A098000A4B9D1000037315D494C6F"}
+ with mock.patch(self.SLEEP_FUNC, return_value=None):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(200, self.GET_LONG_LIVED_OPERATION_RESPONSE[0]),
+ (200, self.GET_LONG_LIVED_OPERATION_RESPONSE[1]),
+ (200, self.GET_LONG_LIVED_OPERATION_RESPONSE[2]),
+ (200, self.GET_LONG_LIVED_OPERATION_RESPONSE[3])]):
+ volume_object.wait_for_volume_action()
+
+ def tests_wait_for_volume_action_fail(self):
+ """Ensure wait_for_volume_action throws the expected exceptions."""
+ self._set_args({"state": "present", "name": "NewVolume", "storage_pool_name": "employee_data_storage_pool", "size": 100,
+ "wait_for_initialization": True})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"id": "02000000600A098000A4B9D1000037315DXXXXXX",
+ "storageVolumeRef": "02000000600A098000A4B9D1000037315D494C6F"}
+ with mock.patch(self.SLEEP_FUNC, return_value=None):
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to get volume expansion progress."):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ volume_object.wait_for_volume_action()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Expansion action failed to complete."):
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, self.GET_LONG_LIVED_OPERATION_RESPONSE[0])):
+ volume_object.wait_for_volume_action(timeout=300)
+
+ def test_get_storage_pool_pass(self):
+ """Evaluate the get_storage_pool method."""
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, self.STORAGE_POOL_GET_RESPONSE)):
+ self._set_args({"state": "present", "name": "NewVolume", "storage_pool_name": "employee_data_storage_pool",
+ "size": 100})
+ volume_object = NetAppESeriesVolume()
+ self.assertEqual(volume_object.get_storage_pool(), [entry for entry in self.STORAGE_POOL_GET_RESPONSE if
+ entry["name"] == "employee_data_storage_pool"][0])
+
+ self._set_args(
+ {"state": "present", "name": "NewVolume", "storage_pool_name": "NotAStoragePool", "size": 100})
+ volume_object = NetAppESeriesVolume()
+ self.assertEqual(volume_object.get_storage_pool(), {})
+
+ def test_get_storage_pool_fail(self):
+ """Evaluate the get_storage_pool exception paths."""
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to obtain list of storage pools."):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ self._set_args({"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100})
+ volume_object = NetAppESeriesVolume()
+ volume_object.get_storage_pool()
+
+ def test_check_storage_pool_sufficiency_pass(self):
+ """Ensure passing logic."""
+ self._set_args({"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100})
+ volume_object = NetAppESeriesVolume()
+ volume_object.pool_detail = [entry for entry in self.STORAGE_POOL_GET_RESPONSE
+ if entry["name"] == "employee_data_storage_pool"][0]
+ volume_object.check_storage_pool_sufficiency()
+
+ def test_check_storage_pool_sufficiency_fail(self):
+ """Validate exceptions are thrown for insufficient storage pool resources."""
+ self._set_args({"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 100, "size_unit": "tb",
+ "thin_provision": True, "thin_volume_repo_size": 64, "thin_volume_max_repo_size": 1000,
+ "thin_volume_growth_alert_threshold": 10})
+ volume_object = NetAppESeriesVolume()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "Requested storage pool"):
+ volume_object.check_storage_pool_sufficiency()
+
+ with self.assertRaisesRegexp(AnsibleFailJson,
+ "Thin provisioned volumes can only be created on raid disk pools."):
+ volume_object.pool_detail = [entry for entry in self.STORAGE_POOL_GET_RESPONSE
+ if entry["name"] == "database_storage_pool"][0]
+ volume_object.volume_detail = {}
+ volume_object.check_storage_pool_sufficiency()
+
+ with self.assertRaisesRegexp(AnsibleFailJson, "requires the storage pool to be DA-compatible."):
+ volume_object.pool_detail = {"diskPool": True,
+ "protectionInformationCapabilities": {"protectionType": "type0Protection",
+ "protectionInformationCapable": False}}
+ volume_object.volume_detail = {}
+ volume_object.data_assurance_enabled = True
+ volume_object.check_storage_pool_sufficiency()
+
+ volume_object.pool_detail = {"diskPool": True,
+ "protectionInformationCapabilities": {"protectionType": "type2Protection",
+ "protectionInformationCapable": True}}
+ volume_object.check_storage_pool_sufficiency()
+
+ self._set_args({"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 100, "size_unit": "tb",
+ "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ with self.assertRaisesRegexp(AnsibleFailJson,
+ "Not enough storage pool free space available for the volume's needs."):
+ volume_object.pool_detail = {"freeSpace": 10, "diskPool": True,
+ "protectionInformationCapabilities": {"protectionType": "type2Protection",
+ "protectionInformationCapable": True}}
+ volume_object.volume_detail = {"totalSizeInBytes": 100}
+ volume_object.data_assurance_enabled = True
+ volume_object.size_b = 1
+ volume_object.check_storage_pool_sufficiency()
+
+ def test_update_workload_tags_pass(self):
+ """Validate updating workload tags."""
+ test_sets = [[{"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100}, False],
+ [{"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100,
+ "workload_name": "employee_data"}, False],
+ [{"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100,
+ "workload_name": "customer_database",
+ "metadata": {"use": "customer_information", "location": "global"}}, False],
+ [{"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100,
+ "workload_name": "customer_database",
+ "metadata": {"use": "customer_information"}}, True],
+ [{"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100,
+ "workload_name": "customer_database",
+ "metadata": {"use": "customer_information", "location": "local"}}, True],
+ [{"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100,
+ "workload_name": "customer_database",
+ "metadata": {"use": "customer_information", "location": "global", "importance": "no"}}, True],
+ [{"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100,
+ "workload_name": "newWorkload",
+ "metadata": {"for_testing": "yes"}}, True],
+ [{"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100,
+ "workload_name": "newWorkload"}, True]]
+
+ for test in test_sets:
+ self._set_args(test[0])
+ volume_object = NetAppESeriesVolume()
+
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(200, self.WORKLOAD_GET_RESPONSE), (200, {"id": 1})]):
+ self.assertEqual(volume_object.update_workload_tags(), test[1])
+
+ def test_update_workload_tags_fail(self):
+ """Validate updating workload tags fails appropriately."""
+ self._set_args({"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100,
+ "workload_name": "employee_data"})
+ volume_object = NetAppESeriesVolume()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve storage array workload tags."):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ volume_object.update_workload_tags()
+
+ self._set_args({"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100,
+ "workload_name": "employee_data", "metadata": {"key": "not-use", "value": "EmployeeData"}})
+ volume_object = NetAppESeriesVolume()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to create new workload tag."):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(200, self.WORKLOAD_GET_RESPONSE), Exception()]):
+ volume_object.update_workload_tags()
+
+ self._set_args({"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100,
+ "workload_name": "employee_data2", "metadata": {"key": "use", "value": "EmployeeData"}})
+ volume_object = NetAppESeriesVolume()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to create new workload tag."):
+ with mock.patch(self.REQUEST_FUNC, side_effect=[(200, self.WORKLOAD_GET_RESPONSE), Exception()]):
+ volume_object.update_workload_tags()
+
+ def test_get_volume_property_changes_pass(self):
+ """Verify correct dictionary is returned"""
+
+ # no property changes
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "ssd_cache_enabled": True,
+ "read_cache_enable": True, "write_cache_enable": True,
+ "read_ahead_enable": True, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"metadata": [],
+ "cacheSettings": {"cwob": False, "readCacheEnable": True, "writeCacheEnable": True,
+ "readAheadMultiplier": 1}, "flashCached": True,
+ "segmentSize": str(128 * 1024)}
+ self.assertEqual(volume_object.get_volume_property_changes(), dict())
+
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "ssd_cache_enabled": True,
+ "read_cache_enable": True, "write_cache_enable": True,
+ "read_ahead_enable": True, "thin_provision": True, "thin_volume_repo_size": 64,
+ "thin_volume_max_repo_size": 1000, "thin_volume_growth_alert_threshold": 90})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"metadata": [],
+ "cacheSettings": {"cwob": False, "readCacheEnable": True, "writeCacheEnable": True,
+ "readAheadMultiplier": 1},
+ "flashCached": True, "growthAlertThreshold": "90",
+ "expansionPolicy": "automatic", "segmentSize": str(128 * 1024)}
+ self.assertEqual(volume_object.get_volume_property_changes(), dict())
+
+ # property changes
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "ssd_cache_enabled": True,
+ "read_cache_enable": True, "write_cache_enable": True,
+ "read_ahead_enable": True, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"metadata": [],
+ "cacheSettings": {"cwob": False, "readCacheEnable": False, "writeCacheEnable": True,
+ "readAheadMultiplier": 1}, "flashCached": True,
+ "segmentSize": str(128 * 1024)}
+ self.assertEqual(volume_object.get_volume_property_changes(),
+ {"metaTags": [], 'cacheSettings': {'readCacheEnable': True, 'writeCacheEnable': True},
+ 'flashCache': True})
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "ssd_cache_enabled": True,
+ "read_cache_enable": True, "write_cache_enable": True, "cache_without_batteries": False,
+ "read_ahead_enable": True, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"metadata": [],
+ "cacheSettings": {"cwob": False, "readCacheEnable": True, "writeCacheEnable": False,
+ "readAheadMultiplier": 1}, "flashCached": True,
+ "segmentSize": str(128 * 1024)}
+ self.assertEqual(volume_object.get_volume_property_changes(),
+ {"metaTags": [], 'cacheSettings': {'readCacheEnable': True, 'writeCacheEnable': True},
+ 'flashCache': True})
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "ssd_cache_enabled": True,
+ "read_cache_enable": True, "write_cache_enable": True, "cache_without_batteries": True,
+ "read_ahead_enable": True, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"metadata": [],
+ "cacheSettings": {"cwob": False, "readCacheEnable": True, "writeCacheEnable": True,
+ "readAheadMultiplier": 1}, "flashCached": False,
+ "segmentSize": str(128 * 1024)}
+ self.assertEqual(volume_object.get_volume_property_changes(),
+ {"metaTags": [], 'cacheSettings': {'readCacheEnable': True, 'writeCacheEnable': True, "cacheWithoutBatteries": True},
+ 'flashCache': True})
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "ssd_cache_enabled": True,
+ "read_cache_enable": True, "write_cache_enable": True, "cache_without_batteries": True,
+ "read_ahead_enable": False, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"metadata": [],
+ "cacheSettings": {"cwob": False, "readCacheEnable": True, "writeCacheEnable": True,
+ "readAheadMultiplier": 1}, "flashCached": False,
+ "segmentSize": str(128 * 1024)}
+ self.assertEqual(volume_object.get_volume_property_changes(), {"metaTags": [],
+ 'cacheSettings': {'readCacheEnable': True,
+ 'writeCacheEnable': True,
+ 'readAheadEnable': False,
+ "cacheWithoutBatteries": True},
+ 'flashCache': True})
+
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "ssd_cache_enabled": True,
+ "read_cache_enable": True, "write_cache_enable": True,
+ "read_ahead_enable": True, "thin_provision": True, "thin_volume_repo_size": 64,
+ "thin_volume_max_repo_size": 1000, "thin_volume_growth_alert_threshold": 90})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"metadata": [],
+ "cacheSettings": {"cwob": True, "readCacheEnable": True, "writeCacheEnable": True,
+ "readAheadMultiplier": 1},
+ "flashCached": True, "growthAlertThreshold": "95",
+ "expansionPolicy": "automatic", "segmentSize": str(128 * 1024)}
+ self.assertEqual(volume_object.get_volume_property_changes(),
+ {"metaTags": [], 'cacheSettings': {'readCacheEnable': True, 'writeCacheEnable': True},
+ 'growthAlertThreshold': 90, 'flashCache': True})
+
+ def test_get_volume_property_changes_fail(self):
+ """Verify correct exception is thrown"""
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "ssd_cache_enabled": True,
+ "read_cache_enable": True, "write_cache_enable": True, "read_ahead_enable": True, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {
+ "cacheSettings": {"cwob": False, "readCacheEnable": True, "writeCacheEnable": True, "readAheadMultiplier": 1},
+ "flashCached": True, "segmentSize": str(512 * 1024)}
+ with self.assertRaisesRegexp(AnsibleFailJson, "Existing volume segment size is"):
+ volume_object.get_volume_property_changes()
+
+ def test_get_expand_volume_changes_pass(self):
+ """Verify expansion changes."""
+ # thick volumes
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"capacity": str(50 * 1024 * 1024 * 1024), "thinProvisioned": False}
+ self.assertEqual(volume_object.get_expand_volume_changes(),
+ {"sizeUnit": "bytes", "expansionSize": 100 * 1024 * 1024 * 1024})
+
+ # thin volumes
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True,
+ "thin_volume_expansion_policy": "automatic", "thin_volume_repo_size": 64,
+ "thin_volume_max_repo_size": 1000, "thin_volume_growth_alert_threshold": 90})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"capacity": str(50 * 1024 * 1024 * 1024), "thinProvisioned": True,
+ "expansionPolicy": "automatic",
+ "provisionedCapacityQuota": str(1000 * 1024 * 1024 * 1024)}
+ self.assertEqual(volume_object.get_expand_volume_changes(),
+ {"sizeUnit": "bytes", "newVirtualSize": 100 * 1024 * 1024 * 1024})
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True,
+ "thin_volume_expansion_policy": "automatic", "thin_volume_repo_size": 64,
+ "thin_volume_max_repo_size": 1000, "thin_volume_growth_alert_threshold": 90})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"capacity": str(100 * 1024 * 1024 * 1024), "thinProvisioned": True,
+ "expansionPolicy": "automatic",
+ "provisionedCapacityQuota": str(500 * 1024 * 1024 * 1024)}
+ self.assertEqual(volume_object.get_expand_volume_changes(),
+ {"sizeUnit": "bytes", "newRepositorySize": 1000 * 1024 * 1024 * 1024})
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True,
+ "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 504, "thin_volume_max_repo_size": 1000,
+ "thin_volume_growth_alert_threshold": 90})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"capacity": str(100 * 1024 * 1024 * 1024), "thinProvisioned": True,
+ "expansionPolicy": "manual",
+ "currentProvisionedCapacity": str(500 * 1024 * 1024 * 1024)}
+ self.assertEqual(volume_object.get_expand_volume_changes(),
+ {"sizeUnit": "bytes", "newRepositorySize": 504 * 1024 * 1024 * 1024})
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True,
+ "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 756, "thin_volume_max_repo_size": 1000,
+ "thin_volume_growth_alert_threshold": 90})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"capacity": str(100 * 1024 * 1024 * 1024), "thinProvisioned": True,
+ "expansionPolicy": "manual",
+ "currentProvisionedCapacity": str(500 * 1024 * 1024 * 1024)}
+ self.assertEqual(volume_object.get_expand_volume_changes(),
+ {"sizeUnit": "bytes", "newRepositorySize": 756 * 1024 * 1024 * 1024})
+
+ def test_get_expand_volume_changes_fail(self):
+ """Verify exceptions are thrown."""
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"capacity": str(1000 * 1024 * 1024 * 1024)}
+ with self.assertRaisesRegexp(AnsibleFailJson, "Reducing the size of volumes is not permitted."):
+ volume_object.get_expand_volume_changes()
+
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True,
+ "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 502, "thin_volume_max_repo_size": 1000,
+ "thin_volume_growth_alert_threshold": 90})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"capacity": str(100 * 1024 * 1024 * 1024), "thinProvisioned": True,
+ "expansionPolicy": "manual",
+ "currentProvisionedCapacity": str(500 * 1024 * 1024 * 1024)}
+ with self.assertRaisesRegexp(AnsibleFailJson, "The thin volume repository increase must be between or equal"):
+ volume_object.get_expand_volume_changes()
+
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True,
+ "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 760, "thin_volume_max_repo_size": 1000,
+ "thin_volume_growth_alert_threshold": 90})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"capacity": str(100 * 1024 * 1024 * 1024), "thinProvisioned": True,
+ "expansionPolicy": "manual",
+ "currentProvisionedCapacity": str(500 * 1024 * 1024 * 1024)}
+ with self.assertRaisesRegexp(AnsibleFailJson, "The thin volume repository increase must be between or equal"):
+ volume_object.get_expand_volume_changes()
+
+ def test_create_volume_pass(self):
+ """Verify volume creation."""
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ volume_object.pool_detail = {"id": "12345"}
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, {})):
+ volume_object.create_volume()
+
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True,
+ "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 760, "thin_volume_max_repo_size": 1000,
+ "thin_volume_growth_alert_threshold": 90})
+ volume_object = NetAppESeriesVolume()
+ volume_object.pool_detail = {"id": "12345"}
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, {})):
+ volume_object.create_volume()
+
+ def test_create_volume_fail(self):
+ """Verify exceptions thrown."""
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ volume_object.pool_detail = {"id": "12345"}
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to create volume."):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ volume_object.create_volume()
+
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True,
+ "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 760, "thin_volume_max_repo_size": 1000,
+ "thin_volume_growth_alert_threshold": 90})
+ volume_object = NetAppESeriesVolume()
+ volume_object.pool_detail = {"id": "12345"}
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to create thin volume."):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ volume_object.create_volume()
+
+ def test_update_volume_properties_pass(self):
+ """verify property update."""
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ volume_object.pool_detail = {"id": "12345"}
+ volume_object.wait_for_volume_availability = lambda: None
+ volume_object.get_volume = lambda: {"id": "12345'"}
+ volume_object.get_volume_property_changes = lambda: {
+ 'cacheSettings': {'readCacheEnable': True, 'writeCacheEnable': True}, 'growthAlertThreshold': 90,
+ 'flashCached': True}
+ volume_object.workload_id = "4200000001000000000000000000000000000000"
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, {})):
+ self.assertTrue(volume_object.update_volume_properties())
+
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True,
+ "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 760, "thin_volume_max_repo_size": 1000,
+ "thin_volume_growth_alert_threshold": 90})
+ volume_object = NetAppESeriesVolume()
+ volume_object.pool_detail = {"id": "12345"}
+ volume_object.wait_for_volume_availability = lambda: None
+ volume_object.get_volume = lambda: {"id": "12345'"}
+ volume_object.get_volume_property_changes = lambda: {
+ 'cacheSettings': {'readCacheEnable': True, 'writeCacheEnable': True}, 'growthAlertThreshold': 90,
+ 'flashCached': True}
+ volume_object.workload_id = "4200000001000000000000000000000000000000"
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, {})):
+ self.assertTrue(volume_object.update_volume_properties())
+
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ volume_object.pool_detail = {"metadata": [{"key": "workloadId", "value": "12345"}]}
+ volume_object.wait_for_volume_availability = lambda: None
+ volume_object.get_volume = lambda: {"id": "12345'"}
+ volume_object.get_volume_property_changes = lambda: {}
+ volume_object.workload_id = "4200000001000000000000000000000000000000"
+ self.assertFalse(volume_object.update_volume_properties())
+
+ def test_update_volume_properties_fail(self):
+ """Verify exceptions are thrown."""
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ volume_object.pool_detail = {"id": "12345"}
+ volume_object.wait_for_volume_availability = lambda: None
+ volume_object.get_volume = lambda: {"id": "12345'"}
+ volume_object.get_volume_property_changes = lambda: {
+ 'cacheSettings': {'readCacheEnable': True, 'writeCacheEnable': True}, 'growthAlertThreshold': 90,
+ 'flashCached': True}
+ volume_object.workload_id = "4200000001000000000000000000000000000000"
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to update volume properties."):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ self.assertTrue(volume_object.update_volume_properties())
+
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True,
+ "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 760, "thin_volume_max_repo_size": 1000,
+ "thin_volume_growth_alert_threshold": 90})
+ volume_object = NetAppESeriesVolume()
+ volume_object.pool_detail = {"id": "12345"}
+ volume_object.wait_for_volume_availability = lambda: None
+ volume_object.get_volume = lambda: {"id": "12345'"}
+ volume_object.get_volume_property_changes = lambda: {
+ 'cacheSettings': {'readCacheEnable': True, 'writeCacheEnable': True}, 'growthAlertThreshold': 90,
+ 'flashCached': True}
+ volume_object.workload_id = "4200000001000000000000000000000000000000"
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to update thin volume properties."):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ self.assertTrue(volume_object.update_volume_properties())
+
+ def test_expand_volume_pass(self):
+ """Verify volume expansion."""
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ volume_object.get_expand_volume_changes = lambda: {"sizeUnit": "bytes",
+ "expansionSize": 100 * 1024 * 1024 * 1024}
+ volume_object.volume_detail = {"id": "12345", "thinProvisioned": True}
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, {})):
+ volume_object.expand_volume()
+
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True,
+ "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 760, "thin_volume_max_repo_size": 1000,
+ "thin_volume_growth_alert_threshold": 90})
+ volume_object = NetAppESeriesVolume()
+ volume_object.get_expand_volume_changes = lambda: {"sizeUnit": "bytes",
+ "expansionSize": 100 * 1024 * 1024 * 1024}
+ volume_object.volume_detail = {"id": "12345", "thinProvisioned": True}
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, {})):
+ volume_object.expand_volume()
+
+ def test_expand_volume_fail(self):
+ """Verify exceptions are thrown."""
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ volume_object.get_expand_volume_changes = lambda: {"sizeUnit": "bytes",
+ "expansionSize": 100 * 1024 * 1024 * 1024}
+ volume_object.volume_detail = {"id": "12345", "thinProvisioned": False}
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to expand volume."):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ volume_object.expand_volume()
+
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True})
+ volume_object = NetAppESeriesVolume()
+ volume_object.get_expand_volume_changes = lambda: {"sizeUnit": "bytes",
+ "expansionSize": 100 * 1024 * 1024 * 1024}
+ volume_object.volume_detail = {"id": "12345", "thinProvisioned": True}
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to expand thin volume."):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ volume_object.expand_volume()
+
+ def test_delete_volume_pass(self):
+ """Verify volume deletion."""
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"id": "12345"}
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, {})):
+ volume_object.delete_volume()
+
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True,
+ "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 760, "thin_volume_max_repo_size": 1000,
+ "thin_volume_growth_alert_threshold": 90})
+ volume_object = NetAppESeriesVolume()
+ volume_object.volume_detail = {"id": "12345"}
+ with mock.patch(self.REQUEST_FUNC, return_value=(200, {})):
+ volume_object.delete_volume()
+
+ def test_delete_volume_fail(self):
+ """Verify exceptions are thrown."""
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False})
+ volume_object = NetAppESeriesVolume()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to delete volume."):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ volume_object.delete_volume()
+
+ self._set_args(
+ {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True})
+ volume_object = NetAppESeriesVolume()
+ with self.assertRaisesRegexp(AnsibleFailJson, "Failed to delete thin volume."):
+ with mock.patch(self.REQUEST_FUNC, return_value=Exception()):
+ volume_object.delete_volume()
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/hubPreCheck.groovy b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/hubPreCheck.groovy
new file mode 100644
index 00000000..43f0efef
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/hubPreCheck.groovy
@@ -0,0 +1,8 @@
+def call(Map optional) {
+ if (optional.docker) {
+ echo "Ensuring that Docker is available on the system."
+ sh """
+ docker --version
+ """
+ }
+}
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/hubScan.groovy b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/hubScan.groovy
new file mode 100644
index 00000000..ca99cee4
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/hubScan.groovy
@@ -0,0 +1,13 @@
+def call(Map optional = [:], String projectName, String projectVersion) {
+ optional.projectName = projectName
+ optional.projectVersion = projectVersion
+ call(optional)
+}
+
+def call(Map optional) {
+ // Correctly set if the scan is intended for production.
+ // hubScan uses the variable 'staging' (defaulting to true), and hubScanProject uses 'productionScan' (defaulting to false).
+ optional.productionScan = !((boolean) optional.staging)
+
+ hubScanProject(optional)
+}
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/hubScanDocker.groovy b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/hubScanDocker.groovy
new file mode 100644
index 00000000..10ced62f
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/hubScanDocker.groovy
@@ -0,0 +1,76 @@
+def call(Map optional, String projectName, String projectVersion, String imageDirectory) {
+ optional.projectName = projectName
+ optional.projectVersion = projectVersion
+ optional.imageDirectory = imageDirectory
+ call(optional)
+}
+
+
+def call(Map optional) {
+
+ String projectVersion = optional.projectVersion
+ String projectName = optional.projectName
+ String imageDirectory = optional.imageDirectory
+ String url = "https://blackduck.eng.netapp.com"
+ String credId = 'hubProductionToken'
+
+ if((boolean) optional.staging){
+ url = "https://blackduck-staging.eng.netapp.com"
+ credId = 'hubStagingToken'
+ }
+
+ BLACKDUCK_SKIP_PHONE_HOME = true
+ withCredentials([string(credentialsId: credId, variable: 'TOKEN')]) {
+ String memory = optional.scannerMemoryMb ?: '8192'
+ String logLevel = optional.logLevel ?: 'INFO'
+ String coreCount = optional.coreCount ?: 1
+ String timeoutMinutes = optional.timeout ?: 60
+
+ sh''' wget -qN http://esgweb.eng.netapp.com/~lorenp/synopsys-detect-6.0.0-air-gap.zip -O /tmp/synopsys-detect.zip
+ unzip -u -d /tmp/tools /tmp/synopsys-detect.zip
+ rm -f /tmp/synopsys-detect.zip
+ '''
+
+ // Create the temporary directory for the scan logs
+ def scanTempDir = sh(returnStdout: true, script: "mktemp --directory \"/tmp/synopsys-detect-${projectName}-${projectVersion}-XXXXXXXXXX\"").trim()
+
+ echo "Initiating Hub Scanning Process on every image in ${imageDirectory}"
+ echo "Sending results to ${url}"
+ echo "Using a logLevel of ${logLevel}"
+ echo "Additional parameters: ${optional}"
+ echo "Running with a timeout value of ${timeoutMinutes} minutes"
+
+ // We need to locate all of the images to scan.
+ sh "find ${imageDirectory} -type f -iname '*.tar'> listFiles"
+ def files = readFile( "listFiles" ).split('\n');
+ try {
+ files.each {
+ def fileName = it.split('/')[-1];
+ timeout(time: "${timeoutMinutes}", unit: 'MINUTES') {
+ // Run a single scan for each image we find, using the filename as a scan identifier
+ sh """
+ java -Xms4096m -Xmx8192m -Xss1024m -jar /tmp/tools/synopsys-detect-6.0.0.jar \
+ --blackduck.url=${url} \
+ --detect.blackduck.signature.scanner.memory="${memory}" \
+ --detect.blackduck.signature.scanner.individual.file.matching="ALL" \
+ --blackduck.api.token=${TOKEN} \
+ --detect.docker.tar=${it} \
+ --detect.parallel.processors=${coreCount} \
+ --detect.code.location.name=${projectName}-${projectVersion}-${fileName} \
+ --detect.project.name=${projectName} \
+ --detect.project.version.name=${projectVersion} \
+ --detect.cleanup=false \
+ --blackduck.trust.cert=true \
+ --detect.output.path=${scanTempDir} \
+ --logging.level.com.synopsys.integration="${logLevel}"
+
+ """
+ }
+ }
+ } finally {
+ dir("${scanTempDir}") {
+ deleteDir()
+ }
+ }
+ }
+}
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/hubScanProject.groovy b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/hubScanProject.groovy
new file mode 100644
index 00000000..b980d7da
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/hubScanProject.groovy
@@ -0,0 +1,123 @@
+/**
+ * Initiate a scan of Synopsys Detect. By default the working directory ('./') is scanned and all detectors are enabled.
+ * Java MUST be installed for this to be successful, and it is suggested to scan in a docker container due to the
+ * detector possibly building the project automatically.
+ *
+ * The 'optional' map supports these fields:
+ * - clearPriorScans: false. Clear previous scans (but doesn't delete them) for the associated project and version on the server.
+ * - coreCount: -1. Scanner parallel processors where -1 uses the number of cores on the system.
+ * - disableDetector: false. Disable the synopsys detector; the detector SHOULD be run but it can result in build issues
+ * and can be disabled.
+ * - logLevel: info. Logging level of synopsys.
+ * - productionScan: false. Set this to true to send scan results to the production blackduck server; staging is used by default.
+ * - scanOpts: [:]. A map of additional hub command-line arguments, or overrides, depending on project needs. for example,
+ * users can control the detector search depth with optional.scanOpts["--detect.detector.search.depth"] = "0".
+ * - scannerMemoryMB: 1024.
+ * - timeout: 60. Maximum scan timeout, in minutes, before failing the build.
+ *
+ * Important implementation notes:
+ * - Java must be installed and in the path.
+ * - A temporary directory, scanTempDir, is created at '/tmp/synopsys-detect-<projectName>-<projectVersion>-XXXXXXXX'.
+ * This temporary is DELETED after the scan to avoid excessive storage usage.
+ * - Synopsys Detect Air Gap (600MB+ zip, 1.5GB+ extracted) is generated at '$scanTempDir/synopsys-detect-air-gap/<synopVersion>'.
+ * This path is deleted along with the temp dir after the scan.
+ * - The files in $scanTempDir/runs/** are archived.
+ * - URLs
+ * - https://synopsys.atlassian.net/wiki/spaces/INTDOCS/pages/622673/Synopsys+Detect+Properties
+ * - https://synopsys.atlassian.net/wiki/spaces/INTDOCS/pages/62423113/Synopsys+Detect
+ *
+ * @param optional map of optional arguments
+ * @param projectName the name of the project
+ * @param projectVersion the version of the project
+ */
+def call(Map optional = [:], String projectName, String projectVersion) {
+ optional.projectName = projectName
+ optional.projectVersion = projectVersion
+ optional.scanOpts = (Map) optional.scanOpts ?: [:]
+ call(optional)
+}
+
+def call(Map optional) {
+ String projectVersion = optional.projectVersion
+ String projectName = optional.projectName
+ String synopsysDetectVersion = optional.synopsysDetectVersion ?: "6.3.0"
+ BLACKDUCK_SKIP_PHONE_HOME = true
+
+ String url = "https://blackduck-staging.eng.netapp.com"
+ String credId = 'hubStagingToken'
+
+ // Use the production server if productionScan is explicitly set to true
+ if (new Boolean(optional.productionScan)) {
+ url = "https://blackduck.eng.netapp.com"
+ credId = 'hubProductionToken'
+ }
+
+ withCredentials([string(credentialsId: credId, variable: 'TOKEN')]) {
+ String timeoutMinutes = optional.timeout ?: 60
+
+ // Create the temporary directory for the scan logs and the extracted hub-detect zip
+ def scanTempDir = sh(returnStdout: true, script: "mktemp --directory \"/tmp/synopsys-detect-${projectName}-${projectVersion}-XXXXXXXXXX\"").trim()
+ def synopsysDir = "${scanTempDir}/synopsys-detect-air-gap/${synopsysDetectVersion}"
+ setupSynopsysDetect(synopsysDetectVersion, synopsysDir: synopsysDir)
+
+ echo "Using temporary directory ${scanTempDir}"
+ echo "Sending results to ${url}"
+ echo "Additional parameters: ${optional}"
+ echo "Using timeout of ${timeoutMinutes} minutes"
+
+ Map m = [:]
+ m["--blackduck.trust.cert"] = "true"
+ m["--blackduck.url"] = url
+ m["--blackduck.api.token"] = TOKEN
+ m["--detect.project.name"] = projectName
+ m["--detect.project.version.name"] = projectVersion
+ m["--detect.code.location.name"] = "${projectName}-${projectVersion}"
+ m["--detect.project.codelocation.unmap"] = optional.clearPriorScans ?: "false"
+ m["--detect.blackduck.signature.scanner.memory"] = optional.scannerMemoryMB ?: "1024"
+ m["--detect.parallel.processors"] = optional.coreCount ?: -1
+ m["--detect.cleanup"] = "false"
+ m["--detect.blackduck.signature.scanner.paths"] = optional.scanDir ?: './'
+ m["--detect.output.path"] = scanTempDir
+ m["--logging.level.com.synopsys.integration"] = optional.logLevel ?: "INFO"
+ m["--detect.detector.search.depth"] = "3"
+ m["--detect.sbt.report.depth"] = "3"
+ m["--detect.blackduck.signature.scanner.exclusion.name.patterns"] = "node_modules,.git,.gradle"
+ m["--detect.blackduck.signature.scanner.exclusion.pattern.search.depth"] = "30"
+ m["--detect.docker.inspector.air.gap.path"] = "${synopsysDir}/packaged-inspectors/docker"
+ m["--detect.nuget.inspector.air.gap.path"] = "${synopsysDir}/packaged-inspectors/nuget"
+ m["--detect.gradle.inspector.air.gap.path"] = "${synopsysDir}/packaged-inspectors/gradle"
+ m["--detect.blackduck.signature.scanner.individual.file.matching"] = "ALL"
+
+ if (optional.cloneVersion) {
+ m["--detect.clone.project.version.name"] = optional.cloneVersion
+ }
+ if ((boolean) optional.disableDetector) {
+ m["--detect.tools.excluded"] = "DETECTOR"
+ }
+
+ m.putAll((Map) optional.scanOpts)
+
+ synopsysArgs = m.collectEntries { k, v -> ["$k=$v"] }.keySet().join(" \\\n ")
+ synopsysExec = "java -Xms1024m -Xmx2048m -jar ${synopsysDir}/synopsys-detect-${synopsysDetectVersion}.jar ${synopsysArgs}"
+ echo "The blackduck scan execute command: \n'${synopsysExec}'"
+
+ try {
+ timeout(time: "${timeoutMinutes}", unit: 'MINUTES') {
+ sh """
+ ${synopsysExec}
+ # Delete any existing docker extractions from this scan to avoid excessive storage use.
+ rm -rf ${scanTempDir}/runs/*/extractions || true
+ mv ${scanTempDir}/runs synopsysRuns
+ """
+
+ // NOTE: Archiving works **ONLY** in the build workspace. All artifacts must be copied to the workspace.
+ // Ignore gz to avoid archiving docker images.
+ archiveArtifacts artifacts: "synopsysRuns/**", excludes: "**/*.gz"
+ }
+ } finally {
+ dir("${scanTempDir}") {
+ deleteDir()
+ }
+ }
+ }
+}
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/setupBlackduckBuildParameters.groovy b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/setupBlackduckBuildParameters.groovy
new file mode 100644
index 00000000..c2e15a08
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/setupBlackduckBuildParameters.groovy
@@ -0,0 +1,16 @@
+def call(Map options = [:]) {
+ String buildArtifactKeepNum = options.buildArtifactKeepNum ?: '15'
+ String buildKeepNum = options.buildKeepNum ?: '30'
+ // The default cron schedule is one build between 1:xx pm - 4:xx pm on Monday
+ String buildCronSchedule = options.buildCronSchedule ?: 'H H(13-16) * * 1'
+
+ properties([
+ parameters([
+ choice(name: 'logLevel', choices: ['WARN', 'INFO', 'DEBUG', 'TRACE'], description: 'Set the logging level. WARN is the default.')
+ ]),
+ buildDiscarder(
+ logRotator(artifactNumToKeepStr: buildArtifactKeepNum, numToKeepStr: buildKeepNum)
+ ),
+ pipelineTriggers([cron(buildCronSchedule)])
+ ])
+}
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/setupBuildParameters.groovy b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/setupBuildParameters.groovy
new file mode 100644
index 00000000..8e049575
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/setupBuildParameters.groovy
@@ -0,0 +1,3 @@
+def call(Map options = [:]) {
+ setupBlackduckBuildParameters(options)
+}
diff --git a/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/setupSynopsysDetect.groovy b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/setupSynopsysDetect.groovy
new file mode 100644
index 00000000..f5eed5c4
--- /dev/null
+++ b/collections-debian-merged/ansible_collections/netapp_eseries/santricity/vars/setupSynopsysDetect.groovy
@@ -0,0 +1,15 @@
+
+def call(Map options = [:], String synopsysDetectVersion) {
+ options.synopsysDir = options.synopsysDir ?: "/tmp/synopsys-detect-air-gap/${synopsysDetectVersion}"
+ if (new File(options.synopsysDir).exists()) {
+ echo "No need to fetch synopsys-${synopsysDetectVersion}, directory exists ${options.synopsysDir}"
+ return
+ }
+
+ sh """
+ wget -qN http://esgweb.eng.netapp.com/~blucas/packages/synopsys-detect-${synopsysDetectVersion}-air-gap.zip -O synopsys-detect.zip
+ mkdir -p ${options.synopsysDir}
+ unzip -q -d ${options.synopsysDir} -u synopsys-detect.zip
+ rm -f synopsys-detect.zip
+ """
+}