summaryrefslogtreecommitdiffstats
path: root/ansible_collections/purestorage/flashblade/plugins/modules
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
commit975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch)
tree89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/purestorage/flashblade/plugins/modules
parentInitial commit. (diff)
downloadansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz
ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/purestorage/flashblade/plugins/modules')
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_ad.py404
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_admin.py137
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_alert.py245
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_apiclient.py250
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_banner.py143
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_bladename.py115
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_bucket.py398
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_bucket_replica.py313
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_certgrp.py249
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_certs.py198
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_connect.py574
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_dns.py175
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_ds.py470
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_dsrole.py213
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_eula.py131
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_fs.py944
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_fs_replica.py308
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_groupquota.py321
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_info.py1548
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_inventory.py279
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_keytabs.py254
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_lag.py315
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_lifecycle.py490
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_messages.py193
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_network.py224
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_ntp.py158
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_phonehome.py124
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_pingtrace.py277
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_policy.py2079
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_proxy.py155
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_ra.py126
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_remote_cred.py243
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_s3acc.py314
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_s3user.py436
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_smtp.py124
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_snap.py379
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_snmp_agent.py210
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_snmp_mgr.py357
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_subnet.py347
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_syslog.py198
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_target.py201
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_timeout.py136
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_tz.py206
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_user.py211
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_userpolicy.py269
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_userquota.py315
-rw-r--r--ansible_collections/purestorage/flashblade/plugins/modules/purefb_virtualhost.py142
47 files changed, 15898 insertions, 0 deletions
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_ad.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_ad.py
new file mode 100644
index 000000000..ccd1f5d92
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_ad.py
@@ -0,0 +1,404 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2021, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_ad
+version_added: '1.6.0'
+short_description: Manage FlashBlade Active Directory Account
+description:
+- Add or delete FlashBlade Active Directory Account
+- FlashBlade allows the creation of one AD computer account, or joining of an
+ existing AD computer account.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - Name of the AD account
+ type: str
+ required: true
+ existing:
+ description:
+ - Does the account I(name) already exist in the AD environment
+ type: bool
+ default: false
+ state:
+ description:
+ - Define whether the AD sccount is deleted or not
+ default: present
+ choices: [ absent, present ]
+ type: str
+ computer:
+ description:
+ - The common name of the computer account to be created in the Active Directory domain.
+ - If not specified, defaults to the name of the Active Directory configuration.
+ type: str
+ domain:
+ description:
+ - The Active Directory domain to join
+ type: str
+ username:
+ description:
+ - A user capable of creating a computer account within the domain
+ type: str
+ password:
+ description:
+ - Password string for I(username)
+ type: str
+ encryption:
+ description:
+ - The encryption types that will be supported for use by clients for Kerberos authentication
+ type: list
+ elements: str
+ choices: [ aes256-sha1, aes128-sha1, arcfour-hmac]
+ default: aes256-sha1
+ join_ou:
+ description:
+ - Location where the Computer account will be created. e.g. OU=Arrays,OU=Storage.
+ - If left empty, defaults to B(CN=Computers).
+ type: str
+ directory_servers:
+ description:
+ - A list of directory servers that will be used for lookups related to user authorization
+ - Accepted server formats are IP address and DNS name
+ - All specified servers must be registered to the domain appropriately in the array
+ configured DNS and will only be communicated with over the secure LDAP (LDAPS) protocol.
+ If not specified, servers are resolved for the domain in DNS
+ - The specified list can have a maximum length of 5. If more are provided only the first
+ 5 are used.
+ type: list
+ elements: str
+ kerberos_servers:
+ description:
+ - A list of key distribution servers to use for Kerberos protocol
+ - Accepted server formats are IP address and DNS name
+ - All specified servers must be registered to the domain appropriately in the array
+ configured DNS. If not specified, servers are resolved for the domain in DNS.
+ - The specified list can have a maximum length of 5. If more are provided only the first
+ 5 are used.
+ type: list
+ elements: str
+ service_principals:
+ description:
+ - A list of either FQDNs or SPNs for registering services with the domain.
+ - If not specified B(Computer Name.Domain) is used
+ type: list
+ elements: str
+ service:
+ description:
+ - Service protocol for Active Directory principals
+ - Refer to FlashBlade User Guide for more details
+ type: list
+ elements: str
+ choices: ['nfs', 'cifs', 'HOST']
+ default: nfs
+ local_only:
+ description:
+ - Do a local-only delete of an active directory account
+ type: bool
+ default: false
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Create new AD account
+ purestorage.flashblade.purefb_ad:
+ name: ad_account
+ computer: FLASHBLADE
+ domain: acme.com
+ username: Administrator
+ password: Password
+ join_ou: "CN=FakeOU"
+ encryption:
+ - aes128-cts-hmac-sha1-96
+ - aes256-cts-hmac-sha1-96
+ kerberos_servers:
+ - kdc.acme.com
+ directory_servers:
+ - ldap.acme.com
+ service_principals:
+ - vip1.flashblade.acme.com
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Connect to existing AD account
+ purestorage.flashblade.purefb_ad:
+ name: ad_account
+ computer: FLASHBLADE
+ domain: acme.com
+ username: Administrator
+ password: Password
+ existing: true
+ kerberos_servers:
+ - kdc.acme.com
+ directory_servers:
+ - ldap.acme.com
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Update existing AD account
+ purestorage.flashblade.purefb_ad:
+ name: ad_account
+ encryption:
+ - aes256-cts-hmac-sha1-96
+ kerberos_servers:
+ - kdc.acme.com
+ directory_servers:
+ - ldap.acme.com
+ service_principals:
+ - vip1.flashblade.acme.com
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Delete local AD account
+ purestorage.flashblade.purefb_ad:
+ name: ad_account
+ local_only: true
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Fully delete AD account
+ purestorage.flashblade.purefb_ad:
+ name: ad_account
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURESTORAGE = True
+try:
+ from pypureclient.flashblade import ActiveDirectoryPost, ActiveDirectoryPatch
+except ImportError:
+ HAS_PURESTORAGE = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_system,
+ purefb_argument_spec,
+)
+
+MIN_REQUIRED_API_VERSION = "2.0"
+
+
+def delete_account(module, blade):
+ """Delete Active directory Account"""
+ changed = True
+ if not module.check_mode:
+ res = blade.delete_active_directory(
+ names=[module.params["name"]], local_only=module.params["local_only"]
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to delete AD Account {0}".format(module.params["name"])
+ )
+ module.exit_json(changed=changed)
+
+
+def create_account(module, blade):
+ """Create Active Directory Account"""
+ changed = True
+ if not module.params["existing"]:
+ ad_config = ActiveDirectoryPost(
+ computer_name=module.params["computer"],
+ directory_servers=module.params["directory_servers"],
+ kerberos_servers=module.params["kerberos_servers"],
+ domain=module.params["domain"],
+ encryption_types=module.params["encryption"],
+ fqdns=module.params["service_principals"],
+ join_ou=module.params["join_ou"],
+ user=module.params["username"],
+ password=module.params["password"],
+ )
+ if not module.check_mode:
+ res = blade.post_active_directory(
+ names=[module.params["name"]], active_directory=ad_config
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to add Active Directory Account {0}. Error: {1}".format(
+ module.params["name"], res.errors[0].message
+ )
+ )
+ else:
+ ad_config = ActiveDirectoryPost(
+ computer_name=module.params["computer"],
+ directory_servers=module.params["directory_servers"],
+ kerberos_servers=module.params["kerberos_servers"],
+ domain=module.params["domain"],
+ encryption_types=module.params["encryption"],
+ user=module.params["username"],
+ password=module.params["password"],
+ )
+ if not module.check_mode:
+ res = blade.post_active_directory(
+ names=[module.params["name"]],
+ active_directory=ad_config,
+ join_existing_account=True,
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to add Active Directory Account {0}. Error: {1}".format(
+ module.params["name"], res.errors[0].message
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def update_account(module, blade):
+ """Update Active Directory Account"""
+ changed = False
+ mod_ad = False
+ current_ad = list(blade.get_active_directory(names=[module.params["name"]]).items)[
+ 0
+ ]
+ attr = {}
+ if (
+ module.params["join_ou"] != current_ad.join_ou
+ and module.params["encryption"].sort() != current_ad.encryption_types.sort()
+ ):
+ module.fail_json(msg="Cannot make changes to OU when changing encryption types")
+ if module.params["directory_servers"]:
+ if current_ad.directory_servers:
+ if set(module.params["directory_servers"]) != set(
+ current_ad.directory_servers
+ ):
+ attr["directory_servers"] = module.params["directory_servers"]
+ mod_ad = True
+ if module.params["kerberos_servers"]:
+ if current_ad.kerberos_servers:
+ if set(module.params["kerberos_servers"]) != set(
+ current_ad.kerberos_servers
+ ):
+ attr["kerberos_servers"] = module.params["kerberos_servers"]
+ mod_ad = True
+ if module.params["join_ou"] != current_ad.join_ou:
+ attr["join_ou"] = module.params["join_ou"]
+ mod_ad = True
+ if set(module.params["encryption"]) != set(current_ad.encryption_types):
+ attr["encryption_types"] = module.params["encryption"]
+ mod_ad = True
+ if module.params["service_principals"]:
+ if current_ad.service_principal_names:
+ full_spns = []
+ for spn in range(0, len(module.params["service_principals"])):
+ for service in range(0, len(module.params["service"])):
+ full_spns.append(
+ module.params["service"][service]
+ + "/"
+ + module.params["service_principals"][spn]
+ )
+ if set(current_ad.service_principal_names) != set(full_spns):
+ attr["service_principal_names"] = full_spns
+ mod_ad = True
+ if mod_ad:
+ changed = True
+ if not module.check_mode:
+ ad_attr = ActiveDirectoryPatch(**attr)
+ res = blade.patch_active_directory(
+ names=[module.params["name"]], active_directory=ad_attr
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to update Active Directory Account {0}. Error: {1}".format(
+ module.params["name"], res.errors[0].message
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ username=dict(type="str"),
+ password=dict(type="str", no_log=True),
+ name=dict(type="str", required=True),
+ service=dict(
+ type="list",
+ elements="str",
+ default="nfs",
+ choices=["nfs", "cifs", "HOST"],
+ ),
+ computer=dict(type="str"),
+ existing=dict(type="bool", default=False),
+ local_only=dict(type="bool", default=False),
+ domain=dict(type="str"),
+ join_ou=dict(type="str"),
+ directory_servers=dict(type="list", elements="str"),
+ kerberos_servers=dict(type="list", elements="str"),
+ service_principals=dict(type="list", elements="str"),
+ encryption=dict(
+ type="list",
+ elements="str",
+ choices=["aes256-sha1", "aes128-sha1", "arcfour-hmac"],
+ default=["aes256-sha1"],
+ ),
+ )
+ )
+
+ required_if = [["state", "present", ["username", "password", "domain"]]]
+
+ module = AnsibleModule(
+ argument_spec, required_if=required_if, supports_check_mode=True
+ )
+
+ if not HAS_PURESTORAGE:
+ module.fail_json(msg="py-pure-client sdk is required for this module")
+
+ blade = get_system(module)
+ api_version = list(blade.get_versions().items)
+ module.params["encryption"] = [
+ crypt.replace("aes256-sha1", "aes256-cts-hmac-sha1-96").replace(
+ "aes128-sha1", "aes128-cts-hmac-sha1-96"
+ )
+ for crypt in module.params["encryption"]
+ ]
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(
+ msg="FlashBlade REST version not supported. "
+ "Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
+ )
+ state = module.params["state"]
+ exists = bool(blade.get_active_directory().total_item_count == 1)
+
+ # TODO: Check SMB mode.
+ # If mode is SMB adapter only allow nfs
+ # Only allow cifs or HOST is SMB mode is native
+
+ if not module.params["computer"]:
+ module.params["computer"] = module.params["name"].replace("_", "-")
+ if module.params["kerberos_servers"]:
+ module.params["kerberos_servers"] = module.params["kerberos_servers"][0:5]
+ if module.params["directory_servers"]:
+ module.params["directory_servers"] = module.params["directory_servers"][0:5]
+
+ if not exists and state == "present":
+ create_account(module, blade)
+ elif exists and state == "present":
+ update_account(module, blade)
+ elif exists and state == "absent":
+ delete_account(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_admin.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_admin.py
new file mode 100644
index 000000000..3ee87bca1
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_admin.py
@@ -0,0 +1,137 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2021, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_admin
+version_added: '1.8.0'
+short_description: Configure Pure Storage FlashBlade Global Admin settings
+description:
+- Set global admin settings for the FlashBlade
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ max_login:
+ description:
+ - Maximum number of failed logins before account is locked
+ type: int
+ min_password:
+ description:
+ - Minimum user password length
+ - Range between 1 and 100
+ default: 1
+ type: int
+ lockout:
+ description:
+ - Account lockout duration, in seconds, after max_login exceeded
+ - Range between 1 second and 90 days (7776000 seconds)
+ type: int
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Set global login parameters
+ purestorage.flashblade.purefb_admin:
+ max_login: 5
+ min_password: 10
+ lockout: 300
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURESTORAGE = True
+try:
+ from pypureclient.flashblade import AdminSetting
+except ImportError:
+ HAS_PURESTORAGE = False
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_system,
+ purefb_argument_spec,
+)
+
+MIN_API_VERSION = "2.3"
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ max_login=dict(type="int"),
+ min_password=dict(type="int", default=1, no_log=False),
+ lockout=dict(type="int"),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ if not HAS_PURESTORAGE:
+ module.fail_json(msg="py-pure-client sdk is required for this module")
+ if module.params["lockout"] and not 1 <= module.params["lockout"] <= 7776000:
+ module.fail_json(msg="Lockout must be between 1 and 7776000 seconds")
+ if not 1 <= module.params["min_password"] <= 100:
+ module.fail_json(msg="Minimum password length must be between 1 and 100")
+ blade = get_system(module)
+ api_version = list(blade.get_versions().items)
+ changed = False
+ if MIN_API_VERSION in api_version:
+ current_settings = list(blade.get_admins_settings().items)[0]
+ lockout = getattr(current_settings, "lockout_duration", None)
+ max_login = getattr(current_settings, "max_login_attempts", None)
+ min_password = getattr(current_settings, "min_password_length", 1)
+ if min_password != module.params["min_password"]:
+ changed = True
+ min_password = module.params["min_password"]
+ if lockout and lockout != module.params["lockout"] * 1000:
+ changed = True
+ lockout = module.params["lockout"] * 1000
+ elif not lockout and module.params["lockout"]:
+ changed = True
+ lockout = module.params["lockout"] * 1000
+ if max_login and max_login != module.params["max_login"]:
+ changed = True
+ max_login = module.params["max_login"]
+ elif not max_login and module.params["max_login"]:
+ changed = True
+ max_login = module.params["max_login"]
+
+ if changed and not module.check_mode:
+ admin = AdminSetting(
+ min_password_length=min_password,
+ max_login_attempts=max_login,
+ lockout_duration=lockout,
+ )
+
+ res = blade.patch_admins_settings(admin_setting=admin)
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to change Global Admin settings. Error: {0}".format(
+ res.errors[0].message
+ )
+ )
+ else:
+ module.fail_json(msg="Purity version does not support Global Admin settings")
+ module.exit_json(changed=changed)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_alert.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_alert.py
new file mode 100644
index 000000000..406fe1c39
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_alert.py
@@ -0,0 +1,245 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# 2018, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_alert
+version_added: '1.0.0'
+short_description: Configure Pure Storage FlashBlade alert email settings
+description:
+- Configure alert email configuration for Pure Storage FlashArrays.
+- Add or delete an individual syslog server to the existing
+ list of serves.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ type: str
+ description:
+ - Create or delete alert email
+ default: present
+ choices: [ absent, present ]
+ address:
+ type: str
+ description:
+ - Email address (valid format required)
+ required: true
+ enabled:
+ type: bool
+ default: true
+ description:
+ - Set specified email address to be enabled or disabled
+ severity:
+ type: str
+ description:
+ - The minimum severity that an alert must have in order for
+ emails to be sent to the array's alert watchers
+ default: info
+ choices: [ info, warning, critical ]
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Add new email recipient and enable, or enable existing email
+ purestorage.flashblade.purefb_alert:
+ address: "user@domain.com"
+ enabled: true
+ state: present
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+- name: Delete existing email recipient
+ purestorage.flashblade.purefb_alert:
+ state: absent
+ address: "user@domain.com"
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+"""
+
+RETURN = r"""
+"""
+
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import AlertWatcher
+except ImportError:
+ HAS_PURITY_FB = False
+
+
+import re
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.9"
+
+
+def create_alert(module, blade):
+ """Create Alert Email"""
+ changed = True
+ if not module.check_mode:
+ api_version = blade.api_version.list_versions().versions
+ if MIN_REQUIRED_API_VERSION in api_version:
+ watcher_settings = AlertWatcher(
+ minimum_notification_severity=module.params["severity"]
+ )
+ try:
+ blade.alert_watchers.create_alert_watchers(
+ names=[module.params["address"]], watcher_settings=watcher_settings
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to create alert email: {0}".format(
+ module.params["address"]
+ )
+ )
+ else:
+ try:
+ blade.alert_watchers.create_alert_watchers(
+ names=[module.params["address"]]
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to create alert email: {0}".format(
+ module.params["address"]
+ )
+ )
+ if not module.params["enabled"]:
+ watcher_settings = AlertWatcher(enabled=module.params["enabled"])
+ try:
+ blade.alert_watchers.update_alert_watchers(
+ names=[module.params["address"]], watcher_settings=watcher_settings
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to disable during create alert email: {0}".format(
+ module.params["address"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def update_alert(module, blade):
+ """Update alert Watcher"""
+ api_version = blade.api_version.list_versions().versions
+ mod_alert = False
+ try:
+ alert = blade.alert_watchers.list_alert_watchers(
+ names=[module.params["address"]]
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to get information for alert email: {0}".format(
+ module.params["address"]
+ )
+ )
+ current_state = {
+ "enabled": alert.items[0].enabled,
+ "severity": alert.items[0].minimum_notification_severity,
+ }
+ if current_state["enabled"] != module.params["enabled"]:
+ mod_alert = True
+ if MIN_REQUIRED_API_VERSION in api_version:
+ if current_state["severity"] != module.params["severity"]:
+ mod_alert = True
+ if mod_alert:
+ changed = True
+ if not module.check_mode:
+ if MIN_REQUIRED_API_VERSION in api_version:
+ watcher_settings = AlertWatcher(
+ enabled=module.params["enabled"],
+ minimum_notification_severity=module.params["severity"],
+ )
+ else:
+ watcher_settings = AlertWatcher(enabled=module.params["enabled"])
+ try:
+ blade.alert_watchers.update_alert_watchers(
+ names=[module.params["address"]], watcher_settings=watcher_settings
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to update alert email: {0}".format(
+ module.params["address"]
+ )
+ )
+ else:
+ changed = False
+ module.exit_json(changed=changed)
+
+
+def delete_alert(module, blade):
+ """Delete Alert Email"""
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.alert_watchers.delete_alert_watchers(names=[module.params["address"]])
+ except Exception:
+ module.fail_json(
+ msg="Failed to delete alert email: {0}".format(module.params["address"])
+ )
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ address=dict(type="str", required=True),
+ enabled=dict(type="bool", default=True),
+ severity=dict(
+ type="str", default="info", choices=["info", "warning", "critical"]
+ ),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb SDK is required for this module")
+
+ pattern = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
+ if not pattern.match(module.params["address"]):
+ module.fail_json(msg="Valid email address not provided.")
+
+ blade = get_blade(module)
+
+ exists = False
+ try:
+ emails = blade.alert_watchers.list_alert_watchers()
+ except Exception:
+ module.fail_json(msg="Failed to get exisitng email list")
+ for email in range(0, len(emails.items)):
+ if emails.items[email].name == module.params["address"]:
+ exists = True
+ break
+ if module.params["state"] == "present" and not exists:
+ create_alert(module, blade)
+ elif module.params["state"] == "present" and exists:
+ update_alert(module, blade)
+ elif module.params["state"] == "absent" and exists:
+ delete_alert(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_apiclient.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_apiclient.py
new file mode 100644
index 000000000..6a4755a95
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_apiclient.py
@@ -0,0 +1,250 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2021, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_apiclient
+version_added: '1.6.0'
+short_description: Manage FlashBlade API Clients
+description:
+- Enable or disable FlashBlade API Clients
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - Name of the API Client
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the API client should exist or not.
+ default: present
+ choices: [ absent, present ]
+ type: str
+ role:
+ description:
+ - The maximum role allowed for ID Tokens issued by this API client
+ type: str
+ choices: [readonly, ops_admin, storage_admin, array_admin]
+ issuer:
+ description:
+ - The name of the identity provider that will be issuing ID Tokens for this API client
+ - If not specified, defaults to the API client name, I(name).
+ type: str
+ public_key:
+ description:
+ - The API clients PEM formatted (Base64 encoded) RSA public key.
+ - Include the I(—–BEGIN PUBLIC KEY—–) and I(—–END PUBLIC KEY—–) lines
+ type: str
+ token_ttl:
+ description:
+ - Time To Live length in seconds for the exchanged access token
+ - Range is 1 second to 1 day (86400 seconds)
+ type: int
+ default: 86400
+ enabled:
+ description:
+ - State of the API Client Key
+ type: bool
+ default: true
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Create API token ansible-token
+ purestorage.flashblade.purefb_apiclient:
+ name: ansible_token
+ issuer: "Pure_Storage"
+ token_ttl: 3000
+ role: array_admin
+ public_key: "{{lookup('file', 'public_pem_file') }}"
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+
+- name: Disable API CLient
+ purestorage.flashblade.purefb_apiclient:
+ name: ansible_token
+ enabled: false
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+
+- name: Enable API CLient
+ purestorage.flashblade.purefb_apiclient:
+ name: ansible_token
+ enabled: true
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+
+- name: Delete API Client
+ purestorage.flashblade.purefb_apiclient:
+ state: absent
+ name: ansible_token
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURESTORAGE = True
+try:
+ from pypureclient import flashblade
+except ImportError:
+ HAS_PURESTORAGE = False
+
+import re
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_system,
+ purefb_argument_spec,
+)
+
+MIN_REQUIRED_API_VERSION = "2.0"
+
+
+def delete_client(module, blade):
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.delete_api_clients(names=[module.params["name"]])
+ except Exception:
+ module.fail_json(
+ msg="Failed to delete API Client {0}".format(module.params["name"])
+ )
+ module.exit_json(changed=changed)
+
+
+def update_client(module, blade, client):
+ """Update API Client"""
+ changed = False
+ if client.enabled != module.params["enabled"]:
+ changed = True
+ if not module.check_mode:
+ res = blade.patch_api_clients(
+ names=[module.params["name"]],
+ api_clients=flashblade.ApiClient(enabled=module.params["enabled"]),
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to update API Client {0}".format(module.params["name"])
+ )
+ module.exit_json(changed=changed)
+
+
+def create_client(module, blade):
+ """Create API Client"""
+ changed = True
+ if not 1 <= module.params["token_ttl"] <= 86400:
+ module.fail_json(msg="token_ttl parameter is out of range (1 to 86400)")
+ else:
+ token_ttl = module.params["token_ttl"] * 1000
+ if not module.params["issuer"]:
+ module.params["issuer"] = module.params["name"]
+ if not module.check_mode:
+ api_client = flashblade.ApiClientsPost(
+ max_role={"name": module.params["role"]},
+ issuer=module.params["issuer"],
+ access_token_ttl_in_ms=token_ttl,
+ public_key=module.params["public_key"],
+ )
+ res = blade.post_api_clients(
+ names=[module.params["name"]], api_client=api_client
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to create API Client {0}. Error message: {1}".format(
+ module.params["name"], res.errors[0].message
+ )
+ )
+ if module.params["enabled"]:
+ attr = flashblade.ApiClient(enabled=True)
+ res = blade.patch_api_clients(
+ api_clients=attr, names=[module.params["name"]]
+ )
+ if res.status_code != 200:
+ module.warn(
+ "API Client {0} created by enable failed. Please investigate.".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ enabled=dict(type="bool", default=True),
+ name=dict(type="str", required=True),
+ role=dict(
+ type="str",
+ choices=["readonly", "ops_admin", "storage_admin", "array_admin"],
+ ),
+ public_key=dict(type="str", no_log=True),
+ token_ttl=dict(type="int", default=86400, no_log=False),
+ issuer=dict(type="str"),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ pattern = re.compile("^[a-zA-Z0-9]([a-zA-Z0-9_]{0,54}[a-zA-Z0-9])?$")
+ if module.params["issuer"]:
+ if not pattern.match(module.params["issuer"]):
+ module.fail_json(
+ msg="API Client Issuer name {0} does not conform to required naming convention".format(
+ module.params["issuer"]
+ )
+ )
+ if not pattern.match(module.params["name"]):
+ module.fail_json(
+ msg="Object Store Virtual Host name {0} does not conform to required naming convention".format(
+ module.params["name"]
+ )
+ )
+ if not HAS_PURESTORAGE:
+ module.fail_json(msg="py-pure-client sdk is required for this module")
+
+ blade = get_system(module)
+ api_version = list(blade.get_versions().items)
+
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(
+ msg="FlashBlade REST version not supported. "
+ "Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
+ )
+ state = module.params["state"]
+
+ exists = bool(
+ blade.get_api_clients(names=[module.params["name"]]).status_code == 200
+ )
+ if exists:
+ client = list(blade.get_api_clients(names=[module.params["name"]]).items)[0]
+
+ if not exists and state == "present":
+ create_client(module, blade)
+ elif exists and state == "present":
+ update_client(module, blade, client)
+ elif exists and state == "absent":
+ delete_client(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_banner.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_banner.py
new file mode 100644
index 000000000..739c2ab9a
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_banner.py
@@ -0,0 +1,143 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2020, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_banner
+version_added: '1.4.0'
+short_description: Configure Pure Storage FlashBlade GUI and SSH MOTD message
+description:
+- Configure MOTD for Pure Storage FlashBlades.
+- This will be shown during an SSH or GUI login to the system.
+- Multiple line messages can be achieved using \\n.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Set ot delete the MOTD
+ default: present
+ type: str
+ choices: [ present, absent ]
+ banner:
+ description:
+ - Banner text, or MOTD, to use
+ type: str
+ default: "Welcome to the machine..."
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Set new banner text
+ purestorage.flashblade.purefb_banner:
+ banner: "Banner over\ntwo lines"
+ state: present
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Delete banner text
+ purestorage.flashblade.purefb_banner:
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import PureArray
+except ImportError:
+ HAS_PURITY_FB = False
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.10"
+
+
+def set_banner(module, blade):
+ """Set MOTD banner text"""
+ changed = True
+ if not module.check_mode:
+ try:
+ if not module.params["banner"]:
+ module.fail_json(msg="Invalid MOTD banner given")
+ blade_settings = PureArray(banner=module.params["banner"])
+ blade.arrays.update_arrays(array_settings=blade_settings)
+ except Exception:
+ module.fail_json(msg="Failed to set MOTD banner text")
+
+ module.exit_json(changed=changed)
+
+
+def delete_banner(module, blade):
+ """Delete MOTD banner text"""
+ changed = True
+ if not module.check_mode:
+ try:
+ blade_settings = PureArray(banner="")
+ blade.arrays.update_arrays(array_settings=blade_settings)
+ except Exception:
+ module.fail_json(msg="Failed to delete current MOTD banner text")
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ banner=dict(type="str", default="Welcome to the machine..."),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ required_if = [("state", "present", ["banner"])]
+
+ module = AnsibleModule(
+ argument_spec, required_if=required_if, supports_check_mode=True
+ )
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ api_version = blade.api_version.list_versions().versions
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(msg="Purity//FB must be upgraded to support this module.")
+ current_banner = blade.login_banner.list_login_banner().login_banner
+
+ # set banner if empty value or value differs
+ if state == "present" and (
+ not current_banner or current_banner != module.params["banner"]
+ ):
+ set_banner(module, blade)
+ # clear banner if it has a value
+ elif state == "absent" and current_banner:
+ delete_banner(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_bladename.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_bladename.py
new file mode 100644
index 000000000..0e0b5c755
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_bladename.py
@@ -0,0 +1,115 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2018, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_bladename
+version_added: '1.0.0'
+short_description: Configure Pure Storage FlashBlade name
+description:
+- Configure name of Pure Storage FlashBlades.
+- Ideal for Day 0 initial configuration.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Set the FlashBlade name
+ type: str
+ default: present
+ choices: [ present ]
+ name:
+ description:
+ - Name of the FlashBlade. Must conform to correct naming schema.
+ type: str
+ required: true
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Set new FlashBlade name
+ purestorage.flashblade.purefb_bladename:
+ name: new-flashblade-name
+ state: present
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+"""
+
+RETURN = r"""
+"""
+
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import PureArray
+except ImportError:
+ HAS_PURITY_FB = False
+
+
+import re
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+def update_name(module, blade):
+ """Change aray name"""
+ changed = True
+ if not module.check_mode:
+ try:
+ blade_settings = PureArray(name=module.params["name"])
+ blade.arrays.update_arrays(array_settings=blade_settings)
+ except Exception:
+ module.fail_json(
+ msg="Failed to change array name to {0}".format(module.params["name"])
+ )
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ state=dict(type="str", default="present", choices=["present"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ blade = get_blade(module)
+ pattern = re.compile("^[a-zA-Z0-9]([a-zA-Z0-9-]{0,54}[a-zA-Z0-9])?$")
+ if not pattern.match(module.params["name"]):
+ module.fail_json(
+ msg="FlashBlade name {0} does not conform to array name rules".format(
+ module.params["name"]
+ )
+ )
+ if module.params["name"] != blade.arrays.list_arrays().items[0].name:
+ update_name(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_bucket.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_bucket.py
new file mode 100644
index 000000000..67b6b1545
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_bucket.py
@@ -0,0 +1,398 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2017, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+---
+module: purefb_bucket
+version_added: "1.0.0"
+short_description: Manage Object Store Buckets on a Pure Storage FlashBlade.
+description:
+ - This module managess object store (s3) buckets on Pure Storage FlashBlade.
+author: Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - Bucket Name.
+ required: true
+ type: str
+ account:
+ description:
+ - Object Store Account for Bucket.
+ required: true
+ type: str
+ versioning:
+ description:
+ - State of S3 bucket versioning
+ required: false
+ default: absent
+ type: str
+ choices: [ "enabled", "suspended", "absent" ]
+ state:
+ description:
+ - Create, delete or modifies a bucket.
+ required: false
+ default: present
+ type: str
+ choices: [ "present", "absent" ]
+ eradicate:
+ description:
+ - Define whether to eradicate the bucket on delete or leave in trash.
+ required: false
+ type: bool
+ default: false
+ mode:
+ description:
+ - The type of bucket to be created. Also referred to a VSO Mode.
+ - Requires Purity//FB 3.3.3 or higher
+ - I(multi-site) type can only be used after feature is
+ enabled by Pure Technical Support
+ type: str
+ choices: [ "classic", "multi-site" ]
+ version_added: '1.10.0'
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = """
+- name: Create new bucket named foo in account bar
+ purestorage.flashblade.purefb_bucket:
+ name: foo
+ account: bar
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Delete bucket named foo in account bar
+ purestorage.flashblade.purefb_bucket:
+ name: foo
+ account: bar
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Change bucket versioning state
+ purestorage.flashblade.purefb_bucket:
+ name: foo
+ account: bar
+ versioning: enabled
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Recover deleted bucket named foo in account bar
+ purestorage.flashblade.purefb_bucket:
+ name: foo
+ account: bar
+ state: present
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Eradicate bucket named foo in account bar
+ purestorage.flashblade.purefb_bucket:
+ name: foo
+ account: bar
+ state: absent
+ eradicate: true
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+"""
+
+RETURN = """
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import Bucket, Reference, BucketPatch, BucketPost
+except ImportError:
+ HAS_PURITY_FB = False
+
+HAS_PYPURECLIENT = True
+try:
+ from pypureclient import flashblade
+except ImportError:
+ HAS_PYPURECLIENT = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ get_system,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.5"
+VERSIONING_VERSION = "1.9"
+VSO_VERSION = "2.4"
+
+
+def get_s3acc(module, blade):
+ """Return Object Store Account or None"""
+ s3acc = None
+ accts = blade.object_store_accounts.list_object_store_accounts()
+ for acct in range(0, len(accts.items)):
+ if accts.items[acct].name == module.params["account"]:
+ s3acc = accts.items[acct]
+ return s3acc
+
+
+def get_bucket(module, blade):
+ """Return Bucket or None"""
+ s3bucket = None
+ buckets = blade.buckets.list_buckets()
+ for bucket in range(0, len(buckets.items)):
+ if buckets.items[bucket].name == module.params["name"]:
+ s3bucket = buckets.items[bucket]
+ return s3bucket
+
+
+def create_bucket(module, blade):
+ """Create bucket"""
+ changed = True
+ if not module.check_mode:
+ try:
+ api_version = blade.api_version.list_versions().versions
+ if VSO_VERSION in api_version and module.params["mode"]:
+ bladev2 = get_system(module)
+ res = bladev2.post_buckets(
+ names=[module.params["name"]],
+ bucket=flashblade.BucketPost(
+ account=flashblade.Reference(name=module.params["account"]),
+ bucket_type=module.params["mode"],
+ ),
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Object Store Bucket {0} creation failed. Error: {1}".format(
+ module.params["name"],
+ res.errors[0].message,
+ )
+ )
+ elif VERSIONING_VERSION in api_version:
+ attr = BucketPost()
+ attr.account = Reference(name=module.params["account"])
+ blade.buckets.create_buckets(names=[module.params["name"]], bucket=attr)
+ else:
+ attr = Bucket()
+ attr.account = Reference(name=module.params["account"])
+ blade.buckets.create_buckets(
+ names=[module.params["name"]], account=attr
+ )
+ if (
+ module.params["versioning"] != "absent"
+ and VERSIONING_VERSION in api_version
+ ):
+ try:
+ blade.buckets.update_buckets(
+ names=[module.params["name"]],
+ bucket=BucketPatch(versioning=module.params["versioning"]),
+ )
+ except Exception:
+ module.fail_json(
+ msg="Object Store Bucket {0} Created but versioning state failed".format(
+ module.params["name"]
+ )
+ )
+ except Exception:
+ module.fail_json(
+ msg="Object Store Bucket {0}: Creation failed".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def delete_bucket(module, blade):
+ """Delete Bucket"""
+ changed = True
+ if not module.check_mode:
+ try:
+ api_version = blade.api_version.list_versions().versions
+ if VERSIONING_VERSION in api_version:
+ blade.buckets.update_buckets(
+ names=[module.params["name"]], bucket=BucketPatch(destroyed=True)
+ )
+ else:
+ blade.buckets.update_buckets(
+ names=[module.params["name"]], destroyed=Bucket(destroyed=True)
+ )
+ if module.params["eradicate"]:
+ try:
+ blade.buckets.delete_buckets(names=[module.params["name"]])
+ except Exception:
+ module.fail_json(
+ msg="Object Store Bucket {0}: Eradication failed".format(
+ module.params["name"]
+ )
+ )
+ except Exception:
+ module.fail_json(
+ msg="Object Store Bucket {0}: Deletion failed".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def recover_bucket(module, blade):
+ """Recover Bucket"""
+ changed = True
+ if not module.check_mode:
+ try:
+ api_version = blade.api_version.list_versions().versions
+ if VERSIONING_VERSION in api_version:
+ blade.buckets.update_buckets(
+ names=[module.params["name"]], bucket=BucketPatch(destroyed=False)
+ )
+ else:
+ blade.buckets.update_buckets(
+ names=[module.params["name"]], destroyed=Bucket(destroyed=False)
+ )
+ except Exception:
+ module.fail_json(
+ msg="Object Store Bucket {0}: Recovery failed".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def update_bucket(module, blade, bucket):
+ """Update Bucket"""
+ changed = False
+ api_version = blade.api_version.list_versions().versions
+ if VSO_VERSION in api_version:
+ if module.params["mode"]:
+ bladev2 = get_system(module)
+ bucket_detail = bladev2.get_buckets(names=[module.params["name"]])
+ if list(bucket_detail.items)[0].bucket_type != module.params["mode"]:
+ module.warn("Changing bucket type is not permitted.")
+
+ if VERSIONING_VERSION in api_version:
+ if bucket.versioning != "none":
+ if module.params["versioning"] == "absent":
+ versioning = "suspended"
+ else:
+ versioning = module.params["versioning"]
+ if bucket.versioning != versioning:
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.buckets.update_buckets(
+ names=[module.params["name"]],
+ bucket=BucketPatch(versioning=versioning),
+ )
+ changed = True
+ except Exception:
+ module.fail_json(
+ msg="Object Store Bucket {0}: Versioning change failed".format(
+ module.params["name"]
+ )
+ )
+ elif module.params["versioning"] != "absent":
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.buckets.update_buckets(
+ names=[module.params["name"]],
+ bucket=BucketPatch(versioning=module.params["versioning"]),
+ )
+ changed = True
+ except Exception:
+ module.fail_json(
+ msg="Object Store Bucket {0}: Versioning change failed".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def eradicate_bucket(module, blade):
+ """Eradicate Bucket"""
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.buckets.delete_buckets(names=[module.params["name"]])
+ except Exception:
+ module.fail_json(
+ msg="Object Store Bucket {0}: Eradication failed".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(required=True),
+ account=dict(required=True),
+ eradicate=dict(default="false", type="bool"),
+ mode=dict(type="str", choices=["classic", "multi-site"]),
+ versioning=dict(
+ default="absent", choices=["enabled", "suspended", "absent"]
+ ),
+ state=dict(default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+ if module.params["mode"]:
+ if not HAS_PYPURECLIENT:
+ module.fail_json(msg="py-pure-client sdk is required to support VSO mode")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ api_version = blade.api_version.list_versions().versions
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(msg="Purity//FB must be upgraded to support this module.")
+ if module.params["mode"] and VSO_VERSION not in api_version:
+ module.fail_json(msg="VSO mode requires Purity//FB 3.3.3 or higher.")
+
+ bucket = get_bucket(module, blade)
+ if not get_s3acc(module, blade):
+ module.fail_json(
+ msg="Object Store Account {0} does not exist.".format(
+ module.params["account"]
+ )
+ )
+
+ if module.params["eradicate"] and state == "present":
+ module.warn("Eradicate flag ignored without state=absent")
+
+ if state == "present" and not bucket:
+ create_bucket(module, blade)
+ elif state == "present" and bucket and bucket.destroyed:
+ recover_bucket(module, blade)
+ elif state == "absent" and bucket and not bucket.destroyed:
+ delete_bucket(module, blade)
+ elif state == "present" and bucket:
+ update_bucket(module, blade, bucket)
+ elif (
+ state == "absent" and bucket and bucket.destroyed and module.params["eradicate"]
+ ):
+ eradicate_bucket(module, blade)
+ elif state == "absent" and not bucket:
+ module.exit_json(changed=False)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_bucket_replica.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_bucket_replica.py
new file mode 100644
index 000000000..6ac3775ae
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_bucket_replica.py
@@ -0,0 +1,313 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2020, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+---
+module: purefb_bucket_replica
+version_added: '1.0.0'
+short_description: Manage bucket replica links between Pure Storage FlashBlades
+description:
+ - This module manages bucket replica links between Pure Storage FlashBlades.
+author: Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - Local Bucket Name.
+ required: true
+ type: str
+ state:
+ description:
+ - Creates or modifies a bucket replica link
+ required: false
+ default: present
+ type: str
+ choices: [ "present", "absent" ]
+ target:
+ description:
+ - Remote array or target name to create replica on.
+ required: false
+ type: str
+ target_bucket:
+ description:
+ - Name of target bucket name
+ - If not supplied, will default to I(name).
+ type: str
+ required: false
+ paused:
+ description:
+ - State of the bucket replica link
+ type: bool
+ default: false
+ credential:
+ description:
+ - Name of remote credential name to use.
+ required: false
+ type: str
+extends_documentation_fragment:
+ - purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = """
+- name: Create new bucket replica from foo to bar on arrayB
+ purestorage.flashblade.purefb_bucket_replica:
+ name: foo
+ target: arrayB
+ target_bucket: bar
+ credentials: cred_1
+ state: present
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Pause exisitng bucket replica link
+ purestorage.flashblade.purefb_bucket_replica:
+ name: foo
+ paused: true
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Delete bucket replica link foo
+ purestorage.flashblade.purefb_fs_replica:
+ name: foo
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641"""
+
+RETURN = """
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import BucketReplicaLink, ObjectStoreRemoteCredentials
+except ImportError:
+ HAS_PURITY_FB = False
+
+MIN_REQUIRED_API_VERSION = "1.9"
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+def get_local_bucket(module, blade):
+ """Return Bucket or None"""
+ try:
+ res = blade.buckets.list_buckets(names=[module.params["name"]])
+ return res.items[0]
+ except Exception:
+ return None
+
+
+def get_remote_cred(module, blade, target):
+ """Return Remote Credential or None"""
+ try:
+ res = (
+ blade.object_store_remote_credentials.list_object_store_remote_credentials(
+ names=[target + "/" + module.params["credential"]]
+ )
+ )
+ return res.items[0]
+ except Exception:
+ return None
+
+
+def get_local_rl(module, blade):
+ """Return Bucket Replica Link or None"""
+ try:
+ res = blade.bucket_replica_links.list_bucket_replica_links(
+ local_bucket_names=[module.params["name"]]
+ )
+ return res.items[0]
+ except Exception:
+ return None
+
+
+def get_connected(module, blade):
+ connected_blades = blade.array_connections.list_array_connections()
+ for target in range(0, len(connected_blades.items)):
+ if (
+ connected_blades.items[target].remote.name == module.params["target"]
+ or connected_blades.items[target].management_address
+ == module.params["target"]
+ ) and connected_blades.items[target].status in [
+ "connected",
+ "connecting",
+ "partially_connected",
+ ]:
+ return connected_blades.items[target].remote.name
+ connected_targets = blade.targets.list_targets()
+ for target in range(0, len(connected_targets.items)):
+ if connected_targets.items[target].name == module.params[
+ "target"
+ ] and connected_targets.items[target].status in [
+ "connected",
+ "connecting",
+ "partially_connected",
+ ]:
+ return connected_targets.items[target].name
+ return None
+
+
+def create_rl(module, blade, remote_cred):
+ """Create Bucket Replica Link"""
+ changed = True
+ if not module.check_mode:
+ try:
+ if not module.params["target_bucket"]:
+ module.params["target_bucket"] = module.params["name"]
+ else:
+ module.params["target_bucket"] = module.params["target_bucket"].lower()
+ blade.bucket_replica_links.create_bucket_replica_links(
+ local_bucket_names=[module.params["name"]],
+ remote_bucket_names=[module.params["target_bucket"]],
+ remote_credentials_names=[remote_cred.name],
+ bucket_replica_link=BucketReplicaLink(paused=module.params["paused"]),
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to create bucket replica link {0}.".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def update_rl_policy(module, blade, local_replica_link):
+ """Update Bucket Replica Link"""
+ changed = False
+ new_cred = local_replica_link.remote.name + "/" + module.params["credential"]
+ if (
+ local_replica_link.paused != module.params["paused"]
+ or local_replica_link.remote_credentials.name != new_cred
+ ):
+ changed = True
+ if not module.check_mode:
+ try:
+ module.warn("{0}".format(local_replica_link))
+ blade.bucket_replica_links.update_bucket_replica_links(
+ local_bucket_names=[module.params["name"]],
+ remote_bucket_names=[local_replica_link.remote_bucket.name],
+ remote_names=[local_replica_link.remote.name],
+ bucket_replica_link=BucketReplicaLink(
+ paused=module.params["paused"],
+ remote_credentials=ObjectStoreRemoteCredentials(name=new_cred),
+ ),
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to update bucket replica link {0}.".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def delete_rl_policy(module, blade, local_replica_link):
+ """Delete Bucket Replica Link"""
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.bucket_replica_links.delete_bucket_replica_links(
+ remote_names=[local_replica_link.remote.name],
+ local_bucket_names=[module.params["name"]],
+ remote_bucket_names=[local_replica_link.remote_bucket.name],
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to delete bucket replica link {0}.".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ target=dict(type="str"),
+ target_bucket=dict(type="str"),
+ paused=dict(type="bool", default=False),
+ credential=dict(type="str"),
+ state=dict(default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ state = module.params["state"]
+ module.params["name"] = module.params["name"].lower()
+ blade = get_blade(module)
+ versions = blade.api_version.list_versions().versions
+
+ if MIN_REQUIRED_API_VERSION not in versions:
+ module.fail_json(
+ msg="Minimum FlashBlade REST version required: {0}".format(
+ MIN_REQUIRED_API_VERSION
+ )
+ )
+
+ local_bucket = get_local_bucket(module, blade)
+ local_replica_link = get_local_rl(module, blade)
+ target = get_connected(module, blade)
+
+ if not target:
+ module.fail_json(
+ msg="Selected target {0} is not connected.".format(module.params["target"])
+ )
+
+ if local_replica_link and not module.params["credential"]:
+ module.params["credential"] = local_replica_link.remote_credentials.name.split(
+ "/"
+ )[1]
+ remote_cred = get_remote_cred(module, blade, target)
+ if not remote_cred:
+ module.fail_json(
+ msg="Selected remote credential {0} does not exist for target {1}.".format(
+ module.params["credential"], module.params["target"]
+ )
+ )
+
+ if not local_bucket:
+ module.fail_json(
+ msg="Selected local bucket {0} does not exist.".format(
+ module.params["name"]
+ )
+ )
+
+ if local_replica_link:
+ if local_replica_link.status == "unhealthy":
+ module.fail_json(msg="Replica Link unhealthy - please check target")
+
+ if state == "present" and not local_replica_link:
+ create_rl(module, blade, remote_cred)
+ elif state == "present" and local_replica_link:
+ update_rl_policy(module, blade, local_replica_link)
+ elif state == "absent" and local_replica_link:
+ delete_rl_policy(module, blade, local_replica_link)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_certgrp.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_certgrp.py
new file mode 100644
index 000000000..2308b6f16
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_certgrp.py
@@ -0,0 +1,249 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2020, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_certgrp
+version_added: '1.4.0'
+short_description: Manage FlashBlade Certifcate Groups
+description:
+- Manage certifcate groups for FlashBlades
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Create or delete certifcate group
+ default: present
+ type: str
+ choices: [ absent, present ]
+ name:
+ description:
+ - Name of the certificate group
+ type: str
+ certificates:
+ description:
+ - List of certifcates to add to a policy on creation
+ type: list
+ elements: str
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Create a certifcate group
+ purestorage.flashblade.purefb_certgrp:
+ name: test_grp
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Create a cerifcate group and add existing certificates
+ purestorage.flashblade.purefb_certgrp:
+ name: test_grp
+ certifcates:
+ - cert1
+ - cert2
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Delete a certifcate from a group
+ purestorage.flashblade.purefb_certgrp:
+ name: test_grp
+ certificates:
+ - cert2
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Delete a certifcate group
+ purestorage.flashblade.purefb_certgrp:
+ name: test_grp
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+"""
+
+RETURN = r"""
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.9"
+
+
+def delete_certgrp(module, blade):
+ """Delete certifcate group"""
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.certificate_groups.delete_certificate_groups(
+ names=[module.params["name"]]
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to delete certifcate group {0}.".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def create_certgrp(module, blade):
+ """Create certifcate group"""
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.certificate_groups.create_certificate_groups(
+ names=[module.params["name"]]
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to create certificate group {0}.".format(
+ module.params["name"]
+ )
+ )
+ if module.params["certificates"]:
+ try:
+ blade.certificate_groups.add_certificate_group_certificates(
+ certificate_names=module.params["certificates"],
+ certificate_group_names=[module.params["name"]],
+ )
+ except Exception:
+ blade.certificate_groups.delete_certificate_groups(
+ names=[module.params["name"]]
+ )
+ module.fail_json(
+ msg="Failed to add certifcates {0}. "
+ "Please check they all exist".format(module.params["certificates"])
+ )
+ module.exit_json(changed=changed)
+
+
+def update_certgrp(module, blade):
+ """Update certificate group"""
+ changed = False
+ try:
+ certs = blade.certificate_groups.list_certificate_group_certificates(
+ certificate_group_names=[module.params["name"]]
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to get certifates list for group {0}.".format(
+ module.params["name"]
+ )
+ )
+ if not certs:
+ if module.params["state"] == "present":
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.certificate_groups.add_certificate_group_certificates(
+ certificate_names=module.params["certificates"],
+ certificate_group_names=[module.params["name"]],
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to add certifcates {0}. "
+ "Please check they all exist".format(
+ module.params["certificates"]
+ )
+ )
+ else:
+ current = []
+ for cert in range(0, len(certs.items)):
+ current.append(certs.items[cert].member.name)
+ for new_cert in range(0, len(module.params["certificates"])):
+ certificate = module.params["certificates"][new_cert]
+ if certificate in current:
+ if module.params["state"] == "absent":
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.certificate_groups.remove_certificate_group_certificates(
+ certificate_names=[certificate],
+ certificate_group_names=[module.params["name"]],
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to delete certifcate {0} from group {1}.".format(
+ certificate, module.params["name"]
+ )
+ )
+ else:
+ if module.params["state"] == "present":
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.certificate_groups.add_certificate_group_certificates(
+ certificate_names=[certificate],
+ certificate_group_names=[module.params["name"]],
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to add certifcate {0} to group {1}".format(
+ certificate, module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ name=dict(type="str"),
+ certificates=dict(type="list", elements="str"),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ versions = blade.api_version.list_versions().versions
+
+ if MIN_REQUIRED_API_VERSION not in versions:
+ module.fail_json(
+ msg="Minimum FlashBlade REST version required: {0}".format(
+ MIN_REQUIRED_API_VERSION
+ )
+ )
+
+ try:
+ certgrp = blade.certificate_groups.list_certificate_groups(
+ names=[module.params["name"]]
+ ).items[0]
+ except Exception:
+ certgrp = None
+
+ if certgrp and state == "present" and module.params["certificates"]:
+ update_certgrp(module, blade)
+ elif state == "present" and not certgrp:
+ create_certgrp(module, blade)
+ elif state == "absent" and certgrp:
+ if module.params["certificates"]:
+ update_certgrp(module, blade)
+ else:
+ delete_certgrp(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_certs.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_certs.py
new file mode 100644
index 000000000..b9a2c76f7
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_certs.py
@@ -0,0 +1,198 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2020, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_certs
+version_added: '1.4.0'
+short_description: Manage FlashBlade SSL Certificates
+description:
+- Manage SSL certificates for FlashBlades
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Create or delete certificate
+ default: present
+ type: str
+ choices: [ absent, present ]
+ name:
+ description:
+ - Name of the certificate
+ type: str
+ contents:
+ description:
+ - SSL certificate text
+ type: str
+ private_key:
+ description:
+ - SSL certificate private key test
+ type: str
+ passphrase:
+ description:
+ - Passphrase for the private_key
+ type: str
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Create a SSL certificate
+ purestorage.flashblade.purefb_certs:
+ name: test_cert
+ contents: "{{lookup('file', 'certificate_file_name') }}"
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Delete a SSL certificate
+ purestorage.flashblade.purefb_certs:
+ name: test_cert
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Update SSL certificate
+ purestorage.flashblade.purefb_certs:
+ name: global
+ contents: "{{ lookup('file', 'certificate_file_name') }}"
+ private_key: "{{ lookup('file', 'certificate_key_file_name') }}"
+ passphrase: 'mypassword'
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURITYFB = True
+try:
+ from purity_fb import Certificate, CertificatePost
+except ImportError:
+ HAS_PURITYFB = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.9"
+
+
+def delete_cert(module, blade):
+ """Delete certificate"""
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.certificates.delete_certificates(names=[module.params["name"]])
+ except Exception:
+ module.fail_json(
+ msg="Failed to delete certificate {0}.".format(module.params["name"])
+ )
+ module.exit_json(changed=changed)
+
+
+def create_cert(module, blade):
+ """Create certificate"""
+ changed = True
+ if not module.check_mode:
+ try:
+ body = CertificatePost(
+ certificate=module.params["contents"], certificate_type="external"
+ )
+ blade.certificates.create_certificates(
+ names=[module.params["name"]], certificate=body
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to create certificate {0}.".format(module.params["name"])
+ )
+ module.exit_json(changed=changed)
+
+
+def update_cert(module, blade, cert):
+ """Update certificate"""
+ changed = False
+ if cert.certificate_type == "external":
+ module.fail_json(msg="External certificates cannot be modified")
+
+ if not module.params["private_key"]:
+ module.fail_json(msg="private_key must be specified for the global certificate")
+
+ if cert.certificate.strip() != module.params["contents"].strip():
+ changed = True
+ if not module.check_mode:
+ try:
+ body = Certificate(
+ certificate=module.params["contents"],
+ private_key=module.params["private_key"],
+ )
+ if module.params["passphrase"]:
+ Certificate.passphrase = module.params["passphrase"]
+ blade.certificates.update_certificates(
+ names=[module.params["name"]], certificate=body
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to create certificate {0}.".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ name=dict(type="str"),
+ contents=dict(type="str", no_log=True),
+ private_key=dict(type="str", no_log=True),
+ passphrase=dict(type="str", no_log=True),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ versions = blade.api_version.list_versions().versions
+
+ if MIN_REQUIRED_API_VERSION not in versions:
+ module.fail_json(
+ msg="Minimum FlashBlade REST version required: {0}".format(
+ MIN_REQUIRED_API_VERSION
+ )
+ )
+
+ try:
+ cert = blade.certificates.list_certificates(names=[module.params["name"]])
+ except Exception:
+ cert = None
+
+ if not cert and state == "present":
+ create_cert(module, blade)
+ elif state == "present":
+ update_cert(module, blade, cert.items[0])
+ elif state == "absent" and cert:
+ delete_cert(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_connect.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_connect.py
new file mode 100644
index 000000000..508c6a322
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_connect.py
@@ -0,0 +1,574 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2020, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_connect
+version_added: '1.0.0'
+short_description: Manage replication connections between two FlashBlades
+description:
+- Manage replication connections to specified remote FlashBlade system
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Create or delete replication connection
+ default: present
+ type: str
+ choices: [ absent, present ]
+ encrypted:
+ description:
+ - Define if replication connection is encrypted
+ type: bool
+ default: false
+ target_url:
+ description:
+ - Management IP address of target FlashBlade system
+ type: str
+ required: true
+ target_api:
+ description:
+ - API token for target FlashBlade system
+ type: str
+ target_repl:
+ description:
+ - Replication IP address of target FlashBlade system
+ - If not set at time of connection creation, will default to
+ all the replication addresses available on the target array
+ at the time of connection creation.
+ type: list
+ elements: str
+ version_added: "1.9.0"
+ default_limit:
+ description:
+ - Default maximum bandwidth threshold for outbound traffic in bytes.
+ - B, K, M, or G units. See examples.
+ - Must be 0 or between 5MB and 28GB
+ - Once exceeded, bandwidth throttling occurs
+ type: str
+ version_added: "1.9.0"
+ window_limit:
+ description:
+ - Maximum bandwidth threshold for outbound traffic during the specified
+ time range in bytes.
+ - B, K, M, or G units. See examples.
+ - Must be 0 or between 5MB and 28GB
+ - Once exceeded, bandwidth throttling occurs
+ type: str
+ version_added: "1.9.0"
+ window_start:
+ description:
+ - The window start time.
+ - The time must be set to the hour.
+ type: str
+ version_added: "1.9.0"
+ window_end:
+ description:
+ - The window end time.
+ - The time must be set to the hour.
+ type: str
+ version_added: "1.9.0"
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Create a connection to remote FlashBlade system
+ purestorage.flashblade.purefb_connect:
+ target_url: 10.10.10.20
+ target_api: T-b3275b1c-8958-4190-9052-eb46b0bd09f8
+ fb_url: 10.10.10.2
+ api_token: T-91528421-fe42-47ee-bcb1-47eefb0a9220
+- name: Create a connection to remote FlashBlade system with bandwidth limits
+ purestorage.flashblade.purefb_connect:
+ target_url: 10.10.10.20
+ target_api: T-b3275b1c-8958-4190-9052-eb46b0bd09f8
+ window_limit: 28G
+ window_start: 1AM
+ window_end: 7AM
+ default_limit: 5M
+ fb_url: 10.10.10.2
+ api_token: T-91528421-fe42-47ee-bcb1-47eefb0a9220
+- name: Delete connection to target FlashBlade system
+ purestorage.flashblade.purefb_connect:
+ state: absent
+ target_url: 10.10.10.20
+ target_api: T-b3275b1c-8958-4190-9052-eb46b0bd09f8
+ fb_url: 10.10.10.2
+ api_token: T-91528421-fe42-47ee-bcb1-47eefb0a9220
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURITYFB = True
+try:
+ from purity_fb import PurityFb, ArrayConnection, ArrayConnectionPost
+except ImportError:
+ HAS_PURITYFB = False
+
+HAS_PYPURECLIENT = True
+try:
+ from pypureclient import flashblade
+ from pypureclient.flashblade import ArrayConnection, ArrayConnectionPost
+except ImportError:
+ HAS_PYPURECLIENT = False
+
+from ansible.module_utils.basic import AnsibleModule, human_to_bytes
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ get_system,
+ purefb_argument_spec,
+)
+
+
+FAN_IN_MAXIMUM = 1
+FAN_OUT_MAXIMUM = 3
+MIN_REQUIRED_API_VERSION = "1.9"
+THROTTLE_API_VERSION = "2.3"
+
+
+def _convert_to_millisecs(hour):
+ if hour[-2:] == "AM" and hour[:2] == "12":
+ return 0
+ elif hour[-2:] == "AM":
+ return int(hour[:-2]) * 3600000
+ elif hour[-2:] == "PM" and hour[:2] == "12":
+ return 43200000
+ return (int(hour[:-2]) + 12) * 3600000
+
+
+def _check_connected(module, blade):
+ connected_blades = blade.array_connections.list_array_connections()
+ for target in range(0, len(connected_blades.items)):
+ if connected_blades.items[target].management_address is None:
+ try:
+ remote_system = PurityFb(module.params["target_url"])
+ remote_system.login(module.params["target_api"])
+ remote_array = remote_system.arrays.list_arrays().items[0].name
+ if connected_blades.items[target].remote.name == remote_array:
+ return connected_blades.items[target]
+ except Exception:
+ module.fail_json(
+ msg="Failed to connect to remote array {0}.".format(
+ module.params["target_url"]
+ )
+ )
+ if connected_blades.items[target].management_address == module.params[
+ "target_url"
+ ] and connected_blades.items[target].status in [
+ "connected",
+ "connecting",
+ "partially_connected",
+ ]:
+ return connected_blades.items[target]
+ return None
+
+
+def break_connection(module, blade, target_blade):
+ """Break connection between arrays"""
+ changed = True
+ if not module.check_mode:
+ source_blade = blade.arrays.list_arrays().items[0].name
+ try:
+ if target_blade.management_address is None:
+ module.fail_json(
+ msg="Disconnect can only happen from the array that formed the connection"
+ )
+ blade.array_connections.delete_array_connections(
+ remote_names=[target_blade.remote.name]
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to disconnect {0} from {1}.".format(
+ target_blade.remote.name, source_blade
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def create_connection(module, blade):
+ """Create connection between arrays"""
+ changed = True
+ if not module.check_mode:
+ remote_array = module.params["target_url"]
+ try:
+ remote_system = PurityFb(module.params["target_url"])
+ remote_system.login(module.params["target_api"])
+ remote_array = remote_system.arrays.list_arrays().items[0].name
+ remote_conn_cnt = (
+ remote_system.array_connections.list_array_connections().pagination_info.total_item_count
+ )
+ if remote_conn_cnt == FAN_IN_MAXIMUM:
+ module.fail_json(
+ msg="Remote array {0} already connected to {1} other array. Fan-In not supported".format(
+ remote_array, remote_conn_cnt
+ )
+ )
+ connection_key = (
+ remote_system.array_connections.create_array_connections_connection_keys()
+ .items[0]
+ .connection_key
+ )
+ connection_info = ArrayConnectionPost(
+ management_address=module.params["target_url"],
+ encrypted=module.params["encrypted"],
+ connection_key=connection_key,
+ )
+ blade.array_connections.create_array_connections(
+ array_connection=connection_info
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to connect to remote array {0}.".format(remote_array)
+ )
+ module.exit_json(changed=changed)
+
+
+def create_v2_connection(module, blade):
+ """Create connection between REST 2 capable arrays"""
+ changed = True
+ if blade.get_array_connections().total_item_count == FAN_OUT_MAXIMUM:
+ module.fail_json(
+ msg="FlashBlade fan-out maximum of {0} already reached".format(
+ FAN_OUT_MAXIMUM
+ )
+ )
+ try:
+ remote_system = flashblade.Client(
+ target=module.params["target_url"], api_token=module.params["target_api"]
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to connect to remote array {0}.".format(
+ module.params["target_url"]
+ )
+ )
+ remote_array = list(remote_system.get_arrays().items)[0].name
+ remote_conn_cnt = remote_system.get_array_connections().total_item_count
+ if remote_conn_cnt == FAN_IN_MAXIMUM:
+ module.fail_json(
+ msg="Remote array {0} already connected to {1} other array. Fan-In not supported".format(
+ remote_array, remote_conn_cnt
+ )
+ )
+ connection_key = list(remote_system.post_array_connections_connection_key().items)[
+ 0
+ ].connection_key
+
+ if module.params["default_limit"] or module.params["window_limit"]:
+ if THROTTLE_API_VERSION in list(blade.get_versions().items):
+ if THROTTLE_API_VERSION not in list(remote_system.get_versions().items):
+ module.fail_json(msg="Remote array does not support throttling")
+ if module.params["window_limit"]:
+ if not module.params["window_start"]:
+ module.params["window_start"] = "12AM"
+ if not module.params["window_end"]:
+ module.params["window_end"] = "12AM"
+ window = flashblade.TimeWindow(
+ start=_convert_to_millisecs(module.params["window_start"]),
+ end=_convert_to_millisecs(module.params["window_end"]),
+ )
+ if module.params["window_limit"] and module.params["default_limit"]:
+ throttle = flashblade.Throttle(
+ default_limit=human_to_bytes(module.params["default_limit"]),
+ window_limit=human_to_bytes(module.params["window_limit"]),
+ window=window,
+ )
+ elif module.params["window_limit"] and not module.params["default_limit"]:
+ throttle = flashblade.Throttle(
+ window_limit=human_to_bytes(module.params["window_limit"]),
+ window=window,
+ )
+ else:
+ throttle = flashblade.Throttle(
+ default_limit=human_to_bytes(module.params["default_limit"]),
+ )
+ connection_info = ArrayConnectionPost(
+ management_address=module.params["target_url"],
+ replication_addresses=module.params["target_repl"],
+ encrypted=module.params["encrypted"],
+ connection_key=connection_key,
+ throttle=throttle,
+ )
+ else:
+ connection_info = ArrayConnectionPost(
+ management_address=module.params["target_url"],
+ replication_addresses=module.params["target_repl"],
+ encrypted=module.params["encrypted"],
+ connection_key=connection_key,
+ )
+ else:
+ connection_info = ArrayConnectionPost(
+ management_address=module.params["target_url"],
+ replication_addresses=module.params["target_repl"],
+ encrypted=module.params["encrypted"],
+ connection_key=connection_key,
+ )
+ if not module.check_mode:
+ res = blade.post_array_connections(array_connection=connection_info)
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to connect to remote array {0}. Error: {1}".format(
+ remote_array, res.errors[0].message
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def update_connection(module, blade, target_blade):
+ """Update array connection - only encryption currently"""
+ changed = False
+ if target_blade.management_address is None:
+ module.fail_json(
+ msg="Update can only happen from the array that formed the connection"
+ )
+ if module.params["encrypted"] != target_blade.encrypted:
+ if (
+ module.params["encrypted"]
+ and blade.file_system_replica_links.list_file_system_replica_links().pagination_info.total_item_count
+ != 0
+ ):
+ module.fail_json(
+ msg="Cannot turn array connection encryption on if file system replica links exist"
+ )
+ new_attr = ArrayConnection(encrypted=module.params["encrypted"])
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.array_connections.update_array_connections(
+ remote_names=[target_blade.remote.name],
+ array_connection=new_attr,
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to change encryption setting for array connection."
+ )
+ module.exit_json(changed=changed)
+
+
+def update_v2_connection(module, blade):
+ """Update REST 2 based array connection"""
+ changed = False
+ versions = list(blade.get_versions().items)
+ remote_blade = flashblade.Client(
+ target=module.params["target_url"], api_token=module.params["target_api"]
+ )
+ remote_name = list(remote_blade.get_arrays().items)[0].name
+ remote_connection = list(
+ blade.get_array_connections(filter="remote.name='" + remote_name + "'").items
+ )[0]
+ if remote_connection.management_address is None:
+ module.fail_json(
+ msg="Update can only happen from the array that formed the connection"
+ )
+ if module.params["encrypted"] != remote_connection.encrypted:
+ if (
+ module.params["encrypted"]
+ and blade.get_file_system_replica_links().total_item_count != 0
+ ):
+ module.fail_json(
+ msg="Cannot turn array connection encryption on if file system replica links exist"
+ )
+ current_connection = {
+ "encrypted": remote_connection.encrypted,
+ "replication_addresses": sorted(remote_connection.replication_addresses),
+ "throttle": [],
+ }
+ if (
+ not remote_connection.throttle.default_limit
+ and not remote_connection.throttle.window_limit
+ ):
+ if (
+ module.params["default_limit"] or module.params["window_limit"]
+ ) and blade.get_bucket_replica_links().total_item_count != 0:
+ module.fail_json(
+ msg="Cannot set throttle when bucket replica links already exist"
+ )
+ if THROTTLE_API_VERSION in versions:
+ current_connection["throttle"] = {
+ "default_limit": remote_connection.throttle.default_limit,
+ "window_limit": remote_connection.throttle.window_limit,
+ "start": remote_connection.throttle.window.start,
+ "end": remote_connection.throttle.window.end,
+ }
+ if module.params["encrypted"]:
+ encryption = module.params["encrypted"]
+ else:
+ encryption = remote_connection.encrypted
+ if module.params["target_repl"]:
+ target_repl = sorted(module.params["target_repl"])
+ else:
+ target_repl = remote_connection.replication_addresses
+ if module.params["default_limit"]:
+ default_limit = human_to_bytes(module.params["default_limit"])
+ if default_limit == 0:
+ default_limit = None
+ else:
+ default_limit = remote_connection.throttle.default_limit
+ if module.params["window_limit"]:
+ window_limit = human_to_bytes(module.params["window_limit"])
+ else:
+ window_limit = remote_connection.throttle.window_limit
+ if module.params["window_start"]:
+ start = _convert_to_millisecs(module.params["window_start"])
+ else:
+ start = remote_connection.throttle.window.start
+ if module.params["window_end"]:
+ end = _convert_to_millisecs(module.params["window_end"])
+ else:
+ end = remote_connection.throttle.window.end
+
+ new_connection = {
+ "encrypted": encryption,
+ "replication_addresses": target_repl,
+ "throttle": [],
+ }
+ if THROTTLE_API_VERSION in versions:
+ new_connection["throttle"] = {
+ "default_limit": default_limit,
+ "window_limit": window_limit,
+ "start": start,
+ "end": end,
+ }
+ if new_connection != current_connection:
+ changed = True
+ if not module.check_mode:
+ if THROTTLE_API_VERSION in versions:
+ window = flashblade.TimeWindow(
+ start=new_connection["throttle"]["start"],
+ end=new_connection["throttle"]["end"],
+ )
+ throttle = flashblade.Throttle(
+ default_limit=new_connection["throttle"]["default_limit"],
+ window_limit=new_connection["throttle"]["window_limit"],
+ window=window,
+ )
+ connection_info = ArrayConnectionPost(
+ replication_addresses=new_connection["replication_addresses"],
+ encrypted=new_connection["encrypted"],
+ throttle=throttle,
+ )
+ else:
+ connection_info = ArrayConnection(
+ replication_addresses=new_connection["replication_addresses"],
+ encrypted=new_connection["encrypted"],
+ )
+ res = blade.patch_array_connections(
+ remote_names=[remote_name], array_connection=connection_info
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to update connection to remote array {0}. Error: {1}".format(
+ remote_name, res.errors[0].message
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ encrypted=dict(type="bool", default=False),
+ target_url=dict(type="str", required=True),
+ target_api=dict(type="str", no_log=True),
+ target_repl=dict(type="list", elements="str"),
+ default_limit=dict(type="str"),
+ window_limit=dict(type="str"),
+ window_start=dict(type="str"),
+ window_end=dict(type="str"),
+ )
+ )
+
+ required_if = [("state", "present", ["target_api"])]
+
+ module = AnsibleModule(
+ argument_spec, required_if=required_if, supports_check_mode=True
+ )
+
+ if not HAS_PURITYFB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ versions = blade.api_version.list_versions().versions
+
+ if MIN_REQUIRED_API_VERSION not in versions:
+ module.fail_json(
+ msg="Minimum FlashBlade REST version required: {0}".format(
+ MIN_REQUIRED_API_VERSION
+ )
+ )
+ if "2.0" in versions:
+ bladev2 = get_system(module)
+ if not HAS_PYPURECLIENT:
+ module.fail_json(msg="py-pure-client sdk is required for this module")
+ v2_connection = True
+ if module.params["default_limit"]:
+ if (
+ human_to_bytes(module.params["default_limit"]) != 0
+ and 5242880
+ >= human_to_bytes(module.params["default_limit"])
+ >= 30064771072
+ ):
+ module.fail_json(msg="Default Bandwidth must be between 5MB and 28GB")
+ if module.params["window_limit"]:
+ if (
+ human_to_bytes(module.params["window_limit"]) != 0
+ and 5242880
+ >= human_to_bytes(module.params["window_limit"])
+ >= 30064771072
+ ):
+ module.fail_json(msg="Window Bandwidth must be between 5MB and 28GB")
+ else:
+ if module.params["target_repl"]:
+ module.warn(
+ "Target Replication addresses can only be set for systems"
+ " that support REST 2.0 and higher"
+ )
+ v2_connection = False
+
+ target_blade = _check_connected(module, blade)
+ if state == "present" and not target_blade:
+ # REST 1 does not support fan-out for replication
+ # REST 2 has a limit which we can check
+ if v2_connection:
+ create_v2_connection(module, bladev2)
+ else:
+ if (
+ blade.array_connections.list_array_connections().pagination_info.total_item_count
+ == 1
+ ):
+ module.fail_json(
+ msg="Source FlashBlade already connected to another array. Fan-Out not supported"
+ )
+ create_connection(module, blade)
+ elif state == "present" and target_blade:
+ if v2_connection:
+ update_v2_connection(module, bladev2)
+ else:
+ update_connection(module, blade, target_blade)
+ elif state == "absent" and target_blade:
+ break_connection(module, blade, target_blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_dns.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_dns.py
new file mode 100644
index 000000000..b5abd9289
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_dns.py
@@ -0,0 +1,175 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2020, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_dns
+version_added: '1.0.0'
+short_description: Configure Pure Storage FlashBlade DNS settings
+description:
+- Set or erase DNS configuration for Pure Storage FlashBlades.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Create or delete DNS servers configuration
+ type: str
+ default: present
+ choices: [ absent, present ]
+ domain:
+ description:
+ - Domain suffix to be appended when perofrming DNS lookups.
+ type: str
+ nameservers:
+ description:
+ - List of up to 3 unique DNS server IP addresses. These can be
+ IPv4 or IPv6 - No validation is done of the addresses is performed.
+ type: list
+ elements: str
+ search:
+ description:
+ - Ordered list of domain names to search
+ - Deprecated option. Will be removed in Collection v1.6.0, There is no replacement for this.
+ type: list
+ elements: str
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Delete exisitng DNS settings
+ purestorage.flashblade.purefb_dns:
+ state: absent
+ fa_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+
+- name: Set DNS settings
+ purestorage.flashblade.purefb_dns:
+ domain: purestorage.com
+ nameservers:
+ - 8.8.8.8
+ - 8.8.4.4
+ search:
+ - purestorage.com
+ - acme.com
+ fa_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import Dns
+except ImportError:
+ HAS_PURITY_FB = False
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+def remove(duplicate):
+ final_list = []
+ for num in duplicate:
+ if num not in final_list:
+ final_list.append(num)
+ return final_list
+
+
+def delete_dns(module, blade):
+ """Delete DNS Settings"""
+ changed = True
+ if not module.check_mode:
+ changed = False
+ current_dns = blade.dns.list_dns()
+ if current_dns.items[0].domain or current_dns.items[0].nameservers != []:
+ try:
+ blade.dns.update_dns(dns_settings=Dns(domain="", nameservers=[]))
+ changed = True
+ except Exception:
+ module.fail_json(msg="Deletion of DNS settings failed")
+ module.exit_json(changed=changed)
+
+
+def update_dns(module, blade):
+ """Set DNS Settings"""
+ changed = False
+ current_dns = blade.dns.list_dns()
+ if module.params["domain"]:
+ if current_dns.items[0].domain != module.params["domain"]:
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.dns.update_dns(
+ dns_settings=Dns(domain=module.params["domain"])
+ )
+ except Exception:
+ module.fail_json(msg="Update of DNS domain failed")
+ if module.params["nameservers"]:
+ if sorted(module.params["nameservers"]) != sorted(
+ current_dns.items[0].nameservers
+ ):
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.dns.update_dns(
+ dns_settings=Dns(nameservers=module.params["nameservers"])
+ )
+ except Exception:
+ module.fail_json(msg="Update of DNS nameservers failed")
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ nameservers=dict(type="list", elements="str"),
+ search=dict(type="list", elements="str"),
+ domain=dict(type="str"),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ blade = get_blade(module)
+
+ if module.params["state"] == "absent":
+ delete_dns(module, blade)
+ elif module.params["state"] == "present":
+ if module.params["nameservers"]:
+ module.params["nameservers"] = remove(module.params["nameservers"])
+ if module.params["search"]:
+ module.warn(
+ "'search' parameter is deprecated and will be removed in Collection v1.6.0"
+ )
+ update_dns(module, blade)
+ else:
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_ds.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_ds.py
new file mode 100644
index 000000000..6433d3d9d
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_ds.py
@@ -0,0 +1,470 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2018, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_ds
+version_added: '1.0.0'
+short_description: Configure FlashBlade Directory Service
+description:
+- Create, modify or erase directory services configurations. There is no
+ facility to SSL certificates at this time. Use the FlashBlade GUI for this
+ additional configuration work.
+- If updating a directory service and i(bind_password) is provided this
+ will always cause a change, even if the password given isn't different from
+ the current. This makes this part of the module non-idempotent..
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Create or delete directory service configuration
+ default: present
+ type: str
+ choices: [ absent, present ]
+ dstype:
+ description:
+ - The type of directory service to work on
+ choices: [ management, nfs, smb ]
+ type: str
+ required: true
+ enable:
+ description:
+ - Whether to enable or disable directory service support.
+ default: false
+ type: bool
+ uri:
+ description:
+ - A list of up to 30 URIs of the directory servers. Each URI must include
+ the scheme ldap:// or ldaps:// (for LDAP over SSL), a hostname, and a
+ domain name or IP address. For example, ldap://ad.company.com configures
+ the directory service with the hostname "ad" in the domain "company.com"
+ while specifying the unencrypted LDAP protocol.
+ type: list
+ elements: str
+ base_dn:
+ description:
+ - Sets the base of the Distinguished Name (DN) of the directory service
+ groups. The base should consist of only Domain Components (DCs). The
+ base_dn will populate with a default value when a URI is entered by
+ parsing domain components from the URI. The base DN should specify DC=
+ for each domain component and multiple DCs should be separated by commas.
+ type: str
+ bind_password:
+ description:
+ - Sets the password of the bind_user user name account.
+ type: str
+ bind_user:
+ description:
+ - Sets the user name that can be used to bind to and query the directory.
+ - For Active Directory, enter the username - often referred to as
+ sAMAccountName or User Logon Name - of the account that is used to
+ perform directory lookups.
+ - For OpenLDAP, enter the full DN of the user.
+ type: str
+ nis_servers:
+ description:
+ - A list of up to 30 IP addresses or FQDNs for NIS servers.
+ - This cannot be used in conjunction with LDAP configurations.
+ type: list
+ elements: str
+ nis_domain:
+ description:
+ - The NIS domain to search
+ - This cannot be used in conjunction with LDAP configurations.
+ type: str
+ join_ou:
+ description:
+ - The optional organizational unit (OU) where the machine account
+ for the directory service will be created.
+ type: str
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Delete existing management directory service
+ purestorage.flashblade.purefb_ds:
+ dstype: management
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+
+- name: Create NFS directory service (disabled)
+ purestorage.flashblade.purefb_ds:
+ dstype: nfs
+ uri: "ldaps://lab.purestorage.com"
+ base_dn: "DC=lab,DC=purestorage,DC=com"
+ bind_user: Administrator
+ bind_password: password
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+
+- name: Enable existing SMB directory service
+ purestorage.flashblade.purefb_ds:
+ dstypr: smb
+ enable: true
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+
+- name: Disable existing management directory service
+ purestorage.flashblade.purefb_ds:
+ dstype: management
+ enable: false
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+
+- name: Create NFS directory service (enabled)
+ purestorage.flashblade.purefb_ds:
+ dstype: nfs
+ enable: true
+ uri: "ldaps://lab.purestorage.com"
+ base_dn: "DC=lab,DC=purestorage,DC=com"
+ bind_user: Administrator
+ bind_password: password
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+"""
+
+RETURN = r"""
+"""
+
+
+NIS_API_VERSION = "1.7"
+HAS_PURITY_FB = True
+try:
+ from purity_fb import DirectoryService
+except ImportError:
+ HAS_PURITY_FB = False
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+def enable_ds(module, blade):
+ """Enable Directory Service"""
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.directory_services.update_directory_services(
+ names=[module.params["dstype"]],
+ directory_service=DirectoryService(enabled=True),
+ )
+ changed = True
+ except Exception:
+ module.fail_json(
+ msg="Enable {0} Directory Service failed".format(
+ module.params["dstype"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def disable_ds(module, blade):
+ """Disable Directory Service"""
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.directory_services.update_directory_services(
+ names=[module.params["dstype"]],
+ directory_service=DirectoryService(enabled=False),
+ )
+ except Exception:
+ module.fail_json(
+ msg="Disable {0} Directory Service failed".format(
+ module.params["dstype"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def delete_ds(module, blade):
+ """Delete Directory Service"""
+ changed = True
+ if not module.check_mode:
+ dirserv = blade.directory_services.list_directory_services(
+ names=[module.params["dstype"]]
+ )
+ try:
+ if module.params["dstype"] == "management":
+ if dirserv.items[0].uris:
+ dir_service = DirectoryService(
+ uris=[""],
+ base_dn="",
+ bind_user="",
+ bind_password="",
+ enabled=False,
+ )
+ else:
+ changed = False
+ elif module.params["dstype"] == "smb":
+ if dirserv.items[0].uris:
+ smb_attrs = {"join_ou": ""}
+ dir_service = DirectoryService(
+ uris=[""],
+ base_dn="",
+ bind_user="",
+ bind_password="",
+ smb=smb_attrs,
+ enabled=False,
+ )
+ else:
+ changed = False
+ elif module.params["dstype"] == "nfs":
+ if dirserv.items[0].uris:
+ dir_service = DirectoryService(
+ uris=[""],
+ base_dn="",
+ bind_user="",
+ bind_password="",
+ enabled=False,
+ )
+ elif dirserv.items[0].nfs.nis_domains:
+ nfs_attrs = {"nis_domains": [], "nis_servers": []}
+ dir_service = DirectoryService(nfs=nfs_attrs, enabled=False)
+ else:
+ changed = False
+ if changed:
+ blade.directory_services.update_directory_services(
+ names=[module.params["dstype"]], directory_service=dir_service
+ )
+ except Exception:
+ module.fail_json(
+ msg="Delete {0} Directory Service failed".format(
+ module.params["dstype"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def update_ds(module, blade):
+ """Update Directory Service"""
+ mod_ds = False
+ attr = {}
+ try:
+ ds_now = blade.directory_services.list_directory_services(
+ names=[module.params["dstype"]]
+ ).items[0]
+ if module.params["dstype"] == "nfs" and module.params["nis_servers"]:
+ if sorted(module.params["nis_servers"]) != sorted(
+ ds_now.nfs.nis_servers
+ ) or module.params["nis_domain"] != "".join(
+ map(str, ds_now.nfs.nis_domains)
+ ):
+ attr["nfs"] = {
+ "nis_domains": [module.params["nis_domain"]],
+ "nis_servers": module.params["nis_servers"][0:30],
+ }
+ mod_ds = True
+ else:
+ if module.params["uri"]:
+ if sorted(module.params["uri"][0:30]) != sorted(ds_now.uris):
+ attr["uris"] = module.params["uri"][0:30]
+ mod_ds = True
+ if module.params["base_dn"]:
+ if module.params["base_dn"] != ds_now.base_dn:
+ attr["base_dn"] = module.params["base_dn"]
+ mod_ds = True
+ if module.params["bind_user"]:
+ if module.params["bind_user"] != ds_now.bind_user:
+ attr["bind_user"] = module.params["bind_user"]
+ mod_ds = True
+ if module.params["enable"]:
+ if module.params["enable"] != ds_now.enabled:
+ attr["enabled"] = module.params["enable"]
+ mod_ds = True
+ if module.params["bind_password"]:
+ attr["bind_password"] = module.params["bind_password"]
+ mod_ds = True
+ if module.params["dstype"] == "smb":
+ if module.params["join_ou"] != ds_now.smb.join_ou:
+ attr["smb"] = {"join_ou": module.params["join_ou"]}
+ mod_ds = True
+ if mod_ds:
+ changed = True
+ if not module.check_mode:
+ n_attr = DirectoryService(**attr)
+ try:
+ blade.directory_services.update_directory_services(
+ names=[module.params["dstype"]], directory_service=n_attr
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to change {0} directory service.".format(
+ module.params["dstype"]
+ )
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to get current {0} directory service.".format(
+ module.params["dstype"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def create_ds(module, blade):
+ """Create Directory Service"""
+ changed = True
+ if not module.check_mode:
+ try:
+ if module.params["dstype"] == "management":
+ if module.params["uri"]:
+ dir_service = DirectoryService(
+ uris=module.params["uri"][0:30],
+ base_dn=module.params["base_dn"],
+ bind_user=module.params["bind_user"],
+ bind_password=module.params["bind_password"],
+ enabled=module.params["enable"],
+ )
+ else:
+ module.fail_json(
+ msg="Incorrect parameters provided for dstype {0}".format(
+ module.params["dstype"]
+ )
+ )
+ elif module.params["dstype"] == "smb":
+ if module.params["uri"]:
+ smb_attrs = {"join_ou": module.params["join_ou"]}
+ dir_service = DirectoryService(
+ uris=module.params["uri"][0:30],
+ base_dn=module.params["base_dn"],
+ bind_user=module.params["bind_user"],
+ bind_password=module.params["bind_password"],
+ smb=smb_attrs,
+ enabled=module.params["enable"],
+ )
+ else:
+ module.fail_json(
+ msg="Incorrect parameters provided for dstype {0}".format(
+ module.params["dstype"]
+ )
+ )
+ elif module.params["dstype"] == "nfs":
+ if module.params["nis_domain"]:
+ nfs_attrs = {
+ "nis_domains": [module.params["nis_domain"]],
+ "nis_servers": module.params["nis_servers"][0:30],
+ }
+ dir_service = DirectoryService(
+ nfs=nfs_attrs, enabled=module.params["enable"]
+ )
+ else:
+ dir_service = DirectoryService(
+ uris=module.params["uri"][0:30],
+ base_dn=module.params["base_dn"],
+ bind_user=module.params["bind_user"],
+ bind_password=module.params["bind_password"],
+ enabled=module.params["enable"],
+ )
+ blade.directory_services.update_directory_services(
+ names=[module.params["dstype"]], directory_service=dir_service
+ )
+ except Exception:
+ module.fail_json(
+ msg="Create {0} Directory Service failed".format(
+ module.params["dstype"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ uri=dict(type="list", elements="str"),
+ dstype=dict(
+ required=True, type="str", choices=["management", "nfs", "smb"]
+ ),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ enable=dict(type="bool", default=False),
+ bind_password=dict(type="str", no_log=True),
+ bind_user=dict(type="str"),
+ base_dn=dict(type="str"),
+ join_ou=dict(type="str"),
+ nis_domain=dict(type="str"),
+ nis_servers=dict(type="list", elements="str"),
+ )
+ )
+
+ required_together = [
+ ["uri", "bind_password", "bind_user", "base_dn"],
+ ["nis_servers", "nis_domain"],
+ ]
+ mutually_exclusive = [["uri", "nis_domain"]]
+
+ module = AnsibleModule(
+ argument_spec,
+ required_together=required_together,
+ mutually_exclusive=mutually_exclusive,
+ supports_check_mode=True,
+ )
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ api_version = blade.api_version.list_versions().versions
+ ds_configured = False
+ dirserv = blade.directory_services.list_directory_services(
+ names=[module.params["dstype"]]
+ )
+ ds_enabled = dirserv.items[0].enabled
+ if dirserv.items[0].base_dn is not None:
+ ds_configured = True
+ if (module.params["nis_domain"] or module.params["join_ou"]) and (
+ NIS_API_VERSION not in api_version
+ ):
+ module.fail_json(
+ msg="NFS or SMB directory service attributes not supported by FlashBlade Purity version"
+ )
+ ldap_uri = False
+ set_ldap = False
+ for uri in range(0, len(dirserv.items[0].uris)):
+ if "ldap" in dirserv.items[0].uris[uri].lower():
+ ldap_uri = True
+ if module.params["uri"]:
+ for uri in range(0, len(module.params["uri"])):
+ if "ldap" in module.params["uri"][uri].lower():
+ set_ldap = True
+ if not module.params["uri"] and ldap_uri or module.params["uri"] and set_ldap:
+ if module.params["nis_servers"] or module.params["nis_domain"]:
+ module.fail_json(
+ msg="NIS configuration not supported in an LDAP environment"
+ )
+ if state == "absent":
+ delete_ds(module, blade)
+ elif ds_configured and module.params["enable"] and ds_enabled:
+ update_ds(module, blade)
+ elif ds_configured and not module.params["enable"] and ds_enabled:
+ disable_ds(module, blade)
+ elif ds_configured and module.params["enable"] and not ds_enabled:
+ enable_ds(module, blade)
+ # Now we have enabled the DS lets make sure there aren't any new updates...
+ update_ds(module, blade)
+ elif not ds_configured and state == "present":
+ create_ds(module, blade)
+ else:
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_dsrole.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_dsrole.py
new file mode 100644
index 000000000..61934cc6e
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_dsrole.py
@@ -0,0 +1,213 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2018, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_dsrole
+version_added: '1.0.0'
+short_description: Configure FlashBlade Management Directory Service Roles
+description:
+- Set or erase directory services role configurations.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Create or delete directory service role
+ default: present
+ type: str
+ choices: [ absent, present ]
+ role:
+ description:
+ - The directory service role to work on
+ choices: [ array_admin, ops_admin, readonly, storage_admin ]
+ type: str
+ required: true
+ group_base:
+ description:
+ - Specifies where the configured group is located in the directory
+ tree. This field consists of Organizational Units (OUs) that combine
+ with the base DN attribute and the configured group CNs to complete
+ the full Distinguished Name of the groups. The group base should
+ specify OU= for each OU and multiple OUs should be separated by commas.
+ The order of OUs is important and should get larger in scope from left
+ to right.
+ - Each OU should not exceed 64 characters in length.
+ type: str
+ group:
+ description:
+ - Sets the common Name (CN) of the configured directory service group
+ containing users for the FlashBlade. This name should be just the
+ Common Name of the group without the CN= specifier.
+ - Common Names should not exceed 64 characters in length.
+ type: str
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Delete existing array_admin directory service role
+ purestorage.flashblade.purefb_dsrole:
+ role: array_admin
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+
+- name: Create array_admin directory service role
+ purestorage.flashblade.purefb_dsrole:
+ role: array_admin
+ group_base: "OU=PureGroups,OU=SANManagers"
+ group: pureadmins
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+
+- name: Update ops_admin directory service role
+ purestorage.flashblade.purefb_dsrole:
+ role: ops_admin
+ group_base: "OU=PureGroups"
+ group: opsgroup
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+"""
+
+RETURN = r"""
+"""
+
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import DirectoryServiceRole
+except ImportError:
+ HAS_PURITY_FB = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+def update_role(module, blade):
+ """Update Directory Service Role"""
+ changed = False
+ role = blade.directory_services.list_directory_services_roles(
+ names=[module.params["role"]]
+ )
+ if (
+ role.items[0].group_base != module.params["group_base"]
+ or role.items[0].group != module.params["group"]
+ ):
+ changed = True
+ if not module.check_mode:
+ try:
+ role = DirectoryServiceRole(
+ group_base=module.params["group_base"], group=module.params["group"]
+ )
+ blade.directory_services.update_directory_services_roles(
+ names=[module.params["role"]], directory_service_role=role
+ )
+ except Exception:
+ module.fail_json(
+ msg="Update Directory Service Role {0} failed".format(
+ module.params["role"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def delete_role(module, blade):
+ """Delete Directory Service Role"""
+ changed = True
+ if not module.check_mode:
+ try:
+ role = DirectoryServiceRole(group_base="", group="")
+ blade.directory_services.update_directory_services_roles(
+ names=[module.params["role"]], directory_service_role=role
+ )
+ except Exception:
+ module.fail_json(
+ msg="Delete Directory Service Role {0} failed".format(
+ module.params["role"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def create_role(module, blade):
+ """Create Directory Service Role"""
+ changed = True
+ if not module.check_mode:
+ try:
+ role = DirectoryServiceRole(
+ group_base=module.params["group_base"], group=module.params["group"]
+ )
+ blade.directory_services.update_directory_services_roles(
+ names=[module.params["role"]], directory_service_role=role
+ )
+ except Exception:
+ module.fail_json(
+ msg="Create Directory Service Role {0} failed".format(
+ module.params["role"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ role=dict(
+ required=True,
+ type="str",
+ choices=["array_admin", "ops_admin", "readonly", "storage_admin"],
+ ),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ group_base=dict(type="str"),
+ group=dict(type="str"),
+ )
+ )
+
+ required_together = [["group", "group_base"]]
+
+ module = AnsibleModule(
+ argument_spec, required_together=required_together, supports_check_mode=True
+ )
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ role_configured = False
+ role = blade.directory_services.list_directory_services_roles(
+ names=[module.params["role"]]
+ )
+ if role.items[0].group is not None:
+ role_configured = True
+
+ if state == "absent" and role_configured:
+ delete_role(module, blade)
+ elif role_configured and state == "present":
+ update_role(module, blade)
+ elif not role_configured and state == "present":
+ create_role(module, blade)
+ else:
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_eula.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_eula.py
new file mode 100644
index 000000000..83b5e656a
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_eula.py
@@ -0,0 +1,131 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2021, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_eula
+version_added: '1.6.0'
+short_description: Sign Pure Storage FlashBlade EULA
+description:
+- Sign the FlashBlade EULA for Day 0 config, or change signatory.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ company:
+ description:
+ - Full legal name of the entity.
+ - The value must be between 1 and 64 characters in length.
+ type: str
+ required: true
+ name:
+ description:
+ - Full legal name of the individual at the company who has the authority to accept the terms of the agreement.
+ - The value must be between 1 and 64 characters in length.
+ type: str
+ required: true
+ title:
+ description:
+ - Individual's job title at the company.
+ - The value must be between 1 and 64 characters in length.
+ type: str
+ required: true
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Sign EULA for FlashBlade
+ purestorage.flashblade.purefb_eula:
+ company: "ACME Storage, Inc."
+ name: "Fred Bloggs"
+ title: "Storage Manager"
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import Eula, EulaSignature
+except ImportError:
+ HAS_PURITY_FB = False
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_system,
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+EULA_API_VERSION = "2.0"
+
+
+def set_eula(module, blade):
+ """Sign EULA"""
+ changed = False
+ if not module.check_mode:
+ current_eula = list(blade.get_arrays_eula().items)[0].signature
+ if not current_eula.accepted:
+ if (
+ current_eula.company != module.params["company"]
+ or current_eula.title != module.params["title"]
+ or current_eula.name != module.params["name"]
+ ):
+ signature = EulaSignature(
+ company=module.params["company"],
+ title=module.params["title"],
+ name=module.params["name"],
+ )
+ eula_body = Eula(signature=signature)
+ if not module.check_mode:
+ changed = True
+ rc = blade.patch_arrays_eula(eula=eula_body)
+ if rc.status_code != 200:
+ module.fail_json(msg="Signing EULA failed")
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ company=dict(type="str", required=True),
+ name=dict(type="str", required=True),
+ title=dict(type="str", required=True),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ blade = get_blade(module)
+ api_version = blade.api_version.list_versions().versions
+ api_version = blade.api_version.list_versions().versions
+ if EULA_API_VERSION not in api_version:
+ module.fail_json(msg="Purity//FB must be upgraded to support this module.")
+ blade = get_system(module)
+ set_eula(module, blade)
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_fs.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_fs.py
new file mode 100644
index 000000000..a07180793
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_fs.py
@@ -0,0 +1,944 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2017, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+---
+module: purefb_fs
+version_added: "1.0.0"
+short_description: Manage filesystemon Pure Storage FlashBlade`
+description:
+ - This module manages filesystems on Pure Storage FlashBlade.
+author: Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - Filesystem Name.
+ required: true
+ type: str
+ state:
+ description:
+ - Create, delete or modifies a filesystem.
+ required: false
+ default: present
+ type: str
+ choices: [ "present", "absent" ]
+ eradicate:
+ description:
+ - Define whether to eradicate the filesystem on delete or leave in trash.
+ required: false
+ type: bool
+ default: false
+ size:
+ description:
+ - Volume size in M, G, T or P units. See examples.
+ - If size is not set at filesystem creation time the filesystem size becomes unlimited.
+ type: str
+ required: false
+ nfsv3:
+ description:
+ - Define whether to NFSv3 protocol is enabled for the filesystem.
+ required: false
+ type: bool
+ default: true
+ nfsv4:
+ description:
+ - Define whether to NFSv4.1 protocol is enabled for the filesystem.
+ required: false
+ type: bool
+ default: true
+ nfs_rules:
+ description:
+ - Define the NFS rules in operation.
+ - If not set at filesystem creation time it defaults to I(*(rw,no_root_squash))
+ - Supported binary options are ro/rw, secure/insecure, fileid_32bit/no_fileid_32bit,
+ root_squash/no_root_squash, all_squash/no_all_squash and atime/noatime
+ - Supported non-binary options are anonuid=#, anongid=#, sec=(sys|krb5)
+ - Superceeded by I(export_policy) if provided
+ required: false
+ type: str
+ smb:
+ description:
+ - Define whether to SMB protocol is enabled for the filesystem.
+ required: false
+ type: bool
+ default: false
+ smb_aclmode:
+ description:
+ - Specify the ACL mode for the SMB protocol.
+ - Deprecated from Purity//FB 3.1.1. Use I(access_control) instead.
+ required: false
+ type: str
+ default: shared
+ choices: [ "shared", "native" ]
+ http:
+ description:
+ - Define whether to HTTP/HTTPS protocol is enabled for the filesystem.
+ required: false
+ type: bool
+ default: false
+ snapshot:
+ description:
+ - Define whether a snapshot directory is enabled for the filesystem.
+ required: false
+ type: bool
+ default: false
+ writable:
+ description:
+ - Define if a filesystem is writeable.
+ required: false
+ type: bool
+ promote:
+ description:
+ - Promote/demote a filesystem.
+ - Can only demote the file-system if it is in a replica-link relationship.
+ required: false
+ type: bool
+ fastremove:
+ description:
+ - Define whether the fast remove directory is enabled for the filesystem.
+ required: false
+ type: bool
+ default: false
+ hard_limit:
+ description:
+ - Define whether the capacity for a filesystem is a hard limit.
+ - CAUTION This will cause the filesystem to go Read-Only if the
+ capacity has already exceeded the logical size of the filesystem.
+ required: false
+ type: bool
+ default: false
+ user_quota:
+ description:
+ - Default quota in M, G, T or P units for a user under this file system.
+ required: false
+ type: str
+ group_quota:
+ description:
+ - Default quota in M, G, T or P units for a group under this file system.
+ required: false
+ type: str
+ policy:
+ description:
+ - Filesystem policy to assign to or remove from a filesystem.
+ required: false
+ type: str
+ policy_state:
+ description:
+ - Add or delete a policy from a filesystem
+ required: false
+ default: present
+ type: str
+ choices: [ "absent", "present" ]
+ delete_link:
+ description:
+ - Define if the filesystem can be deleted even if it has a replica link
+ required: false
+ default: false
+ type: bool
+ discard_snaps:
+ description:
+ - Allow a filesystem to be demoted.
+ required: false
+ default: false
+ type: bool
+ access_control:
+ description:
+ - The access control style that is utilized for client actions such
+ as setting file and directory ACLs.
+ - Only available from Purity//FB 3.1.1
+ type: str
+ default: shared
+ choices: [ 'nfs', 'smb', 'shared', 'independent', 'mode-bits' ]
+ safeguard_acls:
+ description:
+ - Safeguards ACLs on a filesystem.
+ - Performs different roles depending on the filesystem protocol enabled.
+ - See Purity//FB documentation for detailed description.
+ - Only available from Purity//FB 3.1.1
+ type: bool
+ default: true
+ export_policy:
+ description:
+ - Name of NFS export policy to assign to filesystem
+ - Overrides I(nfs_rules)
+ - Only valid for Purity//FB 3.3.0 or higher
+ type: str
+ version_added: "1.9.0"
+extends_documentation_fragment:
+ - purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = """
+- name: Create new filesystem named foo
+ purestorage.flashblade.purefb_fs:
+ name: foo
+ size: 1T
+ state: present
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Delete filesystem named foo
+ purestorage.flashblade.purefb_fs:
+ name: foo
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Recover filesystem named foo
+ purestorage.flashblade.purefb_fs:
+ name: foo
+ state: present
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Eradicate filesystem named foo
+ purestorage.flashblade.purefb_fs:
+ name: foo
+ state: absent
+ eradicate: true
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Promote filesystem named foo ready for failover
+ purestorage.flashblade.purefb_fs:
+ name: foo
+ promote: true
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Demote filesystem named foo after failover
+ purestorage.flashblade.purefb_fs:
+ name: foo
+ promote: false
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Modify attributes of an existing filesystem named foo
+ purestorage.flashblade.purefb_fs:
+ name: foo
+ size: 2T
+ nfsv3 : false
+ nfsv4 : true
+ user_quota: 10K
+ group_quota: 25M
+ nfs_rules: '10.21.200.0/24(ro)'
+ snapshot: true
+ fastremove: true
+ hard_limit: true
+ smb: true
+ state: present
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641"""
+
+RETURN = """
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import (
+ FileSystem,
+ ProtocolRule,
+ NfsRule,
+ SmbRule,
+ MultiProtocolRule,
+ rest,
+ )
+except ImportError:
+ HAS_PURITY_FB = False
+
+HAS_PYPURECLIENT = True
+try:
+ from pypureclient.flashblade import (
+ FileSystemPatch,
+ NfsPatch,
+ Reference,
+ )
+except ImportError:
+ HAS_PYPURECLIENT = False
+
+HAS_JSON = True
+try:
+ import json
+except ImportError:
+ HAS_JSON = False
+
+from ansible.module_utils.basic import AnsibleModule, human_to_bytes
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ get_system,
+ purefb_argument_spec,
+)
+
+
+HARD_LIMIT_API_VERSION = "1.4"
+NFSV4_API_VERSION = "1.6"
+REPLICATION_API_VERSION = "1.9"
+MULTIPROTOCOL_API_VERSION = "1.11"
+EXPORT_POLICY_API_VERSION = "2.3"
+
+
+def get_fs(module, blade):
+ """Return Filesystem or None"""
+ fsys = []
+ fsys.append(module.params["name"])
+ try:
+ res = blade.file_systems.list_file_systems(names=fsys)
+ return res.items[0]
+ except Exception:
+ return None
+
+
+def create_fs(module, blade):
+ """Create Filesystem"""
+ changed = True
+ if not module.check_mode:
+ try:
+ if not module.params["nfs_rules"]:
+ module.params["nfs_rules"] = "*(rw,no_root_squash)"
+ if module.params["size"]:
+ size = human_to_bytes(module.params["size"])
+ else:
+ size = 0
+
+ if module.params["user_quota"]:
+ user_quota = human_to_bytes(module.params["user_quota"])
+ else:
+ user_quota = None
+ if module.params["group_quota"]:
+ group_quota = human_to_bytes(module.params["group_quota"])
+ else:
+ group_quota = None
+
+ api_version = blade.api_version.list_versions().versions
+ if HARD_LIMIT_API_VERSION in api_version:
+ if NFSV4_API_VERSION in api_version:
+ if REPLICATION_API_VERSION in api_version:
+ if MULTIPROTOCOL_API_VERSION in api_version:
+ if module.params["access_control"] == "nfs" and not (
+ module.params["nfsv3"] or module.params["nfsv4"]
+ ):
+ module.fail_json(
+ msg="Cannot set access_control to nfs when NFS is not enabled."
+ )
+ if (
+ module.params["access_control"]
+ in ["smb", "independent"]
+ and not module.params["smb"]
+ ):
+ module.fail_json(
+ msg="Cannot set access_control to smb or independent when SMB is not enabled."
+ )
+ if module.params["safeguard_acls"] and (
+ module.params["access_control"]
+ in ["mode-bits", "independent"]
+ or module.params["smb"]
+ ):
+ module.fail_json(
+ msg="ACL Safeguarding cannot be enabled with SMB or if access_control is mode-bits or independent."
+ )
+ fs_obj = FileSystem(
+ name=module.params["name"],
+ provisioned=size,
+ fast_remove_directory_enabled=module.params[
+ "fastremove"
+ ],
+ hard_limit_enabled=module.params["hard_limit"],
+ snapshot_directory_enabled=module.params["snapshot"],
+ nfs=NfsRule(
+ v3_enabled=module.params["nfsv3"],
+ v4_1_enabled=module.params["nfsv4"],
+ rules=module.params["nfs_rules"],
+ ),
+ smb=SmbRule(enabled=module.params["smb"]),
+ http=ProtocolRule(enabled=module.params["http"]),
+ multi_protocol=MultiProtocolRule(
+ safeguard_acls=module.params["safeguard_acls"],
+ access_control_style=module.params[
+ "access_control"
+ ],
+ ),
+ default_user_quota=user_quota,
+ default_group_quota=group_quota,
+ )
+ else:
+ fs_obj = FileSystem(
+ name=module.params["name"],
+ provisioned=size,
+ fast_remove_directory_enabled=module.params[
+ "fastremove"
+ ],
+ hard_limit_enabled=module.params["hard_limit"],
+ snapshot_directory_enabled=module.params["snapshot"],
+ nfs=NfsRule(
+ v3_enabled=module.params["nfsv3"],
+ v4_1_enabled=module.params["nfsv4"],
+ rules=module.params["nfs_rules"],
+ ),
+ smb=SmbRule(
+ enabled=module.params["smb"],
+ acl_mode=module.params["smb_aclmode"],
+ ),
+ http=ProtocolRule(enabled=module.params["http"]),
+ default_user_quota=user_quota,
+ default_group_quota=group_quota,
+ )
+ else:
+ fs_obj = FileSystem(
+ name=module.params["name"],
+ provisioned=size,
+ fast_remove_directory_enabled=module.params["fastremove"],
+ hard_limit_enabled=module.params["hard_limit"],
+ snapshot_directory_enabled=module.params["snapshot"],
+ nfs=NfsRule(
+ v3_enabled=module.params["nfsv3"],
+ v4_1_enabled=module.params["nfsv4"],
+ rules=module.params["nfs_rules"],
+ ),
+ smb=ProtocolRule(enabled=module.params["smb"]),
+ http=ProtocolRule(enabled=module.params["http"]),
+ default_user_quota=user_quota,
+ default_group_quota=group_quota,
+ )
+ else:
+ fs_obj = FileSystem(
+ name=module.params["name"],
+ provisioned=size,
+ fast_remove_directory_enabled=module.params["fastremove"],
+ hard_limit_enabled=module.params["hard_limit"],
+ snapshot_directory_enabled=module.params["snapshot"],
+ nfs=NfsRule(
+ enabled=module.params["nfsv3"],
+ rules=module.params["nfs_rules"],
+ ),
+ smb=ProtocolRule(enabled=module.params["smb"]),
+ http=ProtocolRule(enabled=module.params["http"]),
+ )
+ else:
+ fs_obj = FileSystem(
+ name=module.params["name"],
+ provisioned=size,
+ fast_remove_directory_enabled=module.params["fastremove"],
+ snapshot_directory_enabled=module.params["snapshot"],
+ nfs=NfsRule(
+ enabled=module.params["nfs"], rules=module.params["nfs_rules"]
+ ),
+ smb=ProtocolRule(enabled=module.params["smb"]),
+ http=ProtocolRule(enabled=module.params["http"]),
+ )
+ blade.file_systems.create_file_systems(fs_obj)
+ except rest.ApiException as err:
+ message = json.loads(err.body)["errors"][0]["message"]
+ module.fail_json(
+ msg="Failed to create filesystem {0}. Error: {1}".format(
+ module.params["name"], message
+ )
+ )
+ if REPLICATION_API_VERSION in api_version:
+ if module.params["policy"]:
+ try:
+ blade.policies.list_policies(names=[module.params["policy"]])
+ except Exception:
+ _delete_fs(module, blade)
+ module.fail_json(
+ msg="Policy {0} doesn't exist.".format(module.params["policy"])
+ )
+ try:
+ blade.policies.create_policy_filesystems(
+ policy_names=[module.params["policy"]],
+ member_names=[module.params["name"]],
+ )
+ except Exception:
+ _delete_fs(module, blade)
+ module.fail_json(
+ msg="Failed to apply policy {0} when creating filesystem {1}.".format(
+ module.params["policy"], module.params["name"]
+ )
+ )
+ if EXPORT_POLICY_API_VERSION in api_version and module.params["export_policy"]:
+ system = get_system(module)
+ export_attr = FileSystemPatch(
+ nfs=NfsPatch(
+ export_policy=Reference(name=module.params["export_policy"])
+ )
+ )
+ res = system.patch_file_systems(
+ names=[module.params["name"]], file_system=export_attr
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Filesystem {0} created, but failed to assign export "
+ "policy {1}. Error: {2}".format(
+ module.params["name"],
+ module.params["export_policy"],
+ res.errors[0].message,
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def modify_fs(module, blade):
+ """Modify Filesystem"""
+ changed = False
+ mod_fs = False
+ attr = {}
+ if module.params["policy"] and module.params["policy_state"] == "present":
+ try:
+ policy = blade.policies.list_policy_filesystems(
+ policy_names=[module.params["policy"]],
+ member_names=[module.params["name"]],
+ )
+ except Exception:
+ module.fail_json(
+ msg="Policy {0} does not exist.".format(module.params["policy"])
+ )
+ if not policy.items:
+ try:
+ blade.policies.create_policy_filesystems(
+ policy_names=[module.params["policy"]],
+ member_names=[module.params["name"]],
+ )
+ mod_fs = True
+ except Exception:
+ module.fail_json(
+ msg="Failed to add filesystem {0} to policy {1}.".format(
+ module.params["name"], module.params["polict"]
+ )
+ )
+ if module.params["policy"] and module.params["policy_state"] == "absent":
+ try:
+ policy = blade.policies.list_policy_filesystems(
+ policy_names=[module.params["policy"]],
+ member_names=[module.params["name"]],
+ )
+ except Exception:
+ module.fail_json(
+ msg="Policy {0} does not exist.".format(module.params["policy"])
+ )
+ if len(policy.items) == 1:
+ try:
+ blade.policies.delete_policy_filesystems(
+ policy_names=[module.params["policy"]],
+ member_names=[module.params["name"]],
+ )
+ mod_fs = True
+ except Exception:
+ module.fail_json(
+ msg="Failed to remove filesystem {0} to policy {1}.".format(
+ module.params["name"], module.params["polict"]
+ )
+ )
+ if module.params["user_quota"]:
+ user_quota = human_to_bytes(module.params["user_quota"])
+ if module.params["group_quota"]:
+ group_quota = human_to_bytes(module.params["group_quota"])
+ fsys = get_fs(module, blade)
+ if fsys.destroyed:
+ attr["destroyed"] = False
+ mod_fs = True
+ if module.params["size"]:
+ if human_to_bytes(module.params["size"]) != fsys.provisioned:
+ attr["provisioned"] = human_to_bytes(module.params["size"])
+ mod_fs = True
+ api_version = blade.api_version.list_versions().versions
+ if NFSV4_API_VERSION in api_version:
+ v3_state = v4_state = None
+ if module.params["nfsv3"] and not fsys.nfs.v3_enabled:
+ v3_state = module.params["nfsv3"]
+ if not module.params["nfsv3"] and fsys.nfs.v3_enabled:
+ v3_state = module.params["nfsv3"]
+ if module.params["nfsv4"] and not fsys.nfs.v4_1_enabled:
+ v4_state = module.params["nfsv4"]
+ if not module.params["nfsv4"] and fsys.nfs.v4_1_enabled:
+ v4_state = module.params["nfsv4"]
+ if v3_state is not None or v4_state is not None:
+ attr["nfs"] = NfsRule(v4_1_enabled=v4_state, v3_enabled=v3_state)
+ mod_fs = True
+ if (
+ module.params["nfsv3"]
+ or module.params["nfsv4"]
+ and fsys.nfs.v3_enabled
+ or fsys.nfs.v4_1_enabled
+ ):
+ if module.params["nfs_rules"] is not None:
+ if fsys.nfs.rules != module.params["nfs_rules"]:
+ attr["nfs"] = NfsRule(rules=module.params["nfs_rules"])
+ mod_fs = True
+ if module.params["user_quota"] and user_quota != fsys.default_user_quota:
+ attr["default_user_quota"] = user_quota
+ mod_fs = True
+ if module.params["group_quota"] and group_quota != fsys.default_group_quota:
+ attr["default_group_quota"] = group_quota
+ mod_fs = True
+ else:
+ if module.params["nfsv3"] and not fsys.nfs.enabled:
+ attr["nfs"] = NfsRule(enabled=module.params["nfsv3"])
+ mod_fs = True
+ if not module.params["nfsv3"] and fsys.nfs.enabled:
+ attr["nfs"] = NfsRule(enabled=module.params["nfsv3"])
+ mod_fs = True
+ if module.params["nfsv3"] and fsys.nfs.enabled:
+ if fsys.nfs.rules != module.params["nfs_rules"]:
+ attr["nfs"] = NfsRule(rules=module.params["nfs_rules"])
+ mod_fs = True
+ if REPLICATION_API_VERSION in api_version:
+ if module.params["smb"] and not fsys.smb.enabled:
+ if MULTIPROTOCOL_API_VERSION in api_version:
+ attr["smb"] = SmbRule(enabled=module.params["smb"])
+ else:
+ attr["smb"] = SmbRule(
+ enabled=module.params["smb"], acl_mode=module.params["smb_aclmode"]
+ )
+ mod_fs = True
+ if not module.params["smb"] and fsys.smb.enabled:
+ attr["smb"] = ProtocolRule(enabled=module.params["smb"])
+ mod_fs = True
+ if (
+ module.params["smb"]
+ and fsys.smb.enabled
+ and MULTIPROTOCOL_API_VERSION not in api_version
+ ):
+ if fsys.smb.acl_mode != module.params["smb_aclmode"]:
+ attr["smb"] = SmbRule(
+ enabled=module.params["smb"], acl_mode=module.params["smb_aclmode"]
+ )
+ mod_fs = True
+ else:
+ if module.params["smb"] and not fsys.smb.enabled:
+ attr["smb"] = ProtocolRule(enabled=module.params["smb"])
+ mod_fs = True
+ if not module.params["smb"] and fsys.smb.enabled:
+ attr["smb"] = ProtocolRule(enabled=module.params["smb"])
+ mod_fs = True
+ if module.params["http"] and not fsys.http.enabled:
+ attr["http"] = ProtocolRule(enabled=module.params["http"])
+ mod_fs = True
+ if not module.params["http"] and fsys.http.enabled:
+ attr["http"] = ProtocolRule(enabled=module.params["http"])
+ mod_fs = True
+ if module.params["snapshot"] and not fsys.snapshot_directory_enabled:
+ attr["snapshot_directory_enabled"] = module.params["snapshot"]
+ mod_fs = True
+ if not module.params["snapshot"] and fsys.snapshot_directory_enabled:
+ attr["snapshot_directory_enabled"] = module.params["snapshot"]
+ mod_fs = True
+ if module.params["fastremove"] and not fsys.fast_remove_directory_enabled:
+ attr["fast_remove_directory_enabled"] = module.params["fastremove"]
+ mod_fs = True
+ if not module.params["fastremove"] and fsys.fast_remove_directory_enabled:
+ attr["fast_remove_directory_enabled"] = module.params["fastremove"]
+ mod_fs = True
+ if HARD_LIMIT_API_VERSION in api_version:
+ if not module.params["hard_limit"] and fsys.hard_limit_enabled:
+ attr["hard_limit_enabled"] = module.params["hard_limit"]
+ mod_fs = True
+ if module.params["hard_limit"] and not fsys.hard_limit_enabled:
+ attr["hard_limit_enabled"] = module.params["hard_limit"]
+ mod_fs = True
+ if MULTIPROTOCOL_API_VERSION in api_version:
+ if module.params["safeguard_acls"] and not fsys.multi_protocol.safeguard_acls:
+ attr["multi_protocol"] = MultiProtocolRule(safeguard_acls=True)
+ mod_fs = True
+ if not module.params["safeguard_acls"] and fsys.multi_protocol.safeguard_acls:
+ attr["multi_protocol"] = MultiProtocolRule(safeguard_acls=False)
+ mod_fs = True
+ if module.params["access_control"] != fsys.multi_protocol.access_control_style:
+ attr["multi_protocol"] = MultiProtocolRule(
+ access_control_style=module.params["access_control"]
+ )
+ mod_fs = True
+ if REPLICATION_API_VERSION in api_version:
+ if module.params["writable"] is not None:
+ if not module.params["writable"] and fsys.writable:
+ attr["writable"] = module.params["writable"]
+ mod_fs = True
+ if (
+ module.params["writable"]
+ and not fsys.writable
+ and fsys.promotion_status == "promoted"
+ ):
+ attr["writable"] = module.params["writable"]
+ mod_fs = True
+ if module.params["promote"] is not None:
+ if module.params["promote"] and fsys.promotion_status != "promoted":
+ attr["requested_promotion_state"] = "promoted"
+ mod_fs = True
+ if not module.params["promote"] and fsys.promotion_status == "promoted":
+ # Demotion only allowed on filesystems in a replica-link
+ try:
+ blade.file_system_replica_links.list_file_system_replica_links(
+ local_file_system_names=[module.params["name"]]
+ ).items[0]
+ except Exception:
+ module.fail_json(
+ msg="Filesystem {0} not demoted. Not in a replica-link".format(
+ module.params["name"]
+ )
+ )
+ attr["requested_promotion_state"] = module.params["promote"]
+ mod_fs = True
+ if mod_fs:
+ changed = True
+ if not module.check_mode:
+ n_attr = FileSystem(**attr)
+ if REPLICATION_API_VERSION in api_version:
+ try:
+ blade.file_systems.update_file_systems(
+ name=module.params["name"],
+ attributes=n_attr,
+ discard_non_snapshotted_data=module.params["discard_snaps"],
+ )
+ except rest.ApiException as err:
+ message = json.loads(err.body)["errors"][0]["message"]
+ module.fail_json(
+ msg="Failed to update filesystem {0}. Error {1}".format(
+ module.params["name"], message
+ )
+ )
+ else:
+ try:
+ blade.file_systems.update_file_systems(
+ name=module.params["name"], attributes=n_attr
+ )
+ except rest.ApiException as err:
+ message = json.loads(err.body)["errors"][0]["message"]
+ module.fail_json(
+ msg="Failed to update filesystem {0}. Error {1}".format(
+ module.params["name"], message
+ )
+ )
+ if EXPORT_POLICY_API_VERSION in api_version and module.params["export_policy"]:
+ system = get_system(module)
+ change_export = False
+ current_fs = list(
+ system.get_file_systems(filter="name='" + module.params["name"] + "'").items
+ )[0]
+ if (
+ current_fs.nfs.export_policy.name
+ and current_fs.nfs.export_policy.name != module.params["export_policy"]
+ ):
+ change_export = True
+ if not current_fs.nfs.export_policy.name and module.params["export_policy"]:
+ change_export = True
+ if change_export and not module.check_mode:
+ export_attr = FileSystemPatch(
+ nfs=NfsPatch(
+ export_policy=Reference(name=module.params["export_policy"])
+ )
+ )
+ res = system.patch_file_systems(
+ names=[module.params["name"]], file_system=export_attr
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to modify export policy {1} for "
+ "filesystem {0}. Error: {2}".format(
+ module.params["name"],
+ module.params["export_policy"],
+ res.errors[0].message,
+ )
+ )
+
+ module.exit_json(changed=changed)
+
+
+def _delete_fs(module, blade):
+ """In module Delete Filesystem"""
+ api_version = blade.api_version.list_versions().versions
+ if NFSV4_API_VERSION in api_version:
+ if MULTIPROTOCOL_API_VERSION in api_version:
+ blade.file_systems.update_file_systems(
+ name=module.params["name"],
+ attributes=FileSystem(
+ nfs=NfsRule(v3_enabled=False, v4_1_enabled=False),
+ smb=ProtocolRule(enabled=False),
+ http=ProtocolRule(enabled=False),
+ multi_protocol=MultiProtocolRule(access_control_style="shared"),
+ destroyed=True,
+ ),
+ )
+ else:
+ blade.file_systems.update_file_systems(
+ name=module.params["name"],
+ attributes=FileSystem(
+ nfs=NfsRule(v3_enabled=False, v4_1_enabled=False),
+ smb=ProtocolRule(enabled=False),
+ http=ProtocolRule(enabled=False),
+ destroyed=True,
+ ),
+ )
+ else:
+ blade.file_systems.update_file_systems(
+ name=module.params["name"],
+ attributes=FileSystem(
+ nfs=NfsRule(enabled=False),
+ smb=ProtocolRule(enabled=False),
+ http=ProtocolRule(enabled=False),
+ destroyed=True,
+ ),
+ )
+
+ blade.file_systems.delete_file_systems(module.params["name"])
+
+
+def delete_fs(module, blade):
+ """Delete Filesystem"""
+ changed = True
+ if not module.check_mode:
+ try:
+ api_version = blade.api_version.list_versions().versions
+ if REPLICATION_API_VERSION in api_version:
+ if NFSV4_API_VERSION in api_version:
+ blade.file_systems.update_file_systems(
+ name=module.params["name"],
+ attributes=FileSystem(
+ nfs=NfsRule(v3_enabled=False, v4_1_enabled=False),
+ smb=ProtocolRule(enabled=False),
+ http=ProtocolRule(enabled=False),
+ destroyed=True,
+ ),
+ delete_link_on_eradication=module.params["delete_link"],
+ )
+ else:
+ blade.file_systems.update_file_systems(
+ name=module.params["name"],
+ attributes=FileSystem(
+ nfs=NfsRule(enabled=False),
+ smb=ProtocolRule(enabled=False),
+ http=ProtocolRule(enabled=False),
+ destroyed=True,
+ ),
+ delete_link_on_eradication=module.params["delete_link"],
+ )
+ else:
+ if NFSV4_API_VERSION in api_version:
+ blade.file_systems.update_file_systems(
+ name=module.params["name"],
+ attributes=FileSystem(
+ nfs=NfsRule(v3_enabled=False, v4_1_enabled=False),
+ smb=ProtocolRule(enabled=False),
+ http=ProtocolRule(enabled=False),
+ destroyed=True,
+ ),
+ )
+ else:
+ blade.file_systems.update_file_systems(
+ name=module.params["name"],
+ attributes=FileSystem(
+ nfs=NfsRule(enabled=False),
+ smb=ProtocolRule(enabled=False),
+ http=ProtocolRule(enabled=False),
+ destroyed=True,
+ ),
+ )
+ if module.params["eradicate"]:
+ try:
+ blade.file_systems.delete_file_systems(name=module.params["name"])
+ except Exception:
+ module.fail_json(
+ msg="Failed to delete filesystem {0}.".format(
+ module.params["name"]
+ )
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to update filesystem {0} prior to deletion.".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def eradicate_fs(module, blade):
+ """Eradicate Filesystem"""
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.file_systems.delete_file_systems(name=module.params["name"])
+ except Exception:
+ module.fail_json(
+ msg="Failed to eradicate filesystem {0}.".format(module.params["name"])
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ eradicate=dict(default="false", type="bool"),
+ nfsv3=dict(default="true", type="bool"),
+ nfsv4=dict(default="true", type="bool"),
+ nfs_rules=dict(type="str"),
+ smb=dict(default="false", type="bool"),
+ http=dict(default="false", type="bool"),
+ snapshot=dict(default="false", type="bool"),
+ writable=dict(type="bool"),
+ promote=dict(type="bool"),
+ fastremove=dict(default="false", type="bool"),
+ hard_limit=dict(default="false", type="bool"),
+ user_quota=dict(type="str"),
+ policy=dict(type="str"),
+ group_quota=dict(type="str"),
+ smb_aclmode=dict(
+ type="str", default="shared", choices=["shared", "native"]
+ ),
+ policy_state=dict(default="present", choices=["present", "absent"]),
+ state=dict(default="present", choices=["present", "absent"]),
+ delete_link=dict(default=False, type="bool"),
+ discard_snaps=dict(default=False, type="bool"),
+ safeguard_acls=dict(default=True, type="bool"),
+ access_control=dict(
+ type="str",
+ default="shared",
+ choices=["nfs", "smb", "shared", "independent", "mode-bits"],
+ ),
+ size=dict(type="str"),
+ export_policy=dict(type="str"),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ if not HAS_JSON:
+ module.fail_json(msg="json sdk is required for this module")
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ fsys = get_fs(module, blade)
+
+ if module.params["eradicate"] and state == "present":
+ module.warn("Eradicate flag ignored without state=absent")
+
+ if state == "present" and not fsys:
+ create_fs(module, blade)
+ elif state == "present" and fsys:
+ modify_fs(module, blade)
+ elif state == "absent" and fsys and not fsys.destroyed:
+ delete_fs(module, blade)
+ elif state == "absent" and fsys and fsys.destroyed and module.params["eradicate"]:
+ eradicate_fs(module, blade)
+ elif state == "absent" and not fsys:
+ module.exit_json(changed=False)
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_fs_replica.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_fs_replica.py
new file mode 100644
index 000000000..f96903788
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_fs_replica.py
@@ -0,0 +1,308 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2020, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+---
+module: purefb_fs_replica
+version_added: '1.0.0'
+short_description: Manage filesystem replica links between Pure Storage FlashBlades
+description:
+ - This module manages filesystem replica links between Pure Storage FlashBlades.
+author: Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - Local Filesystem Name.
+ required: true
+ type: str
+ state:
+ description:
+ - Creates or modifies a filesystem replica link
+ required: false
+ default: present
+ type: str
+ choices: [ "present", "absent" ]
+ target_array:
+ description:
+ - Remote array name to create replica on.
+ required: false
+ type: str
+ target_fs:
+ description:
+ - Name of target filesystem name
+ - If not supplied, will default to I(name).
+ type: str
+ required: false
+ policy:
+ description:
+ - Name of filesystem snapshot policy to apply to the replica link.
+ required: false
+ type: str
+extends_documentation_fragment:
+ - purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = """
+- name: Create new filesystem replica from foo to bar on arrayB
+ purestorage.flashblade.purefb_fs_replica:
+ name: foo
+ target_array: arrayB
+ target_fs: bar
+ policy: daily
+ state: present
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Add new snapshot policy to exisitng filesystem replica link
+ purestorage.flashblade.purefb_fs_replica:
+ name: foo
+ policy: weekly
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Delete snapshot policy from filesystem replica foo
+ purestorage.flashblade.purefb_fs_replica:
+ name: foo
+ policy: weekly
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641"""
+
+RETURN = """
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import FileSystemReplicaLink, LocationReference
+except ImportError:
+ HAS_PURITY_FB = False
+
+MIN_REQUIRED_API_VERSION = "1.9"
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+def get_local_fs(module, blade):
+ """Return Filesystem or None"""
+ try:
+ res = blade.file_systems.list_file_systems(names=[module.params["name"]])
+ return res.items[0]
+ except Exception:
+ return None
+
+
+def get_local_rl(module, blade):
+ """Return Filesystem Replica Link or None"""
+ try:
+ res = blade.file_system_replica_links.list_file_system_replica_links(
+ local_file_system_names=[module.params["name"]]
+ )
+ return res.items[0]
+ except Exception:
+ return None
+
+
+def _check_connected(module, blade):
+ connected_blades = blade.array_connections.list_array_connections()
+ for target in range(0, len(connected_blades.items)):
+ if (
+ connected_blades.items[target].remote.name == module.params["target_array"]
+ or connected_blades.items[target].management_address
+ == module.params["target_array"]
+ ) and connected_blades.items[target].status in [
+ "connected",
+ "connecting",
+ "partially_connected",
+ ]:
+ return connected_blades.items[target]
+ return None
+
+
+def create_rl(module, blade):
+ """Create Filesystem Replica Link"""
+ changed = True
+ if not module.check_mode:
+ try:
+ remote_array = _check_connected(module, blade)
+ if remote_array:
+ if not module.params["target_fs"]:
+ module.params["target_fs"] = module.params["name"]
+ if not module.params["policy"]:
+ blade.file_system_replica_links.create_file_system_replica_links(
+ local_file_system_names=[module.params["name"]],
+ remote_file_system_names=[module.params["target_fs"]],
+ remote_names=[remote_array.remote.name],
+ )
+ else:
+ blade.file_system_replica_links.create_file_system_replica_links(
+ local_file_system_names=[module.params["name"]],
+ remote_file_system_names=[module.params["target_fs"]],
+ remote_names=[remote_array.remote.name],
+ file_system_replica_link=FileSystemReplicaLink(
+ policies=[LocationReference(name=module.params["policy"])]
+ ),
+ )
+ else:
+ module.fail_json(
+ msg="Target array {0} is not connected".format(
+ module.params["target_array"]
+ )
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to create filesystem replica link for {0}.".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def add_rl_policy(module, blade):
+ """Add Policy to Filesystem Replica Link"""
+ changed = False
+ if not module.params["target_array"]:
+ module.params["target_array"] = (
+ blade.file_system_replica_links.list_file_system_replica_links(
+ local_file_system_names=[module.params["name"]]
+ )
+ .items[0]
+ .remote.name
+ )
+ remote_array = _check_connected(module, blade)
+ try:
+ already_a_policy = (
+ blade.file_system_replica_links.list_file_system_replica_link_policies(
+ local_file_system_names=[module.params["name"]],
+ policy_names=[module.params["policy"]],
+ remote_names=[remote_array.remote.name],
+ )
+ )
+ if not already_a_policy.items:
+ changed = True
+ if not module.check_mode:
+ blade.file_system_replica_links.create_file_system_replica_link_policies(
+ policy_names=[module.params["policy"]],
+ local_file_system_names=[module.params["name"]],
+ remote_names=[remote_array.remote.name],
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to add policy {0} to replica link {1}.".format(
+ module.params["policy"], module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def delete_rl_policy(module, blade):
+ """Delete Policy from Filesystem Replica Link"""
+ changed = True
+ if not module.check_mode:
+ current_policy = (
+ blade.file_system_replica_links.list_file_system_replica_link_policies(
+ local_file_system_names=[module.params["name"]],
+ policy_names=[module.params["policy"]],
+ )
+ )
+ if current_policy.items:
+ try:
+ blade.file_system_replica_links.delete_file_system_replica_link_policies(
+ policy_names=[module.params["policy"]],
+ local_file_system_names=[module.params["name"]],
+ remote_names=[current_policy.items[0].link.remote.name],
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to remove policy {0} from replica link {1}.".format(
+ module.params["policy"], module.params["name"]
+ )
+ )
+ else:
+ changed = False
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ target_fs=dict(type="str"),
+ target_array=dict(type="str"),
+ policy=dict(type="str"),
+ state=dict(default="present", choices=["present", "absent"]),
+ )
+ )
+
+ required_if = [["state", "absent", ["policy"]]]
+
+ module = AnsibleModule(
+ argument_spec, required_if=required_if, supports_check_mode=True
+ )
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ versions = blade.api_version.list_versions().versions
+
+ if MIN_REQUIRED_API_VERSION not in versions:
+ module.fail_json(
+ msg="Minimum FlashBlade REST version required: {0}".format(
+ MIN_REQUIRED_API_VERSION
+ )
+ )
+
+ local_fs = get_local_fs(module, blade)
+ local_replica_link = get_local_rl(module, blade)
+
+ if not local_fs:
+ module.fail_json(
+ msg="Selected local filesystem {0} does not exist.".format(
+ module.params["name"]
+ )
+ )
+
+ if module.params["policy"]:
+ try:
+ policy = blade.policies.list_policies(names=[module.params["policy"]])
+ except Exception:
+ module.fail_json(
+ msg="Selected policy {0} does not exist.".format(
+ module.params["policy"]
+ )
+ )
+ else:
+ policy = None
+ if state == "present" and not local_replica_link:
+ create_rl(module, blade)
+ elif state == "present" and local_replica_link and policy:
+ add_rl_policy(module, blade)
+ elif state == "absent" and policy:
+ delete_rl_policy(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_groupquota.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_groupquota.py
new file mode 100644
index 000000000..2ae610275
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_groupquota.py
@@ -0,0 +1,321 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2021, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+---
+module: purefb_groupquota
+version_added: "1.7.0"
+short_description: Manage filesystem group quotas
+description:
+ - This module manages group quotas for filesystems on Pure Storage FlashBlade.
+author: Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - Filesystem Name.
+ required: true
+ type: str
+ state:
+ description:
+ - Create, delete or modifies a quota.
+ required: false
+ default: present
+ type: str
+ choices: [ "present", "absent" ]
+ quota:
+ description:
+ - Group quota in M, G, T or P units. This cannot be 0.
+ - This value will override the file system's default group quota.
+ type: str
+ gid:
+ description:
+ - The group id on which the quota is enforced.
+ - Cannot be combined with I(gname)
+ type: int
+ gname:
+ description:
+ - The group name on which the quota is enforced.
+ - Cannot be combined with I(gid)
+ type: str
+extends_documentation_fragment:
+ - purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = """
+- name: Create new group (using GID) quota for filesystem named foo
+ purestorage.flashblade.purefb_groupquota:
+ name: foo
+ quota: 1T
+ gid: 1234
+ state: present
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Create new group (using groupname) quota for filesystem named foo
+ purestorage.flashblade.purefb_groupquota:
+ name: foo
+ quota: 1T
+ gname: bar
+ state: present
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Delete group quota on filesystem foo for group by GID
+ purestorage.flashblade.purefb_groupquota:
+ name: foo
+ gid: 1234
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Delete group quota on filesystem foo for group by groupname
+ purestorage.flashblade.purefb_groupquota:
+ name: foo
+ gname: bar
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Update group quota on filesystem foo for group by groupname
+ purestorage.flashblade.purefb_groupquota:
+ name: foo
+ quota: 20G
+ gname: bar
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Update group quota on filesystem foo for group by GID
+ purestorage.flashblade.purefb_groupquota:
+ name: foo
+ quota: 20G
+ gid: bar
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641"""
+
+RETURN = """
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import QuotasGroup
+except ImportError:
+ HAS_PURITY_FB = False
+
+from ansible.module_utils.basic import AnsibleModule, human_to_bytes
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.6"
+
+
+def get_fs(module, blade):
+ """Return Filesystem or None"""
+ fsys = []
+ fsys.append(module.params["name"])
+ try:
+ res = blade.file_systems.list_file_systems(names=fsys)
+ return res.items[0]
+ except Exception:
+ return None
+
+
+def get_quota(module, blade):
+ """Return Filesystem User Quota or None"""
+ fsys = []
+ fsys.append(module.params["name"])
+ try:
+ if module.params["gid"]:
+ res = blade.quotas_groups.list_group_quotas(
+ file_system_names=fsys, filter="group.id=" + str(module.params["gid"])
+ )
+ else:
+ res = blade.quotas_groups.list_group_quotas(
+ file_system_names=fsys,
+ filter="group.name='" + module.params["gname"] + "'",
+ )
+ return res.items[0]
+ except Exception:
+ return None
+
+
+def create_quota(module, blade):
+ """Create Filesystem User Quota"""
+ changed = True
+ if not module.check_mode:
+ try:
+ if module.params["gid"]:
+ blade.quotas_groups.create_group_quotas(
+ file_system_names=[module.params["name"]],
+ gids=[module.params["gid"]],
+ quota=QuotasGroup(
+ quota=int(human_to_bytes(module.params["quota"]))
+ ),
+ )
+ else:
+ blade.quotas_groups.create_group_quotas(
+ file_system_names=[module.params["name"]],
+ group_names=[module.params["gname"]],
+ quota=QuotasGroup(
+ quota=int(human_to_bytes(module.params["quota"]))
+ ),
+ )
+ except Exception:
+ if module.params["gid"]:
+ module.fail_json(
+ msg="Failed to create quote for UID {0} on filesystem {1}.".format(
+ module.params["gid"], module.params["name"]
+ )
+ )
+ else:
+ module.fail_json(
+ msg="Failed to create quote for groupname {0} on filesystem {1}.".format(
+ module.params["gname"], module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def update_quota(module, blade):
+ """Upodate Filesystem User Quota"""
+ changed = False
+ current_quota = get_quota(module, blade)
+ if current_quota.quota != human_to_bytes(module.params["quota"]):
+ changed = True
+ if not module.check_mode:
+ if module.params["gid"]:
+ try:
+ blade.quotas_groups.update_group_quotas(
+ file_system_names=[module.params["name"]],
+ gids=[module.params["gid"]],
+ quota=QuotasGroup(
+ quota=int(human_to_bytes(module.params["quota"]))
+ ),
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to update quota for UID {0} on filesystem {1}.".format(
+ module.params["gid"], module.params["name"]
+ )
+ )
+ else:
+ try:
+ blade.quotas_groups.update_group_quotas(
+ file_system_names=[module.params["name"]],
+ group_names=[module.params["gname"]],
+ quota=QuotasGroup(
+ quota=int(human_to_bytes(module.params["quota"]))
+ ),
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to update quota for UID {0} on filesystem {1}.".format(
+ module.params["gname"], module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def delete_quota(module, blade):
+ """Delete Filesystem User Quota"""
+ changed = True
+ if not module.check_mode:
+ try:
+ if module.params["gid"]:
+ blade.quotas_groups.delete_group_quotas(
+ file_system_names=[module.params["name"]],
+ gids=[module.params["gid"]],
+ )
+ else:
+ blade.quotas_groups.delete_group_quotas(
+ file_system_names=[module.params["name"]],
+ group_names=[module.params["gname"]],
+ )
+ except Exception:
+ if module.params["gid"]:
+ module.fail_json(
+ msg="Failed to delete quota for UID {0} on filesystem {1}.".format(
+ module.params["gid"], module.params["name"]
+ )
+ )
+ else:
+ module.fail_json(
+ msg="Failed to delete quota for groupname {0} on filesystem {1}.".format(
+ module.params["gname"], module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ gid=dict(type="int"),
+ gname=dict(type="str"),
+ state=dict(default="present", choices=["present", "absent"]),
+ quota=dict(type="str"),
+ )
+ )
+
+ mutually_exclusive = [["gid", "gname"]]
+ required_if = [["state", "present", ["quota"]]]
+ module = AnsibleModule(
+ argument_spec,
+ mutually_exclusive=mutually_exclusive,
+ required_if=required_if,
+ supports_check_mode=True,
+ )
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ versions = blade.api_version.list_versions().versions
+
+ if MIN_REQUIRED_API_VERSION not in versions:
+ module.fail_json(
+ msg="Minimum FlashBlade REST version required: {0}".format(
+ MIN_REQUIRED_API_VERSION
+ )
+ )
+ fsys = get_fs(module, blade)
+ if not fsys:
+ module.fail_json(
+ msg="Filesystem {0} does not exist.".format(module.params["name"])
+ )
+ quota = get_quota(module, blade)
+
+ if state == "present" and not quota:
+ create_quota(module, blade)
+ elif state == "present" and quota:
+ update_quota(module, blade)
+ elif state == "absent" and quota:
+ delete_quota(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_info.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_info.py
new file mode 100644
index 000000000..8525bd8e3
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_info.py
@@ -0,0 +1,1548 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2019, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or
+# https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_info
+version_added: '1.0.0'
+short_description: Collect information from Pure Storage FlashBlade
+description:
+ - Collect information from a Pure Storage FlashBlade running the
+ Purity//FB operating system. By default, the module will collect basic
+ information including hosts, host groups, protection
+ groups and volume counts. Additional information can be collected
+ based on the configured set of arguements.
+author:
+ - Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ gather_subset:
+ description:
+ - When supplied, this argument will define the information to be collected.
+ Possible values for this include all, minimum, config, performance,
+ capacity, network, subnets, lags, filesystems, snapshots, buckets,
+ replication, policies, arrays, accounts, admins, ad, kerberos
+ and drives.
+ required: false
+ type: list
+ elements: str
+ default: minimum
+extends_documentation_fragment:
+ - purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: collect default set of info
+ purestorage.flashblade.purefb_info:
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+ register: blade_info
+- name: show default information
+ debug:
+ msg: "{{ blade_info['purefb_info']['default'] }}"
+
+- name: collect configuration and capacity info
+ purestorage.flashblade.purefb_info:
+ gather_subset:
+ - config
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+ register: blade_info
+- name: show config information
+ debug:
+ msg: "{{ blade_info['purefb_info']['config'] }}"
+
+- name: collect all info
+ purestorage.flashblade.purefb_info:
+ gather_subset:
+ - all
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+ register: blade_info
+- name: show all information
+ debug:
+ msg: "{{ blade_info['purefb_info'] }}"
+"""
+
+RETURN = r"""
+purefb_info:
+ description: Returns the information collected from the FlashBlade
+ returned: always
+ type: complex
+ sample: {
+ "admins": {
+ "pureuser": {
+ "api_token_timeout": null,
+ "local": true,
+ "public_key": null
+ },
+ "another_user": {
+ "api_token_timeout": null,
+ "local": false,
+ "public_key": null
+ },
+ },
+ "buckets": {
+ "central": {
+ "account_name": "jake",
+ "bucket_type": "classic",
+ "created": 1628900154000,
+ "data_reduction": null,
+ "destroyed": false,
+ "id": "43758f09-9e71-7bf7-5757-2028a95a2b65",
+ "lifecycle_rules": {},
+ "object_count": 0,
+ "snapshot_space": 0,
+ "time_remaining": null,
+ "total_physical_space": 0,
+ "unique_space": 0,
+ "versioning": "none",
+ "virtual_space": 0
+ },
+ "test": {
+ "account_name": "acme",
+ "bucket_type": "classic",
+ "created": 1630591952000,
+ "data_reduction": 3.6,
+ "destroyed": false,
+ "id": "d5f6149c-fbef-f3c5-58b6-8fd143110ba9",
+ "lifecycle_rules": {
+ "test": {
+ "abort_incomplete_multipart_uploads_after (days)": 1,
+ "cleanup_expired_object_delete_marker": true,
+ "enabled": true,
+ "keep_current_version_for (days)": null,
+ "keep_current_version_until": "2023-12-21",
+ "keep_previous_version_for (days)": null,
+ "prefix": "foo"
+ }
+ },
+ },
+ },
+ "capacity": {
+ "aggregate": {
+ "data_reduction": 1.1179228,
+ "snapshots": 0,
+ "total_physical": 17519748439,
+ "unique": 17519748439,
+ "virtual": 19585726464
+ },
+ "file-system": {
+ "data_reduction": 1.3642412,
+ "snapshots": 0,
+ "total_physical": 4748219708,
+ "unique": 4748219708,
+ "virtual": 6477716992
+ },
+ "object-store": {
+ "data_reduction": 1.0263462,
+ "snapshots": 0,
+ "total_physical": 12771528731,
+ "unique": 12771528731,
+ "virtual": 6477716992
+ },
+ "total": 83359896948925
+ },
+ "config": {
+ "alert_watchers": {
+ "enabled": true,
+ "name": "notify@acmestorage.com"
+ },
+ "array_management": {
+ "base_dn": null,
+ "bind_password": null,
+ "bind_user": null,
+ "enabled": false,
+ "name": "management",
+ "services": [
+ "management"
+ ],
+ "uris": []
+ },
+ "directory_service_roles": {
+ "array_admin": {
+ "group": null,
+ "group_base": null
+ },
+ "ops_admin": {
+ "group": null,
+ "group_base": null
+ },
+ "readonly": {
+ "group": null,
+ "group_base": null
+ },
+ "storage_admin": {
+ "group": null,
+ "group_base": null
+ }
+ },
+ "dns": {
+ "domain": "demo.acmestorage.com",
+ "name": "demo-fb-1",
+ "nameservers": [
+ "8.8.8.8"
+ ],
+ "search": [
+ "demo.acmestorage.com"
+ ]
+ },
+ "nfs_directory_service": {
+ "base_dn": null,
+ "bind_password": null,
+ "bind_user": null,
+ "enabled": false,
+ "name": "nfs",
+ "services": [
+ "nfs"
+ ],
+ "uris": []
+ },
+ "ntp": [
+ "0.ntp.pool.org"
+ ],
+ "smb_directory_service": {
+ "base_dn": null,
+ "bind_password": null,
+ "bind_user": null,
+ "enabled": false,
+ "name": "smb",
+ "services": [
+ "smb"
+ ],
+ "uris": []
+ },
+ "smtp": {
+ "name": "demo-fb-1",
+ "relay_host": null,
+ "sender_domain": "acmestorage.com"
+ },
+ "ssl_certs": {
+ "certificate": "-----BEGIN CERTIFICATE-----\n\n-----END CERTIFICATE-----",
+ "common_name": "Acme Storage",
+ "country": "US",
+ "email": null,
+ "intermediate_certificate": null,
+ "issued_by": "Acme Storage",
+ "issued_to": "Acme Storage",
+ "key_size": 4096,
+ "locality": null,
+ "name": "global",
+ "organization": "Acme Storage",
+ "organizational_unit": "Acme Storage",
+ "passphrase": null,
+ "private_key": null,
+ "state": null,
+ "status": "self-signed",
+ "valid_from": "1508433967000",
+ "valid_to": "2458833967000"
+ }
+ },
+ "default": {
+ "blades": 15,
+ "buckets": 7,
+ "filesystems": 2,
+ "flashblade_name": "demo-fb-1",
+ "object_store_accounts": 1,
+ "object_store_users": 1,
+ "purity_version": "2.2.0",
+ "snapshots": 1,
+ "total_capacity": 83359896948925,
+ "smb_mode": "native"
+ },
+ "filesystems": {
+ "k8s-pvc-d24b1357-579e-11e8-811f-ecf4bbc88f54": {
+ "default_group_quota": 0,
+ "default_user_quota": 0,
+ "destroyed": false,
+ "fast_remove": false,
+ "hard_limit": true,
+ "nfs_rules": "10.21.255.0/24(rw,no_root_squash)",
+ "provisioned": 21474836480,
+ "snapshot_enabled": false
+ },
+ "z": {
+ "default_group_quota": 0,
+ "default_user_quota": 0,
+ "destroyed": false,
+ "fast_remove": false,
+ "hard_limit": false,
+ "provisioned": 1073741824,
+ "snapshot_enabled": false
+ }
+ },
+ "lag": {
+ "uplink": {
+ "lag_speed": 0,
+ "port_speed": 40000000000,
+ "ports": [
+ {
+ "name": "CH1.FM1.ETH1.1"
+ },
+ {
+ "name": "CH1.FM1.ETH1.2"
+ },
+ ],
+ "status": "healthy"
+ }
+ },
+ "network": {
+ "fm1.admin0": {
+ "address": "10.10.100.6",
+ "gateway": "10.10.100.1",
+ "mtu": 1500,
+ "netmask": "255.255.255.0",
+ "services": [
+ "support"
+ ],
+ "type": "vip",
+ "vlan": 2200
+ },
+ "fm2.admin0": {
+ "address": "10.10.100.7",
+ "gateway": "10.10.100.1",
+ "mtu": 1500,
+ "netmask": "255.255.255.0",
+ "services": [
+ "support"
+ ],
+ "type": "vip",
+ "vlan": 2200
+ },
+ "nfs1": {
+ "address": "10.10.100.4",
+ "gateway": "10.10.100.1",
+ "mtu": 1500,
+ "netmask": "255.255.255.0",
+ "services": [
+ "data"
+ ],
+ "type": "vip",
+ "vlan": 2200
+ },
+ "vir0": {
+ "address": "10.10.100.5",
+ "gateway": "10.10.100.1",
+ "mtu": 1500,
+ "netmask": "255.255.255.0",
+ "services": [
+ "management"
+ ],
+ "type": "vip",
+ "vlan": 2200
+ }
+ },
+ "performance": {
+ "aggregate": {
+ "bytes_per_op": 0,
+ "bytes_per_read": 0,
+ "bytes_per_write": 0,
+ "read_bytes_per_sec": 0,
+ "reads_per_sec": 0,
+ "usec_per_other_op": 0,
+ "usec_per_read_op": 0,
+ "usec_per_write_op": 0,
+ "write_bytes_per_sec": 0,
+ "writes_per_sec": 0
+ },
+ "http": {
+ "bytes_per_op": 0,
+ "bytes_per_read": 0,
+ "bytes_per_write": 0,
+ "read_bytes_per_sec": 0,
+ "reads_per_sec": 0,
+ "usec_per_other_op": 0,
+ "usec_per_read_op": 0,
+ "usec_per_write_op": 0,
+ "write_bytes_per_sec": 0,
+ "writes_per_sec": 0
+ },
+ "nfs": {
+ "bytes_per_op": 0,
+ "bytes_per_read": 0,
+ "bytes_per_write": 0,
+ "read_bytes_per_sec": 0,
+ "reads_per_sec": 0,
+ "usec_per_other_op": 0,
+ "usec_per_read_op": 0,
+ "usec_per_write_op": 0,
+ "write_bytes_per_sec": 0,
+ "writes_per_sec": 0
+ },
+ "s3": {
+ "bytes_per_op": 0,
+ "bytes_per_read": 0,
+ "bytes_per_write": 0,
+ "read_bytes_per_sec": 0,
+ "reads_per_sec": 0,
+ "usec_per_other_op": 0,
+ "usec_per_read_op": 0,
+ "usec_per_write_op": 0,
+ "write_bytes_per_sec": 0,
+ "writes_per_sec": 0
+ }
+ },
+ "snapshots": {
+ "z.188": {
+ "destroyed": false,
+ "source": "z",
+ "source_destroyed": false,
+ "suffix": "188"
+ }
+ },
+ "subnet": {
+ "new-mgmt": {
+ "gateway": "10.10.100.1",
+ "interfaces": [
+ {
+ "name": "fm1.admin0"
+ },
+ {
+ "name": "fm2.admin0"
+ },
+ {
+ "name": "nfs1"
+ },
+ {
+ "name": "vir0"
+ }
+ ],
+ "lag": "uplink",
+ "mtu": 1500,
+ "prefix": "10.10.100.0/24",
+ "services": [
+ "data",
+ "management",
+ "support"
+ ],
+ "vlan": 2200
+ }
+ }
+ }
+"""
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ get_system,
+ purefb_argument_spec,
+)
+from datetime import datetime
+
+
+MIN_REQUIRED_API_VERSION = "1.3"
+HARD_LIMIT_API_VERSION = "1.4"
+POLICIES_API_VERSION = "1.5"
+CERT_GROUPS_API_VERSION = "1.8"
+REPLICATION_API_VERSION = "1.9"
+MULTIPROTOCOL_API_VERSION = "1.11"
+MIN_32_API = "2.0"
+LIFECYCLE_API_VERSION = "2.1"
+SMB_MODE_API_VERSION = "2.2"
+NFS_POLICY_API_VERSION = "2.3"
+VSO_VERSION = "2.4"
+DRIVES_API_VERSION = "2.5"
+SECURITY_API_VERSION = "2.7"
+BUCKET_API_VERSION = "2.8"
+
+
+def _millisecs_to_time(millisecs):
+ if millisecs:
+ return (str(int(millisecs / 3600000 % 24)).zfill(2) + ":00",)
+ return None
+
+
+def _bytes_to_human(bytes_number):
+ if bytes_number:
+ labels = ["B/s", "KB/s", "MB/s", "GB/s", "TB/s", "PB/s"]
+ i = 0
+ double_bytes = bytes_number
+ while i < len(labels) and bytes_number >= 1024:
+ double_bytes = bytes_number / 1024.0
+ i += 1
+ bytes_number = bytes_number / 1024
+ return str(round(double_bytes, 2)) + " " + labels[i]
+ return None
+
+
+def generate_default_dict(module, blade):
+ default_info = {}
+ defaults = blade.arrays.list_arrays().items[0]
+ default_info["flashblade_name"] = defaults.name
+ default_info["purity_version"] = defaults.version
+ default_info["filesystems"] = len(blade.file_systems.list_file_systems().items)
+ default_info["snapshots"] = len(
+ blade.file_system_snapshots.list_file_system_snapshots().items
+ )
+ default_info["buckets"] = len(blade.buckets.list_buckets().items)
+ default_info["object_store_users"] = len(
+ blade.object_store_users.list_object_store_users().items
+ )
+ default_info["object_store_accounts"] = len(
+ blade.object_store_accounts.list_object_store_accounts().items
+ )
+ default_info["blades"] = len(blade.blade.list_blades().items)
+ default_info["certificates"] = len(blade.certificates.list_certificates().items)
+ default_info["total_capacity"] = blade.arrays.list_arrays_space().items[0].capacity
+ api_version = blade.api_version.list_versions().versions
+ default_info["api_versions"] = api_version
+ if POLICIES_API_VERSION in api_version:
+ default_info["policies"] = len(blade.policies.list_policies().items)
+ if CERT_GROUPS_API_VERSION in api_version:
+ default_info["certificate_groups"] = len(
+ blade.certificate_groups.list_certificate_groups().items
+ )
+ if REPLICATION_API_VERSION in api_version:
+ default_info["fs_replicas"] = len(
+ blade.file_system_replica_links.list_file_system_replica_links().items
+ )
+ default_info["remote_credentials"] = len(
+ blade.object_store_remote_credentials.list_object_store_remote_credentials().items
+ )
+ default_info["bucket_replicas"] = len(
+ blade.bucket_replica_links.list_bucket_replica_links().items
+ )
+ default_info["connected_arrays"] = len(
+ blade.array_connections.list_array_connections().items
+ )
+ default_info["targets"] = len(blade.targets.list_targets().items)
+ default_info["kerberos_keytabs"] = len(blade.keytabs.list_keytabs().items)
+ # This section is just for REST 2.x features
+ if MIN_32_API in api_version:
+ blade = get_system(module)
+ blade_info = list(blade.get_arrays().items)[0]
+ default_info["object_store_virtual_hosts"] = len(
+ blade.get_object_store_virtual_hosts().items
+ )
+ default_info["api_clients"] = len(blade.get_api_clients().items)
+ default_info["idle_timeout"] = int(blade_info.idle_timeout / 60000)
+ if list(blade.get_arrays_eula().items)[0].signature.accepted:
+ default_info["EULA"] = "Signed"
+ else:
+ default_info["EULA"] = "Not Signed"
+ if NFS_POLICY_API_VERSION in api_version:
+ admin_settings = list(blade.get_admins_settings().items)[0]
+ default_info["max_login_attempts"] = admin_settings.max_login_attempts
+ default_info["min_password_length"] = admin_settings.min_password_length
+ if admin_settings.lockout_duration:
+ default_info["lockout_duration"] = (
+ str(admin_settings.lockout_duration / 1000) + " seconds"
+ )
+ if NFS_POLICY_API_VERSION in api_version:
+ default_info["smb_mode"] = blade_info.smb_mode
+ if VSO_VERSION in api_version:
+ default_info["timezone"] = blade_info.time_zone
+ if DRIVES_API_VERSION in api_version:
+ default_info["product_type"] = getattr(
+ blade_info, "product_type", "Unknown"
+ )
+ if SECURITY_API_VERSION in api_version:
+ dar = blade_info.encryption.data_at_rest
+ default_info["encryption"] = {
+ "data_at_rest_enabled": dar.enabled,
+ "data_at_rest_algorithms": dar.algorithms,
+ "data_at_rest_entropy_source": dar.entropy_source,
+ }
+ keys = list(blade.get_support_verification_keys().items)
+ default_info["support_keys"] = {}
+ for key in range(0, len(keys)):
+ keyname = keys[key].name
+ default_info["support_keys"][keyname] = {keys[key].verification_key}
+ default_info["security_update"] = getattr(
+ blade_info, "security_update", None
+ )
+
+ return default_info
+
+
+def generate_perf_dict(blade):
+ perf_info = {}
+ total_perf = blade.arrays.list_arrays_performance()
+ http_perf = blade.arrays.list_arrays_performance(protocol="http")
+ s3_perf = blade.arrays.list_arrays_performance(protocol="s3")
+ nfs_perf = blade.arrays.list_arrays_performance(protocol="nfs")
+ perf_info["aggregate"] = {
+ "bytes_per_op": total_perf.items[0].bytes_per_op,
+ "bytes_per_read": total_perf.items[0].bytes_per_read,
+ "bytes_per_write": total_perf.items[0].bytes_per_write,
+ "read_bytes_per_sec": total_perf.items[0].read_bytes_per_sec,
+ "reads_per_sec": total_perf.items[0].reads_per_sec,
+ "usec_per_other_op": total_perf.items[0].usec_per_other_op,
+ "usec_per_read_op": total_perf.items[0].usec_per_read_op,
+ "usec_per_write_op": total_perf.items[0].usec_per_write_op,
+ "write_bytes_per_sec": total_perf.items[0].write_bytes_per_sec,
+ "writes_per_sec": total_perf.items[0].writes_per_sec,
+ }
+ perf_info["http"] = {
+ "bytes_per_op": http_perf.items[0].bytes_per_op,
+ "bytes_per_read": http_perf.items[0].bytes_per_read,
+ "bytes_per_write": http_perf.items[0].bytes_per_write,
+ "read_bytes_per_sec": http_perf.items[0].read_bytes_per_sec,
+ "reads_per_sec": http_perf.items[0].reads_per_sec,
+ "usec_per_other_op": http_perf.items[0].usec_per_other_op,
+ "usec_per_read_op": http_perf.items[0].usec_per_read_op,
+ "usec_per_write_op": http_perf.items[0].usec_per_write_op,
+ "write_bytes_per_sec": http_perf.items[0].write_bytes_per_sec,
+ "writes_per_sec": http_perf.items[0].writes_per_sec,
+ }
+ perf_info["s3"] = {
+ "bytes_per_op": s3_perf.items[0].bytes_per_op,
+ "bytes_per_read": s3_perf.items[0].bytes_per_read,
+ "bytes_per_write": s3_perf.items[0].bytes_per_write,
+ "read_bytes_per_sec": s3_perf.items[0].read_bytes_per_sec,
+ "reads_per_sec": s3_perf.items[0].reads_per_sec,
+ "usec_per_other_op": s3_perf.items[0].usec_per_other_op,
+ "usec_per_read_op": s3_perf.items[0].usec_per_read_op,
+ "usec_per_write_op": s3_perf.items[0].usec_per_write_op,
+ "write_bytes_per_sec": s3_perf.items[0].write_bytes_per_sec,
+ "writes_per_sec": s3_perf.items[0].writes_per_sec,
+ }
+ perf_info["nfs"] = {
+ "bytes_per_op": nfs_perf.items[0].bytes_per_op,
+ "bytes_per_read": nfs_perf.items[0].bytes_per_read,
+ "bytes_per_write": nfs_perf.items[0].bytes_per_write,
+ "read_bytes_per_sec": nfs_perf.items[0].read_bytes_per_sec,
+ "reads_per_sec": nfs_perf.items[0].reads_per_sec,
+ "usec_per_other_op": nfs_perf.items[0].usec_per_other_op,
+ "usec_per_read_op": nfs_perf.items[0].usec_per_read_op,
+ "usec_per_write_op": nfs_perf.items[0].usec_per_write_op,
+ "write_bytes_per_sec": nfs_perf.items[0].write_bytes_per_sec,
+ "writes_per_sec": nfs_perf.items[0].writes_per_sec,
+ }
+ api_version = blade.api_version.list_versions().versions
+ if REPLICATION_API_VERSION in api_version:
+ file_repl_perf = (
+ blade.array_connections.list_array_connections_performance_replication(
+ type="file-system"
+ )
+ )
+ obj_repl_perf = (
+ blade.array_connections.list_array_connections_performance_replication(
+ type="object-store"
+ )
+ )
+ if len(file_repl_perf.total):
+ perf_info["file_replication"] = {
+ "received_bytes_per_sec": file_repl_perf.total[
+ 0
+ ].periodic.received_bytes_per_sec,
+ "transmitted_bytes_per_sec": file_repl_perf.total[
+ 0
+ ].periodic.transmitted_bytes_per_sec,
+ }
+ if len(obj_repl_perf.total):
+ perf_info["object_replication"] = {
+ "received_bytes_per_sec": obj_repl_perf.total[
+ 0
+ ].periodic.received_bytes_per_sec,
+ "transmitted_bytes_per_sec": obj_repl_perf.total[
+ 0
+ ].periodic.transmitted_bytes_per_sec,
+ }
+ return perf_info
+
+
+def generate_config_dict(blade):
+ config_info = {}
+ config_info["dns"] = blade.dns.list_dns().items[0].to_dict()
+ config_info["smtp"] = blade.smtp.list_smtp().items[0].to_dict()
+ try:
+ config_info["alert_watchers"] = (
+ blade.alert_watchers.list_alert_watchers().items[0].to_dict()
+ )
+ except Exception:
+ config_info["alert_watchers"] = ""
+ api_version = blade.api_version.list_versions().versions
+ if HARD_LIMIT_API_VERSION in api_version:
+ config_info["array_management"] = (
+ blade.directory_services.list_directory_services(names=["management"])
+ .items[0]
+ .to_dict()
+ )
+ config_info["directory_service_roles"] = {}
+ roles = blade.directory_services.list_directory_services_roles()
+ for role in range(0, len(roles.items)):
+ role_name = roles.items[role].name
+ config_info["directory_service_roles"][role_name] = {
+ "group": roles.items[role].group,
+ "group_base": roles.items[role].group_base,
+ }
+ config_info["nfs_directory_service"] = (
+ blade.directory_services.list_directory_services(names=["nfs"])
+ .items[0]
+ .to_dict()
+ )
+ config_info["smb_directory_service"] = (
+ blade.directory_services.list_directory_services(names=["smb"])
+ .items[0]
+ .to_dict()
+ )
+ config_info["ntp"] = blade.arrays.list_arrays().items[0].ntp_servers
+ config_info["ssl_certs"] = blade.certificates.list_certificates().items[0].to_dict()
+ api_version = blade.api_version.list_versions().versions
+ if CERT_GROUPS_API_VERSION in api_version:
+ try:
+ config_info["certificate_groups"] = (
+ blade.certificate_groups.list_certificate_groups().items[0].to_dict()
+ )
+ except Exception:
+ config_info["certificate_groups"] = ""
+ if REPLICATION_API_VERSION in api_version:
+ config_info["snmp_agents"] = {}
+ snmp_agents = blade.snmp_agents.list_snmp_agents()
+ for agent in range(0, len(snmp_agents.items)):
+ agent_name = snmp_agents.items[agent].name
+ config_info["snmp_agents"][agent_name] = {
+ "version": snmp_agents.items[agent].version,
+ "engine_id": snmp_agents.items[agent].engine_id,
+ }
+ if config_info["snmp_agents"][agent_name]["version"] == "v3":
+ config_info["snmp_agents"][agent_name][
+ "auth_protocol"
+ ] = snmp_agents.items[agent].v3.auth_protocol
+ config_info["snmp_agents"][agent_name][
+ "privacy_protocol"
+ ] = snmp_agents.items[agent].v3.privacy_protocol
+ config_info["snmp_agents"][agent_name]["user"] = snmp_agents.items[
+ agent
+ ].v3.user
+ config_info["snmp_managers"] = {}
+ snmp_managers = blade.snmp_managers.list_snmp_managers()
+ for manager in range(0, len(snmp_managers.items)):
+ mgr_name = snmp_managers.items[manager].name
+ config_info["snmp_managers"][mgr_name] = {
+ "version": snmp_managers.items[manager].version,
+ "host": snmp_managers.items[manager].host,
+ "notification": snmp_managers.items[manager].notification,
+ }
+ if config_info["snmp_managers"][mgr_name]["version"] == "v3":
+ config_info["snmp_managers"][mgr_name][
+ "auth_protocol"
+ ] = snmp_managers.items[manager].v3.auth_protocol
+ config_info["snmp_managers"][mgr_name][
+ "privacy_protocol"
+ ] = snmp_managers.items[manager].v3.privacy_protocol
+ config_info["snmp_managers"][mgr_name]["user"] = snmp_managers.items[
+ manager
+ ].v3.user
+ return config_info
+
+
+def generate_subnet_dict(blade):
+ sub_info = {}
+ subnets = blade.subnets.list_subnets()
+ for sub in range(0, len(subnets.items)):
+ sub_name = subnets.items[sub].name
+ if subnets.items[sub].enabled:
+ sub_info[sub_name] = {
+ "gateway": subnets.items[sub].gateway,
+ "mtu": subnets.items[sub].mtu,
+ "vlan": subnets.items[sub].vlan,
+ "prefix": subnets.items[sub].prefix,
+ "services": subnets.items[sub].services,
+ }
+ sub_info[sub_name]["lag"] = subnets.items[sub].link_aggregation_group.name
+ sub_info[sub_name]["interfaces"] = []
+ for iface in range(0, len(subnets.items[sub].interfaces)):
+ sub_info[sub_name]["interfaces"].append(
+ {"name": subnets.items[sub].interfaces[iface].name}
+ )
+ return sub_info
+
+
+def generate_lag_dict(blade):
+ lag_info = {}
+ groups = blade.link_aggregation_groups.list_link_aggregation_groups()
+ for groupcnt in range(0, len(groups.items)):
+ lag_name = groups.items[groupcnt].name
+ lag_info[lag_name] = {
+ "lag_speed": groups.items[groupcnt].lag_speed,
+ "port_speed": groups.items[groupcnt].port_speed,
+ "status": groups.items[groupcnt].status,
+ }
+ lag_info[lag_name]["ports"] = []
+ for port in range(0, len(groups.items[groupcnt].ports)):
+ lag_info[lag_name]["ports"].append(
+ {"name": groups.items[groupcnt].ports[port].name}
+ )
+ return lag_info
+
+
+def generate_admin_dict(module, blade):
+ admin_info = {}
+ api_version = blade.api_version.list_versions().versions
+ if MULTIPROTOCOL_API_VERSION in api_version:
+ admins = blade.admins.list_admins()
+ for admin in range(0, len(admins.items)):
+ admin_name = admins.items[admin].name
+ admin_info[admin_name] = {
+ "api_token_timeout": admins.items[admin].api_token_timeout,
+ "public_key": admins.items[admin].public_key,
+ "local": admins.items[admin].is_local,
+ }
+
+ if MIN_32_API in api_version:
+ bladev2 = get_system(module)
+ admins = list(bladev2.get_admins().items)
+ for admin in range(0, len(admins)):
+ admin_name = admins[admin].name
+ if admins[admin].api_token.expires_at:
+ admin_info[admin_name]["token_expires"] = datetime.fromtimestamp(
+ admins[admin].api_token.expires_at / 1000
+ ).strftime("%Y-%m-%d %H:%M:%S")
+ else:
+ admin_info[admin_name]["token_expires"] = None
+ admin_info[admin_name]["token_created"] = datetime.fromtimestamp(
+ admins[admin].api_token.created_at / 1000
+ ).strftime("%Y-%m-%d %H:%M:%S")
+ admin_info[admin_name]["role"] = admins[admin].role.name
+ if NFS_POLICY_API_VERSION in api_version:
+ admin_info[admin_name]["locked"] = admins[admin].locked
+ admin_info[admin_name]["lockout_remaining"] = admins[
+ admin
+ ].lockout_remaining
+ return admin_info
+
+
+def generate_targets_dict(blade):
+ targets_info = {}
+ targets = blade.targets.list_targets()
+ for target in range(0, len(targets.items)):
+ target_name = targets.items[target].name
+ targets_info[target_name] = {
+ "address": targets.items[target].address,
+ "status": targets.items[target].status,
+ "status_details": targets.items[target].status_details,
+ }
+ return targets_info
+
+
+def generate_remote_creds_dict(blade):
+ remote_creds_info = {}
+ remote_creds = (
+ blade.object_store_remote_credentials.list_object_store_remote_credentials()
+ )
+ for cred_cnt in range(0, len(remote_creds.items)):
+ cred_name = remote_creds.items[cred_cnt].name
+ remote_creds_info[cred_name] = {
+ "access_key": remote_creds.items[cred_cnt].access_key_id,
+ "remote_array": remote_creds.items[cred_cnt].remote.name,
+ }
+ return remote_creds_info
+
+
+def generate_file_repl_dict(blade):
+ file_repl_info = {}
+ file_links = blade.file_system_replica_links.list_file_system_replica_links()
+ for linkcnt in range(0, len(file_links.items)):
+ fs_name = file_links.items[linkcnt].local_file_system.name
+ file_repl_info[fs_name] = {
+ "direction": file_links.items[linkcnt].direction,
+ "lag": file_links.items[linkcnt].lag,
+ "status": file_links.items[linkcnt].status,
+ "remote_fs": file_links.items[linkcnt].remote.name
+ + ":"
+ + file_links.items[linkcnt].remote_file_system.name,
+ "recovery_point": file_links.items[linkcnt].recovery_point,
+ }
+ file_repl_info[fs_name]["policies"] = []
+ for policy_cnt in range(0, len(file_links.items[linkcnt].policies)):
+ file_repl_info[fs_name]["policies"].append(
+ file_links.items[linkcnt].policies[policy_cnt].display_name
+ )
+ return file_repl_info
+
+
+def generate_bucket_repl_dict(module, blade):
+ bucket_repl_info = {}
+ bucket_links = blade.bucket_replica_links.list_bucket_replica_links()
+ for linkcnt in range(0, len(bucket_links.items)):
+ bucket_name = bucket_links.items[linkcnt].local_bucket.name
+ bucket_repl_info[bucket_name] = {
+ "direction": bucket_links.items[linkcnt].direction,
+ "lag": bucket_links.items[linkcnt].lag,
+ "paused": bucket_links.items[linkcnt].paused,
+ "status": bucket_links.items[linkcnt].status,
+ "remote_bucket": bucket_links.items[linkcnt].remote_bucket.name,
+ "remote_credentials": bucket_links.items[linkcnt].remote_credentials.name,
+ "recovery_point": bucket_links.items[linkcnt].recovery_point,
+ "object_backlog": {},
+ }
+ api_version = blade.api_version.list_versions().versions
+ if SMB_MODE_API_VERSION in api_version:
+ blade = get_system(module)
+ bucket_links = list(blade.get_bucket_replica_links().items)
+ for linkcnt in range(0, len(bucket_links)):
+ bucket_name = bucket_links[linkcnt].local_bucket.name
+ bucket_repl_info[bucket_name]["object_backlog"] = {
+ "bytes_count": bucket_links[linkcnt].object_backlog.bytes_count,
+ "delete_ops_count": bucket_links[
+ linkcnt
+ ].object_backlog.delete_ops_count,
+ "other_ops_count": bucket_links[linkcnt].object_backlog.other_ops_count,
+ "put_ops_count": bucket_links[linkcnt].object_backlog.put_ops_count,
+ }
+ bucket_repl_info[bucket_name]["cascading_enabled"] = bucket_links[
+ linkcnt
+ ].cascading_enabled
+ return bucket_repl_info
+
+
+def generate_network_dict(blade):
+ net_info = {}
+ ports = blade.network_interfaces.list_network_interfaces()
+ for portcnt in range(0, len(ports.items)):
+ int_name = ports.items[portcnt].name
+ if ports.items[portcnt].enabled:
+ net_info[int_name] = {
+ "type": ports.items[portcnt].type,
+ "mtu": ports.items[portcnt].mtu,
+ "vlan": ports.items[portcnt].vlan,
+ "address": ports.items[portcnt].address,
+ "services": ports.items[portcnt].services,
+ "gateway": ports.items[portcnt].gateway,
+ "netmask": ports.items[portcnt].netmask,
+ }
+ return net_info
+
+
+def generate_capacity_dict(blade):
+ capacity_info = {}
+ total_cap = blade.arrays.list_arrays_space()
+ file_cap = blade.arrays.list_arrays_space(type="file-system")
+ object_cap = blade.arrays.list_arrays_space(type="object-store")
+ capacity_info["total"] = total_cap.items[0].capacity
+ capacity_info["aggregate"] = {
+ "data_reduction": total_cap.items[0].space.data_reduction,
+ "snapshots": total_cap.items[0].space.snapshots,
+ "total_physical": total_cap.items[0].space.total_physical,
+ "unique": total_cap.items[0].space.unique,
+ "virtual": total_cap.items[0].space.virtual,
+ }
+ capacity_info["file-system"] = {
+ "data_reduction": file_cap.items[0].space.data_reduction,
+ "snapshots": file_cap.items[0].space.snapshots,
+ "total_physical": file_cap.items[0].space.total_physical,
+ "unique": file_cap.items[0].space.unique,
+ "virtual": file_cap.items[0].space.virtual,
+ }
+ capacity_info["object-store"] = {
+ "data_reduction": object_cap.items[0].space.data_reduction,
+ "snapshots": object_cap.items[0].space.snapshots,
+ "total_physical": object_cap.items[0].space.total_physical,
+ "unique": object_cap.items[0].space.unique,
+ "virtual": file_cap.items[0].space.virtual,
+ }
+
+ return capacity_info
+
+
+def generate_snap_dict(blade):
+ snap_info = {}
+ snaps = blade.file_system_snapshots.list_file_system_snapshots()
+ api_version = blade.api_version.list_versions().versions
+ for snap in range(0, len(snaps.items)):
+ snapshot = snaps.items[snap].name
+ snap_info[snapshot] = {
+ "destroyed": snaps.items[snap].destroyed,
+ "source": snaps.items[snap].source,
+ "suffix": snaps.items[snap].suffix,
+ "source_destroyed": snaps.items[snap].source_destroyed,
+ }
+ if REPLICATION_API_VERSION in api_version:
+ snap_info[snapshot]["owner"] = snaps.items[snap].owner.name
+ snap_info[snapshot]["owner_destroyed"] = snaps.items[snap].owner_destroyed
+ snap_info[snapshot]["source_display_name"] = snaps.items[
+ snap
+ ].source_display_name
+ snap_info[snapshot]["source_is_local"] = snaps.items[snap].source_is_local
+ snap_info[snapshot]["source_location"] = snaps.items[
+ snap
+ ].source_location.name
+ return snap_info
+
+
+def generate_snap_transfer_dict(blade):
+ snap_transfer_info = {}
+ snap_transfers = blade.file_system_snapshots.list_file_system_snapshots_transfer()
+ for snap_transfer in range(0, len(snap_transfers.items)):
+ transfer = snap_transfers.items[snap_transfer].name
+ snap_transfer_info[transfer] = {
+ "completed": snap_transfers.items[snap_transfer].completed,
+ "data_transferred": snap_transfers.items[snap_transfer].data_transferred,
+ "progress": snap_transfers.items[snap_transfer].progress,
+ "direction": snap_transfers.items[snap_transfer].direction,
+ "remote": snap_transfers.items[snap_transfer].remote.name,
+ "remote_snapshot": snap_transfers.items[snap_transfer].remote_snapshot.name,
+ "started": snap_transfers.items[snap_transfer].started,
+ "status": snap_transfers.items[snap_transfer].status,
+ }
+ return snap_transfer_info
+
+
+def generate_array_conn_dict(module, blade):
+ array_conn_info = {}
+ arraysv2 = {}
+ api_version = blade.api_version.list_versions().versions
+ arrays = blade.array_connections.list_array_connections()
+ if NFS_POLICY_API_VERSION in api_version:
+ bladev2 = get_system(module)
+ arraysv2 = list(bladev2.get_array_connections().items)
+ for arraycnt in range(0, len(arrays.items)):
+ array = arrays.items[arraycnt].remote.name
+ array_conn_info[array] = {
+ "encrypted": arrays.items[arraycnt].encrypted,
+ "replication_addresses": arrays.items[arraycnt].replication_addresses,
+ "management_address": arrays.items[arraycnt].management_address,
+ "status": arrays.items[arraycnt].status,
+ "version": arrays.items[arraycnt].version,
+ "throttle": [],
+ }
+ if arrays.items[arraycnt].encrypted:
+ array_conn_info[array]["ca_certificate_group"] = arrays.items[
+ arraycnt
+ ].ca_certificate_group.name
+ for v2array in range(0, len(arraysv2)):
+ if arraysv2[v2array].remote.name == array:
+ array_conn_info[array]["throttle"] = {
+ "default_limit": _bytes_to_human(
+ arraysv2[v2array].throttle.default_limit
+ ),
+ "window_limit": _bytes_to_human(
+ arraysv2[v2array].throttle.window_limit
+ ),
+ "window_start": _millisecs_to_time(
+ arraysv2[v2array].throttle.window.start
+ ),
+ "window_end": _millisecs_to_time(
+ arraysv2[v2array].throttle.window.end
+ ),
+ }
+ return array_conn_info
+
+
+def generate_policies_dict(blade):
+ policies_info = {}
+ policies = blade.policies.list_policies()
+ for policycnt in range(0, len(policies.items)):
+ policy = policies.items[policycnt].name
+ policies_info[policy] = {}
+ policies_info[policy]["enabled"] = policies.items[policycnt].enabled
+ if policies.items[policycnt].rules:
+ policies_info[policy]["rules"] = (
+ policies.items[policycnt].rules[0].to_dict()
+ )
+ return policies_info
+
+
+def generate_bucket_dict(module, blade):
+ bucket_info = {}
+ buckets = blade.buckets.list_buckets()
+ for bckt in range(0, len(buckets.items)):
+ bucket = buckets.items[bckt].name
+ bucket_info[bucket] = {
+ "versioning": buckets.items[bckt].versioning,
+ "bucket_type": getattr(buckets.items[bckt], "bucket_type", None),
+ "object_count": buckets.items[bckt].object_count,
+ "id": buckets.items[bckt].id,
+ "account_name": buckets.items[bckt].account.name,
+ "data_reduction": buckets.items[bckt].space.data_reduction,
+ "snapshot_space": buckets.items[bckt].space.snapshots,
+ "total_physical_space": buckets.items[bckt].space.total_physical,
+ "unique_space": buckets.items[bckt].space.unique,
+ "virtual_space": buckets.items[bckt].space.virtual,
+ "created": buckets.items[bckt].created,
+ "destroyed": buckets.items[bckt].destroyed,
+ "time_remaining": buckets.items[bckt].time_remaining,
+ "lifecycle_rules": {},
+ }
+ api_version = blade.api_version.list_versions().versions
+ if LIFECYCLE_API_VERSION in api_version:
+ blade = get_system(module)
+ for bckt in range(0, len(buckets.items)):
+ if buckets.items[bckt].destroyed:
+ # skip processing buckets marked as destroyed
+ continue
+ all_rules = list(
+ blade.get_lifecycle_rules(bucket_ids=[buckets.items[bckt].id]).items
+ )
+ for rule in range(0, len(all_rules)):
+ bucket_name = all_rules[rule].bucket.name
+ rule_id = all_rules[rule].rule_id
+ if all_rules[rule].keep_previous_version_for:
+ keep_previous_version_for = int(
+ all_rules[rule].keep_previous_version_for / 86400000
+ )
+ else:
+ keep_previous_version_for = None
+ if all_rules[rule].keep_current_version_for:
+ keep_current_version_for = int(
+ all_rules[rule].keep_current_version_for / 86400000
+ )
+ else:
+ keep_current_version_for = None
+ if all_rules[rule].abort_incomplete_multipart_uploads_after:
+ abort_incomplete_multipart_uploads_after = int(
+ all_rules[rule].abort_incomplete_multipart_uploads_after
+ / 86400000
+ )
+ else:
+ abort_incomplete_multipart_uploads_after = None
+ if all_rules[rule].keep_current_version_until:
+ keep_current_version_until = datetime.fromtimestamp(
+ all_rules[rule].keep_current_version_until / 1000
+ ).strftime("%Y-%m-%d")
+ else:
+ keep_current_version_until = None
+ bucket_info[bucket_name]["lifecycle_rules"][rule_id] = {
+ "keep_previous_version_for (days)": keep_previous_version_for,
+ "keep_current_version_for (days)": keep_current_version_for,
+ "keep_current_version_until": keep_current_version_until,
+ "prefix": all_rules[rule].prefix,
+ "enabled": all_rules[rule].enabled,
+ "abort_incomplete_multipart_uploads_after (days)": abort_incomplete_multipart_uploads_after,
+ "cleanup_expired_object_delete_marker": all_rules[
+ rule
+ ].cleanup_expired_object_delete_marker,
+ }
+ if VSO_VERSION in api_version:
+ buckets = list(blade.get_buckets().items)
+ for bucket in range(0, len(buckets)):
+ bucket_info[buckets[bucket].name]["bucket_type"] = buckets[
+ bucket
+ ].bucket_type
+ if BUCKET_API_VERSION in api_version:
+ for bucket in range(0, len(buckets)):
+ bucket_info[buckets[bucket].name]["retention_lock"] = buckets[
+ bucket
+ ].retention_lock
+ bucket_info[buckets[bucket].name]["quota_limit"] = buckets[
+ bucket
+ ].quota_limit
+ bucket_info[buckets[bucket].name]["object_lock_config"] = {
+ "enabled": buckets[bucket].object_lock_config.enabled,
+ "freeze_locked_objects": buckets[
+ bucket
+ ].object_lock_config.freeze_locked_objects,
+ }
+ bucket_info[buckets[bucket].name]["eradication_config"] = {
+ "eradication_delay": buckets[
+ bucket
+ ].eradication_config.eradication_delay,
+ "manual_eradication": buckets[
+ bucket
+ ].eradication_config.manual_eradication,
+ }
+ return bucket_info
+
+
+def generate_kerb_dict(blade):
+ kerb_info = {}
+ keytabs = list(blade.get_keytabs().items)
+ for ktab in range(0, len(keytabs)):
+ keytab_name = keytabs[ktab].prefix
+ kerb_info[keytab_name] = {}
+ for key in range(0, len(keytabs)):
+ if keytabs[key].prefix == keytab_name:
+ kerb_info[keytab_name][keytabs[key].suffix] = {
+ "fqdn": keytabs[key].fqdn,
+ "kvno": keytabs[key].kvno,
+ "principal": keytabs[key].principal,
+ "realm": keytabs[key].realm,
+ "encryption_type": keytabs[key].encryption_type,
+ }
+ return kerb_info
+
+
+def generate_ad_dict(blade):
+ ad_info = {}
+ active_directory = blade.get_active_directory()
+ if active_directory.total_item_count != 0:
+ ad_account = list(active_directory.items)[0]
+ ad_info[ad_account.name] = {
+ "computer": ad_account.computer_name,
+ "domain": ad_account.domain,
+ "directory_servers": ad_account.directory_servers,
+ "kerberos_servers": ad_account.kerberos_servers,
+ "service_principals": ad_account.service_principal_names,
+ "join_ou": ad_account.join_ou,
+ "encryption_types": ad_account.encryption_types,
+ }
+ return ad_info
+
+
+def generate_object_store_access_policies_dict(blade):
+ policies_info = {}
+ policies = list(blade.get_object_store_access_policies().items)
+ for policy in range(0, len(policies)):
+ policy_name = policies[policy].name
+ policies_info[policy_name] = {
+ "ARN": policies[policy].arn,
+ "description": policies[policy].description,
+ "enabled": policies[policy].enabled,
+ "local": policies[policy].is_local,
+ "rules": [],
+ }
+ for rule in range(0, len(policies[policy].rules)):
+ policies_info[policy_name]["rules"].append(
+ {
+ "actions": policies[policy].rules[rule].actions,
+ "conditions": {
+ "source_ips": policies[policy]
+ .rules[rule]
+ .conditions.source_ips,
+ "s3_delimiters": policies[policy]
+ .rules[rule]
+ .conditions.s3_delimiters,
+ "s3_prefixes": policies[policy]
+ .rules[rule]
+ .conditions.s3_prefixes,
+ },
+ "effect": policies[policy].rules[rule].effect,
+ "name": policies[policy].rules[rule].name,
+ }
+ )
+ return policies_info
+
+
+def generate_nfs_export_policies_dict(blade):
+ policies_info = {}
+ policies = list(blade.get_nfs_export_policies().items)
+ for policy in range(0, len(policies)):
+ policy_name = policies[policy].name
+ policies_info[policy_name] = {
+ "local": policies[policy].is_local,
+ "enabled": policies[policy].enabled,
+ "rules": [],
+ }
+ for rule in range(0, len(policies[policy].rules)):
+ policies_info[policy_name]["rules"].append(
+ {
+ "access": policies[policy].rules[rule].access,
+ "anongid": policies[policy].rules[rule].anongid,
+ "anonuid": policies[policy].rules[rule].anonuid,
+ "atime": policies[policy].rules[rule].atime,
+ "client": policies[policy].rules[rule].client,
+ "fileid_32bit": policies[policy].rules[rule].fileid_32bit,
+ "permission": policies[policy].rules[rule].permission,
+ "secure": policies[policy].rules[rule].secure,
+ "security": policies[policy].rules[rule].security,
+ "index": policies[policy].rules[rule].index,
+ }
+ )
+ return policies_info
+
+
+def generate_object_store_accounts_dict(blade):
+ account_info = {}
+ accounts = list(blade.get_object_store_accounts().items)
+ for account in range(0, len(accounts)):
+ acc_name = accounts[account].name
+ account_info[acc_name] = {
+ "object_count": accounts[account].object_count,
+ "data_reduction": accounts[account].space.data_reduction,
+ "snapshots_space": accounts[account].space.snapshots,
+ "total_physical_space": accounts[account].space.total_physical,
+ "unique_space": accounts[account].space.unique,
+ "virtual_space": accounts[account].space.virtual,
+ "quota_limit": getattr(accounts[account], "quota_limit", None),
+ "hard_limit_enabled": getattr(
+ accounts[account], "hard_limit_enabled", None
+ ),
+ "total_provisioned": getattr(
+ accounts[account].space, "total_provisioned", None
+ ),
+ "users": {},
+ }
+ try:
+ account_info[acc_name]["bucket_defaults"] = {
+ "hard_limit_enabled": accounts[
+ account
+ ].bucket_defaults.hard_limit_enabled,
+ "quota_limit": accounts[account].bucket_defaults.quota_limit,
+ }
+ except AttributeError:
+ pass
+ acc_users = list(
+ blade.get_object_store_users(filter='name="' + acc_name + '/*"').items
+ )
+ for acc_user in range(0, len(acc_users)):
+ user_name = acc_users[acc_user].name.split("/")[1]
+ account_info[acc_name]["users"][user_name] = {"keys": [], "policies": []}
+ if (
+ blade.get_object_store_access_keys(
+ filter='user.name="' + acc_users[acc_user].name + '"'
+ ).total_item_count
+ != 0
+ ):
+ access_keys = list(
+ blade.get_object_store_access_keys(
+ filter='user.name="' + acc_users[acc_user].name + '"'
+ ).items
+ )
+ for key in range(0, len(access_keys)):
+ account_info[acc_name]["users"][user_name]["keys"].append(
+ {
+ "name": access_keys[key].name,
+ "enabled": bool(access_keys[key].enabled),
+ }
+ )
+ if (
+ blade.get_object_store_access_policies_object_store_users(
+ member_names=[acc_users[acc_user].name]
+ ).total_item_count
+ != 0
+ ):
+ policies = list(
+ blade.get_object_store_access_policies_object_store_users(
+ member_names=[acc_users[acc_user].name]
+ ).items
+ )
+ for policy in range(0, len(policies)):
+ account_info[acc_name]["users"][user_name]["policies"].append(
+ policies[policy].policy.name
+ )
+ return account_info
+
+
+def generate_fs_dict(module, blade):
+ api_version = blade.api_version.list_versions().versions
+ if SMB_MODE_API_VERSION in api_version:
+ bladev2 = get_system(module)
+ fsys_v2 = list(bladev2.get_file_systems().items)
+ fs_info = {}
+ fsys = blade.file_systems.list_file_systems()
+ for fsystem in range(0, len(fsys.items)):
+ share = fsys.items[fsystem].name
+ fs_info[share] = {
+ "fast_remove": fsys.items[fsystem].fast_remove_directory_enabled,
+ "snapshot_enabled": fsys.items[fsystem].snapshot_directory_enabled,
+ "provisioned": fsys.items[fsystem].provisioned,
+ "destroyed": fsys.items[fsystem].destroyed,
+ "nfs_rules": fsys.items[fsystem].nfs.rules,
+ "nfs_v3": getattr(fsys.items[fsystem].nfs, "v3_enabled", False),
+ "nfs_v4_1": getattr(fsys.items[fsystem].nfs, "v4_1_enabled", False),
+ "user_quotas": {},
+ "group_quotas": {},
+ }
+ if fsys.items[fsystem].http.enabled:
+ fs_info[share]["http"] = fsys.items[fsystem].http.enabled
+ if fsys.items[fsystem].smb.enabled:
+ fs_info[share]["smb_mode"] = fsys.items[fsystem].smb.acl_mode
+ api_version = blade.api_version.list_versions().versions
+ if MULTIPROTOCOL_API_VERSION in api_version:
+ fs_info[share]["multi_protocol"] = {
+ "safegaurd_acls": fsys.items[fsystem].multi_protocol.safeguard_acls,
+ "access_control_style": fsys.items[
+ fsystem
+ ].multi_protocol.access_control_style,
+ }
+ if HARD_LIMIT_API_VERSION in api_version:
+ fs_info[share]["hard_limit"] = fsys.items[fsystem].hard_limit_enabled
+ if REPLICATION_API_VERSION in api_version:
+ fs_info[share]["promotion_status"] = fsys.items[fsystem].promotion_status
+ fs_info[share]["requested_promotion_state"] = fsys.items[
+ fsystem
+ ].requested_promotion_state
+ fs_info[share]["writable"] = fsys.items[fsystem].writable
+ fs_info[share]["source"] = {
+ "is_local": fsys.items[fsystem].source.is_local,
+ "name": fsys.items[fsystem].source.name,
+ }
+ if SMB_MODE_API_VERSION in api_version:
+ for v2fs in range(0, len(fsys_v2)):
+ if fsys_v2[v2fs].name == share:
+ fs_info[share]["default_group_quota"] = fsys_v2[
+ v2fs
+ ].default_group_quota
+ fs_info[share]["default_user_quota"] = fsys_v2[
+ v2fs
+ ].default_user_quota
+ if NFS_POLICY_API_VERSION in api_version:
+ fs_info[share]["export_policy"] = fsys_v2[
+ v2fs
+ ].nfs.export_policy.name
+ if VSO_VERSION in api_version:
+ for v2fs in range(0, len(fsys_v2)):
+ if fsys_v2[v2fs].name == share:
+ try:
+ fs_groups = True
+ fs_group_quotas = list(
+ bladev2.get_quotas_groups(file_system_names=[share]).items
+ )
+ except Exception:
+ fs_groups = False
+ try:
+ fs_users = True
+ fs_user_quotas = list(
+ bladev2.get_quotas_users(file_system_names=[share]).items
+ )
+ except Exception:
+ fs_users = False
+ if fs_groups:
+ for group_quota in range(0, len(fs_group_quotas)):
+ group_name = fs_group_quotas[group_quota].name.rsplit("/")[
+ 1
+ ]
+ fs_info[share]["group_quotas"][group_name] = {
+ "group_id": fs_group_quotas[group_quota].group.id,
+ "group_name": fs_group_quotas[group_quota].group.name,
+ "quota": fs_group_quotas[group_quota].quota,
+ "usage": fs_group_quotas[group_quota].usage,
+ }
+ if fs_users:
+ for user_quota in range(0, len(fs_user_quotas)):
+ user_name = fs_user_quotas[user_quota].name.rsplit("/")[1]
+ fs_info[share]["user_quotas"][user_name] = {
+ "user_id": fs_user_quotas[user_quota].user.id,
+ "user_name": fs_user_quotas[user_quota].user.name,
+ "quota": fs_user_quotas[user_quota].quota,
+ "usage": fs_user_quotas[user_quota].usage,
+ }
+
+ return fs_info
+
+
+def generate_drives_dict(blade):
+ """
+ Drives information is only available for the Legend chassis.
+ The Legend chassis product_name has // in it so only bother if
+ that is the case.
+ """
+ drives_info = {}
+ drives = list(blade.get_drives().items)
+ if "//" in list(blade.get_arrays().items)[0].product_type:
+ for drive in range(0, len(drives)):
+ name = drives[drive].name
+ drives_info[name] = {
+ "progress": getattr(drives[drive], "progress", None),
+ "raw_capacity": getattr(drives[drive], "raw_capacity", None),
+ "status": getattr(drives[drive], "status", None),
+ "details": getattr(drives[drive], "details", None),
+ }
+ return drives_info
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(gather_subset=dict(default="minimum", type="list", elements="str"))
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ blade = get_blade(module)
+ versions = blade.api_version.list_versions().versions
+
+ if MIN_REQUIRED_API_VERSION not in versions:
+ module.fail_json(
+ msg="Minimum FlashBlade REST version required: {0}".format(
+ MIN_REQUIRED_API_VERSION
+ )
+ )
+ if not module.params["gather_subset"]:
+ module.params["gather_subset"] = ["minimum"]
+ subset = [test.lower() for test in module.params["gather_subset"]]
+ valid_subsets = (
+ "all",
+ "minimum",
+ "config",
+ "performance",
+ "capacity",
+ "network",
+ "subnets",
+ "lags",
+ "filesystems",
+ "snapshots",
+ "buckets",
+ "arrays",
+ "replication",
+ "policies",
+ "accounts",
+ "admins",
+ "ad",
+ "kerberos",
+ "drives",
+ )
+ subset_test = (test in valid_subsets for test in subset)
+ if not all(subset_test):
+ module.fail_json(
+ msg="value must gather_subset must be one or more of: %s, got: %s"
+ % (",".join(valid_subsets), ",".join(subset))
+ )
+
+ info = {}
+
+ api_version = blade.api_version.list_versions().versions
+ if "minimum" in subset or "all" in subset:
+ info["default"] = generate_default_dict(module, blade)
+ if "performance" in subset or "all" in subset:
+ info["performance"] = generate_perf_dict(blade)
+ if "config" in subset or "all" in subset:
+ info["config"] = generate_config_dict(blade)
+ if "capacity" in subset or "all" in subset:
+ info["capacity"] = generate_capacity_dict(blade)
+ if "lags" in subset or "all" in subset:
+ info["lag"] = generate_lag_dict(blade)
+ if "network" in subset or "all" in subset:
+ info["network"] = generate_network_dict(blade)
+ if "subnets" in subset or "all" in subset:
+ info["subnet"] = generate_subnet_dict(blade)
+ if "filesystems" in subset or "all" in subset:
+ info["filesystems"] = generate_fs_dict(module, blade)
+ if "admins" in subset or "all" in subset:
+ info["admins"] = generate_admin_dict(module, blade)
+ if "snapshots" in subset or "all" in subset:
+ info["snapshots"] = generate_snap_dict(blade)
+ if "buckets" in subset or "all" in subset:
+ info["buckets"] = generate_bucket_dict(module, blade)
+ if POLICIES_API_VERSION in api_version:
+ if "policies" in subset or "all" in subset:
+ info["policies"] = generate_policies_dict(blade)
+ info["snapshot_policies"] = generate_policies_dict(blade)
+ if REPLICATION_API_VERSION in api_version:
+ if "arrays" in subset or "all" in subset:
+ info["arrays"] = generate_array_conn_dict(module, blade)
+ if "replication" in subset or "all" in subset:
+ info["file_replication"] = generate_file_repl_dict(blade)
+ info["bucket_replication"] = generate_bucket_repl_dict(module, blade)
+ info["snap_transfers"] = generate_snap_transfer_dict(blade)
+ info["remote_credentials"] = generate_remote_creds_dict(blade)
+ info["targets"] = generate_targets_dict(blade)
+ if MIN_32_API in api_version:
+ # Calls for data only available from Purity//FB 3.2 and higher
+ blade = get_system(module)
+ if "accounts" in subset or "all" in subset:
+ info["accounts"] = generate_object_store_accounts_dict(blade)
+ if "ad" in subset or "all" in subset:
+ info["active_directory"] = generate_ad_dict(blade)
+ if "kerberos" in subset or "all" in subset:
+ info["kerberos"] = generate_kerb_dict(blade)
+ if "policies" in subset or "all" in subset:
+ if SMB_MODE_API_VERSION in api_version:
+ info["access_policies"] = generate_object_store_access_policies_dict(
+ blade
+ )
+ if NFS_POLICY_API_VERSION in api_version:
+ info["export_policies"] = generate_nfs_export_policies_dict(blade)
+ if "drives" in subset or "all" in subset and DRIVES_API_VERSION in api_version:
+ info["drives"] = generate_drives_dict(blade)
+ module.exit_json(changed=False, purefb_info=info)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_inventory.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_inventory.py
new file mode 100644
index 000000000..b17bc3f9e
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_inventory.py
@@ -0,0 +1,279 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2020, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_inventory
+version_added: '1.0.0'
+short_description: Collect information from Pure Storage FlashBlade
+description:
+ - Collect information from a Pure Storage FlashBlade running the
+ Purity//FB operating system. By default, the module will collect basic
+ information including hosts, host groups, protection
+ groups and volume counts. Additional information can be collected
+ based on the configured set of arguements.
+author:
+ - Pure Storage ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+extends_documentation_fragment:
+ - purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: collect FlashBlade inventory
+ purestorage.flashblade.purefb_inventory:
+ fa_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+ register: blade_info
+- name: show default information
+ debug:
+ msg: "{{ blade_info['purefb_info'] }}"
+
+"""
+
+RETURN = r"""
+purefb_inventory:
+ description: Returns the inventory information for the FlashBlade
+ returned: always
+ type: complex
+ sample: {
+ "blades": {
+ "CH1.FB1": {
+ "model": "FB-17TB",
+ "serial": "PPCXA1942AFF5",
+ "slot": 1,
+ "status": "healthy"
+ }
+ },
+ "chassis": {
+ "CH1": {
+ "index": 1,
+ "model": null,
+ "serial": "PMPAM163402AE",
+ "slot": null,
+ "status": "healthy"
+ }
+ },
+ "controllers": {},
+ "ethernet": {
+ "CH1.FM1.ETH1": {
+ "model": "624410002",
+ "serial": "APF16360021PRV",
+ "slot": 1,
+ "speed": 40000000000,
+ "status": "healthy"
+ }
+ },
+ "fans": {
+ "CH1.FM1.FAN1": {
+ "slot": 1,
+ "status": "healthy"
+ }
+ },
+ "modules": {
+ "CH1.FM1": {
+ "model": "EFM-110",
+ "serial": "PSUFS1640002C",
+ "slot": 1,
+ "status": "healthy"
+ },
+ "CH1.FM2": {
+ "model": "EFM-110",
+ "serial": "PSUFS1640004A",
+ "slot": 2,
+ "status": "healthy"
+ }
+ },
+ "power": {
+ "CH1.PWR1": {
+ "model": "DS1600SPE-3",
+ "serial": "M0500E00D8AJZ",
+ "slot": 1,
+ "status": "healthy"
+ }
+ },
+ "switch": {}
+ }
+"""
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ get_system,
+ purefb_argument_spec,
+)
+
+
+MIN_API_VERSION = "2.1"
+PART_NUMBER_API_VERSION = "2.8"
+
+
+def generate_hardware_dict(module, blade, api_version):
+ hw_info = {
+ "modules": {},
+ "ethernet": {},
+ "mgmt_ports": {},
+ "fans": {},
+ "bays": {},
+ "controllers": {},
+ "blades": {},
+ "chassis": {},
+ "power": {},
+ "switch": {},
+ }
+ blade = get_system(module)
+ components = list(blade.get_hardware(filter="type='fm'").items)
+ for component in range(0, len(components)):
+ component_name = components[component].name
+ hw_info["modules"][component_name] = {
+ "slot": components[component].slot,
+ "status": components[component].status,
+ "serial": components[component].serial,
+ "model": components[component].model,
+ "identify": components[component].identify_enabled,
+ }
+ if PART_NUMBER_API_VERSION in api_version:
+ hw_info["modules"][component_name]["part_number"] = components[
+ component
+ ].part_number
+ components = list(blade.get_hardware(filter="type='eth'").items)
+ for component in range(0, len(components)):
+ component_name = components[component].name
+ hw_info["ethernet"][component_name] = {
+ "slot": components[component].slot,
+ "status": components[component].status,
+ "serial": components[component].serial,
+ "model": components[component].model,
+ "speed": components[component].speed,
+ }
+ if PART_NUMBER_API_VERSION in api_version:
+ hw_info["ethernet"][component_name]["part_number"] = components[
+ component
+ ].part_number
+ components = list(blade.get_hardware(filter="type='mgmt_port'").items)
+ for component in range(0, len(components)):
+ component_name = components[component].name
+ hw_info["mgmt_ports"][component_name] = {
+ "slot": components[component].slot,
+ "status": components[component].status,
+ "serial": components[component].serial,
+ "model": components[component].model,
+ "speed": components[component].speed,
+ }
+ if PART_NUMBER_API_VERSION in api_version:
+ hw_info["mgmt_ports"][component_name]["part_number"] = components[
+ component
+ ].part_number
+ components = list(blade.get_hardware(filter="type='fan'").items)
+ for component in range(0, len(components)):
+ component_name = components[component].name
+ hw_info["fans"][component_name] = {
+ "slot": components[component].slot,
+ "status": components[component].status,
+ "identify": components[component].identify_enabled,
+ }
+ if PART_NUMBER_API_VERSION in api_version:
+ hw_info["fans"][component_name]["part_number"] = components[
+ component
+ ].part_number
+ components = list(blade.get_hardware(filter="type='fb'").items)
+ for component in range(0, len(components)):
+ component_name = components[component].name
+ hw_info["blades"][component_name] = {
+ "slot": components[component].slot,
+ "status": components[component].status,
+ "serial": components[component].serial,
+ "model": components[component].model,
+ "temperature": components[component].temperature,
+ "identify": components[component].identify_enabled,
+ }
+ if PART_NUMBER_API_VERSION in api_version:
+ hw_info["blades"][component_name]["part_number"] = components[
+ component
+ ].part_number
+ components = list(blade.get_hardware(filter="type='pwr'").items)
+ for component in range(0, len(components)):
+ component_name = components[component].name
+ hw_info["power"][component_name] = {
+ "slot": components[component].slot,
+ "status": components[component].status,
+ "serial": components[component].serial,
+ "model": components[component].model,
+ }
+ if PART_NUMBER_API_VERSION in api_version:
+ hw_info["power"][component_name]["part_number"] = components[
+ component
+ ].part_number
+ components = list(blade.get_hardware(filter="type='xfm'").items)
+ for component in range(0, len(components)):
+ component_name = components[component].name
+ hw_info["switch"][component_name] = {
+ "slot": components[component].slot,
+ "status": components[component].status,
+ "serial": components[component].serial,
+ "model": components[component].model,
+ }
+ if PART_NUMBER_API_VERSION in api_version:
+ hw_info["switch"][component_name]["part_number"] = components[
+ component
+ ].part_number
+ components = list(blade.get_hardware(filter="type='ch'").items)
+ for component in range(0, len(components)):
+ component_name = components[component].name
+ hw_info["chassis"][component_name] = {
+ "slot": components[component].slot,
+ "index": components[component].index,
+ "status": components[component].status,
+ "serial": components[component].serial,
+ "model": components[component].model,
+ }
+ if PART_NUMBER_API_VERSION in api_version:
+ hw_info["chassis"][component_name]["part_number"] = components[
+ component
+ ].part_number
+ components = list(blade.get_hardware(filter="type='bay'").items)
+ for component in range(0, len(components)):
+ component_name = components[component].name
+ hw_info["bays"][component_name] = {
+ "slot": components[component].slot,
+ "index": components[component].index,
+ "status": components[component].status,
+ "serial": components[component].serial,
+ "model": components[component].model,
+ "identify": components[component].identify_enabled,
+ }
+ if PART_NUMBER_API_VERSION in api_version:
+ hw_info["bay"][component_name]["part_number"] = components[
+ component
+ ].part_number
+
+ return hw_info
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ blade = get_blade(module)
+ api_version = blade.api_version.list_versions().versions
+
+ module.exit_json(
+ changed=False, purefb_info=generate_hardware_dict(module, blade, api_version)
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_keytabs.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_keytabs.py
new file mode 100644
index 000000000..7268bc01c
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_keytabs.py
@@ -0,0 +1,254 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2021, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: purefb_keytabs
+version_added: '1.6.0'
+short_description: Manage FlashBlade Kerberos Keytabs
+description:
+- Manage Kerberos Keytabs for FlashBlades
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Manage Kerberos Keytabs
+ default: import
+ type: str
+ choices: [ absent, import, export, rotate ]
+ name:
+ description:
+ - Name of the Keytab
+ - Must include prefix and suffix
+ type: str
+ prefix:
+ description:
+ - Only required for I(import) or I(rotate)
+ - Prefix to use for naming the files slots
+ - Specifying a file entry prefix is required because a single keytab file can contain
+ multiple keytab entries in multiple slots.
+ - If not provided for I(import) the current AD Account name will be used.
+ type: str
+ keytab_file:
+ description:
+ - Name of file holding Keytab
+ type: str
+ filetype:
+ description:
+ - Format of the keytab file
+ type: str
+ choices: [ binary, base64 ]
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Import a binary keytab
+ purestorage.flashblade.purefb_keytabs:
+ state: import
+ prefix: example
+ keytab_file: pure_krb.keytab
+ filetype: binary
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+
+- name: Import a base64 keytab
+ purestorage.flashblade.purefb_keytabs:
+ state: import
+ prefix: example
+ keytab_file: pure_krb.keytab.mime
+ filetype: base64
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+
+- name: Export a keytab
+ purestorage.flashblade.purefb_keytabs:
+ state: export
+ name: example.3
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+ register: download_file
+
+- name: Delete a keytab
+ purestorage.flashblade.purefb_keytabs:
+ state: absent
+ name: example.3
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+
+- name: Rotate current AD account keytabs
+ purestorage.flashblade.purefb_keytabs:
+ state: rotate
+ fb_url: 10.10.10.2
+
+- name: Rotate AD account keytabs by creating new series
+ purestorage.flashblade.purefb_keytabs:
+ state: rotate
+ name: next_prefix
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+"""
+
+RETURN = r"""
+download_file:
+ description:
+ - Name of file containing exported keytab
+ returned: When using I(export) option
+ type: str
+ sample: "/tmp/pure_krb8939478070214877726.keytab"
+"""
+
+HAS_PURESTORAGE = True
+try:
+ from pypureclient.flashblade import KeytabPost, Reference
+except ImportError:
+ HAS_PURESTORAGE = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_system,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "2.0"
+
+
+def rotate_keytab(module, blade):
+ """Rotate keytab"""
+ changed = True
+ account = Reference(
+ name=list(blade.get_active_directory().items)[0].name,
+ resource_type="active-directory",
+ )
+ keytab = KeytabPost(source=account)
+ if not module.check_mode:
+ res = blade.post_keytabs(keytab=keytab, name_prefixes=module.params["prefix"])
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to rotate AD account keytabs, prefix {0}.".format(
+ module.params["prefix"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def delete_keytab(module, blade):
+ """Delete keytab"""
+ changed = False
+ if blade.get_keytabs(names=[module.params["name"]]).status_code == 200:
+ changed = True
+ if not module.check_mode:
+ res = blade.delete_keytabs(names=[module.params["name"]])
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to delete keytab {0}. Error: {1}".format(
+ module.params["name"], res.errors[0].message
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def import_keytab(module, blade):
+ """Import keytab"""
+ changed = True
+ if not module.check_mode:
+ if module.params["filetype"] == "binary":
+ readtype = "rb"
+ else:
+ readtype = "r"
+ with open(module.params["keytab_file"], readtype) as keytab_file:
+ keytab_data = keytab_file.read()
+ short_name = module.params["keytab_file"].split("/")[-1]
+ res = blade.post_keytabs_upload(
+ name_prefixes=module.params["prefix"], keytab_file=(short_name, keytab_data)
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to import keytab file {0}. Error: {1}".format(
+ module.params["keytab_file"], res.errors[0].message
+ )
+ )
+
+ module.exit_json(changed=changed)
+
+
+def export_keytab(module, blade):
+ """Export keytab"""
+ changed = False
+ download_file = ""
+ if blade.get_keytabs(names=[module.params["name"]]).status_code == 200:
+ changed = True
+ if not module.check_mode:
+ res = blade.get_keytabs_download(keytab_names=[module.params["name"]])
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to export keytab {0}. Error: {1}".format(
+ module.params["name"], res.errors[0].message
+ )
+ )
+ else:
+ download_file = list(res.items)[0]
+ module.exit_json(changed=changed, download_file=download_file)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(
+ type="str",
+ default="import",
+ choices=["absent", "rotate", "import", "export"],
+ ),
+ name=dict(type="str"),
+ prefix=dict(type="str"),
+ keytab_file=dict(type="str"),
+ filetype=dict(type="str", choices=["binary", "base64"]),
+ )
+ )
+
+ required_if = [["state", "import", ["prefix"]]]
+ module = AnsibleModule(
+ argument_spec, required_if=required_if, supports_check_mode=True
+ )
+
+ if not HAS_PURESTORAGE:
+ module.fail_json(msg="py-pure-client sdk is required for this module")
+
+ state = module.params["state"]
+ blade = get_system(module)
+ api_version = list(blade.get_versions().items)
+
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(
+ msg="Minimum FlashBlade REST version required: {0}".format(
+ MIN_REQUIRED_API_VERSION
+ )
+ )
+
+ if not module.params["prefix"]:
+ module.params["prefix"] = list(blade.get_active_directory().items)[0].name
+
+ if state == "import":
+ import_keytab(module, blade)
+ elif state == "export":
+ export_keytab(module, blade)
+ elif state == "rotate":
+ rotate_keytab(module, blade)
+ elif state == "absent":
+ delete_keytab(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_lag.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_lag.py
new file mode 100644
index 000000000..e5c46e730
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_lag.py
@@ -0,0 +1,315 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2021, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_lag
+version_added: '1.7.0'
+short_description: Manage FlashBlade Link Aggregation Groups
+description:
+- Maintain FlashBlade Link Aggregation Groups
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - Name of the Link Aggregation Group
+ type: str
+ default: uplink
+ state:
+ description:
+ - Define whether the LAG should be added or deleted
+ default: present
+ choices: [ absent, present ]
+ type: str
+ ports:
+ description:
+ - Name of network ports assigned to the LAG
+ - Format should be CHx.ETHy, where CHx is the chassis number and
+ ETHy is the ethernet port number.
+ - Matched port pairs from each Fabric Module in the Chassis will
+ be used.
+ - To modify required ports for a LAG specify only the ports required
+ by the LAG. Any ports currently used by the LAG not specified will be
+ disconnected from the LAG.
+ type: list
+ elements: str
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Add LAG
+ purestorage.flashblade.purefb_lag:
+ name: lag2
+ ports:
+ - ch1.eth2
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+
+- name: Upate LAG
+ purestorage.flashblade.purefb_lag:
+ name: lag2
+ ports:
+ - ch1.eth2
+ - ch1.eth4
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+
+- name: Delete LAG
+ purestorage.flashblade.purefb_lag:
+ name: lag2
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+"""
+
+RETURN = r"""
+lag:
+ description: A dictionary describing the LAG.
+ type: dict
+ returned: success
+ contains:
+ lag_speed:
+ description: Combined speed of all ports in the LAG in Gb/s
+ type: str
+ port_speed:
+ description: Configured speed of each port in the LAG in Gb/s
+ type: str
+ mac_address:
+ description: Unique MAC address assigned to the LAG
+ type: str
+ status:
+ description: Health status of the LAG.
+ type: str
+"""
+
+HAS_PURESTORAGE = True
+try:
+ from pypureclient import flashblade
+except ImportError:
+ HAS_PURESTORAGE = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_system,
+ purefb_argument_spec,
+)
+
+MIN_REQUIRED_API_VERSION = "2.0"
+
+
+def delete_lag(module, blade):
+ """Delete Link Aggregation Group"""
+ changed = True
+ if not module.check_mode:
+ res = blade.delete_link_aggregation_groups(names=[module.params["name"]])
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to delete LAG {0}. Error: {1}".format(
+ module.params["name"], res.errors[0].message
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def update_lag(module, blade):
+ """Update Link Aggregation Group"""
+ changed = False
+ used_ports = []
+ current_ports = []
+ lagfact = []
+ current_lag = list(
+ blade.get_link_aggregation_groups(names=[module.params["name"]]).items
+ )[0]
+ for port in range(0, len(current_lag.ports)):
+ used_ports.append(current_lag.ports[port].name)
+ for lag_port in range(0, len(module.params["ports"]), 2):
+ if (
+ not (
+ module.params["ports"][lag_port].split(".")[0].upper()
+ + ".FM1."
+ + module.params["ports"][lag_port].split(".")[1].upper()
+ )
+ in used_ports
+ ):
+ current_lags = list(blade.get_link_aggregation_groups().items)
+ for lag in range(0, len(current_lags)):
+ for port in range(0, len(current_lags[lag].ports)):
+ current_ports.append(current_lags[lag].ports[port].name)
+ for current_lag_port in range(0, len(current_ports)):
+ if (
+ module.params["ports"][lag_port].split(".")[0].upper()
+ + ".FM1."
+ + module.params["ports"][lag_port].split(".")[1].upper()
+ ) in current_ports:
+ module.fail_json(
+ msg="Selected port {0} is currently in use by another LAG.".format(
+ module.params["ports"][lag_port].upper()
+ )
+ )
+ new_ports = []
+ for port in range(0, len(module.params["ports"])):
+ new_ports.append(
+ module.params["ports"][port].split(".")[0].upper()
+ + ".FM1."
+ + module.params["ports"][port].split(".")[1].upper()
+ )
+ new_ports.append(
+ module.params["ports"][port].split(".")[0].upper()
+ + ".FM2."
+ + module.params["ports"][port].split(".")[1].upper()
+ )
+ ports = []
+ for final_port in range(0, len(new_ports)):
+ ports.append(flashblade.FixedReference(name=new_ports[final_port]))
+ link_aggregation_group = flashblade.Linkaggregationgroup(ports=ports)
+ if sorted(used_ports) != sorted(new_ports):
+ changed = True
+ if not module.check_mode:
+ res = blade.patch_link_aggregation_groups(
+ names=[module.params["name"]],
+ link_aggregation_group=link_aggregation_group,
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to update LAG {0}. Error: {1}".format(
+ module.params["name"],
+ res.errors[0].message,
+ )
+ )
+ else:
+ response = list(res.items)[0]
+ lagfact = {
+ "mac_address": response.mac_address,
+ "port_speed": str(response.port_speed / 1000000000) + "Gb/s",
+ "lag_speed": str(response.lag_speed / 1000000000) + "Gb/s",
+ "status": response.status,
+ }
+ module.exit_json(changed=changed, lag=lagfact)
+
+
+def create_lag(module, blade):
+ """Create Link Aggregation Group"""
+ changed = True
+ used_ports = []
+ lagfact = []
+ current_lags = list(blade.get_link_aggregation_groups().items)
+ for lag in range(0, len(current_lags)):
+ for port in range(0, len(current_lags[lag].ports)):
+ used_ports.append(current_lags[lag].ports[port].name)
+ for lag_port in range(0, len(module.params["ports"])):
+ if (
+ module.params["ports"][lag_port].split(".")[0].upper()
+ + ".FM1."
+ + module.params["ports"][0].split(".")[1].upper()
+ ) in used_ports:
+ module.fail_json(
+ msg="Selected port {0} is currently in use by another LAG.".format(
+ module.params["ports"][lag_port].upper()
+ )
+ )
+ new_ports = []
+ for new_port in range(0, len(module.params["ports"])):
+ new_ports.append(
+ module.params["ports"][new_port].split(".")[0].upper()
+ + ".FM1."
+ + module.params["ports"][new_port].split(".")[1].upper()
+ )
+ new_ports.append(
+ module.params["ports"][new_port].split(".")[0].upper()
+ + ".FM2."
+ + module.params["ports"][new_port].split(".")[1].upper()
+ )
+ ports = []
+ for final_port in range(0, len(new_ports)):
+ ports.append(flashblade.FixedReference(name=new_ports[final_port]))
+ link_aggregation_group = flashblade.LinkAggregationGroup(ports=ports)
+ if not module.check_mode:
+ res = blade.post_link_aggregation_groups(
+ names=[module.params["name"]], link_aggregation_group=link_aggregation_group
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to create LAG {0}. Error: {1}".format(
+ module.params["name"],
+ res.errors[0].message,
+ )
+ )
+ else:
+ response = list(res.items)[0]
+ lagfact = {
+ "mac_address": response.mac_address,
+ "port_speed": str(response.port_speed / 1000000000) + "Gb/s",
+ "lag_speed": str(response.lag_speed / 1000000000) + "Gb/s",
+ "status": response.status,
+ }
+ module.exit_json(changed=changed, lag=lagfact)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ name=dict(type="str", default="uplink"),
+ ports=dict(type="list", elements="str"),
+ )
+ )
+
+ required_if = [["state", "present", ["ports"]]]
+
+ module = AnsibleModule(
+ argument_spec, required_if=required_if, supports_check_mode=True
+ )
+
+ if not HAS_PURESTORAGE:
+ module.fail_json(msg="py-pure-client sdk is required for this module")
+
+ blade = get_system(module)
+ api_version = list(blade.get_versions().items)
+
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(
+ msg="FlashBlade REST version not supported. "
+ "Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
+ )
+ state = module.params["state"]
+
+ exists = bool(
+ blade.get_link_aggregation_groups(names=[module.params["name"]]).status_code
+ == 200
+ )
+ if module.params["ports"]:
+ # Remove duplicates
+ module.params["ports"] = list(dict.fromkeys(module.params["ports"]))
+ if not exists and state == "present":
+ create_lag(module, blade)
+ elif exists and state == "present":
+ update_lag(module, blade)
+ elif exists and state == "absent":
+ if module.params["name"].lower() == "uplink":
+ module.fail_json(
+ msg="Preconfigured Link Aggregation Group cannot be deleted"
+ )
+ else:
+ delete_lag(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_lifecycle.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_lifecycle.py
new file mode 100644
index 000000000..0403aedcb
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_lifecycle.py
@@ -0,0 +1,490 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2020, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_lifecycle
+version_added: '1.4.0'
+short_description: Manage FlashBlade object lifecycles
+description:
+- Manage lifecycles for object buckets
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Create or delete lifecycle rule
+ default: present
+ type: str
+ choices: [ absent, present ]
+ bucket:
+ description:
+ - Bucket the lifecycle rule applies to
+ type: str
+ required: true
+ name:
+ description:
+ - Name of the lifecycle rule
+ type: str
+ required: true
+ enabled:
+ description:
+ - State of lifecycle rule
+ type: bool
+ default: true
+ keep_previous_for:
+ aliases: [ keep_for ]
+ description:
+ - Time after which previous versions will be marked expired.
+ - Enter as days (d) or weeks (w). Range is 1 - 2147483647 days.
+ type: str
+ keep_current_for:
+ description:
+ - Time after which current versions will be marked expired.
+ - Enter as days (d) or weeks (w). Range is 1 - 2147483647 days.
+ version_added: "1.8.0"
+ type: str
+ keep_current_until:
+ description:
+ - Date after which current versions will be marked expired.
+ - Enter as date in form YYYY-MM-DD.
+ - B(Note:) setting a date in the past will delete ALL objects with
+ the value of I(prefix) as they are created.
+ version_added: "1.8.0"
+ type: str
+ abort_uploads_after:
+ description:
+ - Duration of time after which incomplete multipart uploads will be aborted.
+ - Enter as days (d) or weeks (w). Range is 1 - 2147483647 days.
+ version_added: "1.8.0"
+ type: str
+ prefix:
+ description:
+ - Object key prefix identifying one or more objects in the bucket
+ type: str
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Create a lifecycle rule called bar for bucket foo (pre-Purity//FB 3.2.3)
+ purestorage.flashblade.purefb_lifecycle:
+ name: bar
+ bucket: foo
+ keep_previous_for: 2d
+ prefix: test
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Create a lifecycle rule called bar for bucket foo (post-Purity//FB 3.2.3)
+ purestorage.flashblade.purefb_lifecycle:
+ name: bar
+ bucket: foo
+ keep_previous_for: 2d
+ keep_current_for: 1w
+ abort_uploads_after: 1d
+ keep_current_until: 2020-11-23
+ prefix: test
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Modify a lifecycle rule (post-Purity//FB 3.2.3)
+ purestorage.flashblade.purefb_lifecycle:
+ name: bar
+ bucket: foo
+ keep_previous_for: 10d
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Delete lifecycle rule foo from bucket foo
+ purestorage.flashblade.purefb_lifecycle:
+ name: foo
+ bucket: bar
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURITYFB = True
+try:
+ from purity_fb import LifecycleRulePost, LifecycleRulePatch, Reference
+except ImportError:
+ HAS_PURITYFB = False
+
+HAS_PYPURECLIENT = True
+try:
+ from pypureclient import flashblade
+except ImportError:
+ HAS_PYPURECLIENT = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ get_system,
+ purefb_argument_spec,
+)
+from datetime import datetime
+
+
+MIN_REQUIRED_API_VERSION = "1.10"
+LIFECYCLE_API_VERSION = "2.1"
+
+
+def _get_bucket(module, blade):
+ s3bucket = None
+ buckets = blade.buckets.list_buckets()
+ for bucket in range(0, len(buckets.items)):
+ if buckets.items[bucket].name == module.params["bucket"]:
+ s3bucket = buckets.items[bucket]
+ return s3bucket
+
+
+def _convert_date_to_epoch(module):
+ try:
+ unix_date = datetime.strptime(module.params["keep_current_until"], "%Y-%m-%d")
+ except ValueError:
+ module.fail_json(msg="Incorrect data format, should be YYYY-MM-DD")
+ if unix_date < datetime.utcnow():
+ module.warn(
+ "This value of `keep_current_until` will permanently delete objects "
+ "as they are created. Using this date is not recommended"
+ )
+ epoch_milliseconds = int((unix_date - datetime(1970, 1, 1)).total_seconds() * 1000)
+ return epoch_milliseconds
+
+
+def _convert_to_millisecs(day):
+ try:
+ if day[-1:].lower() == "w":
+ return int(day[:-1]) * 7 * 86400000
+ elif day[-1:].lower() == "d":
+ return int(day[:-1]) * 86400000
+ except Exception:
+ return 0
+ return 0
+
+
+def _findstr(text, match):
+ for line in text.splitlines():
+ if match in line:
+ found = line
+ return found
+
+
+def delete_rule(module, blade):
+ """Delete lifecycle rule"""
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.lifecycle_rules.delete_lifecycle_rules(
+ names=[module.params["bucket"] + "/" + module.params["name"]]
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to delete lifecycle rule {0} for bucket {1}.".format(
+ module.params["name"], module.params["bucket"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def create_rule(module, blade, bladev2=None):
+ """Create lifecycle policy"""
+ changed = True
+ if bladev2:
+ if (
+ not module.params["keep_previous_for"]
+ and not module.params["keep_current_until"]
+ and not module.params["keep_current_for"]
+ ):
+ module.fail_json(
+ msg="At least one `keep...` parameter is required to create a new lifecycle rule"
+ )
+
+ else:
+ if not module.params["keep_previous_for"] and not bladev2:
+ module.fail_json(
+ msg="'keep_previous_for' is required to create a new lifecycle rule"
+ )
+ if not module.check_mode:
+ if not bladev2:
+ try:
+ attr = LifecycleRulePost(
+ bucket=Reference(name=module.params["bucket"]),
+ rule_id=module.params["name"],
+ keep_previous_version_for=_convert_to_millisecs(
+ module.params["keep_previous_for"]
+ ),
+ prefix=module.params["prefix"],
+ )
+ blade.lifecycle_rules.create_lifecycle_rules(
+ rule=attr, confirm_date=True
+ )
+ if not module.params["enabled"]:
+ attr = LifecycleRulePatch()
+ attr.enabled = False
+ blade.lifecycle_rules.update_lifecycle_rules(
+ name=[module.params["bucket"] + "/" + module.params["name"]],
+ rule=attr,
+ confirm_date=True,
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to create lifecycle rule {0} for bucket {1}.".format(
+ module.params["name"], module.params["bucket"]
+ )
+ )
+ else:
+ attr = flashblade.LifecycleRulePost(
+ bucket=flashblade.Reference(name=module.params["bucket"]),
+ rule_id=module.params["name"],
+ keep_previous_version_for=_convert_to_millisecs(
+ module.params["keep_previous_for"]
+ ),
+ keep_current_version_until=module.params["keep_current_until"],
+ keep_current_version_for=_convert_to_millisecs(
+ module.params["keep_current_for"]
+ ),
+ abort_incomplete_multipart_uploads_after=_convert_to_millisecs(
+ module.params["abort_uploads_after"]
+ ),
+ prefix=module.params["prefix"],
+ )
+ if attr.keep_current_version_until:
+ res = bladev2.post_lifecycle_rules(rule=attr, confirm_date=True)
+ else:
+ res = bladev2.post_lifecycle_rules(rule=attr)
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to create lifecycle rule {0} for bucket {1}. Error: {2}".format(
+ module.params["name"],
+ module.params["bucket"],
+ res.errors[0].message,
+ )
+ )
+ if not module.params["enabled"]:
+ attr = flashblade.LifecycleRulePatch(enabled=module.params["enabled"])
+ res = bladev2.patch_lifecycle_rules(
+ names=[module.params["bucket"] + "/" + module.params["name"]],
+ lifecycle=attr,
+ )
+ if res.status_code != 200:
+ module.warn(
+ "Lifecycle Rule {0} did not enable correctly. "
+ "Please chack your FlashBlade".format(module.params["name"])
+ )
+ module.exit_json(changed=changed)
+
+
+def update_rule(module, blade, rule, bladev2=None):
+ """Update snapshot policy"""
+ changed = False
+ if not bladev2:
+ current_rule = {
+ "prefix": rule.prefix,
+ "keep_previous_version_for": rule.keep_previous_version_for,
+ "enabled": rule.enabled,
+ }
+ else:
+ current_rule = {
+ "prefix": rule.prefix,
+ "abort_incomplete_multipart_uploads_after": rule.abort_incomplete_multipart_uploads_after,
+ "keep_current_version_for": rule.keep_current_version_for,
+ "keep_previous_version_for": rule.keep_previous_version_for,
+ "keep_current_version_until": rule.keep_current_version_until,
+ "enabled": rule.enabled,
+ }
+ if not module.params["prefix"]:
+ prefix = current_rule["prefix"]
+ else:
+ prefix = module.params["prefix"]
+ if not module.params["keep_previous_for"]:
+ keep_previous_for = current_rule["keep_previous_version_for"]
+ else:
+ keep_previous_for = _convert_to_millisecs(module.params["keep_previous_for"])
+ if bladev2:
+ if not module.params["keep_current_for"]:
+ keep_current_for = current_rule["keep_current_version_for"]
+ else:
+ keep_current_for = _convert_to_millisecs(module.params["keep_current_for"])
+ if not module.params["abort_uploads_after"]:
+ abort_uploads_after = current_rule[
+ "abort_incomplete_multipart_uploads_after"
+ ]
+ else:
+ abort_uploads_after = _convert_to_millisecs(
+ module.params["abort_uploads_after"]
+ )
+ if not module.params["keep_current_until"]:
+ keep_current_until = current_rule["keep_current_version_until"]
+ else:
+ keep_current_until = module.params["keep_current_until"]
+ new_rule = {
+ "prefix": prefix,
+ "abort_incomplete_multipart_uploads_after": abort_uploads_after,
+ "keep_current_version_for": keep_current_for,
+ "keep_previous_version_for": keep_previous_for,
+ "keep_current_version_until": keep_current_until,
+ "enabled": module.params["enabled"],
+ }
+ else:
+ new_rule = {
+ "prefix": prefix,
+ "keep_previous_version_for": keep_previous_for,
+ "enabled": module.params["enabled"],
+ }
+ if current_rule != new_rule:
+ changed = True
+ if not module.check_mode:
+ if not bladev2:
+ try:
+ attr = LifecycleRulePatch(
+ keep_previous_version_for=new_rule["keep_previous_version_for"],
+ prefix=new_rule["prefix"],
+ )
+ attr.enabled = module.params["enabled"]
+ blade.lifecycle_rules.update_lifecycle_rules(
+ names=[module.params["bucket"] + "/" + module.params["name"]],
+ rule=attr,
+ confirm_date=True,
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to update lifecycle rule {0} for bucket {1}.".format(
+ module.params["name"], module.params["bucket"]
+ )
+ )
+ else:
+ attr = flashblade.LifecycleRulePatch(
+ keep_previous_version_for=new_rule["keep_previous_version_for"],
+ keep_current_version_for=new_rule["keep_current_version_for"],
+ keep_current_version_until=new_rule["keep_current_version_until"],
+ abort_incomplete_multipart_uploads_after=new_rule[
+ "abort_incomplete_multipart_uploads_after"
+ ],
+ prefix=new_rule["prefix"],
+ enabled=new_rule["enabled"],
+ )
+ if attr.keep_current_version_until:
+ res = bladev2.patch_lifecycle_rules(
+ names=[module.params["bucket"] + "/" + module.params["name"]],
+ lifecycle=attr,
+ confirm_date=True,
+ )
+ else:
+ res = bladev2.patch_lifecycle_rules(
+ names=[module.params["bucket"] + "/" + module.params["name"]],
+ lifecycle=attr,
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to update lifecycle rule {0} for bucket {1}. Error: {2}".format(
+ module.params["name"],
+ module.params["bucket"],
+ res.errors[0].message,
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ enabled=dict(type="bool", default=True),
+ bucket=dict(type="str", required=True),
+ name=dict(type="str", required=True),
+ prefix=dict(
+ type="str",
+ ),
+ keep_previous_for=dict(type="str", aliases=["keep_for"]),
+ keep_current_for=dict(type="str"),
+ keep_current_until=dict(type="str"),
+ abort_uploads_after=dict(type="str"),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ if not HAS_PURITYFB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ bladev2 = get_system(module)
+ versions = blade.api_version.list_versions().versions
+
+ if module.params["keep_previous_for"] and not module.params["keep_previous_for"][
+ -1:
+ ].lower() in ["w", "d"]:
+ module.fail_json(
+ msg="'keep_previous_for' format incorrect - specify as 'd' or 'w'"
+ )
+ if module.params["keep_current_for"] and not module.params["keep_current_for"][
+ -1:
+ ].lower() in ["w", "d"]:
+ module.fail_json(
+ msg="'keep_current_for' format incorrect - specify as 'd' or 'w'"
+ )
+ if module.params["abort_uploads_after"] and not module.params[
+ "abort_uploads_after"
+ ][-1:].lower() in ["w", "d"]:
+ module.fail_json(
+ msg="'abort_uploads_after' format incorrect - specify as 'd' or 'w'"
+ )
+
+ if MIN_REQUIRED_API_VERSION not in versions:
+ module.fail_json(
+ msg="Minimum FlashBlade REST version required: {0}".format(
+ MIN_REQUIRED_API_VERSION
+ )
+ )
+
+ if not _get_bucket(module, blade):
+ module.fail_json(
+ msg="Specified bucket {0} does not exist".format(module.params["bucket"])
+ )
+
+ try:
+ if LIFECYCLE_API_VERSION not in versions:
+ rule = blade.lifecycle_rules.list_lifecycle_rules(
+ names=[module.params["bucket"] + "/" + module.params["name"]]
+ ).items[0]
+ else:
+ if module.params["keep_current_until"]:
+ module.params["keep_current_until"] = _convert_date_to_epoch(module)
+ bladev2 = get_system(module)
+ rule = list(
+ bladev2.get_lifecycle_rules(
+ names=[module.params["bucket"] + "/" + module.params["name"]]
+ ).items
+ )[0]
+ except Exception:
+ rule = None
+
+ if rule and state == "present":
+ update_rule(module, blade, rule, bladev2)
+ elif state == "present" and not rule:
+ create_rule(module, blade, bladev2)
+ elif state == "absent" and rule:
+ delete_rule(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_messages.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_messages.py
new file mode 100644
index 000000000..bbfe57f95
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_messages.py
@@ -0,0 +1,193 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2022, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_messages
+version_added: '1.10.0'
+short_description: List FlashBlade Alert Messages
+description:
+- List Alert messages based on filters provided
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ severity:
+ description:
+ - severity of the alerts to show
+ type: list
+ elements: str
+ choices: [ all, critical, warning, info ]
+ default: [ all ]
+ state:
+ description:
+ - State of alerts to show
+ default: open
+ choices: [ all, open, closed ]
+ type: str
+ flagged:
+ description:
+ - Show alerts that have been acknowledged or not
+ default: false
+ type: bool
+ history:
+ description:
+ - Historical time period to show alerts for, from present time
+ - Allowed time period are hour(h), day(d), week(w) and year(y)
+ type: str
+ default: 1w
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Show critical alerts from past 4 weeks that haven't been acknowledged
+ purefb_messages:
+ history: 4w
+ flagged : false
+ severity:
+ - critical
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+"""
+
+RETURN = r"""
+"""
+
+import time
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_system,
+ purefb_argument_spec,
+)
+
+MIN_REQUIRED_API_VERSION = "2.0"
+ALLOWED_PERIODS = ["h", "d", "w", "y"]
+# Time periods in micro-seconds
+HOUR = 3600000
+DAY = HOUR * 24
+WEEK = DAY * 7
+YEAR = WEEK * 52
+
+
+def _create_time_window(window):
+ period = window[-1].lower()
+ multiple = int(window[0:-1])
+ if period == "h":
+ return HOUR * multiple
+ if period == "d":
+ return DAY * multiple
+ if period == "w":
+ return WEEK * multiple
+ if period == "y":
+ return YEAR * multiple
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(type="str", default="open", choices=["all", "open", "closed"]),
+ history=dict(type="str", default="1w"),
+ flagged=dict(type="bool", default=False),
+ severity=dict(
+ type="list",
+ elements="str",
+ default=["all"],
+ choices=["all", "critical", "warning", "info"],
+ ),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ time_now = int(time.time() * 1000)
+ blade = get_system(module)
+ api_version = list(blade.get_versions().items)
+
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(
+ msg="FlashBlade REST version not supported. "
+ "Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
+ )
+ if module.params["history"][-1].lower() not in ALLOWED_PERIODS:
+ module.fail_json(msg="historical window value is not an allowsd time period")
+ since_time = str(time_now - _create_time_window(module.params["history"].lower()))
+ if module.params["flagged"]:
+ flagged = " and flagged='True'"
+ else:
+ flagged = " and flagged='False'"
+
+ multi_sev = False
+ if len(module.params["severity"]) > 1:
+ if "all" in module.params["severity"]:
+ module.params["severity"] = ["*"]
+ else:
+ multi_sev = True
+ if multi_sev:
+ severity = " and ("
+ for level in range(0, len(module.params["severity"])):
+ severity += "severity='" + str(module.params["severity"][level]) + "' or "
+ severity = severity[0:-4] + ")"
+ else:
+ if module.params["severity"] == ["all"]:
+ severity = " and severity='*'"
+ else:
+ severity = " and severity='" + str(module.params["severity"][0]) + "'"
+ messages = {}
+ if module.params["state"] == "all":
+ state = " and state='*'"
+ else:
+ state = " and state='" + module.params["state"] + "'"
+ filter_string = "notified>" + since_time + state + flagged + severity
+ try:
+ res = blade.get_alerts(filter=filter_string)
+ alerts = list(res.items)
+ except Exception:
+ module.fail_json(
+ msg="Failed to get alert messages. Error: {0}".format(res.errors[0].message)
+ )
+ for message in range(0, len(alerts)):
+ name = alerts[message].name
+ messages[name] = {
+ "summary": alerts[message].summary,
+ "component_type": alerts[message].component_type,
+ "component_name": alerts[message].component_name,
+ "description": alerts[message].description,
+ "code": alerts[message].code,
+ "severity": alerts[message].severity,
+ "state": alerts[message].state,
+ "flagged": alerts[message].flagged,
+ "created": time.strftime(
+ "%Y-%m-%d %H:%M:%S",
+ time.gmtime(alerts[message].created / 1000),
+ )
+ + " UTC",
+ "notified": time.strftime(
+ "%Y-%m-%d %H:%M:%S",
+ time.gmtime(alerts[message].notified / 1000),
+ )
+ + " UTC",
+ "updated": time.strftime(
+ "%Y-%m-%d %H:%M:%S",
+ time.gmtime(alerts[message].updated / 1000),
+ )
+ + " UTC",
+ }
+ module.exit_json(changed=False, purefb_messages=messages)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_network.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_network.py
new file mode 100644
index 000000000..27693e32c
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_network.py
@@ -0,0 +1,224 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2017, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+---
+module: purefb_network
+version_added: "1.0.0"
+short_description: Manage network interfaces in a Pure Storage FlashBlade
+description:
+ - This module manages network interfaces on Pure Storage FlashBlade.
+ - When creating a network interface a subnet must already exist with
+ a network prefix that covers the IP address of the interface being
+ created.
+author: Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - Interface Name.
+ required: true
+ type: str
+ state:
+ description:
+ - Create, delete or modifies a network interface.
+ required: false
+ default: present
+ choices: [ "present", "absent" ]
+ type: str
+ address:
+ description:
+ - IP address of interface.
+ required: false
+ type: str
+ services:
+ description:
+ - Define which services are configured for the interfaces.
+ required: false
+ choices: [ "data", "replication" ]
+ default: data
+ type: str
+ itype:
+ description:
+ - Type of interface.
+ required: false
+ choices: [ "vip" ]
+ default: vip
+ type: str
+extends_documentation_fragment:
+ - purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = """
+- name: Create new network interface named foo
+ purestorage.flashblade.purefb_network:
+ name: foo
+ address: 10.21.200.23
+ state: present
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Change IP address of network interface named foo
+ purestorage.flashblade.purefb_network:
+ name: foo
+ state: present
+ address: 10.21.200.123
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Delete network interface named foo
+ purestorage.flashblade.purefb_network:
+ name: foo
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641"""
+
+RETURN = """
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import NetworkInterface
+except ImportError:
+ HAS_PURITY_FB = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+MINIMUM_API_VERSION = "1.3"
+
+
+def get_iface(module, blade):
+ """Return Filesystem or None"""
+ iface = []
+ iface.append(module.params["name"])
+ try:
+ res = blade.network_interfaces.list_network_interfaces(names=iface)
+ return res.items[0]
+ except Exception:
+ return None
+
+
+def create_iface(module, blade):
+ """Create Network Interface"""
+ changed = True
+ if not module.check_mode:
+ iface = []
+ services = []
+ iface.append(module.params["name"])
+ services.append(module.params["services"])
+ try:
+ blade.network_interfaces.create_network_interfaces(
+ names=iface,
+ network_interface=NetworkInterface(
+ address=module.params["address"],
+ services=services,
+ type=module.params["itype"],
+ ),
+ )
+ except Exception:
+ module.fail_json(
+ msg="Interface creation failed. Check subnet exists for {0}".format(
+ module.params["address"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def modify_iface(module, blade):
+ """Modify Network Interface IP address"""
+ changed = False
+ iface = get_iface(module, blade)
+ iface_new = []
+ iface_new.append(module.params["name"])
+ if module.params["address"] != iface.address:
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.network_interfaces.update_network_interfaces(
+ names=iface_new,
+ network_interface=NetworkInterface(
+ address=module.params["address"]
+ ),
+ )
+ changed = True
+ except Exception:
+ module.fail_json(
+ msg="Failed to modify Interface {0}".format(module.params["name"])
+ )
+ module.exit_json(changed=changed)
+
+
+def delete_iface(module, blade):
+ """Delete Network Interface"""
+ changed = True
+ if not module.check_mode:
+ iface = []
+ iface.append(module.params["name"])
+ try:
+ blade.network_interfaces.delete_network_interfaces(names=iface)
+ except Exception:
+ module.fail_json(
+ msg="Failed to delete network {0}".format(module.params["name"])
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(required=True),
+ state=dict(default="present", choices=["present", "absent"]),
+ address=dict(),
+ services=dict(default="data", choices=["data", "replication"]),
+ itype=dict(default="vip", choices=["vip"]),
+ )
+ )
+
+ required_if = [["state", "present", ["address"]]]
+
+ module = AnsibleModule(
+ argument_spec, required_if=required_if, supports_check_mode=True
+ )
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ api_version = blade.api_version.list_versions().versions
+ if MINIMUM_API_VERSION not in api_version:
+ module.fail_json(msg="Upgrade Purity//FB to enable this module")
+ iface = get_iface(module, blade)
+
+ if state == "present" and not iface:
+ create_iface(module, blade)
+ elif state == "present" and iface:
+ modify_iface(module, blade)
+ elif state == "absent" and iface:
+ delete_iface(module, blade)
+ elif state == "absent" and not iface:
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_ntp.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_ntp.py
new file mode 100644
index 000000000..7b18442c9
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_ntp.py
@@ -0,0 +1,158 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2018, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_ntp
+version_added: '1.0.0'
+short_description: Configure Pure Storage FlashBlade NTP settings
+description:
+- Set or erase NTP configuration for Pure Storage FlashBlades.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Create or delete NTP servers configuration
+ type: str
+ default: present
+ choices: [ absent, present ]
+ ntp_servers:
+ type: list
+ elements: str
+ description:
+ - A list of up to 4 alternate NTP servers. These may include IPv4,
+ IPv6 or FQDNs. Invalid IP addresses will cause the module to fail.
+ No validation is performed for FQDNs.
+ - If more than 4 servers are provided, only the first 4 unique
+ nameservers will be used.
+ - if no servers are given a default of I(0.pool.ntp.org) will be used.
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Delete exisitng NTP server entries
+ purestorage.flashblade.purefb_ntp:
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+
+- name: Set array NTP servers
+ purestorage.flashblade.purefb_ntp:
+ state: present
+ ntp_servers:
+ - "0.pool.ntp.org"
+ - "1.pool.ntp.org"
+ - "2.pool.ntp.org"
+ - "3.pool.ntp.org"
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import PureArray
+except ImportError:
+ HAS_PURITY_FB = False
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.3"
+
+
+def remove(duplicate):
+ final_list = []
+ for num in duplicate:
+ if num not in final_list:
+ final_list.append(num)
+ return final_list
+
+
+def delete_ntp(module, blade):
+ """Delete NTP Servers"""
+ changed = True
+ if not module.check_mode:
+ if blade.arrays.list_arrays().items[0].ntp_servers != []:
+ try:
+ blade_settings = PureArray(ntp_servers=[])
+ blade.arrays.update_arrays(array_settings=blade_settings)
+ except Exception:
+ module.fail_json(msg="Deletion of NTP servers failed")
+ module.exit_json(changed=changed)
+
+
+def create_ntp(module, blade):
+ """Set NTP Servers"""
+ changed = True
+ if not module.check_mode:
+ if not module.params["ntp_servers"]:
+ module.params["ntp_servers"] = ["0.pool.ntp.org"]
+ try:
+ blade_settings = PureArray(ntp_servers=module.params["ntp_servers"][0:4])
+ blade.arrays.update_arrays(array_settings=blade_settings)
+ except Exception:
+ module.fail_json(msg="Update of NTP servers failed")
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ ntp_servers=dict(type="list", elements="str"),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ )
+ )
+
+ required_if = [["state", "present", ["ntp_servers"]]]
+
+ module = AnsibleModule(
+ argument_spec, required_if=required_if, supports_check_mode=True
+ )
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ blade = get_blade(module)
+
+ api_version = blade.api_version.list_versions().versions
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(msg="Purity//FB must be upgraded to support this module.")
+
+ if module.params["state"] == "absent":
+ delete_ntp(module, blade)
+ else:
+ module.params["ntp_servers"] = remove(module.params["ntp_servers"])
+ if sorted(blade.arrays.list_arrays().items[0].ntp_servers) != sorted(
+ module.params["ntp_servers"][0:4]
+ ):
+ create_ntp(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_phonehome.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_phonehome.py
new file mode 100644
index 000000000..20b99e8a0
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_phonehome.py
@@ -0,0 +1,124 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2018, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_phonehome
+version_added: '1.0.0'
+short_description: Enable or Disable Pure Storage FlashBlade Phone Home
+description:
+- Enablke or Disable Remote Phone Home for a Pure Storage FlashBlade.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Define state of phone home
+ type: str
+ default: present
+ choices: [ present, absent ]
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Enable Remote Phone Home
+ purestorage.flashblade.purefb_phonehome:
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Disable Remote Phone Home
+ purestorage.flashblade.purefb_phonehome:
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import Support
+except ImportError:
+ HAS_PURITY_FB = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.6"
+
+
+def enable_ph(module, blade):
+ """Enable Phone Hone"""
+ changed = True
+ if not module.check_mode:
+ ph_settings = Support(phonehome_enabled=True)
+ try:
+ blade.support.update_support(support=ph_settings)
+ except Exception:
+ module.fail_json(msg="Enabling Phone Home failed")
+ module.exit_json(changed=changed)
+
+
+def disable_ph(module, blade):
+ """Disable Phone Home"""
+ changed = True
+ if not module.check_mode:
+ ph_settings = Support(phonehome_enabled=False)
+ try:
+ blade.support.update_support(support=ph_settings)
+ except Exception:
+ module.fail_json(msg="Disabling Phone Home failed")
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ blade = get_blade(module)
+ api_version = blade.api_version.list_versions().versions
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(msg="Purity//FB must be upgraded to support this module.")
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb SDK is required for this module")
+
+ if (
+ module.params["state"] == "present"
+ and not blade.support.list_support().items[0].phonehome_enabled
+ ):
+ enable_ph(module, blade)
+ elif (
+ module.params["state"] == "absent"
+ and blade.support.list_support().items[0].phonehome_enabled
+ ):
+ disable_ph(module, blade)
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_pingtrace.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_pingtrace.py
new file mode 100644
index 000000000..e9f20a158
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_pingtrace.py
@@ -0,0 +1,277 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2023, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_pingtrace
+version_added: '1.11.0'
+short_description: Employ the internal FlashBlade ping and trace mechanisms
+description:
+- Ping or trace a destination
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ action:
+ description:
+ - Which action is required, ping or trace
+ type: str
+ choices: [ ping, trace ]
+ default: ping
+ count:
+ description:
+ - Used by ping to specify the number of packets to send
+ type: int
+ default: 1
+ resolve:
+ description:
+ - Specify whether or not to map IP addresses to host names
+ type: bool
+ default: True
+ latency:
+ description:
+ - Specify whether or not to print the full user-to-user latency
+ type: bool
+ default: False
+ packet_size:
+ description:
+ - Used by ping to specify the number of data bytes to send per packet
+ type: int
+ default: 56
+ destination:
+ description:
+ - IP addtress or hostname used to run ping or trace against.
+ type: str
+ required: true
+ method:
+ description:
+ - Used by trace to specify the method to use for operations
+ type: str
+ choices: [ icmp, tcp, udp ]
+ default: udp
+ fragment:
+ description:
+ - Used by trace to specify whether or not to fragment packets
+ type: bool
+ default: true
+ discover_mtu:
+ description:
+ - Used by trace to specify whether or not to discover the MTU
+ along the path being traced
+ type: bool
+ default: false
+ port:
+ description:
+ - Used by trace to specify a destination port
+ type: str
+ source:
+ description:
+ - IP address or hostname used by ping and trace to specify where
+ to start to run the specified operation
+ - If not specified will use all available sources
+ type: str
+ component:
+ description:
+ - Used by ping and trace to specify where to run the operation.
+ - Valid values are controllers and blades from hardware list.
+ - If not specified defaults to all available controllers and selected blades
+ type: str
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: ping Google DNS server
+ purestorage.flashblade.purefb_pingtrace:
+ destination: 8.8.8.8
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+
+- name: trace to Google DNS server from CH1.FM0
+ purestorage.flashblade.purefb_pingtrace:
+ action: trace
+ destination: 8.8.8.8
+ fragment_packet: true
+ source: CH1.FM0
+ discover_mtu: true
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+"""
+
+RETURN = r"""
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_system,
+ purefb_argument_spec,
+)
+
+MIN_REQUIRED_API_VERSION = "2.6"
+
+
+def run_ping(module, blade):
+ """Run network ping"""
+ ping_fact = {}
+ if module.params["source"] and module.params["component"]:
+ res = blade.get_network_interfaces_ping(
+ destination=module.params["destination"],
+ component=module.params["component"],
+ source=module.params["source"],
+ packet_size=module.params["packet_size"],
+ count=module.params["count"],
+ print_latency=module.params["latency"],
+ resolve_hostname=module.params["resolve"],
+ )
+ elif module.params["source"] and not module.params["component"]:
+ res = blade.get_network_interfaces_ping(
+ destination=module.params["destination"],
+ source=module.params["source"],
+ packet_size=module.params["packet_size"],
+ count=module.params["count"],
+ print_latency=module.params["latency"],
+ resolve_hostname=module.params["resolve"],
+ )
+ elif not module.params["source"] and module.params["component"]:
+ res = blade.get_network_interfaces_ping(
+ destination=module.params["destination"],
+ component=module.params["component"],
+ packet_size=module.params["packet_size"],
+ count=module.params["count"],
+ print_latency=module.params["latency"],
+ resolve_hostname=module.params["resolve"],
+ )
+ else:
+ res = blade.get_network_interfaces_ping(
+ destination=module.params["destination"],
+ packet_size=module.params["packet_size"],
+ count=module.params["count"],
+ print_latency=module.params["latency"],
+ resolve_hostname=module.params["resolve"],
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to run ping. Error: {0}".format(res.errors[0].message)
+ )
+ else:
+ responses = list(res.items)
+ for resp in range(0, len(responses)):
+ comp_name = responses[resp].component_name.replace(".", "_")
+ ping_fact[comp_name] = {
+ "details": responses[resp].details,
+ }
+
+ module.exit_json(changed=False, pingfact=ping_fact)
+
+
+def run_trace(module, blade):
+ """Run network trace"""
+ trace_fact = {}
+ if module.params["source"] and module.params["component"]:
+ res = blade.get_network_interfaces_trace(
+ port=module.params["port"],
+ destination=module.params["destination"],
+ component=module.params["component"],
+ discover_mtu=module.params["discover_mtu"],
+ source=module.params["source"],
+ fragment_packet=module.params["fragment"],
+ method=module.params["method"],
+ resolve_hostname=module.params["resolve"],
+ )
+ elif module.params["source"] and not module.params["component"]:
+ res = blade.get_network_interfaces_trace(
+ port=module.params["port"],
+ destination=module.params["destination"],
+ discover_mtu=module.params["discover_mtu"],
+ source=module.params["source"],
+ fragment_packet=module.params["fragment"],
+ method=module.params["method"],
+ resolve_hostname=module.params["resolve"],
+ )
+ elif not module.params["source"] and module.params["component"]:
+ res = blade.get_network_interfaces_trace(
+ port=module.params["port"],
+ destination=module.params["destination"],
+ discover_mtu=module.params["discover_mtu"],
+ component=module.params["component"],
+ fragment_packet=module.params["fragment"],
+ method=module.params["method"],
+ resolve_hostname=module.params["resolve"],
+ )
+ else:
+ res = blade.get_network_interfaces_trace(
+ port=module.params["port"],
+ destination=module.params["destination"],
+ discover_mtu=module.params["discover_mtu"],
+ fragment_packet=module.params["fragment"],
+ method=module.params["method"],
+ resolve_hostname=module.params["resolve"],
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to run trace. Error: {0}".format(res.errors[0].message)
+ )
+ else:
+ responses = list(res.items)
+ for resp in range(0, len(responses)):
+ comp_name = responses[resp].component_name.replace(".", "_")
+ trace_fact[comp_name] = {
+ "details": responses[resp].details,
+ }
+
+ module.exit_json(changed=False, tracefact=trace_fact)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ action=dict(type="str", choices=["ping", "trace"], default="ping"),
+ method=dict(type="str", choices=["icmp", "tcp", "udp"], default="udp"),
+ destination=dict(type="str", required=True),
+ source=dict(type="str"),
+ component=dict(type="str"),
+ port=dict(type="str"),
+ count=dict(type="int", default=1),
+ packet_size=dict(type="int", default=56),
+ resolve=dict(type="bool", default=True),
+ fragment=dict(type="bool", default=True),
+ latency=dict(type="bool", default=False),
+ discover_mtu=dict(type="bool", default=False),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ blade = get_system(module)
+ api_version = list(blade.get_versions().items)
+
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(
+ msg="FlashBlade REST version not supported. "
+ "Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
+ )
+
+ blade = get_system(module)
+ if module.params["action"] == "ping":
+ run_ping(module, blade)
+ else:
+ run_trace(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_policy.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_policy.py
new file mode 100644
index 000000000..273166de8
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_policy.py
@@ -0,0 +1,2079 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2020, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_policy
+version_added: '1.0.0'
+short_description: Manage FlashBlade policies
+description:
+- Manage policies for filesystem, file replica links and object store access.
+- To update an existing snapshot policy rule, you must first delete the
+ original rule and then add the new rule to replace it. Purity's best-fit
+ will try to ensure that any required snapshots deleted on the deletion of
+ the first rule will be recovered as long replacement rule is added before
+ the snapshot eradication period is exceeded (usuually 24 hours).
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Create or delete policy.
+ - Copy is applicable only to Object Store Access Policies Rules
+ default: present
+ type: str
+ choices: [ absent, present, copy ]
+ target:
+ description:
+ - Name of policy to copy rule to
+ type: str
+ version_added: "1.9.0"
+ target_rule:
+ description:
+ - Name of the rule to copy the exisitng rule to.
+ - If not defined the existing rule name is used.
+ type: str
+ version_added: "1.9.0"
+ policy_type:
+ description:
+ - Type of policy
+ default: snapshot
+ type: str
+ choices: [ snapshot, access, nfs ]
+ version_added: "1.9.0"
+ account:
+ description:
+ - Name of Object Store account policy applies to.
+ - B(Special Case) I(pure policy) is used for the system-wide S3 policies
+ type: str
+ version_added: "1.9.0"
+ rule:
+ description:
+ - Name of the rule for the Object Store Access Policy
+ - Rules in system wide policies cannot be deleted or modified
+ type: str
+ version_added: "1.9.0"
+ effect:
+ description:
+ - Allow S3 requests that match all of the I(actions) item selected.
+ Rules are additive.
+ type: str
+ default: allow
+ choices: [ allow ]
+ version_added: "1.9.0"
+ actions:
+ description:
+ - List of permissions to grant.
+ - System-wide policy rules cannot be deleted or modified
+ type: list
+ elements: str
+ choices:
+ - s3:*
+ - s3:AbortMultipartUpload
+ - s3:CreateBucket
+ - s3:DeleteBucket
+ - s3:DeleteObject
+ - s3:DeleteObjectVersion
+ - s3:ExtendSafemodeRetentionPeriod
+ - s3:GetBucketAcl
+ - s3:GetBucketLocation
+ - s3:GetBucketVersioning
+ - s3:GetLifecycleConfiguration
+ - s3:GetObject
+ - s3:GetObjectAcl
+ - s3:GetObjectVersion
+ - s3:ListAllMyBuckets
+ - s3:ListBucket
+ - s3:ListBucketMultipartUploads
+ - s3:ListBucketVersions
+ - s3:ListMultipartUploadParts
+ - s3:PutBucketVersioning
+ - s3:PutLifecycleConfiguration
+ - s3:PutObject
+ version_added: "1.9.0"
+ object_resources:
+ description:
+ - List of bucket names and object paths, with a wildcard (*) to
+ specify objects in a bucket; e.g., bucket1, bucket1/*, bucket2,
+ bucket2/*.
+ - System-wide policy rules cannot be deleted or modified
+ type: list
+ elements: str
+ version_added: "1.9.0"
+ source_ips:
+ description:
+ - List of IPs and subnets from which this rule should allow requests;
+ e.g., 10.20.30.40, 10.20.30.0/24, 2001:DB8:1234:5678::/64.
+ - System-wide policy rules cannot be deleted or modified
+ type: list
+ elements: str
+ version_added: "1.9.0"
+ s3_prefixes:
+ description:
+ - List of 'folders' (object key prefixes) for which object listings
+ may be requested.
+ - System-wide policy rules cannot be deleted or modified
+ type: list
+ elements: str
+ version_added: "1.9.0"
+ s3_delimiters:
+ description:
+ - List of delimiter characters allowed in object list requests.
+ - Grants permissions to list 'folder names' (prefixes ending in a
+ delimiter) instead of object keys.
+ - System-wide policy rules cannot be deleted or modified
+ type: list
+ elements: str
+ version_added: "1.9.0"
+ ignore_enforcement:
+ description:
+ - Certain combinations of actions and other rule elements are inherently
+ ignored if specified together in a rule.
+ - If set to true, operations which attempt to set these combinations will fail.
+ - If set to false, such operations will instead be allowed.
+ type: bool
+ default: true
+ version_added: "1.9.0"
+ user:
+ description:
+ - User in the I(account) that the policy is granted to.
+ type: str
+ version_added: "1.9.0"
+ force_delete:
+ description:
+ - Force the deletion of a Object Store Access Policy is this
+ has attached users.
+ - WARNING This can have undesired side-effects.
+ - System-wide policies cannot be deleted
+ type: bool
+ default: false
+ version_added: "1.9.0"
+ name:
+ description:
+ - Name of the policy
+ type: str
+ enabled:
+ description:
+ - State of policy
+ type: bool
+ default: true
+ every:
+ description:
+ - Interval between snapshots in seconds
+ - Range available 300 - 31536000 (equates to 5m to 365d)
+ type: int
+ keep_for:
+ description:
+ - How long to keep snapshots for
+ - Range available 300 - 31536000 (equates to 5m to 365d)
+ - Must not be set less than I(every)
+ type: int
+ at:
+ description:
+ - Provide a time in 12-hour AM/PM format, eg. 11AM
+ type: str
+ timezone:
+ description:
+ - Time Zone used for the I(at) parameter
+ - If not provided, the module will attempt to get the current local timezone from the server
+ type: str
+ filesystem:
+ description:
+ - List of filesystems to add to a policy on creation
+ - To amend policy members use the I(purestorage.flashblade.purefb_fs) module
+ type: list
+ elements: str
+ replica_link:
+ description:
+ - List of filesystem replica links to add to a policy on creation
+ - To amend policy members use the I(purestorage.flashblade.purefb_fs_replica) module
+ type: list
+ elements: str
+ access:
+ description:
+ - Specifies access control for the export policy rule
+ type: str
+ choices: [ root-squash, all-squash, no-squash ]
+ default: root-squash
+ version_added: "1.9.0"
+ anonuid:
+ description:
+ - Any user whose UID is affected by an I(access) of `root_squash` or `all_squash`
+ will have their UID mapped to anonuid.
+ The defaultis null, which means 65534.
+ Use "" to clear.
+ type: str
+ version_added: "1.9.0"
+ anongid:
+ description:
+ - Any user whose GID is affected by an I(access) of `root_squash` or `all_squash`
+ will have their GID mapped to anongid.
+ The default anongid is null, which means 65534.
+ Use "" to clear.
+ type: str
+ version_added: "1.9.0"
+ atime:
+ description:
+ - After a read operation has occurred, the inode access time is updated only if any
+ of the following conditions is true; the previous access time is less than the
+ inode modify time, the previous access time is less than the inode change time,
+ or the previous access time is more than 24 hours ago.
+ - If set to false, disables the update of inode access times after read operations.
+ type: bool
+ default: true
+ version_added: "1.9.0"
+ client:
+ description:
+ - Specifies the clients that will be permitted to access the export.
+ - Accepted notation is a single IP address, subnet in CIDR notation, netgroup, or
+ anonymous (*).
+ type: str
+ default: "*"
+ version_added: "1.9.0"
+ fileid_32bit:
+ description:
+ - Whether the file id is 32 bits or not.
+ type: bool
+ default: false
+ version_added: "1.9.0"
+ permission:
+ description:
+ - Specifies which read-write client access permissions are allowed for the export.
+ type: str
+ choices: [ rw, ro ]
+ default: ro
+ version_added: "1.9.0"
+ secure:
+ description:
+ - If true, this prevents NFS access to client connections coming from non-reserved ports.
+ - If false, allows NFS access to client connections coming from non-reserved ports.
+ - Applies to NFSv3, NFSv4.1, and auxiliary protocols MOUNT and NLM.
+ type: bool
+ default: false
+ version_added: "1.9.0"
+ security:
+ description:
+ - The security flavors to use for accessing files on this mount point.
+ - If the server does not support the requested flavor, the mount operation fails.
+ - I(sys) trusts the client to specify users identity.
+ - I(krb) provides cryptographic proof of a users identity in each RPC request.
+ - I(krb5i) adds integrity checking to krb5, to ensure the data has not been tampered with.
+ - I(krb5p) adds integrity checking and encryption to krb5.
+ type: list
+ elements: str
+ choices: [ sys, krb5, krb5i, krb5p ]
+ default: sys
+ version_added: "1.9.0"
+ before_rule:
+ description:
+ - The index of the client rule to insert or move a client rule before.
+ type: int
+ version_added: "1.9.0"
+ rename:
+ description:
+ - New name for export policy
+ - Only applies to NFS export policies
+ type: str
+ version_added: "1.10.0"
+ destroy_snapshots:
+ description:
+ - This parameter must be set to true in order to modify a policy such that local or remote snapshots would be destroyed.
+ type: bool
+ version_added: '1.11.0'
+ default: false
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Create a simple snapshot policy with no rules
+ purestorage.flashblade.purefb_policy:
+ name: test_policy
+ policy_type: snapshot
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Create a snapshot policy and connect to existing filesystems and filesystem replica links
+ purestorage.flashblade.purefb_policy:
+ name: test_policy_with_members
+ policy_type: snapshot
+ filesystem:
+ - fs1
+ - fs2
+ replica_link:
+ - rl1
+ - rl2
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Create a snapshot policy with rules
+ purestorage.flashblade.purefb_policy:
+ name: test_policy2
+ policy_type: snapshot
+ at: 11AM
+ keep_for: 86400
+ every: 86400
+ timezone: Asia/Shanghai
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Delete a snapshot policy
+ purestorage.flashblade.purefb_policy:
+ name: test_policy
+ policy_type: snapshot
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Create an empty object store access policy
+ purestorage.flashblade.purefb_policy:
+ name: test_os_policy
+ account: test
+ policy_type: access
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Create an empty object store access policy and assign user
+ purestorage.flashblade.purefb_policy:
+ name: test_os_policy
+ account: test
+ policy_type: access
+ user: fred
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Create a object store access policy with simple rule
+ purestorage.flashblade.purefb_policy:
+ name: test_os_policy_rule
+ policy_type: access
+ account: test
+ rule: rule1
+ actions: "s3:*"
+ object_resources: "*"
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Create an empty NFS export policy
+ purestorage.flashblade.purefb_policy:
+ name: test_nfs_export
+ policy_type: nfs
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Create an NFS export policy with a client rule
+ purestorage.flashblade.purefb_policy:
+ name: test_nfs_export
+ policy_type: nfs
+ atime: true
+ client: "10.0.1.0/24"
+ secure: true
+ security: [sys, krb5]
+ permission: rw
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Create a new rule for an existing NFS export policy
+ purestorage.flashblade.purefb_policy:
+ name: test_nfs_export
+ policy_type: nfs
+ atime: true
+ client: "10.0.2.0/24"
+ security: sys
+ permission: ro
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Delete a client rule from an NFS export policy
+ purestorage.flashblade.purefb_policy:
+ name: test_nfs_export
+ client: "10.0.1.0/24"
+ policy_type: nfs
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Delete an NFS export policy and all associated rules
+ purestorage.flashblade.purefb_policy:
+ name: test_nfs_export
+ state: absent
+ policy_type: nfs
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Delete a rule from an object store access policy
+ purestorage.flashblade.purefb_policy:
+ name: test_os_policy_rule
+ account: test
+ policy_type: access
+ rule: rule1
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Delete a user from an object store access policy
+ purestorage.flashblade.purefb_policy:
+ name: test_os_policy_rule
+ account: test
+ user: fred
+ policy_type: access
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Delete an object store access policy with attached users (USE WITH CAUTION)
+ purestorage.flashblade.purefb_policy:
+ name: test_os_policy_rule
+ account: test
+ policy_type: access
+ force_delete: true
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Delete an object store access policy with no attached users
+ purestorage.flashblade.purefb_policy:
+ name: test_os_policy_rule
+ account: test
+ policy_type: access
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Copy an object store access policy rule to another exisitng policy
+ purestorage.flashblade.purefb_policy:
+ name: test_os_policy_rule
+ policy_type: access
+ account: test
+ target: "account2/anotherpolicy"
+ target_rule: new_rule1
+ state: copy
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Rename an NFS Export Policy
+ purestorage.flashblade.purefb_policy:
+ name: old_name
+ policy_type: nfs
+ rename: new_name
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURITYFB = True
+try:
+ from purity_fb import Policy, PolicyRule, PolicyPatch
+except ImportError:
+ HAS_PURITYFB = False
+
+HAS_PYPURECLIENT = True
+try:
+ from pypureclient.flashblade import (
+ PolicyRuleObjectAccessCondition,
+ PolicyRuleObjectAccessPost,
+ PolicyRuleObjectAccess,
+ NfsExportPolicy,
+ NfsExportPolicyRule,
+ Policy,
+ PolicyRule,
+ )
+except ImportError:
+ HAS_PYPURECLIENT = False
+
+HAS_PYTZ = True
+try:
+ import pytz
+except ImportError:
+ HAS_PYTX = False
+
+import os
+import re
+import platform
+
+from ansible.module_utils.common.process import get_bin_path
+from ansible.module_utils.facts.utils import get_file_content
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ get_system,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.9"
+SNAPSHOT_POLICY_API_VERSION = "2.1"
+ACCESS_POLICY_API_VERSION = "2.2"
+NFS_POLICY_API_VERSION = "2.3"
+NFS_RENAME_API_VERSION = "2.4"
+
+
+def _convert_to_millisecs(hour):
+ if hour[-2:] == "AM" and hour[:2] == "12":
+ return 0
+ elif hour[-2:] == "AM":
+ return int(hour[:-2]) * 3600000
+ elif hour[-2:] == "PM" and hour[:2] == "12":
+ return 43200000
+ return (int(hour[:-2]) + 12) * 3600000
+
+
+def _findstr(text, match):
+ for line in text.splitlines():
+ if match in line:
+ found = line
+ return found
+
+
+def _get_local_tz(module, timezone="UTC"):
+ """
+ We will attempt to get the local timezone of the server running the module and use that.
+ If we can't get the timezone then we will set the default to be UTC
+
+ Linnux has been tested and other opersting systems should be OK.
+ Failures cause assumption of UTC
+
+ Windows is not supported and will assume UTC
+ """
+ if platform.system() == "Linux":
+ timedatectl = get_bin_path("timedatectl")
+ if timedatectl is not None:
+ rcode, stdout, stderr = module.run_command(timedatectl)
+ if rcode == 0 and stdout:
+ line = _findstr(stdout, "Time zone")
+ full_tz = line.split(":", 1)[1].rstrip()
+ timezone = full_tz.split()[0]
+ return timezone
+ else:
+ module.warn("Incorrect timedatectl output. Timezone will be set to UTC")
+ else:
+ if os.path.exists("/etc/timezone"):
+ timezone = get_file_content("/etc/timezone")
+ else:
+ module.warn("Could not find /etc/timezone. Assuming UTC")
+
+ elif platform.system() == "SunOS":
+ if os.path.exists("/etc/default/init"):
+ for line in get_file_content("/etc/default/init", "").splitlines():
+ if line.startswith("TZ="):
+ timezone = line.split("=", 1)[1]
+ return timezone
+ else:
+ module.warn("Could not find /etc/default/init. Assuming UTC")
+
+ elif re.match("^Darwin", platform.platform()):
+ systemsetup = get_bin_path("systemsetup")
+ if systemsetup is not None:
+ rcode, stdout, stderr = module.execute(systemsetup, "-gettimezone")
+ if rcode == 0 and stdout:
+ timezone = stdout.split(":", 1)[1].lstrip()
+ else:
+ module.warn("Could not run systemsetup. Assuming UTC")
+ else:
+ module.warn("Could not find systemsetup. Assuming UTC")
+
+ elif re.match("^(Free|Net|Open)BSD", platform.platform()):
+ if os.path.exists("/etc/timezone"):
+ timezone = get_file_content("/etc/timezone")
+ else:
+ module.warn("Could not find /etc/timezone. Assuming UTC")
+
+ elif platform.system() == "AIX":
+ aix_oslevel = int(platform.version() + platform.release())
+ if aix_oslevel >= 61:
+ if os.path.exists("/etc/environment"):
+ for line in get_file_content("/etc/environment", "").splitlines():
+ if line.startswith("TZ="):
+ timezone = line.split("=", 1)[1]
+ return timezone
+ else:
+ module.warn("Could not find /etc/environment. Assuming UTC")
+ else:
+ module.warn(
+ "Cannot determine timezone when AIX os level < 61. Assuming UTC"
+ )
+
+ else:
+ module.warn("Could not find /etc/timezone. Assuming UTC")
+
+ return timezone
+
+
+def delete_nfs_policy(module, blade):
+ """Delete NFS Export Policy, or Rule
+
+ If client is provided then delete the client rule if it exists.
+ """
+
+ changed = False
+ policy_delete = True
+ if module.params["client"]:
+ policy_delete = False
+ res = blade.get_nfs_export_policies_rules(
+ policy_names=[module.params["name"]],
+ filter="client='" + module.params["client"] + "'",
+ )
+ if res.status_code == 200:
+ if res.total_item_count == 0:
+ pass
+ elif res.total_item_count == 1:
+ rule = list(res.items)[0]
+ if module.params["client"] == rule.client:
+ changed = True
+ if not module.check_mode:
+ res = blade.delete_nfs_export_policies_rules(names=[rule.name])
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to delete rule for client {0} in policy {1}. "
+ "Error: {2}".format(
+ module.params["client"],
+ module.params["name"],
+ res.errors[0].message,
+ )
+ )
+ else:
+ rules = list(res.items)
+ for cli in range(0, len(rules)):
+ if rules[cli].client == "*":
+ changed = True
+ if not module.check_mode:
+ res = blade.delete_nfs_export_policies_rules(
+ names=[rules[cli].name]
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to delete rule for client {0} in policy {1}. "
+ "Error: {2}".format(
+ module.params["client"],
+ module.params["name"],
+ res.errors[0].message,
+ )
+ )
+ if policy_delete:
+ changed = True
+ if not module.check_mode:
+ res = blade.delete_nfs_export_policies(names=[module.params["name"]])
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to delete export policy {0}. Error: {1}".format(
+ module.params["name"], res.errors[0].message
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def rename_nfs_policy(module, blade):
+ """Rename NFS Export Policy"""
+
+ changed = True
+ if not module.check_mode:
+ res = blade.patch_nfs_export_policies(
+ names=[module.params["name"]],
+ policy=NfsExportPolicy(name=module.params["rename"]),
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to rename NFS export policy {0} to {1}. Error: {2}".format(
+ module.params["name"],
+ module.params["rename"],
+ res.errors[0].message,
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def update_nfs_policy(module, blade):
+ """Update NFS Export Policy Rule"""
+
+ changed = False
+ if module.params["client"]:
+ current_policy_rule = blade.get_nfs_export_policies_rules(
+ policy_names=[module.params["name"]],
+ filter="client='" + module.params["client"] + "'",
+ )
+ if (
+ current_policy_rule.status_code == 200
+ and current_policy_rule.total_item_count == 0
+ ):
+ rule = NfsExportPolicyRule(
+ client=module.params["client"],
+ permission=module.params["permission"],
+ access=module.params["access"],
+ anonuid=module.params["anonuid"],
+ anongid=module.params["anongid"],
+ fileid_32bit=module.params["fileid_32bit"],
+ atime=module.params["atime"],
+ secure=module.params["secure"],
+ security=module.params["security"],
+ )
+ changed = True
+ if not module.check_mode:
+ if module.params["before_rule"]:
+ before_name = (
+ module.params["name"] + "." + str(module.params["before_rule"])
+ )
+ res = blade.post_nfs_export_policies_rules(
+ policy_names=[module.params["name"]],
+ rule=rule,
+ before_rule_name=before_name,
+ )
+ else:
+ res = blade.post_nfs_export_policies_rules(
+ policy_names=[module.params["name"]],
+ rule=rule,
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to create rule for client {0} "
+ "in export policy {1}. Error: {2}".format(
+ module.params["client"],
+ module.params["name"],
+ res.errors[0].message,
+ )
+ )
+ else:
+ rules = list(current_policy_rule.items)
+ cli_count = None
+ done = False
+ if module.params["client"] == "*":
+ for cli in range(0, len(rules)):
+ if rules[cli].client == "*":
+ cli_count = cli
+ if not cli_count:
+ rule = NfsExportPolicyRule(
+ client=module.params["client"],
+ permission=module.params["permission"],
+ access=module.params["access"],
+ anonuid=module.params["anonuid"],
+ anongid=module.params["anongid"],
+ fileid_32bit=module.params["fileid_32bit"],
+ atime=module.params["atime"],
+ secure=module.params["secure"],
+ security=module.params["security"],
+ )
+ done = True
+ changed = True
+ if not module.check_mode:
+ if module.params["before_rule"]:
+ res = blade.post_nfs_export_policies_rules(
+ policy_names=[module.params["name"]],
+ rule=rule,
+ before_rule_name=(
+ module.params["name"]
+ + "."
+ + str(module.params["before_rule"]),
+ ),
+ )
+ else:
+ res = blade.post_nfs_export_policies_rules(
+ policy_names=[module.params["name"]],
+ rule=rule,
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to create rule for "
+ "client {0} in export policy {1}. Error: {2}".format(
+ module.params["client"],
+ module.params["name"],
+ res.errors[0].message,
+ )
+ )
+ if not done:
+ old_policy_rule = rules[0]
+ current_rule = {
+ "anongid": getattr(old_policy_rule, "anongid", None),
+ "anonuid": getattr(old_policy_rule, "anonuid", None),
+ "atime": old_policy_rule.atime,
+ "client": sorted(old_policy_rule.client),
+ "fileid_32bit": old_policy_rule.fileid_32bit,
+ "permission": sorted(old_policy_rule.permission),
+ "secure": old_policy_rule.secure,
+ "security": sorted(old_policy_rule.security),
+ }
+ if module.params["permission"]:
+ new_permission = sorted(module.params["permission"])
+ else:
+ new_permission = sorted(current_rule["permission"])
+ if module.params["client"]:
+ new_client = sorted(module.params["client"])
+ else:
+ new_client = sorted(current_rule["client"])
+ if module.params["security"]:
+ new_security = sorted(module.params["security"])
+ else:
+ new_security = sorted(current_rule["security"])
+ if module.params["anongid"]:
+ new_anongid = module.params["anongid"]
+ else:
+ new_anongid = current_rule["anongid"]
+ if module.params["anonuid"]:
+ new_anonuid = module.params["anonuid"]
+ else:
+ new_anonuid = current_rule["anonuid"]
+ if module.params["atime"] != current_rule["atime"]:
+ new_atime = module.params["atime"]
+ else:
+ new_atime = current_rule["atime"]
+ if module.params["secure"] != current_rule["secure"]:
+ new_secure = module.params["secure"]
+ else:
+ new_secure = current_rule["secure"]
+ if module.params["fileid_32bit"] != current_rule["fileid_32bit"]:
+ new_fileid_32bit = module.params["fileid_32bit"]
+ else:
+ new_fileid_32bit = current_rule["fileid_32bit"]
+ new_rule = {
+ "anongid": new_anongid,
+ "anonuid": new_anonuid,
+ "atime": new_atime,
+ "client": new_client,
+ "fileid_32bit": new_fileid_32bit,
+ "permission": new_permission,
+ "secure": new_secure,
+ "security": new_security,
+ }
+ if current_rule != new_rule:
+ changed = True
+ if not module.check_mode:
+ rule = NfsExportPolicyRule(
+ client=module.params["client"],
+ permission=module.params["permission"],
+ access=module.params["access"],
+ anonuid=module.params["anonuid"],
+ anongid=module.params["anongid"],
+ fileid_32bit=module.params["fileid_32bit"],
+ atime=module.params["atime"],
+ secure=module.params["secure"],
+ security=module.params["security"],
+ )
+ res = blade.patch_nfs_export_policies_rules(
+ names=[
+ module.params["name"] + "." + str(old_policy_rule.index)
+ ],
+ rule=rule,
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to update NFS export rule {0}. Error: {1}".format(
+ module.params["name"]
+ + "."
+ + str(old_policy_rule.index),
+ res.errors[0].message,
+ )
+ )
+ if (
+ module.params["before_rule"]
+ and module.params["before_rule"] != old_policy_rule.index
+ ):
+ changed = True
+ if not module.check_mode:
+ before_name = (
+ module.params["name"]
+ + "."
+ + str(module.params["before_rule"])
+ )
+ res = blade.patch_nfs_export_policies_rules(
+ names=[
+ module.params["name"] + "." + str(old_policy_rule.index)
+ ],
+ rule=NfsExportPolicyRule(),
+ before_rule_name=before_name,
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to move NFS export rule {0}. Error: {1}".format(
+ module.params["name"]
+ + "."
+ + str(old_policy_rule.index),
+ res.errors[0].message,
+ )
+ )
+ current_policy = list(
+ blade.get_nfs_export_policies(names=[module.params["name"]]).items
+ )[0]
+ if current_policy.enabled != module.params["enabled"]:
+ changed = True
+ if not module.check_mode:
+ res = blade.patch_nfs_export_policies(
+ policy=NfsExportPolicy(enabled=module.params["enabled"]),
+ names=[module.params["name"]],
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to change state of nfs export policy {0}.Error: {1}".format(
+ module.params["name"], res.errors[0].message
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def create_nfs_policy(module, blade):
+ """Create NFS Export Policy"""
+ changed = True
+ if not module.check_mode:
+ res = blade.post_nfs_export_policies(names=[module.params["name"]])
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to create nfs export policy {0}.Error: {1}".format(
+ module.params["name"], res.errors[0].message
+ )
+ )
+ if not module.params["enabled"]:
+ res = blade.patch_nfs_export_policies(
+ policy=NfsExportPolicy(enabled=False), names=[module.params["name"]]
+ )
+ if res.status_code != 200:
+ blade.delete_nfs_export_policies(names=[module.params["name"]])
+ module.fail_json(
+ msg="Failed to create nfs export policy {0}.Error: {1}".format(
+ module.params["name"], res.errors[0].message
+ )
+ )
+ if not module.params["client"]:
+ module.fail_json(msg="client is required to create a new rule")
+ else:
+ rule = NfsExportPolicyRule(
+ client=module.params["client"],
+ permission=module.params["permission"],
+ access=module.params["access"],
+ anonuid=module.params["anonuid"],
+ anongid=module.params["anongid"],
+ fileid_32bit=module.params["fileid_32bit"],
+ atime=module.params["atime"],
+ secure=module.params["secure"],
+ security=module.params["security"],
+ )
+ res = blade.post_nfs_export_policies_rules(
+ policy_names=[module.params["name"]],
+ rule=rule,
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to rule for policy {0}. Error: {1}".format(
+ module.params["name"],
+ res.errors[0].message,
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def delete_os_policy(module, blade):
+ """Delete Object Store Access Policy, Rule, or User
+
+ If rule is provided then delete the rule if it exists.
+ If user is provided then remove grant from user if granted.
+ If no user or rule provided delete the whole policy.
+ Cannot delete a policy with attached users, so delete all users
+ if the force_delete option is selected.
+ """
+
+ changed = False
+ policy_name = module.params["account"] + "/" + module.params["name"]
+ policy_delete = True
+ if module.params["rule"]:
+ policy_delete = False
+ res = blade.get_object_store_access_policies_rules(
+ policy_names=[policy_name], names=[module.params["rule"]]
+ )
+ if res.status_code == 200 and res.total_item_count != 0:
+ changed = True
+ if not module.check_mode:
+ res = blade.delete_object_store_access_policies_object_store_rules(
+ policy_names=[policy_name], names=[module.params["rule"]]
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to delete users from policy {0}. Error: {1} - {2}".format(
+ policy_name, res.errors[0].context, res.errors[0].message
+ )
+ )
+
+ if module.params["user"]:
+ member_name = module.params["account"] + "/" + module.params["user"]
+ policy_delete = False
+ res = blade.get_object_store_access_policies_object_store_users(
+ policy_names=[policy_name], member_names=[member_name]
+ )
+ if res.status_code == 200 and res.total_item_count != 0:
+ changed = True
+ if not module.check_mode:
+ member_name = module.params["account"] + "/" + module.params["user"]
+ res = blade.delete_object_store_access_policies_object_store_users(
+ policy_names=[policy_name], member_names=[member_name]
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to delete users from policy {0}. Error: {1} - {2}".format(
+ policy_name, res.errors[0].context, res.errors[0].message
+ )
+ )
+
+ if policy_delete:
+ if module.params["account"].lower() == "pure:policy":
+ module.fail_json(msg="System-Wide policies cannot be deleted.")
+ policy_users = list(
+ blade.get_object_store_access_policies_object_store_users(
+ policy_names=[policy_name]
+ ).items
+ )
+ if len(policy_users) == 0:
+ changed = True
+ if not module.check_mode:
+ res = blade.delete_object_store_access_policies(names=[policy_name])
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to delete policy {0}. Error: {1}".format(
+ policy_name, res.errors[0].message
+ )
+ )
+ else:
+ if module.params["force_delete"]:
+ changed = True
+ if not module.check_mode:
+ for user in range(0, len(policy_users)):
+ res = blade.delete_object_store_access_policies_object_store_users(
+ member_names=[policy_users[user].member.name],
+ policy_names=[policy_name],
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to delete user {0} from policy {1}, "
+ "Error: {2}".format(
+ policy_users[user].member,
+ policy_name,
+ res.errors[0].message,
+ )
+ )
+ res = blade.delete_object_store_access_policies(names=[policy_name])
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to delete policy {0}. Error: {1}".format(
+ policy_name, res.errors[0].message
+ )
+ )
+ else:
+ module.fail_json(
+ msg="Policy {0} cannot be deleted with connected users".format(
+ policy_name
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def create_os_policy(module, blade):
+ """Create Object Store Access Policy"""
+ changed = True
+ policy_name = module.params["account"] + "/" + module.params["name"]
+ if not module.check_mode:
+ res = blade.post_object_store_access_policies(names=[policy_name])
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to create access policy {0}.".format(policy_name)
+ )
+ if module.params["rule"]:
+ if not module.params["actions"] or not module.params["object_resources"]:
+ module.fail_json(
+ msg="Parameters `actions` and `object_resources` "
+ "are required to create a new rule"
+ )
+ conditions = PolicyRuleObjectAccessCondition(
+ source_ips=module.params["source_ips"],
+ s3_delimiters=module.params["s3_delimiters"],
+ s3_prefixes=module.params["s3_prefixes"],
+ )
+ rule = PolicyRuleObjectAccessPost(
+ actions=module.params["actions"],
+ resources=module.params["object_resources"],
+ conditions=conditions,
+ )
+ res = blade.post_object_store_access_policies_rules(
+ policy_names=policy_name,
+ names=[module.params["rule"]],
+ enforce_action_restrictions=module.params["ignore_enforcement"],
+ rule=rule,
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to create rule {0} to policy {1}. Error: {2}".format(
+ module.params["rule"], policy_name, res.errors[0].message
+ )
+ )
+ if module.params["user"]:
+ member_name = module.params["account"] + "/" + module.params["user"]
+ res = blade.post_object_store_access_policies_object_store_users(
+ member_names=[member_name], policy_names=[policy_name]
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to add users to policy {0}. Error: {1} - {2}".format(
+ policy_name, res.errors[0].context, res.errors[0].message
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def update_os_policy(module, blade):
+ """Update Object Store Access Policy"""
+ changed = False
+ policy_name = module.params["account"] + "/" + module.params["name"]
+ if module.params["rule"]:
+ current_policy_rule = blade.get_object_store_access_policies_rules(
+ policy_names=[policy_name], names=[module.params["rule"]]
+ )
+ if current_policy_rule.status_code != 200:
+ conditions = PolicyRuleObjectAccessCondition(
+ source_ips=module.params["source_ips"],
+ s3_delimiters=module.params["s3_delimiters"],
+ s3_prefixes=module.params["s3_prefixes"],
+ )
+ rule = PolicyRuleObjectAccessPost(
+ actions=module.params["actions"],
+ resources=module.params["object_resources"],
+ conditions=conditions,
+ )
+ res = blade.post_object_store_access_policies_rules(
+ policy_names=policy_name,
+ names=[module.params["rule"]],
+ enforce_action_restrictions=module.params["ignore_enforcement"],
+ rule=rule,
+ )
+ else:
+ old_policy_rule = list(current_policy_rule.items)[0]
+ current_rule = {
+ "actions": old_policy_rule.actions,
+ "resources": old_policy_rule.resources,
+ "ips": getattr(old_policy_rule.conditions, "source_ips", None),
+ "prefixes": getattr(old_policy_rule.conditions, "s3_prefixes", None),
+ "delimiters": getattr(
+ old_policy_rule.conditions, "s3_delimiters", None
+ ),
+ }
+ if module.params["actions"]:
+ new_actions = sorted(module.params["actions"])
+ else:
+ new_actions = sorted(current_rule["actions"])
+ if module.params["object_resources"]:
+ new_resources = sorted(module.params["object_resources"])
+ else:
+ new_resources = sorted(current_rule["resources"])
+ if module.params["s3_prefixes"]:
+ new_prefixes = sorted(module.params["s3_prefixes"])
+ elif current_rule["prefixes"]:
+ new_prefixes = sorted(current_rule["prefixes"])
+ else:
+ new_prefixes = None
+ if module.params["s3_delimiters"]:
+ new_delimiters = sorted(module.params["s3_delimiters"])
+ elif current_rule["delimiters"]:
+ new_delimiters = sorted(current_rule["delimiters"])
+ else:
+ new_delimiters = None
+ if module.params["source_ips"]:
+ new_ips = sorted(module.params["source_ips"])
+ elif current_rule["ips"]:
+ new_ips = sorted(current_rule["source_ips"])
+ else:
+ new_ips = None
+ new_rule = {
+ "actions": new_actions,
+ "resources": new_resources,
+ "ips": new_ips,
+ "prefixes": new_prefixes,
+ "delimiters": new_delimiters,
+ }
+ if current_rule != new_rule:
+ changed = True
+ if not module.check_mode:
+ conditions = PolicyRuleObjectAccessCondition(
+ source_ips=new_rule["ips"],
+ s3_prefixes=new_rule["prefixes"],
+ s3_delimiters=new_rule["delimiters"],
+ )
+ rule = PolicyRuleObjectAccess(
+ actions=new_rule["actions"],
+ resources=new_rule["resources"],
+ conditions=conditions,
+ )
+ res = blade.patch_object_store_access_policies_rules(
+ policy_names=[policy_name],
+ names=[module.params["rule"]],
+ rule=rule,
+ enforce_action_restrictions=module.params["ignore_enforcement"],
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to update rule {0} in policy {1}. Error: {2}".format(
+ module.params["rule"], policy_name, res.errors[0].message
+ )
+ )
+ if module.params["user"]:
+ member_name = module.params["account"] + "/" + module.params["user"]
+ res = blade.get_object_store_access_policies_object_store_users(
+ policy_names=[policy_name], member_names=[member_name]
+ )
+ if res.status_code != 200 or (
+ res.status_code == 200 and res.total_item_count == 0
+ ):
+ changed = True
+ if not module.check_mode:
+ res = blade.post_object_store_access_policies_object_store_users(
+ member_names=[member_name], policy_names=[policy_name]
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to add user {0} to policy {1}. Error: {2}".format(
+ member_name, policy_name, res.errors[0].message
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def copy_os_policy_rule(module, blade):
+ """Copy an existing policy rule to a new policy"""
+ changed = True
+ policy_name = module.params["account"] + "/" + module.params["name"]
+ if not module.params["target_rule"]:
+ module.params["target_rule"] = module.params["rule"]
+ if (
+ blade.get_object_store_access_policies_rules(
+ policy_names=[module.params["target"]], names=[module.params["target_rule"]]
+ ).status_code
+ == 200
+ ):
+ module.fail_json(
+ msg="Target rule {0} already exists in policy {1}".format(
+ module.params["target_rule"], policy_name
+ )
+ )
+ current_rule = list(
+ blade.get_object_store_access_policies_rules(
+ policy_names=[policy_name], names=[module.params["rule"]]
+ ).items
+ )[0]
+ if not module.check_mode:
+ conditions = PolicyRuleObjectAccessCondition(
+ source_ips=current_rule.conditions.source_ips,
+ s3_delimiters=current_rule.conditions.s3_delimiters,
+ s3_prefixes=current_rule.conditions.s3_prefixes,
+ )
+ rule = PolicyRuleObjectAccessPost(
+ actions=current_rule.actions,
+ resources=current_rule.resources,
+ conditions=conditions,
+ )
+ res = blade.post_object_store_access_policies_rules(
+ policy_names=module.params["target"],
+ names=[module.params["target_rule"]],
+ enforce_action_restrictions=module.params["ignore_enforcement"],
+ rule=rule,
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to copy rule {0} from policy {1} to policy {2}. "
+ "Error: {3}".format(
+ module.params["rule"],
+ policy_name,
+ module.params["target"],
+ res.errors[0].message,
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def delete_policy(module, blade):
+ """Delete policy"""
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.policies.delete_policies(names=[module.params["name"]])
+ except Exception:
+ module.fail_json(
+ msg="Failed to delete policy {0}.".format(module.params["name"])
+ )
+ module.exit_json(changed=changed)
+
+
+def delete_snap_policy(module, blade):
+ """Delete REST 2 snapshot policy
+
+ If any rule parameters are provided then delete any rules that match
+ all of the parameters provided.
+ If no rule parameters are provided delete the entire policy
+ """
+
+ changed = False
+ rule_delete = False
+ if (
+ module.params["at"]
+ or module.params["every"]
+ or module.params["timezone"]
+ or module.params["keep_for"]
+ ):
+ rule_delete = True
+ if rule_delete:
+ current_rules = list(blade.get_policies(names=[module.params["name"]]).items)[
+ 0
+ ].rules
+ for rule in range(0, len(current_rules)):
+ current_rule = {
+ "at": current_rules[rule].at,
+ "every": current_rules[rule].every,
+ "keep_for": current_rules[rule].keep_for,
+ "time_zone": current_rules[rule].time_zone,
+ }
+ if not module.params["at"]:
+ delete_at = current_rules[rule].at
+ else:
+ delete_at = _convert_to_millisecs(module.params["at"])
+ if module.params["keep_for"]:
+ delete_keep_for = module.params["keep_for"]
+ else:
+ delete_keep_for = int(current_rules[rule].keep_for / 1000)
+ if module.params["every"]:
+ delete_every = module.params["every"]
+ else:
+ delete_every = int(current_rules[rule].every / 1000)
+ if not module.params["timezone"]:
+ delete_tz = current_rules[rule].time_zone
+ else:
+ delete_tz = module.params["timezone"]
+ delete_rule = {
+ "at": delete_at,
+ "every": delete_every * 1000,
+ "keep_for": delete_keep_for * 1000,
+ "time_zone": delete_tz,
+ }
+ if current_rule == delete_rule:
+ changed = True
+ attr = PolicyPatch(remove_rules=[delete_rule])
+ if not module.check_mode:
+ res = blade.patch_policies(
+ destroy_snapshots=module.params["destroy_snapshots"],
+ names=[module.params["name"]],
+ policy=attr,
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to delete policy rule {0}. Error: {1}".format(
+ module.params["name"], res.errors[0].message
+ )
+ )
+ else:
+ changed = True
+ if not module.check_mode:
+ res = blade.delete_policies(names=[module.params["name"]])
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to delete policy {0}. Error: {1}".format(
+ module.params["name"], res.errors[0].message
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def create_snap_policy(module, blade):
+ """Create REST 2 snapshot policy"""
+ changed = True
+ if (
+ module.params["keep_for"]
+ and not module.params["every"]
+ or module.params["every"]
+ and not module.params["keep_for"]
+ ):
+ module.fail_json(msg="`keep_for` and `every` are required.")
+ if module.params["timezone"] and not module.params["at"]:
+ module.fail_json(msg="`timezone` requires `at` to be provided.")
+ if module.params["at"] and not module.params["every"]:
+ module.fail_json(msg="`at` requires `every` to be provided.")
+
+ if not module.check_mode:
+ if module.params["at"] and module.params["every"]:
+ if not module.params["every"] % 86400 == 0:
+ module.fail_json(
+ msg="At time can only be set if every value is a multiple of 86400"
+ )
+ if not module.params["timezone"]:
+ module.params["timezone"] = _get_local_tz(module)
+ if module.params["timezone"] not in pytz.all_timezones_set:
+ module.fail_json(
+ msg="Timezone {0} is not valid".format(
+ module.params["timezone"]
+ )
+ )
+ if not module.params["keep_for"]:
+ module.params["keep_for"] = 0
+ if not module.params["every"]:
+ module.params["every"] = 0
+ if module.params["keep_for"] < module.params["every"]:
+ module.fail_json(
+ msg="Retention period cannot be less than snapshot interval."
+ )
+ if module.params["at"] and not module.params["timezone"]:
+ module.params["timezone"] = _get_local_tz(module)
+ if module.params["timezone"] not in set(pytz.all_timezones_set):
+ module.fail_json(
+ msg="Timezone {0} is not valid".format(module.params["timezone"])
+ )
+
+ if module.params["keep_for"]:
+ if not 300 <= module.params["keep_for"] <= 34560000:
+ module.fail_json(
+ msg="keep_for parameter is out of range (300 to 34560000)"
+ )
+ if not 300 <= module.params["every"] <= 34560000:
+ module.fail_json(
+ msg="every parameter is out of range (300 to 34560000)"
+ )
+ if module.params["at"]:
+ attr = Policy(
+ enabled=module.params["enabled"],
+ rules=[
+ PolicyRule(
+ keep_for=module.params["keep_for"] * 1000,
+ every=module.params["every"] * 1000,
+ at=_convert_to_millisecs(module.params["at"]),
+ time_zone=module.params["timezone"],
+ )
+ ],
+ )
+ else:
+ attr = Policy(
+ enabled=module.params["enabled"],
+ rules=[
+ PolicyRule(
+ keep_for=module.params["keep_for"] * 1000,
+ every=module.params["every"] * 1000,
+ )
+ ],
+ )
+ else:
+ attr = Policy(enabled=module.params["enabled"])
+ res = blade.post_policies(names=[module.params["name"]], policy=attr)
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to create snapshot policy {0}. Error: {1}".format(
+ module.params["name"], res.errors[0].message
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def create_policy(module, blade):
+ """Create snapshot policy"""
+ changed = True
+ if not module.check_mode:
+ try:
+ if module.params["at"] and module.params["every"]:
+ if not module.params["every"] % 86400 == 0:
+ module.fail_json(
+ msg="At time can only be set if every value is a multiple of 86400"
+ )
+ if not module.params["timezone"]:
+ module.params["timezone"] = _get_local_tz(module)
+ if module.params["timezone"] not in pytz.all_timezones_set:
+ module.fail_json(
+ msg="Timezone {0} is not valid".format(
+ module.params["timezone"]
+ )
+ )
+ if not module.params["keep_for"]:
+ module.params["keep_for"] = 0
+ if not module.params["every"]:
+ module.params["every"] = 0
+ if module.params["keep_for"] < module.params["every"]:
+ module.fail_json(
+ msg="Retention period cannot be less than snapshot interval."
+ )
+ if module.params["at"] and not module.params["timezone"]:
+ module.params["timezone"] = _get_local_tz(module)
+ if module.params["timezone"] not in set(pytz.all_timezones_set):
+ module.fail_json(
+ msg="Timezone {0} is not valid".format(
+ module.params["timezone"]
+ )
+ )
+
+ if module.params["keep_for"]:
+ if not 300 <= module.params["keep_for"] <= 34560000:
+ module.fail_json(
+ msg="keep_for parameter is out of range (300 to 34560000)"
+ )
+ if not 300 <= module.params["every"] <= 34560000:
+ module.fail_json(
+ msg="every parameter is out of range (300 to 34560000)"
+ )
+ if module.params["at"]:
+ attr = Policy(
+ enabled=module.params["enabled"],
+ rules=[
+ PolicyRule(
+ keep_for=module.params["keep_for"] * 1000,
+ every=module.params["every"] * 1000,
+ at=_convert_to_millisecs(module.params["at"]),
+ time_zone=module.params["timezone"],
+ )
+ ],
+ )
+ else:
+ attr = Policy(
+ enabled=module.params["enabled"],
+ rules=[
+ PolicyRule(
+ keep_for=module.params["keep_for"] * 1000,
+ every=module.params["every"] * 1000,
+ )
+ ],
+ )
+ else:
+ attr = Policy(enabled=module.params["enabled"])
+ blade.policies.create_policies(names=[module.params["name"]], policy=attr)
+ except Exception:
+ module.fail_json(
+ msg="Failed to create policy {0}.".format(module.params["name"])
+ )
+ if module.params["filesystem"]:
+ try:
+ blade.file_systems.list_file_systems(names=module.params["filesystem"])
+ blade.policies.create_policy_filesystems(
+ policy_names=[module.params["name"]],
+ member_names=module.params["filesystem"],
+ )
+ except Exception:
+ blade.policies.delete_policies(names=[module.params["name"]])
+ module.fail_json(
+ msg="Failed to connect filesystems to policy {0}, "
+ "or one of {1} doesn't exist.".format(
+ module.params["name"], module.params["filesystem"]
+ )
+ )
+ if module.params["replica_link"]:
+ for link in module.params["replica_link"]:
+ remote_array = (
+ blade.file_system_replica_links.list_file_system_replica_links(
+ local_file_system_names=[link]
+ )
+ )
+ try:
+ blade.policies.create_policy_file_system_replica_links(
+ policy_names=[module.params["name"]],
+ member_names=[link],
+ remote_names=[remote_array.items[0].remote.name],
+ )
+ except Exception:
+ blade.policies.delete_policies(names=[module.params["name"]])
+ module.fail_json(
+ msg="Failed to connect filesystem replicsa link {0} to policy {1}. "
+ "Replica Link {0} does not exist.".format(
+ link, module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def update_snap_policy(module, blade):
+ """Update REST 2 snapshot policy
+
+ Add new rules to the policy using this function.
+ Should it be necessary to modify an existing rule these are the rules:
+
+ Due to the 'best fit' nature of Purity we only add new rulkes in this function.
+ If you trying to update an existing rule, then this should be done by deleting
+ the current rule and then adding the new rule.
+
+ Purity may recover some snapshots as long as the add happens before the eradication delay
+ (typically 24h) causes the snapshots to be eradicated.
+ """
+
+ changed = False
+ if (
+ module.params["keep_for"]
+ and not module.params["every"]
+ or module.params["every"]
+ and not module.params["keep_for"]
+ ):
+ module.fail_json(msg="`keep_for` and `every` are required.")
+ if module.params["timezone"] and not module.params["at"]:
+ module.fail_json(msg="`timezone` requires `at` to be provided.")
+ if module.params["at"] and not module.params["every"]:
+ module.fail_json(msg="`at` requires `every` to be provided.")
+ current_rules = list(blade.get_policies(names=[module.params["name"]]).items)[
+ 0
+ ].rules
+ create_new = True
+ for rule in range(0, len(current_rules)):
+ current_rule = {
+ "at": current_rules[rule].at,
+ "every": current_rules[rule].every,
+ "keep_for": current_rules[rule].keep_for,
+ "time_zone": current_rules[rule].time_zone,
+ }
+ if not module.params["at"]:
+ new_at = current_rules[rule].at
+ else:
+ new_at = _convert_to_millisecs(module.params["at"])
+ if module.params["keep_for"]:
+ new_keep_for = module.params["keep_for"]
+ else:
+ new_keep_for = int(current_rules[rule].keep_for / 1000)
+ if module.params["every"]:
+ new_every = module.params["every"]
+ else:
+ new_every = int(current_rules[rule].every / 1000)
+ if not module.params["timezone"]:
+ new_tz = current_rules[rule].time_zone
+ else:
+ new_tz = module.params["timezone"]
+ new_rule = {
+ "at": new_at,
+ "every": new_every * 1000,
+ "keep_for": new_keep_for * 1000,
+ "time_zone": new_tz,
+ }
+ if current_rule == new_rule:
+ create_new = False
+
+ if create_new:
+ changed = True
+ if not module.check_mode:
+ if module.params["at"] and module.params["every"]:
+ if not module.params["every"] % 86400 == 0:
+ module.fail_json(
+ msg="At time can only be set if every value is a multiple of 86400"
+ )
+ if not module.params["timezone"]:
+ module.params["timezone"] = _get_local_tz(module)
+ if module.params["timezone"] not in pytz.all_timezones_set:
+ module.fail_json(
+ msg="Timezone {0} is not valid".format(
+ module.params["timezone"]
+ )
+ )
+ if not module.params["keep_for"]:
+ module.params["keep_for"] = 0
+ if not module.params["every"]:
+ module.params["every"] = 0
+ if module.params["keep_for"] < module.params["every"]:
+ module.fail_json(
+ msg="Retention period cannot be less than snapshot interval."
+ )
+ if module.params["at"] and not module.params["timezone"]:
+ module.params["timezone"] = _get_local_tz(module)
+ if module.params["timezone"] not in set(pytz.all_timezones_set):
+ module.fail_json(
+ msg="Timezone {0} is not valid".format(
+ module.params["timezone"]
+ )
+ )
+
+ if module.params["keep_for"]:
+ if not 300 <= module.params["keep_for"] <= 34560000:
+ module.fail_json(
+ msg="keep_for parameter is out of range (300 to 34560000)"
+ )
+ if not 300 <= module.params["every"] <= 34560000:
+ module.fail_json(
+ msg="every parameter is out of range (300 to 34560000)"
+ )
+ if module.params["at"]:
+ attr = PolicyPatch(
+ enabled=module.params["enabled"],
+ add_rules=[
+ PolicyRule(
+ keep_for=module.params["keep_for"] * 1000,
+ every=module.params["every"] * 1000,
+ at=_convert_to_millisecs(module.params["at"]),
+ time_zone=module.params["timezone"],
+ )
+ ],
+ )
+ else:
+ attr = PolicyPatch(
+ enabled=module.params["enabled"],
+ add_rules=[
+ PolicyRule(
+ keep_for=module.params["keep_for"] * 1000,
+ every=module.params["every"] * 1000,
+ )
+ ],
+ )
+ else:
+ attr = PolicyPatch(enabled=module.params["enabled"])
+ res = blade.patch_policies(
+ names=[module.params["name"]],
+ policy=attr,
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to update snapshot policy {0}. Error: {1}".format(
+ module.params["name"], res.errors[0].message
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def update_policy(module, blade, policy):
+ """Update snapshot policy"""
+ changed = False
+ if not policy.rules:
+ current_policy = {
+ "time_zone": None,
+ "every": 0,
+ "keep_for": 0,
+ "at": 0,
+ "enabled": policy.enabled,
+ }
+ else:
+ if policy.rules[0].keep_for != 0:
+ policy.rules[0].keep_for = int(policy.rules[0].keep_for / 1000)
+ if policy.rules[0].every != 0:
+ policy.rules[0].every = int(policy.rules[0].every / 1000)
+
+ current_policy = {
+ "time_zone": policy.rules[0].time_zone,
+ "every": policy.rules[0].every,
+ "keep_for": policy.rules[0].keep_for,
+ "at": policy.rules[0].at,
+ "enabled": policy.enabled,
+ }
+ if not module.params["every"]:
+ every = 0
+ else:
+ every = module.params["every"]
+ if not module.params["keep_for"]:
+ keep_for = 0
+ else:
+ keep_for = module.params["keep_for"]
+ if module.params["at"]:
+ at_time = _convert_to_millisecs(module.params["at"])
+ else:
+ at_time = None
+ if not module.params["timezone"]:
+ timezone = _get_local_tz(module)
+ else:
+ timezone = module.params["timezone"]
+ if at_time:
+ new_policy = {
+ "time_zone": timezone,
+ "every": every,
+ "keep_for": keep_for,
+ "at": at_time,
+ "enabled": module.params["enabled"],
+ }
+ else:
+ new_policy = {
+ "time_zone": None,
+ "every": every,
+ "keep_for": keep_for,
+ "at": None,
+ "enabled": module.params["enabled"],
+ }
+ if (
+ new_policy["time_zone"]
+ and new_policy["time_zone"] not in pytz.all_timezones_set
+ ):
+ module.fail_json(
+ msg="Timezone {0} is not valid".format(module.params["timezone"])
+ )
+
+ if current_policy != new_policy:
+ if not module.params["at"]:
+ module.params["at"] = current_policy["at"]
+ if not module.params["keep_for"]:
+ module.params["keep_for"] = current_policy["keep_for"]
+ if not module.params["every"]:
+ module.params["every"] = current_policy["every"]
+ if module.params["at"] and module.params["every"]:
+ if not module.params["every"] % 86400 == 0:
+ module.fail_json(
+ msg="At time can only be set if every value is a multiple of 86400"
+ )
+ if module.params["keep_for"] < module.params["every"]:
+ module.fail_json(
+ msg="Retention period cannot be less than snapshot interval."
+ )
+ if module.params["at"] and not module.params["timezone"]:
+ module.params["timezone"] = _get_local_tz(module)
+ if module.params["timezone"] not in set(pytz.all_timezones_set):
+ module.fail_json(
+ msg="Timezone {0} is not valid".format(module.params["timezone"])
+ )
+
+ changed = True
+ if not module.check_mode:
+ try:
+ attr = PolicyPatch()
+ attr.enabled = module.params["enabled"]
+ if at_time:
+ attr.add_rules = [
+ PolicyRule(
+ keep_for=module.params["keep_for"] * 1000,
+ every=module.params["every"] * 1000,
+ at=at_time,
+ time_zone=timezone,
+ )
+ ]
+ else:
+ attr.add_rules = [
+ PolicyRule(
+ keep_for=module.params["keep_for"] * 1000,
+ every=module.params["every"] * 1000,
+ )
+ ]
+ attr.remove_rules = [
+ PolicyRule(
+ keep_for=current_policy["keep_for"] * 1000,
+ every=current_policy["every"] * 1000,
+ at=current_policy["at"],
+ time_zone=current_policy["time_zone"],
+ )
+ ]
+ blade.policies.update_policies(
+ names=[module.params["name"]], policy_patch=attr
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to update policy {0}.".format(module.params["name"])
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(
+ type="str", default="present", choices=["absent", "present", "copy"]
+ ),
+ policy_type=dict(
+ type="str", default="snapshot", choices=["snapshot", "access", "nfs"]
+ ),
+ enabled=dict(type="bool", default=True),
+ timezone=dict(type="str"),
+ name=dict(type="str"),
+ at=dict(type="str"),
+ every=dict(type="int"),
+ keep_for=dict(type="int"),
+ filesystem=dict(type="list", elements="str"),
+ replica_link=dict(type="list", elements="str"),
+ account=dict(type="str"),
+ target=dict(type="str"),
+ target_rule=dict(type="str"),
+ rename=dict(type="str"),
+ rule=dict(type="str"),
+ user=dict(type="str"),
+ effect=dict(type="str", default="allow", choices=["allow"]),
+ actions=dict(
+ type="list",
+ elements="str",
+ choices=[
+ "s3:*",
+ "s3:AbortMultipartUpload",
+ "s3:CreateBucket",
+ "s3:DeleteBucket",
+ "s3:DeleteObject",
+ "s3:DeleteObjectVersion",
+ "s3:ExtendSafemodeRetentionPeriod",
+ "s3:GetBucketAcl",
+ "s3:GetBucketLocation",
+ "s3:GetBucketVersioning",
+ "s3:GetLifecycleConfiguration",
+ "s3:GetObject",
+ "s3:GetObjectAcl",
+ "s3:GetObjectVersion",
+ "s3:ListAllMyBuckets",
+ "s3:ListBucket",
+ "s3:ListBucketMultipartUploads",
+ "s3:ListBucketVersions",
+ "s3:ListMultipartUploadParts",
+ "s3:PutBucketVersioning",
+ "s3:PutLifecycleConfiguration",
+ "s3:PutObject",
+ ],
+ ),
+ object_resources=dict(type="list", elements="str"),
+ source_ips=dict(type="list", elements="str"),
+ s3_prefixes=dict(type="list", elements="str"),
+ s3_delimiters=dict(type="list", elements="str"),
+ ignore_enforcement=dict(type="bool", default=True),
+ force_delete=dict(type="bool", default=False),
+ access=dict(
+ type="str",
+ choices=["root-squash", "all-squash", "no-squash"],
+ default="root-squash",
+ ),
+ anonuid=dict(type="str"),
+ anongid=dict(type="str"),
+ atime=dict(type="bool", default=True),
+ client=dict(type="str", default="*"),
+ fileid_32bit=dict(type="bool", default=False),
+ permission=dict(type="str", choices=["rw", "ro"], default="ro"),
+ secure=dict(type="bool", default=False),
+ destroy_snapshots=dict(type="bool", default=False),
+ security=dict(
+ type="list",
+ elements="str",
+ choices=["sys", "krb5", "krb5i", "krb5p"],
+ default=["sys"],
+ ),
+ before_rule=dict(type="int"),
+ )
+ )
+
+ required_together = [["keep_for", "every"]]
+ required_if = [
+ ["policy_type", "access", ["account", "name"]],
+ ["policy_type", "nfs", ["name"]],
+ ]
+
+ module = AnsibleModule(
+ argument_spec,
+ required_together=required_together,
+ required_if=required_if,
+ supports_check_mode=True,
+ )
+
+ if not HAS_PURITYFB:
+ module.fail_json(msg="purity-fb sdk is required for this module")
+ if not HAS_PYTZ:
+ module.fail_json(msg="pytz is required for this module")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ versions = blade.api_version.list_versions().versions
+ if module.params["policy_type"] == "access":
+ if ACCESS_POLICY_API_VERSION not in versions:
+ module.fail_json(
+ msg=(
+ "Minimum FlashBlade REST version required: {0}".format(
+ ACCESS_POLICY_API_VERSION
+ )
+ )
+ )
+ if not HAS_PYPURECLIENT:
+ module.fail_json(msg="py-pure-client sdk is required for this module")
+ blade = get_system(module)
+ try:
+ policy = list(
+ blade.get_object_store_access_policies(
+ names=[module.params["account"] + "/" + module.params["name"]]
+ ).items
+ )[0]
+ except AttributeError:
+ policy = None
+ if module.params["user"]:
+ member_name = module.params["account"] + "/" + module.params["user"]
+ res = blade.get_object_store_users(filter='name="' + member_name + "'")
+ if res.status_code != 200:
+ module.fail_json(
+ msg="User {0} does not exist in account {1}".format(
+ module.params["user"], module.params["account"]
+ )
+ )
+ if policy and state == "present":
+ update_os_policy(module, blade)
+ elif state == "present" and not policy:
+ create_os_policy(module, blade)
+ elif state == "absent" and policy:
+ delete_os_policy(module, blade)
+ elif state == "copy" and module.params["target"] and module.params["rule"]:
+ if "/" not in module.params["target"]:
+ module.fail_json(
+ msg='Incorrect format for target policy. Must be "<account>/<name>"'
+ )
+ if (
+ blade.get_object_store_access_policies(
+ names=[module.params["target"]]
+ ).status_code
+ != 200
+ ):
+ module.fail_json(
+ msg="Target policy {0} does not exist".format(
+ module.params["target"]
+ )
+ )
+ copy_os_policy_rule(module, blade)
+ elif module.params["policy_type"] == "nfs":
+ if NFS_POLICY_API_VERSION not in versions:
+ module.fail_json(
+ msg=(
+ "Minimum FlashBlade REST version required: {0}".format(
+ NFS_POLICY_API_VERSION
+ )
+ )
+ )
+ if not HAS_PYPURECLIENT:
+ module.fail_json(msg="py-pure-client sdk is required for this module")
+ blade = get_system(module)
+ try:
+ policy = list(
+ blade.get_nfs_export_policies(names=[module.params["name"]]).items
+ )[0]
+ except AttributeError:
+ policy = None
+ if module.params["rename"]:
+ try:
+ new_policy = list(
+ blade.get_nfs_export_policies(names=[module.params["rename"]]).items
+ )[0]
+ except AttributeError:
+ new_policy = None
+ if policy and state == "present" and not module.params["rename"]:
+ if module.params["before_rule"]:
+ res = blade.get_nfs_export_policies_rules(
+ policy_names=[module.params["name"]],
+ names=[
+ module.params["name"] + "." + str(module.params["before_rule"])
+ ],
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Rule index {0} does not exist.".format(
+ module.params["before_rule"]
+ )
+ )
+ update_nfs_policy(module, blade)
+ elif (
+ state == "present" and module.params["rename"] and policy and not new_policy
+ ):
+ rename_nfs_policy(module, blade)
+ elif state == "present" and not policy and not module.params["rename"]:
+ create_nfs_policy(module, blade)
+ elif state == "absent" and policy:
+ delete_nfs_policy(module, blade)
+ elif SNAPSHOT_POLICY_API_VERSION in versions:
+ if not HAS_PYPURECLIENT:
+ module.fail_json(msg="py-pure-client sdk is required for this module")
+ blade = get_system(module)
+ try:
+ policy = list(blade.get_policies(names=[module.params["name"]]).items)[0]
+ except AttributeError:
+ policy = None
+ if not policy and state == "present":
+ create_snap_policy(module, blade)
+ elif policy and state == "present":
+ update_snap_policy(module, blade)
+ elif policy and state == "absent":
+ delete_snap_policy(module, blade)
+ else:
+ if MIN_REQUIRED_API_VERSION not in versions:
+ module.fail_json(
+ msg="Minimum FlashBlade REST version required: {0}".format(
+ MIN_REQUIRED_API_VERSION
+ )
+ )
+ try:
+ policy = blade.policies.list_policies(names=[module.params["name"]]).items[
+ 0
+ ]
+ except Exception:
+ policy = None
+
+ if policy and state == "present":
+ update_policy(module, blade, policy)
+ elif state == "present" and not policy:
+ create_policy(module, blade)
+ elif state == "absent" and policy:
+ delete_policy(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_proxy.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_proxy.py
new file mode 100644
index 000000000..ed9e39ac8
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_proxy.py
@@ -0,0 +1,155 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2019, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_proxy
+version_added: '1.0.0'
+author:
+ - Pure Storage ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+short_description: Configure FlashBlade phonehome HTTPs proxy settings
+description:
+- Set or erase configuration for the phonehome proxy settings.
+options:
+ state:
+ description:
+ - Set or delete proxy configuration
+ default: present
+ type: str
+ choices: [ absent, present ]
+ host:
+ description:
+ - The proxy host name.
+ type: str
+ port:
+ description:
+ - The proxy TCP/IP port number.
+ type: int
+ secure:
+ description:
+ - Use http or https as the proxy protocol.
+ - True uses https, false uses http.
+ default: true
+ type: bool
+ version_added: '1.11.0'
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Delete exisitng proxy settings
+ purestorage.flashblade.purefb_proxy:
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+
+- name: Set proxy settings
+ purestorage.flashblade.purefb_proxy:
+ host: purestorage.com
+ port: 8080
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import Support
+except ImportError:
+ HAS_PURITY_FB = False
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+def delete_proxy(module, blade):
+ """Delete proxy settings"""
+ changed = False
+ current_proxy = blade.support.list_support().items[0].proxy
+ if current_proxy != "":
+ changed = True
+ if not module.check_mode:
+ try:
+ proxy_settings = Support(proxy="")
+ blade.support.update_support(support=proxy_settings)
+ except Exception:
+ module.fail_json(msg="Delete proxy settigs failed")
+ module.exit_json(changed=changed)
+
+
+def create_proxy(module, blade):
+ """Set proxy settings"""
+ changed = False
+ current_proxy = blade.support.list_support().items[0].proxy
+ if module.params["secure"]:
+ protocol = "https://"
+ else:
+ protocol = "http://"
+ if current_proxy is not None:
+ changed = True
+ if not module.check_mode:
+ new_proxy = (
+ protocol + module.params["host"] + ":" + str(module.params["port"])
+ )
+ if new_proxy != current_proxy:
+ try:
+ proxy_settings = Support(proxy=new_proxy)
+ blade.support.update_support(support=proxy_settings)
+ except Exception:
+ module.fail_json(msg="Set phone home proxy failed.")
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ secure=dict(type="bool", default=True),
+ host=dict(type="str"),
+ port=dict(type="int"),
+ )
+ )
+
+ required_together = [["host", "port"]]
+
+ module = AnsibleModule(
+ argument_spec, required_together=required_together, supports_check_mode=True
+ )
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb SDK is required for this module")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+
+ if state == "absent":
+ delete_proxy(module, blade)
+ elif state == "present":
+ create_proxy(module, blade)
+ else:
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_ra.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_ra.py
new file mode 100644
index 000000000..c84ba1e41
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_ra.py
@@ -0,0 +1,126 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2018, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_ra
+version_added: '1.0.0'
+short_description: Enable or Disable Pure Storage FlashBlade Remote Assist
+description:
+- Enablke or Disable Remote Assist for a Pure Storage FlashBlade.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Define state of remote assist
+ - When set to I(enable) the RA port can be exposed using the
+ I(debug) module.
+ type: str
+ default: present
+ choices: [ present, absent ]
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Enable Remote Assist port
+ purestorage.flashblade.purefb_ra:
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Disable Remote Assist port
+ purestorage.flashblade.purefb_ra:
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import Support
+except ImportError:
+ HAS_PURITY_FB = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.6"
+
+
+def enable_ra(module, blade):
+ """Enable Remote Assist"""
+ changed = True
+ if not module.check_mode:
+ ra_settings = Support(remote_assist_active=True)
+ try:
+ blade.support.update_support(support=ra_settings)
+ except Exception:
+ module.fail_json(msg="Enabling Remote Assist failed")
+ module.exit_json(changed=changed)
+
+
+def disable_ra(module, blade):
+ """Disable Remote Assist"""
+ changed = True
+ if not module.check_mode:
+ ra_settings = Support(remote_assist_active=False)
+ try:
+ blade.support.update_support(support=ra_settings)
+ except Exception:
+ module.fail_json(msg="Disabling Remote Assist failed")
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ blade = get_blade(module)
+ api_version = blade.api_version.list_versions().versions
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(msg="Purity//FB must be upgraded to support this module.")
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb SDK is required for this module")
+
+ if (
+ module.params["state"] == "present"
+ and not blade.support.list_support().items[0].remote_assist_active
+ ):
+ enable_ra(module, blade)
+ elif (
+ module.params["state"] == "absent"
+ and blade.support.list_support().items[0].remote_assist_active
+ ):
+ disable_ra(module, blade)
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_remote_cred.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_remote_cred.py
new file mode 100644
index 000000000..53c985d35
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_remote_cred.py
@@ -0,0 +1,243 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2020, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_remote_cred
+version_added: '1.0.0'
+short_description: Create, modify and delete FlashBlade object store remote credentials
+description:
+- Create, modify and delete object store remote credentials
+- You must have a correctly configured remote array or target
+- This module is B(not) idempotent when updating existing remote credentials
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Define state of remote credential
+ default: present
+ choices: [ absent, present ]
+ type: str
+ name:
+ description:
+ - The name of the credential
+ required: true
+ type: str
+ access_key:
+ description:
+ - Access Key ID of the S3 target
+ type: str
+ secret:
+ description:
+ - Secret Access Key for the S3 or Azure target
+ type: str
+ target:
+ description:
+ - Define whether to initialize the S3 bucket
+ required: true
+ type: str
+
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Create remote credential
+ purestorage.flashblade.purefb_remote_cred:
+ name: cred1
+ access_key: "3794fb12c6204e19195f"
+ secret: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
+ target: target1
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Delete remote credential
+ purestorage.flashblade.purefb_remote_cred:
+ name: cred1
+ target: target1
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import ObjectStoreRemoteCredentials
+except ImportError:
+ HAS_PURITY_FB = False
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+MIN_REQUIRED_API_VERSION = "1.9"
+
+
+def get_connected(module, blade):
+ """Return connected device or None"""
+ connected_blades = blade.array_connections.list_array_connections()
+ for target in range(0, len(connected_blades.items)):
+ if (
+ connected_blades.items[target].remote.name == module.params["target"]
+ or connected_blades.items[target].management_address
+ == module.params["target"]
+ ) and connected_blades.items[target].status in [
+ "connected",
+ "connecting",
+ "partially_connected",
+ ]:
+ return connected_blades.items[target].remote.name
+ connected_targets = blade.targets.list_targets()
+ for target in range(0, len(connected_targets.items)):
+ if connected_targets.items[target].name == module.params[
+ "target"
+ ] and connected_targets.items[target].status in [
+ "connected",
+ "connecting",
+ "partially_connected",
+ ]:
+ return connected_targets.items[target].name
+ return None
+
+
+def get_remote_cred(module, blade):
+ """Return Remote Credential or None"""
+ try:
+ res = (
+ blade.object_store_remote_credentials.list_object_store_remote_credentials(
+ names=[module.params["target"] + "/" + module.params["name"]]
+ )
+ )
+ return res.items[0]
+ except Exception:
+ return None
+
+
+def create_credential(module, blade):
+ """Create remote credential"""
+ changed = True
+ if not module.check_mode:
+ remote_cred = module.params["target"] + "/" + module.params["name"]
+ remote_credentials = ObjectStoreRemoteCredentials(
+ access_key_id=module.params["access_key"],
+ secret_access_key=module.params["secret"],
+ )
+ try:
+ blade.object_store_remote_credentials.create_object_store_remote_credentials(
+ names=[remote_cred], remote_credentials=remote_credentials
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to create remote credential {0}".format(remote_cred)
+ )
+ module.exit_json(changed=changed)
+
+
+def update_credential(module, blade):
+ """Update remote credential"""
+ changed = True
+ if not module.check_mode:
+ remote_cred = module.params["target"] + "/" + module.params["name"]
+ new_attr = ObjectStoreRemoteCredentials(
+ access_key_id=module.params["access_key"],
+ secret_access_key=module.params["secret"],
+ )
+ try:
+ blade.object_store_remote_credentials.update_object_store_remote_credentials(
+ names=[remote_cred], remote_credentials=new_attr
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to update remote credential {0}".format(remote_cred)
+ )
+ module.exit_json(changed=changed)
+
+
+def delete_credential(module, blade):
+ """Delete remote credential"""
+ changed = True
+ if not module.check_mode:
+ remote_cred = module.params["target"] + "/" + module.params["name"]
+ try:
+ blade.object_store_remote_credentials.delete_object_store_remote_credentials(
+ names=[remote_cred]
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to delete remote credential {0}.".format(remote_cred)
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ name=dict(type="str", required=True),
+ access_key=dict(type="str", no_log=False),
+ secret=dict(type="str", no_log=True),
+ target=dict(type="str", required=True),
+ )
+ )
+
+ required_if = [["state", "present", ["access_key", "secret"]]]
+
+ module = AnsibleModule(
+ argument_spec, required_if=required_if, supports_check_mode=True
+ )
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ blade = get_blade(module)
+ api_version = blade.api_version.list_versions().versions
+
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(
+ msg="FlashBlade REST version not supported. "
+ "Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
+ )
+
+ target = get_connected(module, blade)
+
+ if not target:
+ module.fail_json(
+ msg="Selected target {0} is not connected.".format(module.params["target"])
+ )
+
+ remote_cred = get_remote_cred(module, blade)
+
+ if module.params["state"] == "present" and not remote_cred:
+ create_credential(module, blade)
+ elif module.params["state"] == "present":
+ update_credential(module, blade)
+ elif module.params["state"] == "absent" and remote_cred:
+ delete_credential(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_s3acc.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_s3acc.py
new file mode 100644
index 000000000..034731994
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_s3acc.py
@@ -0,0 +1,314 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2018, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_s3acc
+version_added: '1.0.0'
+short_description: Create or delete FlashBlade Object Store accounts
+description:
+- Create or delete object store accounts on a Pure Stoage FlashBlade.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Create or delete object store account
+ default: present
+ choices: [ absent, present ]
+ type: str
+ name:
+ description:
+ - The name of object store account
+ type: str
+ required: true
+ quota:
+ description:
+ - The effective quota limit to be applied against the size of the account in bytes.
+ - If set to '' (empty string), the account is unlimited in size.
+ version_added: 1.11.0
+ type: str
+ hard_limit:
+ description:
+ - If set to true, the account size, as defined by I(quota_limit), is used as a hard limit quota.
+ - If set to false, a hard limit quota will not be applied to the account, but soft quota alerts
+ will still be sent if the account has a value set for I(quota_limit).
+ version_added: 1.11.0
+ type: bool
+ default: false
+ default_quota:
+ description:
+ - The value of this field will be used to configure the I(quota_limit) field of newly created buckets
+ associated with this object store account, if the bucket creation does not specify its own value.
+ - If set to '' (empty string), the bucket default is unlimited in size.
+ version_added: 1.11.0
+ type: str
+ default_hard_limit:
+ description:
+ - The value of this field will be used to configure the I(hard_limit) field of newly created buckets
+ associated with this object store account, if the bucket creation does not specify its own value.
+ version_added: 1.11.0
+ type: bool
+ default: false
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Crrate object store account foo (with no quotas)
+ purestorage.flashblade.purefb_s3acc:
+ name: foo
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+
+- name: Create object store account foo (with quotas)
+ purestorage.flashblade.purefb_s3acc:
+ name: foo
+ quota: 20480000
+ hard_limit: true
+ default_quota: 1024000
+ default_hard_limit: false
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+
+- name: Delete object store account foo
+ purestorage.flashblade.purefb_s3acc:
+ name: foo
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURESTORAGE = True
+try:
+ from pypureclient.flashblade import ObjectStoreAccountPatch, BucketDefaults
+except ImportError:
+ HAS_PURESTORAGE = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ get_system,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.3"
+QUOTA_API_VERSION = "2.1"
+
+
+def get_s3acc(module, blade):
+ """Return Object Store Account or None"""
+ s3acc = None
+ accts = blade.object_store_accounts.list_object_store_accounts()
+ for acct in range(0, len(accts.items)):
+ if accts.items[acct].name == module.params["name"]:
+ s3acc = accts.items[acct]
+ return s3acc
+
+
+def update_s3acc(module):
+ """Update Object Store Account"""
+ changed = False
+ blade = get_system(module)
+ acc_settings = list(
+ blade.get_object_store_accounts(names=[module.params["name"]]).items
+ )[0]
+ current_account = {
+ "hard_limit": acc_settings.hard_limit_enabled,
+ "default_hard_limit": acc_settings.bucket_defaults.hard_limit_enabled,
+ "quota": str(acc_settings.quota_limit),
+ "default_quota": str(acc_settings.bucket_defaults.quota_limit),
+ }
+ if current_account["quota"] == "None":
+ current_account["quota"] = ""
+ if current_account["default_quota"] == "None":
+ current_account["default_quota"] = ""
+ if module.params["quota"] is None:
+ module.params["quota"] = current_account["quota"]
+ if module.params["default_quota"] is None:
+ module.params["default_quota"] = current_account["default_quota"]
+ new_account = {
+ "hard_limit": module.params["hard_limit"],
+ "default_hard_limit": module.params["default_hard_limit"],
+ "quota": module.params["quota"],
+ "default_quota": module.params["default_quota"],
+ }
+ if new_account != current_account:
+ changed = True
+ if not module.check_mode:
+ osa = ObjectStoreAccountPatch(
+ hard_limit_enabled=new_account["hard_limit"],
+ quota_limit=new_account["quota"],
+ bucket_defaults=BucketDefaults(
+ hard_limit_enabled=new_account["default_hard_limit"],
+ quota_limit=new_account["default_quota"],
+ ),
+ )
+ res = blade.patch_object_store_accounts(
+ object_store_account=osa, names=[module.params["name"]]
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to update account {0}. "
+ "Error: {1}".format(module.params["name"], res.errors[0].message)
+ )
+ module.exit_json(changed=changed)
+
+
+def create_s3acc(module, blade):
+ """Create Object Store Account"""
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.object_store_accounts.create_object_store_accounts(
+ names=[module.params["name"]]
+ )
+ except Exception:
+ module.fail_json(
+ msg="Object Store Account {0}: Creation failed".format(
+ module.params["name"]
+ )
+ )
+ if module.params["quota"] or module.params["default_quota"]:
+ blade2 = get_system(module)
+ if module.params["quota"] and not module.params["default_quota"]:
+ osa = ObjectStoreAccountPatch(
+ hard_limit_enabled=module.params["hard_limit"],
+ quota_limit=module.params["quota"],
+ )
+ if not module.params["quota"] and module.params["default_quota"]:
+ osa = ObjectStoreAccountPatch(
+ bucket_defaults=BucketDefaults(
+ hard_limit_enabled=module.params["default_hard_limit"],
+ quota_limit=module.params["default_quota"],
+ )
+ )
+ else:
+ osa = ObjectStoreAccountPatch(
+ hard_limit_enabled=module.params["hard_limit"],
+ quota_limit=module.params["quota"],
+ bucket_defaults=BucketDefaults(
+ hard_limit_enabled=module.params["default_hard_limit"],
+ quota_limit=module.params["default_quota"],
+ ),
+ )
+ res = blade2.patch_object_store_accounts(
+ object_store_account=osa, names=[module.params["name"]]
+ )
+ if res.status_code != 200:
+ blade.object_store_accounts.delete_object_store_accounts(
+ names=[module.params["name"]]
+ )
+ module.fail_json(
+ msg="Failed to set quotas correctly for account {0}. "
+ "Error: {1}".format(module.params["name"], res.errors[0].message)
+ )
+ module.exit_json(changed=changed)
+
+
+def delete_s3acc(module, blade):
+ """Delete Object Store Account"""
+ changed = True
+ if not module.check_mode:
+ count = len(
+ blade.object_store_users.list_object_store_users(
+ filter="name='" + module.params["name"] + "/*'"
+ ).items
+ )
+ if count != 0:
+ module.fail_json(
+ msg="Remove all Users from Object Store Account {0} \
+ before deletion".format(
+ module.params["name"]
+ )
+ )
+ else:
+ try:
+ blade.object_store_accounts.delete_object_store_accounts(
+ names=[module.params["name"]]
+ )
+ except Exception:
+ module.fail_json(
+ msg="Object Store Account {0}: Deletion failed".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(required=True, type="str"),
+ hard_limit=dict(type="bool", default=False),
+ default_hard_limit=dict(type="bool", default=False),
+ quota=dict(type="str"),
+ default_quota=dict(type="str"),
+ state=dict(default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ versions = blade.api_version.list_versions().versions
+
+ if MIN_REQUIRED_API_VERSION not in versions:
+ module.fail_json(
+ msg="Minimum FlashBlade REST version required: {0}".format(
+ MIN_REQUIRED_API_VERSION
+ )
+ )
+
+ if module.params["quota"] or module.params["default_quota"]:
+ if not HAS_PURESTORAGE:
+ module.fail_json(msg="py-pure-client sdk is required for to set quotas")
+ if QUOTA_API_VERSION not in versions:
+ module.fail_json(
+ msg="Quotas require minimum FlashBlade REST version: {0}".format(
+ QUOTA_API_VERSION
+ )
+ )
+
+ upper = False
+ for element in module.params["name"]:
+ if element.isupper():
+ upper = True
+ break
+ if upper:
+ module.warn("Changing account name to lowercase...")
+ module.params["name"] = module.params["name"].lower()
+
+ s3acc = get_s3acc(module, blade)
+
+ if state == "absent" and s3acc:
+ delete_s3acc(module, blade)
+ elif state == "present" and s3acc:
+ update_s3acc(module)
+ elif not s3acc and state == "present":
+ create_s3acc(module, blade)
+ else:
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_s3user.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_s3user.py
new file mode 100644
index 000000000..55bc05c3f
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_s3user.py
@@ -0,0 +1,436 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2018, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_s3user
+version_added: '1.0.0'
+short_description: Create or delete FlashBlade Object Store account users
+description:
+- Create or delete object store account users on a Pure Stoage FlashBlade.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Create or delete object store account user
+ - Remove a specified access key for a user
+ default: present
+ choices: [ absent, present, remove_key ]
+ type: str
+ name:
+ description:
+ - The name of object store user
+ type: str
+ required: true
+ account:
+ description:
+ - The name of object store account associated with user
+ type: str
+ required: true
+ access_key:
+ description:
+ - Create secret access key.
+ - Key can be exposed using the I(debug) module
+ - If enabled this will override I(imported_key)
+ type: bool
+ default: false
+ remove_key:
+ description:
+ - Access key to be removed from user
+ type: str
+ version_added: "1.5.0"
+ imported_key:
+ description:
+ - Access key of imported credentials
+ type: str
+ version_added: "1.4.0"
+ imported_secret:
+ description:
+ - Access key secret for access key to import
+ type: str
+ version_added: "1.4.0"
+ policy:
+ description:
+ - User Access Policies to be assigned to user on creation
+ - To amend policies use the I(purestorage.flashblade.purefb_userpolicy) module
+ - If not specified, I(pure\:policy/full-access) will be added
+ type: list
+ elements: str
+ version_added: "1.6.0"
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Create object store user (with access ID and key) foo in account bar
+ purestorage.flashblade.purefb_s3user:
+ name: foo
+ account: bar
+ access_key: true
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+ register: result
+
+- debug:
+ msg: "S3 User: {{ result['s3user_info'] }}"
+
+- name: Create object store user (with access ID and key) foo in account bar with access policy (Purity 3.2 and higher)
+ purestorage.flashblade.purefb_s3user:
+ name: foo
+ account: bar
+ access_key: true
+ policy:
+ - pure:policy/safemode-configure
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+
+- name: Create object store user foo using imported key/secret in account bar
+ purestorage.flashblade.purefb_s3user:
+ name: foo
+ account: bar
+ imported_key: "PSABSSZRHPMEDKHMAAJPJBONPJGGDDAOFABDGLBJLHO"
+ imported_secret: "BAG61F63105e0d3669/e066+5C5DFBE2c127d395LBGG"
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+
+- name: Delete object store user foo in account bar
+ purestorage.flashblade.purefb_s3user:
+ name: foo
+ account: bar
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+"""
+
+RETURN = r"""
+"""
+
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import ObjectStoreAccessKey, ObjectStoreAccessKeyPost
+except ImportError:
+ HAS_PURITY_FB = False
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ get_system,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.3"
+IMPORT_KEY_API_VERSION = "1.10"
+POLICY_API_VERSION = "2.0"
+
+
+def get_s3acc(module, blade):
+ """Return Object Store Account or None"""
+ s3acc = None
+ accts = blade.object_store_accounts.list_object_store_accounts()
+ for acct in range(0, len(accts.items)):
+ if accts.items[acct].name == module.params["account"]:
+ s3acc = accts.items[acct]
+ return s3acc
+
+
+def get_s3user(module, blade):
+ """Return Object Store Account or None"""
+ full_user = module.params["account"] + "/" + module.params["name"]
+ s3user = None
+ s3users = blade.object_store_users.list_object_store_users()
+ for user in range(0, len(s3users.items)):
+ if s3users.items[user].name == full_user:
+ s3user = s3users.items[user]
+ return s3user
+
+
+def update_s3user(module, blade):
+ """Update Object Store User"""
+ changed = False
+ exists = False
+ s3user_facts = {}
+ user = module.params["account"] + "/" + module.params["name"]
+ if module.params["access_key"] or module.params["imported_key"]:
+ key_count = 0
+ keys = blade.object_store_access_keys.list_object_store_access_keys()
+ for key in range(0, len(keys.items)):
+ if module.params["imported_key"]:
+ versions = blade.api_version.list_versions().versions
+ if IMPORT_KEY_API_VERSION in versions:
+ if keys.items[key].name == module.params["imported_key"]:
+ module.warn("Imported key provided already belongs to a user")
+ exists = True
+ if keys.items[key].user.name == user:
+ key_count += 1
+ if not exists:
+ if key_count < 2:
+ changed = True
+ if not module.check_mode:
+ try:
+ if (
+ module.params["access_key"]
+ and module.params["imported_key"]
+ ):
+ module.warn("'access_key: true' overrides imported keys")
+ if module.params["access_key"]:
+ result = blade.object_store_access_keys.create_object_store_access_keys(
+ object_store_access_key=ObjectStoreAccessKey(
+ user={"name": user}
+ )
+ )
+ s3user_facts["fb_s3user"] = {
+ "user": user,
+ "access_key": result.items[0].secret_access_key,
+ "access_id": result.items[0].name,
+ }
+ else:
+ if IMPORT_KEY_API_VERSION in versions:
+ blade.object_store_access_keys.create_object_store_access_keys(
+ names=[module.params["imported_key"]],
+ object_store_access_key=ObjectStoreAccessKeyPost(
+ user={"name": user},
+ secret_access_key=module.params[
+ "imported_secret"
+ ],
+ ),
+ )
+ except Exception:
+ if module.params["imported_key"]:
+ module.fail_json(
+ msg="Object Store User {0}: Access Key import failed".format(
+ user
+ )
+ )
+ else:
+ module.fail_json(
+ msg="Object Store User {0}: Access Key creation failed".format(
+ user
+ )
+ )
+ else:
+ module.warn(
+ "Object Store User {0}: Maximum Access Key count reached".format(
+ user
+ )
+ )
+ module.exit_json(changed=changed, s3user_info=s3user_facts)
+
+
+def create_s3user(module, blade):
+ """Create Object Store Account"""
+ s3user_facts = {}
+ changed = True
+ if not module.check_mode:
+ user = module.params["account"] + "/" + module.params["name"]
+ blade.object_store_users.create_object_store_users(names=[user])
+ if module.params["access_key"] and module.params["imported_key"]:
+ module.warn("'access_key: true' overrides imported keys")
+ if module.params["access_key"]:
+ try:
+ result = blade.object_store_access_keys.create_object_store_access_keys(
+ object_store_access_key=ObjectStoreAccessKey(user={"name": user})
+ )
+ s3user_facts["fb_s3user"] = {
+ "user": user,
+ "access_key": result.items[0].secret_access_key,
+ "access_id": result.items[0].name,
+ }
+ except Exception:
+ delete_s3user(module, blade, True)
+ module.fail_json(
+ msg="Object Store User {0}: Creation failed".format(user)
+ )
+ else:
+ if module.params["imported_key"]:
+ versions = blade.api_version.list_versions().versions
+ if IMPORT_KEY_API_VERSION in versions:
+ try:
+ blade.object_store_access_keys.create_object_store_access_keys(
+ names=[module.params["imported_key"]],
+ object_store_access_key=ObjectStoreAccessKeyPost(
+ user={"name": user},
+ secret_access_key=module.params["imported_secret"],
+ ),
+ )
+ except Exception:
+ delete_s3user(module, blade)
+ module.fail_json(
+ msg="Object Store User {0}: Creation failed with imported access key".format(
+ user
+ )
+ )
+ if module.params["policy"]:
+ blade = get_system(module)
+ api_version = list(blade.get_versions().items)
+
+ if POLICY_API_VERSION in api_version:
+ policy_list = module.params["policy"]
+ for policy in range(0, len(policy_list)):
+ if (
+ blade.get_object_store_access_policies(
+ names=[policy_list[policy]]
+ ).status_code
+ != 200
+ ):
+ module.warn(
+ "Policy {0} is not valid. Ignoring...".format(
+ policy_list[policy]
+ )
+ )
+ policy_list.remove(policy_list[policy])
+ username = module.params["account"] + "/" + module.params["name"]
+ for policy in range(0, len(policy_list)):
+ if not (
+ blade.get_object_store_users_object_store_access_policies(
+ member_names=[username], policy_names=[policy_list[policy]]
+ ).items
+ ):
+ res = (
+ blade.post_object_store_access_policies_object_store_users(
+ member_names=[username],
+ policy_names=[policy_list[policy]],
+ )
+ )
+ if res.status_code != 200:
+ module.warn(
+ "Failed to add policy {0} to account user {1}. Skipping...".format(
+ policy_list[policy], username
+ )
+ )
+ if "pure:policy/full-access" not in policy_list:
+ # User Create adds the pure:policy/full-access policy by default
+ # If we are specifying a list then remove this default value
+ blade.delete_object_store_access_policies_object_store_users(
+ member_names=[username],
+ policy_names=["pure:policy/full-access"],
+ )
+ else:
+ module.warn(
+ "FlashBlade REST version not supported for user access policies. Skipping..."
+ )
+ module.exit_json(changed=changed, s3user_info=s3user_facts)
+
+
+def remove_key(module, blade):
+ """Remove Access Key from User"""
+ changed = False
+ if not module.check_mode:
+ try:
+ keys = blade.object_store_access_keys.list_object_store_access_keys()
+ for key in range(0, len(keys.items)):
+ if keys.items[key].name == module.params["remove_key"]:
+ blade.object_store_access_keys.delete_object_store_access_keys(
+ names=[module.params["remove_key"]]
+ )
+ changed = True
+ except Exception:
+ module.fail_json(msg="Failed to correctly read or delete access keys")
+ module.exit_json(changed=changed)
+
+
+def delete_s3user(module, blade, internal=False):
+ """Delete Object Store Account"""
+ changed = True
+ if not module.check_mode:
+ user = module.params["account"] + "/" + module.params["name"]
+ try:
+ blade.object_store_users.delete_object_store_users(names=[user])
+ except Exception:
+ module.fail_json(
+ msg="Object Store Account {0}: Deletion failed".format(
+ module.params["name"]
+ )
+ )
+ if internal:
+ return
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(required=True, type="str"),
+ account=dict(required=True, type="str"),
+ access_key=dict(default="false", type="bool"),
+ imported_key=dict(type="str", no_log=False),
+ remove_key=dict(type="str", no_log=False),
+ imported_secret=dict(type="str", no_log=True),
+ policy=dict(type="list", elements="str"),
+ state=dict(default="present", choices=["present", "absent", "remove_key"]),
+ )
+ )
+
+ required_together = [["imported_key", "imported_secret"]]
+ required_if = [["state", "remove_key", ["remove_key"]]]
+
+ module = AnsibleModule(
+ argument_spec,
+ required_together=required_together,
+ required_if=required_if,
+ supports_check_mode=True,
+ )
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ versions = blade.api_version.list_versions().versions
+
+ if MIN_REQUIRED_API_VERSION not in versions:
+ module.fail_json(
+ msg="Minimum FlashBlade REST version required: {0}".format(
+ MIN_REQUIRED_API_VERSION
+ )
+ )
+ upper = False
+ for element in module.params["account"]:
+ if element.isupper():
+ upper = True
+ break
+ if upper:
+ module.warn("Changing account name to lowercase...")
+ module.params["account"] = module.params["account"].lower()
+
+ s3acc = get_s3acc(module, blade)
+ if not s3acc:
+ module.fail_json(
+ msg="Object Store Account {0} does not exist".format(
+ module.params["account"]
+ )
+ )
+
+ s3user = get_s3user(module, blade)
+
+ if state == "absent" and s3user:
+ delete_s3user(module, blade)
+ elif state == "present" and s3user:
+ update_s3user(module, blade)
+ elif not s3user and state == "present":
+ create_s3user(module, blade)
+ elif state == "remove_key" and s3user:
+ remove_key(module, blade)
+ else:
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_smtp.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_smtp.py
new file mode 100644
index 000000000..379443669
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_smtp.py
@@ -0,0 +1,124 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2018, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_smtp
+version_added: '1.0.0'
+short_description: Configure SMTP for Pure Storage FlashBlade
+description:
+- Configure SMTP for a Pure Storage FlashBlade.
+- Whilst there can be no relay host, a sender domain must be configured.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ host:
+ description:
+ - Relay server name
+ type: str
+ domain:
+ description:
+ - Domain name for alert messages
+ required: true
+ type: str
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Configure SMTP settings
+ purestorage.flashblade.purefb_smtp:
+ host: hostname
+ domain: xyz.com
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import Smtp
+except ImportError:
+ HAS_PURITY_FB = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.6"
+
+
+def set_smtp(module, blade):
+ """Configure SMTP settings"""
+ changed = False
+ current_smtp = blade.smtp.list_smtp().items[0]
+ if module.params["host"] and module.params["host"] != current_smtp.relay_host:
+ smtp_settings = Smtp(relay_host=module.params["host"])
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.smtp.update_smtp(smtp_settings=smtp_settings)
+ except Exception:
+ module.fail_json(msg="Configuring SMTP relay host failed")
+ elif current_smtp.relay_host and not module.params["host"]:
+ smtp_settings = Smtp(relay_host="")
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.smtp.update_smtp(smtp_settings=smtp_settings)
+ except Exception:
+ module.fail_json(msg="Configuring SMTP relay host failed")
+ if module.params["domain"] != current_smtp.sender_domain:
+ smtp_settings = Smtp(sender_domain=module.params["domain"])
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.smtp.update_smtp(smtp_settings=smtp_settings)
+ except Exception:
+ module.fail_json(msg="Configuring SMTP sender domain failed")
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ host=dict(type="str"),
+ domain=dict(type="str", required=True),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ blade = get_blade(module)
+ api_version = blade.api_version.list_versions().versions
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(msg="Purity//FB must be upgraded to support this module.")
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb SDK is required for this module")
+
+ set_smtp(module, blade)
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_snap.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_snap.py
new file mode 100644
index 000000000..5df0455f8
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_snap.py
@@ -0,0 +1,379 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2017, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_snap
+version_added: '1.0.0'
+short_description: Manage filesystem snapshots on Pure Storage FlashBlades
+description:
+- Create or delete volumes and filesystem snapshots on Pure Storage FlashBlades.
+- Restoring a filesystem from a snapshot is only supported using
+ the latest snapshot.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - The name of the source filesystem.
+ required: true
+ type: str
+ suffix:
+ description:
+ - Suffix of snapshot name.
+ type: str
+ state:
+ description:
+ - Define whether the filesystem snapshot should exist or not.
+ choices: [ absent, present, restore ]
+ default: present
+ type: str
+ targets:
+ description:
+ - Name of target to replicate snapshot to.
+ - This is only applicable when I(now) is B(true)
+ type: list
+ elements: str
+ version_added: "1.7.0"
+ now:
+ description:
+ - Whether to initiate a snapshot replication immeadiately
+ type: bool
+ default: false
+ version_added: "1.7.0"
+ eradicate:
+ description:
+ - Define whether to eradicate the snapshot on delete or leave in trash.
+ type: bool
+ default: false
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Create snapshot foo.ansible
+ purestorage.flashblade.purefb_snap:
+ name: foo
+ suffix: ansible
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+ state: present
+
+- name: Create immeadiate snapshot foo.ansible to connected FB bar
+ purestorage.flashblade.purefb_snap:
+ name: foo
+ suffix: ansible
+ now: true
+ targets:
+ - bar
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+ state: present
+
+- name: Delete snapshot named foo.snap
+ purestorage.flashblade.purefb_snap:
+ name: foo
+ suffix: snap
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+ state: absent
+
+- name: Recover deleted snapshot foo.ansible
+ purestorage.flashblade.purefb_snap:
+ name: foo
+ suffix: ansible
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+ state: present
+
+- name: Restore filesystem foo (uses latest snapshot)
+ purestorage.flashblade.purefb_snap:
+ name: foo
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+ state: restore
+
+- name: Eradicate snapshot named foo.snap
+ purestorage.flashblade.purefb_snap:
+ name: foo
+ suffix: snap
+ eradicate: true
+ fb_url: 10.10.10.2
+ api_token: e31060a7-21fc-e277-6240-25983c6c4592
+ state: absent
+"""
+
+RETURN = r"""
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+from datetime import datetime
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import FileSystemSnapshot, SnapshotSuffix, FileSystem, Reference
+except ImportError:
+ HAS_PURITY_FB = False
+
+SNAP_NOW_API = 1.10
+
+
+def get_fs(module, blade):
+ """Return Filesystem or None"""
+ filesystem = []
+ filesystem.append(module.params["name"])
+ try:
+ res = blade.file_systems.list_file_systems(names=filesystem)
+ return res.items[0]
+ except Exception:
+ return None
+
+
+def get_latest_fssnapshot(module, blade):
+ """Get the name of the latest snpshot or None"""
+ try:
+ filt = "source='" + module.params["name"] + "'"
+ all_snaps = blade.file_system_snapshots.list_file_system_snapshots(filter=filt)
+ if not all_snaps.items[0].destroyed:
+ return all_snaps.items[0].name
+ else:
+ module.fail_json(
+ msg="Latest snapshot {0} is destroyed."
+ " Eradicate or recover this first.".format(all_snaps.items[0].name)
+ )
+ except Exception:
+ return None
+
+
+def get_fssnapshot(module, blade):
+ """Return Snapshot or None"""
+ try:
+ filt = (
+ "source='"
+ + module.params["name"]
+ + "' and suffix='"
+ + module.params["suffix"]
+ + "'"
+ )
+ res = blade.file_system_snapshots.list_file_system_snapshots(filter=filt)
+ return res.items[0]
+ except Exception:
+ return None
+
+
+def create_snapshot(module, blade):
+ """Create Snapshot"""
+ changed = False
+ source = []
+ source.append(module.params["name"])
+ try:
+ if module.params["now"]:
+ blade_exists = []
+ connected_blades = blade.array_connections.list_array_connections()
+ for target in range(0, len(module.params["targets"])):
+ blade_exists.append(False)
+ for blade in range(0, len(connected_blades)):
+ if (
+ target[target] == connected_blades.items[blade].name
+ and connected_blades.items[blade].status == "connected"
+ ):
+ blade_exists[target] = True
+ if not blade_exists:
+ module.fail_json(
+ msg="Not all selected targets are correctly connected blades"
+ )
+ changed = True
+ if not module.check_mode:
+ blade.file_system_snapshots.create_file_system_snapshots(
+ sources=source,
+ send=True,
+ targets=module.params["targets"],
+ suffix=SnapshotSuffix(module.params["suffix"]),
+ )
+ else:
+ changed = True
+ if not module.check_mode:
+ blade.file_system_snapshots.create_file_system_snapshots(
+ sources=source, suffix=SnapshotSuffix(module.params["suffix"])
+ )
+ except Exception:
+ changed = False
+ module.exit_json(changed=changed)
+
+
+def restore_snapshot(module, blade):
+ """Restore a filesystem back from the latest snapshot"""
+ changed = True
+ snapname = get_latest_fssnapshot(module, blade)
+ if snapname is not None:
+ if not module.check_mode:
+ fs_attr = FileSystem(
+ name=module.params["name"], source=Reference(name=snapname)
+ )
+ try:
+ blade.file_systems.create_file_systems(
+ overwrite=True,
+ discard_non_snapshotted_data=True,
+ file_system=fs_attr,
+ )
+ except Exception:
+ changed = False
+ else:
+ module.fail_json(
+ msg="Filesystem {0} has no snapshots to restore from.".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def recover_snapshot(module, blade):
+ """Recover deleted Snapshot"""
+ changed = True
+ if not module.check_mode:
+ snapname = module.params["name"] + "." + module.params["suffix"]
+ new_attr = FileSystemSnapshot(destroyed=False)
+ try:
+ blade.file_system_snapshots.update_file_system_snapshots(
+ name=snapname, attributes=new_attr
+ )
+ except Exception:
+ changed = False
+ module.exit_json(changed=changed)
+
+
+def update_snapshot(module, blade):
+ """Update Snapshot"""
+ changed = False
+ module.exit_json(changed=changed)
+
+
+def delete_snapshot(module, blade):
+ """Delete Snapshot"""
+ if not module.check_mode:
+ snapname = module.params["name"] + "." + module.params["suffix"]
+ new_attr = FileSystemSnapshot(destroyed=True)
+ try:
+ blade.file_system_snapshots.update_file_system_snapshots(
+ name=snapname, attributes=new_attr
+ )
+ changed = True
+ if module.params["eradicate"]:
+ try:
+ blade.file_system_snapshots.delete_file_system_snapshots(
+ name=snapname
+ )
+ changed = True
+ except Exception:
+ changed = False
+ except Exception:
+ changed = False
+ module.exit_json(changed=changed)
+
+
+def eradicate_snapshot(module, blade):
+ """Eradicate Snapshot"""
+ if not module.check_mode:
+ snapname = module.params["name"] + "." + module.params["suffix"]
+ try:
+ blade.file_system_snapshots.delete_file_system_snapshots(name=snapname)
+ changed = True
+ except Exception:
+ changed = False
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(required=True),
+ suffix=dict(type="str"),
+ now=dict(type="bool", default=False),
+ targets=dict(type="list", elements="str"),
+ eradicate=dict(default="false", type="bool"),
+ state=dict(default="present", choices=["present", "absent", "restore"]),
+ )
+ )
+
+ required_if = [["now", True, ["targets"]]]
+ module = AnsibleModule(
+ argument_spec, required_if=required_if, supports_check_mode=True
+ )
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ if module.params["suffix"] is None:
+ suffix = "snap-" + str(
+ (datetime.utcnow() - datetime(1970, 1, 1, 0, 0, 0, 0)).total_seconds()
+ )
+ module.params["suffix"] = suffix.replace(".", "")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ versions = blade.api_version.list_versions().versions
+
+ if SNAP_NOW_API not in versions and module.params["now"]:
+ module.fail_json(
+ msg="Minimum FlashBlade REST version for immeadiate remote snapshots: {0}".format(
+ SNAP_NOW_API
+ )
+ )
+ filesystem = get_fs(module, blade)
+ snap = get_fssnapshot(module, blade)
+
+ if state == "present" and filesystem and not filesystem.destroyed and not snap:
+ create_snapshot(module, blade)
+ elif (
+ state == "present"
+ and filesystem
+ and not filesystem.destroyed
+ and snap
+ and not snap.destroyed
+ ):
+ update_snapshot(module, blade)
+ elif (
+ state == "present"
+ and filesystem
+ and not filesystem.destroyed
+ and snap
+ and snap.destroyed
+ ):
+ recover_snapshot(module, blade)
+ elif state == "present" and filesystem and filesystem.destroyed:
+ update_snapshot(module, blade)
+ elif state == "present" and not filesystem:
+ update_snapshot(module, blade)
+ elif state == "restore" and filesystem:
+ restore_snapshot(module, blade)
+ elif state == "absent" and snap and not snap.destroyed:
+ delete_snapshot(module, blade)
+ elif state == "absent" and snap and snap.destroyed:
+ eradicate_snapshot(module, blade)
+ elif state == "absent" and not snap:
+ module.exit_json(changed=False)
+ else:
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_snmp_agent.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_snmp_agent.py
new file mode 100644
index 000000000..8db09f7b6
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_snmp_agent.py
@@ -0,0 +1,210 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2020, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_snmp_agent
+version_added: '1.0.0'
+short_description: Configure the FlashBlade SNMP Agent
+description:
+- Configure the management SNMP Agent on a Pure Storage FlashBlade.
+- This module is not idempotent and will always modify the
+ existing management SNMP agent due to hidden parameters that cannot
+ be compared to the play parameters.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ auth_passphrase:
+ type: str
+ description:
+ - SNMPv3 only. Passphrase of 8 - 32 characters.
+ auth_protocol:
+ type: str
+ description:
+ - SNMP v3 only. Hash algorithm to use
+ choices: [ MD5, SHA ]
+ community:
+ type: str
+ description:
+ - SNMP v2c only. Manager community ID. Between 1 and 32 characters long.
+ user:
+ type: str
+ description:
+ - SNMP v3 only. User ID recognized by the specified SNMP agent.
+ Must be between 1 and 32 characters.
+ version:
+ type: str
+ description:
+ - Version of SNMP protocol to use for the agent.
+ choices: [ v2c, v3 ]
+ privacy_passphrase:
+ type: str
+ description:
+ - SNMPv3 only. Passphrase to encrypt SNMP messages.
+ Must be between 8 and 63 non-space ASCII characters.
+ privacy_protocol:
+ type: str
+ description:
+ - SNMP v3 only. Encryption protocol to use
+ choices: [ AES, DES ]
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Update v2c SNMP agent
+ purestorage.flashblade.purefb_snmp_agent:
+ community: public
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Update v3 SNMP agent
+ purestorage.flashblade.purefb_snmp_agent:
+ version: v3
+ auth_protocol: MD5
+ auth_passphrase: password
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+"""
+
+RETURN = r"""
+"""
+
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import SnmpAgent, SnmpV2c, SnmpV3
+except ImportError:
+ HAS_PURITY_FB = False
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.9"
+
+
+def update_agent(module, blade):
+ """Update SNMP Agent"""
+ changed = False
+ try:
+ agent = blade.snmp_agents.list_snmp_agents()
+ except Exception:
+ module.fail_json(msg="Failed to get configuration for SNMP agent.")
+ current_attr = {
+ "community": agent.items[0].v2c.community,
+ "version": agent.items[0].version,
+ "auth_passphrase": agent.items[0].v3.auth_passphrase,
+ "auth_protocol": agent.items[0].v3.auth_protocol,
+ "privacy_passphrase": agent.items[0].v3.privacy_passphrase,
+ "privacy_protocol": agent.items[0].v3.privacy_protocol,
+ "user": agent.items[0].v3.user,
+ }
+ new_attr = {
+ "community": module.params["community"],
+ "version": module.params["version"],
+ "auth_passphrase": module.params["auth_passphrase"],
+ "auth_protocol": module.params["auth_protocol"],
+ "privacy_passphrase": module.params["privacy_passphrase"],
+ "privacy_protocol": module.params["privacy_protocol"],
+ "user": module.params["user"],
+ }
+ if current_attr != new_attr:
+ changed = True
+ if not module.check_mode:
+ if new_attr["version"] == "v2c":
+ updated_v2c_attrs = SnmpV2c(community=new_attr["community"])
+ updated_v2c_agent = SnmpAgent(version="v2c", v2c=updated_v2c_attrs)
+ try:
+ blade.snmp_agents.update_snmp_agents(snmp_agent=updated_v2c_agent)
+ changed = True
+ except Exception:
+ module.fail_json(msg="Failed to update v2c SNMP agent.")
+ else:
+ updated_v3_attrs = SnmpV3(
+ auth_protocol=new_attr["auth_protocol"],
+ auth_passphrase=new_attr["auth_passphrase"],
+ privacy_protocol=new_attr["privacy_protocol"],
+ privacy_passphrase=new_attr["privacy_passphrase"],
+ user=new_attr["user"],
+ )
+ updated_v3_agent = SnmpAgent(version="v3", v3=updated_v3_attrs)
+ try:
+ blade.snmp_agents.update_snmp_agents(snmp_agent=updated_v3_agent)
+ changed = True
+ except Exception:
+ module.fail_json(msg="Failed to update v3 SNMP agent.")
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ user=dict(type="str"),
+ auth_passphrase=dict(type="str", no_log=True),
+ auth_protocol=dict(type="str", choices=["MD5", "SHA"]),
+ privacy_passphrase=dict(type="str", no_log=True),
+ privacy_protocol=dict(type="str", choices=["AES", "DES"]),
+ version=dict(type="str", choices=["v2c", "v3"]),
+ community=dict(type="str"),
+ )
+ )
+
+ required_together = [
+ ["auth_passphrase", "auth_protocol"],
+ ["privacy_passphrase", "privacy_protocol"],
+ ]
+ required_if = [["version", "v2c", ["community"]], ["version", "v3", ["user"]]]
+
+ module = AnsibleModule(
+ argument_spec,
+ required_together=required_together,
+ required_if=required_if,
+ supports_check_mode=True,
+ )
+
+ blade = get_blade(module)
+ api_version = blade.api_version.list_versions().versions
+
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(msg="Purity//FB must be upgraded to support this module.")
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb SDK is required for this module")
+
+ if module.params["version"] == "v3":
+ if module.params["auth_passphrase"] and (
+ 8 > len(module.params["auth_passphrase"]) > 32
+ ):
+ module.fail_json(msg="auth_password must be between 8 and 32 characters")
+ if (
+ module.params["privacy_passphrase"]
+ and 8 > len(module.params["privacy_passphrase"]) > 63
+ ):
+ module.fail_json(msg="privacy_password must be between 8 and 63 characters")
+
+ update_agent(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_snmp_mgr.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_snmp_mgr.py
new file mode 100644
index 000000000..66c2417dd
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_snmp_mgr.py
@@ -0,0 +1,357 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2020, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_snmp_mgr
+version_added: '1.0.0'
+short_description: Configure FlashBlade SNMP Managers
+description:
+- Manage SNMP managers on a Pure Storage FlashBlade.
+- This module is not idempotent and will always modify an
+ existing SNMP manager due to hidden parameters that cannot
+ be compared to the play parameters.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - Name of SNMP Manager
+ required: true
+ type: str
+ state:
+ description:
+ - Create or delete SNMP manager
+ type: str
+ default: present
+ choices: [ absent, present ]
+ auth_passphrase:
+ type: str
+ description:
+ - SNMPv3 only. Passphrase of 8 - 32 characters.
+ auth_protocol:
+ type: str
+ description:
+ - SNMP v3 only. Hash algorithm to use
+ choices: [ MD5, SHA ]
+ community:
+ type: str
+ description:
+ - SNMP v2c only. Manager community ID. Between 1 and 32 characters long.
+ host:
+ type: str
+ description:
+ - IPv4 or IPv6 address or FQDN to send trap messages to.
+ user:
+ type: str
+ description:
+ - SNMP v3 only. User ID recognized by the specified SNMP manager.
+ Must be between 1 and 32 characters.
+ version:
+ type: str
+ description:
+ - Version of SNMP protocol to use for the manager.
+ choices: [ v2c, v3 ]
+ notification:
+ type: str
+ description:
+ - Action to perform on event.
+ default: trap
+ choices: [ inform, trap ]
+ privacy_passphrase:
+ type: str
+ description:
+ - SNMPv3 only. Passphrase to encrypt SNMP messages.
+ Must be between 8 and 63 non-space ASCII characters.
+ privacy_protocol:
+ type: str
+ description:
+ - SNMP v3 only. Encryption protocol to use
+ choices: [ AES, DES ]
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Delete exisitng SNMP manager
+ purestorage.flashblade.purefb_snmp_mgr:
+ name: manager1
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Create v2c SNMP manager
+ purestorage.flashblade.purefb_snmp_mgr:
+ name: manager1
+ community: public
+ host: 10.21.22.23
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Create v3 SNMP manager
+ purestorage.flashblade.purefb_snmp_mgr:
+ name: manager2
+ version: v3
+ auth_protocol: MD5
+ auth_passphrase: password
+ host: 10.21.22.23
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+- name: Update existing SNMP manager
+ purestorage.flashblade.purefb_snmp_mgr:
+ name: manager1
+ community: private
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+"""
+
+RETURN = r"""
+"""
+
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import SnmpManager, SnmpV2c, SnmpV3
+except ImportError:
+ HAS_PURITY_FB = False
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.9"
+
+
+def update_manager(module, blade):
+ """Update SNMP Manager"""
+ changed = False
+ try:
+ mgr = blade.snmp_managers.list_snmp_managers(names=[module.params["name"]])
+ except Exception:
+ module.fail_json(
+ msg="Failed to get configuration for SNMP manager {0}.".format(
+ module.params["name"]
+ )
+ )
+ current_attr = {
+ "community": mgr.items[0].v2c.community,
+ "notification": mgr.items[0].notification,
+ "host": mgr.items[0].host,
+ "version": mgr.items[0].version,
+ "auth_passphrase": mgr.items[0].v3.auth_passphrase,
+ "auth_protocol": mgr.items[0].v3.auth_protocol,
+ "privacy_passphrase": mgr.items[0].v3.privacy_passphrase,
+ "privacy_protocol": mgr.items[0].v3.privacy_protocol,
+ "user": mgr.items[0].v3.user,
+ }
+ new_attr = {
+ "community": module.params["community"],
+ "notification": module.params["notification"],
+ "host": module.params["host"],
+ "version": module.params["version"],
+ "auth_passphrase": module.params["auth_passphrase"],
+ "auth_protocol": module.params["auth_protocol"],
+ "privacy_passphrase": module.params["privacy_passphrase"],
+ "privacy_protocol": module.params["privacy_protocol"],
+ "user": module.params["user"],
+ }
+ if current_attr != new_attr:
+ changed = True
+ if not module.check_mode:
+ if new_attr["version"] == "v2c":
+ updated_v2c_attrs = SnmpV2c(community=new_attr["community"])
+ updated_v2c_manager = SnmpManager(
+ host=new_attr["host"],
+ notification=new_attr["notification"],
+ version="v2c",
+ v2c=updated_v2c_attrs,
+ )
+ try:
+ blade.snmp_managers.update_snmp_managers(
+ names=[module.params["name"]], snmp_manager=updated_v2c_manager
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to update v2c SNMP manager {0}.".format(
+ module.params["name"]
+ )
+ )
+ else:
+ updated_v3_attrs = SnmpV3(
+ auth_protocol=new_attr["auth_protocol"],
+ auth_passphrase=new_attr["auth_passphrase"],
+ privacy_protocol=new_attr["privacy_protocol"],
+ privacy_passphrase=new_attr["privacy_passphrase"],
+ user=new_attr["user"],
+ )
+ updated_v3_manager = SnmpManager(
+ host=new_attr["host"],
+ notification=new_attr["notification"],
+ version="v3",
+ v3=updated_v3_attrs,
+ )
+ try:
+ blade.snmp_managers.update_snmp_managers(
+ names=[module.params["name"]], snmp_manager=updated_v3_manager
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to update v3 SNMP manager {0}.".format(
+ module.params["name"]
+ )
+ )
+
+ module.exit_json(changed=changed)
+
+
+def delete_manager(module, blade):
+ """Delete SNMP Manager"""
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.snmp_managers.delete_snmp_managers(names=[module.params["name"]])
+ except Exception:
+ module.fail_json(
+ msg="Delete SNMP manager {0} failed".format(module.params["name"])
+ )
+ module.exit_json(changed=changed)
+
+
+def create_manager(module, blade):
+ """Create SNMP Manager"""
+ changed = True
+ if not module.check_mode:
+ if not module.params["version"]:
+ module.fail_json(msg="SNMP version required to create a new manager")
+ if module.params["version"] == "v2c":
+ v2_attrs = SnmpV2c(community=module.params["community"])
+ new_v2_manager = SnmpManager(
+ host=module.params["host"],
+ notification=module.params["notification"],
+ version="v2c",
+ v2c=v2_attrs,
+ )
+ try:
+ blade.snmp_managers.create_snmp_managers(
+ names=[module.params["name"]], snmp_manager=new_v2_manager
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to create v2c SNMP manager {0}.".format(
+ module.params["name"]
+ )
+ )
+ else:
+ v3_attrs = SnmpV3(
+ auth_protocol=module.params["auth_protocol"],
+ auth_passphrase=module.params["auth_passphrase"],
+ privacy_protocol=module.params["privacy_protocol"],
+ privacy_passphrase=module.params["privacy_passphrase"],
+ user=module.params["user"],
+ )
+ new_v3_manager = SnmpManager(
+ host=module.params["host"],
+ notification=module.params["notification"],
+ version="v3",
+ v3=v3_attrs,
+ )
+ try:
+ blade.snmp_managers.create_snmp_managers(
+ names=[module.params["name"]], snmp_manager=new_v3_manager
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to create v3 SNMP manager {0}.".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ host=dict(type="str"),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ user=dict(type="str"),
+ notification=dict(type="str", choices=["inform", "trap"], default="trap"),
+ auth_passphrase=dict(type="str", no_log=True),
+ auth_protocol=dict(type="str", choices=["MD5", "SHA"]),
+ privacy_passphrase=dict(type="str", no_log=True),
+ privacy_protocol=dict(type="str", choices=["AES", "DES"]),
+ version=dict(type="str", choices=["v2c", "v3"]),
+ community=dict(type="str"),
+ )
+ )
+
+ required_together = [
+ ["auth_passphrase", "auth_protocol"],
+ ["privacy_passphrase", "privacy_protocol"],
+ ]
+ required_if = [
+ ["version", "v2c", ["community", "host"]],
+ ["version", "v3", ["host", "user"]],
+ ]
+
+ module = AnsibleModule(
+ argument_spec,
+ required_together=required_together,
+ required_if=required_if,
+ supports_check_mode=True,
+ )
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ api_version = blade.api_version.list_versions().versions
+
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(msg="Purity//FB must be upgraded to support this module.")
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb SDK is required for this module")
+
+ mgr_configured = False
+ mgrs = blade.snmp_managers.list_snmp_managers()
+ for mgr in range(0, len(mgrs.items)):
+ if mgrs.items[mgr].name == module.params["name"]:
+ mgr_configured = True
+ break
+ if module.params["version"] == "v3":
+ if module.params["auth_passphrase"] and (
+ 8 > len(module.params["auth_passphrase"]) > 32
+ ):
+ module.fail_json(msg="auth_password must be between 8 and 32 characters")
+ if (
+ module.params["privacy_passphrase"]
+ and 8 > len(module.params["privacy_passphrase"]) > 63
+ ):
+ module.fail_json(msg="privacy_password must be between 8 and 63 characters")
+ if state == "absent" and mgr_configured:
+ delete_manager(module, blade)
+ elif mgr_configured and state == "present":
+ update_manager(module, blade)
+ elif not mgr_configured and state == "present":
+ create_manager(module, blade)
+ else:
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_subnet.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_subnet.py
new file mode 100644
index 000000000..7e3a35484
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_subnet.py
@@ -0,0 +1,347 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2017, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+---
+module: purefb_subnet
+version_added: "1.0.0"
+short_description: Manage network subnets in a Pure Storage FlashBlade
+description:
+ - This module manages network subnets on Pure Storage FlashBlade.
+author: Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - Subnet Name.
+ required: true
+ type: str
+ state:
+ description:
+ - Create, delete or modifies a subnet.
+ required: false
+ default: present
+ choices: [ "present", "absent" ]
+ type: str
+ lag:
+ description:
+ - Name of the Link Aggreation Group to use for the subnet.
+ default: uplink
+ type: str
+ version_added: "1.7.0"
+ gateway:
+ description:
+ - IPv4 or IPv6 address of subnet gateway.
+ required: false
+ type: str
+ mtu:
+ description:
+ - MTU size of the subnet. Range is 1280 to 9216.
+ required: false
+ default: 1500
+ type: int
+ prefix:
+ description:
+ - IPv4 or IPv6 address associated with the subnet.
+ - Supply the prefix length (CIDR) as well as the IP address.
+ - Required for subnet creation.
+ required: false
+ type: str
+ vlan:
+ description:
+ - VLAN ID of the subnet.
+ required: false
+ default: 0
+ type: int
+extends_documentation_fragment:
+ - purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = """
+- name: Create new network subnet named foo
+ purestorage.flashblade.purefb_subnet:
+ name: foo
+ prefix: "10.21.200.3/24"
+ gateway: 10.21.200.1
+ mtu: 9000
+ vlan: 2200
+ lag: bar
+ state: present
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Change configuration of existing subnet foo
+ purestorage.flashblade.purefb_subnet:
+ name: foo
+ state: present
+ prefix: "10.21.100.3/24"
+ gateway: 10.21.100.1
+ mtu: 1500
+ address: 10.21.200.123
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Delete network subnet named foo
+ purestorage.flashblade.purefb_subnet:
+ name: foo
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641"""
+
+RETURN = """
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import Subnet, Reference
+except ImportError:
+ HAS_PURITY_FB = False
+
+try:
+ import netaddr
+
+ HAS_NETADDR = True
+except ImportError:
+ HAS_NETADDR = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+MINIMUM_API_VERSION = "1.3"
+
+
+def get_subnet(module, blade):
+ """Return Subnet or None"""
+ subnet = []
+ subnet.append(module.params["name"])
+ try:
+ res = blade.subnets.list_subnets(names=subnet)
+ return res.items[0]
+ except Exception:
+ return None
+
+
+def create_subnet(module, blade):
+ """Create Subnet"""
+ changed = True
+ if not module.params["prefix"]:
+ module.fail_json(msg="prefix is required for subnet creation")
+ if not module.check_mode:
+ subnet = []
+ subnet.append(module.params["name"])
+ try:
+ if module.params["gateway"]:
+ blade.subnets.create_subnets(
+ names=subnet,
+ subnet=Subnet(
+ prefix=module.params["prefix"],
+ vlan=module.params["vlan"],
+ mtu=module.params["mtu"],
+ gateway=module.params["gateway"],
+ link_aggregation_group=Reference(name=module.params["lag"]),
+ ),
+ )
+ else:
+ blade.subnets.create_subnets(
+ names=subnet,
+ subnet=Subnet(
+ prefix=module.params["prefix"],
+ vlan=module.params["vlan"],
+ mtu=module.params["mtu"],
+ link_aggregation_group=Reference(name=module.params["lag"]),
+ ),
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to create subnet {0}. Confirm supplied parameters".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def modify_subnet(module, blade):
+ """Modify Subnet settings"""
+ changed = False
+ subnet = get_subnet(module, blade)
+ subnet_new = []
+ subnet_new.append(module.params["name"])
+ if module.params["prefix"]:
+ if module.params["prefix"] != subnet.prefix:
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.subnets.update_subnets(
+ names=subnet_new, subnet=Subnet(prefix=module.params["prefix"])
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to change subnet {0} prefix to {1}".format(
+ module.params["name"], module.params["prefix"]
+ )
+ )
+ if module.params["vlan"]:
+ if module.params["vlan"] != subnet.vlan:
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.subnets.update_subnets(
+ names=subnet_new, subnet=Subnet(vlan=module.params["vlan"])
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to change subnet {0} VLAN to {1}".format(
+ module.params["name"], module.params["vlan"]
+ )
+ )
+ if module.params["gateway"]:
+ if module.params["gateway"] != subnet.gateway:
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.subnets.update_subnets(
+ names=subnet_new,
+ subnet=Subnet(gateway=module.params["gateway"]),
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to change subnet {0} gateway to {1}".format(
+ module.params["name"], module.params["gateway"]
+ )
+ )
+ if module.params["mtu"]:
+ if module.params["mtu"] != subnet.mtu:
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.subnets.update_subnets(
+ names=subnet_new, subnet=Subnet(mtu=module.params["mtu"])
+ )
+ changed = True
+ except Exception:
+ module.fail_json(
+ msg="Failed to change subnet {0} MTU to {1}".format(
+ module.params["name"], module.params["mtu"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def delete_subnet(module, blade):
+ """Delete Subnet"""
+ changed = True
+ if not module.check_mode:
+ subnet = []
+ subnet.append(module.params["name"])
+ try:
+ blade.subnets.delete_subnets(names=subnet)
+ except Exception:
+ module.fail_json(
+ msg="Failed to delete subnet {0}".format(module.params["name"])
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(required=True),
+ state=dict(default="present", choices=["present", "absent"]),
+ gateway=dict(),
+ lag=dict(type="str", default="uplink"),
+ mtu=dict(type="int", default=1500),
+ prefix=dict(),
+ vlan=dict(type="int", default=0),
+ )
+ )
+
+ required_if = [["state", "present", ["prefix"]]]
+
+ module = AnsibleModule(
+ argument_spec, required_if=required_if, supports_check_mode=True
+ )
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ if not HAS_NETADDR:
+ module.fail_json(msg="netaddr module is required")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ api_version = blade.api_version.list_versions().versions
+ if MINIMUM_API_VERSION not in api_version:
+ module.fail_json(msg="Upgrade Purity//FB to enable this module")
+ subnet = get_subnet(module, blade)
+ try:
+ blade.link_aggregation_groups.list_link_aggregation_groups(
+ names=[module.params["lag"]]
+ )
+ except Exception:
+ module.fail_json(msg="LAG {0} does not exist.".format(module.params["lag"]))
+
+ if state == "present":
+ if not (1280 <= module.params["mtu"] <= 9216):
+ module.fail_json(
+ msg="MTU {0} is out of range (1280 to 9216)".format(
+ module.params["mtu"]
+ )
+ )
+ if not (0 <= module.params["vlan"] <= 4094):
+ module.fail_json(
+ msg="VLAN ID {0} is out of range (0 to 4094)".format(
+ module.params["vlan"]
+ )
+ )
+ if module.params["gateway"]:
+ if netaddr.IPAddress(module.params["gateway"]) not in netaddr.IPNetwork(
+ module.params["prefix"]
+ ):
+ module.fail_json(msg="Gateway and subnet are not compatible.")
+ subnets = blade.subnets.list_subnets()
+ nrange = netaddr.IPSet([module.params["prefix"]])
+ for sub in range(0, len(subnets.items)):
+ if (
+ subnets.items[sub].vlan == module.params["vlan"]
+ and subnets.items[sub].name != module.params["name"]
+ ):
+ module.fail_json(
+ msg="VLAN ID {0} is already in use.".format(module.params["vlan"])
+ )
+ if (
+ nrange & netaddr.IPSet([subnets.items[sub].prefix])
+ and subnets.items[sub].name != module.params["name"]
+ ):
+ module.fail_json(msg="Prefix CIDR overlaps with existing subnet.")
+
+ if state == "present" and not subnet:
+ create_subnet(module, blade)
+ elif state == "present" and subnet:
+ modify_subnet(module, blade)
+ elif state == "absent" and subnet:
+ delete_subnet(module, blade)
+ elif state == "absent" and not subnet:
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_syslog.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_syslog.py
new file mode 100644
index 000000000..2a7406418
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_syslog.py
@@ -0,0 +1,198 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2020, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_syslog
+version_added: '1.4.0'
+short_description: Configure Pure Storage FlashBlade syslog settings
+description:
+- Configure syslog configuration for Pure Storage FlashBlades.
+- Add or delete an individual syslog server to the existing
+ list of serves.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - Unique identifier for the syslog server address
+ type: str
+ required: true
+ state:
+ description:
+ - Create or delete syslog servers configuration
+ default: present
+ type: str
+ choices: [ absent, present ]
+ protocol:
+ description:
+ - Protocol which server uses
+ type: str
+ choices: [ tcp, tls, udp ]
+ port:
+ description:
+ - Port at which the server is listening. If no port is specified
+ the system will use 514
+ type: str
+ address:
+ description:
+ - Syslog server address.
+ This field supports IPv4 or FQDN.
+ An invalid IP addresses will cause the module to fail.
+ No validation is performed for FQDNs.
+ type: str
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Delete exisitng syslog server entries
+ purestorage.flashblade.purefb_syslog:
+ name: syslog1
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Set array syslog servers
+ purestorage.flashblade.purefb_syslog:
+ state: present
+ name: syslog1
+ address: syslog1.com
+ protocol: udp
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+"""
+
+RETURN = r"""
+"""
+
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import SyslogServerPostOrPatch
+except ImportError:
+ HAS_PURITY_FB = False
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.10"
+
+
+def delete_syslog(module, blade):
+ """Delete Syslog Server"""
+ changed = False
+ try:
+ server = blade.syslog.list_syslog_servers(names=[module.params["name"]])
+ except Exception:
+ server = None
+
+ if server:
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.syslog.delete_syslog_servers(names=[module.params["name"]])
+ changed = True
+ except Exception:
+ module.fail_json(
+ msg="Failed to remove syslog server: {0}".format(
+ module.params["name"]
+ )
+ )
+
+ module.exit_json(changed=changed)
+
+
+def add_syslog(module, blade):
+ """Add Syslog Server"""
+ changed = False
+ noport_address = module.params["protocol"] + "://" + module.params["address"]
+
+ if module.params["port"]:
+ full_address = noport_address + ":" + module.params["port"]
+ else:
+ full_address = noport_address
+
+ address_list = blade.syslog.list_syslog_servers()
+ if len(address_list.items) == 3:
+ module.fail_json(msg="Maximum number of syslog servers (3) already configured.")
+ exists = False
+
+ if address_list:
+ for address in range(0, len(address_list.items)):
+ if address_list.items[address].name == module.params["name"]:
+ exists = True
+ break
+ if not exists:
+ changed = True
+ if not module.check_mode:
+ try:
+ attr = SyslogServerPostOrPatch(uri=full_address)
+ blade.syslog.create_syslog_servers(
+ syslog=attr, names=[module.params["name"]]
+ )
+ changed = True
+ except Exception:
+ module.fail_json(
+ msg="Failed to add syslog server {0} - {1}".format(
+ module.params["name"], full_address
+ )
+ )
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ address=dict(type="str"),
+ protocol=dict(type="str", choices=["tcp", "tls", "udp"]),
+ port=dict(type="str"),
+ name=dict(type="str", required=True),
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ )
+ )
+
+ required_if = [["state", "present", ["address", "protocol"]]]
+
+ module = AnsibleModule(
+ argument_spec, required_if=required_if, supports_check_mode=True
+ )
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ blade = get_blade(module)
+ api_version = blade.api_version.list_versions().versions
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(msg="Purity//FB must be upgraded to support this module.")
+
+ if module.params["state"] == "absent":
+ delete_syslog(module, blade)
+ else:
+ add_syslog(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_target.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_target.py
new file mode 100644
index 000000000..ab37bfda3
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_target.py
@@ -0,0 +1,201 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2020, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_target
+version_added: '1.0.0'
+short_description: Manage remote S3-capable targets for a FlashBlade
+description:
+- Manage remote S3-capable targets for a FlashBlade system
+- Use this for non-FlashBlade targets.
+- Use I(purestorage.flashblade.purefb_connect) for FlashBlade targets.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Create or delete remote target
+ default: present
+ type: str
+ choices: [ absent, present ]
+ name:
+ description:
+ - Name of S3-capable target (IP or FQDN)
+ type: str
+ required: true
+ address:
+ description:
+ - Address of S3-capable target (IP or FQDN)
+ type: str
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Create a connection to remote S3-capable target
+ purestorage.flashblade.purefb_target:
+ name: target_1
+ address: 10.10.10.20
+ fb_url: 10.10.10.2
+ api_token: T-89faa581-c668-483d-b77d-23c5d88ba35c
+- name: Delete connection to remote S3-capable system
+ purestorage.flashblade.purefb_target:
+ state: absent
+ name: target_1
+ target_api: 9c0b56bc-f941-f7a6-9f85-dcc3e9a8f7d6
+ fb_url: 10.10.10.2
+ api_token: T-89faa581-c668-483d-b77d-23c5d88ba35c
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURITYFB = True
+try:
+ from purity_fb import TargetPost, Target
+except ImportError:
+ HAS_PURITYFB = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+MINIMUM_API_VERSION = "1.9"
+
+
+def _check_replication_configured(module, blade):
+ interfaces = blade.network_interfaces.list_network_interfaces()
+ repl_ok = False
+ for link in range(0, len(interfaces.items)):
+ if "replication" in interfaces.items[link].services:
+ repl_ok = True
+ if not repl_ok:
+ module.fail_json(
+ msg="Replication network interface required to configure a target"
+ )
+
+
+def _check_connected(module, blade):
+ connected_targets = blade.targets.list_targets()
+ for target in range(0, len(connected_targets.items)):
+ if connected_targets.items[target].name == module.params["name"]:
+ return connected_targets.items[target]
+ return None
+
+
+def break_connection(module, blade):
+ """Break connection to remote target"""
+ changed = True
+ if not module.check_mode:
+ try:
+ blade.targets.delete_targets(names=[module.params["name"]])
+ except Exception:
+ module.fail_json(
+ msg="Failed to disconnect target {0}.".format(module.params["name"])
+ )
+ module.exit_json(changed=changed)
+
+
+def create_connection(module, blade):
+ """Create connection to remote target"""
+ changed = True
+ if not module.check_mode:
+ connected_targets = blade.targets.list_targets()
+ for target in range(0, len(connected_targets.items)):
+ if connected_targets.items[target].address == module.params["address"]:
+ module.fail_json(
+ msg="Target already exists with same connection address"
+ )
+ try:
+ target = TargetPost(address=module.params["address"])
+ blade.targets.create_targets(names=[module.params["name"]], target=target)
+ except Exception:
+ module.fail_json(
+ msg="Failed to connect to remote target {0}.".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def update_connection(module, blade, connection):
+ """Update target connection address"""
+ changed = False
+ connected_targets = blade.targets.list_targets()
+ for target in range(0, len(connected_targets.items)):
+ if (
+ connected_targets.items[target].address == module.params["address"]
+ and connected_targets.items[target].name != module.params["name"]
+ ):
+ module.fail_json(msg="Target already exists with same connection address")
+ if module.params["address"] != connection.address:
+ changed = True
+ if not module.check_mode:
+ new_address = Target(
+ name=module.params["name"], address=module.params["address"]
+ )
+ try:
+ blade.targets.update_targets(
+ names=[connection.name], target=new_address
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to change address for target {0}.".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ name=dict(type="str", required=True),
+ address=dict(type="str"),
+ )
+ )
+
+ required_if = [["state", "present", ["address"]]]
+
+ module = AnsibleModule(
+ argument_spec, required_if=required_if, supports_check_mode=True
+ )
+
+ if not HAS_PURITYFB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ _check_replication_configured(module, blade)
+ target = _check_connected(module, blade)
+ if state == "present" and not target:
+ create_connection(module, blade)
+ elif state == "present" and target:
+ update_connection(module, blade, target)
+ elif state == "absent" and target:
+ break_connection(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_timeout.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_timeout.py
new file mode 100644
index 000000000..21e83c002
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_timeout.py
@@ -0,0 +1,136 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2021, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_timeout
+version_added: '1.6.0'
+short_description: Configure Pure Storage FlashBlade GUI idle timeout
+description:
+- Configure GUI idle timeout for Pure Storage FlashBlade.
+- This does not affect existing GUI sessions.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ state:
+ description:
+ - Set or disable the GUI idle timeout
+ default: present
+ type: str
+ choices: [ present, absent ]
+ timeout:
+ description:
+ - Minutes for idle timeout.
+ type: int
+ default: 30
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Set GUI idle timeout to 25 minutes
+ purestorage.flashblade.purefb_timeout:
+ timeout: 25
+ state: present
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+
+- name: Disable idle timeout
+ purestorage.flashblade.purefb_timeout:
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURESTORAGE = True
+try:
+ from pypureclient import flashblade
+except ImportError:
+ HAS_PURESTORAGE = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_system,
+ purefb_argument_spec,
+)
+
+MIN_REQUIRED_API_VERSION = "2.0"
+
+
+def set_timeout(module, blade):
+ """Set GUI idle timeout"""
+ changed = True
+ if not module.check_mode:
+ res = blade.patch_arrays(
+ flashblade.Array(idle_timeout=module.params["timeout"] * 60000)
+ )
+ if res.status_code != 200:
+ module.fail_json(msg="Failed to set GUI idle timeout")
+
+ module.exit_json(changed=changed)
+
+
+def disable_timeout(module, blade):
+ """Disable idle timeout"""
+ changed = True
+ if not module.check_mode:
+ res = blade.patch_arrays(flashblade.Array(idle_timeout=0))
+ if res.status_code != 200:
+ module.fail_json(msg="Failed to disable GUI idle timeout")
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ timeout=dict(type="int", default=30),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ if not HAS_PURESTORAGE:
+ module.fail_json(msg="py-pure-client sdk is required for this module")
+
+ blade = get_system(module)
+ api_version = list(blade.get_versions().items)
+
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(
+ msg="FlashBlade REST version not supported. "
+ "Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
+ )
+
+ state = module.params["state"]
+ if 5 < module.params["timeout"] > 180 and module.params["timeout"] != 0:
+ module.fail_json(msg="Timeout value must be between 5 and 180 minutes")
+ blade = get_system(module)
+ current_timeout = list(blade.get_arrays().items)[0].idle_timeout * 60000
+ if state == "present" and current_timeout != module.params["timeout"]:
+ set_timeout(module, blade)
+ elif state == "absent" and current_timeout != 0:
+ disable_timeout(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_tz.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_tz.py
new file mode 100644
index 000000000..9f6acc5d4
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_tz.py
@@ -0,0 +1,206 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2021, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_tz
+version_added: '1.10.0'
+short_description: Configure Pure Storage FlashBlade timezone
+description:
+- Configure the timezone for a Pure Storage FlashBlade.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ timezone:
+ description:
+ - If not provided, the module will attempt to get the current local timezone from the server
+ type: str
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Set FlashBlade Timezone to Americas/Los_Angeles
+ purestorage.flashblade.purefb_tz:
+ timezone: "America/Los_Angeles"
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURESTORAGE = True
+try:
+ from pypureclient import flashblade
+except ImportError:
+ HAS_PURESTORAGE = False
+
+HAS_PYTZ = True
+try:
+ import pytz
+except ImportError:
+ HAS_PYTX = False
+
+import os
+import re
+import platform
+
+from ansible.module_utils.common.process import get_bin_path
+from ansible.module_utils.facts.utils import get_file_content
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_system,
+ purefb_argument_spec,
+)
+
+MIN_REQUIRED_API_VERSION = "2.0"
+
+
+def _findstr(text, match):
+ for line in text.splitlines():
+ if match in line:
+ found = line
+ return found
+
+
+def _get_local_tz(module, timezone="UTC"):
+ """
+ We will attempt to get the local timezone of the server running the module and use that.
+ If we can't get the timezone then we will set the default to be UTC
+
+ Linnux has been tested and other opersting systems should be OK.
+ Failures cause assumption of UTC
+
+ Windows is not supported and will assume UTC
+ """
+ if platform.system() == "Linux":
+ timedatectl = get_bin_path("timedatectl")
+ if timedatectl is not None:
+ rcode, stdout, stderr = module.run_command(timedatectl)
+ if rcode == 0 and stdout:
+ line = _findstr(stdout, "Time zone")
+ full_tz = line.split(":", 1)[1].rstrip()
+ timezone = full_tz.split()[0]
+ return timezone
+ else:
+ module.warn("Incorrect timedatectl output. Timezone will be set to UTC")
+ else:
+ if os.path.exists("/etc/timezone"):
+ timezone = get_file_content("/etc/timezone")
+ else:
+ module.warn("Could not find /etc/timezone. Assuming UTC")
+
+ elif platform.system() == "SunOS":
+ if os.path.exists("/etc/default/init"):
+ for line in get_file_content("/etc/default/init", "").splitlines():
+ if line.startswith("TZ="):
+ timezone = line.split("=", 1)[1]
+ return timezone
+ else:
+ module.warn("Could not find /etc/default/init. Assuming UTC")
+
+ elif re.match("^Darwin", platform.platform()):
+ systemsetup = get_bin_path("systemsetup")
+ if systemsetup is not None:
+ rcode, stdout, stderr = module.execute(systemsetup, "-gettimezone")
+ if rcode == 0 and stdout:
+ timezone = stdout.split(":", 1)[1].lstrip()
+ else:
+ module.warn("Could not run systemsetup. Assuming UTC")
+ else:
+ module.warn("Could not find systemsetup. Assuming UTC")
+
+ elif re.match("^(Free|Net|Open)BSD", platform.platform()):
+ if os.path.exists("/etc/timezone"):
+ timezone = get_file_content("/etc/timezone")
+ else:
+ module.warn("Could not find /etc/timezone. Assuming UTC")
+
+ elif platform.system() == "AIX":
+ aix_oslevel = int(platform.version() + platform.release())
+ if aix_oslevel >= 61:
+ if os.path.exists("/etc/environment"):
+ for line in get_file_content("/etc/environment", "").splitlines():
+ if line.startswith("TZ="):
+ timezone = line.split("=", 1)[1]
+ return timezone
+ else:
+ module.warn("Could not find /etc/environment. Assuming UTC")
+ else:
+ module.warn(
+ "Cannot determine timezone when AIX os level < 61. Assuming UTC"
+ )
+
+ else:
+ module.warn("Could not find /etc/timezone. Assuming UTC")
+
+ return timezone
+
+
+def set_timezone(module, blade):
+ """Set timezone"""
+ changed = True
+ if not module.check_mode:
+ res = blade.patch_arrays(flashblade.Array(time_zone=module.params["timezone"]))
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to timezone. Error: {0}".format(res.errors[0].message)
+ )
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ timezone=dict(type="str"),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ if not HAS_PURESTORAGE:
+ module.fail_json(msg="py-pure-client sdk is required for this module")
+ if not HAS_PYTZ:
+ module.fail_json(msg="pytz is required for this module")
+
+ blade = get_system(module)
+ api_version = list(blade.get_versions().items)
+
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(
+ msg="FlashBlade REST version not supported. "
+ "Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
+ )
+ if not module.params["timezone"]:
+ module.params["timezone"] = _get_local_tz(module)
+ if module.params["timezone"] not in pytz.all_timezones_set:
+ module.fail_json(
+ msg="Timezone {0} is not valid".format(module.params["timezone"])
+ )
+
+ blade = get_system(module)
+ current_tz = list(blade.get_arrays().items)[0].time_zone
+ if current_tz != module.params["timezone"]:
+ set_timezone(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_user.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_user.py
new file mode 100644
index 000000000..ffa34be8e
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_user.py
@@ -0,0 +1,211 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2019, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_user
+version_added: '1.0.0'
+short_description: Modify FlashBlade user accounts
+description:
+- Modify user on a Pure Stoage FlashBlade.
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - The name of the user account
+ type: str
+ password:
+ description:
+ - Password for the local user.
+ - Only applies to the local user 'pureuser'
+ type: str
+ old_password:
+ description:
+ - If changing an existing password, you must provide the old password for security
+ - Only applies to the local user 'pureuser'
+ type: str
+ public_key:
+ description:
+ - The API clients PEM formatted (Base64 encoded) RSA public key.
+ - Include the I(—–BEGIN PUBLIC KEY—–) and I(—–END PUBLIC KEY—–) lines
+ type: str
+ version_added: "1.8.0"
+ clear_lock:
+ description:
+ - Clear user lockout flag
+ type: bool
+ default: false
+ version_added: "1.8.0"
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Change password for local user (NOT IDEMPOTENT)
+ purestorage.flashblade.purefb_user:
+ name: pureuser
+ password: anewpassword
+ old_password: apassword
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+
+- name: Set public key for user
+ purestorage.flashblade.purefb_user:
+ name: fred
+ public_key: "{{lookup('file', 'public_pem_file') }}"
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+
+- name: Clear user lockout
+ purestorage.flashblade.purefb_user:
+ name: fred
+ clear_lock: true
+ fb_url: 10.10.10.2
+ api_token: T-9f276a18-50ab-446e-8a0c-666a3529a1b6
+"""
+
+RETURN = r"""
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import Admin
+except ImportError:
+ HAS_PURITY_FB = False
+
+HAS_PURESTORAGE = True
+try:
+ from pypureclient.flashblade import AdminPatch
+except ImportError:
+ HAS_PURESTORAGE = False
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ get_system,
+ purefb_argument_spec,
+)
+
+MIN_REQUIRED_API_VERSION = "1.3"
+MIN_KEY_API_VERSION = "2.1"
+MIN_LOCK_API_VERSION = "2.3"
+
+
+def update_user(module, blade):
+ """Create or Update Local User Account"""
+ changed = False
+ if module.params["password"] and module.params["name"].lower() == "pureuser":
+ if module.params["password"] != module.params["old_password"]:
+ changed = True
+ if not module.check_mode:
+ try:
+ new_admin = Admin()
+ new_admin.password = module.params["password"]
+ new_admin.old_password = module.params["old_password"]
+ blade.admins.update_admins(names=["pureuser"], admin=new_admin)
+ except Exception:
+ module.fail_json(
+ msg="Local User {0}: Password reset failed. "
+ "Check passwords. One of these is incorrect.".format(
+ module.params["name"]
+ )
+ )
+ else:
+ module.fail_json(
+ msg="Local User Account {0}: Password change failed - "
+ "Old and new passwords are the same".format(module.params["name"])
+ )
+ if module.params["password"] and module.params["name"].lower() != "pureuser":
+ module.fail_json(msg="Changing password for remote accounts is not supported.")
+ api_version = blade.api_version.list_versions().versions
+ if MIN_KEY_API_VERSION in api_version:
+ bladev2 = get_system(module)
+ try:
+ user_data = list(bladev2.get_admins(names=[module.params["name"]]).items)[0]
+ except AttributeError:
+ module.fail_json(
+ msg="User {0} does not currently exist in the FlashBlade. "
+ "Please login to this user before attempting to modify it.".format(
+ module.params["name"]
+ )
+ )
+ current_key = user_data.public_key
+ if module.params["public_key"] and current_key != module.params["public_key"]:
+ changed = True
+ if not module.check_mode:
+ my_admin = AdminPatch(public_key=module.params["public_key"])
+ res = bladev2.patch_admins(
+ names=[module.params["name"]], admin=my_admin
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to change public_key for {0}.".format(
+ module.params["name"]
+ )
+ )
+ if MIN_LOCK_API_VERSION in api_version:
+ if user_data.locked and module.params["clear_lock"]:
+ changed = True
+ if not module.check_mode:
+ my_admin = AdminPatch(locked=False)
+ res = bladev2.patch_admins(
+ names=[module.params["name"]], admin=my_admin
+ )
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to unlock user {0}.".format(
+ module.params["name"]
+ )
+ )
+
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str"),
+ public_key=dict(type="str", no_log=True),
+ password=dict(type="str", no_log=True),
+ old_password=dict(type="str", no_log=True),
+ clear_lock=dict(type="bool", default=False),
+ )
+ )
+
+ required_together = [["password", "old_password"]]
+ module = AnsibleModule(
+ argument_spec, supports_check_mode=True, required_together=required_together
+ )
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+ if not HAS_PURESTORAGE and module.params["public_key"]:
+ module.fail_json(msg="py-pure-client sdk is required for to set public keys")
+
+ blade = get_blade(module)
+ api_version = blade.api_version.list_versions().versions
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(msg="Purity//FB must be upgraded to support this module.")
+
+ update_user(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_userpolicy.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_userpolicy.py
new file mode 100644
index 000000000..6e7dbe49d
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_userpolicy.py
@@ -0,0 +1,269 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2021, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_userpolicy
+version_added: '1.6.0'
+short_description: Manage FlashBlade Object Store User Access Policies
+description:
+- Add or Remove FlashBlade Object Store Access Policies for Account User
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - Name of the Object Store User
+ - The user to have the policy request applied to
+ type: str
+ account:
+ description:
+ - Name of the Object Store Account associated with the user
+ type: str
+ state:
+ description:
+ - Define whether the Access Policy should be added or deleted
+ - Option to list all available policies
+ default: present
+ choices: [ absent, present, show ]
+ type: str
+ policy:
+ description:
+ - Policies to added or deleted from the Object Store User
+ - Only valid policies can be used
+ - use I(list) to see available policies
+ type: list
+ elements: str
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: List existng ruser access policies for a specific user
+ purestorage.flashblade.purefb_userpolicy:
+ state: show
+ account: foo
+ name: bar
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+ register: policy_list
+
+- name: List all available user access policies
+ purestorage.flashblade.purefb_userpolicy:
+ state: show
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+ register: policy_list
+
+- name: Add user access policies to account user foo/bar
+ purestorage.flashblade.purefb_userpolicy:
+ name: bar
+ account: foo
+ policy:
+ - pure:policy/bucket-create
+ - pure:policy/bucket-delete
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+
+- name: Delete user access policies to account user foo/bar
+ purestorage.flashblade.purefb_userpolicy:
+ name: bar
+ account: foo
+ policy:
+ - pure:policy/bucket-create
+ - pure:policy/bucket-delete
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+"""
+
+RETURN = r"""
+policy_list:
+ description:
+ - Returns the list of access policies for a user
+ - If no user specified returns all available access policies
+ returned: always
+ type: list
+ elements: str
+ sample: ['pure:policy/object-list', 'pure:policy/bucket-list', 'pure:policy/object-read', 'pure:policy/bucket-delete', 'pure:policy/full-access']
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_system,
+ purefb_argument_spec,
+)
+
+MIN_REQUIRED_API_VERSION = "2.0"
+
+
+def _check_valid_policy(blade, policy):
+ try:
+ return bool(blade.get_object_store_access_policies(names=[policy]))
+ except AttributeError:
+ return False
+
+
+def add_policy(module, blade):
+ """Add a single or list of policies to an account user"""
+ changed = False
+ user_policy_list = []
+ policy_list = module.params["policy"]
+ for policy in range(0, len(policy_list)):
+ if not _check_valid_policy(blade, policy_list[policy]):
+ module.fail_json(msg="Policy {0} is not valid.".format(policy_list[policy]))
+ username = module.params["account"] + "/" + module.params["name"]
+ for policy in range(0, len(policy_list)):
+ if not (
+ blade.get_object_store_users_object_store_access_policies(
+ member_names=[username], policy_names=[policy_list[policy]]
+ ).items
+ ):
+ if not module.check_mode:
+ changed = True
+ res = blade.post_object_store_access_policies_object_store_users(
+ member_names=[username], policy_names=[policy_list[policy]]
+ )
+ user_policies = list(
+ blade.get_object_store_access_policies_object_store_users(
+ member_names=[username]
+ ).items
+ )
+ for user_policy in range(0, len(user_policies)):
+ user_policy_list.append(user_policies[user_policy].policy.name)
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to add policy {0} to account user {1}. Error: {2}".format(
+ policy_list[policy], username, res.errors[0].message
+ )
+ )
+ module.exit_json(changed=changed, policy_list=user_policy_list)
+
+
+def remove_policy(module, blade):
+ """Remove a single or list of policies to an account user"""
+ changed = False
+ user_policy_list = []
+ policy_list = module.params["policy"]
+ for policy in range(0, len(policy_list)):
+ if not _check_valid_policy(blade, policy):
+ module.fail_json(msg="Policy {0} is not valid.".format(policy))
+ username = module.params["account"] + "/" + module.params["name"]
+ for policy in range(0, len(policy_list)):
+ if (
+ blade.get_object_store_users_object_store_access_policies(
+ member_names=[username], policy_names=[policy_list[policy]]
+ ).total_item_count
+ == 1
+ ):
+ if not module.check_mode:
+ changed = True
+ res = blade.delete_object_store_access_policies_object_store_users(
+ member_names=[username], policy_names=[policy_list[policy]]
+ )
+ user_policies = list(
+ blade.get_object_store_access_policies_object_store_users(
+ member_names=[username]
+ ).items
+ )
+ for user_policy in range(0, len(user_policies)):
+ user_policy_list.append(user_policies[user_policy].policy.name)
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to remove policy {0} from account user {1}. Error: {2}".format(
+ policy_list[policy], username, res.errors[0].message
+ )
+ )
+ module.exit_json(changed=changed, policy_list=user_policy_list)
+
+
+def list_policy(module, blade):
+ """List Object Store User Access Policies"""
+ changed = True
+ policy_list = []
+ if not module.check_mode:
+ if module.params["account"] and module.params["name"]:
+ username = module.params["account"] + "/" + module.params["name"]
+ user_policies = list(
+ blade.get_object_store_access_policies_object_store_users(
+ member_names=[username]
+ ).items
+ )
+ for user_policy in range(0, len(user_policies)):
+ policy_list.append(user_policies[user_policy].policy.name)
+ else:
+ policies = blade.get_object_store_access_policies()
+ p_list = list(policies.items)
+ if policies.status_code != 200:
+ module.fail_json(msg="Failed to get Object Store User Access Policies")
+ for policy in range(0, len(p_list)):
+ policy_list.append(p_list[policy].name)
+ module.exit_json(changed=changed, policy_list=policy_list)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(
+ type="str", default="present", choices=["absent", "present", "show"]
+ ),
+ name=dict(type="str"),
+ account=dict(type="str"),
+ policy=dict(type="list", elements="str"),
+ )
+ )
+ required_if = [
+ ["state", "present", ["name", "account", "policy"]],
+ ["state", "absent", ["name", "account", "policy"]],
+ ]
+ module = AnsibleModule(
+ argument_spec, required_if=required_if, supports_check_mode=True
+ )
+
+ blade = get_system(module)
+ api_version = list(blade.get_versions().items)
+
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(
+ msg="FlashBlade REST version not supported. "
+ "Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
+ )
+
+ state = module.params["state"]
+ if (
+ blade.get_object_store_users(
+ names=[module.params["account"] + "/" + module.params["name"]]
+ ).status_code
+ != 200
+ ):
+ module.fail_json(
+ msg="Account User {0}/{1} does not exist".format(
+ module.params["account"], module.params["name"]
+ )
+ )
+ if state == "show":
+ list_policy(module, blade)
+ elif state == "present":
+ add_policy(module, blade)
+ elif state == "absent":
+ remove_policy(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_userquota.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_userquota.py
new file mode 100644
index 000000000..74361f2ea
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_userquota.py
@@ -0,0 +1,315 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2021, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+
+DOCUMENTATION = """
+---
+module: purefb_userquota
+version_added: "1.7.0"
+short_description: Manage filesystem user quotas
+description:
+ - This module manages user quotas for filesystems on Pure Storage FlashBlade.
+author: Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - Filesystem Name.
+ required: true
+ type: str
+ state:
+ description:
+ - Create, delete or modifies a quota.
+ required: false
+ default: present
+ type: str
+ choices: [ "present", "absent" ]
+ quota:
+ description:
+ - User quota in M, G, T or P units. This cannot be 0.
+ - This value will override the file system's default user quota.
+ type: str
+ uid:
+ description:
+ - The user id on which the quota is enforced.
+ - Cannot be combined with I(uname)
+ type: int
+ uname:
+ description:
+ - The user name on which the quota is enforced.
+ - Cannot be combined with I(uid)
+ type: str
+extends_documentation_fragment:
+ - purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = """
+- name: Create new user (using UID) quota for filesystem named foo
+ purestorage.flashblade.purefb_userquota:
+ name: foo
+ quota: 1T
+ uid: 1234
+ state: present
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Create new user (using username) quota for filesystem named foo
+ purestorage.flashblade.purefb_userquota:
+ name: foo
+ quota: 1T
+ uname: bar
+ state: present
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Delete user quota on filesystem foo for user by UID
+ purestorage.flashblade.purefb_userquota:
+ name: foo
+ uid: 1234
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Delete user quota on filesystem foo for user by username
+ purestorage.flashblade.purefb_userquota:
+ name: foo
+ uname: bar
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Update user quota on filesystem foo for user by username
+ purestorage.flashblade.purefb_userquota:
+ name: foo
+ quota: 20G
+ uname: bar
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641
+
+- name: Update user quota on filesystem foo for user by UID
+ purestorage.flashblade.purefb_userquota:
+ name: foo
+ quota: 20G
+ uid: bar
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641"""
+
+RETURN = """
+"""
+
+HAS_PURITY_FB = True
+try:
+ from purity_fb import QuotasUser
+except ImportError:
+ HAS_PURITY_FB = False
+
+from ansible.module_utils.basic import AnsibleModule, human_to_bytes
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_blade,
+ purefb_argument_spec,
+)
+
+
+MIN_REQUIRED_API_VERSION = "1.6"
+
+
+def get_fs(module, blade):
+ """Return Filesystem or None"""
+ fsys = []
+ fsys.append(module.params["name"])
+ try:
+ res = blade.file_systems.list_file_systems(names=fsys)
+ return res.items[0]
+ except Exception:
+ return None
+
+
+def get_quota(module, blade):
+ """Return Filesystem User Quota or None"""
+ fsys = []
+ fsys.append(module.params["name"])
+ try:
+ if module.params["uid"]:
+ res = blade.quotas_users.list_user_quotas(
+ file_system_names=fsys, filter="user.id=" + str(module.params["uid"])
+ )
+ else:
+ res = blade.quotas_users.list_user_quotas(
+ file_system_names=fsys,
+ filter="user.name='" + module.params["uname"] + "'",
+ )
+ return res.items[0]
+ except Exception:
+ return None
+
+
+def create_quota(module, blade):
+ """Create Filesystem User Quota"""
+ changed = True
+ quota = int(human_to_bytes(module.params["quota"]))
+ if not module.check_mode:
+ try:
+ if module.params["uid"]:
+ blade.quotas_users.create_user_quotas(
+ file_system_names=[module.params["name"]],
+ uids=[module.params["uid"]],
+ quota=QuotasUser(quota=quota),
+ )
+ else:
+ blade.quotas_users.create_user_quotas(
+ file_system_names=[module.params["name"]],
+ user_names=[module.params["uname"]],
+ quota=QuotasUser(quota=quota),
+ )
+ except Exception:
+ if module.params["uid"]:
+ module.fail_json(
+ msg="Failed to create quote for UID {0} on filesystem {1}.".format(
+ module.params["uid"], module.params["name"]
+ )
+ )
+ else:
+ module.fail_json(
+ msg="Failed to create quote for username {0} on filesystem {1}.".format(
+ module.params["uname"], module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def update_quota(module, blade):
+ """Upodate Filesystem User Quota"""
+ changed = False
+ current_quota = get_quota(module, blade)
+ quota = int(human_to_bytes(module.params["quota"]))
+ if current_quota.quota != quota:
+ changed = True
+ if not module.check_mode:
+ if module.params["uid"]:
+ try:
+ blade.quotas_users.update_user_quotas(
+ file_system_names=[module.params["name"]],
+ uids=[module.params["uid"]],
+ quota=QuotasUser(quota=quota),
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to update quota for UID {0} on filesystem {1}.".format(
+ module.params["uid"], module.params["name"]
+ )
+ )
+ else:
+ try:
+ blade.quotas_users.update_user_quotas(
+ file_system_names=[module.params["name"]],
+ user_names=[module.params["uname"]],
+ quota=QuotasUser(quota=quota),
+ )
+ except Exception:
+ module.fail_json(
+ msg="Failed to update quota for UID {0} on filesystem {1}.".format(
+ module.params["uname"], module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def delete_quota(module, blade):
+ """Delete Filesystem User Quota"""
+ changed = True
+ if not module.check_mode:
+ try:
+ if module.params["uid"]:
+ blade.quotas_users.delete_user_quotas(
+ file_system_names=[module.params["name"]],
+ uids=[module.params["uid"]],
+ )
+ else:
+ blade.quotas_users.delete_user_quotas(
+ file_system_names=[module.params["name"]],
+ user_names=[module.params["uname"]],
+ )
+ except Exception:
+ if module.params["uid"]:
+ module.fail_json(
+ msg="Failed to delete quota for UID {0} on filesystem {1}.".format(
+ module.params["uid"], module.params["name"]
+ )
+ )
+ else:
+ module.fail_json(
+ msg="Failed to delete quota for username {0} on filesystem {1}.".format(
+ module.params["uname"], module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ name=dict(type="str", required=True),
+ uid=dict(type="int"),
+ uname=dict(type="str"),
+ state=dict(default="present", choices=["present", "absent"]),
+ quota=dict(type="str"),
+ )
+ )
+
+ mutually_exclusive = [["uid", "uname"]]
+ required_if = [["state", "present", ["quota"]]]
+ module = AnsibleModule(
+ argument_spec,
+ mutually_exclusive=mutually_exclusive,
+ required_if=required_if,
+ supports_check_mode=True,
+ )
+
+ if not HAS_PURITY_FB:
+ module.fail_json(msg="purity_fb sdk is required for this module")
+
+ state = module.params["state"]
+ blade = get_blade(module)
+ versions = blade.api_version.list_versions().versions
+
+ if MIN_REQUIRED_API_VERSION not in versions:
+ module.fail_json(
+ msg="Minimum FlashBlade REST version required: {0}".format(
+ MIN_REQUIRED_API_VERSION
+ )
+ )
+ fsys = get_fs(module, blade)
+ if not fsys:
+ module.fail_json(
+ msg="Filesystem {0} does not exist.".format(module.params["name"])
+ )
+ quota = get_quota(module, blade)
+
+ if state == "present" and not quota:
+ create_quota(module, blade)
+ elif state == "present" and quota:
+ update_quota(module, blade)
+ elif state == "absent" and quota:
+ delete_quota(module, blade)
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/purestorage/flashblade/plugins/modules/purefb_virtualhost.py b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_virtualhost.py
new file mode 100644
index 000000000..7f4a2310e
--- /dev/null
+++ b/ansible_collections/purestorage/flashblade/plugins/modules/purefb_virtualhost.py
@@ -0,0 +1,142 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2021, Simon Dodsley (simon@purestorage.com)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+ "metadata_version": "1.1",
+ "status": ["preview"],
+ "supported_by": "community",
+}
+
+DOCUMENTATION = r"""
+---
+module: purefb_virtualhost
+version_added: '1.6.0'
+short_description: Manage FlashBlade Object Store Virtual Hosts
+description:
+- Add or delete FlashBlade Object Store Virtual Hosts
+author:
+- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
+options:
+ name:
+ description:
+ - Name of the Object Store Virtual Host
+ - A hostname or domain by which the array can be addressed for virtual
+ hosted-style S3 requests.
+ type: str
+ required: true
+ state:
+ description:
+ - Define whether the Object Store Virtual Host should be added or deleted
+ default: present
+ choices: [ absent, present ]
+ type: str
+extends_documentation_fragment:
+- purestorage.flashblade.purestorage.fb
+"""
+
+EXAMPLES = r"""
+- name: Add Object Store Virtual Host
+ purestorage.flashblade.purefb_virtualhost:
+ name: "s3.acme.com"
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+
+- name: Delete Object Store Virtual Host
+ purestorage.flashblade.purefb_virtualhost:
+ name: "nohost.acme.com"
+ state: absent
+ fb_url: 10.10.10.2
+ api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3
+"""
+
+RETURN = r"""
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
+ get_system,
+ purefb_argument_spec,
+)
+
+MIN_REQUIRED_API_VERSION = "2.0"
+MAX_HOST_COUNT = 10
+
+
+def delete_host(module, blade):
+ """Delete Object Store Virtual Host"""
+ changed = False
+ if module.params["name"] == "s3.amazonaws.com":
+ module.warn("s3.amazonaws.com is a reserved name and cannot be deleted")
+ else:
+ changed = True
+ if not module.check_mode:
+ res = blade.delete_object_store_virtual_hosts(names=[module.params["name"]])
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to delete Object Store Virtual Host {0}".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def add_host(module, blade):
+ """Add Object Store Virtual Host"""
+ changed = True
+ if not module.check_mode:
+ res = blade.post_object_store_virtual_hosts(names=[module.params["name"]])
+ if res.status_code != 200:
+ module.fail_json(
+ msg="Failed to add Object Store Virtual Host {0}".format(
+ module.params["name"]
+ )
+ )
+ module.exit_json(changed=changed)
+
+
+def main():
+ argument_spec = purefb_argument_spec()
+ argument_spec.update(
+ dict(
+ state=dict(type="str", default="present", choices=["absent", "present"]),
+ name=dict(type="str", required=True),
+ )
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+
+ blade = get_system(module)
+ api_version = list(blade.get_versions().items)
+
+ if MIN_REQUIRED_API_VERSION not in api_version:
+ module.fail_json(
+ msg="FlashBlade REST version not supported. "
+ "Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
+ )
+ state = module.params["state"]
+
+ exists = bool(
+ blade.get_object_store_virtual_hosts(names=[module.params["name"]]).status_code
+ == 200
+ )
+
+ if len(list(blade.get_object_store_virtual_hosts().items)) < MAX_HOST_COUNT:
+ if not exists and state == "present":
+ add_host(module, blade)
+ elif exists and state == "absent":
+ delete_host(module, blade)
+ else:
+ module.warn("Maximum Object Store Virtual Host reached.")
+
+ module.exit_json(changed=False)
+
+
+if __name__ == "__main__":
+ main()