Init: mediaserver

This commit is contained in:
2023-02-08 12:13:28 +01:00
parent 848bc9739c
commit f7c23d4ba9
31914 changed files with 6175775 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Copyright: (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
class ModuleDocFragment(object):
# Standard Pure Storage documentation fragment
DOCUMENTATION = r"""
options:
- See separate platform section for more details
requirements:
- See separate platform section for more details
notes:
- Ansible modules are available for the following Pure Storage products: FlashArray, FlashBlade
"""
# Documentation fragment for FlashArray
FA = r"""
options:
fa_url:
description:
- FlashArray management IPv4 address or Hostname.
type: str
api_token:
description:
- FlashArray API token for admin privileged user.
type: str
notes:
- This module requires the C(purestorage) and C(py-pure-client) Python libraries
- Additional Python librarues may be required for specific modules.
- You must set C(PUREFA_URL) and C(PUREFA_API) environment variables
if I(fa_url) and I(api_token) arguments are not passed to the module directly
requirements:
- python >= 3.3
- purestorage >= 1.19
- py-pure-client >= 1.26.0
- netaddr
- requests
- pycountry
- packaging
"""

View File

@@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
# This code is part of Ansible, but is an independent component.
# This particular file snippet, and this file snippet only, is BSD licensed.
# Modules you write using this snippet, which is embedded dynamically by Ansible
# still belong to the author of the module, and may assign their own license
# to the complete work.
#
# Copyright (c), Simon Dodsley <simon@purestorage.com>,2017
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
HAS_PURESTORAGE = True
try:
from purestorage import purestorage
except ImportError:
HAS_PURESTORAGE = False
HAS_PYPURECLIENT = True
try:
from pypureclient import flasharray
except ImportError:
HAS_PYPURECLIENT = False
from os import environ
import platform
VERSION = 1.4
USER_AGENT_BASE = "Ansible"
def get_system(module):
"""Return System Object or Fail"""
user_agent = "%(base)s %(class)s/%(version)s (%(platform)s)" % {
"base": USER_AGENT_BASE,
"class": __name__,
"version": VERSION,
"platform": platform.platform(),
}
array_name = module.params["fa_url"]
api = module.params["api_token"]
if HAS_PURESTORAGE:
if array_name and api:
system = purestorage.FlashArray(
array_name, api_token=api, user_agent=user_agent, verify_https=False
)
elif environ.get("PUREFA_URL") and environ.get("PUREFA_API"):
system = purestorage.FlashArray(
environ.get("PUREFA_URL"),
api_token=(environ.get("PUREFA_API")),
user_agent=user_agent,
verify_https=False,
)
else:
module.fail_json(
msg="You must set PUREFA_URL and PUREFA_API environment variables "
"or the fa_url and api_token module arguments"
)
try:
system.get()
except Exception:
module.fail_json(
msg="Pure Storage FlashArray authentication failed. Check your credentials"
)
else:
module.fail_json(msg="purestorage SDK is not installed.")
return system
def get_array(module):
"""Return System Object or Fail"""
user_agent = "%(base)s %(class)s/%(version)s (%(platform)s)" % {
"base": USER_AGENT_BASE,
"class": __name__,
"version": VERSION,
"platform": platform.platform(),
}
array_name = module.params["fa_url"]
api = module.params["api_token"]
if HAS_PYPURECLIENT:
if array_name and api:
system = flasharray.Client(
target=array_name,
api_token=api,
user_agent=user_agent,
)
elif environ.get("PUREFA_URL") and environ.get("PUREFA_API"):
system = flasharray.Client(
target=(environ.get("PUREFA_URL")),
api_token=(environ.get("PUREFA_API")),
user_agent=user_agent,
)
else:
module.fail_json(
msg="You must set PUREFA_URL and PUREFA_API environment variables "
"or the fa_url and api_token module arguments"
)
try:
system.get_hardware()
except Exception:
module.fail_json(
msg="Pure Storage FlashArray authentication failed. Check your credentials"
)
else:
module.fail_json(msg="py-pure-client and/or requests are not installed.")
return system
def purefa_argument_spec():
"""Return standard base dictionary used for the argument_spec argument in AnsibleModule"""
return dict(
fa_url=dict(),
api_token=dict(no_log=True),
)

View File

@@ -0,0 +1,323 @@
#!/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: purefa_ad
version_added: '1.9.0'
short_description: Manage FlashArray Active Directory Account
description:
- Add or delete FlashArray Active Directory Account
- FlashArray 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
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
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 are only 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 1, or 3 for Purity 6.1.6 or higher.
If more are provided only the first allowed count 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 and are only 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 1, or 3 for Purity 6.1.6 or higher.
If more are provided only the first allowed count used.
type: list
elements: str
local_only:
description:
- Do a local-only delete of an active directory account
type: bool
default: false
join_ou:
description:
- Distinguished name of organization unit in which the computer account
should be created when joining the domain. e.g. OU=Arrays,OU=Storage.
- The B(DC=...) components can be omitted.
- If left empty, defaults to B(CN=Computers).
- Requires Purity//FA 6.1.8 or higher
type: str
version_added: '1.10.0'
tls:
description:
- TLS mode for communication with domain controllers.
type: str
choices: [ required, optional ]
default: required
version_added: '1.14.0'
join_existing:
description:
- If specified as I(true), the domain is searched for a pre-existing
computer account to join to, and no new account will be created within the domain.
The C(username) specified when joining a pre-existing account must have
permissions to 'read all properties from' and 'reset the password of'
the pre-existing account. C(join_ou) will be read from the pre-existing
account and cannot be specified when joining to an existing account
type: bool
default: false
version_added: '1.14.0'
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Create new AD account
purestorage.flasharray.purefa_ad:
name: ad_account
computer: FLASHARRAY
domain: acme.com
join_ou: "OU=Acme,OU=Dev"
username: Administrator
password: Password
kerberos_servers:
- kdc.acme.com
directory_servers:
- ldap.acme.com
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete AD account locally
purestorage.flasharray.purefa_ad:
name: ad_account
local_only: True
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Fully delete AD account. Note that correct AD permissions are required
purestorage.flasharray.purefa_ad:
name: ad_account
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from pypureclient.flasharray import ActiveDirectoryPost, ActiveDirectoryPatch
except ImportError:
HAS_PURESTORAGE = False
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
MIN_REQUIRED_API_VERSION = "2.2"
SERVER_API_VERSION = "2.6"
MIN_JOIN_OU_API_VERSION = "2.8"
MIN_TLS_API_VERSION = "2.15"
def delete_account(module, array):
"""Delete Active directory Account"""
changed = True
if not module.check_mode:
res = array.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}. Error: {1}".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def update_account(module, array):
"""Update existing AD account"""
changed = False
current_acc = list(array.get_active_directory(names=[module.params["name"]]).items)[
0
]
if current_acc.tls != module.params["tls"]:
changed = True
if not module.check_mode:
res = array.patch_active_directory(
names=[module.params["name"]],
active_directory=ActiveDirectoryPatch(tls=module.params["tls"]),
)
if res.status_code != 200:
module.fail_json(
msg="Failed to update AD Account {0} TLS setting. Error: {1}".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def create_account(module, array, api_version):
"""Create Active Directory Account"""
changed = True
if MIN_JOIN_OU_API_VERSION not in api_version:
ad_config = ActiveDirectoryPost(
computer_name=module.params["computer"],
directory_servers=module.params["directory_servers"],
kerberos_servers=module.params["kerberos_servers"],
domain=module.params["domain"],
user=module.params["username"],
password=module.params["password"],
)
elif MIN_TLS_API_VERSION in api_version:
ad_config = ActiveDirectoryPost(
computer_name=module.params["computer"],
directory_servers=module.params["directory_servers"],
kerberos_servers=module.params["kerberos_servers"],
domain=module.params["domain"],
user=module.params["username"],
join_ou=module.params["join_ou"],
password=module.params["password"],
tls=module.params["tls"],
)
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"],
user=module.params["username"],
join_ou=module.params["join_ou"],
password=module.params["password"],
)
if not module.check_mode:
if MIN_TLS_API_VERSION in api_version:
res = array.post_active_directory(
names=[module.params["name"]],
join_existing_account=module.params["join_existing"],
active_directory=ad_config,
)
else:
res = array.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
)
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_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),
computer=dict(type="str"),
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"),
tls=dict(type="str", default="required", choices=["required", "optional"]),
join_existing=dict(type="bool", default=False),
)
)
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")
array = get_system(module)
api_version = array._list_available_rest_versions()
if MIN_REQUIRED_API_VERSION not in api_version:
module.fail_json(
msg="FlashArray REST version not supported. "
"Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
)
state = module.params["state"]
array = get_array(module)
exists = bool(
array.get_active_directory(names=[module.params["name"]]).status_code == 200
)
if not module.params["computer"]:
module.params["computer"] = module.params["name"].replace("_", "-")
if module.params["kerberos_servers"]:
if SERVER_API_VERSION in api_version:
module.params["kerberos_servers"] = module.params["kerberos_servers"][0:3]
else:
module.params["kerberos_servers"] = module.params["kerberos_servers"][0:1]
if module.params["directory_servers"]:
if SERVER_API_VERSION in api_version:
module.params["directory_servers"] = module.params["directory_servers"][0:3]
else:
module.params["directory_servers"] = module.params["directory_servers"][0:1]
if not exists and state == "present":
create_account(module, array, api_version)
elif exists and state == "present" and MIN_TLS_API_VERSION in api_version:
update_account(module, array)
elif exists and state == "absent":
delete_account(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,180 @@
#!/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: purefa_admin
version_added: '1.12.0'
short_description: Configure Pure Storage FlashArray Global Admin settings
description:
- Set global admin settings for the FlashArray
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
sso:
description:
- Enable or disable the array Signle Sign-On from Pure1 Manage
default: false
type: bool
max_login:
description:
- Maximum number of failed logins before account is locked
type: int
min_password:
description:
- Minimum user password length
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.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Set global login parameters
purestorage.flasharray.purefa_admin:
sso: false
max_login: 5
min_password: 10
lockout: 300
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from pypureclient.flasharray import AdminSettings
except ImportError:
HAS_PURESTORAGE = False
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
MIN_API_VERSION = "2.2"
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
sso=dict(type="bool", default=False),
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")
array = get_system(module)
api_version = array._list_available_rest_versions()
changed = False
if MIN_API_VERSION in api_version:
array = get_array(module)
current_settings = list(array.get_admins_settings().items)[0]
if (
module.params["sso"]
and module.params["sso"] != current_settings.single_sign_on_enabled
):
changed = True
sso = module.params["sso"]
else:
sso = current_settings.single_sign_on_enabled
if (
module.params["min_password"]
and module.params["min_password"] != current_settings.min_password_length
):
changed = True
min_password = module.params["min_password"]
else:
min_password = current_settings.min_password_length
lockout = getattr(current_settings, "lockout_duration", None)
if (
lockout
and module.params["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
max_login = getattr(current_settings, "max_login_attempts", None)
if (
max_login
and module.params["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:
if max_login:
admin = AdminSettings(
single_sign_on_enabled=sso,
min_password_length=min_password,
max_login_attempts=max_login,
)
if lockout:
admin = AdminSettings(
single_sign_on_enabled=sso,
min_password_length=min_password,
lockout_duration=lockout,
)
if lockout and max_login:
admin = AdminSettings(
single_sign_on_enabled=sso,
min_password_length=min_password,
lockout_duration=lockout,
max_login_attempts=max_login,
)
if not lockout and not max_login:
admin = AdminSettings(
single_sign_on_enabled=sso,
min_password_length=min_password,
)
res = array.patch_admins_settings(admin_settings=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()

View File

@@ -0,0 +1,208 @@
#!/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: purefa_alert
version_added: '1.0.0'
short_description: Configure Pure Storage FlashArray alert email settings
description:
- Configure alert email configuration for Pure Storage FlashArrays.
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
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Add new email recipient and enable, or enable existing email
purestorage.flasharray.purefa_alert:
address: "user@domain.com"
enabled: true
state: present
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete existing email recipient
purestorage.flasharray.purefa_alert:
state: absent
address: "user@domain.com"
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
import re
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
purefa_argument_spec,
)
def create_alert(module, array):
"""Create Alert Email"""
changed = True
if not module.check_mode:
changed = False
try:
array.create_alert_recipient(module.params["address"])
changed = True
except Exception:
module.fail_json(
msg="Failed to create alert email: {0}".format(module.params["address"])
)
if not module.params["enabled"]:
try:
array.disable_alert_recipient(module.params["address"])
changed = True
except Exception:
module.fail_json(
msg="Failed to create alert email: {0}".format(
module.params["address"]
)
)
module.exit_json(changed=changed)
def enable_alert(module, array):
"""Enable Alert Email"""
changed = True
if not module.check_mode:
changed = False
try:
array.enable_alert_recipient(module.params["address"])
changed = True
except Exception:
module.fail_json(
msg="Failed to enable alert email: {0}".format(module.params["address"])
)
module.exit_json(changed=changed)
def disable_alert(module, array):
"""Disable Alert Email"""
changed = True
if not module.check_mode:
changed = False
try:
array.disable_alert_recipient(module.params["address"])
changed = True
except Exception:
module.fail_json(
msg="Failed to disable alert email: {0}".format(
module.params["address"]
)
)
module.exit_json(changed=changed)
def delete_alert(module, array):
"""Delete Alert Email"""
changed = True
if module.params["address"] == "flasharray-alerts@purestorage.com":
module.fail_json(
msg="Built-in address {0} cannot be deleted.".format(
module.params["address"]
)
)
if not module.check_mode:
changed = False
try:
array.delete_alert_recipient(module.params["address"])
changed = True
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 = purefa_argument_spec()
argument_spec.update(
dict(
address=dict(type="str", required=True),
enabled=dict(type="bool", default=True),
state=dict(type="str", default="present", choices=["absent", "present"]),
)
)
module = AnsibleModule(argument_spec, supports_check_mode=True)
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.")
array = get_system(module)
exists = False
try:
emails = array.list_alert_recipients()
except Exception:
module.fail_json(msg="Failed to get exisitng email list")
for email in range(0, len(emails)):
if emails[email]["name"] == module.params["address"]:
exists = True
enabled = emails[email]["enabled"]
break
if module.params["state"] == "present" and not exists:
create_alert(module, array)
elif (
module.params["state"] == "present"
and exists
and not enabled
and module.params["enabled"]
):
enable_alert(module, array)
elif (
module.params["state"] == "present"
and exists
and enabled
and not module.params["enabled"]
):
disable_alert(module, array)
elif module.params["state"] == "absent" and exists:
delete_alert(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,250 @@
#!/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: purefa_apiclient
version_added: '1.5.0'
short_description: Manage FlashArray API Clients
description:
- Enable or disable FlashArray 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.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Create API token ansible-token
purestorage.flasharray.purefa_apiclient:
name: ansible-token
issuer: "Pure Storage"
ttl: 3000
role: array_admin
public_key: "{{lookup('file', 'public_pem_file') }}"
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Disable API CLient
purestorage.flasharray.purefa_apiclient:
name: ansible-token
enabled: false
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Enable API CLient
purestorage.flasharray.purefa_apiclient:
name: ansible-token
enabled: true
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete API Client
purestorage.flasharray.purefa_apiclient:
state: absent
name: ansible-token
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from pypureclient import flasharray
except ImportError:
HAS_PURESTORAGE = False
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
MIN_REQUIRED_API_VERSION = "2.1"
def delete_client(module, array):
changed = True
if not module.check_mode:
try:
array.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, array, client):
"""Update API Client"""
changed = False
if client.enabled != module.params["enabled"]:
changed = True
if not module.check_mode:
try:
array.patch_api_clients(
names=[module.params["name"]],
api_clients=flasharray.ApiClientPatch(
enabled=module.params["enabled"]
),
)
except Exception:
module.fail_json(
msg="Failed to update API Client {0}".format(module.params["name"])
)
module.exit_json(changed=changed)
def create_client(module, array):
"""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"]
try:
client = flasharray.ApiClientPost(
max_role=module.params["role"],
issuer=module.params["issuer"],
access_token_ttl_in_ms=token_ttl,
public_key=module.params["public_key"],
)
if not module.check_mode:
res = array.post_api_clients(
names=[module.params["name"]], api_clients=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"]:
try:
array.patch_api_clients(
names=[module.params["name"]],
api_clients=flasharray.ApiClientPatch(
enabled=module.params["enabled"]
),
)
except Exception:
array.delete_api_clients(names=[module.params["name"]])
module.fail_json(
msg="Failed to create API Client {0}".format(
module.params["name"]
)
)
except Exception:
module.fail_json(
msg="Failed to create API Client {0}".format(module.params["name"])
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_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)
if not HAS_PURESTORAGE:
module.fail_json(msg="py-pure-client sdk is required for this module")
array = get_system(module)
api_version = array._list_available_rest_versions()
if MIN_REQUIRED_API_VERSION not in api_version:
module.fail_json(
msg="FlashArray REST version not supported. "
"Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
)
array = get_array(module)
state = module.params["state"]
try:
client = list(array.get_api_clients(names=[module.params["name"]]).items)[0]
exists = True
except Exception:
exists = False
if not exists and state == "present":
create_client(module, array)
elif exists and state == "present":
update_client(module, array, client)
elif exists and state == "absent":
delete_client(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,104 @@
#!/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: purefa_arrayname
version_added: '1.0.0'
short_description: Configure Pure Storage FlashArray array name
description:
- Configure name of array for Pure Storage FlashArrays.
- Ideal for Day 0 initial configuration.
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
state:
description:
- Set the array name
type: str
default: present
choices: [ present ]
name:
description:
- Name of the array. Must conform to correct naming schema.
type: str
required: true
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Set new array name
purestorage.flasharray.purefa_arrayname:
name: new-array-name
state: present
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
import re
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
purefa_argument_spec,
)
def update_name(module, array):
"""Change aray name"""
changed = True
if not module.check_mode:
try:
array.set(name=module.params["name"])
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 = purefa_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)
array = get_system(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="Array name {0} does not conform to array name rules".format(
module.params["name"]
)
)
if module.params["name"] != array.get()["array_name"]:
update_name(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,125 @@
#!/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: purefa_banner
version_added: '1.0.0'
short_description: Configure Pure Storage FlashArray GUI and SSH MOTD message
description:
- Configure MOTD for Pure Storage FlashArrays.
- This will be shown during an SSH or GUI login to the array.
- 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.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Set new banner text
purestorage.flasharray.purefa_banner:
banner: "Banner over\ntwo lines"
state: present
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete banner text
purestorage.flasharray.purefa_banner:
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
purefa_argument_spec,
)
def set_banner(module, array):
"""Set MOTD banner text"""
changed = True
if not module.params["banner"]:
module.fail_json(msg="Invalid MOTD banner given")
if not module.check_mode:
try:
array.set(banner=module.params["banner"])
except Exception:
module.fail_json(msg="Failed to set MOTD banner text")
module.exit_json(changed=changed)
def delete_banner(module, array):
"""Delete MOTD banner text"""
changed = True
if not module.check_mode:
try:
array.set(banner="")
except Exception:
module.fail_json(msg="Failed to delete current MOTD banner text")
module.exit_json(changed=changed)
def main():
argument_spec = purefa_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
)
state = module.params["state"]
array = get_system(module)
current_banner = array.get(banner=True)["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, array)
# clear banner if it has a value
elif state == "absent" and current_banner:
delete_banner(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,524 @@
#!/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: purefa_certs
version_added: '1.8.0'
short_description: Manage FlashArray SSL Certificates
description:
- Create, delete, import and export FlashArray SSL Certificates
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
name:
description:
- Name of the SSL Certificate
type: str
default: management
state:
description:
- Action for the module to perform
- I(present) will create or re-create an SSL certificate
- I(absent) will delete an existing SSL certificate
- I(sign) will construct a Certificate Signing request (CSR)
- I(export) will export the exisitng SSL certificate
- I(import) will import a CA provided certificate.
default: present
choices: [ absent, present, import, export, sign ]
type: str
country:
type: str
description:
- The two-letter ISO code for the country where your organization is located
province:
type: str
description:
- The full name of the state or province where your organization is located
locality:
type: str
description:
- The full name of the city where your organization is located
organization:
type: str
description:
- The full and exact legal name of your organization.
- The organization name should not be abbreviated and should
include suffixes such as Inc, Corp, or LLC.
org_unit:
type: str
description:
- The department within your organization that is managing the certificate
common_name:
type: str
description:
- The fully qualified domain name (FQDN) of the current array
- For example, the common name for https://purearray.example.com is
purearray.example.com, or *.example.com for a wildcard certificate
- This can also be the management IP address of the array or the
shortname of the current array.
- Maximum of 64 characters
- If not provided this will default to the shortname of the array
email:
type: str
description:
- The email address used to contact your organization
key_size:
type: int
description:
- The key size in bits if you generate a new private key
default: 2048
choices: [ 1024, 2048, 4096 ]
days:
default: 3650
type: int
description:
- The number of valid days for the self-signed certificate being generated
- If not specified, the self-signed certificate expires after 3650 days.
generate:
default: false
type: bool
description:
- Generate a new private key.
- If not selected, the certificate will use the existing key
certificate:
type: str
description:
- Required for I(import)
- A valid signed certicate in PEM format (Base64 encoded)
- Includes the "-----BEGIN CERTIFICATE-----" and "-----END CERTIFICATE-----" lines
- Does not exceed 3000 characters in length
intermeadiate_cert:
type: str
description:
- Intermeadiate certificate provided by the CA
key:
type: str
description:
- If the Certificate Signed Request (CSR) was not constructed on the array
or the private key has changed since construction the CSR, provide
a new private key here
passphrase:
type: str
description:
- Passphrase if the private key is encrypted
export_file:
type: str
description:
- Name of file to contain Certificate Signing Request when `status sign`
- Name of file to export the current SSL Certificate when `status export`
- File will be overwritten if it already exists
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Create SSL certifcate foo
purestorage.flasharray.purefa_certs:
name: foo
key_size: 4096
country: US
province: FL
locality: Miami
organization: "Acme Inc"
org_unit: "DevOps"
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete SSL certificate foo
purestorage.flasharray.purefa_certs:
name: foo
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Request CSR
purestorage.flasharray.purefa_certs:
state: sign
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Regenerate key for SSL foo
purestorage.flasharray.purefa_certs:
generate: true
name: foo
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Import SSL Cert foo and Private Key
purestorage.flasharray.purefa_certs:
state: import
name: foo
certificate: "{{lookup('file', 'example.crt') }}"
key: "{{lookup('file', 'example.key') }}"
passphrase: password
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from pypureclient import flasharray
except ImportError:
HAS_PURESTORAGE = False
HAS_PYCOUNTRY = True
try:
import pycountry
except ImportError:
HAS_PYCOUNTRY = False
import re
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
MIN_REQUIRED_API_VERSION = "2.4"
def update_cert(module, array):
"""Update existing SSL Certificate"""
changed = True
current_cert = list(array.get_certificates(names=[module.params["name"]]).items)[0]
try:
if module.params["common_name"] != current_cert.common_name:
module.params["common_name"] = current_cert.common_name
except AttributeError:
pass
try:
if module.params["country"] != current_cert.country:
module.params["country"] = current_cert.country
except AttributeError:
pass
try:
if module.params["email"] != current_cert.email:
module.params["email"] = current_cert.email
except AttributeError:
pass
try:
if module.params["key_size"] != current_cert.key_size:
module.params["key_size"] = current_cert.key_size
except AttributeError:
pass
try:
if module.params["locality"] != current_cert.locality:
module.params["locality"] = current_cert.locality
except AttributeError:
pass
try:
if module.params["province"] != current_cert.state:
module.params["province"] = current_cert.state
except AttributeError:
pass
try:
if module.params["organization"] != current_cert.organization:
module.params["organization"] = current_cert.organization
except AttributeError:
pass
try:
if module.params["org_unit"] != current_cert.organizational_unit:
module.params["org_unit"] = current_cert.organizational_unit
except AttributeError:
pass
certificate = flasharray.CertificatePost(
common_name=module.params["common_name"],
country=module.params["country"],
email=module.params["email"],
key_size=module.params["key_size"],
locality=module.params["locality"],
organization=module.params["organization"],
organizational_unit=module.params["org_unit"],
state=module.params["province"],
days=module.params["days"],
)
if not module.check_mode:
res = array.patch_certificates(
names=[module.params["name"]],
certificate=certificate,
generate_new_key=module.params["generate"],
)
if res.status_code != 200:
module.fail_json(
msg="Updating existing SSL certificate {0} failed. Error: {1}".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def create_cert(module, array):
changed = True
certificate = flasharray.CertificatePost(
common_name=module.params["common_name"],
country=module.params["country"],
email=module.params["email"],
key_size=module.params["key_size"],
locality=module.params["locality"],
organization=module.params["organization"],
organizational_unit=module.params["org_unit"],
state=module.params["province"],
status="self-signed",
days=module.params["days"],
)
if not module.check_mode:
res = array.post_certificates(
names=[module.params["name"]], certificate=certificate
)
if res.status_code != 200:
module.fail_json(
msg="Creating SSL certificate {0} failed. Error: {1}".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def delete_cert(module, array):
changed = True
if module.params["name"] == "management":
module.fail_json(msg="management SSL cannot be deleted")
if not module.check_mode:
res = array.delete_certificates(names=[module.params["name"]])
if res.status_code != 200:
module.fail_json(
msg="Failed to delete {0} SSL certifcate. Error: {1}".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def import_cert(module, array, reimport=False):
"""Import a CA provided SSL certificate"""
changed = True
if len(module.params["certificate"]) > 3000:
module.fail_json(msg="Imported Certificate exceeds 3000 characters")
certificate = flasharray.CertificatePost(
certificate=module.params["certificate"],
intermediate_certificate=module.params["intermeadiate_cert"],
key=module.params["key"],
passphrase=module.params["passphrase"],
status="imported",
)
if not module.check_mode:
if reimport:
res = array.patch_certificates(
names=[module.params["name"]], certificate=certificate
)
else:
res = array.post_certificates(
names=[module.params["name"]], certificate=certificate
)
if res.status_code != 200:
module.fail_json(
msg="Importing Certificate failed. Error: {0}".format(
res.errors[0].message
)
)
module.exit_json(changed=changed)
def export_cert(module, array):
"""Export current SSL certificate"""
changed = True
if not module.check_mode:
ssl = array.get_certificates(names=[module.params["name"]])
if ssl.status_code != 200:
module.fail_json(
msg="Exporting Certificate failed. Error: {0}".format(
ssl.errors[0].message
)
)
ssl_file = open(module.params["export_file"], "w")
ssl_file.write(list(ssl.items)[0].certificate)
ssl_file.close()
module.exit_json(changed=changed)
def create_csr(module, array):
"""Construct a Certificate Signing Request
Output the result to a specified file
"""
changed = True
current_attr = list(array.get_certificates(names=[module.params["name"]]).items)[0]
try:
if module.params["common_name"] != current_attr.common_name:
module.params["common_name"] = current_attr.common_name
except AttributeError:
pass
try:
if module.params["country"] != current_attr.country:
module.params["country"] = current_attr.country
except AttributeError:
pass
try:
if module.params["email"] != current_attr.email:
module.params["email"] = current_attr.email
except AttributeError:
pass
try:
if module.params["locality"] != current_attr.locality:
module.params["locality"] = current_attr.locality
except AttributeError:
pass
try:
if module.params["province"] != current_attr.state:
module.params["province"] = current_attr.state
except AttributeError:
pass
try:
if module.params["organization"] != current_attr.organization:
module.params["organization"] = current_attr.organization
except AttributeError:
pass
try:
if module.params["org_unit"] != current_attr.organization_unit:
module.params["org_unit"] = current_attr.organization_unit
except AttributeError:
pass
if not module.check_mode:
certificate = flasharray.CertificateSigningRequestPost(
certificate={"name": "management"},
common_name=module.params["common_name"],
country=module.params["country"],
email=module.params["email"],
locality=module.params["locality"],
state=module.params["province"],
organization=module.params["organization"],
organization_unit=module.params["org_unit"],
)
csr = list(
array.post_certificates_certificate_signing_requests(
certificate=certificate
).items
)[0].certificate_signing_request
csr_file = open(module.params["export_file"], "w")
csr_file.write(csr)
csr_file.close()
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
state=dict(
type="str",
default="present",
choices=["absent", "present", "import", "export", "sign"],
),
generate=dict(type="bool", default=False),
name=dict(type="str", default="management"),
country=dict(type="str"),
province=dict(type="str"),
locality=dict(type="str"),
organization=dict(type="str"),
org_unit=dict(type="str"),
common_name=dict(type="str"),
email=dict(type="str"),
key_size=dict(type="int", default=2048, choices=[1024, 2048, 4096]),
certificate=dict(type="str", no_log=True),
intermeadiate_cert=dict(type="str", no_log=True),
key=dict(type="str", no_log=True),
export_file=dict(type="str"),
passphrase=dict(type="str", no_log=True),
days=dict(type="int", default=3650),
)
)
mutually_exclusive = [["certificate", "key_size"]]
required_if = [
["state", "import", ["certificate"]],
["state", "export", ["export_file"]],
]
module = AnsibleModule(
argument_spec,
mutually_exclusive=mutually_exclusive,
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")
if not HAS_PYCOUNTRY:
module.fail_json(msg="pycountry sdk is required for this module")
email_pattern = r"^(\w|\.|\_|\-)+[@](\w|\_|\-|\.)+[.]\w{2,3}$"
array = get_system(module)
api_version = array._list_available_rest_versions()
if MIN_REQUIRED_API_VERSION not in api_version:
module.fail_json(
msg="FlashArray REST version not supported. "
"Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
)
array = get_array(module)
if module.params["email"]:
if not re.search(email_pattern, module.params["email"]):
module.fail_json(
msg="Email {0} is not valid".format(module.params["email"])
)
if module.params["country"]:
if len(module.params["country"]) != 2:
module.fail_json(msg="Country must be a two-letter country (ISO) code")
if not pycountry.countries.get(alpha_2=module.params["country"].upper()):
module.fail_json(
msg="Country code {0} is not an assigned ISO 3166-1 code".format(
module.params["country"].upper()
)
)
state = module.params["state"]
if state in ["present", "sign"]:
if not module.params["common_name"]:
module.params["common_name"] = list(array.get_arrays().items)[0].name
module.params["common_name"] = module.params["common_name"][:64]
exists = bool(
array.get_certificates(names=[module.params["name"]]).status_code == 200
)
if not exists and state == "present":
create_cert(module, array)
elif exists and state == "present":
update_cert(module, array)
elif state == "sign":
create_csr(module, array)
elif not exists and state == "import":
import_cert(module, array)
elif exists and state == "import":
import_cert(module, array, reimport=True)
elif state == "export":
export_cert(module, array)
elif exists and state == "absent":
delete_cert(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,238 @@
#!/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: purefa_connect
version_added: '1.0.0'
short_description: Manage replication connections between two FlashArrays
description:
- Manage array connections to specified target array
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
state:
description:
- Create or delete array connection
default: present
type: str
choices: [ absent, present ]
target_url:
description:
- Management IP address of remote array.
type: str
required: true
target_api:
description:
- API token for target array
type: str
connection:
description:
- Type of connection between arrays.
type: str
choices: [ sync, async ]
default: async
transport:
description:
- Type of transport protocol to use for replication
type: str
choices: [ ip, fc ]
default: ip
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Create an async connection to remote array
purestorage.flasharray.purefa_connect:
target_url: 10.10.10.20
target_api: 9c0b56bc-f941-f7a6-9f85-dcc3e9a8f7d6
connection: async
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete connection to remote array
purestorage.flasharray.purefa_connect:
state: absent
target_url: 10.10.10.20
target_api: 9c0b56bc-f941-f7a6-9f85-dcc3e9a8f7d6
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from purestorage import FlashArray
except ImportError:
HAS_PURESTORAGE = False
HAS_PYPURECLIENT = True
try:
from pypureclient import flasharray
except ImportError:
HAS_PYPURECLIENT = False
import platform
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_array,
get_system,
purefa_argument_spec,
)
P53_API_VERSION = "1.17"
FC_REPL_VERSION = "2.4"
def _check_connected(module, array):
connected_arrays = array.list_array_connections()
api_version = array._list_available_rest_versions()
for target in range(0, len(connected_arrays)):
if P53_API_VERSION in api_version:
if (
connected_arrays[target]["management_address"]
== module.params["target_url"]
and "connected" in connected_arrays[target]["status"]
):
return connected_arrays[target]
else:
if (
connected_arrays[target]["management_address"]
== module.params["target_url"]
and connected_arrays[target]["connected"]
):
return connected_arrays[target]
return None
def break_connection(module, array, target_array):
"""Break connection between arrays"""
changed = True
source_array = array.get()["array_name"]
if target_array["management_address"] is None:
module.fail_json(
msg="disconnect can only happen from the array that formed the connection"
)
if not module.check_mode:
try:
array.disconnect_array(target_array["array_name"])
except Exception:
module.fail_json(
msg="Failed to disconnect {0} from {1}.".format(
target_array["array_name"], source_array
)
)
module.exit_json(changed=changed)
def create_connection(module, array):
"""Create connection between arrays"""
changed = True
remote_array = module.params["target_url"]
user_agent = "%(base)s %(class)s/%(version)s (%(platform)s)" % {
"base": "Ansible",
"class": __name__,
"version": 1.2,
"platform": platform.platform(),
}
try:
remote_system = FlashArray(
module.params["target_url"],
api_token=module.params["target_api"],
user_agent=user_agent,
)
connection_key = remote_system.get(connection_key=True)["connection_key"]
remote_array = remote_system.get()["array_name"]
api_version = array._list_available_rest_versions()
# TODO: Refactor when FC async is supported
if (
FC_REPL_VERSION in api_version
and module.params["transport"].lower() == "fc"
):
if module.params["connection"].lower() == "async":
module.fail_json(
msg="Asynchronous replication not supported using FC transport"
)
array_connection = flasharray.ArrayConnectionPost(
type="sync-replication",
management_address=module.params["target_url"],
replication_transport="fc",
connection_key=connection_key,
)
array = get_array(module)
if not module.check_mode:
res = array.post_array_connections(array_connection=array_connection)
if res.status_code != 200:
module.fail_json(
msg="Array Connection failed. Error: {0}".format(
res.errors[0].message
)
)
else:
if not module.check_mode:
array.connect_array(
module.params["target_url"],
connection_key,
[module.params["connection"]],
)
except Exception:
module.fail_json(
msg="Failed to connect to remote array {0}.".format(remote_array)
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
state=dict(type="str", default="present", choices=["absent", "present"]),
connection=dict(type="str", default="async", choices=["async", "sync"]),
transport=dict(type="str", default="ip", choices=["ip", "fc"]),
target_url=dict(type="str", required=True),
target_api=dict(type="str"),
)
)
required_if = [("state", "present", ["target_api"])]
module = AnsibleModule(
argument_spec, required_if=required_if, supports_check_mode=True
)
if not HAS_PURESTORAGE:
module.fail_json(msg="purestorage sdk is required for this module")
if module.params["transport"] == "fc" and not HAS_PYPURECLIENT:
module.fail_json(msg="pypureclient sdk is required for this module")
state = module.params["state"]
array = get_system(module)
target_array = _check_connected(module, array)
if state == "present" and target_array is None:
create_connection(module, array)
elif state == "absent" and target_array is not None:
break_connection(module, array, target_array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,107 @@
#!/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: purefa_console
version_added: '1.0.0'
short_description: Enable or Disable Pure Storage FlashArray Console Lock
description:
- Enablke or Disable root lockout from the array at the physical console for a Pure Storage FlashArray.
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
state:
description:
- Define state of console lockout
- When set to I(enable) the console port is locked from root login.
type: str
default: disable
choices: [ enable, disable ]
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Enable Console Lockout
purestorage.flasharray.purefa_console:
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Disable Console Lockout
purestorage.flasharray.purefa_console:
state: disable
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
purefa_argument_spec,
)
def enable_console(module, array):
"""Enable Console Lockout"""
changed = False
if array.get_console_lock_status()["console_lock"] != "enabled":
changed = True
if not module.check_mode:
try:
array.enable_console_lock()
except Exception:
module.fail_json(msg="Enabling Console Lock failed")
module.exit_json(changed=changed)
def disable_console(module, array):
"""Disable Console Lock"""
changed = False
if array.get_console_lock_status()["console_lock"] == "enabled":
changed = True
if not module.check_mode:
try:
array.disable_console_lock()
except Exception:
module.fail_json(msg="Disabling Console Lock failed")
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
state=dict(type="str", default="disable", choices=["enable", "disable"]),
)
)
module = AnsibleModule(argument_spec, supports_check_mode=True)
array = get_system(module)
if module.params["state"] == "enable":
enable_console(module, array)
else:
disable_console(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,328 @@
#!/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: purefa_default_protection
version_added: '1.14.0'
short_description: Manage SafeMode default protection for a Pure Storage FlashArray
description:
- Configure automatic protection group membership for new volumes and copied volumes
array wide, or at the pod level.
- Requires a minimum of Purity 6.3.4
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
scope:
description:
- The scope of the default protection group
type: str
choices: [ array, pod ]
default: array
name:
description:
- The name of the protection group to assign or remove as default for the scope.
- If I(scope) is I(pod) only the short-name for the pod protection group is needed.
See examples
elements: str
type: list
required: true
pod:
description:
- name of the pod to apply the default protection to.
- Only required for I(scope) is I(pod)
type: str
state:
description:
- Define whether to add or delete the protection group to the default list
default: present
choices: [ absent, present ]
type: str
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Add protection group foo::bar as default for pod foo
purestorage.flasharray.purefa_default_protection:
name: bar
pod: foo
scope: pod
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Add protection group foo as default for array
purestorage.flasharray.purefa_default_protection:
name: foo
scope: array
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Remove protection group foo from array default protection
purestorage.flasharray.purefa_default_protection:
name: foo
scope: array
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: absent
- name: Clear default protection for the array
purestorage.flasharray.purefa_volume_tags:
name: ''
scope: array
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: absent
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from pypureclient import flasharray
except ImportError:
HAS_PURESTORAGE = False
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
DEFAULT_API_VERSION = "2.16"
def _get_pod(module, array):
"""Return Pod or None"""
try:
return array.get_pods(names=[module.params["pod"]])
except Exception:
return None
def _get_pg(array, pod):
"""Return Protection Group or None"""
try:
return array.get_protection_groups(names=[pod])
except Exception:
return None
def create_default(module, array):
"""Create Default Protection"""
changed = True
pg_list = []
if not module.check_mode:
for pgroup in range(0, len(module.params["name"])):
if module.params["scope"] == "array":
pg_list.append(
flasharray.DefaultProtectionReference(
name=module.params["name"][pgroup], type="protection_group"
)
)
else:
pg_list.append(
flasharray.DefaultProtectionReference(
name=module.params["pod"]
+ "::"
+ module.params["name"][pgroup],
type="protection_group",
)
)
if module.params["scope"] == "array":
protection = flasharray.ContainerDefaultProtection(
name="", type="", default_protections=pg_list
)
res = array.patch_container_default_protections(
names=[""], container_default_protection=protection
)
else:
protection = flasharray.ContainerDefaultProtection(
name=module.params["pod"], type="pod", default_protections=pg_list
)
res = array.patch_container_default_protections(
names=[module.params["pod"]], container_default_protection=protection
)
if res.status_code != 200:
module.fail_json(
msg="Failed to set default protection. Error: {0}".format(
res.errors[0].message
)
)
module.exit_json(changed=changed)
def update_default(module, array, current_default):
"""Update Default Protection"""
changed = False
current = []
for default in range(0, len(current_default)):
if module.params["scope"] == "array":
current.append(current_default[default].name)
else:
current.append(current_default[default].name.split(":")[-1])
pg_list = []
if module.params["state"] == "present":
if current:
new_list = sorted(list(set(module.params["name"] + current)))
else:
new_list = sorted(list(set(module.params["name"])))
elif current:
new_list = sorted(list(set(current).difference(module.params["name"])))
else:
new_list = []
if not new_list:
delete_default(module, array)
elif new_list == current:
changed = False
else:
changed = True
if not module.check_mode:
for pgroup in range(0, len(new_list)):
if module.params["scope"] == "array":
pg_list.append(
flasharray.DefaultProtectionReference(
name=new_list[pgroup], type="protection_group"
)
)
else:
pg_list.append(
flasharray.DefaultProtectionReference(
name=module.params["pod"] + "::" + new_list[pgroup],
type="protection_group",
)
)
if module.params["scope"] == "array":
protection = flasharray.ContainerDefaultProtection(
name="", type="", default_protections=pg_list
)
res = array.patch_container_default_protections(
names=[""], container_default_protection=protection
)
else:
protection = flasharray.ContainerDefaultProtection(
name=module.params["pod"],
type="pod",
default_protections=pg_list,
)
res = array.patch_container_default_protections(
names=[module.params["pod"]],
container_default_protection=protection,
)
if res.status_code != 200:
module.fail_json(
msg="Failed to update default protection. Error: {0}".format(
res.errors[0].message
)
)
module.exit_json(changed=changed)
def delete_default(module, array):
"""Delete Default Protection"""
changed = True
if not module.check_mode:
if module.params["scope"] == "array":
protection = flasharray.ContainerDefaultProtection(
name="", type="", default_protections=[]
)
res = array.patch_container_default_protections(
names=[""], container_default_protection=protection
)
else:
protection = flasharray.ContainerDefaultProtection(
name=module.params["pod"], type="pod", default_protections=[]
)
res = array.patch_container_default_protections(
names=[module.params["pod"]], container_default_protection=[]
)
if res.status_code != 200:
module.fail_json(
msg="Failed to delete default protection. Error: {0}".format(
res.errors[0].message
)
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
name=dict(type="list", elements="str", required=True),
pod=dict(type="str"),
scope=dict(type="str", default="array", choices=["array", "pod"]),
state=dict(type="str", default="present", choices=["absent", "present"]),
)
)
required_if = [["scope", "pod", ["pod"]]]
module = AnsibleModule(
argument_spec, required_if=required_if, supports_check_mode=True
)
state = module.params["state"]
if not HAS_PURESTORAGE:
module.fail_json(
msg="py-pure-client sdk is required to support 'count' parameter"
)
arrayv5 = get_system(module)
module.params["name"] = sorted(module.params["name"])
api_version = arrayv5._list_available_rest_versions()
if DEFAULT_API_VERSION not in api_version:
module.fail_json(
msg="Default Protection is not supported. Purity//FA 6.3.4, or higher, is required."
)
array = get_array(module)
if module.params["scope"] == "pod":
if not _get_pod(module, array):
module.fail_json(
msg="Invalid pod {0} specified.".format(module.params["pod"])
)
current_default = list(
array.get_container_default_protections(names=[module.params["pod"]]).items
)[0].default_protections
else:
current_default = list(array.get_container_default_protections().items)[
0
].default_protections
for pgroup in range(0, len(module.params["name"])):
if module.params["scope"] == "pod":
pod_name = module.params["pod"] + module.params["name"][pgroup]
else:
pod_name = module.params["name"][pgroup]
if not _get_pg(array, pod_name):
module.fail_json(msg="Protection Group {0} does not exist".format(pod_name))
if state == "present" and not current_default:
create_default(module, array)
elif state == "absent" and not current_default:
module.exit_json(changed=False)
elif state == "present" and current_default:
update_default(module, array, current_default)
elif state == "absent" and current_default and module.params["name"] != [""]:
update_default(module, array, current_default)
elif state == "absent" and current_default and module.params["name"] == [""]:
delete_default(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,234 @@
#!/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: purefa_directory
version_added: '1.5.0'
short_description: Manage FlashArray File System Directories
description:
- Create/Delete FlashArray File Systems
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
name:
description:
- Name of the directory
type: str
required: true
state:
description:
- Define whether the directory should exist or not.
default: present
choices: [ absent, present ]
type: str
filesystem:
description:
- Name of the filesystem the directory links to.
type: str
required: true
path:
description:
- Path of the managed directory in the file system
- If not provided will default to I(name)
type: str
rename:
description:
- Value to rename the specified directory to
type: str
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Create direcotry foo in filesysten bar with path zeta
purestorage.flasharray.purefa_directory:
name: foo
filesystem: bar
path: zeta
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Rename directory foo to fin in filesystem bar
purestorage.flasharray.purefa_directory:
name: foo
rename: fin
filesystem: bar
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete diectory foo in filesystem bar
purestorage.flasharray.purefa_directory:
name: foo
filesystem: bar
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from pypureclient import flasharray
except ImportError:
HAS_PURESTORAGE = False
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
MIN_REQUIRED_API_VERSION = "2.2"
def delete_dir(module, array):
"""Delete a file system directory"""
changed = True
if not module.check_mode:
res = array.delete_directories(
names=[module.params["filesystem"] + ":" + module.params["name"]]
)
if res.status_code != 200:
module.fail_json(
msg="Failed to delete file system {0}. {1}".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def rename_dir(module, array):
"""Rename a file system directory"""
changed = False
target = array.get_directories(
names=[module.params["filesystem"] + ":" + module.params["rename"]]
)
if target.status_code != 200:
if not module.check_mode:
changed = True
directory = flasharray.DirectoryPatch(
name=module.params["filesystem"] + ":" + module.params["rename"]
)
res = array.patch_directories(
names=[module.params["filesystem"] + ":" + module.params["name"]],
directory=directory,
)
if res.status_code != 200:
module.fail_json(
msg="Failed to delete file system {0}".format(module.params["name"])
)
else:
module.fail_json(
msg="Target file system {0} already exists".format(module.params["rename"])
)
module.exit_json(changed=changed)
def create_dir(module, array):
"""Create a file system directory"""
changed = False
if not module.params["path"]:
module.params["path"] = module.params["name"]
all_fs = list(
array.get_directories(file_system_names=[module.params["filesystem"]]).items
)
for check in range(0, len(all_fs)):
if module.params["path"] == all_fs[check].path[1:]:
module.fail_json(
msg="Path {0} already existis in file system {1}".format(
module.params["path"], module.params["filesystem"]
)
)
changed = True
if not module.check_mode:
directory = flasharray.DirectoryPost(
directory_name=module.params["name"], path=module.params["path"]
)
res = array.post_directories(
file_system_names=[module.params["filesystem"]], directory=directory
)
if res.status_code != 200:
module.fail_json(
msg="Failed to create file system {0}. {1}".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
state=dict(type="str", default="present", choices=["absent", "present"]),
filesystem=dict(type="str", required=True),
name=dict(type="str", required=True),
rename=dict(type="str"),
path=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")
array = get_system(module)
api_version = array._list_available_rest_versions()
if MIN_REQUIRED_API_VERSION not in api_version:
module.fail_json(
msg="FlashArray REST version not supported. "
"Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
)
array = get_array(module)
state = module.params["state"]
try:
filesystem = list(
array.get_file_systems(names=[module.params["filesystem"]]).items
)[0]
except Exception:
module.fail_json(
msg="Selected file system {0} does not exist".format(
module.params["filesystem"]
)
)
res = array.get_directories(
names=[module.params["filesystem"] + ":" + module.params["name"]]
)
exists = bool(res.status_code == 200)
if state == "present" and not exists:
create_dir(module, array)
elif (
state == "present"
and exists
and module.params["rename"]
and not filesystem.destroyed
):
rename_dir(module, array)
elif state == "absent" and exists:
delete_dir(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,474 @@
#!/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: purefa_dirsnap
version_added: '1.9.0'
short_description: Manage FlashArray File System Directory Snapshots
description:
- Create/Delete FlashArray File System directory snapshots
- A full snapshot name is constructed in the form of DIR.CLIENT_NAME.SUFFIX
where DIR is the managed directory name, CLIENT_NAME is the client name,
and SUFFIX is the suffix.
- The client visible snapshot name is CLIENT_NAME.SUFFIX.
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
name:
description:
- Name of the directory to snapshot
type: str
required: true
state:
description:
- Define whether the directory snapshot should exist or not.
default: present
choices: [ absent, present ]
type: str
filesystem:
description:
- Name of the filesystem the directory links to.
type: str
required: true
eradicate:
description:
- Define whether to eradicate the snapshot on delete or leave in trash
type: bool
default: false
client:
description:
- The client name portion of the client visible snapshot name
type: str
required: true
suffix:
description:
- Snapshot suffix to use
type: str
new_client:
description:
- The new client name when performing a rename
type: str
version_added: '1.12.0'
new_suffix:
description:
- The new suffix when performing a rename
type: str
version_added: '1.12.0'
rename:
description:
- Whether to rename a directory snapshot
- The snapshot client name and suffix can be changed
- Required with I(new_client) ans I(new_suffix)
type: bool
default: false
version_added: '1.12.0'
keep_for:
description:
- Retention period, after which snapshots will be eradicated
- Specify in seconds. Range 300 - 31536000 (5 minutes to 1 year)
- Value of 0 will set no retention period.
- If not specified on create will default to 0 (no retention period)
type: int
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Create a snapshot direcotry foo in filesysten bar for client test with suffix test
purestorage.flasharray.purefa_dirsnap:
name: foo
filesystem: bar
client: test
suffix: test
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Update retention time for a snapshot foo:bar.client.test
purestorage.flasharray.purefa_dirsnap:
name: foo
filesystem: bar
client: client
suffix: test
keep_for: 300 # 5 minutes
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete snapshot foo:bar.client.test
purestorage.flasharray.purefa_dirsnap:
name: foo
filesystem: bar
client: client
suffix: test
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Recover deleted snapshot foo:bar.client.test
purestorage.flasharray.purefa_dirsnap:
name: foo
filesystem: bar
client: client
suffix: test
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete and eradicate snapshot foo:bar.client.test
purestorage.flasharray.purefa_dirsnap:
name: foo
filesystem: bar
client: client
suffix: test
state: absent
eradicate: true
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Eradicate deleted snapshot foo:bar.client.test
purestorage.flasharray.purefa_dirsnap:
name: foo
filesystem: bar
client: client
suffix: test
eradicate: true
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Rename snapshot
purestorage.flasharray.purefa_dirsnap:
name: foo
filesystem: bar
client: client
suffix: test
rename: true
new_client: client2
new_suffix: test2
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from pypureclient.flasharray import DirectorySnapshotPost, DirectorySnapshotPatch
except ImportError:
HAS_PURESTORAGE = False
import re
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
MIN_REQUIRED_API_VERSION = "2.2"
MIN_RENAME_API_VERSION = "2.10"
def eradicate_snap(module, array):
"""Eradicate a filesystem snapshot"""
changed = True
if not module.check_mode:
snapname = (
module.params["filesystem"]
+ ":"
+ module.params["name"]
+ "."
+ module.params["client"]
+ "."
+ module.params["suffix"]
)
res = array.delete_directory_snapshots(names=[snapname])
if res.status_code != 200:
module.fail_json(
msg="Failed to eradicate filesystem snapshot {0}. Error: {1}".format(
snapname, res.errors[0].message
)
)
module.exit_json(changed=changed)
def delete_snap(module, array):
"""Delete a filesystem snapshot"""
changed = True
if not module.check_mode:
snapname = (
module.params["filesystem"]
+ ":"
+ module.params["name"]
+ "."
+ module.params["client"]
+ "."
+ module.params["suffix"]
)
directory_snapshot = DirectorySnapshotPatch(destroyed=True)
res = array.patch_directory_snapshots(
names=[snapname], directory_snapshot=directory_snapshot
)
if res.status_code != 200:
module.fail_json(
msg="Failed to delete filesystem snapshot {0}. Error: {1}".format(
snapname, res.errors[0].message
)
)
if module.params["eradicate"]:
eradicate_snap(module, array)
module.exit_json(changed=changed)
def update_snap(module, array, snap_detail):
"""Update a filesystem snapshot retention time"""
changed = True
snapname = (
module.params["filesystem"]
+ ":"
+ module.params["name"]
+ "."
+ module.params["client"]
+ "."
+ module.params["suffix"]
)
if module.params["rename"]:
if not module.params["new_client"]:
new_client = module.params["client"]
else:
new_client = module.params["new_client"]
if not module.params["new_suffix"]:
new_suffix = module.params["suffix"]
else:
new_suffix = module.params["new_suffix"]
new_snapname = (
module.params["filesystem"]
+ ":"
+ module.params["name"]
+ "."
+ new_client
+ "."
+ new_suffix
)
directory_snapshot = DirectorySnapshotPatch(
client_name=new_client, suffix=new_suffix
)
if not module.check_mode:
res = array.patch_directory_snapshots(
names=[snapname], directory_snapshot=directory_snapshot
)
if res.status_code != 200:
module.fail_json(
msg="Failed to rename snapshot {0}. Error: {1}".format(
snapname, res.errors[0].message
)
)
else:
snapname = new_snapname
if not module.params["keep_for"] or module.params["keep_for"] == 0:
keep_for = 0
elif 300 <= module.params["keep_for"] <= 31536000:
keep_for = module.params["keep_for"] * 1000
else:
module.fail_json(msg="keep_for not in range of 300 - 31536000")
if not module.check_mode:
if snap_detail.destroyed:
directory_snapshot = DirectorySnapshotPatch(destroyed=False)
res = array.patch_directory_snapshots(
names=[snapname], directory_snapshot=directory_snapshot
)
if res.status_code != 200:
module.fail_json(
msg="Failed to recover snapshot {0}. Error: {1}".format(
snapname, res.errors[0].message
)
)
directory_snapshot = DirectorySnapshotPatch(keep_for=keep_for)
if snap_detail.time_remaining == 0 and keep_for != 0:
res = array.patch_directory_snapshots(
names=[snapname], directory_snapshot=directory_snapshot
)
if res.status_code != 200:
module.fail_json(
msg="Failed to retention time for snapshot {0}. Error: {1}".format(
snapname, res.errors[0].message
)
)
elif snap_detail.time_remaining > 0:
if module.params["rename"] and module.params["keep_for"]:
res = array.patch_directory_snapshots(
names=[snapname], directory_snapshot=directory_snapshot
)
if res.status_code != 200:
module.fail_json(
msg="Failed to retention time for renamed snapshot {0}. Error: {1}".format(
snapname, res.errors[0].message
)
)
module.exit_json(changed=changed)
def create_snap(module, array):
"""Create a filesystem snapshot"""
changed = True
if not module.check_mode:
if not module.params["keep_for"] or module.params["keep_for"] == 0:
keep_for = 0
elif 300 <= module.params["keep_for"] <= 31536000:
keep_for = module.params["keep_for"] * 1000
else:
module.fail_json(msg="keep_for not in range of 300 - 31536000")
directory = module.params["filesystem"] + ":" + module.params["name"]
if module.params["suffix"]:
directory_snapshot = DirectorySnapshotPost(
client_name=module.params["client"],
keep_for=keep_for,
suffix=module.params["suffix"],
)
else:
directory_snapshot = DirectorySnapshotPost(
client_name=module.params["client"], keep_for=keep_for
)
res = array.post_directory_snapshots(
source_names=[directory], directory_snapshot=directory_snapshot
)
if res.status_code != 200:
module.fail_json(
msg="Failed to create client {0} snapshot for {1}. Error: {2}".format(
module.params["client"], directory, res.errors[0].message
)
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
state=dict(type="str", default="present", choices=["absent", "present"]),
filesystem=dict(type="str", required=True),
name=dict(type="str", required=True),
eradicate=dict(type="bool", default=False),
client=dict(type="str", required=True),
suffix=dict(type="str"),
rename=dict(type="bool", default=False),
new_client=dict(type="str"),
new_suffix=dict(type="str"),
keep_for=dict(type="int"),
)
)
required_if = [["state", "absent", ["suffix"]]]
module = AnsibleModule(
argument_spec, required_if=required_if, supports_check_mode=True
)
if module.params["rename"]:
if not module.params["new_client"] and not module.params["new_suffix"]:
module.fail_json(msg="Rename requires one of: new_client, new_suffix")
if not HAS_PURESTORAGE:
module.fail_json(msg="py-pure-client sdk is required for this module")
client_pattern = re.compile(
"^(?=.*[a-zA-Z-])[a-zA-Z0-9]([a-zA-Z0-9-]{0,56}[a-zA-Z0-9])?$"
)
suffix_pattern = re.compile(
"^(?=.*[a-zA-Z-])[a-zA-Z0-9]([a-zA-Z0-9-]{0,63}[a-zA-Z0-9])?$"
)
if module.params["suffix"]:
if not suffix_pattern.match(module.params["suffix"]):
module.fail_json(
msg="Suffix name {0} does not conform to the suffix name rules.".format(
module.params["suffix"]
)
)
if module.params["new_suffix"]:
if not suffix_pattern.match(module.params["new_suffix"]):
module.fail_json(
msg="Suffix rename {0} does not conform to the suffix name rules.".format(
module.params["new_suffix"]
)
)
if module.params["client"]:
if not client_pattern.match(module.params["client"]):
module.fail_json(
msg="Client name {0} does not conform to the client name rules.".format(
module.params["client"]
)
)
array = get_system(module)
api_version = array._list_available_rest_versions()
if MIN_REQUIRED_API_VERSION not in api_version:
module.fail_json(
msg="FlashArray REST version not supported. "
"Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
)
if module.params["rename"] and MIN_RENAME_API_VERSION not in api_version:
module.fail_json(
msg="Directory snapshot rename not supported. "
"Minimum Purity//FA version required: 6.2.1"
)
array = get_array(module)
state = module.params["state"]
snapshot_root = module.params["filesystem"] + ":" + module.params["name"]
if bool(
array.get_directories(
filter='name="' + snapshot_root + '"', total_item_count=True
).total_item_count
== 0
):
module.fail_json(msg="Directory {0} does not exist.".format(snapshot_root))
snap_exists = False
if module.params["suffix"]:
snap_detail = array.get_directory_snapshots(
filter="name='"
+ snapshot_root
+ "."
+ module.params["client"]
+ "."
+ module.params["suffix"]
+ "'",
total_item_count=True,
)
if bool(snap_detail.status_code == 200):
snap_exists = bool(snap_detail.total_item_count != 0)
if snap_exists:
snap_facts = list(snap_detail.items)[0]
if state == "present" and not snap_exists:
create_snap(module, array)
elif state == "present" and snap_exists and module.params["suffix"]:
update_snap(module, array, snap_facts)
elif state == "absent" and snap_exists and not snap_facts.destroyed:
delete_snap(module, array)
elif (
state == "absent"
and snap_exists
and snap_facts.destroyed
and module.params["eradicate"]
):
eradicate_snap(module, array)
else:
module.exit_json(changed=False)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,349 @@
#!/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: purefa_dns
version_added: '1.0.0'
short_description: Configure FlashArray DNS settings
description:
- Set or erase configuration for the DNS settings.
- Nameservers provided will overwrite any existing nameservers.
- From Purity//FA 6.3.3 DNS setting for FA-File can be configured seperately
to the management DNS settings
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
name:
description:
- Name of the DNS configuration.
- Default value only supported for management service
default: management
type: str
version_added: 1.14.0
state:
description:
- Set or delete directory service configuration
default: present
type: str
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
service:
description:
- Type of ser vice the DNS will work with
type: str
version_added: 1.14.0
choices: [ management, file ]
default: management
source:
description:
- A virtual network interface (vif)
type: str
version_added: 1.14.0
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Delete exisitng DNS settings
purestorage.flasharray.purefa_dns:
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Set managemnt DNS settings
purestorage.flasharray.purefa_dns:
domain: purestorage.com
nameservers:
- 8.8.8.8
- 8.8.4.4
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Set file DNS settings
purestorage.flasharray.purefa_dns:
domain: purestorage.com
nameservers:
- 8.8.8.8
- 8.8.4.4
name: ad_dns
service: file
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from pypureclient import flasharray
except ImportError:
HAS_PURESTORAGE = False
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
MULTIPLE_DNS = "2.15"
def remove(duplicate):
final_list = []
for num in duplicate:
if num not in final_list:
final_list.append(num)
return final_list
def _get_source(module, array):
res = array.get_network_interfaces(names=[module.params["source"]])
if res.status_code == 200:
return True
else:
return False
def delete_dns(module, array):
"""Delete DNS settings"""
changed = False
current_dns = array.get_dns()
if current_dns["domain"] == "" and current_dns["nameservers"] == [""]:
module.exit_json(changed=changed)
else:
try:
changed = True
if not module.check_mode:
array.set_dns(domain="", nameservers=[])
except Exception:
module.fail_json(msg="Delete DNS settigs failed")
module.exit_json(changed=changed)
def create_dns(module, array):
"""Set DNS settings"""
changed = False
current_dns = array.get_dns()
if current_dns["domain"] != module.params["domain"] or sorted(
module.params["nameservers"]
) != sorted(current_dns["nameservers"]):
try:
changed = True
if not module.check_mode:
array.set_dns(
domain=module.params["domain"],
nameservers=module.params["nameservers"][0:3],
)
except Exception:
module.fail_json(msg="Set DNS settings failed: Check configuration")
module.exit_json(changed=changed)
def update_multi_dns(module, array):
"""Update a DNS configuration"""
changed = False
current_dns = list(array.get_dns(names=[module.params["name"]]).items)[0]
new_dns = current_dns
if module.params["domain"] and current_dns.domain != module.params["domain"]:
new_dns.domain = module.params["domain"]
changed = True
if module.params["service"] and current_dns.services != [module.params["service"]]:
module.fail_json(msg="Changing service type is not permitted")
if module.params["nameservers"] and sorted(current_dns.nameservers) != sorted(
module.params["nameservers"]
):
new_dns.nameservers = module.params["nameservers"]
changed = True
if (
module.params["source"] or module.params["source"] == ""
) and current_dns.source.name != module.params["source"]:
new_dns.source.name = module.params["source"]
changed = True
if changed and not module.check_mode:
res = array.patch_dns(
names=[module.params["name"]],
dns=flasharray.Dns(
domain=new_dns.domain,
nameservers=new_dns.nameservers,
source=flasharray.ReferenceNoId(module.params["source"]),
),
)
if res.status_code != 200:
module.fail_json(
msg="Update to DNS service {0} failed. Error: {1}".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def delete_multi_dns(module, array):
"""Delete a DNS configuration"""
changed = True
if module.params["name"] == "management":
res = array.update_dns(
names=[module.params["name"]],
dns=flasharray.DnsPatch(
domain=module.params["domain"],
nameservers=module.params["nameservers"],
),
)
if res.status_code != 200:
module.fail_json(
msg="Management DNS configuration not deleted. Error: {0}".format(
res.errors[0].message
)
)
else:
if not module.check_mode:
res = array.delete_dns(names=[module.params["name"]])
if res.status_code != 200:
module.fail_json(
msg="Failed to delete DNS configuration {0}. Error: {1}".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def create_multi_dns(module, array):
"""Create a DNS configuration"""
changed = True
if not module.check_mode:
if module.params["service"] == "file":
if module.params["source"]:
res = array.post_dns(
names=[module.params["name"]],
dns=flasharray.DnsPost(
services=[module.params["service"]],
domain=module.params["domain"],
nameservers=module.params["nameservers"],
source=flasharray.ReferenceNoId(
module.params["source"].lower()
),
),
)
else:
res = array.post_dns(
names=[module.params["name"]],
dns=flasharray.DnsPost(
services=[module.params["service"]],
domain=module.params["domain"],
nameservers=module.params["nameservers"],
),
)
else:
res = array.create_dns(
names=[module.params["name"]],
services=[module.params["service"]],
domain=module.params["domain"],
nameservers=module.params["nameservers"],
)
if res.status_code != 200:
module.fail_json(
msg="Failed to create {0} DNS configuration {1}. Error: {2}".format(
module.params["service"],
module.params["name"],
res.errors[0].message,
)
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
state=dict(type="str", default="present", choices=["absent", "present"]),
name=dict(type="str", default="management"),
service=dict(
type="str", default="management", choices=["management", "file"]
),
domain=dict(type="str"),
source=dict(type="str"),
nameservers=dict(type="list", elements="str"),
)
)
module = AnsibleModule(argument_spec, supports_check_mode=True)
state = module.params["state"]
array = get_system(module)
api_version = array._list_available_rest_versions()
if module.params["nameservers"]:
module.params["nameservers"] = remove(module.params["nameservers"])
if module.params["service"] == "management":
module.params["nameservers"] = module.params["nameservers"][0:3]
if MULTIPLE_DNS in api_version:
array = get_array(module)
configs = list(array.get_dns().items)
exists = False
for config in range(0, len(configs)):
if configs[config].name == module.params["name"]:
exists = True
if (
module.params["service"] == "management"
and module.params["name"] != "management"
and not exists
):
module.warn("Overriding configuration name to management")
module.params["name"] = "management"
if module.params["source"] and not _get_source(module, array):
module.fail_json(
msg="Specified VIF {0} does not exist.".format(module.params["source"])
)
if state == "present" and exists:
update_multi_dns(module, array)
elif state == "present" and not exists:
if len(configs) == 2:
module.fail_json(
msg="Only 2 DNS configurations are currently "
"supported. One for management and one for file services"
)
create_multi_dns(module, array)
elif exists and state == "absent":
delete_multi_dns(module, array)
else:
module.exit_json(changed=False)
else:
if state == "absent":
delete_dns(module, array)
elif state == "present":
if not module.params["domain"] or not module.params["nameservers"]:
module.fail_json(
msg="`domain` and `nameservers` are required for DNS configuration"
)
create_dns(module, array)
else:
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,609 @@
#!/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: purefa_ds
version_added: '1.0.0'
short_description: Configure FlashArray Directory Service
description:
- Set or erase configuration for the directory service. There is no facility
to SSL certificates at this time. Use the FlashArray GUI for this
additional configuration work.
- To modify an existing directory service configuration you must first delete
an exisitng configuration and then recreate with new settings.
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
state:
type: str
description:
- Create or delete directory service configuration
default: present
choices: [ absent, present ]
enable:
description:
- Whether to enable or disable directory service support.
default: false
type: bool
dstype:
description:
- The type of directory service to work on
choices: [ management, data ]
type: str
default: management
uri:
type: list
elements: str
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.
base_dn:
type: str
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.
bind_password:
type: str
description:
- Sets the password of the bind_user user name account.
force_bind_password:
type: bool
default: true
description:
- Will force the bind password to be reset even if the bind user password
is unchanged.
- If set to I(false) and I(bind_user) is unchanged the password will not
be reset.
version_added: 1.14.0
bind_user:
type: str
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.
group_base:
type: str
description:
- Specifies where the configured groups are 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.
- Not Supported from Purity 5.2.0 or higher.
Use I(purestorage.flasharray.purefa_dsrole) module.
ro_group:
type: str
description:
- Sets the common Name (CN) of the configured directory service group
containing users with read-only privileges on the FlashArray. This
name should be just the Common Name of the group without the CN=
specifier. Common Names should not exceed 64 characters in length.
- Not Supported from Purity 5.2.0 or higher.
Use I(purestorage.flasharray.purefa_dsrole) module.
sa_group:
type: str
description:
- Sets the common Name (CN) of the configured directory service group
containing administrators with storage-related privileges on the
FlashArray. This name should be just the Common Name of the group
without the CN= specifier. Common Names should not exceed 64
characters in length.
- Not Supported from Purity 5.2.0 or higher.
Use I(purestorage.flasharray.purefa_dsrole) module.
aa_group:
type: str
description:
- Sets the common Name (CN) of the directory service group containing
administrators with full privileges when managing the FlashArray.
The name should be just the Common Name of the group without the
CN= specifier. Common Names should not exceed 64 characters in length.
- Not Supported from Purity 5.2.0 or higher.
Use I(purestorage.flasharray.purefa_dsrole) module.
user_login:
type: str
description:
- User login attribute in the structure of the configured LDAP servers.
Typically the attribute field that holds the users unique login name.
Default value is I(sAMAccountName) for Active Directory or I(uid)
for all other directory services
- Supported from Purity 6.0 or higher.
user_object:
type: str
description:
- Value of the object class for a management LDAP user.
Defaults to I(User) for Active Directory servers, I(posixAccount) or
I(shadowAccount) for OpenLDAP servers dependent on the group type
of the server, or person for all other directory servers.
- Supported from Purity 6.0 or higher.
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Delete existing directory service
purestorage.flasharray.purefa_ds:
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Create directory service (disabled) - Pre-5.2.0
purestorage.flasharray.purefa_ds:
uri: "ldap://lab.purestorage.com"
base_dn: "DC=lab,DC=purestorage,DC=com"
bind_user: Administrator
bind_password: password
group_base: "OU=Pure-Admin"
ro_group: PureReadOnly
sa_group: PureStorage
aa_group: PureAdmin
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Create directory service (disabled) - 5.2.0 or higher
purestorage.flasharray.purefa_ds:
dstype: management
uri: "ldap://lab.purestorage.com"
base_dn: "DC=lab,DC=purestorage,DC=com"
bind_user: Administrator
bind_password: password
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Enable existing directory service
purestorage.flasharray.purefa_ds:
enable: true
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Disable existing directory service
purestorage.flasharray.purefa_ds:
enable: false
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Create directory service (enabled) - Pre-5.2.0
purestorage.flasharray.purefa_ds:
enable: true
uri: "ldap://lab.purestorage.com"
base_dn: "DC=lab,DC=purestorage,DC=com"
bind_user: Administrator
bind_password: password
group_base: "OU=Pure-Admin"
ro_group: PureReadOnly
sa_group: PureStorage
aa_group: PureAdmin
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Create directory service (enabled) - 5.2.0 or higher
purestorage.flasharray.purefa_ds:
enable: true
dstype: management
uri: "ldap://lab.purestorage.com"
base_dn: "DC=lab,DC=purestorage,DC=com"
bind_user: Administrator
bind_password: password
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from pypureclient import flasharray
except ImportError:
HAS_PURESTORAGE = False
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_array,
get_system,
purefa_argument_spec,
)
DS_ROLE_REQUIRED_API_VERSION = "1.16"
FAFILES_API_VERSION = "2.2"
def disable_ds(module, array):
"""Disable Directory Service"""
changed = True
if not module.check_mode:
try:
array.disable_directory_service()
except Exception:
module.fail_json(msg="Disable Directory Service failed")
module.exit_json(changed=changed)
def enable_ds(module, array):
"""Enable Directory Service"""
changed = False
api_version = array._list_available_rest_versions()
if DS_ROLE_REQUIRED_API_VERSION in api_version:
try:
roles = array.list_directory_service_roles()
enough_roles = False
for role in range(0, len(roles)):
if roles[role]["group_base"]:
enough_roles = True
if enough_roles:
changed = True
if not module.check_mode:
array.enable_directory_service()
else:
module.fail_json(
msg="Cannot enable directory service - please create a directory service role"
)
except Exception:
module.fail_json(msg="Enable Directory Service failed: Check Configuration")
else:
try:
changed = True
if not module.check_mode:
array.enable_directory_service()
except Exception:
module.fail_json(msg="Enable Directory Service failed: Check Configuration")
module.exit_json(changed=changed)
def delete_ds(module, array):
"""Delete Directory Service"""
changed = True
if not module.check_mode:
try:
api_version = array._list_available_rest_versions()
array.set_directory_service(enabled=False)
if DS_ROLE_REQUIRED_API_VERSION in api_version:
array.set_directory_service(
uri=[""], base_dn="", bind_user="", bind_password="", certificate=""
)
else:
array.set_directory_service(
uri=[""],
base_dn="",
group_base="",
bind_user="",
bind_password="",
readonly_group="",
storage_admin_group="",
array_admin_group="",
certificate="",
)
except Exception:
module.fail_json(msg="Delete Directory Service failed")
module.exit_json(changed=changed)
def delete_ds_v6(module, array):
"""Delete Directory Service"""
changed = True
if module.params["dstype"] == "management":
management = flasharray.DirectoryServiceManagement(
user_login_attribute="", user_object_class=""
)
directory_service = flasharray.DirectoryService(
uris=[""],
base_dn="",
bind_user="",
bind_password="",
enabled=False,
services=module.params["dstype"],
management=management,
)
else:
directory_service = flasharray.DirectoryService(
uris=[""],
base_dn="",
bind_user="",
bind_password="",
enabled=False,
services=module.params["dstype"],
)
if not module.check_mode:
res = array.patch_directory_services(
names=[module.params["dstype"]], directory_service=directory_service
)
if res.status_code != 200:
module.fail_json(
msg="Delete {0} Directory Service failed. Error message: {1}".format(
module.params["dstype"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def create_ds(module, array):
"""Create Directory Service"""
changed = False
if None in (
module.params["bind_password"],
module.params["bind_user"],
module.params["base_dn"],
module.params["uri"],
):
module.fail_json(
msg="Parameters 'bind_password', 'bind_user', 'base_dn' and 'uri' are all required"
)
api_version = array._list_available_rest_versions()
if DS_ROLE_REQUIRED_API_VERSION in api_version:
try:
changed = True
if not module.check_mode:
array.set_directory_service(
uri=module.params["uri"],
base_dn=module.params["base_dn"],
bind_user=module.params["bind_user"],
bind_password=module.params["bind_password"],
)
roles = array.list_directory_service_roles()
enough_roles = False
for role in range(0, len(roles)):
if roles[role]["group_base"]:
enough_roles = True
if enough_roles:
array.set_directory_service(enabled=module.params["enable"])
else:
module.fail_json(
msg="Cannot enable directory service - please create a directory service role"
)
except Exception:
module.fail_json(msg="Create Directory Service failed: Check configuration")
else:
groups_rule = [
not module.params["ro_group"],
not module.params["sa_group"],
not module.params["aa_group"],
]
if all(groups_rule):
module.fail_json(msg="At least one group must be configured")
try:
changed = True
if not module.check_mode:
array.set_directory_service(
uri=module.params["uri"],
base_dn=module.params["base_dn"],
group_base=module.params["group_base"],
bind_user=module.params["bind_user"],
bind_password=module.params["bind_password"],
readonly_group=module.params["ro_group"],
storage_admin_group=module.params["sa_group"],
array_admin_group=module.params["aa_group"],
)
array.set_directory_service(enabled=module.params["enable"])
except Exception:
module.fail_json(msg="Create Directory Service failed: Check configuration")
module.exit_json(changed=changed)
def update_ds_v6(module, array):
"""Update Directory Service"""
changed = False
ds_change = False
password_required = False
dirserv = list(
array.get_directory_services(
filter="name='" + module.params["dstype"] + "'"
).items
)[0]
current_ds = dirserv
if module.params["uri"] and current_ds.uris is None:
password_required = True
if current_ds.uris != module.params["uri"]:
uris = module.params["uri"]
ds_change = True
else:
uris = current_ds.uris
try:
base_dn = current_ds.base_dn
except AttributeError:
base_dn = ""
try:
bind_user = current_ds.bind_user
except AttributeError:
bind_user = ""
if module.params["base_dn"] != "" and module.params["base_dn"] != base_dn:
base_dn = module.params["base_dn"]
ds_change = True
if module.params["bind_user"] != "":
bind_user = module.params["bind_user"]
if module.params["bind_user"] != bind_user:
password_required = True
ds_change = True
elif module.params["force_bind_password"]:
password_required = True
ds_change = True
if module.params["bind_password"] is not None and password_required:
bind_password = module.params["bind_password"]
ds_change = True
if module.params["enable"] != current_ds.enabled:
ds_change = True
if password_required and not module.params["bind_password"]:
module.fail_json(msg="'bind_password' must be provided for this task")
if module.params["dstype"] == "management":
try:
user_login = current_ds.management.user_login_attribute
except AttributeError:
user_login = ""
try:
user_object = current_ds.management.user_object_class
except AttributeError:
user_object = ""
if (
module.params["user_object"] is not None
and user_object != module.params["user_object"]
):
user_object = module.params["user_object"]
ds_change = True
if (
module.params["user_login"] is not None
and user_login != module.params["user_login"]
):
user_login = module.params["user_login"]
ds_change = True
management = flasharray.DirectoryServiceManagement(
user_login_attribute=user_login, user_object_class=user_object
)
if password_required:
directory_service = flasharray.DirectoryService(
uris=uris,
base_dn=base_dn,
bind_user=bind_user,
bind_password=bind_password,
enabled=module.params["enable"],
services=module.params["dstype"],
management=management,
)
else:
directory_service = flasharray.DirectoryService(
uris=uris,
base_dn=base_dn,
bind_user=bind_user,
enabled=module.params["enable"],
services=module.params["dstype"],
management=management,
)
else:
if password_required:
directory_service = flasharray.DirectoryService(
uris=uris,
base_dn=base_dn,
bind_user=bind_user,
bind_password=bind_password,
enabled=module.params["enable"],
services=module.params["dstype"],
)
else:
directory_service = flasharray.DirectoryService(
uris=uris,
base_dn=base_dn,
bind_user=bind_user,
enabled=module.params["enable"],
services=module.params["dstype"],
)
if ds_change:
changed = True
if not module.check_mode:
res = array.patch_directory_services(
names=[module.params["dstype"]], directory_service=directory_service
)
if res.status_code != 200:
module.fail_json(
msg="{0} Directory Service failed. Error message: {1}".format(
module.params["dstype"].capitalize(), res.errors[0].message
)
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
uri=dict(type="list", elements="str"),
state=dict(type="str", default="present", choices=["absent", "present"]),
enable=dict(type="bool", default=False),
force_bind_password=dict(type="bool", default=True, no_log=True),
bind_password=dict(type="str", no_log=True),
bind_user=dict(type="str"),
base_dn=dict(type="str"),
group_base=dict(type="str"),
user_login=dict(type="str"),
user_object=dict(type="str"),
ro_group=dict(type="str"),
sa_group=dict(type="str"),
aa_group=dict(type="str"),
dstype=dict(
type="str", default="management", choices=["management", "data"]
),
)
)
module = AnsibleModule(argument_spec, supports_check_mode=True)
array = get_system(module)
api_version = array._list_available_rest_versions()
if not HAS_PURESTORAGE:
module.fail_json(msg="py-pure-client sdk is required to for this module")
if FAFILES_API_VERSION in api_version:
arrayv6 = get_array(module)
if module.params["dstype"] == "data":
if FAFILES_API_VERSION in api_version:
if len(list(arrayv6.get_directory_services().items)) == 1:
module.warn("FA-Files is not enabled - ignoring")
module.exit_json(changed=False)
else:
module.fail_json(
msg="'data' directory service requires Purity//FA 6.0.0 or higher"
)
state = module.params["state"]
ds_exists = False
if FAFILES_API_VERSION in api_version:
dirserv = list(
arrayv6.get_directory_services(
filter="name='" + module.params["dstype"] + "'"
).items
)[0]
if state == "absent" and dirserv.uris != []:
delete_ds_v6(module, arrayv6)
else:
update_ds_v6(module, arrayv6)
else:
dirserv = array.get_directory_service()
ds_enabled = dirserv["enabled"]
if dirserv["base_dn"]:
ds_exists = True
if state == "absent" and ds_exists:
delete_ds(module, array)
elif ds_exists and module.params["enable"] and ds_enabled:
module.warn(
"To update an existing directory service configuration in Purity//FA 5.x, please delete and recreate"
)
module.exit_json(changed=False)
elif ds_exists and not module.params["enable"] and ds_enabled:
disable_ds(module, array)
elif ds_exists and module.params["enable"] and not ds_enabled:
enable_ds(module, array)
elif not ds_exists and state == "present":
create_ds(module, array)
else:
module.exit_json(changed=False)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,200 @@
#!/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: purefa_dsrole
version_added: '1.0.0'
short_description: Configure FlashArray Directory Service Roles
description:
- Set or erase directory services role configurations.
- Only available for FlashArray running Purity 5.2.0 or higher
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
state:
description:
- Create or delete directory service role
type: str
default: present
choices: [ absent, present ]
role:
description:
- The directory service role to work on
type: str
required: true
choices: [ array_admin, ops_admin, readonly, storage_admin ]
group_base:
type: str
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.
group:
type: str
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.
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Delete exisitng array_admin directory service role
purestorage.flasharray.purefa_dsrole:
role: array_admin
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Create array_admin directory service role
purestorage.flasharray.purefa_dsrole:
role: array_admin
group_base: "OU=PureGroups,OU=SANManagers"
group: pureadmins
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Update ops_admin directory service role
purestorage.flasharray.purefa_dsrole:
role: ops_admin
group_base: "OU=PureGroups"
group: opsgroup
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
purefa_argument_spec,
)
def update_role(module, array):
"""Update Directory Service Role"""
changed = False
role = array.list_directory_service_roles(names=[module.params["role"]])
if (
role[0]["group_base"] != module.params["group_base"]
or role[0]["group"] != module.params["group"]
):
try:
changed = True
if not module.check_mode:
array.set_directory_service_roles(
names=[module.params["role"]],
group_base=module.params["group_base"],
group=module.params["group"],
)
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, array):
"""Delete Directory Service Role"""
changed = True
if not module.check_mode:
try:
array.set_directory_service_roles(
names=[module.params["role"]], group_base="", group=""
)
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, array):
"""Create Directory Service Role"""
changed = False
if not module.params["group"] == "" or not module.params["group_base"] == "":
changed = True
if not module.check_mode:
try:
array.set_directory_service_roles(
names=[module.params["role"]],
group_base=module.params["group_base"],
group=module.params["group"],
)
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 = purefa_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
)
state = module.params["state"]
array = get_system(module)
role_configured = False
role = array.list_directory_service_roles(names=[module.params["role"]])
if role[0]["group"] is not None:
role_configured = True
if state == "absent" and role_configured:
delete_role(module, array)
elif role_configured and state == "present":
update_role(module, array)
elif not role_configured and state == "present":
create_role(module, array)
else:
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,347 @@
#!/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: purefa_endpoint
short_description: Manage VMware protocol-endpoints on Pure Storage FlashArrays
version_added: '1.0.0'
description:
- Create, delete or eradicate the an endpoint on a Pure Storage FlashArray.
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
name:
description:
- The name of the endpoint.
type: str
required: true
state:
description:
- Define whether the endpoint should exist or not.
default: present
choices: [ absent, present ]
type: str
eradicate:
description:
- Define whether to eradicate the endpoint on delete or leave in trash.
type: bool
default: 'no'
rename:
description:
- Value to rename the specified endpoint to.
- Rename only applies to the container the current endpoint is in.
type: str
host:
description:
- name of host to attach endpoint to
type: str
hgroup:
description:
- name of hostgroup to attach endpoint to
type: str
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Create new endpoint named foo
purestorage.flasharray.purefa_endpoint:
name: test-endpoint
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: present
- name: Delete and eradicate endpoint named foo
purestorage.flasharray.purefa_endpoint:
name: foo
eradicate: yes
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: absent
- name: Rename endpoint foor to bar
purestorage.flasharray.purefa_endpoint:
name: foo
rename: bar
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
volume:
description: A dictionary describing the changed volume. Only some
attributes below will be returned with various actions.
type: dict
returned: success
contains:
source:
description: Volume name of source volume used for volume copy
type: str
serial:
description: Volume serial number
type: str
sample: '361019ECACE43D83000120A4'
created:
description: Volume creation time
type: str
sample: '2019-03-13T22:49:24Z'
name:
description: Volume name
type: str
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
purefa_argument_spec,
)
VGROUPS_API_VERSION = "1.13"
def get_volume(volume, array):
"""Return Volume or None"""
try:
return array.get_volume(volume, pending=True)
except Exception:
return None
def get_target(volume, array):
"""Return Volume or None"""
try:
return array.get_volume(volume, pending=True)
except Exception:
return None
def get_endpoint(vol, array):
"""Return Endpoint or None"""
try:
return array.get_volume(vol, protocol_endpoint=True)
except Exception:
return None
def get_destroyed_endpoint(vol, array):
"""Return Endpoint Endpoint or None"""
try:
return bool(
array.get_volume(vol, protocol_endpoint=True, pending=True)[
"time_remaining"
]
!= ""
)
except Exception:
return None
def check_vgroup(module, array):
"""Check is the requested VG to create volume in exists"""
vg_exists = False
vg_name = module.params["name"].split("/")[0]
try:
vgs = array.list_vgroups()
except Exception:
module.fail_json(msg="Failed to get volume groups list. Check array.")
for vgroup in range(0, len(vgs)):
if vg_name == vgs[vgroup]["name"]:
vg_exists = True
break
return vg_exists
def create_endpoint(module, array):
"""Create Endpoint"""
changed = False
volfact = []
if "/" in module.params["name"] and not check_vgroup(module, array):
module.fail_json(
msg="Failed to create endpoint {0}. Volume Group does not exist.".format(
module.params["name"]
)
)
try:
changed = True
if not module.check_mode:
volfact = array.create_conglomerate_volume(module.params["name"])
except Exception:
module.fail_json(
msg="Endpoint {0} creation failed.".format(module.params["name"])
)
if module.params["host"]:
try:
if not module.check_mode:
array.connect_host(module.params["host"], module.params["name"])
except Exception:
module.fail_json(
msg="Failed to attach endpoint {0} to host {1}.".format(
module.params["name"], module.params["host"]
)
)
if module.params["hgroup"]:
try:
if not module.check_mode:
array.connect_hgroup(module.params["hgroup"], module.params["name"])
except Exception:
module.fail_json(
msg="Failed to attach endpoint {0} to hostgroup {1}.".format(
module.params["name"], module.params["hgroup"]
)
)
module.exit_json(changed=changed, volume=volfact)
def rename_endpoint(module, array):
"""Rename endpoint within a container, ie vgroup or local array"""
changed = False
volfact = []
target_name = module.params["rename"]
if "/" in module.params["rename"] or "::" in module.params["rename"]:
module.fail_json(msg="Target endpoint cannot include a container name")
if "/" in module.params["name"]:
vgroup_name = module.params["name"].split("/")[0]
target_name = vgroup_name + "/" + module.params["rename"]
if get_target(target_name, array) or get_destroyed_endpoint(target_name, array):
module.fail_json(msg="Target endpoint {0} already exists.".format(target_name))
else:
try:
changed = True
if not module.check_mode:
volfact = array.rename_volume(module.params["name"], target_name)
except Exception:
module.fail_json(
msg="Rename endpoint {0} to {1} failed.".format(
module.params["name"], module.params["rename"]
)
)
module.exit_json(changed=changed, volume=volfact)
def delete_endpoint(module, array):
"""Delete Endpoint"""
changed = True
volfact = []
if not module.check_mode:
try:
array.destroy_volume(module.params["name"])
if module.params["eradicate"]:
try:
volfact = array.eradicate_volume(module.params["name"])
except Exception:
module.fail_json(
msg="Eradicate endpoint {0} failed.".format(
module.params["name"]
)
)
except Exception:
module.fail_json(
msg="Delete endpoint {0} failed.".format(module.params["name"])
)
module.exit_json(changed=changed, volume=volfact)
def recover_endpoint(module, array):
"""Recover Deleted Endpoint"""
changed = True
volfact = []
if not module.check_mode:
try:
array.recover_volume(module.params["name"])
except Exception:
module.fail_json(
msg="Recovery of endpoint {0} failed".format(module.params["name"])
)
module.exit_json(changed=changed, volume=volfact)
def eradicate_endpoint(module, array):
"""Eradicate Deleted Endpoint"""
changed = True
volfact = []
if not module.check_mode:
if module.params["eradicate"]:
try:
array.eradicate_volume(module.params["name"], protocol_endpoint=True)
except Exception:
module.fail_json(
msg="Eradication of endpoint {0} failed".format(
module.params["name"]
)
)
module.exit_json(changed=changed, volume=volfact)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
name=dict(type="str", required=True),
rename=dict(type="str"),
host=dict(type="str"),
hgroup=dict(type="str"),
eradicate=dict(type="bool", default=False),
state=dict(type="str", default="present", choices=["absent", "present"]),
)
)
mutually_exclusive = [["rename", "eradicate"], ["host", "hgroup"]]
module = AnsibleModule(
argument_spec, mutually_exclusive=mutually_exclusive, supports_check_mode=True
)
state = module.params["state"]
destroyed = False
array = get_system(module)
api_version = array._list_available_rest_versions()
if VGROUPS_API_VERSION not in api_version:
module.fail_json(
msg="Purity version does not support endpoints. Please contact support"
)
volume = get_volume(module.params["name"], array)
if volume:
module.fail_json(
msg="Volume {0} is an true volume. Please use the purefa_volume module".format(
module.params["name"]
)
)
endpoint = get_endpoint(module.params["name"], array)
if not endpoint:
destroyed = get_destroyed_endpoint(module.params["name"], array)
if state == "present" and not endpoint and not destroyed:
create_endpoint(module, array)
elif state == "present" and endpoint and module.params["rename"]:
rename_endpoint(module, array)
elif state == "present" and destroyed:
recover_endpoint(module, array)
elif state == "absent" and endpoint:
delete_endpoint(module, array)
elif state == "absent" and destroyed:
eradicate_endpoint(module, array)
elif state == "absent" and not endpoint and not volume:
module.exit_json(changed=False)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,117 @@
#!/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: purefa_eradication
version_added: '1.9.0'
short_description: Configure Pure Storage FlashArray Eradication Timer
description:
- Configure the eradication timer for destroyed items on a FlashArray.
- Valid values are integer days from 1 to 30. Default is 1.
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
timer:
description:
- Set the eradication timer for the FlashArray
- Allowed values are integers from 1 to 30. Default is 1
default: 1
type: int
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Set eradication timer to 30 days
purestorage.flasharray.purefa_eradication:
timer: 30
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Set eradication timer to 1 day
purestorage.flasharray.purefa_eradication:
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from pypureclient.flasharray import Arrays, EradicationConfig
except ImportError:
HAS_PURESTORAGE = False
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
SEC_PER_DAY = 86400000
ERADICATION_API_VERSION = "2.6"
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
timer=dict(type="int", default="1"),
)
)
module = AnsibleModule(argument_spec, supports_check_mode=True)
if not 30 >= module.params["timer"] >= 1:
module.fail_json(msg="Eradication Timer must be between 1 and 30 days.")
if not HAS_PURESTORAGE:
module.fail_json(msg="py-pure-client sdk is required for this module")
array = get_system(module)
api_version = array._list_available_rest_versions()
changed = False
if ERADICATION_API_VERSION in api_version:
array = get_array(module)
current_timer = (
list(array.get_arrays().items)[0].eradication_config.eradication_delay
/ SEC_PER_DAY
)
if module.params["timer"] != current_timer:
changed = True
if not module.check_mode:
new_timer = SEC_PER_DAY * module.params["timer"]
eradication_config = EradicationConfig(eradication_delay=new_timer)
res = array.patch_arrays(
array=Arrays(eradication_config=eradication_config)
)
if res.status_code != 200:
module.fail_json(
msg="Failed to change Eradication Timer. Error: {0}".format(
res.errors[0].message
)
)
else:
module.fail_json(
msg="Purity version does not support changing Eradication Timer"
)
module.exit_json(changed=changed)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,117 @@
#!/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: purefa_eula
version_added: '1.0.0'
short_description: Sign Pure Storage FlashArray EULA
description:
- Sign the FlashArray 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.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Sign EULA for FlashArray
purestorage.flasharray.purefa_eula:
company: "ACME Storage, Inc."
name: "Fred Bloggs"
title: "Storage Manager"
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
purefa_argument_spec,
)
EULA_API_VERSION = "1.17"
def set_eula(module, array):
"""Sign EULA"""
changed = False
try:
current_eula = array.get_eula()
except Exception:
module.fail_json(msg="Failed to get current EULA")
if (
current_eula["acceptance"]["company"] != module.params["company"]
or current_eula["acceptance"]["title"] != module.params["title"]
or current_eula["acceptance"]["name"] != module.params["name"]
):
try:
changed = True
if not module.check_mode:
array.set_eula(
company=module.params["company"],
title=module.params["title"],
name=module.params["name"],
)
except Exception:
module.fail_json(msg="Signing EULA failed")
module.exit_json(changed=changed)
def main():
argument_spec = purefa_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)
array = get_system(module)
api_version = array._list_available_rest_versions()
if EULA_API_VERSION in api_version:
set_eula(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,251 @@
#!/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: purefa_export
version_added: '1.5.0'
short_description: Manage FlashArray File System Exports
description:
- Create/Delete FlashArray File Systems Exports
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
name:
description:
- Name of the export
type: str
required: true
state:
description:
- Define whether the export should exist or not.
- You must specify an NFS or SMB policy, or both on creation and deletion.
default: present
choices: [ absent, present ]
type: str
filesystem:
description:
- Name of the filesystem the export applies to
type: str
required: true
directory:
description:
- Name of the managed directory in the file system the export applies to
type: str
required: true
nfs_policy:
description:
- Name of NFS Policy to apply to the export
type: str
smb_policy:
description:
- Name of SMB Policy to apply to the export
type: str
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Create NFS and SMB exports for directory foo in filesysten bar
purestorage.flasharray.purefa_export:
name: export1
filesystem: bar
directory: foo
nfs_policy: nfs-example
smb_polict: smb-example
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete NFS export for directory foo in filesystem bar
purestorage.flasharray.purefa_export:
name: export1
filesystem: bar
directory: foo
nfs_policy: nfs-example
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from pypureclient import flasharray
except ImportError:
HAS_PURESTORAGE = False
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
MIN_REQUIRED_API_VERSION = "2.3"
def delete_export(module, array):
"""Delete a file system export"""
changed = False
all_policies = []
directory = module.params["filesystem"] + ":" + module.params["directory"]
if not module.params["nfs_policy"] and not module.params["smb_policy"]:
module.fail_json(msg="At least one policy must be provided")
if module.params["nfs_policy"]:
policy_exists = bool(
array.get_directory_exports(
export_names=[module.params["name"]],
policy_names=[module.params["nfs_policy"]],
directory_names=[directory],
).status_code
== 200
)
if policy_exists:
all_policies.append(module.params["nfs_policy"])
if module.params["smb_policy"]:
policy_exists = bool(
array.get_directory_exports(
export_names=[module.params["name"]],
policy_names=[module.params["smb_policy"]],
directory_names=[directory],
).status_code
== 200
)
if policy_exists:
all_policies.append(module.params["smb_policy"])
if all_policies:
changed = True
if not module.check_mode:
res = array.delete_directory_exports(
export_names=[module.params["name"]], policy_names=all_policies
)
if res.status_code != 200:
module.fail_json(
msg="Failed to delete file system export {0}. {1}".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def create_export(module, array):
"""Create a file system export"""
changed = False
if not module.params["nfs_policy"] and not module.params["smb_policy"]:
module.fail_json(msg="At least one policy must be provided")
all_policies = []
if module.params["nfs_policy"]:
if bool(
array.get_policies_nfs(names=[module.params["nfs_policy"]]).status_code
!= 200
):
module.fail_json(
msg="NFS Policy {0} does not exist.".format(module.params["nfs_policy"])
)
if bool(
array.get_directory_exports(
export_names=[module.params["name"]],
policy_names=[module.params["nfs_policy"]],
).status_code
!= 200
):
all_policies.append(module.params["nfs_policy"])
if module.params["smb_policy"]:
if bool(
array.get_policies_smb(names=[module.params["smb_policy"]]).status_code
!= 200
):
module.fail_json(
msg="SMB Policy {0} does not exist.".format(module.params["smb_policy"])
)
if bool(
array.get_directory_exports(
export_names=[module.params["name"]],
policy_names=[module.params["smb_policy"]],
).status_code
!= 200
):
all_policies.append(module.params["smb_policy"])
if all_policies:
export = flasharray.DirectoryExportPost(export_name=module.params["name"])
changed = True
if not module.check_mode:
res = array.post_directory_exports(
directory_names=[
module.params["filesystem"] + ":" + module.params["directory"]
],
exports=export,
policy_names=all_policies,
)
if res.status_code != 200:
module.fail_json(
msg="Failed to create file system exports for {0}:{1}. Error: {2}".format(
module.params["filesystem"],
module.params["directory"],
res.errors[0].message,
)
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
state=dict(type="str", default="present", choices=["absent", "present"]),
filesystem=dict(type="str", required=True),
directory=dict(type="str", required=True),
name=dict(type="str", required=True),
nfs_policy=dict(type="str"),
smb_policy=dict(type="str"),
)
)
required_if = [["state", "present", ["filesystem", "directory"]]]
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")
array = get_system(module)
api_version = array._list_available_rest_versions()
if MIN_REQUIRED_API_VERSION not in api_version:
module.fail_json(
msg="FlashArray REST version not supported. "
"Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
)
array = get_array(module)
state = module.params["state"]
exists = bool(
array.get_directory_exports(export_names=[module.params["name"]]).status_code
== 200
)
if state == "present":
create_export(module, array)
elif state == "absent" and exists:
delete_export(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,367 @@
#!/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: purefa_fs
version_added: '1.5.0'
short_description: Manage FlashArray File Systems
description:
- Create/Delete FlashArray File Systems
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
name:
description:
- Name of the file system
type: str
required: true
state:
description:
- Define whether the file system should exist or not.
default: present
choices: [ absent, present ]
type: str
eradicate:
description:
- Define whether to eradicate the file system on delete or leave in trash.
type: bool
default: false
rename:
description:
- Value to rename the specified file system to
- Rename only applies to the container the current filesystem is in.
- There is no requirement to specify the pod name as this is implied.
type: str
move:
description:
- Move a filesystem in and out of a pod
- Provide the name of pod to move the filesystem to
- Pod names must be unique in the array
- To move to the local array, specify C(local)
- This is not idempotent - use C(ignore_errors) in the play
type: str
version_added: '1.13.0'
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Create file system foo
purestorage.flasharray.purefa_fs:
name: foo
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete and eradicate file system foo
purestorage.flasharray.purefa_fs:
name: foo
eradicate: true
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Rename file system foo to bar
purestorage.flasharray.purefa_fs:
name: foo
rename: bar
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from pypureclient import flasharray
except ImportError:
HAS_PURESTORAGE = False
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
MIN_REQUIRED_API_VERSION = "2.2"
REPL_SUPPORT_API = "2.13"
def delete_fs(module, array):
"""Delete a file system"""
changed = True
if not module.check_mode:
try:
file_system = flasharray.FileSystemPatch(destroyed=True)
array.patch_file_systems(
names=[module.params["name"]], file_system=file_system
)
except Exception:
module.fail_json(
msg="Failed to delete file system {0}".format(module.params["name"])
)
if module.params["eradicate"]:
try:
array.delete_file_systems(names=[module.params["name"]])
except Exception:
module.fail_json(
msg="Eradication of file system {0} failed".format(
module.params["name"]
)
)
module.exit_json(changed=changed)
def recover_fs(module, array):
"""Recover a deleted file system"""
changed = True
if not module.check_mode:
try:
file_system = flasharray.FileSystemPatch(destroyed=False)
array.patch_file_systems(
names=[module.params["name"]], file_system=file_system
)
except Exception:
module.fail_json(
msg="Failed to recover file system {0}".format(module.params["name"])
)
module.exit_json(changed=changed)
def eradicate_fs(module, array):
"""Eradicate a file system"""
changed = True
if not module.check_mode:
try:
array.delete_file_systems(names=[module.params["name"]])
except Exception:
module.fail_json(
msg="Failed to eradicate file system {0}".format(module.params["name"])
)
module.exit_json(changed=changed)
def rename_fs(module, array):
"""Rename a file system"""
changed = False
target_name = module.params["rename"]
if "::" in module.params["name"]:
pod_name = module.params["name"].split("::")[0]
target_name = pod_name + "::" + module.params["rename"]
try:
target = list(array.get_file_systems(names=[target_name]).items)[0]
except Exception:
target = None
if not target:
changed = True
if not module.check_mode:
try:
file_system = flasharray.FileSystemPatch(name=target_name)
array.patch_file_systems(
names=[module.params["name"]], file_system=file_system
)
except Exception:
module.fail_json(
msg="Failed to rename file system {0}".format(module.params["name"])
)
else:
module.fail_json(
msg="Target file system {0} already exists".format(module.params["rename"])
)
module.exit_json(changed=changed)
def create_fs(module, array):
"""Create a file system"""
changed = True
if "::" in module.params["name"]:
pod_name = module.params["name"].split("::")[0]
try:
pod = list(array.get_pods(names=[pod_name]).items)[0]
except Exception:
module.fail_json(
msg="Failed to create filesystem. Pod {0} does not exist".format(
pod_name
)
)
if pod.promotion_status == "demoted":
module.fail_json(msg="Filesystem cannot be created in a demoted pod")
if not module.check_mode:
try:
array.post_file_systems(names=[module.params["name"]])
except Exception:
module.fail_json(
msg="Failed to create file system {0}".format(module.params["name"])
)
module.exit_json(changed=changed)
def move_fs(module, array):
"""Move filesystem between pods or local array"""
changed = False
target_exists = False
pod_name = ""
fs_name = module.params["name"]
if "::" in module.params["name"]:
fs_name = module.params["name"].split("::")[1]
pod_name = module.params["name"].split("::")[0]
if module.params["move"] == "local":
target_location = ""
if "::" not in module.params["name"]:
module.fail_json(msg="Source and destination [local] cannot be the same.")
try:
target_exists = list(array.get_file_systems(names=[fs_name]).items)[0]
except Exception:
target_exists = False
if target_exists:
module.fail_json(msg="Target filesystem {0} already exists".format(fs_name))
else:
try:
pod = list(array.get_pods(names=[module.params["move"]]).items)[0]
if len(pod.arrays) > 1:
module.fail_json(msg="Filesystem cannot be moved into a stretched pod")
if pod.link_target_count != 0:
module.fail_json(
msg="Filesystem cannot be moved into a linked source pod"
)
if pod.promotion_status == "demoted":
module.fail_json(msg="Volume cannot be moved into a demoted pod")
except Exception:
module.fail_json(
msg="Failed to move filesystem. Pod {0} does not exist".format(pod_name)
)
if "::" in module.params["name"]:
pod = list(array.get_pods(names=[module.params["move"]]).items)[0]
if len(pod.arrays) > 1:
module.fail_json(
msg="Filesystem cannot be moved out of a stretched pod"
)
if pod.linked_target_count != 0:
module.fail_json(
msg="Filesystem cannot be moved out of a linked source pod"
)
if pod.promotion_status == "demoted":
module.fail_json(msg="Volume cannot be moved out of a demoted pod")
target_location = module.params["move"]
changed = True
if not module.check_mode:
file_system = flasharray.FileSystemPatch(
pod=flasharray.Reference(name=target_location)
)
move_res = array.patch_file_systems(
names=[module.params["name"]], file_system=file_system
)
if move_res.status_code != 200:
module.fail_json(
msg="Move of filesystem {0} failed. Error: {1}".format(
module.params["name"], move_res.errors[0].message
)
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
state=dict(type="str", default="present", choices=["absent", "present"]),
eradicate=dict(type="bool", default=False),
name=dict(type="str", required=True),
move=dict(type="str"),
rename=dict(type="str"),
)
)
mutually_exclusive = [["move", "rename"]]
module = AnsibleModule(
argument_spec, mutually_exclusive=mutually_exclusive, supports_check_mode=True
)
if not HAS_PURESTORAGE:
module.fail_json(msg="py-pure-client sdk is required for this module")
array = get_system(module)
api_version = array._list_available_rest_versions()
if MIN_REQUIRED_API_VERSION not in api_version:
module.fail_json(
msg="FlashArray REST version not supported. "
"Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
)
if REPL_SUPPORT_API not in api_version and "::" in module.params["name"]:
module.fail_json(
msg="Filesystem Replication is only supported in Purity//FA 6.3.0 or higher"
)
array = get_array(module)
state = module.params["state"]
try:
filesystem = list(array.get_file_systems(names=[module.params["name"]]).items)[
0
]
exists = True
except Exception:
exists = False
if state == "present" and not exists and not module.params["move"]:
create_fs(module, array)
elif (
state == "present"
and exists
and module.params["move"]
and not filesystem.destroyed
):
move_fs(module, array)
elif (
state == "present"
and exists
and module.params["rename"]
and not filesystem.destroyed
):
rename_fs(module, array)
elif (
state == "present"
and exists
and filesystem.destroyed
and not module.params["rename"]
and not module.params["move"]
):
recover_fs(module, array)
elif (
state == "present" and exists and filesystem.destroyed and module.params["move"]
):
module.fail_json(
msg="Filesystem {0} exists, but in destroyed state".format(
module.params["name"]
)
)
elif state == "absent" and exists and not filesystem.destroyed:
delete_fs(module, array)
elif (
state == "absent"
and exists
and module.params["eradicate"]
and filesystem.destroyed
):
eradicate_fs(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,435 @@
#!/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: purefa_hg
version_added: '1.0.0'
short_description: Manage hostgroups on Pure Storage FlashArrays
description:
- Create, delete or modifiy hostgroups on Pure Storage FlashArrays.
author:
- Pure Storage ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
hostgroup:
description:
- The name of the hostgroup.
type: str
required: true
state:
description:
- Define whether the hostgroup should exist or not.
type: str
default: present
choices: [ absent, present ]
host:
type: list
elements: str
description:
- List of existing hosts to add to hostgroup.
- Note that hostnames are case-sensitive however FlashArray hostnames are unique
and ignore case - you cannot have I(hosta) and I(hostA)
volume:
type: list
elements: str
description:
- List of existing volumes to add to hostgroup.
- Note that volumes are case-sensitive however FlashArray volume names are unique
and ignore case - you cannot have I(volumea) and I(volumeA)
lun:
description:
- LUN ID to assign to volume for hostgroup. Must be unique.
- Only applicable when only one volume is specified for connection.
- If not provided the ID will be automatically assigned.
- Range for LUN ID is 1 to 4095.
type: int
rename:
description:
- New name of hostgroup
type: str
version_added: '1.10.0'
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Create empty hostgroup
purestorage.flasharray.purefa_hg:
hostgroup: foo
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Add hosts and volumes to existing or new hostgroup
purestorage.flasharray.purefa_hg:
hostgroup: foo
host:
- host1
- host2
volume:
- vol1
- vol2
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete hosts and volumes from hostgroup
purestorage.flasharray.purefa_hg:
hostgroup: foo
host:
- host1
- host2
volume:
- vol1
- vol2
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: absent
# This will disconnect all hosts and volumes in the hostgroup
- name: Delete hostgroup
purestorage.flasharray.purefa_hg:
hostgroup: foo
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: absent
- name: Rename hostgroup
purestorage.flasharray.purefa_hg:
hostgroup: foo
rename: bar
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Create host group with hosts and volumes
purestorage.flasharray.purefa_hg:
hostgroup: bar
host:
- host1
- host2
volume:
- vol1
- vol2
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
purefa_argument_spec,
)
def rename_exists(module, array):
"""Determine if rename target already exists"""
exists = False
new_name = module.params["rename"]
for hgroup in array.list_hgroups():
if hgroup["name"].lower() == new_name.lower():
exists = True
break
return exists
def get_hostgroup(module, array):
hostgroup = None
for host in array.list_hgroups():
if host["name"] == module.params["hostgroup"]:
hostgroup = host
break
return hostgroup
def make_hostgroup(module, array):
if module.params["rename"]:
module.fail_json(
msg="Hostgroup {0} does not exist - rename failed.".format(
module.params["hostgroup"]
)
)
changed = True
if not module.check_mode:
try:
array.create_hgroup(module.params["hostgroup"])
except Exception:
module.fail_json(
msg="Failed to create hostgroup {0}".format(module.params["hostgroup"])
)
if module.params["host"]:
array.set_hgroup(module.params["hostgroup"], hostlist=module.params["host"])
if module.params["volume"]:
if len(module.params["volume"]) == 1 and module.params["lun"]:
try:
array.connect_hgroup(
module.params["hostgroup"],
module.params["volume"][0],
lun=module.params["lun"],
)
except Exception:
module.fail_json(
msg="Failed to add volume {0} with LUN ID {1}".format(
module.params["volume"][0], module.params["lun"]
)
)
else:
for vol in module.params["volume"]:
try:
array.connect_hgroup(module.params["hostgroup"], vol)
except Exception:
module.fail_json(msg="Failed to add volume to hostgroup")
module.exit_json(changed=changed)
def update_hostgroup(module, array):
changed = False
renamed = False
hgroup = get_hostgroup(module, array)
current_hostgroup = module.params["hostgroup"]
volumes = array.list_hgroup_connections(module.params["hostgroup"])
if module.params["state"] == "present":
if module.params["rename"]:
if not rename_exists(module, array):
try:
if not module.check_mode:
array.rename_hgroup(
module.params["hostgroup"], module.params["rename"]
)
current_hostgroup = module.params["rename"]
renamed = True
except Exception:
module.fail_json(
msg="Rename to {0} failed.".format(module.params["rename"])
)
else:
module.warn(
"Rename failed. Hostgroup {0} already exists. Continuing with other changes...".format(
module.params["rename"]
)
)
if module.params["host"]:
cased_hosts = [host.lower() for host in module.params["host"]]
cased_hghosts = [host.lower() for host in hgroup["hosts"]]
new_hosts = list(set(cased_hosts).difference(cased_hghosts))
if new_hosts:
try:
if not module.check_mode:
array.set_hgroup(current_hostgroup, addhostlist=new_hosts)
changed = True
except Exception:
module.fail_json(msg="Failed to add host(s) to hostgroup")
if module.params["volume"]:
if volumes:
current_vols = [vol["vol"].lower() for vol in volumes]
cased_vols = [vol.lower() for vol in module.params["volume"]]
new_volumes = list(set(cased_vols).difference(set(current_vols)))
if len(new_volumes) == 1 and module.params["lun"]:
try:
if not module.check_mode:
array.connect_hgroup(
current_hostgroup,
new_volumes[0],
lun=module.params["lun"],
)
changed = True
except Exception:
module.fail_json(
msg="Failed to add volume {0} with LUN ID {1}".format(
new_volumes[0], module.params["lun"]
)
)
else:
for cvol in new_volumes:
try:
if not module.check_mode:
array.connect_hgroup(current_hostgroup, cvol)
changed = True
except Exception:
module.fail_json(
msg="Failed to connect volume {0} to hostgroup {1}.".format(
cvol, current_hostgroup
)
)
else:
if len(module.params["volume"]) == 1 and module.params["lun"]:
try:
if not module.check_mode:
array.connect_hgroup(
current_hostgroup,
module.params["volume"][0],
lun=module.params["lun"],
)
changed = True
except Exception:
module.fail_json(
msg="Failed to add volume {0} with LUN ID {1}".format(
module.params["volume"], module.params["lun"]
)
)
else:
for cvol in module.params["volume"]:
try:
if not module.check_mode:
array.connect_hgroup(current_hostgroup, cvol)
changed = True
except Exception:
module.fail_json(
msg="Failed to connect volume {0} to hostgroup {1}.".format(
cvol, current_hostgroup
)
)
else:
if module.params["host"]:
cased_old_hosts = [host.lower() for host in module.params["host"]]
cased_hosts = [host.lower() for host in hgroup["hosts"]]
old_hosts = list(set(cased_old_hosts).intersection(cased_hosts))
if old_hosts:
try:
if not module.check_mode:
array.set_hgroup(current_hostgroup, remhostlist=old_hosts)
changed = True
except Exception:
module.fail_json(
msg="Failed to remove hosts {0} from hostgroup {1}".format(
old_hosts, current_hostgroup
)
)
if module.params["volume"]:
cased_old_vols = [vol.lower() for vol in module.params["volume"]]
old_volumes = list(
set(cased_old_vols).intersection(
set([vol["vol"].lower() for vol in volumes])
)
)
if old_volumes:
changed = True
for cvol in old_volumes:
try:
if not module.check_mode:
array.disconnect_hgroup(current_hostgroup, cvol)
except Exception:
module.fail_json(
msg="Failed to disconnect volume {0} from hostgroup {1}".format(
cvol, current_hostgroup
)
)
changed = changed or renamed
module.exit_json(changed=changed)
def delete_hostgroup(module, array):
changed = True
try:
vols = array.list_hgroup_connections(module.params["hostgroup"])
except Exception:
module.fail_json(
msg="Failed to get volume connection for hostgroup {0}".format(
module.params["hostgroup"]
)
)
if not module.check_mode:
for vol in vols:
try:
array.disconnect_hgroup(module.params["hostgroup"], vol["vol"])
except Exception:
module.fail_json(
msg="Failed to disconnect volume {0} from hostgroup {1}".format(
vol["vol"], module.params["hostgroup"]
)
)
host = array.get_hgroup(module.params["hostgroup"])
if not module.check_mode:
try:
array.set_hgroup(module.params["hostgroup"], remhostlist=host["hosts"])
try:
array.delete_hgroup(module.params["hostgroup"])
except Exception:
module.fail_json(
msg="Failed to delete hostgroup {0}".format(
module.params["hostgroup"]
)
)
except Exception:
module.fail_json(
msg="Failed to remove hosts {0} from hostgroup {1}".format(
host["hosts"], module.params["hostgroup"]
)
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
hostgroup=dict(type="str", required=True),
state=dict(type="str", default="present", choices=["absent", "present"]),
host=dict(type="list", elements="str"),
lun=dict(type="int"),
rename=dict(type="str"),
volume=dict(type="list", elements="str"),
)
)
module = AnsibleModule(argument_spec, supports_check_mode=True)
state = module.params["state"]
array = get_system(module)
hostgroup = get_hostgroup(module, array)
if module.params["host"]:
try:
for hst in module.params["host"]:
array.get_host(hst)
except Exception:
module.fail_json(msg="Host {0} not found".format(hst))
if module.params["lun"] and len(module.params["volume"]) > 1:
module.fail_json(msg="LUN ID cannot be specified with multiple volumes.")
if module.params["lun"] and not 1 <= module.params["lun"] <= 4095:
module.fail_json(
msg="LUN ID of {0} is out of range (1 to 4095)".format(module.params["lun"])
)
if module.params["volume"]:
try:
for vol in module.params["volume"]:
array.get_volume(vol)
except Exception:
module.fail_json(msg="Volume {0} not found".format(vol))
if hostgroup and state == "present":
update_hostgroup(module, array)
elif hostgroup and module.params["volume"] and state == "absent":
update_hostgroup(module, array)
elif hostgroup and module.params["host"] and state == "absent":
update_hostgroup(module, array)
elif hostgroup and state == "absent":
delete_hostgroup(module, array)
elif hostgroup is None and state == "absent":
module.exit_json(changed=False)
else:
make_hostgroup(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,469 @@
#!/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: purefa_inventory
short_description: Collect information from Pure Storage FlashArray
version_added: '1.0.0'
description:
- Collect hardware inventory information from a Pure Storage Flasharray
author:
- Pure Storage ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: collect FlashArray invenroty
purestorage.flasharray.purefa_inventory:
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: show inventory information
debug:
msg: "{{ array_info['purefa_inv'] }}"
"""
RETURN = r"""
purefa_inventory:
description: Returns the inventory information for the FlashArray
returned: always
type: complex
sample: {
"chassis": {
"CH0": {
"model": null,
"serial": "ABC123",
"status": "ok"
},
},
"controllers": {
"CT0": {
"model": null,
"serial": null,
"status": "ok"
},
"CT1": {
"model": "FA-405",
"serial": "FHVBT52",
"status": "ok"
}
},
"drives": {
"SH0.BAY0": {
"capacity": 2147483648,
"protocol": "SAS",
"serial": "S18NNEAFA01416",
"status": "healthy",
"type": "NVRAM"
},
"SH0.BAY1": {
"capacity": 511587647488,
"protocol": "SAS",
"serial": "S0WZNEACC00517",
"status": "healthy",
"type": "SSD"
},
"SH0.BAY10": {
"capacity": 511587647488,
"protocol": "SAS",
"serial": "S0WZNEACB00266",
"status": "healthy",
"type": "SSD"
}
},
"fans": {
"CT0.FAN0": {
"status": "ok"
},
"CT0.FAN1": {
"status": "ok"
},
"CT0.FAN10": {
"status": "ok"
}
},
"interfaces": {
"CT0.ETH0": {
"speed": 1000000000,
"status": "ok"
},
"CT0.ETH1": {
"speed": 0,
"status": "ok"
},
"CT0.FC0": {
"speed": 8000000000,
"status": "ok"
},
"CT1.IB1": {
"speed": 56000000000,
"status": "ok"
},
"CT1.SAS0": {
"speed": 24000000000,
"status": "ok"
}
},
"power": {
"CT0.PWR0": {
"model": null,
"serial": null,
"status": "ok",
"voltage": null
},
"CT0.PWR1": {
"model": null,
"serial": null,
"status": "ok",
"voltage": null
}
},
"temps": {
"CT0.TMP0": {
"status": "ok",
"temperature": 18
},
"CT0.TMP1": {
"status": "ok",
"temperature": 32
}
}
}
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
NEW_API_VERSION = "2.2"
SFP_API_VERSION = "2.16"
def generate_new_hardware_dict(array, versions):
hw_info = {
"fans": {},
"controllers": {},
"temps": {},
"drives": {},
"interfaces": {},
"power": {},
"chassis": {},
"tempatures": {},
}
components = list(array.get_hardware().items)
for component in range(0, len(components)):
component_name = components[component].name
if components[component].type == "chassis":
hw_info["chassis"][component_name] = {
"status": components[component].status,
"serial": components[component].serial,
"model": components[component].model,
"identify_enabled": components[component].identify_enabled,
}
if components[component].type == "controller":
hw_info["controllers"][component_name] = {
"status": components[component].status,
"serial": components[component].serial,
"model": components[component].model,
"identify_enabled": components[component].identify_enabled,
}
if components[component].type == "cooling":
hw_info["fans"][component_name] = {
"status": components[component].status,
}
if components[component].type == "temp_sensor":
hw_info["controllers"][component_name] = {
"status": components[component].status,
"temperature": components[component].temperature,
}
if components[component].type == "drive_bay":
hw_info["drives"][component_name] = {
"status": components[component].status,
"identify_enabled": components[component].identify_enabled,
"serial": getattr(components[component], "serial", None),
}
if components[component].type in [
"sas_port",
"fc_port",
"eth_port",
"ib_port",
]:
hw_info["interfaces"][component_name] = {
"type": components[component].type,
"status": components[component].status,
"speed": components[component].speed,
"connector_type": None,
"rx_los": None,
"rx_power": None,
"static": {},
"temperature": None,
"tx_bias": None,
"tx_fault": None,
"tx_power": None,
"voltage": None,
}
if components[component].type == "power_supply":
hw_info["power"][component_name] = {
"status": components[component].status,
"voltage": components[component].voltage,
"serial": components[component].serial,
"model": components[component].model,
}
drives = list(array.get_drives().items)
for drive in range(0, len(drives)):
drive_name = drives[drive].name
hw_info["drives"][drive_name] = {
"capacity": drives[drive].capacity,
"status": drives[drive].status,
"protocol": getattr(drives[drive], "protocol", None),
"type": drives[drive].type,
}
if SFP_API_VERSION in versions:
port_details = list(array.get_network_interfaces_port_details().items)
for port_detail in range(0, len(port_details)):
port_name = port_details[port_detail].name
hw_info["interfaces"][port_name]["interface_type"] = port_details[
port_detail
].interface_type
hw_info["interfaces"][port_name]["rx_los"] = (
port_details[port_detail].rx_los[0].flag
)
hw_info["interfaces"][port_name]["rx_power"] = (
port_details[port_detail].rx_power[0].measurement
)
hw_info["interfaces"][port_name]["static"] = {
"connector_type": port_details[port_detail].static.connector_type,
"vendor_name": port_details[port_detail].static.vendor_name,
"vendor_oui": port_details[port_detail].static.vendor_oui,
"vendor_serial_number": port_details[
port_detail
].static.vendor_serial_number,
"vendor_part_number": port_details[
port_detail
].static.vendor_part_number,
"vendor_date_code": port_details[port_detail].static.vendor_date_code,
"signaling_rate": port_details[port_detail].static.signaling_rate,
"wavelength": port_details[port_detail].static.wavelength,
"rate_identifier": port_details[port_detail].static.rate_identifier,
"identifier": port_details[port_detail].static.identifier,
"link_length": port_details[port_detail].static.link_length,
"voltage_thresholds": {
"alarm_high": port_details[
port_detail
].static.voltage_thresholds.alarm_high,
"alarm_low": port_details[
port_detail
].static.voltage_thresholds.alarm_low,
"warn_high": port_details[
port_detail
].static.voltage_thresholds.warn_high,
"warn_low": port_details[
port_detail
].static.voltage_thresholds.warn_low,
},
"tx_power_thresholds": {
"alarm_high": port_details[
port_detail
].static.tx_power_thresholds.alarm_high,
"alarm_low": port_details[
port_detail
].static.tx_power_thresholds.alarm_low,
"warn_high": port_details[
port_detail
].static.tx_power_thresholds.warn_high,
"warn_low": port_details[
port_detail
].static.tx_power_thresholds.warn_low,
},
"rx_power_thresholds": {
"alarm_high": port_details[
port_detail
].static.rx_power_thresholds.alarm_high,
"alarm_low": port_details[
port_detail
].static.rx_power_thresholds.alarm_low,
"warn_high": port_details[
port_detail
].static.rx_power_thresholds.warn_high,
"warn_low": port_details[
port_detail
].static.rx_power_thresholds.warn_low,
},
"tx_bias_thresholds": {
"alarm_high": port_details[
port_detail
].static.tx_bias_thresholds.alarm_high,
"alarm_low": port_details[
port_detail
].static.tx_bias_thresholds.alarm_low,
"warn_high": port_details[
port_detail
].static.tx_bias_thresholds.warn_high,
"warn_low": port_details[
port_detail
].static.tx_bias_thresholds.warn_low,
},
"temperature_thresholds": {
"alarm_high": port_details[
port_detail
].static.temperature_thresholds.alarm_high,
"alarm_low": port_details[
port_detail
].static.temperature_thresholds.alarm_low,
"warn_high": port_details[
port_detail
].static.temperature_thresholds.warn_high,
"warn_low": port_details[
port_detail
].static.temperature_thresholds.warn_low,
},
"fc_speeds": port_details[port_detail].static.fc_speeds,
"fc_technology": port_details[port_detail].static.fc_technology,
"encoding": port_details[port_detail].static.encoding,
"fc_link_lengths": port_details[port_detail].static.fc_link_lengths,
"fc_transmission_media": port_details[
port_detail
].static.fc_transmission_media,
"extended_identifier": port_details[
port_detail
].static.extended_identifier,
}
hw_info["interfaces"][port_name]["temperature"] = (
port_details[port_detail].temperature[0].measurement
)
hw_info["interfaces"][port_name]["tx_bias"] = (
port_details[port_detail].tx_bias[0].measurement
)
hw_info["interfaces"][port_name]["tx_fault"] = (
port_details[port_detail].tx_fault[0].flag
)
hw_info["interfaces"][port_name]["tx_power"] = (
port_details[port_detail].tx_power[0].measurement
)
hw_info["interfaces"][port_name]["voltage"] = (
port_details[port_detail].voltage[0].measurement
)
return hw_info
def generate_hardware_dict(array):
hw_info = {
"fans": {},
"controllers": {},
"temps": {},
"drives": {},
"interfaces": {},
"power": {},
"chassis": {},
}
components = array.list_hardware()
for component in range(0, len(components)):
component_name = components[component]["name"]
if "FAN" in component_name:
fan_name = component_name
hw_info["fans"][fan_name] = {"status": components[component]["status"]}
if "PWR" in component_name:
pwr_name = component_name
hw_info["power"][pwr_name] = {
"status": components[component]["status"],
"voltage": components[component]["voltage"],
"serial": components[component]["serial"],
"model": components[component]["model"],
}
if "IB" in component_name:
ib_name = component_name
hw_info["interfaces"][ib_name] = {
"status": components[component]["status"],
"speed": components[component]["speed"],
}
if "SAS" in component_name:
sas_name = component_name
hw_info["interfaces"][sas_name] = {
"status": components[component]["status"],
"speed": components[component]["speed"],
}
if "ETH" in component_name:
eth_name = component_name
hw_info["interfaces"][eth_name] = {
"status": components[component]["status"],
"speed": components[component]["speed"],
}
if "FC" in component_name:
eth_name = component_name
hw_info["interfaces"][eth_name] = {
"status": components[component]["status"],
"speed": components[component]["speed"],
}
if "TMP" in component_name:
tmp_name = component_name
hw_info["temps"][tmp_name] = {
"status": components[component]["status"],
"temperature": components[component]["temperature"],
}
if component_name in ["CT0", "CT1"]:
cont_name = component_name
hw_info["controllers"][cont_name] = {
"status": components[component]["status"],
"serial": components[component]["serial"],
"model": components[component]["model"],
}
if component_name in ["CH0"]:
cont_name = component_name
hw_info["chassis"][cont_name] = {
"status": components[component]["status"],
"serial": components[component]["serial"],
"model": components[component]["model"],
}
drives = array.list_drives()
for drive in range(0, len(drives)):
drive_name = drives[drive]["name"]
hw_info["drives"][drive_name] = {
"capacity": drives[drive]["capacity"],
"status": drives[drive]["status"],
"protocol": drives[drive]["protocol"],
"type": drives[drive]["type"],
}
for disk in range(0, len(components)):
if components[disk]["name"] == drive_name:
hw_info["drives"][drive_name]["serial"] = components[disk]["serial"]
return hw_info
def main():
argument_spec = purefa_argument_spec()
inv_info = {}
module = AnsibleModule(argument_spec, supports_check_mode=True)
array = get_system(module)
api_version = array._list_available_rest_versions()
if NEW_API_VERSION in api_version:
arrayv6 = get_array(module)
inv_info = generate_new_hardware_dict(arrayv6, api_version)
else:
inv_info = generate_hardware_dict(array)
module.exit_json(changed=False, purefa_inv=inv_info)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,251 @@
#!/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: purefa_kmip
version_added: '1.10.0'
short_description: Manage FlashArray KMIP server objects
description:
- Manage FlashArray KMIP Server objects
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
name:
description:
- Name of the KMIP server object
type: str
required: true
certificate:
description:
- Name of existing certifcate used to verify FlashArray
authenticity to the KMIP server.
- Use the I(purestorage.flasharray.purefa_certs) module to create certificates.
type: str
state:
description:
- Action for the module to perform
default: present
choices: [ absent, present ]
type: str
ca_certificate:
type: str
description:
- The text of the CA certificate for the KMIP server.
- Includes the "-----BEGIN CERTIFICATE-----" and "-----END CERTIFICATE-----" lines
- Does not exceed 3000 characters in length
uris:
type: list
elements: str
description:
- A list of URIs for the configured KMIP servers.
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Create KMIP obejct
purestorage.flasharray.purefa_kmip:
name: foo
certificate: bar
ca_certificate: "{{lookup('file', 'example.crt') }}"
uris:
- 1.1.1.1:8888
- 2.3.3.3:9999
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete KMIP object
purestorage.flasharray.purefa_kmip:
name: foo
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Update KMIP object
purestorage.flasharray.purefa_kmip:
name: foo
ca_certificate: "{{lookup('file', 'example2.crt') }}"
uris:
- 3.3.3.3:8888
- 4.4.4.4:9999
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from pypureclient import flasharray
except ImportError:
HAS_PURESTORAGE = False
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
MIN_REQUIRED_API_VERSION = "2.2"
def update_kmip(module, array):
"""Update existing KMIP object"""
changed = False
current_kmip = list(array.get_kmip(names=[module.params["name"]]).items)[0]
if (
module.params["certificate"]
and current_kmip.certificate.name != module.params["certificate"]
):
if (
array.get_certificates(names=[module.params["certificate"]]).status_code
!= 200
):
module.fail_json(
msg="Array certificate {0} does not exist.".format(
module.params["certificate"]
)
)
changed = True
certificate = module.params["certificate"]
else:
certificate = current_kmip.certificate.name
if module.params["uris"] and sorted(current_kmip.uris) != sorted(
module.params["uris"]
):
changed = True
uris = sorted(module.params["uris"])
else:
uris = sorted(current_kmip.uris)
if (
module.params["ca_certificate"]
and module.params["ca_certificate"] != current_kmip.ca_certificate
):
changed = True
ca_cert = module.params["ca_certificate"]
else:
ca_cert = current_kmip.ca_certificate
if not module.check_mode:
if changed:
kmip = flasharray.KmipPost(
uris=uris,
ca_certificate=ca_cert,
certificate=flasharray.ReferenceNoId(name=certificate),
)
res = array.patch_kmip(names=[module.params["name"]], kmip=kmip)
if res.status_code != 200:
module.fail_json(
msg="Updating existing KMIP object {0} failed. Error: {1}".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def create_kmip(module, array):
"""Create KMIP object"""
if array.get_certificates(names=[module.params["certificate"]]).status_code != 200:
module.fail_json(
msg="Array certificate {0} does not exist.".format(
module.params["certificate"]
)
)
changed = True
kmip = flasharray.KmipPost(
uris=sorted(module.params["uris"]),
ca_certificate=module.params["ca_certificate"],
certificate=flasharray.ReferenceNoId(name=module.params["certificate"]),
)
if not module.check_mode:
res = array.post_kmip(names=[module.params["name"]], kmip=kmip)
if res.status_code != 200:
module.fail_json(
msg="Creating KMIP object {0} failed. Error: {1}".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def delete_kmip(module, array):
"""Delete existing KMIP object"""
changed = True
if not module.check_mode:
res = array.delete_kmip(names=[module.params["name"]])
if res.status_code != 200:
module.fail_json(
msg="Failed to delete {0} KMIP object. Error: {1}".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
state=dict(
type="str",
default="present",
choices=["absent", "present"],
),
name=dict(type="str", required=True),
certificate=dict(type="str"),
ca_certificate=dict(type="str", no_log=True),
uris=dict(type="list", elements="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")
array = get_system(module)
api_version = array._list_available_rest_versions()
if MIN_REQUIRED_API_VERSION not in api_version:
module.fail_json(
msg="FlashArray REST version not supported. "
"Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
)
array = get_array(module)
state = module.params["state"]
exists = bool(array.get_kmip(names=[module.params["name"]]).status_code == 200)
if module.params["certificate"] and len(module.params["certificate"]) > 3000:
module.fail_json(msg="Certificate exceeds 3000 characters")
if not exists and state == "present":
create_kmip(module, array)
elif exists and state == "present":
update_kmip(module, array)
elif exists and state == "absent":
delete_kmip(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,133 @@
#!/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: purefa_maintenance
version_added: '1.7.0'
short_description: Configure Pure Storage FlashArray Maintence Windows
description:
- Configuration for Pure Storage FlashArray Maintenance Windows.
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
state:
description:
- Create or delete maintennance window
type: str
default: present
choices: [ absent, present ]
timeout :
type: int
default: 3600
description:
- Maintenance window period, specified in seconds.
- Range allowed is 1 minute (60 seconds) to 24 hours (86400 seconds)
- Default setting is 1 hour (3600 seconds)
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Delete exisitng maintenance window
purestorage.flasharray.purefa_maintenance:
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Set maintnence window to default of 1 hour
purestorage.flasharray.purefa_maintenance:
state: present
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Update existing maintnence window
purestorage.flasharray.purefa_maintenance:
state: present
timeout: 86400
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from pypureclient import flasharray
except ImportError:
HAS_PURESTORAGE = False
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_array,
purefa_argument_spec,
)
def delete_window(module, array):
"""Delete Maintenance Window"""
changed = False
if list(array.get_maintenance_windows().items):
changed = True
if not module.check_mode:
state = array.delete_maintenance_windows(names=["environment"])
if state.status_code != 200:
changed = False
module.exit_json(changed=changed)
def set_window(module, array):
"""Set Maintenace Window"""
changed = True
if not 60 <= module.params["timeout"] <= 86400:
module.fail_json(msg="Maintenance Window Timeout is out of range (60 to 86400)")
window = flasharray.MaintenanceWindowPost(timeout=module.params["timeout"] * 1000)
if not module.check_mode:
state = array.post_maintenance_windows(
names=["environment"], maintenance_window=window
)
if state.status_code != 200:
module.fail_json(msg="Setting maintenance window failed")
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
timeout=dict(type="int", default=3600),
state=dict(type="str", default="present", choices=["absent", "present"]),
)
)
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")
array = get_array(module)
if module.params["state"] == "absent":
delete_window(module, array)
else:
set_window(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,198 @@
#!/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: purefa_messages
version_added: '1.14.0'
short_description: List FlashArray 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.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Show critical alerts from past 4 weeks that haven't been acknowledged
purefa_messages:
history: 4w
flagged : False
severity:
- critical
fa_url: 10.10.10.2
api_token: 89a9356f-c203-d263-8a89-c229486a13ba
"""
RETURN = r"""
"""
import time
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
MIN_REQUIRED_API_VERSION = "2.2"
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 = purefa_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)
array = get_system(module)
api_version = array._list_available_rest_versions()
if MIN_REQUIRED_API_VERSION not in api_version:
module.fail_json(
msg="FlashArray REST version not supported. "
"Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
)
array_v6 = get_array(module)
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 = array_v6.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,
"code": alerts[message].code,
"severity": alerts[message].severity,
"actual": alerts[message].actual,
"issue": alerts[message].issue,
"state": alerts[message].state,
"flagged": alerts[message].flagged,
"closed": None,
"created": time.strftime(
"%Y-%m-%d %H:%M:%S",
time.gmtime(alerts[message].created / 1000),
)
+ " UTC",
"updated": time.strftime(
"%Y-%m-%d %H:%M:%S",
time.gmtime(alerts[message].updated / 1000),
)
+ " UTC",
}
if alerts[message].state == "closed":
messages[name]["closed"] = (
time.strftime(
"%Y-%m-%d %H:%M:%S", time.gmtime(alerts[message].closed / 1000)
)
+ " UTC"
)
module.exit_json(changed=False, purefa_messages=messages)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,430 @@
#!/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: purefa_network
short_description: Manage network interfaces in a Pure Storage FlashArray
version_added: '1.0.0'
description:
- This module manages the physical and virtual network interfaces on a Pure Storage FlashArray.
- To manage VLAN interfaces use the I(purestorage.flasharray.purefa_vlan) module.
- To manage network subnets use the I(purestorage.flasharray.purefa_subnet) module.
- To remove an IP address from a non-management port use 0.0.0.0/0
author: Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
name:
description:
- Interface name (physical or virtual).
required: true
type: str
state:
description:
- State of existing interface (on/off).
required: false
default: present
choices: [ "present", "absent" ]
type: str
address:
description:
- IPv4 or IPv6 address of interface in CIDR notation.
- To remove an IP address from a non-management port use 0.0.0.0/0
required: false
type: str
gateway:
description:
- IPv4 or IPv6 address of interface gateway.
required: false
type: str
mtu:
description:
- MTU size of the interface. Range is 1280 to 9216.
required: false
default: 1500
type: int
servicelist:
description:
- Assigns the specified (comma-separated) service list to one or more specified interfaces.
- Replaces the previous service list.
- Supported service lists depend on whether the network interface is Ethernet or Fibre Channel.
- Note that I(system) is only valid for Cloud Block Store.
elements: str
type: list
choices: [ "replication", "management", "ds", "file", "iscsi", "scsi-fc", "nvme-fc", "system"]
version_added: '1.15.0'
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = """
- name: Configure and enable network interface ct0.eth8
purestorage.flasharray.purefa_network:
name: ct0.eth8
gateway: 10.21.200.1
address: "10.21.200.18/24"
mtu: 9000
state: present
fa_url: 10.10.10.2
api_token: c6033033-fe69-2515-a9e8-966bb7fe4b40
- name: Disable physical interface ct1.eth2
purestorage.flasharray.purefa_network:
name: ct1.eth2
state: absent
fa_url: 10.10.10.2
api_token: c6033033-fe69-2515-a9e8-966bb7fe4b40
- name: Enable virtual network interface vir0
purestorage.flasharray.purefa_network:
name: vir0
state: present
fa_url: 10.10.10.2
api_token: c6033033-fe69-2515-a9e8-966bb7fe4b40
- name: Remove an IP address from iSCSI interface ct0.eth4
purestorage.flasharray.purefa_network:
name: ct0.eth4
address: 0.0.0.0/0
gateway: 0.0.0.0
fa_url: 10.10.10.2
api_token: c6033033-fe69-2515-a9e8-966bb7fe4b40
- name: Change service list for FC interface ct0.fc1
purestorage.flasharray.purefa_network:
name: ct0.fc1
servicelist:
- replication
fa_url: 10.10.10.2
api_token: c6033033-fe69-2515-a9e8-966bb7fe4b40
"""
RETURN = """
"""
try:
from netaddr import IPAddress, IPNetwork
HAS_NETADDR = True
except ImportError:
HAS_NETADDR = False
try:
from pypureclient.flasharray import NetworkInterfacePatch
HAS_PYPURECLIENT = True
except ImportError:
HAS_PYPURECLIENT = False
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
FC_ENABLE_API = "2.4"
def _is_cbs(array, is_cbs=False):
"""Is the selected array a Cloud Block Store"""
model = array.get(controllers=True)[0]["model"]
is_cbs = bool("CBS" in model)
return is_cbs
def _get_fc_interface(module, array):
"""Return FC Interface or None"""
interface = {}
interface_list = array.get_network_interfaces(names=[module.params["name"]])
if interface_list.status_code == 200:
interface = list(interface_list.items)[0]
return interface
else:
return None
def _get_interface(module, array):
"""Return Network Interface or None"""
interface = {}
if module.params["name"][0] == "v":
try:
interface = array.get_network_interface(module.params["name"])
except Exception:
return None
else:
try:
interfaces = array.list_network_interfaces()
except Exception:
return None
for ints in range(0, len(interfaces)):
if interfaces[ints]["name"] == module.params["name"]:
interface = interfaces[ints]
break
return interface
def update_fc_interface(module, array, interface, api_version):
"""Modify FC Interface settings"""
changed = False
if FC_ENABLE_API in api_version:
if not interface.enabled and module.params["state"] == "present":
changed = True
if not module.check_mode:
network = NetworkInterfacePatch(enabled=True, override_npiv_check=True)
res = array.patch_network_interfaces(
names=[module.params["name"]], network=network
)
if res.status_code != 200:
module.fail_json(
msg="Failed to enable interface {0}.".format(
module.params["name"]
)
)
if interface.enabled and module.params["state"] == "absent":
changed = True
if not module.check_mode:
network = NetworkInterfacePatch(enabled=False, override_npiv_check=True)
res = array.patch_network_interfaces(
names=[module.params["name"]], network=network
)
if res.status_code != 200:
module.fail_json(
msg="Failed to disable interface {0}.".format(
module.params["name"]
)
)
if module.params["servicelist"] and sorted(module.params["servicelist"]) != sorted(
interface.services
):
changed = True
if not module.check_mode:
network = NetworkInterfacePatch(services=module.params["servicelist"])
res = array.patch_network_interfaces(
names=[module.params["name"]], network=network
)
if res.status_code != 200:
module.fail_json(
msg="Failed to update interface service list {0}. Error: {1}".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def update_interface(module, array, interface):
"""Modify Interface settings"""
changed = False
current_state = {
"mtu": interface["mtu"],
"gateway": interface["gateway"],
"address": interface["address"],
"netmask": interface["netmask"],
"services": sorted(interface["services"]),
}
if not module.params["address"]:
address = interface["address"]
else:
if module.params["gateway"]:
if module.params["gateway"] and module.params["gateway"] not in IPNetwork(
module.params["address"]
):
module.fail_json(msg="Gateway and subnet are not compatible.")
elif not module.params["gateway"] and interface["gateway"] not in [
None,
IPNetwork(module.params["address"]),
]:
module.fail_json(msg="Gateway and subnet are not compatible.")
address = str(module.params["address"].split("/", 1)[0])
ip_version = str(IPAddress(address).version)
if not module.params["mtu"]:
mtu = interface["mtu"]
else:
if not 1280 <= module.params["mtu"] <= 9216:
module.fail_json(
msg="MTU {0} is out of range (1280 to 9216)".format(
module.params["mtu"]
)
)
else:
mtu = module.params["mtu"]
if module.params["address"]:
netmask = str(IPNetwork(module.params["address"]).netmask)
else:
netmask = interface["netmask"]
if not module.params["gateway"]:
gateway = interface["gateway"]
else:
cidr = str(IPAddress(netmask).netmask_bits())
full_addr = address + "/" + cidr
if module.params["gateway"] not in IPNetwork(full_addr):
module.fail_json(msg="Gateway and subnet are not compatible.")
gateway = module.params["gateway"]
if ip_version == "6":
netmask = str(IPAddress(netmask).netmask_bits())
new_state = {
"address": address,
"mtu": mtu,
"gateway": gateway,
"netmask": netmask,
}
if new_state != current_state:
changed = True
if (
"management" in interface["services"] or "app" in interface["services"]
) and address == "0.0.0.0/0":
module.fail_json(
msg="Removing IP address from a management or app port is not supported"
)
if not module.check_mode:
try:
if new_state["gateway"] is not None:
array.set_network_interface(
interface["name"],
address=new_state["address"],
mtu=new_state["mtu"],
netmask=new_state["netmask"],
gateway=new_state["gateway"],
)
else:
array.set_network_interface(
interface["name"],
address=new_state["address"],
mtu=new_state["mtu"],
netmask=new_state["netmask"],
)
except Exception:
module.fail_json(
msg="Failed to change settings for interface {0}.".format(
interface["name"]
)
)
if not interface["enabled"] and module.params["state"] == "present":
changed = True
if not module.check_mode:
try:
array.enable_network_interface(interface["name"])
except Exception:
module.fail_json(
msg="Failed to enable interface {0}.".format(interface["name"])
)
if interface["enabled"] and module.params["state"] == "absent":
changed = True
if not module.check_mode:
try:
array.disable_network_interface(interface["name"])
except Exception:
module.fail_json(
msg="Failed to disable interface {0}.".format(interface["name"])
)
if (
module.params["servicelist"]
and sorted(module.params["servicelist"]) != interface["services"]
):
api_version = array._list_available_rest_versions()
if FC_ENABLE_API in api_version:
if HAS_PYPURECLIENT:
array = get_array(module)
changed = True
if not module.check_mode:
network = NetworkInterfacePatch(
services=module.params["servicelist"]
)
res = array.patch_network_interfaces(
names=[module.params["name"]], network=network
)
if res.status_code != 200:
module.fail_json(
msg="Failed to update interface service list {0}. Error: {1}".format(
module.params["name"], res.errors[0].message
)
)
else:
module.warn_json(
"Servicelist not update as pypureclient module is required"
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
name=dict(type="str", required=True),
state=dict(type="str", default="present", choices=["present", "absent"]),
address=dict(type="str"),
gateway=dict(type="str"),
mtu=dict(type="int", default=1500),
servicelist=dict(
type="list",
elements="str",
choices=[
"replication",
"management",
"ds",
"file",
"iscsi",
"scsi-fc",
"nvme-fc",
"system",
],
),
)
)
module = AnsibleModule(argument_spec, supports_check_mode=True)
if not HAS_NETADDR:
module.fail_json(msg="netaddr module is required")
array = get_system(module)
api_version = array._list_available_rest_versions()
if not _is_cbs(array):
if module.params["servicelist"] and "system" in module.params["servicelist"]:
module.fail_json(
msg="Only Cloud Block Store supports the 'system' service type"
)
if "." in module.params["name"]:
if module.params["name"].split(".")[1][0].lower() == "f":
if not HAS_PYPURECLIENT:
module.fail_json(msg="pypureclient module is required")
array = get_array(module)
interface = _get_fc_interface(module, array)
if not interface:
module.fail_json(msg="Invalid network interface specified.")
else:
update_fc_interface(module, array, interface, api_version)
else:
interface = _get_interface(module, array)
if not interface:
module.fail_json(msg="Invalid network interface specified.")
else:
update_interface(module, array, interface)
else:
interface = _get_interface(module, array)
if not interface:
module.fail_json(msg="Invalid network interface specified.")
else:
update_interface(module, array, interface)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,151 @@
#!/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: purefa_ntp
version_added: '1.0.0'
short_description: Configure Pure Storage FlashArray NTP settings
description:
- Set or erase NTP configuration for Pure Storage FlashArrays.
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.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Delete exisitng NTP server entries
purestorage.flasharray.purefa_ntp:
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Set array NTP servers
purestorage.flasharray.purefa_ntp:
state: present
ntp_servers:
- "0.pool.ntp.org"
- "1.pool.ntp.org"
- "2.pool.ntp.org"
- "3.pool.ntp.org"
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
purefa_argument_spec,
)
def _is_cbs(array, is_cbs=False):
"""Is the selected array a Cloud Block Store"""
model = array.get(controllers=True)[0]["model"]
is_cbs = bool("CBS" in model)
return is_cbs
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, array):
"""Delete NTP Servers"""
if array.get(ntpserver=True)["ntpserver"] != []:
changed = True
if not module.check_mode:
try:
array.set(ntpserver=[])
except Exception:
module.fail_json(msg="Deletion of NTP servers failed")
else:
changed = False
module.exit_json(changed=changed)
def create_ntp(module, array):
"""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:
array.set(ntpserver=module.params["ntp_servers"][0:4])
except Exception:
module.fail_json(msg="Update of NTP servers failed")
module.exit_json(changed=changed)
def main():
argument_spec = purefa_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
)
array = get_system(module)
if _is_cbs(array):
module.warn("NTP settings are not necessary for a CBS array - ignoring...")
module.exit_json(changed=False)
if module.params["state"] == "absent":
delete_ntp(module, array)
else:
module.params["ntp_servers"] = remove(module.params["ntp_servers"])
if sorted(array.get(ntpserver=True)["ntpserver"]) != sorted(
module.params["ntp_servers"][0:4]
):
create_ntp(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,443 @@
#!/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: purefa_offload
version_added: '1.0.0'
short_description: Create, modify and delete NFS, S3 or Azure offload targets
description:
- Create, modify and delete NFS, S3 or Azure offload targets.
- Only supported on Purity v5.2.0 or higher.
- You must have a correctly configured offload network for offload to work.
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
state:
description:
- Define state of offload
default: present
choices: [ absent, present ]
type: str
name:
description:
- The name of the offload target
required: true
type: str
protocol:
description:
- Define which protocol the offload engine uses
default: nfs
choices: [ nfs, s3, azure, gcp ]
type: str
address:
description:
- The IP or FQDN address of the NFS server
type: str
share:
description:
- NFS export on the NFS server
type: str
options:
description:
- Additonal mount options for the NFS share
- Supported mount options include I(port), I(rsize),
I(wsize), I(nfsvers), and I(tcp) or I(udp)
required: false
default: ""
type: str
access_key:
description:
- Access Key ID of the offload target
type: str
container:
description:
- Name of the blob container of the Azure target
default: offload
type: str
bucket:
description:
- Name of the bucket for the S3 or GCP target
type: str
account:
description:
- Name of the Azure blob storage account
type: str
secret:
description:
- Secret Access Key for the offload target
type: str
initialize:
description:
- Define whether to initialize the offload bucket
type: bool
default: true
placement:
description:
- AWS S3 placement strategy
type: str
choices: ['retention-based', 'aws-standard-class']
default: retention-based
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Create NFS offload target
purestorage.flasharray.purefa_offload:
name: nfs-offload
protocol: nfs
address: 10.21.200.4
share: "/offload_target"
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Create S3 offload target
purestorage.flasharray.purefa_offload:
name: s3-offload
protocol: s3
access_key: "3794fb12c6204e19195f"
bucket: offload-bucket
secret: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
placement: aws-standard-class
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Create Azure offload target
purestorage.flasharray.purefa_offload:
name: azure-offload
protocol: azure
secret: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
container: offload-container
account: user1
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete offload target
purestorage.flasharray.purefa_offload:
name: nfs-offload
protocol: nfs
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from pypureclient import flasharray
except ImportError:
HAS_PURESTORAGE = False
HAS_PACKAGING = True
try:
from packaging import version
except ImportError:
HAS_PACKAGING = False
import re
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_array,
get_system,
purefa_argument_spec,
)
MIN_REQUIRED_API_VERSION = "1.16"
REGEX_TARGET_NAME = re.compile(r"^[a-zA-Z0-9\-]*$")
P53_API_VERSION = "1.17"
GCP_API_VERSION = "2.3"
MULTIOFFLOAD_API_VERSION = "2.11"
MULTIOFFLOAD_LIMIT = 5
def get_target(module, array):
"""Return target or None"""
try:
return array.get_offload(module.params["name"])
except Exception:
return None
def create_offload(module, array):
"""Create offload target"""
changed = True
api_version = array._list_available_rest_versions()
# First check if the offload network inteface is there and enabled
try:
if not array.get_network_interface("@offload.data")["enabled"]:
module.fail_json(
msg="Offload Network interface not enabled. Please resolve."
)
except Exception:
module.fail_json(
msg="Offload Network interface not correctly configured. Please resolve."
)
if not module.check_mode:
if module.params["protocol"] == "nfs":
try:
array.connect_nfs_offload(
module.params["name"],
mount_point=module.params["share"],
address=module.params["address"],
mount_options=module.params["options"],
)
except Exception:
module.fail_json(
msg="Failed to create NFS offload {0}. "
"Please perform diagnostic checks.".format(module.params["name"])
)
if module.params["protocol"] == "s3":
if P53_API_VERSION in api_version:
try:
array.connect_s3_offload(
module.params["name"],
access_key_id=module.params["access_key"],
secret_access_key=module.params["secret"],
bucket=module.params["bucket"],
placement_strategy=module.params["placement"],
initialize=module.params["initialize"],
)
except Exception:
module.fail_json(
msg="Failed to create S3 offload {0}. "
"Please perform diagnostic checks.".format(
module.params["name"]
)
)
else:
try:
array.connect_s3_offload(
module.params["name"],
access_key_id=module.params["access_key"],
secret_access_key=module.params["secret"],
bucket=module.params["bucket"],
initialize=module.params["initialize"],
)
except Exception:
module.fail_json(
msg="Failed to create S3 offload {0}. "
"Please perform diagnostic checks.".format(
module.params["name"]
)
)
if module.params["protocol"] == "azure" and P53_API_VERSION in api_version:
try:
array.connect_azure_offload(
module.params["name"],
container_name=module.params["container"],
secret_access_key=module.params["secret"],
account_name=module.params[".bucket"],
initialize=module.params["initialize"],
)
except Exception:
module.fail_json(
msg="Failed to create Azure offload {0}. "
"Please perform diagnostic checks.".format(module.params["name"])
)
if module.params["protocol"] == "gcp" and GCP_API_VERSION in api_version:
arrayv6 = get_array(module)
bucket = flasharray.OffloadGoogleCloud(
access_key_id=module.params["access_key"],
bucket=module.params["bucket"],
secret_access_key=module.params["secret"],
)
offload = flasharray.OffloadPost(google_cloud=bucket)
res = arrayv6.post_offloads(
offload=offload,
initialize=module.params["initialize"],
names=[module.params["name"]],
)
if res.status_code != 200:
module.fail_json(
msg="Failed to create GCP offload {0}. Error: {1}"
"Please perform diagnostic checks.".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def update_offload(module, array):
"""Update offload target"""
changed = False
module.exit_json(changed=changed)
def delete_offload(module, array):
"""Delete offload target"""
changed = True
api_version = array._list_available_rest_versions()
if not module.check_mode:
if module.params["protocol"] == "nfs":
try:
array.disconnect_nfs_offload(module.params["name"])
except Exception:
module.fail_json(
msg="Failed to delete NFS offload {0}.".format(
module.params["name"]
)
)
if module.params["protocol"] == "s3":
try:
array.disconnect_s3_offload(module.params["name"])
except Exception:
module.fail_json(
msg="Failed to delete S3 offload {0}.".format(module.params["name"])
)
if module.params["protocol"] == "azure" and P53_API_VERSION in api_version:
try:
array.disconnect_azure_offload(module.params["name"])
except Exception:
module.fail_json(
msg="Failed to delete Azure offload {0}.".format(
module.params["name"]
)
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
state=dict(type="str", default="present", choices=["present", "absent"]),
protocol=dict(
type="str", default="nfs", choices=["nfs", "s3", "azure", "gcp"]
),
placement=dict(
type="str",
default="retention-based",
choices=["retention-based", "aws-standard-class"],
),
name=dict(type="str", required=True),
initialize=dict(default=True, type="bool"),
access_key=dict(type="str", no_log=False),
secret=dict(type="str", no_log=True),
bucket=dict(type="str"),
container=dict(type="str", default="offload"),
account=dict(type="str"),
share=dict(type="str"),
address=dict(type="str"),
options=dict(type="str", default=""),
)
)
required_if = []
if argument_spec["state"] == "present":
required_if = [
("protocol", "nfs", ["address", "share"]),
("protocol", "s3", ["access_key", "secret", "bucket"]),
["protocol", "gcp", ["access_key", "secret", "bucket"]],
("protocol", "azure", ["account", "secret"]),
]
module = AnsibleModule(
argument_spec, required_if=required_if, supports_check_mode=True
)
if not HAS_PACKAGING:
module.fail_json(msg="packagingsdk is required for this module")
if not HAS_PURESTORAGE and module.params["protocol"] == "gcp":
module.fail_json(msg="py-pure-client sdk is required for this module")
array = get_system(module)
api_version = array._list_available_rest_versions()
if MIN_REQUIRED_API_VERSION not in api_version:
module.fail_json(
msg="FlashArray REST version not supported. "
"Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
)
if (
not re.match(r"^[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9]$", module.params["name"])
or len(module.params["name"]) > 56
):
module.fail_json(
msg="Target name invalid. "
"Target name must be between 1 and 56 characters (alphanumeric and -) in length "
"and begin and end with a letter or number. The name must include at least one letter."
)
if module.params["protocol"] in ["s3", "gcp"]:
if (
not re.match(r"^[a-z0-9][a-z0-9.\-]*[a-z0-9]$", module.params["bucket"])
or len(module.params["bucket"]) > 63
):
module.fail_json(
msg="Bucket name invalid. "
"Bucket name must be between 3 and 63 characters "
"(lowercase, alphanumeric, dash or period) in length "
"and begin and end with a letter or number."
)
apps = array.list_apps()
app_version = 0
all_good = False
for app in range(0, len(apps)):
if apps[app]["name"] == "offload":
if (
apps[app]["enabled"]
and apps[app]["status"] == "healthy"
and version.parse(apps[app]["version"]) >= version.parse("5.2.0")
):
all_good = True
app_version = apps[app]["version"]
break
if not all_good:
module.fail_json(
msg="Correct Offload app not installed or incorrectly configured"
)
else:
if version.parse(array.get()["version"]) != version.parse(app_version):
module.fail_json(
msg="Offload app version must match Purity version. Please upgrade."
)
target = get_target(module, array)
if module.params["state"] == "present" and not target:
offloads = array.list_offload()
target_count = len(offloads)
if MIN_REQUIRED_API_VERSION not in api_version:
MULTIOFFLOAD_LIMIT = 1
if target_count >= MULTIOFFLOAD_LIMIT:
module.fail_json(
msg="Cannot add offload target {0}. Offload Target Limit of {1} would be exceeded.".format(
module.params["name"], MULTIOFFLOAD_LIMIT
)
)
# TODO: (SD) Remove this check when multi-protocol offloads are supported
if offloads[0].protocol != module.params["protocol"]:
module.fail_json(msg="Currently all offloads must be of the same type.")
create_offload(module, array)
elif module.params["state"] == "present" and target:
update_offload(module, array)
elif module.params["state"] == "absent" and target:
delete_offload(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,905 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (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: purefa_pg
version_added: '1.0.0'
short_description: Manage protection groups on Pure Storage FlashArrays
description:
- Create, delete or modify protection groups on Pure Storage FlashArrays.
- If a protection group exists and you try to add non-valid types, eg. a host
to a volume protection group the module will ignore the invalid types.
- Protection Groups on Offload targets are supported.
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
name:
description:
- The name of the protection group.
type: str
aliases: [ pgroup ]
required: true
state:
description:
- Define whether the protection group should exist or not.
type: str
default: present
choices: [ absent, present ]
volume:
description:
- List of existing volumes to add to protection group.
- Note that volume are case-sensitive however FlashArray volume names are unique
and ignore case - you cannot have I(volumea) and I(volumeA)
type: list
elements: str
host:
description:
- List of existing hosts to add to protection group.
- Note that hostnames are case-sensitive however FlashArray hostnames are unique
and ignore case - you cannot have I(hosta) and I(hostA)
type: list
elements: str
hostgroup:
description:
- List of existing hostgroups to add to protection group.
- Note that hostgroups are case-sensitive however FlashArray hostgroup names are unique
and ignore case - you cannot have I(groupa) and I(groupA)
type: list
elements: str
eradicate:
description:
- Define whether to eradicate the protection group on delete and leave in trash.
type : bool
default: 'no'
enabled:
description:
- Define whether to enabled snapshots for the protection group.
type : bool
default: 'yes'
target:
description:
- List of remote arrays or offload target for replication protection group
to connect to.
- Note that all replicated protection groups are asynchronous.
- Target arrays or offload targets must already be connected to the source array.
- Maximum number of targets per Portection Group is 4, assuming your
configuration suppors this.
type: list
elements: str
rename:
description:
- Rename a protection group
- If the source protection group is in a Pod or Volume Group 'container'
you only need to provide the new protection group name in the same 'container'
type: str
safe_mode:
description:
- Enables SafeMode restrictions on the protection group
- B(Once set disabling this can only be performed by Pure Technical Support)
type: bool
default: False
version_added: '1.13.0'
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Create new local protection group
purestorage.flasharray.purefa_pg:
name: foo
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Create new protection group called bar in pod called foo
purestorage.flasharray.purefa_pg:
name: "foo::bar"
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Create new replicated protection group
purestorage.flasharray.purefa_pg:
name: foo
target:
- arrayb
- arrayc
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Create new replicated protection group to offload target and remote array
purestorage.flasharray.purefa_pg:
name: foo
target:
- offload
- arrayc
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Create new protection group with snapshots disabled
purestorage.flasharray.purefa_pg:
name: foo
enabled: false
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete protection group
purestorage.flasharray.purefa_pg:
name: foo
eradicate: true
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: absent
- name: Eradicate protection group foo on offload target where source array is arrayA
purestorage.flasharray.purefa_pg:
name: "arrayA:foo"
target: offload
eradicate: true
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: absent
- name: Rename protection group foo in pod arrayA to bar
purestorage.flasharray.purefa_pg:
name: "arrayA::foo"
rename: bar
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Create protection group for hostgroups
purestorage.flasharray.purefa_pg:
name: bar
hostgroup:
- hg1
- hg2
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Create protection group for hosts
purestorage.flasharray.purefa_pg:
name: bar
host:
- host1
- host2
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Create replicated protection group for volumes
purestorage.flasharray.purefa_pg:
name: bar
volume:
- vol1
- vol2
target: arrayb
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
import re
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
HAS_PURESTORAGE = True
try:
from pypureclient import flasharray
except ImportError:
HAS_PURESTORAGE = False
OFFLOAD_API_VERSION = "1.16"
P53_API_VERSION = "1.17"
AC_PG_API_VERSION = "1.13"
RETENTION_LOCK_VERSION = "2.13"
def get_pod(module, array):
"""Get ActiveCluster Pod"""
pod_name = module.params["name"].split("::")[0]
try:
return array.get_pod(pod=pod_name)
except Exception:
return None
def get_targets(array):
"""Get Offload Targets"""
targets = []
try:
target_details = array.list_offload()
except Exception:
return None
for targetcnt in range(0, len(target_details)):
if target_details[targetcnt]["status"] == "connected":
targets.append(target_details[targetcnt]["name"])
return targets
def get_arrays(array):
"""Get Connected Arrays"""
arrays = []
array_details = array.list_array_connections()
api_version = array._list_available_rest_versions()
for arraycnt in range(0, len(array_details)):
if P53_API_VERSION in api_version:
if array_details[arraycnt]["status"] == "connected":
arrays.append(array_details[arraycnt]["array_name"])
else:
if array_details[arraycnt]["connected"]:
arrays.append(array_details[arraycnt]["array_name"])
return arrays
def get_pending_pgroup(module, array):
"""Get Protection Group"""
pgroup = None
if ":" in module.params["name"]:
if "::" not in module.params["name"]:
for pgrp in array.list_pgroups(pending=True, on="*"):
if pgrp["name"] == module.params["name"] and pgrp["time_remaining"]:
pgroup = pgrp
break
else:
for pgrp in array.list_pgroups(pending=True):
if pgrp["name"] == module.params["name"] and pgrp["time_remaining"]:
pgroup = pgrp
break
else:
for pgrp in array.list_pgroups(pending=True):
if pgrp["name"] == module.params["name"] and pgrp["time_remaining"]:
pgroup = pgrp
break
return pgroup
def get_pgroup(module, array):
"""Get Protection Group"""
pgroup = None
if ":" in module.params["name"]:
if "::" not in module.params["name"]:
for pgrp in array.list_pgroups(on="*"):
if pgrp["name"] == module.params["name"]:
pgroup = pgrp
break
else:
for pgrp in array.list_pgroups():
if pgrp["name"] == module.params["name"]:
pgroup = pgrp
break
else:
for pgrp in array.list_pgroups():
if pgrp["name"] == module.params["name"]:
pgroup = pgrp
break
return pgroup
def get_pgroup_sched(module, array):
"""Get Protection Group Schedule"""
pgroup = None
for pgrp in array.list_pgroups(schedule=True):
if pgrp["name"] == module.params["name"]:
pgroup = pgrp
break
return pgroup
def check_pg_on_offload(module, array):
"""Check if PG already exists on offload target"""
array_name = array.get()["array_name"]
remote_pg = array_name + ":" + module.params["name"]
targets = get_targets(array)
for target in targets:
remote_pgs = array.list_pgroups(pending=True, on=target)
for rpg in range(0, len(remote_pgs)):
if remote_pg == remote_pgs[rpg]["name"]:
return target
return None
def make_pgroup(module, array):
"""Create Protection Group"""
changed = True
if module.params["target"]:
api_version = array._list_available_rest_versions()
connected_targets = []
connected_arrays = get_arrays(array)
if OFFLOAD_API_VERSION in api_version:
connected_targets = get_targets(array)
offload_name = check_pg_on_offload(module, array)
if offload_name and offload_name in module.params["target"][0:4]:
module.fail_json(
msg="Protection Group {0} already exists on offload target {1}.".format(
module.params["name"], offload_name
)
)
connected_arrays = connected_arrays + connected_targets
if connected_arrays == []:
module.fail_json(msg="No connected targets on source array.")
if set(module.params["target"][0:4]).issubset(connected_arrays):
if not module.check_mode:
try:
array.create_pgroup(
module.params["name"], targetlist=module.params["target"][0:4]
)
except Exception:
module.fail_json(
msg="Creation of replicated pgroup {0} failed. {1}".format(
module.params["name"], module.params["target"][0:4]
)
)
else:
module.fail_json(
msg="Check all selected targets are connected to the source array."
)
else:
if not module.check_mode:
try:
array.create_pgroup(module.params["name"])
except Exception:
module.fail_json(
msg="Creation of pgroup {0} failed.".format(module.params["name"])
)
try:
if module.params["target"]:
array.set_pgroup(
module.params["name"],
replicate_enabled=module.params["enabled"],
)
else:
array.set_pgroup(
module.params["name"], snap_enabled=module.params["enabled"]
)
except Exception:
module.fail_json(
msg="Enabling pgroup {0} failed.".format(module.params["name"])
)
if module.params["volume"]:
try:
array.set_pgroup(
module.params["name"], vollist=module.params["volume"]
)
except Exception:
module.fail_json(
msg="Adding volumes to pgroup {0} failed.".format(
module.params["name"]
)
)
if module.params["host"]:
try:
array.set_pgroup(
module.params["name"], hostlist=module.params["host"]
)
except Exception:
module.fail_json(
msg="Adding hosts to pgroup {0} failed.".format(
module.params["name"]
)
)
if module.params["hostgroup"]:
try:
array.set_pgroup(
module.params["name"], hgrouplist=module.params["hostgroup"]
)
except Exception:
module.fail_json(
msg="Adding hostgroups to pgroup {0} failed.".format(
module.params["name"]
)
)
if module.params["safe_mode"]:
arrayv6 = get_array(module)
try:
arrayv6.patch_protection_groups(
names=[module.params["name"]],
protection_group=flasharray.ProtectionGroup(
retention_lock="ratcheted"
),
)
except Exception:
module.fail_json(
msg="Failed to set SafeMode on pgroup {0}".format(
module.params["name"]
)
)
module.exit_json(changed=changed)
def rename_exists(module, array):
"""Determine if rename target already exists"""
exists = False
new_name = module.params["rename"]
if ":" in module.params["name"]:
container = module.params["name"].split(":")[0]
new_name = container + ":" + module.params["rename"]
if "::" in module.params["name"]:
new_name = container + "::" + module.params["rename"]
for pgroup in array.list_pgroups(pending=True):
if pgroup["name"].lower() == new_name.lower():
exists = True
break
return exists
def update_pgroup(module, array):
"""Update Protection Group"""
changed = renamed = False
api_version = array._list_available_rest_versions()
if module.params["target"]:
connected_targets = []
connected_arrays = get_arrays(array)
if OFFLOAD_API_VERSION in api_version:
connected_targets = get_targets(array)
connected_arrays = connected_arrays + connected_targets
if connected_arrays == []:
module.fail_json(msg="No targets connected to source array.")
current_connects = array.get_pgroup(module.params["name"])["targets"]
current_targets = []
if current_connects:
for targetcnt in range(0, len(current_connects)):
current_targets.append(current_connects[targetcnt]["name"])
if set(module.params["target"][0:4]) != set(current_targets):
if not set(module.params["target"][0:4]).issubset(connected_arrays):
module.fail_json(
msg="Check all selected targets are connected to the source array."
)
changed = True
if not module.check_mode:
try:
array.set_pgroup(
module.params["name"],
targetlist=module.params["target"][0:4],
)
except Exception:
module.fail_json(
msg="Changing targets for pgroup {0} failed.".format(
module.params["name"]
)
)
if (
module.params["target"]
and module.params["enabled"]
!= get_pgroup_sched(module, array)["replicate_enabled"]
):
changed = True
if not module.check_mode:
try:
array.set_pgroup(
module.params["name"], replicate_enabled=module.params["enabled"]
)
except Exception:
module.fail_json(
msg="Changing enabled status of pgroup {0} failed.".format(
module.params["name"]
)
)
elif (
not module.params["target"]
and module.params["enabled"] != get_pgroup_sched(module, array)["snap_enabled"]
):
changed = True
if not module.check_mode:
try:
array.set_pgroup(
module.params["name"], snap_enabled=module.params["enabled"]
)
except Exception:
module.fail_json(
msg="Changing enabled status of pgroup {0} failed.".format(
module.params["name"]
)
)
if (
module.params["volume"]
and get_pgroup(module, array)["hosts"] is None
and get_pgroup(module, array)["hgroups"] is None
):
if get_pgroup(module, array)["volumes"] is None:
if not module.check_mode:
changed = True
try:
array.set_pgroup(
module.params["name"], vollist=module.params["volume"]
)
except Exception:
module.fail_json(
msg="Adding volumes to pgroup {0} failed.".format(
module.params["name"]
)
)
else:
cased_vols = [vol.lower() for vol in module.params["volume"]]
cased_pgvols = [vol.lower() for vol in get_pgroup(module, array)["volumes"]]
if not all(x in cased_pgvols for x in cased_vols):
if not module.check_mode:
changed = True
try:
array.set_pgroup(
module.params["name"], addvollist=module.params["volume"]
)
except Exception:
module.fail_json(
msg="Changing volumes in pgroup {0} failed.".format(
module.params["name"]
)
)
if (
module.params["host"]
and get_pgroup(module, array)["volumes"] is None
and get_pgroup(module, array)["hgroups"] is None
):
if get_pgroup(module, array)["hosts"] is None:
if not module.check_mode:
changed = True
try:
array.set_pgroup(
module.params["name"], hostlist=module.params["host"]
)
except Exception:
module.fail_json(
msg="Adding hosts to pgroup {0} failed.".format(
module.params["name"]
)
)
else:
cased_hosts = [host.lower() for host in module.params["host"]]
cased_pghosts = [
host.lower() for host in get_pgroup(module, array)["hosts"]
]
if not all(x in cased_pghosts for x in cased_hosts):
if not module.check_mode:
changed = True
try:
array.set_pgroup(
module.params["name"], addhostlist=module.params["host"]
)
except Exception:
module.fail_json(
msg="Changing hosts in pgroup {0} failed.".format(
module.params["name"]
)
)
if (
module.params["hostgroup"]
and get_pgroup(module, array)["hosts"] is None
and get_pgroup(module, array)["volumes"] is None
):
if get_pgroup(module, array)["hgroups"] is None:
if not module.check_mode:
changed = True
try:
array.set_pgroup(
module.params["name"], hgrouplist=module.params["hostgroup"]
)
except Exception:
module.fail_json(
msg="Adding hostgroups to pgroup {0} failed.".format(
module.params["name"]
)
)
else:
cased_hostg = [hostg.lower() for hostg in module.params["hostgroup"]]
cased_pghostg = [
hostg.lower() for hostg in get_pgroup(module, array)["hgroups"]
]
if not all(x in cased_pghostg for x in cased_hostg):
if not module.check_mode:
changed = True
try:
array.set_pgroup(
module.params["name"],
addhgrouplist=module.params["hostgroup"],
)
except Exception:
module.fail_json(
msg="Changing hostgroups in pgroup {0} failed.".format(
module.params["name"]
)
)
if module.params["rename"]:
if not rename_exists(module, array):
if ":" in module.params["name"]:
container = module.params["name"].split(":")[0]
if "::" in module.params["name"]:
rename = container + "::" + module.params["rename"]
else:
rename = container + ":" + module.params["rename"]
else:
rename = module.params["rename"]
renamed = True
if not module.check_mode:
try:
array.rename_pgroup(module.params["name"], rename)
module.params["name"] = rename
except Exception:
module.fail_json(msg="Rename to {0} failed.".format(rename))
else:
module.warn(
"Rename failed. Protection group {0} already exists in container. Continuing with other changes...".format(
module.params["rename"]
)
)
if RETENTION_LOCK_VERSION in api_version:
arrayv6 = get_array(module)
current_pg = list(
arrayv6.get_protection_groups(names=[module.params["name"]]).items
)[0]
if current_pg.retention_lock == "unlocked" and module.params["safe_mode"]:
changed = True
if not module.check_mode:
res = arrayv6.patch_protection_groups(
names=[module.params["name"]],
protection_group=flasharray.ProtectionGroup(
retention_lock="ratcheted"
),
)
if res.status_code != 200:
module.fail_json(
msg="Failed to set SafeMode on protection group {0}. Error: {1}".format(
module.params["name"],
res.errors[0].message,
)
)
if current_pg.retention_lock == "ratcheted" and not module.params["safe_mode"]:
module.warn(
"Disabling SafeMode on protection group {0} can only be performed by Pure Technical Support".format(
module.params["name"]
)
)
changed = changed or renamed
module.exit_json(changed=changed)
def eradicate_pgroup(module, array):
"""Eradicate Protection Group"""
changed = True
if not module.check_mode:
if ":" in module.params["name"]:
if "::" not in module.params["name"]:
try:
target = "".join(module.params["target"])
array.destroy_pgroup(
module.params["name"], on=target, eradicate=True
)
except Exception:
module.fail_json(
msg="Eradicating pgroup {0} failed.".format(
module.params["name"]
)
)
else:
try:
array.destroy_pgroup(module.params["name"], eradicate=True)
except Exception:
module.fail_json(
msg="Eradicating pgroup {0} failed.".format(
module.params["name"]
)
)
else:
try:
array.destroy_pgroup(module.params["name"], eradicate=True)
except Exception:
module.fail_json(
msg="Eradicating pgroup {0} failed.".format(module.params["name"])
)
module.exit_json(changed=changed)
def delete_pgroup(module, array):
"""Delete Protection Group"""
changed = True
if not module.check_mode:
if ":" in module.params["name"]:
if "::" not in module.params["name"]:
try:
target = "".join(module.params["target"])
array.destroy_pgroup(module.params["name"], on=target)
except Exception:
module.fail_json(
msg="Deleting pgroup {0} failed.".format(module.params["name"])
)
else:
try:
array.destroy_pgroup(module.params["name"])
except Exception:
module.fail_json(
msg="Deleting pgroup {0} failed.".format(module.params["name"])
)
else:
try:
array.destroy_pgroup(module.params["name"])
except Exception:
module.fail_json(
msg="Deleting pgroup {0} failed.".format(module.params["name"])
)
if module.params["eradicate"]:
eradicate_pgroup(module, array)
module.exit_json(changed=changed)
def recover_pgroup(module, array):
"""Recover deleted protection group"""
changed = True
if not module.check_mode:
if ":" in module.params["name"]:
if "::" not in module.params["name"]:
try:
target = "".join(module.params["target"])
array.recover_pgroup(module.params["name"], on=target)
except Exception:
module.fail_json(
msg="Recover pgroup {0} failed.".format(module.params["name"])
)
else:
try:
array.recover_pgroup(module.params["name"])
except Exception:
module.fail_json(
msg="Recover pgroup {0} failed.".format(module.params["name"])
)
else:
try:
array.recover_pgroup(module.params["name"])
except Exception:
module.fail_json(
msg="ecover pgroup {0} failed.".format(module.params["name"])
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
name=dict(type="str", required=True, aliases=["pgroup"]),
state=dict(type="str", default="present", choices=["absent", "present"]),
volume=dict(type="list", elements="str"),
host=dict(type="list", elements="str"),
hostgroup=dict(type="list", elements="str"),
target=dict(type="list", elements="str"),
safe_mode=dict(type="bool", default=False),
eradicate=dict(type="bool", default=False),
enabled=dict(type="bool", default=True),
rename=dict(type="str"),
)
)
mutually_exclusive = [["volume", "host", "hostgroup"]]
module = AnsibleModule(
argument_spec, mutually_exclusive=mutually_exclusive, supports_check_mode=True
)
if not HAS_PURESTORAGE and module.params["safe_mode"]:
module.fail_json(
msg="py-pure-client sdk is required to support 'safe_mode' parameter"
)
state = module.params["state"]
array = get_system(module)
pattern = re.compile("^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$")
if module.params["rename"]:
module.params["rename"] = module.params["rename"].lower()
if not pattern.match(module.params["rename"]):
module.fail_json(
msg="Rename value {0} does not conform to naming convention".format(
module.params["rename"]
)
)
if not pattern.match(module.params["name"].split(":")[-1]):
module.fail_json(
msg="Protection Group name {0} does not conform to naming convention".format(
module.params["name"]
)
)
api_version = array._list_available_rest_versions()
if module.params["safe_mode"] and RETENTION_LOCK_VERSION not in api_version:
module.fail_json(
msg="API version does not support setting SafeMode on a protection group."
)
if ":" in module.params["name"] and OFFLOAD_API_VERSION not in api_version:
module.fail_json(msg="API version does not support offload protection groups.")
if "::" in module.params["name"] and AC_PG_API_VERSION not in api_version:
module.fail_json(
msg="API version does not support ActiveCluster protection groups."
)
if ":" in module.params["name"]:
if "::" in module.params["name"]:
pgname = module.params["name"].split("::")[1]
else:
pgname = module.params["name"].split(":")[1]
if not pattern.match(pgname):
module.fail_json(
msg="Protection Group name {0} does not conform to naming convention".format(
pgname
)
)
else:
if not pattern.match(module.params["name"]):
module.fail_json(
msg="Protection Group name {0} does not conform to naming convention".format(
module.params["name"]
)
)
pgroup = get_pgroup(module, array)
xpgroup = get_pending_pgroup(module, array)
if "::" in module.params["name"]:
if not get_pod(module, array):
module.fail_json(
msg="Pod {0} does not exist.".format(
module.params["name"].split("::")[0]
)
)
if module.params["host"]:
try:
for hst in module.params["host"]:
array.get_host(hst)
except Exception:
module.fail_json(msg="Host {0} not found".format(hst))
if module.params["hostgroup"]:
try:
for hstg in module.params["hostgroup"]:
array.get_hgroup(hstg)
except Exception:
module.fail_json(msg="Hostgroup {0} not found".format(hstg))
if pgroup and state == "present":
update_pgroup(module, array)
elif pgroup and state == "absent":
delete_pgroup(module, array)
elif xpgroup and state == "absent" and module.params["eradicate"]:
eradicate_pgroup(module, array)
elif (
not pgroup
and not xpgroup
and state == "present"
and not module.params["rename"]
):
make_pgroup(module, array)
elif not pgroup and state == "present" and module.params["rename"]:
module.exit_json(changed=False)
elif xpgroup and state == "present":
recover_pgroup(module, array)
elif pgroup is None and state == "absent":
module.exit_json(changed=False)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,521 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (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: purefa_pgsched
short_description: Manage protection groups replication schedules on Pure Storage FlashArrays
version_added: '1.0.0'
description:
- Modify or delete protection groups replication schedules on Pure Storage FlashArrays.
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
name:
description:
- The name of the protection group.
type: str
required: true
state:
description:
- Define whether to set or delete the protection group schedule.
type: str
default: present
choices: [ absent, present ]
schedule:
description:
- Which schedule to change.
type: str
choices: ['replication', 'snapshot']
required: True
enabled:
description:
- Enable the schedule being configured.
type: bool
default: True
replicate_at:
description:
- Specifies the preferred time as HH:MM:SS, using 24-hour clock, at which to generate snapshots.
type: int
blackout_start:
description:
- Specifies the time at which to suspend replication.
- Provide a time in 12-hour AM/PM format, eg. 11AM
type: str
blackout_end:
description:
- Specifies the time at which to restart replication.
- Provide a time in 12-hour AM/PM format, eg. 5PM
type: str
replicate_frequency:
description:
- Specifies the replication frequency in seconds.
- Range 900 - 34560000 (FA-405, //M10, //X10i and Cloud Block Store).
- Range 300 - 34560000 (all other arrays).
type: int
snap_at:
description:
- Specifies the preferred time as HH:MM:SS, using 24-hour clock, at which to generate snapshots.
- Only valid if I(snap_frequency) is an exact multiple of 86400, ie 1 day.
type: int
snap_frequency:
description:
- Specifies the snapshot frequency in seconds.
- Range available 300 - 34560000.
type: int
days:
description:
- Specifies the number of days to keep the I(per_day) snapshots beyond the
I(all_for) period before they are eradicated
- Max retention period is 4000 days
type: int
all_for:
description:
- Specifies the length of time, in seconds, to keep the snapshots on the
source array before they are eradicated.
- Range available 1 - 34560000.
type: int
per_day:
description:
- Specifies the number of I(per_day) snapshots to keep beyond the I(all_for) period.
- Maximum number is 1440
type: int
target_all_for:
description:
- Specifies the length of time, in seconds, to keep the replicated snapshots on the targets.
- Range is 1 - 34560000 seconds.
type: int
target_per_day:
description:
- Specifies the number of I(per_day) replicated snapshots to keep beyond the I(target_all_for) period.
- Maximum number is 1440
type: int
target_days:
description:
- Specifies the number of days to keep the I(target_per_day) replicated snapshots
beyond the I(target_all_for) period before they are eradicated.
- Max retention period is 4000 days
type: int
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Update protection group snapshot schedule
purestorage.flasharray.purefa_pgsched:
name: foo
schedule: snapshot
enabled: true
snap_frequency: 86400
snap_at: 15:30:00
per_day: 5
all_for: 5
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Update protection group replication schedule
purestorage.flasharray.purefa_pgsched:
name: foo
schedule: replication
enabled: true
replicate_frequency: 86400
replicate_at: 15:30:00
target_per_day: 5
target_all_for: 5
blackout_start: 2AM
blackout_end: 5AM
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete protection group snapshot schedule
purestorage.flasharray.purefa_pgsched:
name: foo
schedule: snapshot
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete protection group replication schedule
purestorage.flasharray.purefa_pgsched:
name: foo
schedule: replication
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
purefa_argument_spec,
)
def get_pending_pgroup(module, array):
"""Get Protection Group"""
pgroup = None
if ":" in module.params["name"]:
for pgrp in array.list_pgroups(pending=True, on="*"):
if pgrp["name"] == module.params["name"] and pgrp["time_remaining"]:
pgroup = pgrp
break
else:
for pgrp in array.list_pgroups(pending=True):
if pgrp["name"] == module.params["name"] and pgrp["time_remaining"]:
pgroup = pgrp
break
return pgroup
def get_pgroup(module, array):
"""Get Protection Group"""
pgroup = None
if ":" in module.params["name"]:
for pgrp in array.list_pgroups(on="*"):
if pgrp["name"] == module.params["name"]:
pgroup = pgrp
break
else:
for pgrp in array.list_pgroups():
if pgrp["name"] == module.params["name"]:
pgroup = pgrp
break
return pgroup
def _convert_to_minutes(hour):
if hour[-2:] == "AM" and hour[:2] == "12":
return 0
elif hour[-2:] == "AM":
return int(hour[:-2]) * 3600
elif hour[-2:] == "PM" and hour[:2] == "12":
return 43200
return (int(hour[:-2]) + 12) * 3600
def update_schedule(module, array):
"""Update Protection Group Schedule"""
changed = False
try:
schedule = array.get_pgroup(module.params["name"], schedule=True)
retention = array.get_pgroup(module.params["name"], retention=True)
if not schedule["replicate_blackout"]:
schedule["replicate_blackout"] = [{"start": 0, "end": 0}]
except Exception:
module.fail_json(
msg="Failed to get current schedule for pgroup {0}.".format(
module.params["name"]
)
)
current_repl = {
"replicate_frequency": schedule["replicate_frequency"],
"replicate_enabled": schedule["replicate_enabled"],
"target_days": retention["target_days"],
"replicate_at": schedule["replicate_at"],
"target_per_day": retention["target_per_day"],
"target_all_for": retention["target_all_for"],
"blackout_start": schedule["replicate_blackout"][0]["start"],
"blackout_end": schedule["replicate_blackout"][0]["end"],
}
current_snap = {
"days": retention["days"],
"snap_frequency": schedule["snap_frequency"],
"snap_enabled": schedule["snap_enabled"],
"snap_at": schedule["snap_at"],
"per_day": retention["per_day"],
"all_for": retention["all_for"],
}
if module.params["schedule"] == "snapshot":
if not module.params["snap_frequency"]:
snap_frequency = current_snap["snap_frequency"]
else:
if not 300 <= module.params["snap_frequency"] <= 34560000:
module.fail_json(
msg="Snap Frequency support is out of range (300 to 34560000)"
)
else:
snap_frequency = module.params["snap_frequency"]
if not module.params["snap_at"]:
snap_at = current_snap["snap_at"]
else:
snap_at = module.params["snap_at"]
if not module.params["days"]:
if isinstance(module.params["days"], int):
days = module.params["days"]
else:
days = current_snap["days"]
else:
if module.params["days"] > 4000:
module.fail_json(msg="Maximum value for days is 4000")
else:
days = module.params["days"]
if module.params["per_day"] is None:
per_day = current_snap["per_day"]
else:
if module.params["per_day"] > 1440:
module.fail_json(msg="Maximum value for per_day is 1440")
else:
per_day = module.params["per_day"]
if not module.params["all_for"]:
all_for = current_snap["all_for"]
else:
if module.params["all_for"] > 34560000:
module.fail_json(msg="Maximum all_for value is 34560000")
else:
all_for = module.params["all_for"]
new_snap = {
"days": days,
"snap_frequency": snap_frequency,
"snap_enabled": module.params["enabled"],
"snap_at": snap_at,
"per_day": per_day,
"all_for": all_for,
}
if current_snap != new_snap:
changed = True
if not module.check_mode:
try:
array.set_pgroup(
module.params["name"], snap_enabled=module.params["enabled"]
)
array.set_pgroup(
module.params["name"],
snap_frequency=snap_frequency,
snap_at=snap_at,
)
array.set_pgroup(
module.params["name"],
days=days,
per_day=per_day,
all_for=all_for,
)
except Exception:
module.fail_json(
msg="Failed to change snapshot schedule for pgroup {0}.".format(
module.params["name"]
)
)
else:
if not module.params["replicate_frequency"]:
replicate_frequency = current_repl["replicate_frequency"]
else:
model = array.get(controllers=True)[0]["model"]
if "405" in model or "10" in model or "CBS" in model:
if not 900 <= module.params["replicate_frequency"] <= 34560000:
module.fail_json(
msg="Replication Frequency support is out of range (900 to 34560000)"
)
else:
replicate_frequency = module.params["replicate_frequency"]
else:
if not 300 <= module.params["replicate_frequency"] <= 34560000:
module.fail_json(
msg="Replication Frequency support is out of range (300 to 34560000)"
)
else:
replicate_frequency = module.params["replicate_frequency"]
if not module.params["replicate_at"]:
replicate_at = current_repl["replicate_at"]
else:
replicate_at = module.params["replicate_at"]
if not module.params["target_days"]:
if isinstance(module.params["target_days"], int):
target_days = module.params["target_days"]
else:
target_days = current_repl["target_days"]
else:
if module.params["target_days"] > 4000:
module.fail_json(msg="Maximum value for target_days is 4000")
else:
target_days = module.params["target_days"]
if not module.params["target_per_day"]:
if isinstance(module.params["target_per_day"], int):
target_per_day = module.params["target_per_day"]
else:
target_per_day = current_repl["target_per_day"]
else:
if module.params["target_per_day"] > 1440:
module.fail_json(msg="Maximum value for target_per_day is 1440")
else:
target_per_day = module.params["target_per_day"]
if not module.params["target_all_for"]:
target_all_for = current_repl["target_all_for"]
else:
if module.params["target_all_for"] > 34560000:
module.fail_json(msg="Maximum target_all_for value is 34560000")
else:
target_all_for = module.params["target_all_for"]
if not module.params["blackout_end"]:
blackout_end = current_repl["blackout_start"]
else:
blackout_end = _convert_to_minutes(module.params["blackout_end"])
if not module.params["blackout_start"]:
blackout_start = current_repl["blackout_start"]
else:
blackout_start = _convert_to_minutes(module.params["blackout_start"])
new_repl = {
"replicate_frequency": replicate_frequency,
"replicate_enabled": module.params["enabled"],
"target_days": target_days,
"replicate_at": replicate_at,
"target_per_day": target_per_day,
"target_all_for": target_all_for,
"blackout_start": blackout_start,
"blackout_end": blackout_end,
}
if current_repl != new_repl:
changed = True
if not module.check_mode:
blackout = {"start": blackout_start, "end": blackout_end}
try:
array.set_pgroup(
module.params["name"],
replicate_enabled=module.params["enabled"],
)
array.set_pgroup(
module.params["name"],
replicate_frequency=replicate_frequency,
replicate_at=replicate_at,
)
if blackout_start == 0:
array.set_pgroup(module.params["name"], replicate_blackout=None)
else:
array.set_pgroup(
module.params["name"], replicate_blackout=blackout
)
array.set_pgroup(
module.params["name"],
target_days=target_days,
target_per_day=target_per_day,
target_all_for=target_all_for,
)
except Exception:
module.fail_json(
msg="Failed to change replication schedule for pgroup {0}.".format(
module.params["name"]
)
)
module.exit_json(changed=changed)
def delete_schedule(module, array):
"""Delete, ie. disable, Protection Group Schedules"""
changed = False
try:
current_state = array.get_pgroup(module.params["name"], schedule=True)
if module.params["schedule"] == "replication":
if current_state["replicate_enabled"]:
changed = True
if not module.check_mode:
array.set_pgroup(module.params["name"], replicate_enabled=False)
array.set_pgroup(
module.params["name"],
target_days=0,
target_per_day=0,
target_all_for=1,
)
array.set_pgroup(
module.params["name"],
replicate_frequency=14400,
replicate_blackout=None,
)
else:
if current_state["snap_enabled"]:
changed = True
if not module.check_mode:
array.set_pgroup(module.params["name"], snap_enabled=False)
array.set_pgroup(
module.params["name"], days=0, per_day=0, all_for=1
)
array.set_pgroup(module.params["name"], snap_frequency=300)
except Exception:
module.fail_json(
msg="Deleting pgroup {0} {1} schedule failed.".format(
module.params["name"], module.params["schedule"]
)
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
name=dict(type="str", required=True),
state=dict(type="str", default="present", choices=["absent", "present"]),
schedule=dict(
type="str", required=True, choices=["replication", "snapshot"]
),
blackout_start=dict(type="str"),
blackout_end=dict(type="str"),
snap_at=dict(type="int"),
replicate_at=dict(type="int"),
replicate_frequency=dict(type="int"),
snap_frequency=dict(type="int"),
all_for=dict(type="int"),
days=dict(type="int"),
per_day=dict(type="int"),
target_all_for=dict(type="int"),
target_per_day=dict(type="int"),
target_days=dict(type="int"),
enabled=dict(type="bool", default=True),
)
)
required_together = [["blackout_start", "blackout_end"]]
module = AnsibleModule(
argument_spec, required_together=required_together, supports_check_mode=True
)
state = module.params["state"]
array = get_system(module)
pgroup = get_pgroup(module, array)
if module.params["snap_at"] and module.params["snap_frequency"]:
if not module.params["snap_frequency"] % 86400 == 0:
module.fail_json(
msg="snap_at not valid unless snapshot frequency is measured in days, ie. a multiple of 86400"
)
if pgroup and state == "present":
update_schedule(module, array)
elif pgroup and state == "absent":
delete_schedule(module, array)
elif pgroup is None:
module.fail_json(
msg="Specified protection group {0} does not exist.".format(
module.params["name"]
)
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,481 @@
#!/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: purefa_pgsnap
version_added: '1.0.0'
short_description: Manage protection group snapshots on Pure Storage FlashArrays
description:
- Create or delete protection group snapshots on Pure Storage FlashArray.
- Recovery of replicated snapshots on the replica target array is enabled.
- Support for ActiveCluster and Volume Group protection groups is supported.
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
name:
description:
- The name of the source protection group.
type: str
required: true
suffix:
description:
- Suffix of snapshot name.
- Special case. If I(latest) the module will select the latest snapshot created in the group
type: str
state:
description:
- Define whether the protection group snapshot should exist or not.
Copy (added in 2.7) will create a full read/write clone of the
snapshot.
type: str
choices: [ absent, present, copy ]
default: present
eradicate:
description:
- Define whether to eradicate the snapshot on delete or leave in trash.
type: bool
default: 'no'
restore:
description:
- Restore a specific volume from a protection group snapshot.
- The protection group name is not required. Only provide the name of the
volume to be restored.
type: str
overwrite:
description:
- Define whether to overwrite the target volume if it already exists.
type: bool
default: 'no'
target:
description:
- Volume to restore a specified volume to.
- If not supplied this will default to the volume defined in I(restore)
type: str
offload:
description:
- Name of offload target on which the snapshot exists.
- This is only applicable for deletion and erasure of snapshots
type: str
now:
description:
- Whether to initiate a snapshot of the protection group immeadiately
type: bool
default: False
apply_retention:
description:
- Apply retention schedule settings to the snapshot
type: bool
default: False
remote:
description:
- Force immeadiate snapshot to remote targets
type: bool
default: False
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Create protection group snapshot foo.ansible
purestorage.flasharray.purefa_pgsnap:
name: foo
suffix: ansible
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: present
- name: Delete and eradicate protection group snapshot named foo.snap
purestorage.flasharray.purefa_pgsnap:
name: foo
suffix: snap
eradicate: true
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: absent
- name: Restore volume data from local protection group snapshot named foo.snap to volume data2
purestorage.flasharray.purefa_pgsnap:
name: foo
suffix: snap
restore: data
target: data2
overwrite: true
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: copy
- name: Restore remote protection group snapshot arrayA:pgname.snap.data to local copy
purestorage.flasharray.purefa_pgsnap:
name: arrayA:pgname
suffix: snap
restore: data
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: copy
- name: Restore AC pod protection group snapshot pod1::pgname.snap.data to pdo1::data2
purestorage.flasharray.purefa_pgsnap:
name: pod1::pgname
suffix: snap
restore: data
target: pod1::data2
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: copy
- name: Create snapshot of existing pgroup foo with suffix and force immeadiate copy to remote targets
purestorage.flasharray.purefa_pgsnap:
name: pgname
suffix: force
now: True
apply_retention: True
remote: True
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete and eradicate snapshot named foo.snap on offload target bar from arrayA
purestorage.flasharray.purefa_pgsnap:
name: "arrayA:foo"
suffix: snap
offload: bar
eradicate: true
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: absent
"""
RETURN = r"""
"""
import re
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
purefa_argument_spec,
)
from datetime import datetime
OFFLOAD_API = "1.16"
POD_SNAPSHOT = "2.4"
def _check_offload(module, array):
try:
offload = array.get_offload(module.params["offload"])
if offload["status"] == "connected":
return True
return False
except Exception:
return None
def get_pgroup(module, array):
"""Return Protection Group or None"""
try:
return array.get_pgroup(module.params["name"])
except Exception:
return None
def get_pgroupvolume(module, array):
"""Return Protection Group Volume or None"""
try:
pgroup = array.get_pgroup(module.params["name"])
if "::" in module.params["name"]:
restore_volume = (
module.params["name"].split("::")[0] + "::" + module.params["restore"]
)
else:
restore_volume = module.params["restore"]
for volume in pgroup["volumes"]:
if volume == restore_volume:
return volume
except Exception:
return None
def get_rpgsnapshot(module, array):
"""Return iReplicated Snapshot or None"""
try:
snapname = (
module.params["name"]
+ "."
+ module.params["suffix"]
+ "."
+ module.params["restore"]
)
for snap in array.list_volumes(snap=True):
if snap["name"] == snapname:
return snapname
except Exception:
return None
def get_offload_snapshot(module, array):
"""Return Snapshot (active or deleted) or None"""
try:
snapname = module.params["name"] + "." + module.params["suffix"]
for snap in array.get_pgroup(
module.params["name"], snap=True, on=module.params["offload"]
):
if snap["name"] == snapname:
return snapname
except Exception:
return None
def get_pgsnapshot(module, array):
"""Return Snapshot (active or deleted) or None"""
try:
snapname = module.params["name"] + "." + module.params["suffix"]
for snap in array.get_pgroup(module.params["name"], pending=True, snap=True):
if snap["name"] == snapname:
return snapname
except Exception:
return None
def create_pgsnapshot(module, array):
"""Create Protection Group Snapshot"""
changed = True
if not module.check_mode:
try:
if (
module.params["now"]
and array.get_pgroup(module.params["name"])["targets"] is not None
):
array.create_pgroup_snapshot(
source=module.params["name"],
suffix=module.params["suffix"],
snap=True,
apply_retention=module.params["apply_retention"],
replicate_now=module.params["remote"],
)
else:
array.create_pgroup_snapshot(
source=module.params["name"],
suffix=module.params["suffix"],
snap=True,
apply_retention=module.params["apply_retention"],
)
except Exception:
module.fail_json(
msg="Snapshot of pgroup {0} failed.".format(module.params["name"])
)
module.exit_json(changed=changed)
def restore_pgsnapvolume(module, array):
"""Restore a Protection Group Snapshot Volume"""
api_version = array._list_available_rest_versions()
changed = True
if module.params["suffix"] == "latest":
all_snaps = array.get_pgroup(
module.params["name"], snap=True, transfer=True
).reverse()
for snap in all_snaps:
if not snap["completed"]:
latest_snap = snap["name"]
break
try:
module.params["suffix"] = latest_snap.split(".")[1]
except NameError:
module.fail_json(msg="There is no completed snapshot available.")
if ":" in module.params["name"] and "::" not in module.params["name"]:
if get_rpgsnapshot(module, array) is None:
module.fail_json(
msg="Selected restore snapshot {0} does not exist in the Protection Group".format(
module.params["restore"]
)
)
else:
if get_pgroupvolume(module, array) is None:
module.fail_json(
msg="Selected restore volume {0} does not exist in the Protection Group".format(
module.params["restore"]
)
)
volume = (
module.params["name"]
+ "."
+ module.params["suffix"]
+ "."
+ module.params["restore"]
)
if "::" in module.params["target"]:
target_pod_name = module.params["target"].split(":")[0]
if "::" in module.params["name"]:
source_pod_name = module.params["name"].split(":")[0]
else:
source_pod_name = ""
if source_pod_name != target_pod_name:
if (
len(array.get_pod(target_pod_name, mediator=True)["arrays"]) > 1
and POD_SNAPSHOT not in api_version
):
module.fail_json(msg="Volume cannot be restored to a stretched pod")
if not module.check_mode:
try:
array.copy_volume(
volume, module.params["target"], overwrite=module.params["overwrite"]
)
except Exception:
module.fail_json(
msg="Failed to restore {0} from pgroup {1}".format(
volume, module.params["name"]
)
)
module.exit_json(changed=changed)
def delete_offload_snapshot(module, array):
"""Delete Offloaded Protection Group Snapshot"""
changed = False
snapname = module.params["name"] + "." + module.params["suffix"]
if ":" in module.params["name"] and module.params["offload"]:
if _check_offload(module, array):
changed = True
if not module.check_mode:
try:
array.destroy_pgroup(snapname, on=module.params["offload"])
if module.params["eradicate"]:
try:
array.eradicate_pgroup(
snapname, on=module.params["offload"]
)
except Exception:
module.fail_json(
msg="Failed to eradicate offloaded snapshot {0} on target {1}".format(
snapname, module.params["offload"]
)
)
except Exception:
pass
else:
module.fail_json(
msg="Offload target {0} does not exist or not connected".format(
module.params["offload"]
)
)
else:
module.fail_json(msg="Protection Group name not in the correct format")
module.exit_json(changed=changed)
def delete_pgsnapshot(module, array):
"""Delete Protection Group Snapshot"""
changed = True
if not module.check_mode:
snapname = module.params["name"] + "." + module.params["suffix"]
try:
array.destroy_pgroup(snapname)
if module.params["eradicate"]:
try:
array.eradicate_pgroup(snapname)
except Exception:
module.fail_json(
msg="Failed to eradicate pgroup {0}".format(snapname)
)
except Exception:
module.fail_json(msg="Failed to delete pgroup {0}".format(snapname))
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
name=dict(type="str", required=True),
suffix=dict(type="str"),
restore=dict(type="str"),
offload=dict(type="str"),
overwrite=dict(type="bool", default=False),
target=dict(type="str"),
eradicate=dict(type="bool", default=False),
now=dict(type="bool", default=False),
apply_retention=dict(type="bool", default=False),
remote=dict(type="bool", default=False),
state=dict(
type="str", default="present", choices=["absent", "present", "copy"]
),
)
)
required_if = [("state", "copy", ["suffix", "restore"])]
module = AnsibleModule(
argument_spec, required_if=required_if, supports_check_mode=True
)
pattern = re.compile("^(?=.*[a-zA-Z-])[a-zA-Z0-9]([a-zA-Z0-9-]{0,63}[a-zA-Z0-9])?$")
state = module.params["state"]
if state == "present":
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(".", "")
else:
if not pattern.match(module.params["suffix"]):
module.fail_json(
msg="Suffix name {0} does not conform to suffix name rules".format(
module.params["suffix"]
)
)
if not module.params["target"] and module.params["restore"]:
module.params["target"] = module.params["restore"]
array = get_system(module)
api_version = array._list_available_rest_versions()
if OFFLOAD_API not in api_version and module.params["offload"]:
module.fail_json(
msg="Minimum version {0} required for offload support".format(OFFLOAD_API)
)
pgroup = get_pgroup(module, array)
if pgroup is None:
module.fail_json(
msg="Protection Group {0} does not exist.".format(module.params["name"])
)
pgsnap = get_pgsnapshot(module, array)
if state != "absent" and module.params["offload"]:
module.fail_json(
msg="offload parameter not supported for state {0}".format(state)
)
elif state == "copy":
restore_pgsnapvolume(module, array)
elif state == "present" and not pgsnap:
create_pgsnapshot(module, array)
elif state == "present" and pgsnap:
module.exit_json(changed=False)
elif (
state == "absent"
and module.params["offload"]
and get_offload_snapshot(module, array)
):
delete_offload_snapshot(module, array)
elif state == "absent" and pgsnap:
delete_pgsnapshot(module, array)
elif state == "absent" and not pgsnap:
module.exit_json(changed=False)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,106 @@
#!/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: purefa_phonehome
version_added: '1.0.0'
short_description: Enable or Disable Pure Storage FlashArray Phonehome
description:
- Enablke or Disable Phonehome for a Pure Storage FlashArray.
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
state:
description:
- Define state of phonehome
type: str
default: present
choices: [ present, absent ]
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Enable Phonehome
purestorage.flasharray.purefa_phonehome:
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Disable Phonehome
purestorage.flasharray.purefa_phonehome:
state: disable
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
purefa_argument_spec,
)
def enable_ph(module, array):
"""Enable Remote Assist"""
changed = False
if array.get_phonehome()["phonehome"] != "enabled":
try:
if not module.check_mode:
array.enable_phonehome()
changed = True
except Exception:
module.fail_json(msg="Enabling Phonehome failed")
module.exit_json(changed=changed)
def disable_ph(module, array):
"""Disable Remote Assist"""
changed = False
if array.get_phonehome()["phonehome"] == "enabled":
try:
if not module.check_mode:
array.disable_phonehome()
changed = True
except Exception:
module.fail_json(msg="Disabling Remote Assist failed")
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
state=dict(type="str", default="present", choices=["present", "absent"]),
)
)
module = AnsibleModule(argument_spec, supports_check_mode=True)
array = get_system(module)
if module.params["state"] == "present":
enable_ph(module, array)
else:
disable_ph(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,572 @@
#!/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: purefa_pod
short_description: Manage AC pods in Pure Storage FlashArrays
version_added: '1.0.0'
description:
- Manage AC pods in a Pure Storage FlashArray.
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
name:
description:
- The name of the pod.
type: str
required: true
stretch:
description:
- The name of the array to stretch to/unstretch from. Must be synchromously replicated.
- To unstretch an array use state I(absent)
- You can only specify a remote array, ie you cannot unstretch a pod from the
current array and then restretch back to the current array.
- To restretch a pod you must perform this from the remaining array the pod
resides on.
type: str
failover:
description:
- The name of the array given priority to stay online if arrays loose
contact with eachother.
- Oprions are either array in the cluster, or I(auto)
type: list
elements: str
state:
description:
- Define whether the pod should exist or not.
default: present
choices: [ absent, present ]
type: str
eradicate:
description:
- Define whether to eradicate the pod on delete or leave in trash.
type: bool
default: false
target:
description:
- Name of clone target pod.
type: str
mediator:
description:
- Name of the mediator to use for a pod
type: str
default: purestorage
promote:
description:
- Promote/demote any pod not in a stretched relationship. .
- Demoting a pod will render it read-only.
required: false
type: bool
quiesce:
description:
- Quiesce/Skip quiesce when I(promote) is false and demoting an ActiveDR pod.
- Quiesce will ensure all local data has been replicated before demotion.
- Skipping quiesce looses all pending data to be replicated to the remote pod.
- Can only demote the pod if it is in a Acrive DR replica link relationship.
- This will default to True
required: false
type: bool
undo:
description:
- Use the I(undo-remote) pod when I(promote) is true and promoting an ActiveDR pod.
- This will default to True
required: false
type: bool
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Create new pod named foo
purestorage.flasharray.purefa_pod:
name: foo
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: present
- name: Delete and eradicate pod named foo
purestorage.flasharray.purefa_pod:
name: foo
eradicate: yes
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: absent
- name: Set failover array for pod named foo
purestorage.flasharray.purefa_pod:
name: foo
failover:
- array1
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Set mediator for pod named foo
purestorage.flasharray.purefa_pod:
name: foo
mediator: bar
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Stretch a pod named foo to array2
purestorage.flasharray.purefa_pod:
name: foo
stretch: array2
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Unstretch a pod named foo from array2
purestorage.flasharray.purefa_pod:
name: foo
stretch: array2
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Create clone of pod foo named bar
purestorage.flasharray.purefa_pod:
name: foo
target: bar
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
state: present
"""
RETURN = r"""
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
purefa_argument_spec,
)
POD_API_VERSION = "1.13"
def get_pod(module, array):
"""Return Pod or None"""
try:
return array.get_pod(module.params["name"])
except Exception:
return None
def get_undo_pod(module, array):
"""Return Undo Pod or None"""
try:
return array.get_pod(module.params["name"] + ".undo-demote", pending_only=True)
except Exception:
return None
def get_target(module, array):
"""Return Pod or None"""
try:
return array.get_pod(module.params["target"])
except Exception:
return None
def get_destroyed_pod(module, array):
"""Return Destroyed Volume or None"""
try:
return bool(
array.get_pod(module.params["name"], pending=True)["time_remaining"] != ""
)
except Exception:
return False
def get_destroyed_target(module, array):
"""Return Destroyed Volume or None"""
try:
return bool(
array.get_pod(module.params["target"], pending=True)["time_remaining"] != ""
)
except Exception:
return False
def check_arrays(module, array):
"""Check if array name provided are sync-replicated"""
good_arrays = []
good_arrays.append(array.get()["array_name"])
connected_arrays = array.list_array_connections()
for arr in range(0, len(connected_arrays)):
if connected_arrays[arr]["type"] == "sync-replication":
good_arrays.append(connected_arrays[arr]["array_name"])
if module.params["failover"] is not None:
if module.params["failover"] == ["auto"]:
failover_array = []
else:
failover_array = module.params["failover"]
if failover_array != []:
for arr in range(0, len(failover_array)):
if failover_array[arr] not in good_arrays:
module.fail_json(
msg="Failover array {0} is not valid.".format(
failover_array[arr]
)
)
if module.params["stretch"] is not None:
if module.params["stretch"] not in good_arrays:
module.fail_json(
msg="Stretch: Array {0} is not connected.".format(
module.params["stretch"]
)
)
return None
def create_pod(module, array):
"""Create Pod"""
changed = True
if module.params["target"]:
module.fail_json(msg="Cannot clone non-existant pod.")
if not module.check_mode:
try:
if module.params["failover"]:
array.create_pod(
module.params["name"], failover_list=module.params["failover"]
)
else:
array.create_pod(module.params["name"])
except Exception:
module.fail_json(
msg="Pod {0} creation failed.".format(module.params["name"])
)
if module.params["mediator"] != "purestorage":
try:
array.set_pod(module.params["name"], mediator=module.params["mediator"])
except Exception:
module.warn(
"Failed to communicate with mediator {0}, using default value".format(
module.params["mediator"]
)
)
if module.params["stretch"]:
current_array = array.get()["array_name"]
if module.params["stretch"] != current_array:
try:
array.add_pod(module.params["name"], module.params["rrays"])
except Exception:
module.fail_json(
msg="Failed to stretch pod {0} to array {1}.".format(
module.params["name"], module.params["stretch"]
)
)
module.exit_json(changed=changed)
def clone_pod(module, array):
"""Create Pod Clone"""
changed = False
if get_target(module, array) is None:
if not get_destroyed_target(module, array):
changed = True
if not module.check_mode:
try:
array.clone_pod(module.params["name"], module.params["target"])
except Exception:
module.fail_json(
msg="Clone pod {0} to pod {1} failed.".format(
module.params["name"], module.params["target"]
)
)
else:
module.fail_json(
msg="Target pod {0} already exists but deleted.".format(
module.params["target"]
)
)
module.exit_json(changed=changed)
def update_pod(module, array):
"""Update Pod configuration"""
changed = False
current_config = array.get_pod(module.params["name"], failover_preference=True)
if module.params["failover"]:
current_failover = current_config["failover_preference"]
if current_failover == [] or sorted(module.params["failover"]) != sorted(
current_failover
):
changed = True
if not module.check_mode:
try:
if module.params["failover"] == ["auto"]:
if current_failover != []:
array.set_pod(module.params["name"], failover_preference=[])
else:
array.set_pod(
module.params["name"],
failover_preference=module.params["failover"],
)
except Exception:
module.fail_json(
msg="Failed to set failover preference for pod {0}.".format(
module.params["name"]
)
)
current_config = array.get_pod(module.params["name"], mediator=True)
if current_config["mediator"] != module.params["mediator"]:
changed = True
if not module.check_mode:
try:
array.set_pod(module.params["name"], mediator=module.params["mediator"])
except Exception:
module.warn(
"Failed to communicate with mediator {0}. Setting unchanged.".format(
module.params["mediator"]
)
)
if module.params["promote"] is not None:
if len(current_config["arrays"]) > 1:
module.fail_json(
msg="Promotion/Demotion not permitted. Pod {0} is stretched".format(
module.params["name"]
)
)
else:
if (
current_config["promotion_status"] == "demoted"
and module.params["promote"]
):
try:
if module.params["undo"] is None:
module.params["undo"] = True
if current_config["promotion_status"] == "quiescing":
module.fail_json(
msg="Cannot promote pod {0} as it is still quiesing".format(
module.params["name"]
)
)
elif module.params["undo"]:
changed = True
if not module.check_mode:
if get_undo_pod(module, array):
array.promote_pod(
module.params["name"],
promote_from=module.params["name"] + ".undo-demote",
)
else:
array.promote_pod(module.params["name"])
module.warn(
"undo-demote pod remaining for {0}. Consider eradicating this.".format(
module.params["name"]
)
)
else:
changed = True
if not module.check_mode:
array.promote_pod(module.params["name"])
except Exception:
module.fail_json(
msg="Failed to promote pod {0}.".format(module.params["name"])
)
elif (
current_config["promotion_status"] != "demoted"
and not module.params["promote"]
):
try:
if get_undo_pod(module, array):
module.fail_json(
msg="Cannot demote pod {0} due to associated undo-demote pod not being eradicated".format(
module.params["name"]
)
)
if module.params["quiesce"] is None:
module.params["quiesce"] = True
if current_config["link_target_count"] == 0:
changed = True
if not module.check_mode:
array.demote_pod(module.params["name"])
elif not module.params["quiesce"]:
changed = True
if not module.check_mode:
array.demote_pod(module.params["name"], skip_quiesce=True)
else:
changed = True
if not module.check_mode:
array.demote_pod(module.params["name"], quiesce=True)
except Exception:
module.fail_json(
msg="Failed to demote pod {0}.".format(module.params["name"])
)
module.exit_json(changed=changed)
def stretch_pod(module, array):
"""Stretch/unstretch Pod configuration"""
changed = False
current_config = array.get_pod(module.params["name"], failover_preference=True)
if module.params["stretch"]:
current_arrays = []
for arr in range(0, len(current_config["arrays"])):
current_arrays.append(current_config["arrays"][arr]["name"])
if (
module.params["stretch"] not in current_arrays
and module.params["state"] == "present"
):
changed = True
if not module.check_mode:
try:
array.add_pod(module.params["name"], module.params["stretch"])
except Exception:
module.fail_json(
msg="Failed to stretch pod {0} to array {1}.".format(
module.params["name"], module.params["stretch"]
)
)
if (
module.params["stretch"] in current_arrays
and module.params["state"] == "absent"
):
changed = True
if not module.check_mode:
try:
array.remove_pod(module.params["name"], module.params["stretch"])
except Exception:
module.fail_json(
msg="Failed to unstretch pod {0} from array {1}.".format(
module.params["name"], module.params["stretch"]
)
)
module.exit_json(changed=changed)
def delete_pod(module, array):
"""Delete Pod"""
changed = True
if not module.check_mode:
try:
array.destroy_pod(module.params["name"])
if module.params["eradicate"]:
try:
array.eradicate_pod(module.params["name"])
except Exception:
module.fail_json(
msg="Eradicate pod {0} failed.".format(module.params["name"])
)
except Exception:
module.fail_json(msg="Delete pod {0} failed.".format(module.params["name"]))
module.exit_json(changed=changed)
def eradicate_pod(module, array):
"""Eradicate Deleted Pod"""
changed = True
if not module.check_mode:
if module.params["eradicate"]:
try:
array.eradicate_pod(module.params["name"])
except Exception:
module.fail_json(
msg="Eradication of pod {0} failed".format(module.params["name"])
)
module.exit_json(changed=changed)
def recover_pod(module, array):
"""Recover Deleted Pod"""
changed = True
if not module.check_mode:
try:
array.recover_pod(module.params["name"])
except Exception:
module.fail_json(
msg="Recovery of pod {0} failed".format(module.params["name"])
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
name=dict(type="str", required=True),
stretch=dict(type="str"),
target=dict(type="str"),
mediator=dict(type="str", default="purestorage"),
failover=dict(type="list", elements="str"),
promote=dict(type="bool"),
undo=dict(type="bool"),
quiesce=dict(type="bool"),
eradicate=dict(type="bool", default=False),
state=dict(type="str", default="present", choices=["absent", "present"]),
)
)
mutually_exclusive = [
["stretch", "failover"],
["stretch", "eradicate"],
["stretch", "mediator"],
["target", "mediator"],
["target", "stretch"],
["target", "failover"],
["target", "eradicate"],
]
module = AnsibleModule(
argument_spec, mutually_exclusive=mutually_exclusive, supports_check_mode=True
)
module.params["name"] = module.params["name"].lower()
state = module.params["state"]
array = get_system(module)
api_version = array._list_available_rest_versions()
if POD_API_VERSION not in api_version:
module.fail_json(
msg="FlashArray REST version not supported. "
"Minimum version required: {0}".format(POD_API_VERSION)
)
pod = get_pod(module, array)
destroyed = ""
if not pod:
destroyed = get_destroyed_pod(module, array)
if module.params["failover"] or module.params["failover"] != "auto":
check_arrays(module, array)
if state == "present" and not pod:
create_pod(module, array)
elif pod and module.params["stretch"]:
stretch_pod(module, array)
elif state == "present" and pod and module.params["target"]:
clone_pod(module, array)
elif state == "present" and pod and module.params["target"]:
clone_pod(module, array)
elif state == "present" and pod:
update_pod(module, array)
elif state == "absent" and pod and not module.params["stretch"]:
delete_pod(module, array)
elif state == "present" and destroyed:
recover_pod(module, array)
elif state == "absent" and destroyed:
eradicate_pod(module, array)
elif state == "absent" and not pod:
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -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 = """
---
module: purefa_pod_replica
short_description: Manage ActiveDR pod replica links between Pure Storage FlashArrays
version_added: '1.0.0'
description:
- This module manages ActiveDR pod replica links between Pure Storage FlashArrays.
author: Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
name:
description:
- ActiveDR source pod name
required: true
type: str
state:
description:
- Creates or modifies a pod 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_pod:
description:
- Name of target pod
- Must not be the same as the local pod.
type: str
required: false
pause:
description:
- Pause/unpause a pod replica link
required: false
type: bool
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = """
- name: Create new pod replica link from foo to bar on arrayB
purestorage.flasharray.purefa_pod_replica:
name: foo
target_array: arrayB
target_pod: bar
state: present
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Pause an pod replica link
purestorage.flasharray.purefa_pod_replica:
name: foo
pause: true
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete and eradicate pod replica link
purestorage.flasharray.purefa_pod_replica:
name: foo
state: absent
eradicate: true
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = """
"""
MIN_REQUIRED_API_VERSION = "1.19"
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
purefa_argument_spec,
)
def get_local_pod(module, array):
"""Return Pod or None"""
try:
return array.get_pod(module.params["name"])
except Exception:
return None
def get_local_rl(module, array):
"""Return Pod Replica Link or None"""
try:
rlinks = array.list_pod_replica_links()
for link in range(0, len(rlinks)):
if rlinks[link]["local_pod_name"] == module.params["name"]:
return rlinks[link]
return None
except Exception:
return None
def _get_arrays(array):
"""Get Connected Arrays"""
arrays = []
array_details = array.list_array_connections()
for arraycnt in range(0, len(array_details)):
arrays.append(array_details[arraycnt]["array_name"])
return arrays
def update_rl(module, array, local_rl):
"""Create Pod Replica Link"""
changed = False
if module.params["pause"] is not None:
if local_rl["status"] != "paused" and module.params["pause"]:
changed = True
if not module.check_mode:
try:
array.pause_pod_replica_link(
local_pod_name=module.params["name"],
remote_pod_name=local_rl["remote_pod_name"],
)
except Exception:
module.fail_json(
msg="Failed to pause replica link {0}.".format(
module.params["name"]
)
)
elif local_rl["status"] == "paused" and not module.params["pause"]:
changed = True
if not module.check_mode:
try:
array.resume_pod_replica_link(
local_pod_name=module.params["name"],
remote_pod_name=local_rl["remote_pod_name"],
)
except Exception:
module.fail_json(
msg="Failed to resume replica link {0}.".format(
module.params["name"]
)
)
module.exit_json(changed=changed)
def create_rl(module, array):
"""Create Pod Replica Link"""
changed = True
if not module.params["target_pod"]:
module.fail_json(msg="target_pod required to create a new replica link.")
if not module.params["target_array"]:
module.fail_json(msg="target_array required to create a new replica link.")
try:
connected_arrays = array.list_array_connections()
if connected_arrays == []:
module.fail_json(msg="No connected arrays.")
else:
good_array = False
for conn_array in range(0, len(connected_arrays)):
if connected_arrays[conn_array]["array_name"] == module.params[
"target_array"
] and connected_arrays[conn_array]["status"] in [
"connected",
"connecting",
"partially_connected",
]:
good_array = True
break
if not good_array:
module.fail_json(
msg="Target array {0} is not connected to the source array.".format(
module.params["target_array"]
)
)
else:
if not module.check_mode:
try:
array.create_pod_replica_link(
local_pod_name=module.params["name"],
remote_name=module.params["target_array"],
remote_pod_name=module.params["target_pod"],
)
except Exception:
module.fail_json(
msg="Failed to create replica link {0} to target array {1}".format(
module.params["name"], module.params["target_array"]
)
)
except Exception:
module.fail_json(
msg="Failed to create replica link for pod {0}.".format(
module.params["name"]
)
)
module.exit_json(changed=changed)
def delete_rl(module, array, local_rl):
"""Delete Pod Replica Link"""
changed = True
if not module.check_mode:
try:
array.delete_pod_replica_link(
module.params["name"], remote_pod_name=local_rl["remote_pod_name"]
)
except Exception:
module.fail_json(
msg="Failed to delete replica link for pod {0}.".format(
module.params["name"]
)
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
name=dict(type="str", required=True),
target_pod=dict(type="str"),
target_array=dict(type="str"),
pause=dict(type="bool"),
state=dict(default="present", choices=["present", "absent"]),
)
)
module = AnsibleModule(argument_spec, supports_check_mode=True)
state = module.params["state"]
array = get_system(module)
api_version = array._list_available_rest_versions()
if MIN_REQUIRED_API_VERSION not in api_version:
module.fail_json(msg="Purity v6.0.0 or higher required.")
local_pod = get_local_pod(module, array)
local_replica_link = get_local_rl(module, array)
if not local_pod:
module.fail_json(
msg="Selected local pod {0} does not exist.".format(module.params["name"])
)
if len(local_pod["arrays"]) > 1:
module.fail_json(
msg="Local Pod {0} is already stretched.".format(module.params["name"])
)
if local_replica_link:
if local_replica_link["status"] == "unhealthy":
module.fail_json(msg="Replca Link unhealthy - please check remote array")
if state == "present" and not local_replica_link:
create_rl(module, array)
elif state == "present" and local_replica_link:
update_rl(module, array, local_replica_link)
elif state == "absent" and local_replica_link:
delete_rl(module, array, local_replica_link)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,131 @@
#!/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: purefa_proxy
version_added: '1.0.0'
author:
- Pure Storage ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
short_description: Configure FlashArray phonehome HTTPs proxy settings
description:
- Set or erase configuration for the HTTPS 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
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Delete exisitng proxy settings
purestorage.flasharray.purefa_proxy:
state: absent
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Set proxy settings
purestorage.flasharray.purefa_proxy:
host: purestorage.com
port: 8080
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
purefa_argument_spec,
)
def delete_proxy(module, array):
"""Delete proxy settings"""
changed = False
current_proxy = array.get(proxy=True)["proxy"]
if current_proxy != "":
changed = True
if not module.check_mode:
try:
array.set(proxy="")
except Exception:
module.fail_json(msg="Delete proxy settigs failed")
module.exit_json(changed=changed)
def create_proxy(module, array):
"""Set proxy settings"""
changed = False
current_proxy = array.get(proxy=True)
if current_proxy is not None:
new_proxy = (
"https://" + module.params["host"] + ":" + str(module.params["port"])
)
if new_proxy != current_proxy["proxy"]:
changed = True
if not module.check_mode:
try:
array.set(proxy=new_proxy)
except Exception:
module.fail_json(msg="Set phone home proxy failed.")
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
state=dict(type="str", default="present", choices=["absent", "present"]),
host=dict(type="str"),
port=dict(type="int"),
)
)
required_together = [["host", "port"]]
module = AnsibleModule(
argument_spec, required_together=required_together, supports_check_mode=True
)
state = module.params["state"]
array = get_system(module)
if state == "absent":
delete_proxy(module, array)
elif state == "present":
create_proxy(module, array)
else:
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,121 @@
#!/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: purefa_ra
version_added: '1.0.0'
short_description: Enable or Disable Pure Storage FlashArray Remote Assist
description:
- Enablke or Disable Remote Assist for a Pure Storage FlashArray.
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: enable
choices: [ enable, disable ]
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Enable Remote Assist port
purestorage.flasharray.purefa_ra:
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
register: result
debug:
msg: "Remote Assist: {{ result['ra_facts'] }}"
- name: Disable Remote Assist port
purestorage.flasharray.purefa_ra:
state: disable
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
purefa_argument_spec,
)
def enable_ra(module, array):
"""Enable Remote Assist"""
changed = False
ra_facts = {}
if not array.get_remote_assist_status()["status"] in ["connected", "enabled"]:
changed = True
if not module.check_mode:
try:
ra_data = array.enable_remote_assist()
ra_facts["fa_ra"] = {"name": ra_data["name"], "port": ra_data["port"]}
except Exception:
module.fail_json(msg="Enabling Remote Assist failed")
else:
if not module.check_mode:
try:
ra_data = array.get_remote_assist_status()
ra_facts["fa_ra"] = {"name": ra_data["name"], "port": ra_data["port"]}
except Exception:
module.fail_json(msg="Getting Remote Assist failed")
module.exit_json(changed=changed, ra_info=ra_facts)
def disable_ra(module, array):
"""Disable Remote Assist"""
changed = False
if array.get_remote_assist_status()["status"] in ["connected", "enabled"]:
changed = True
if not module.check_mode:
try:
array.disable_remote_assist()
except Exception:
module.fail_json(msg="Disabling Remote Assist failed")
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
state=dict(type="str", default="enable", choices=["enable", "disable"]),
)
)
module = AnsibleModule(argument_spec, supports_check_mode=True)
array = get_system(module)
if module.params["state"] == "enable":
enable_ra(module, array)
else:
disable_ra(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,340 @@
#!/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: purefa_saml
version_added: '1.12.0'
short_description: Manage FlashArray SAML2 service and identity providers
description:
- Enable or disable FlashArray SAML2 providers
author:
- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
options:
name:
description:
- Name of the SAML2 identity provider (IdP)
type: str
required: true
state:
description:
- Define whether the API client should exist or not.
default: present
choices: [ absent, present ]
type: str
url:
description:
- The URL of the identity provider
type: str
array_url:
description:
- The URL of the FlashArray
type: str
metadata_url:
description:
- The URL of the identity provider metadata
type: str
enabled:
description:
- Defines the enabled state of the identity provider
default: false
type: bool
encrypt_asserts:
description:
- If set to true, SAML assertions will be encrypted by the identity provider
default: false
type: bool
sign_request:
description:
- If set to true, SAML requests will be signed by the service provider.
default: false
type: bool
x509_cert:
description:
- The X509 certificate that the service provider uses to verify the SAML
response signature from the identity provider
type: str
decryption_credential:
description:
- The credential used by the service provider to decrypt encrypted SAML assertions from the identity provider
type: str
signing_credential:
description:
- The credential used by the service provider to sign SAML requests
type: str
extends_documentation_fragment:
- purestorage.flasharray.purestorage.fa
"""
EXAMPLES = r"""
- name: Create (disabled) SAML2 SSO with only metadata URL
purestorage.flasharray.purefa_saml:
name: myIDP
array_url: "https://10.10.10.2"
metadata_url: "https://myidp.acme.com/adfs/ls"
x509_cert: "{{lookup('file', 'x509_cert_file') }}"
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Enable SAML2 SSO
purestorage.flasharray.purefa_saml:
name: myISO
enabled: true
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
- name: Delete SAML2 SSO
purestorage.flasharray.purefa_saml:
state: absent
name: myIDP
fa_url: 10.10.10.2
api_token: e31060a7-21fc-e277-6240-25983c6c4592
"""
RETURN = r"""
"""
HAS_PURESTORAGE = True
try:
from pypureclient.flasharray import (
Saml2Sso,
Saml2SsoPost,
Saml2SsoSp,
Saml2SsoIdp,
ReferenceNoId,
)
except ImportError:
HAS_PURESTORAGE = False
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import (
get_system,
get_array,
purefa_argument_spec,
)
MIN_REQUIRED_API_VERSION = "2.11"
def delete_saml(module, array):
"""Delete SSO SAML2 IdP"""
changed = True
if not module.check_mode:
try:
array.delete_sso_saml2_idps(names=[module.params["name"]])
except Exception:
module.fail_json(
msg="Failed to delete SAML2 IdP {0}".format(module.params["name"])
)
module.exit_json(changed=changed)
def update_saml(module, array):
"""Update SSO SAML2 IdP"""
changed = False
current_idp = list(array.get_sso_saml2_idps(names=[module.params["name"]]).items)[0]
old_idp = {
"array_url": current_idp.array_url,
"enabled": current_idp.enabled,
"sp_sign_cred": getattr(current_idp.sp.signing_credential, "name", None),
"sp_decrypt_cred": getattr(current_idp.sp.decryption_credential, "name", None),
"id_metadata": current_idp.idp.metadata_url,
"id_url": getattr(current_idp.idp, "url", None),
"id_sign_enabled": current_idp.idp.sign_request_enabled,
"id_encrypt_enabled": current_idp.idp.encrypt_assertion_enabled,
"id_cert": current_idp.idp.verification_certificate,
}
if module.params["url"]:
new_url = module.params["url"]
else:
new_url = old_idp["id_url"]
if module.params["array_url"]:
new_array_url = module.params["array_url"]
else:
new_array_url = old_idp["array_url"]
if module.params["enabled"] != old_idp["enabled"]:
new_enabled = module.params["enabled"]
else:
new_enabled = old_idp["enabled"]
if module.params["sign_request"] != old_idp["id_sign_enabled"]:
new_sign = module.params["sign_request"]
else:
new_sign = old_idp["id_sign_enabled"]
if module.params["encrypt_asserts"] != old_idp["id_encrypt_enabled"]:
new_encrypt = module.params["encrypt_asserts"]
else:
new_encrypt = old_idp["id_encrypt_enabled"]
if module.params["signing_credential"]:
new_sign_cred = module.params["signing_credential"]
else:
new_sign_cred = old_idp["sp_sign_cred"]
if module.params["decryption_credential"]:
new_decrypt_cred = module.params["decryption_credential"]
else:
new_decrypt_cred = old_idp["sp_decrypt_cred"]
if module.params["metadata_url"]:
new_meta_url = module.params["metadata_url"]
else:
new_meta_url = old_idp["id_metadata"]
if module.params["x509_cert"]:
new_cert = module.params["x509_cert"]
else:
new_cert = old_idp["id_cert"]
new_idp = {
"array_url": new_array_url,
"enabled": new_enabled,
"sp_sign_cred": new_sign_cred,
"sp_decrypt_cred": new_decrypt_cred,
"id_metadata": new_meta_url,
"id_sign_enabled": new_sign,
"id_encrypt_enabled": new_encrypt,
"id_url": new_url,
"id_cert": new_cert,
}
if old_idp != new_idp:
changed = True
if not module.check_mode:
sp = Saml2SsoSp(
decryption_credential=ReferenceNoId(name=new_idp["sp_decrypt_cred"]),
signing_credential=ReferenceNoId(name=new_idp["sp_sign_cred"]),
)
idp = Saml2SsoIdp(
url=new_idp["id_url"],
metadata_url=new_idp["id_metadata"],
sign_request_enabled=new_idp["id_sign_enabled"],
encrypt_assertion_enabled=new_idp["id_encrypt_enabled"],
verification_certificate=new_idp["id_cert"],
)
res = array.patch_sso_saml2_idps(
idp=Saml2Sso(
array_url=new_idp["array_url"],
idp=idp,
sp=sp,
enabled=new_idp["enabled"],
),
names=[module.params["name"]],
)
if res.status_code != 200:
module.fail_json(
msg="Failed to update SAML2 IdP {0}. Error: {1}".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def create_saml(module, array):
"""Create SAML2 IdP"""
changed = True
if not module.check_mode:
sp = Saml2SsoSp(
decryption_credential=ReferenceNoId(
name=module.params["decryption_credential"]
),
signing_credential=ReferenceNoId(name=module.params["signing_credential"]),
)
idp = Saml2SsoIdp(
url=module.params["url"],
metadata_url=module.params["metadata_url"],
sign_request_enabled=module.params["sign_request"],
encrypt_assertion_enabled=module.params["encrypt_asserts"],
verification_certificate=module.params["x509_cert"],
)
if not module.check_mode:
res = array.post_sso_saml2_idps(
idp=Saml2SsoPost(array_url=module.params["array_url"], idp=idp, sp=sp),
names=[module.params["name"]],
)
if res.status_code != 200:
module.fail_json(
msg="Failed to create SAML2 Identity Provider {0}. Error message: {1}".format(
module.params["name"], res.errors[0].message
)
)
if module.params["enabled"]:
res = array.patch_sso_saml2_idps(
idp=Saml2Sso(enabled=module.params["enabled"]),
names=[module.params["name"]],
)
if res.status_code != 200:
array.delete_sso_saml2_idps(names=[module.params["name"]])
module.fail_json(
msg="Failed to create SAML2 Identity Provider {0}. Error message: {1}".format(
module.params["name"], res.errors[0].message
)
)
module.exit_json(changed=changed)
def main():
argument_spec = purefa_argument_spec()
argument_spec.update(
dict(
state=dict(type="str", default="present", choices=["absent", "present"]),
name=dict(type="str", required=True),
url=dict(type="str"),
array_url=dict(type="str"),
metadata_url=dict(type="str"),
x509_cert=dict(type="str", no_log=True),
signing_credential=dict(type="str"),
decryption_credential=dict(type="str"),
enabled=dict(type="bool", default=False),
encrypt_asserts=dict(type="bool", default=False),
sign_request=dict(type="bool", default=False),
)
)
required_if = [
["encrypt_asserts", True, ["decryption_credential"]],
["sign_request", True, ["signing_credential"]],
]
module = AnsibleModule(
argument_spec, supports_check_mode=True, required_if=required_if
)
if not HAS_PURESTORAGE:
module.fail_json(msg="py-pure-client sdk is required for this module")
array = get_system(module)
api_version = array._list_available_rest_versions()
if MIN_REQUIRED_API_VERSION not in api_version:
module.fail_json(
msg="FlashArray REST version not supported. "
"Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION)
)
array = get_array(module)
state = module.params["state"]
try:
list(array.get_sso_saml2_idps(names=[module.params["name"]]).items)[0]
exists = True
except AttributeError:
exists = False
if not exists and state == "present":
create_saml(module, array)
elif exists and state == "present":
update_saml(module, array)
elif exists and state == "absent":
delete_saml(module, array)
module.exit_json(changed=False)
if __name__ == "__main__":
main()

Some files were not shown because too many files have changed in this diff Show More