diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:22 +0000 |
commit | 38b7c80217c4e72b1d8988eb1e60bb6e77334114 (patch) | |
tree | 356e9fd3762877d07cde52d21e77070aeff7e789 /ansible_collections/purestorage/flasharray/plugins | |
parent | Adding upstream version 7.7.0+dfsg. (diff) | |
download | ansible-38b7c80217c4e72b1d8988eb1e60bb6e77334114.tar.xz ansible-38b7c80217c4e72b1d8988eb1e60bb6e77334114.zip |
Adding upstream version 9.4.0+dfsg.upstream/9.4.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/purestorage/flasharray/plugins')
51 files changed, 4030 insertions, 1493 deletions
diff --git a/ansible_collections/purestorage/flasharray/plugins/doc_fragments/purestorage.py b/ansible_collections/purestorage/flasharray/plugins/doc_fragments/purestorage.py index 7c19925e6..e05cbf6a7 100644 --- a/ansible_collections/purestorage/flasharray/plugins/doc_fragments/purestorage.py +++ b/ansible_collections/purestorage/flasharray/plugins/doc_fragments/purestorage.py @@ -42,5 +42,4 @@ requirements: - netaddr - requests - pycountry - - packaging """ diff --git a/ansible_collections/purestorage/flasharray/plugins/module_utils/common.py b/ansible_collections/purestorage/flasharray/plugins/module_utils/common.py new file mode 100644 index 000000000..ddc093731 --- /dev/null +++ b/ansible_collections/purestorage/flasharray/plugins/module_utils/common.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 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 + +""" +This module adds shared functions for the FlashArray modules +""" + + +def human_to_bytes(size): + """Given a human-readable byte string (e.g. 2G, 30M), + return the number of bytes. Will return 0 if the argument has + unexpected form. + """ + bytes = size[:-1] + unit = size[-1].upper() + if bytes.isdigit(): + bytes = int(bytes) + if unit == "P": + bytes *= 1125899906842624 + elif unit == "T": + bytes *= 1099511627776 + elif unit == "G": + bytes *= 1073741824 + elif unit == "M": + bytes *= 1048576 + elif unit == "K": + bytes *= 1024 + else: + bytes = 0 + else: + bytes = 0 + return bytes + + +def human_to_real(iops): + """Given a human-readable string (e.g. 2K, 30M IOPs), + return the real number. Will return 0 if the argument has + unexpected form. + """ + digit = iops[:-1] + unit = iops[-1].upper() + if unit.isdigit(): + digit = iops + elif digit.isdigit(): + digit = int(digit) + if unit == "M": + digit *= 1000000 + elif unit == "K": + digit *= 1000 + else: + digit = 0 + else: + digit = 0 + return digit + + +def convert_to_millisecs(hour): + """Convert a 12-hour clock to milliseconds from + midnight""" + if hour[-2:].upper() == "AM" and hour[:2] == "12": + return 0 + elif hour[-2:].upper() == "AM": + return int(hour[:-2]) * 3600000 + elif hour[-2:].upper() == "PM" and hour[:2] == "12": + return 43200000 + return (int(hour[:-2]) + 12) * 3600000 + + +def convert_time_to_millisecs(time): + """Convert a time period in milliseconds""" + if time[-1:].lower() not in ["w", "d", "h", "m", "s"]: + return 0 + try: + if time[-1:].lower() == "w": + return int(time[:-1]) * 7 * 86400000 + elif time[-1:].lower() == "d": + return int(time[:-1]) * 86400000 + elif time[-1:].lower() == "h": + return int(time[:-1]) * 3600000 + elif time[-1:].lower() == "m": + return int(time[:-1]) * 60000 + except Exception: + return 0 diff --git a/ansible_collections/purestorage/flasharray/plugins/module_utils/purefa.py b/ansible_collections/purestorage/flasharray/plugins/module_utils/purefa.py index b85ce0e29..82d048bcb 100644 --- a/ansible_collections/purestorage/flasharray/plugins/module_utils/purefa.py +++ b/ansible_collections/purestorage/flasharray/plugins/module_utils/purefa.py @@ -32,6 +32,12 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type +HAS_DISTRO = True +try: + import distro +except ImportError: + HAS_DISTRO = False + HAS_PURESTORAGE = True try: from purestorage import purestorage @@ -47,18 +53,26 @@ except ImportError: from os import environ import platform -VERSION = 1.4 +VERSION = 1.5 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(), - } + if HAS_DISTRO: + user_agent = "%(base)s %(class)s/%(version)s (%(platform)s)" % { + "base": USER_AGENT_BASE, + "class": __name__, + "version": VERSION, + "platform": distro.name(pretty=True), + } + else: + 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: @@ -91,12 +105,20 @@ def get_system(module): 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(), - } + if HAS_DISTRO: + user_agent = "%(base)s %(class)s/%(version)s (%(platform)s)" % { + "base": USER_AGENT_BASE, + "class": __name__, + "version": VERSION, + "platform": distro.name(pretty=True), + } + else: + 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: diff --git a/ansible_collections/purestorage/flasharray/plugins/module_utils/version.py b/ansible_collections/purestorage/flasharray/plugins/module_utils/version.py new file mode 100644 index 000000000..d91cf3ab4 --- /dev/null +++ b/ansible_collections/purestorage/flasharray/plugins/module_utils/version.py @@ -0,0 +1,344 @@ +# Vendored copy of distutils/version.py from CPython 3.9.5 +# +# Implements multiple version numbering conventions for the +# Python Module Distribution Utilities. +# +# PSF License (see PSF-license.txt or https://opensource.org/licenses/Python-2.0) +# + +"""Provides classes to represent module version numbers (one class for +each style of version numbering). There are currently two such classes +implemented: StrictVersion and LooseVersion. + +Every version number class implements the following interface: + * the 'parse' method takes a string and parses it to some internal + representation; if the string is an invalid version number, + 'parse' raises a ValueError exception + * the class constructor takes an optional string argument which, + if supplied, is passed to 'parse' + * __str__ reconstructs the string that was passed to 'parse' (or + an equivalent string -- ie. one that will generate an equivalent + version number instance) + * __repr__ generates Python code to recreate the version number instance + * _cmp compares the current instance with either another instance + of the same class or a string (which will be parsed to an instance + of the same class, thus must follow the same rules) +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import re + +try: + RE_FLAGS = re.VERBOSE | re.ASCII +except AttributeError: + RE_FLAGS = re.VERBOSE + + +class Version: + """Abstract base class for version numbering classes. Just provides + constructor (__init__) and reproducer (__repr__), because those + seem to be the same for all version numbering classes; and route + rich comparisons to _cmp. + """ + + def __init__(self, vstring=None): + if vstring: + self.parse(vstring) + + def __repr__(self): + return "%s ('%s')" % (self.__class__.__name__, str(self)) + + def __eq__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c == 0 + + def __lt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c < 0 + + def __le__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c <= 0 + + def __gt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c > 0 + + def __ge__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c >= 0 + + +# Interface for version-number classes -- must be implemented +# by the following classes (the concrete ones -- Version should +# be treated as an abstract class). +# __init__ (string) - create and take same action as 'parse' +# (string parameter is optional) +# parse (string) - convert a string representation to whatever +# internal representation is appropriate for +# this style of version numbering +# __str__ (self) - convert back to a string; should be very similar +# (if not identical to) the string supplied to parse +# __repr__ (self) - generate Python code to recreate +# the instance +# _cmp (self, other) - compare two version numbers ('other' may +# be an unparsed version string, or another +# instance of your version class) + + +class StrictVersion(Version): + """Version numbering for anal retentives and software idealists. + Implements the standard interface for version number classes as + described above. A version number consists of two or three + dot-separated numeric components, with an optional "pre-release" tag + on the end. The pre-release tag consists of the letter 'a' or 'b' + followed by a number. If the numeric components of two version + numbers are equal, then one with a pre-release tag will always + be deemed earlier (lesser) than one without. + + The following are valid version numbers (shown in the order that + would be obtained by sorting according to the supplied cmp function): + + 0.4 0.4.0 (these two are equivalent) + 0.4.1 + 0.5a1 + 0.5b3 + 0.5 + 0.9.6 + 1.0 + 1.0.4a3 + 1.0.4b1 + 1.0.4 + + The following are examples of invalid version numbers: + + 1 + 2.7.2.2 + 1.3.a4 + 1.3pl1 + 1.3c4 + + The rationale for this version numbering system will be explained + in the distutils documentation. + """ + + version_re = re.compile(r"^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$", RE_FLAGS) + + def parse(self, vstring): + match = self.version_re.match(vstring) + if not match: + raise ValueError("invalid version number '%s'" % vstring) + + (major, minor, patch, prerelease, prerelease_num) = match.group(1, 2, 4, 5, 6) + + if patch: + self.version = tuple(map(int, [major, minor, patch])) + else: + self.version = tuple(map(int, [major, minor])) + (0,) + + if prerelease: + self.prerelease = (prerelease[0], int(prerelease_num)) + else: + self.prerelease = None + + def __str__(self): + if self.version[2] == 0: + vstring = ".".join(map(str, self.version[0:2])) + else: + vstring = ".".join(map(str, self.version)) + + if self.prerelease: + vstring = vstring + self.prerelease[0] + str(self.prerelease[1]) + + return vstring + + def _cmp(self, other): + if isinstance(other, str): + other = StrictVersion(other) + elif not isinstance(other, StrictVersion): + return NotImplemented + + if self.version != other.version: + # numeric versions don't match + # prerelease stuff doesn't matter + if self.version < other.version: + return -1 + else: + return 1 + + # have to compare prerelease + # case 1: neither has prerelease; they're equal + # case 2: self has prerelease, other doesn't; other is greater + # case 3: self doesn't have prerelease, other does: self is greater + # case 4: both have prerelease: must compare them! + + if not self.prerelease and not other.prerelease: + return 0 + elif self.prerelease and not other.prerelease: + return -1 + elif not self.prerelease and other.prerelease: + return 1 + elif self.prerelease and other.prerelease: + if self.prerelease == other.prerelease: + return 0 + elif self.prerelease < other.prerelease: + return -1 + else: + return 1 + else: + raise AssertionError("never get here") + + +# end class StrictVersion + +# The rules according to Greg Stein: +# 1) a version number has 1 or more numbers separated by a period or by +# sequences of letters. If only periods, then these are compared +# left-to-right to determine an ordering. +# 2) sequences of letters are part of the tuple for comparison and are +# compared lexicographically +# 3) recognize the numeric components may have leading zeroes +# +# The LooseVersion class below implements these rules: a version number +# string is split up into a tuple of integer and string components, and +# comparison is a simple tuple comparison. This means that version +# numbers behave in a predictable and obvious way, but a way that might +# not necessarily be how people *want* version numbers to behave. There +# wouldn't be a problem if people could stick to purely numeric version +# numbers: just split on period and compare the numbers as tuples. +# However, people insist on putting letters into their version numbers; +# the most common purpose seems to be: +# - indicating a "pre-release" version +# ('alpha', 'beta', 'a', 'b', 'pre', 'p') +# - indicating a post-release patch ('p', 'pl', 'patch') +# but of course this can't cover all version number schemes, and there's +# no way to know what a programmer means without asking him. +# +# The problem is what to do with letters (and other non-numeric +# characters) in a version number. The current implementation does the +# obvious and predictable thing: keep them as strings and compare +# lexically within a tuple comparison. This has the desired effect if +# an appended letter sequence implies something "post-release": +# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002". +# +# However, if letters in a version number imply a pre-release version, +# the "obvious" thing isn't correct. Eg. you would expect that +# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison +# implemented here, this just isn't so. +# +# Two possible solutions come to mind. The first is to tie the +# comparison algorithm to a particular set of semantic rules, as has +# been done in the StrictVersion class above. This works great as long +# as everyone can go along with bondage and discipline. Hopefully a +# (large) subset of Python module programmers will agree that the +# particular flavour of bondage and discipline provided by StrictVersion +# provides enough benefit to be worth using, and will submit their +# version numbering scheme to its domination. The free-thinking +# anarchists in the lot will never give in, though, and something needs +# to be done to accommodate them. +# +# Perhaps a "moderately strict" version class could be implemented that +# lets almost anything slide (syntactically), and makes some heuristic +# assumptions about non-digits in version number strings. This could +# sink into special-case-hell, though; if I was as talented and +# idiosyncratic as Larry Wall, I'd go ahead and implement a class that +# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is +# just as happy dealing with things like "2g6" and "1.13++". I don't +# think I'm smart enough to do it right though. +# +# In any case, I've coded the test suite for this module (see +# ../test/test_version.py) specifically to fail on things like comparing +# "1.2a2" and "1.2". That's not because the *code* is doing anything +# wrong, it's because the simple, obvious design doesn't match my +# complicated, hairy expectations for real-world version numbers. It +# would be a snap to fix the test suite to say, "Yep, LooseVersion does +# the Right Thing" (ie. the code matches the conception). But I'd rather +# have a conception that matches common notions about version numbers. + + +class LooseVersion(Version): + """Version numbering for anarchists and software realists. + Implements the standard interface for version number classes as + described above. A version number consists of a series of numbers, + separated by either periods or strings of letters. When comparing + version numbers, the numeric components will be compared + numerically, and the alphabetic components lexically. The following + are all valid version numbers, in no particular order: + + 1.5.1 + 1.5.2b2 + 161 + 3.10a + 8.02 + 3.4j + 1996.07.12 + 3.2.pl0 + 3.1.1.6 + 2g6 + 11g + 0.960923 + 2.2beta29 + 1.13++ + 5.5.kw + 2.0b1pl0 + + In fact, there is no such thing as an invalid version number under + this scheme; the rules for comparison are simple and predictable, + but may not always give the results you want (for some definition + of "want"). + """ + + component_re = re.compile(r"(\d+ | [a-z]+ | \.)", re.VERBOSE) + + def __init__(self, vstring=None): + if vstring: + self.parse(vstring) + + def parse(self, vstring): + # I've given up on thinking I can reconstruct the version string + # from the parsed tuple -- so I just store the string here for + # use by __str__ + self.vstring = vstring + components = [x for x in self.component_re.split(vstring) if x and x != "."] + for i, obj in enumerate(components): + try: + components[i] = int(obj) + except ValueError: + pass + + self.version = components + + def __str__(self): + return self.vstring + + def __repr__(self): + return "LooseVersion ('%s')" % str(self) + + def _cmp(self, other): + if isinstance(other, str): + other = LooseVersion(other) + elif not isinstance(other, LooseVersion): + return NotImplemented + + if self.version == other.version: + return 0 + if self.version < other.version: + return -1 + if self.version > other.version: + return 1 + + +# end class LooseVersion diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_ad.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_ad.py index d9eee96ac..35530bdf8 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_ad.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_ad.py @@ -152,8 +152,10 @@ except ImportError: HAS_PURESTORAGE = False from ansible.module_utils.basic import AnsibleModule +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, get_array, purefa_argument_spec, ) @@ -205,7 +207,7 @@ def update_account(module, array): def create_account(module, array, api_version): """Create Active Directory Account""" changed = True - if MIN_JOIN_OU_API_VERSION not in api_version: + if LooseVersion(MIN_JOIN_OU_API_VERSION) > LooseVersion(api_version): ad_config = ActiveDirectoryPost( computer_name=module.params["computer"], directory_servers=module.params["directory_servers"], @@ -214,7 +216,7 @@ def create_account(module, array, api_version): user=module.params["username"], password=module.params["password"], ) - elif MIN_TLS_API_VERSION in api_version: + elif LooseVersion(MIN_TLS_API_VERSION) <= LooseVersion(api_version): ad_config = ActiveDirectoryPost( computer_name=module.params["computer"], directory_servers=module.params["directory_servers"], @@ -284,15 +286,14 @@ def main(): 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: + array = get_array(module) + api_version = array.get_rest_version() + if LooseVersion(MIN_REQUIRED_API_VERSION) > LooseVersion(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 ) @@ -300,18 +301,22 @@ def main(): if not module.params["computer"]: module.params["computer"] = module.params["name"].replace("_", "-") if module.params["kerberos_servers"]: - if SERVER_API_VERSION in api_version: + if LooseVersion(SERVER_API_VERSION) <= LooseVersion(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: + if LooseVersion(SERVER_API_VERSION) <= LooseVersion(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: + elif ( + exists + and state == "present" + and LooseVersion(MIN_TLS_API_VERSION) <= LooseVersion(api_version) + ): update_account(module, array) elif exists and state == "absent": delete_account(module, array) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_admin.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_admin.py index becb86893..21eac3896 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_admin.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_admin.py @@ -70,10 +70,12 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, get_array, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) MIN_API_VERSION = "2.2" @@ -95,11 +97,10 @@ def main(): 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() + array = get_array(module) + api_version = array.get_rest_version() changed = False - if MIN_API_VERSION in api_version: - array = get_array(module) + if LooseVersion(MIN_API_VERSION) <= LooseVersion(api_version): current_settings = list(array.get_admins_settings().items)[0] if ( module.params["sso"] diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_apiclient.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_apiclient.py index 12970dddb..9334c6733 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_apiclient.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_apiclient.py @@ -109,10 +109,12 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, get_array, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) MIN_REQUIRED_API_VERSION = "2.1" @@ -219,15 +221,14 @@ def main(): 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() + array = get_array(module) + api_version = array.get_rest_version() - if MIN_REQUIRED_API_VERSION not in api_version: + if LooseVersion(MIN_REQUIRED_API_VERSION) > LooseVersion(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: diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_arrayname.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_arrayname.py index cf5202c6f..550ae1401 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_arrayname.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_arrayname.py @@ -52,10 +52,17 @@ EXAMPLES = r""" RETURN = r""" """ +HAS_PURESTORAGE = True +try: + from pypureclient.flasharray import Arrays +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, ) @@ -64,11 +71,12 @@ def update_name(module, array): """Change aray name""" changed = True if not module.check_mode: - try: - array.set(name=module.params["name"]) - except Exception: + res = array.patch_arrays(array=Arrays(name=module.params["name"])) + if res.status_code != 200: module.fail_json( - msg="Failed to change array name to {0}".format(module.params["name"]) + msg="Failed to change array name to {0}. Error: {1}".format( + module.params["name"], res.errors[0].message + ) ) module.exit_json(changed=changed) @@ -85,7 +93,10 @@ def main(): module = AnsibleModule(argument_spec, supports_check_mode=True) - array = get_system(module) + if not HAS_PURESTORAGE: + module.fail_json(msg="py-pure-client sdk is required for this module") + + array = get_array(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( @@ -93,7 +104,7 @@ def main(): module.params["name"] ) ) - if module.params["name"] != array.get()["array_name"]: + if module.params["name"] != list(array.get_arrays().items)[0].name: update_name(module, array) module.exit_json(changed=False) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_banner.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_banner.py index bd7a367a5..c3c2346b3 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_banner.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_banner.py @@ -61,10 +61,16 @@ RETURN = r""" 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.flasharray import Arrays +except ImportError: + HAS_PURESTORAGE = False + def set_banner(module, array): """Set MOTD banner text""" @@ -72,9 +78,8 @@ def set_banner(module, array): 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: + res = array.patch_arrays(array=Arrays(banner=module.params["banner"])) + if res.status_code != 200: module.fail_json(msg="Failed to set MOTD banner text") module.exit_json(changed=changed) @@ -84,9 +89,8 @@ def delete_banner(module, array): """Delete MOTD banner text""" changed = True if not module.check_mode: - try: - array.set(banner="") - except Exception: + res = array.patch_arrays(array=Arrays(banner="")) + if res.status_code != 200: module.fail_json(msg="Failed to delete current MOTD banner text") module.exit_json(changed=changed) @@ -106,9 +110,12 @@ def main(): argument_spec, required_if=required_if, supports_check_mode=True ) + if not HAS_PURESTORAGE: + module.fail_json(msg="py-pure-client sdk is required for this module") + state = module.params["state"] - array = get_system(module) - current_banner = array.get(banner=True)["banner"] + array = get_array(module) + current_banner = list(array.get_arrays().items)[0].banner # set banner if empty value or value differs if state == "present" and ( not current_banner or current_banner != module.params["banner"] diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_cbsexpand.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_cbsexpand.py new file mode 100644 index 000000000..4221e9013 --- /dev/null +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_cbsexpand.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2023, Simon Dodsley (simon@purestorage.com) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} + +DOCUMENTATION = r""" +--- +module: purefa_cbsexpand +version_added: '1.0.0' +short_description: Modify the CBS array capacity +description: +- Expand the CBS array capacity. Capacity can only be updated to specific values. +author: +- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com> +options: + state: + description: + - Action to be performed on the CBS array. + - I{list) will provide the options that I(capacity), in bytes, can be set to. + default: show + choices: [ show, expand ] + type: str + capacity: + description: + - Requested capacity of CBS array in bytes. + type: int +extends_documentation_fragment: +- purestorage.flasharray.purestorage.fa +""" + +EXAMPLES = r""" +- name: Show available expansion capacities + purestorage.flasharray.purefa_cbsexpand: + fa_url: 10.10.10.2 + api_token: e31060a7-21fc-e277-6240-25983c6c4592 + +- name: Expand CBS to new capacity + purestorage.flasharray.purefa_cbsexpand: + state: expand + capacity: 10995116277760 + 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.version import ( + LooseVersion, +) +from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( + get_array, + purefa_argument_spec, +) + + +EXPAND_API_VERSION = "2.29" + + +def _is_cbs(array): + """Is the selected array a Cloud Block Store""" + model = list(array.get_hardware(filter="type='controller'").items)[0].model + is_cbs = bool("CBS" in model) + return is_cbs + + +def list_capacity(module, array): + """Get avaible expansion points""" + steps = list(array.get_arrays_cloud_capacity_supported_steps().items) + available = [] + for step in range(0, len(steps)): + available.append(steps[step].supported_capacity) + module.exit_json(changed=True, available=available) + + +def update_capacity(module, array): + """Expand CBS capacity""" + steps = list(array.get_arrays_cloud_capacity_supported_steps().items) + available = [] + for step in range(0, len(steps)): + available.append(steps[step].supported_capacity) + if module.params["capacity"] not in available: + module.fail_json( + msg="Selected capacity is not available. " + "Run this module with `list` to get available capapcity points." + ) + expanded = array.patch_arrays_cloud_capacity( + capacity=flasharray.CloudCapacityStatus( + requested_capacity=module.params["capacity"] + ) + ) + if expanded.sttaus_code != 200: + module.fail_json( + msg="Expansion request failed. Error: {0}".format( + expanded.errors[0].message + ) + ) + + +def main(): + argument_spec = purefa_argument_spec() + argument_spec.update( + dict( + state=dict(type="str", default="show", choices=["show", "expand"]), + capacity=dict(type="int"), + ) + ) + + required_if = [["state", "expand", ["capacity"]]] + module = AnsibleModule( + argument_spec, required_if=required_if, supports_check_mode=True + ) + + array = get_array(module) + if not HAS_PURESTORAGE: + module.fail_json( + msg="py-pure-client sdk is required to support 'count' parameter" + ) + if not _is_cbs(array): + module.fail_json(msg="Module only valid on Cloud Block Store array") + api_version = array.get_rest_version() + if LooseVersion(EXPAND_API_VERSION) > LooseVersion(api_version): + module.fail_json( + msg="FlashArray REST version not supported. " + "Minimum version required: {0}".format(EXPAND_API_VERSION) + ) + if module.params["state"] == "show": + list_capacity(module, array) + else: + update_capacity(module, array) + module.exit_json(changed=False) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_certs.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_certs.py index 33ffb60cc..bcc602610 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_certs.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_certs.py @@ -151,6 +151,13 @@ EXAMPLES = r""" fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 +- name: Request CSR with updated fields + purestorage.flasharray.purefa_certs: + state: sign + org_unit: Development + 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 @@ -187,81 +194,95 @@ except ImportError: 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, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) MIN_REQUIRED_API_VERSION = "2.4" def update_cert(module, array): """Update existing SSL Certificate""" - changed = True + changed = False 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"], + new_cert = current_cert + if module.params["common_name"] and module.params["common_name"] != getattr( + current_cert, "common_name", None + ): + new_cert.common_name = module.params["common_name"] + else: + new_cert.common_name = getattr(current_cert, "common_name", None) + if module.params["country"] and module.params["country"] != getattr( + current_cert, "country", None + ): + new_cert.country = module.params["country"] + else: + new_cert.country = getattr(current_cert, "country") + if module.params["email"] and module.params["email"] != getattr( + current_cert, "email", None + ): + new_cert.email = module.params["email"] + else: + new_cert.email = getattr(current_cert, "email", None) + if module.params["key_size"] and module.params["key_size"] != getattr( + current_cert, "key_size", None + ): + new_cert.key_size = module.params["key_size"] + else: + new_cert.key_size = getattr(current_cert, "key_size", None) + if module.params["locality"] and module.params["locality"] != getattr( + current_cert, "locality", None + ): + new_cert.locality = module.params["locality"] + else: + new_cert.locality = getattr(current_cert, "locality", None) + if module.params["province"] and module.params["province"] != getattr( + current_cert, "state", None + ): + new_cert.state = module.params["province"] + else: + new_cert.state = getattr(current_cert, "state", None) + if module.params["organization"] and module.params["organization"] != getattr( + current_cert, "organization", None + ): + new_cert.organization = module.params["organization"] + else: + new_cert.organization = getattr(current_cert, "organization", None) + if module.params["org_unit"] and module.params["org_unit"] != getattr( + current_cert, "organizational_unit", None + ): + new_cert.organizational_unit = module.params["org_unit"] + else: + new_cert.organizational_unit = getattr( + current_cert, "organizational_unit", None ) - 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 - ) + if new_cert != current_cert: + changed = True + certificate = flasharray.CertificatePost( + common_name=new_cert.common_name, + country=getattr(new_cert, "country", None), + email=getattr(new_cert, "email", None), + key_size=getattr(new_cert, "key_size", None), + locality=getattr(new_cert, "locality", None), + organization=getattr(new_cert, "organization", None), + organizational_unit=getattr(new_cert, "organizational_unit", None), + state=getattr(new_cert, "state", None), + ) + 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) @@ -312,12 +333,11 @@ def delete_cert(module, array): 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"], + key_size=module.params["key_size"], passphrase=module.params["passphrase"], status="imported", ) @@ -364,50 +384,64 @@ def create_csr(module, array): 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 + if module.params["common_name"] and module.params["common_name"] != getattr( + current_attr, "common_name", None + ): + current_attr.common_name = module.params["common_name"] except AttributeError: pass try: - if module.params["country"] != current_attr.country: - module.params["country"] = current_attr.country + if module.params["country"] and module.params["country"] != getattr( + current_attr, "country", None + ): + current_attr.country = module.params["country"] except AttributeError: pass try: - if module.params["email"] != current_attr.email: - module.params["email"] = current_attr.email + if module.params["email"] and module.params["email"] != getattr( + current_attr, "email", None + ): + current_attr.email = module.params["email"] except AttributeError: pass try: - if module.params["locality"] != current_attr.locality: - module.params["locality"] = current_attr.locality + if module.params["locality"] and module.params["locality"] != getattr( + current_attr, "locality", None + ): + current_attr.locality = module.params["locality"] except AttributeError: pass try: - if module.params["province"] != current_attr.state: - module.params["province"] = current_attr.state + if module.params["province"] and module.params["province"] != getattr( + current_attr, "state", None + ): + current_attr.state = module.params["province"] except AttributeError: pass try: - if module.params["organization"] != current_attr.organization: - module.params["organization"] = current_attr.organization + if module.params["organization"] and module.params["organization"] != getattr( + current_attr, "organization", None + ): + current_attr.organization = module.params["organization"] except AttributeError: pass try: - if module.params["org_unit"] != current_attr.organization_unit: - module.params["org_unit"] = current_attr.organization_unit + if module.params["org_unit"] and module.params["org_unit"] != getattr( + current_attr, "organizational_unit", None + ): + current_attr.organizational_unit = module.params["org_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"], + certificate={"name": module.params["name"]}, + common_name=getattr(current_attr, "common_name", None), + country=getattr(current_attr, "country", None), + email=getattr(current_attr, "email", None), + locality=getattr(current_attr, "locality", None), + state=getattr(current_attr, "state", None), + organization=getattr(current_attr, "organization", None), + organizational_unit=getattr(current_attr, "organizational_unit", None), ) csr = list( array.post_certificates_certificate_signing_requests( @@ -452,6 +486,7 @@ def main(): required_if = [ ["state", "import", ["certificate"]], ["state", "export", ["export_file"]], + ["state", "sign", ["export_file"]], ] module = AnsibleModule( @@ -468,16 +503,15 @@ def main(): 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() + array = get_array(module) + api_version = array.get_rest_version() - if MIN_REQUIRED_API_VERSION not in api_version: + if LooseVersion(MIN_REQUIRED_API_VERSION) > LooseVersion(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( @@ -493,7 +527,7 @@ def main(): ) ) state = module.params["state"] - if state in ["present", "sign"]: + if state in ["present"]: 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] diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_connect.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_connect.py index 3148ea482..d4d65c20a 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_connect.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_connect.py @@ -56,13 +56,20 @@ extends_documentation_fragment: """ EXAMPLES = r""" -- name: Create an async connection to remote array +- name: Create an IPv4 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: Create an IPv6 async connection to remote array + purestorage.flasharray.purefa_connect: + target_url: "[2001:db8:abcd:12::10]" + target_api: 9c0b56bc-f941-f7a6-9f85-dcc3e9a8f7d6 + connection: async + fa_url: "[2001:db8:abcd:12::13]" + api_token: e31060a7-21fc-e277-6240-25983c6c4592 - name: Delete connection to remote array purestorage.flasharray.purefa_connect: state: absent @@ -87,14 +94,19 @@ try: except ImportError: HAS_PYPURECLIENT = False -import platform +HAS_DISTRO = True +try: + import distro +except ImportError: + HAS_DISTRO = 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, ) - +import platform P53_API_VERSION = "1.17" FC_REPL_VERSION = "2.4" @@ -107,14 +119,14 @@ def _check_connected(module, array): if P53_API_VERSION in api_version: if ( connected_arrays[target]["management_address"] - == module.params["target_url"] + == module.params["target_url"].strip("[]") and "connected" in connected_arrays[target]["status"] ): return connected_arrays[target] else: if ( connected_arrays[target]["management_address"] - == module.params["target_url"] + == module.params["target_url"].strip("[]") and connected_arrays[target]["connected"] ): return connected_arrays[target] @@ -145,12 +157,20 @@ 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(), - } + if HAS_DISTRO: + user_agent = "%(base)s %(class)s/%(version)s (%(platform)s)" % { + "base": "Ansible", + "class": __name__, + "version": 1.5, + "platform": distro.name(pretty=True), + } + else: + user_agent = "%(base)s %(class)s/%(version)s (%(platform)s)" % { + "base": "Ansible", + "class": __name__, + "version": 1.5, + "platform": platform.platform(), + } try: remote_system = FlashArray( module.params["target_url"], @@ -171,7 +191,7 @@ def create_connection(module, array): ) array_connection = flasharray.ArrayConnectionPost( type="sync-replication", - management_address=module.params["target_url"], + management_address=module.params["target_url"].strip("[]"), replication_transport="fc", connection_key=connection_key, ) @@ -187,7 +207,7 @@ def create_connection(module, array): else: if not module.check_mode: array.connect_array( - module.params["target_url"], + module.params["target_url"].strip("[]"), connection_key, [module.params["connection"]], ) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_default_protection.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_default_protection.py index 5038de423..cdb953c0c 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_default_protection.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_default_protection.py @@ -99,11 +99,12 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, get_array, purefa_argument_spec, ) - +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) DEFAULT_API_VERSION = "2.16" @@ -282,14 +283,13 @@ def main(): 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: + array = get_array(module) + api_version = array.get_rest_version() + if LooseVersion(DEFAULT_API_VERSION) > LooseVersion(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( diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_directory.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_directory.py index 125b84172..2cd769771 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_directory.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_directory.py @@ -90,10 +90,12 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, get_array, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) MIN_REQUIRED_API_VERSION = "2.2" @@ -190,14 +192,13 @@ def main(): 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: + array = get_array(module) + api_version = array.get_rest_version() + if LooseVersion(MIN_REQUIRED_API_VERSION) > LooseVersion(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: diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_dirsnap.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_dirsnap.py index 4c090bde8..1c1c11a18 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_dirsnap.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_dirsnap.py @@ -173,10 +173,12 @@ except ImportError: 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, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) MIN_REQUIRED_API_VERSION = "2.2" MIN_RENAME_API_VERSION = "2.10" @@ -413,19 +415,20 @@ def main(): ) ) - array = get_system(module) - api_version = array._list_available_rest_versions() - if MIN_REQUIRED_API_VERSION not in api_version: + array = get_array(module) + api_version = array.get_rest_version() + if LooseVersion(MIN_REQUIRED_API_VERSION) > LooseVersion(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: + if module.params["rename"] and LooseVersion(MIN_RENAME_API_VERSION) > LooseVersion( + 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( diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_dns.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_dns.py index 746a4ed52..7085a19f6 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_dns.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_dns.py @@ -207,12 +207,9 @@ def delete_multi_dns(module, array): """Delete a DNS configuration""" changed = True if module.params["name"] == "management": - res = array.update_dns( + res = array.patch_dns( names=[module.params["name"]], - dns=flasharray.DnsPatch( - domain=module.params["domain"], - nameservers=module.params["nameservers"], - ), + dns=flasharray.DnsPatch(domain="", nameservers=[]), ) if res.status_code != 200: module.fail_json( diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_ds.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_ds.py index 195aa2155..ce96b1afb 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_ds.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_ds.py @@ -139,6 +139,23 @@ options: 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. + check_peer: + type: bool + description: + - Whether or not server authenticity is enforced when a certificate + is provided + default: false + version_added: 1.24.0 + certificate: + type: str + description: + - The certificate of the Certificate Authority (CA) that signed the + certificates of the directory servers, which is used to validate the + authenticity of the configured servers + - A valid signed certicate in PEM format (Base64 encoded) + - Includes the "-----BEGIN CERTIFICATE-----" and "-----END CERTIFICATE-----" lines + - Does not exceed 3000 characters in length + version_added: 1.24.0 extends_documentation_fragment: - purestorage.flasharray.purestorage.fa """ @@ -209,6 +226,15 @@ EXAMPLES = r""" bind_password: password fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 + +- name: Upload CA certificate for management DNS and check peer + purestorage.flasharray.purefa_ds: + enable: true + dstype: management + certificate: "{{lookup('file', 'ca_cert.pem') }}" + check_peer: True + fa_url: 10.10.10.2 + api_token: e31060a7-21fc-e277-6240-25983c6c4592 """ RETURN = r""" @@ -409,33 +435,32 @@ def update_ds_v6(module, array): changed = False ds_change = False password_required = False - dirserv = list( - array.get_directory_services( - filter="name='" + module.params["dstype"] + "'" - ).items - )[0] - current_ds = dirserv + current_ds = [] + dirservlist = list(array.get_directory_services().items) + for dirs in range(0, len(dirservlist)): + if dirservlist[dirs].name == module.params["dstype"]: + current_ds = dirservlist[dirs] if module.params["uri"] and current_ds.uris is None: password_required = True - if current_ds.uris != module.params["uri"]: + if module.params["uri"] and 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 = getattr(current_ds, "base_dn", "") + bind_user = getattr(current_ds, "bind_user", "") + cert = getattr(current_ds, "ca_certificate", None) + 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["enable"] != current_ds.enabled: + ds_change = True + if getattr(current_ds, "bind_password", None) is None: + password_required = True + if module.params["bind_user"] is not None: if module.params["bind_user"] != bind_user: + bind_user = module.params["bind_user"] password_required = True ds_change = True elif module.params["force_bind_password"]: @@ -444,19 +469,27 @@ def update_ds_v6(module, array): 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["certificate"] is not None: + if cert is None and module.params["certificate"] != "": + cert = module.params["certificate"] + ds_change = True + elif cert is None and module.params["certificate"] == "": + pass + elif module.params["certificate"] != cert: + cert = module.params["certificate"] + ds_change = True + if module.params["check_peer"] and not cert: + module.warn( + "Cannot check_peer without a CA certificate. Disabling check_peer" + ) + module.params["check_peer"] = False + if module.params["check_peer"] != current_ds.check_peer: + ds_change = True + user_login = getattr(current_ds.management, "user_login_attribute", "") + user_object = getattr(current_ds.management, "user_object_class", "") if ( module.params["user_object"] is not None and user_object != module.params["user_object"] @@ -481,6 +514,8 @@ def update_ds_v6(module, array): enabled=module.params["enable"], services=module.params["dstype"], management=management, + check_peer=module.params["check_peer"], + ca_certificate=cert, ) else: directory_service = flasharray.DirectoryService( @@ -490,6 +525,8 @@ def update_ds_v6(module, array): enabled=module.params["enable"], services=module.params["dstype"], management=management, + check_peer=module.params["check_peer"], + ca_certificate=cert, ) else: if password_required: @@ -544,6 +581,8 @@ def main(): dstype=dict( type="str", default="management", choices=["management", "data"] ), + check_peer=dict(type="bool", default=False), + certificate=dict(type="str"), ) ) @@ -571,15 +610,17 @@ def main(): 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) + dirserv = [] + dirservlist = list(arrayv6.get_directory_services().items) + for dirs in range(0, len(dirservlist)): + if dirservlist[dirs].name == module.params["dstype"]: + dirserv = dirservlist[dirs] + if dirserv: + if state == "absent": + if dirserv.uris != []: + delete_ds_v6(module, arrayv6) + else: + update_ds_v6(module, arrayv6) else: dirserv = array.get_directory_service() ds_enabled = dirserv["enabled"] diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_eradication.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_eradication.py index ea7bd48bc..52cef0bae 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_eradication.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_eradication.py @@ -31,6 +31,24 @@ options: - Allowed values are integers from 1 to 30. Default is 1 default: 1 type: int + disabled_delay: + description: + - Configures the eradication delay + for destroyed objects that I(are) protected by SafeMode (objects for which + eradication is disabled) + - Allowed values are integers from 1 to 30. Default is 1 + default: 1 + type: int + version_added: "1.22.0" + enabled_delay: + description: + - Configures the eradication delay + for destroyed objects that I(are not) protected by SafeMode (objects for which + eradication is disabled) + - Allowed values are integers from 1 to 30. Default is 1 + default: 1 + type: int + version_added: "1.22.0" extends_documentation_fragment: - purestorage.flasharray.purestorage.fa """ @@ -60,39 +78,69 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, get_array, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) SEC_PER_DAY = 86400000 ERADICATION_API_VERSION = "2.6" +DELAY_API_VERSION = "2.26" def main(): argument_spec = purefa_argument_spec() argument_spec.update( dict( - timer=dict(type="int", default="1"), + timer=dict(type="int", default=1), + disabled_delay=dict(type="int", default=1), + enabled_delay=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 30 >= module.params["disabled_delay"] >= 1: + module.fail_json(msg="disabled_delay must be between 1 and 30 days.") + if not 30 >= module.params["enabled_delay"] >= 1: + module.fail_json(msg="enabled_delay 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() + array = get_array(module) + api_version = array.get_rest_version() changed = False - if ERADICATION_API_VERSION in api_version: + current_disabled = None + current_enabled = None + if LooseVersion(ERADICATION_API_VERSION) <= LooseVersion(api_version): array = get_array(module) - current_timer = ( - list(array.get_arrays().items)[0].eradication_config.eradication_delay - / SEC_PER_DAY + base_eradication_timer = getattr( + list(array.get_arrays().items)[0].eradication_config, + "eradication_delay", + None, ) - if module.params["timer"] != current_timer: + if base_eradication_timer: + current_eradication_timer = base_eradication_timer / SEC_PER_DAY + if ( + LooseVersion(DELAY_API_VERSION) <= LooseVersion(api_version) + and not base_eradication_timer + ): + current_disabled = ( + list(array.get_arrays().items)[0].eradication_config.disabled_delay + / SEC_PER_DAY + ) + current_enabled = ( + list(array.get_arrays().items)[0].eradication_config.enabled_delay + / SEC_PER_DAY + ) + + if ( + base_eradication_timer + and module.params["timer"] != current_eradication_timer + ): changed = True if not module.check_mode: new_timer = SEC_PER_DAY * module.params["timer"] @@ -106,6 +154,26 @@ def main(): res.errors[0].message ) ) + if current_disabled and ( + module.params["enabled_delay"] != current_enabled + or module.params["disabled_delay"] != current_disabled + ): + changed = True + if not module.check_mode: + new_disabled = SEC_PER_DAY * module.params["disabled_delay"] + new_enabled = SEC_PER_DAY * module.params["enabled_delay"] + eradication_config = EradicationConfig( + enabled_delay=new_enabled, disabled_delay=new_disabled + ) + res = array.patch_arrays( + array=Arrays(eradication_config=eradication_config) + ) + if res.status_code != 200: + module.fail_json( + msg="Failed to change Eradication Timers. Error: {0}".format( + res.errors[0].message + ) + ) else: module.fail_json( msg="Purity version does not support changing Eradication Timer" diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_eula.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_eula.py index 8d4d9536c..c810708b1 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_eula.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_eula.py @@ -29,19 +29,16 @@ options: - 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 """ @@ -61,36 +58,48 @@ RETURN = r""" from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, + get_array, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) + +HAS_PURESTORAGE = True +try: + from pypureclient.flasharray import Eula, EulaSignature +except ImportError: + HAS_PURESTORAGE = False -EULA_API_VERSION = "1.17" +EULA_V2 = "2.30" def set_eula(module, array): """Sign EULA""" changed = False try: - current_eula = array.get_eula() + current_eula = list(array.get_arrays_eula().items)[0] 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"], + if not current_eula.signature.accepted: + changed = True + if not module.check_mode: + res = array.patch_arrays_eula( + eula=Eula( + signature=EulaSignature( + company=module.params["company"], + title=module.params["title"], + name=module.params["name"], + ) + ) + ) + if res.status_code != 200: + module.fail_json( + msg="Signing EULA failed. Error: {0}".format(res.erroros[0].message) ) - except Exception: - module.fail_json(msg="Signing EULA failed") + else: + module.warn("EULA already signed") module.exit_json(changed=changed) @@ -98,18 +107,27 @@ 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), + company=dict(type="str"), + name=dict(type="str"), + title=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_array(module) + api_version = array.get_rest_version() + if LooseVersion(EULA_V2) > LooseVersion(api_version): + if not ( + module.params["company"] + and module.params["title"] + and module.params["name"] + ): + module.fail_json(msg="missing required arguments: company, name, title") + set_eula(module, array) - 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) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_export.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_export.py index 5188dbd96..ba8b29041 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_export.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_export.py @@ -91,10 +91,12 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, get_array, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) MIN_REQUIRED_API_VERSION = "2.3" @@ -224,14 +226,13 @@ def main(): 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: + array = get_array(module) + api_version = array.get_rest_version() + if LooseVersion(MIN_REQUIRED_API_VERSION) > LooseVersion(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( diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_file.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_file.py new file mode 100644 index 000000000..70371602c --- /dev/null +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_file.py @@ -0,0 +1,183 @@ +#!/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_file +version_added: '1.22.0' +short_description: Manage FlashArray File Copies +description: +- Copy FlashArray File from one filesystem to another +author: +- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com> +options: + source_file: + description: + - Name of the file to copy + - Include full path from the perspective of the source managed directory + type: str + required: true + source_dir: + description: + - Name of the source managed directory containing the source file to be copied + type: str + required: true + target_file: + description: + - Name of the file to copy to + - Include full path from the perspective of the target managed directory + - If not provided the file will be copied to the relative path specified by I(name) + type: str + target_dir: + description: + - Name of the target managed directory containing the source file to be copied + - If not provided will use managed directory specified by I(source_dir) + type: str + overwrite: + description: + - Define whether to overwrite an existing target file + type: bool + default: false +extends_documentation_fragment: +- purestorage.flasharray.purestorage.fa +""" + +EXAMPLES = r""" +- name: Copy a file from dir foo to dir bar + purestorage.flasharray.purefa_file: + source_file: "/directory1/file1" + source_dir: "fs1:root" + target_file: "/diff_dir/file1" + target_dir: "fs1:root" + fa_url: 10.10.10.2 + api_token: e31060a7-21fc-e277-6240-25983c6c4592 + +- name: Copy a file in a direcotry to the same directory with a different name + purestorage.flasharray.purefa_file: + source_file: "/directory1/file1" + source_dir: "fs1:root" + target_file: "/directory_1/file2" + fa_url: 10.10.10.2 + api_token: e31060a7-21fc-e277-6240-25983c6c4592 + +- name: Copy a file in a direcotry to an existing file with overwrite + purestorage.flasharray.purefa_file: + source_file: "/directory1/file1" + source_dir: "fs1:root" + target_file: "/diff_dir/file1" + target_dir: "fs2:root" + overwrite: true + 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, +) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) + +MIN_REQUIRED_API_VERSION = "2.26" + + +def _check_dirs(module, array): + if array.get_directories(names=[module.params["source_dir"]]).status_code != 200: + module.fail_json( + msg="Source directory {0} does not exist".format( + module.params["source_dir"] + ) + ) + if array.get_directories(names=[module.params["target_dir"]]).status_code != 200: + module.fail_json( + msg="Target directory {0} does not exist".format( + module.params["target_dir"] + ) + ) + + +def main(): + argument_spec = purefa_argument_spec() + argument_spec.update( + dict( + overwrite=dict(type="bool", default=False), + source_file=dict(type="str", required=True), + source_dir=dict(type="str", required=True), + target_file=dict(type="str"), + target_dir=dict(type="str"), + ) + ) + + required_one_of = [["target_file", "target_dir"]] + module = AnsibleModule( + argument_spec, required_one_of=required_one_of, 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) + api_version = array.get_rest_version() + + if LooseVersion(MIN_REQUIRED_API_VERSION) > LooseVersion(api_version): + module.fail_json( + msg="FlashArray REST version not supported. " + "Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION) + ) + + if not module.params["target_file"]: + module.params["target_file"] = module.params["source_file"] + if not module.params["target_dir"]: + module.params["target_dir"] = module.params["source_dir"] + if ":" not in module.params["target_dir"]: + module.fail_json(msg="Target Direcotry is not formatted correctly") + if ":" not in module.params["source_dir"]: + module.fail_json(msg="Source Direcotry is not formatted correctly") + _check_dirs(module, array) + changed = True + if not module.check_mode: + res = array.post_files( + source_file=flasharray.FilePost( + source=flasharray.ReferenceWithType( + name=module.params["source_dir"], resource_type="directories" + ), + source_path=module.params["source_file"], + ), + overwrite=module.params["overwrite"], + paths=[module.params["target_file"]], + directory_names=[module.params["target_dir"]], + ) + if res.status_code != 200: + module.fail_json( + msg="Failed to copy file. Error: {0}".format(res.errors[0].message) + ) + + module.exit_json(changed=changed) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_fs.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_fs.py index 05fbcb29b..1936bf0ff 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_fs.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_fs.py @@ -93,10 +93,12 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, get_array, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) MIN_REQUIRED_API_VERSION = "2.2" REPL_SUPPORT_API = "2.13" @@ -295,19 +297,21 @@ def main(): 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() + array = get_array(module) + api_version = array.get_rest_version() - if MIN_REQUIRED_API_VERSION not in api_version: + if LooseVersion(MIN_REQUIRED_API_VERSION) > LooseVersion(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"]: + if ( + LooseVersion(REPL_SUPPORT_API) > LooseVersion(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: diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_hardware.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_hardware.py new file mode 100644 index 000000000..ffe9718fe --- /dev/null +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_hardware.py @@ -0,0 +1,110 @@ +#!/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_hardware +version_added: '1.24.0' +short_description: Manage FlashArray Hardware Identification +description: +- Enable or disable FlashArray visual identification lights +author: +- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com> +options: + name: + description: + - Name of hardware component + type: str + required: true + enabled: + description: + - State of the component identification LED + type: bool +extends_documentation_fragment: +- purestorage.flasharray.purestorage.fa +""" + +EXAMPLES = r""" +- name: Enable identification LED + purestorage.flasharray.purefa_hardware: + name: "CH1.FB1" + enabled: True + fa_url: 10.10.10.2 + api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3 + +- name: Disable identification LED + purestorage.flasharray.purefa_hardware: + name: "CH1.FB1" + enabled: False + fa_url: 10.10.10.2 + api_token: T-68618f31-0c9e-4e57-aa44-5306a2cf10e3 +""" + +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 main(): + argument_spec = purefa_argument_spec() + argument_spec.update( + dict( + enabled=dict(type="bool"), + name=dict(type="str", required=True), + ) + ) + + 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) + changed = False + res = array.get_hardware(names=[module.params["name"]]) + if res.status_code == 200: + id_state = getattr(list(res.items)[0], "identify_enabled", None) + if id_state is not None and id_state != module.params["enabled"]: + changed = True + if not module.check_mode: + res = array.patch_hardware( + names=[module.params["name"]], + hardware=flasharray.Hardware( + identify_enabled=module.params["enabled"] + ), + ) + if res.status_code != 200: + module.fail_json( + msg="Failed to set identification LED for {0}. Error: {1}".format( + module.params["name"], res.errors[0].message + ) + ) + + module.exit_json(changed=changed) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_host.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_host.py index 9054d8f30..c396975a2 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_host.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_host.py @@ -479,7 +479,7 @@ def _set_chap_security(module, array): host_password=module.params["host_password"], ) except Exception: - module.params(msg="Failed to set CHAP host username and password") + module.fail_json(msg="Failed to set CHAP host username and password") if module.params["target_user"]: if not pattern.match(module.params["target_password"]): module.fail_json( @@ -492,7 +492,7 @@ def _set_chap_security(module, array): target_password=module.params["target_password"], ) except Exception: - module.params(msg="Failed to set CHAP target username and password") + module.fail_json(msg="Failed to set CHAP target username and password") def _update_chap_security(module, array, answer=False): @@ -507,7 +507,7 @@ def _update_chap_security(module, array, answer=False): try: array.set_host(module.params["name"], host_user="") except Exception: - module.params( + module.fail_json( msg="Failed to clear CHAP host username and password" ) else: @@ -524,7 +524,9 @@ def _update_chap_security(module, array, answer=False): host_password=module.params["host_password"], ) except Exception: - module.params(msg="Failed to set CHAP host username and password") + module.fail_json( + msg="Failed to set CHAP host username and password" + ) if module.params["target_user"]: if module.params["target_password"] == "clear": if chap["target_user"]: @@ -533,7 +535,7 @@ def _update_chap_security(module, array, answer=False): try: array.set_host(module.params["name"], target_user="") except Exception: - module.params( + module.fail_json( msg="Failed to clear CHAP target username and password" ) else: @@ -550,7 +552,9 @@ def _update_chap_security(module, array, answer=False): target_password=module.params["target_password"], ) except Exception: - module.params(msg="Failed to set CHAP target username and password") + module.fail_json( + msg="Failed to set CHAP target username and password" + ) return answer diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_info.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_info.py index de7f05002..262d227be 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_info.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_info.py @@ -24,7 +24,7 @@ description: Purity//FA operating system. By default, the module will collect basic information including hosts, host groups, protection groups and volume counts. Additional information can be collected - based on the configured set of arguements. + based on the configured set of arguments. author: - Pure Storage ansible Team (@sdodsley) <pure-ansible-team@purestorage.com> options: @@ -35,7 +35,7 @@ options: capacity, network, subnet, interfaces, hgroups, pgroups, hosts, admins, volumes, snapshots, pods, replication, vgroups, offload, apps, arrays, certs, kmip, clients, policies, dir_snaps, filesystems, - alerts and virtual_machines. + alerts, virtual_machines, hosts_balance and subscriptions. type: list elements: str required: false @@ -90,16 +90,15 @@ from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa impo get_system, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) -HAS_PACKAGING = True -try: - from packaging import version -except ImportError: - HAS_PACKAGING = False try: from purestorage import purestorage except ImportError: purestorage = None +from datetime import datetime import time SEC_TO_DAY = 86400000 @@ -129,6 +128,18 @@ VM_VERSION = "2.14" VLAN_VERSION = "2.17" NEIGHBOR_API_VERSION = "2.22" POD_QUOTA_VERSION = "2.23" +AUTODIR_API_VERSION = "2.24" +SUBS_API_VERSION = "2.26" +NSID_API_VERSION = "2.27" +NFS_SECURITY_VERSION = "2.29" +UPTIME_API_VERSION = "2.30" + + +def _is_cbs(array): + """Is the selected array a Cloud Block Store""" + model = list(array.get_hardware(filter="type='controller'").items)[0].model + is_cbs = bool("CBS" in model) + return is_cbs def generate_default_dict(module, array): @@ -164,14 +175,38 @@ def generate_default_dict(module, array): default_info["encryption_algorithm"] = encryption.data_at_rest.algorithm default_info["encryption_module_version"] = encryption.module_version eradication = array_data.eradication_config - default_info["eradication_days_timer"] = int( - eradication.eradication_delay / SEC_TO_DAY - ) + if SUBS_API_VERSION in api_version: + default_info["eradication_disabled_days_timer"] = int( + eradication.disabled_delay / SEC_TO_DAY + ) + default_info["eradication_enabled_days_timer"] = int( + eradication.enabled_delay / SEC_TO_DAY + ) + eradication_delay = getattr(eradication, "eradication_delay", None) + if eradication_delay is not None: + default_info["eradication_days_timer"] = int( + eradication_delay / SEC_TO_DAY + ) if SAFE_MODE_VERSION in api_version: if eradication.manual_eradication == "all-enabled": default_info["safe_mode"] = "Disabled" else: default_info["safe_mode"] = "Enabled" + if UPTIME_API_VERSION in api_version: + default_info["controller_uptime"] = [] + controllers = list(arrayv6.get_controllers().items) + timenow = datetime.fromtimestamp(time.time()) + for controller in range(0, len(controllers)): + boottime = datetime.fromtimestamp( + controllers[controller].mode_since / 1000 + ) + delta = timenow - boottime + default_info["controller_uptime"].append( + { + "controller": controllers[controller].name, + "uptime": str(delta), + } + ) if AC_REQUIRED_API_VERSION in api_version: default_info["volume_groups"] = len(array.list_vgroups()) default_info["connected_arrays"] = len(array.list_array_connections()) @@ -282,12 +317,9 @@ def generate_config_dict(module, array): "nameservers": dns_configs[config].nameservers, "domain": dns_configs[config].domain, } - try: - config_info["dns"][dns_configs[config].services[0]][ - "source" - ] = dns_configs[config].source["name"] - except Exception: - pass + config_info["dns"][dns_configs[config].services[0]]["source"] = getattr( + dns_configs[config].source, "name", None + ) if SAML2_VERSION in api_version: config_info["saml2sso"] = {} saml2 = list(arrayv6.get_sso_saml2_idps().items) @@ -355,6 +387,12 @@ def generate_config_dict(module, array): .name, } ) + if SUBS_API_VERSION in api_version: + array_info = list(arrayv6.get_arrays().items)[0] + config_info["ntp_keys"] = bool( + getattr(array_info, "ntp_symmetric_key", None) + ) + config_info["timezone"] = array_info.time_zone else: config_info["directory_service"] = {} @@ -419,6 +457,10 @@ def generate_filesystems_dict(array): ), "exports": {}, } + if LooseVersion(SUBS_API_VERSION) <= LooseVersion(array.get_rest_version()): + files_info[fs_name]["directories"][d_name]["total_used"] = directories[ + directory + ].space.total_used exports = list( array.get_directory_exports( directory_names=[ @@ -485,6 +527,8 @@ def generate_dir_snaps_dict(array): snapshots[snapshot].space, "used_provisioned", None ), } + if LooseVersion(SUBS_API_VERSION) <= LooseVersion(array.get_rest_version()): + dir_snaps_info[s_name]["total_used"] = snapshots[snapshot].space.total_used try: dir_snaps_info[s_name]["policy"] = snapshots[snapshot].policy.name except Exception: @@ -496,7 +540,7 @@ def generate_dir_snaps_dict(array): return dir_snaps_info -def generate_policies_dict(array, quota_available, nfs_user_mapping): +def generate_policies_dict(array, quota_available, autodir_available, nfs_user_mapping): policy_info = {} policies = list(array.get_policies().items) for policy in range(0, len(policies)): @@ -528,6 +572,18 @@ def generate_policies_dict(array, quota_available, nfs_user_mapping): policy_info[p_name][ "user_mapping_enabled" ] = nfs_policy.user_mapping_enabled + if LooseVersion(SUBS_API_VERSION) <= LooseVersion( + array.get_rest_version() + ): + policy_info[p_name]["nfs_version"] = getattr( + nfs_policy, "nfs_version", None + ) + if LooseVersion(NFS_SECURITY_VERSION) <= LooseVersion( + array.get_rest_version() + ): + policy_info[p_name]["security"] = getattr( + nfs_policy, "security", None + ) rules = list( array.get_policies_nfs_client_rules(policy_names=[p_name]).items ) @@ -537,14 +593,16 @@ def generate_policies_dict(array, quota_available, nfs_user_mapping): "permission": rules[rule].permission, "client": rules[rule].client, } + if LooseVersion(SUBS_API_VERSION) <= LooseVersion( + array.get_rest_version() + ): + nfs_rules_dict["nfs_version"] = rules[rule].nfs_version policy_info[p_name]["rules"].append(nfs_rules_dict) if policies[policy].policy_type == "snapshot": - if HAS_PACKAGING: - suffix_enabled = version.parse( - array.get_rest_version() - ) >= version.parse(SHARED_CAP_API_VERSION) - else: - suffix_enabled = False + suffix_enabled = bool( + LooseVersion(array.get_rest_version()) + >= LooseVersion(SHARED_CAP_API_VERSION) + ) rules = list(array.get_policies_snapshot_rules(policy_names=[p_name]).items) for rule in range(0, len(rules)): try: @@ -576,6 +634,8 @@ def generate_policies_dict(array, quota_available, nfs_user_mapping): "notifications": rules[rule].notifications, } policy_info[p_name]["rules"].append(quota_rules_dict) + if policies[policy].policy_type == "autodir" and autodir_available: + pass # there are currently no rules for autodir policies return policy_info @@ -657,8 +717,144 @@ def generate_network_dict(module, array): for neighbor in range(0, len(neighbors)): neighbor_info = neighbors[neighbor] int_name = neighbor_info.local_port.name - net_info[int_name].update( - { + try: + net_info[int_name].update( + { + "neighbor": { + "initial_ttl_in_sec": neighbor_info.initial_ttl_in_sec, + "neighbor_port": { + "description": getattr( + neighbor_info.neighbor_port, "description", None + ), + "name": getattr( + neighbor_info.neighbor_chassis, "name", None + ), + "id": getattr( + neighbor_info.neighbor_port.id, "value", None + ), + }, + "neighbor_chassis": { + "addresses": getattr( + neighbor_info.neighbor_chassis, "addresses", None + ), + "description": getattr( + neighbor_info.neighbor_chassis, "description", None + ), + "name": getattr( + neighbor_info.neighbor_chassis, "name", None + ), + "bridge": { + "enabled": getattr( + neighbor_info.neighbor_chassis.bridge, + "enabled", + False, + ), + "supported": getattr( + neighbor_info.neighbor_chassis.bridge, + "supported", + False, + ), + }, + "repeater": { + "enabled": getattr( + neighbor_info.neighbor_chassis.repeater, + "enabled", + False, + ), + "supported": getattr( + neighbor_info.neighbor_chassis.repeater, + "supported", + False, + ), + }, + "router": { + "enabled": getattr( + neighbor_info.neighbor_chassis.router, + "enabled", + False, + ), + "supported": getattr( + neighbor_info.neighbor_chassis.router, + "supported", + False, + ), + }, + "station_only": { + "enabled": getattr( + neighbor_info.neighbor_chassis.station_only, + "enabled", + False, + ), + "supported": getattr( + neighbor_info.neighbor_chassis.station_only, + "supported", + False, + ), + }, + "telephone": { + "enabled": getattr( + neighbor_info.neighbor_chassis.telephone, + "enabled", + False, + ), + "supported": getattr( + neighbor_info.neighbor_chassis.telephone, + "supported", + False, + ), + }, + "wlan_access_point": { + "enabled": getattr( + neighbor_info.neighbor_chassis.wlan_access_point, + "enabled", + False, + ), + "supported": getattr( + neighbor_info.neighbor_chassis.wlan_access_point, + "supported", + False, + ), + }, + "docsis_cable_device": { + "enabled": getattr( + neighbor_info.neighbor_chassis.docsis_cable_device, + "enabled", + False, + ), + "supported": getattr( + neighbor_info.neighbor_chassis.docsis_cable_device, + "supported", + False, + ), + }, + "id": { + "type": getattr( + neighbor_info.neighbor_chassis.id, + "type", + None, + ), + "value": getattr( + neighbor_info.neighbor_chassis.id, + "value", + None, + ), + }, + }, + } + } + ) + except KeyError: + net_info[int_name] = { + "hwaddr": None, + "mtu": None, + "enabled": None, + "speed": None, + "address": None, + "slaves": None, + "services": None, + "gateway": None, + "netmask": None, + "subnet": None, "neighbor": { "initial_ttl_in_sec": neighbor_info.initial_ttl_in_sec, "neighbor_port": { @@ -779,9 +975,9 @@ def generate_network_dict(module, array): ), }, }, - } + }, } - ) + return net_info @@ -830,6 +1026,8 @@ def generate_capacity_dict(module, array): capacity_info["used_provisioned"] = getattr( capacity.space, "used_provisioned", 0 ) + if SUBS_API_VERSION in api_version: + capacity_info["total_used"] = capacity.space.total_used else: capacity_info["provisioned_space"] = capacity.space["total_provisioned"] capacity_info["free_space"] = ( @@ -843,6 +1041,13 @@ def generate_capacity_dict(module, array): capacity_info["thin_provisioning"] = capacity.space["thin_provisioning"] capacity_info["total_reduction"] = capacity.space["total_reduction"] capacity_info["replication"] = capacity.space["replication"] + if NFS_SECURITY_VERSION in api_version and _is_cbs(arrayv6): + cloud = list(arrayv6.get_arrays_cloud_capacity().items)[0] + capacity_info["cloud_capacity"] = { + "current_capacity": cloud.current_capacity, + "requested_capacity": cloud.requested_capacity, + "status": cloud.status, + } elif CAP_REQUIRED_API_VERSION in api_version: volumes = array.list_volumes(pending=True) capacity_info["provisioned_space"] = sum(item["size"] for item in volumes) @@ -890,9 +1095,11 @@ def generate_snap_dict(module, array): ].space.total_provisioned snap_info[snapshot]["unique_space"] = snapsv6[snap].space.unique if SHARED_CAP_API_VERSION in api_version: - snap_info[snapshot]["snapshots_effective"] = snapsv6[ - snap - ].space.snapshots_effective + snap_info[snapshot]["snapshots_effective"] = getattr( + snapsv6[snap].space, "snapshots_effective", None + ) + if SUBS_API_VERSION in api_version: + snap_info[snapshot]["total_used"] = snapsv6[snap].space.total_used offloads = list(arrayv6.get_offloads().items) for offload in range(0, len(offloads)): offload_name = offloads[offload].name @@ -976,6 +1183,8 @@ def generate_del_snap_dict(module, array): snap ].space.total_provisioned snap_info[snapshot]["unique_space"] = snapsv6[snap].space.unique + if SUBS_API_VERSION in api_version: + snap_info[snapshot]["total_used"] = snapsv6[snap].space.total_used offloads = list(arrayv6.get_offloads().items) for offload in range(0, len(offloads)): offload_name = offloads[offload].name @@ -1073,15 +1282,17 @@ def generate_del_vol_dict(module, array): vol ].space.thin_provisioning if SHARED_CAP_API_VERSION in api_version: - volume_info[name]["snapshots_effective"] = vols_space[ - vol - ].space.snapshots_effective - volume_info[name]["unique_effective"] = vols_space[ - vol - ].space.unique_effective + volume_info[name]["snapshots_effective"] = getattr( + vols_space[vol].space, "snapshots_effective", None + ) + volume_info[name]["unique_effective"] = getattr( + vols_space[vol].space, "unique_effective", None + ) volume_info[name]["used_provisioned"] = ( getattr(vols_space[vol].space, "used_provisioned", None), ) + if SUBS_API_VERSION in api_version: + volume_info[name]["total_used"] = vols_space[vol].space.total_used if ACTIVE_DR_API in api_version: voltags = array.list_volumes(tags=True, pending_only=True) for voltag in range(0, len(voltags)): @@ -1094,16 +1305,22 @@ def generate_del_vol_dict(module, array): "namespace": voltags[voltag]["namespace"], } volume_info[volume]["tags"].append(tagdict) - if SAFE_MODE_VERSION in api_version: + if V6_MINIMUM_API_VERSION in api_version: volumes = list(arrayv6.get_volumes(destroyed=True).items) for vol in range(0, len(volumes)): name = volumes[vol].name - volume_info[name]["priority"] = volumes[vol].priority - volume_info[name]["priority_adjustment"] = volumes[ + volume_info[name]["promotion_status"] = volumes[vol].promotion_status + volume_info[name]["requested_promotion_state"] = volumes[ vol - ].priority_adjustment.priority_adjustment_operator + str( - volumes[vol].priority_adjustment.priority_adjustment_value - ) + ].requested_promotion_state + if SAFE_MODE_VERSION in api_version: + volume_info[name]["subtype"] = volumes[vol].subtype + volume_info[name]["priority"] = volumes[vol].priority + volume_info[name]["priority_adjustment"] = volumes[ + vol + ].priority_adjustment.priority_adjustment_operator + str( + volumes[vol].priority_adjustment.priority_adjustment_value + ) return volume_info @@ -1146,18 +1363,20 @@ def generate_vol_dict(module, array): vol ].space.total_physical if SHARED_CAP_API_VERSION in api_version: - volume_info[name]["snapshots_effective"] = vols_space[ - vol - ].space.snapshots_effective - volume_info[name]["unique_effective"] = vols_space[ - vol - ].space.unique_effective - volume_info[name]["total_effective"] = vols_space[ - vol - ].space.total_effective + volume_info[name]["snapshots_effective"] = getattr( + vols_space[vol].space, "snapshots_effective", None + ) + volume_info[name]["unique_effective"] = getattr( + vols_space[vol].space, "unique_effective", None + ) + volume_info[name]["total_effective"] = getattr( + vols_space[vol].space, "total_effective", None + ) volume_info[name]["used_provisioned"] = ( getattr(vols_space[vol].space, "used_provisioned", None), ) + if SUBS_API_VERSION in api_version: + volume_info[name]["total_used"] = vols_space[vol].space.total_used if AC_REQUIRED_API_VERSION in api_version: qvols = array.list_volumes(qos=True) for qvol in range(0, len(qvols)): @@ -1176,9 +1395,9 @@ def generate_vol_dict(module, array): "source": vvols[vvol]["source"], "serial": vvols[vvol]["serial"], "nvme_nguid": "eui.00" - + vols[vol]["serial"][0:14].lower() + + vvols[vvol]["serial"][0:14].lower() + "24a937" - + vols[vol]["serial"][-10:].lower(), + + vvols[vvol]["serial"][-10:].lower(), "page83_naa": PURE_OUI + vvols[vvol]["serial"], "tags": [], "hosts": [], @@ -1190,16 +1409,22 @@ def generate_vol_dict(module, array): volume_info[volume]["host_encryption_key_status"] = e2ees[e2ee][ "host_encryption_key_status" ] - if SAFE_MODE_VERSION in api_version: + if V6_MINIMUM_API_VERSION in api_version: volumes = list(arrayv6.get_volumes(destroyed=False).items) for vol in range(0, len(volumes)): name = volumes[vol].name - volume_info[name]["priority"] = volumes[vol].priority - volume_info[name]["priority_adjustment"] = volumes[ + volume_info[name]["promotion_status"] = volumes[vol].promotion_status + volume_info[name]["requested_promotion_state"] = volumes[ vol - ].priority_adjustment.priority_adjustment_operator + str( - volumes[vol].priority_adjustment.priority_adjustment_value - ) + ].requested_promotion_state + volume_info[name]["subtype"] = volumes[vol].subtype + if SAFE_MODE_VERSION in api_version: + volume_info[name]["priority"] = volumes[vol].priority + volume_info[name]["priority_adjustment"] = volumes[ + vol + ].priority_adjustment.priority_adjustment_operator + str( + volumes[vol].priority_adjustment.priority_adjustment_value + ) cvols = array.list_volumes(connect=True) for cvol in range(0, len(cvols)): volume = cvols[cvol]["name"] @@ -1223,6 +1448,9 @@ def generate_vol_dict(module, array): def generate_host_dict(module, array): api_version = array._list_available_rest_versions() host_info = {} + if FC_REPL_API_VERSION in api_version: + arrayv6 = get_array(module) + hostsv6 = list(arrayv6.get_hosts().items) hosts = array.list_hosts() for host in range(0, len(hosts)): hostname = hosts[host]["name"] @@ -1246,15 +1474,31 @@ def generate_host_dict(module, array): "personality": array.get_host(hostname, personality=True)["personality"], "target_port": all_tports, "volumes": [], + "performance_balance": [], } - host_connections = array.list_host_connections(hostname) - for connection in range(0, len(host_connections)): - connection_dict = { - "hostgroup": host_connections[connection]["hgroup"], - "volume": host_connections[connection]["vol"], - "lun": host_connections[connection]["lun"], - } - host_info[hostname]["volumes"].append(connection_dict) + if FC_REPL_API_VERSION in api_version: + host_connections = list( + arrayv6.get_connections(host_names=[hostname]).items + ) + for connection in range(0, len(host_connections)): + connection_dict = { + "hostgroup": getattr( + host_connections[connection].host_group, "name", "" + ), + "volume": host_connections[connection].volume.name, + "lun": getattr(host_connections[connection], "lun", ""), + "nsid": getattr(host_connections[connection], "nsid", ""), + } + host_info[hostname]["volumes"].append(connection_dict) + else: + host_connections = array.list_host_connections(hostname) + for connection in range(0, len(host_connections)): + connection_dict = { + "hostgroup": host_connections[connection]["hgroup"], + "volume": host_connections[connection]["vol"], + "lun": host_connections[connection]["lun"], + } + host_info[hostname]["volumes"].append(connection_dict) if host_info[hostname]["iqn"]: chap_data = array.get_host(hostname, chap=True) host_info[hostname]["target_user"] = chap_data["target_user"] @@ -1266,6 +1510,35 @@ def generate_host_dict(module, array): for host in range(0, len(hosts)): hostname = hosts[host]["name"] host_info[hostname]["preferred_array"] = hosts[host]["preferred_array"] + if FC_REPL_API_VERSION in api_version: + hosts_balance = list(arrayv6.get_hosts_performance_balance().items) + for host in range(0, len(hostsv6)): + if hostsv6[host].is_local: + host_info[hostsv6[host].name]["port_connectivity"] = hostsv6[ + host + ].port_connectivity.details + host_perf_balance = [] + for balance in range(0, len(hosts_balance)): + if hosts[host]["name"] == hosts_balance[balance].name: + host_balance = { + "fraction_relative_to_max": getattr( + hosts_balance[balance], + "fraction_relative_to_max", + None, + ), + "op_count": getattr(hosts_balance[balance], "op_count", 0), + "target": getattr( + hosts_balance[balance].target, "name", None + ), + "failed": bool( + getattr(hosts_balance[balance].target, "failover", 0) + ), + } + if host_balance["target"]: + host_perf_balance.append(host_balance) + host_info[hosts[host]["name"]]["performance_balance"].append( + host_perf_balance + ) if VLAN_VERSION in api_version: arrayv6 = get_array(module) hosts = list(arrayv6.get_hosts().items) @@ -1276,6 +1549,131 @@ def generate_host_dict(module, array): return host_info +def generate_del_pgroups_dict(module, array): + pgroups_info = {} + api_version = array._list_available_rest_versions() + pgroups = array.list_pgroups(pending_only=True) + if SHARED_CAP_API_VERSION in api_version: + array_v6 = get_array(module) + deleted_enabled = True + else: + deleted_enabled = False + for pgroup in range(0, len(pgroups)): + protgroup = pgroups[pgroup]["name"] + pgroups_info[protgroup] = { + "hgroups": pgroups[pgroup]["hgroups"], + "hosts": pgroups[pgroup]["hosts"], + "source": pgroups[pgroup]["source"], + "targets": pgroups[pgroup]["targets"], + "volumes": pgroups[pgroup]["volumes"], + "time_remaining": pgroups[pgroup]["time_remaining"], + } + try: + prot_sched = array.get_pgroup(protgroup, schedule=True, pending=True) + prot_reten = array.get_pgroup(protgroup, retention=True, pending=True) + snap_transfers = array.get_pgroup( + protgroup, snap=True, transfer=True, pending=True + ) + except purestorage.PureHTTPError as err: + if err.code == 400: + continue + if prot_sched["snap_enabled"] or prot_sched["replicate_enabled"]: + pgroups_info[protgroup]["snap_frequency"] = prot_sched["snap_frequency"] + pgroups_info[protgroup]["replicate_frequency"] = prot_sched[ + "replicate_frequency" + ] + pgroups_info[protgroup]["snap_enabled"] = prot_sched["snap_enabled"] + pgroups_info[protgroup]["replicate_enabled"] = prot_sched[ + "replicate_enabled" + ] + pgroups_info[protgroup]["snap_at"] = prot_sched["snap_at"] + pgroups_info[protgroup]["replicate_at"] = prot_sched["replicate_at"] + pgroups_info[protgroup]["replicate_blackout"] = prot_sched[ + "replicate_blackout" + ] + pgroups_info[protgroup]["per_day"] = prot_reten["per_day"] + pgroups_info[protgroup]["target_per_day"] = prot_reten["target_per_day"] + pgroups_info[protgroup]["target_days"] = prot_reten["target_days"] + pgroups_info[protgroup]["days"] = prot_reten["days"] + pgroups_info[protgroup]["all_for"] = prot_reten["all_for"] + pgroups_info[protgroup]["target_all_for"] = prot_reten["target_all_for"] + pgroups_info[protgroup]["snaps"] = {} + for snap_transfer in range(0, len(snap_transfers)): + snap = snap_transfers[snap_transfer]["name"] + pgroups_info[protgroup]["snaps"][snap] = { + "time_remaining": snap_transfers[snap_transfer]["time_remaining"], + "created": snap_transfers[snap_transfer]["created"], + "started": snap_transfers[snap_transfer]["started"], + "completed": snap_transfers[snap_transfer]["completed"], + "physical_bytes_written": snap_transfers[snap_transfer][ + "physical_bytes_written" + ], + "data_transferred": snap_transfers[snap_transfer]["data_transferred"], + "progress": snap_transfers[snap_transfer]["progress"], + } + if deleted_enabled: + pgroups_info[protgroup]["deleted_volumes"] = [] + volumes = list( + array_v6.get_protection_groups_volumes(group_names=[protgroup]).items + ) + if volumes: + for volume in range(0, len(volumes)): + if volumes[volume].member["destroyed"]: + pgroups_info[protgroup]["deleted_volumes"].append( + volumes[volume].member["name"] + ) + else: + pgroups_info[protgroup]["deleted_volumes"] = None + if PER_PG_VERSION in api_version: + try: + pgroups_info[protgroup]["retention_lock"] = list( + array_v6.get_protection_groups(names=[protgroup]).items + )[0].retention_lock + pgroups_info[protgroup]["manual_eradication"] = list( + array_v6.get_protection_groups(names=[protgroup]).items + )[0].eradication_config.manual_eradication + except Exception: + pass + if V6_MINIMUM_API_VERSION in api_version: + pgroups = list(array_v6.get_protection_groups(destroyed=True).items) + for pgroup in range(0, len(pgroups)): + name = pgroups[pgroup].name + pgroups_info[name]["snapshots"] = getattr( + pgroups[pgroup].space, "snapshots", None + ) + pgroups_info[name]["shared"] = getattr( + pgroups[pgroup].space, "shared", None + ) + pgroups_info[name]["data_reduction"] = getattr( + pgroups[pgroup].space, "data_reduction", None + ) + pgroups_info[name]["thin_provisioning"] = getattr( + pgroups[pgroup].space, "thin_provisioning", None + ) + pgroups_info[name]["total_physical"] = getattr( + pgroups[pgroup].space, "total_physical", None + ) + pgroups_info[name]["total_provisioned"] = getattr( + pgroups[pgroup].space, "total_provisioned", None + ) + pgroups_info[name]["total_reduction"] = getattr( + pgroups[pgroup].space, "total_reduction", None + ) + pgroups_info[name]["unique"] = getattr( + pgroups[pgroup].space, "unique", None + ) + pgroups_info[name]["virtual"] = getattr( + pgroups[pgroup].space, "virtual", None + ) + pgroups_info[name]["replication"] = getattr( + pgroups[pgroup].space, "replication", None + ) + pgroups_info[name]["used_provisioned"] = getattr( + pgroups[pgroup].space, "used_provisioned", None + ) + return pgroups_info + + def generate_pgroups_dict(module, array): pgroups_info = {} api_version = array._list_available_rest_versions() @@ -1361,7 +1759,7 @@ def generate_pgroups_dict(module, array): except Exception: pass if V6_MINIMUM_API_VERSION in api_version: - pgroups = list(array_v6.get_protection_groups().items) + pgroups = list(array_v6.get_protection_groups(destroyed=False).items) for pgroup in range(0, len(pgroups)): name = pgroups[pgroup].name pgroups_info[name]["snapshots"] = getattr( @@ -1408,14 +1806,19 @@ def generate_rl_dict(module, array): rlinks = array.list_pod_replica_links() for rlink in range(0, len(rlinks)): link_name = rlinks[rlink]["local_pod_name"] - since_epoch = rlinks[rlink]["recovery_point"] / 1000 - recovery_datatime = time.strftime( - "%Y-%m-%d %H:%M:%S", time.localtime(since_epoch) - ) + if rlinks[rlink]["recovery_point"]: + since_epoch = rlinks[rlink]["recovery_point"] / 1000 + recovery_datatime = time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(since_epoch) + ) + else: + recovery_datatime = None + if rlinks[rlink]["lag"]: + lag = str(rlinks[rlink]["lag"] / 1000) + "s" rl_info[link_name] = { "status": rlinks[rlink]["status"], "direction": rlinks[rlink]["direction"], - "lag": str(rlinks[rlink]["lag"] / 1000) + "s", + "lag": lag, "remote_pod_name": rlinks[rlink]["remote_pod_name"], "remote_names": rlinks[rlink]["remote_names"], "recovery_point": recovery_datatime, @@ -1464,21 +1867,37 @@ def generate_del_pods_dict(module, array): pods = list(arrayv6.get_pods(destroyed=True).items) for pod in range(0, len(pods)): name = pods[pod].name - pods_info[name]["snapshots"] = pods[pod].space.snapshots - pods_info[name]["shared"] = pods[pod].space.shared - pods_info[name]["data_reduction"] = pods[pod].space.data_reduction - pods_info[name]["thin_provisioning"] = pods[pod].space.thin_provisioning - pods_info[name]["total_physical"] = pods[pod].space.total_physical - pods_info[name]["total_provisioned"] = pods[pod].space.total_provisioned - pods_info[name]["total_reduction"] = pods[pod].space.total_reduction - pods_info[name]["unique"] = pods[pod].space.unique - pods_info[name]["virtual"] = pods[pod].space.virtual + pods_info[name]["snapshots"] = getattr( + pods[pod].space, "snapshots", None + ) + pods_info[name]["shared"] = getattr(pods[pod].space, "shared", None) + pods_info[name]["data_reduction"] = getattr( + pods[pod].space, "data_reduction", None + ) + pods_info[name]["thin_provisioning"] = getattr( + pods[pod].space, "thin_provisioning", None + ) + pods_info[name]["total_physical"] = getattr( + pods[pod].space, "total_physical", None + ) + pods_info[name]["total_provisioned"] = getattr( + pods[pod].space, "total_provisioned", None + ) + pods_info[name]["total_reduction"] = getattr( + pods[pod].space, "total_reduction", None + ) + pods_info[name]["unique"] = getattr(pods[pod].space, "unique", None) + pods_info[name]["virtual"] = getattr(pods[pod].space, "virtual", None) pods_info[name]["replication"] = pods[pod].space.replication pods_info[name]["used_provisioned"] = getattr( pods[pod].space, "used_provisioned", None ) if POD_QUOTA_VERSION in api_version: - pods_info[name]["quota_limit"] = pods[pod].quota_limit + pods_info[name]["quota_limit"] = getattr( + pods[pod], "quota_limit", None + ) + if SUBS_API_VERSION in api_version: + pods_info[name]["total_used"] = pods[pod].space.total_used return pods_info @@ -1547,6 +1966,8 @@ def generate_pods_dict(module, array): pods_info[name]["used_provisioned"] = getattr( pods[pod].space, "used_provisioned", None ) + if SUBS_API_VERSION in api_version: + pods_info[name]["total_used"] = pods[pod].space.total_used return pods_info @@ -1605,16 +2026,16 @@ def generate_conn_array_dict(module, array): pass try: if bool(carrays[carray].throttle.default_limit): - conn_array_info[arrayname]["throttling"][ - "default_limit" - ] = carrays[carray].throttle.default_limit + conn_array_info[arrayname]["throttling"]["default_limit"] = ( + carrays[carray].throttle.default_limit + ) except AttributeError: pass try: if bool(carrays[carray].throttle.window_limit): - conn_array_info[arrayname]["throttling"][ - "window_limit" - ] = carrays[carray].throttle.window_limit + conn_array_info[arrayname]["throttling"]["window_limit"] = ( + carrays[carray].throttle.window_limit + ) except AttributeError: pass else: @@ -1682,6 +2103,10 @@ def generate_vgroups_dict(module, array): vgroups_info[name]["iops_limit"] = getattr( vgroups[vgroup].qos, "iops_limit", "" ) + if SUBS_API_VERSION in api_version: + vgroups_info[name]["total_used"] = getattr( + vgroups[vgroup].space, "total_used", None + ) if SAFE_MODE_VERSION in api_version: for vgroup in range(0, len(vgroups)): name = vgroups[vgroup].name @@ -1844,6 +2269,8 @@ def generate_nfs_offload_dict(module, array): offload_info[name]["used_provisioned"] = getattr( offloads[offload].space, "used_provisioned", None ) + if SUBS_API_VERSION in api_version: + offload_info[name]["total_used"] = offloads[offload].space.total_used return offload_info @@ -1902,6 +2329,8 @@ def generate_s3_offload_dict(module, array): offload_info[name]["used_provisioned"] = getattr( offloads[offload].space, "used_provisioned", None ) + if SUBS_API_VERSION in api_version: + offload_info[name]["total_used"] = offloads[offload].space.total_used return offload_info @@ -1957,6 +2386,8 @@ def generate_azure_offload_dict(module, array): offload_info[name]["used_provisioned"] = getattr( offloads[offload].space, "used_provisioned", None ) + if SUBS_API_VERSION in api_version: + offload_info[name]["total_used"] = offloads[offload].space.total_used return offload_info @@ -1989,6 +2420,8 @@ def generate_google_offload_dict(array): offloads[offload].space, "used_provisioned", None ), } + if LooseVersion(SUBS_API_VERSION) <= LooseVersion(array.get_rest_version()): + offload_info[name]["total_used"] = offloads[offload].space.total_used return offload_info @@ -2016,24 +2449,37 @@ def generate_hgroups_dict(module, array): arrayv6 = get_array(module) hgroups = list(arrayv6.get_host_groups().items) for hgroup in range(0, len(hgroups)): - name = hgroups[hgroup].name - hgroups_info[name]["snapshots"] = hgroups[hgroup].space.snapshots - hgroups_info[name]["data_reduction"] = hgroups[hgroup].space.data_reduction - hgroups_info[name]["thin_provisioning"] = hgroups[ - hgroup - ].space.thin_provisioning - hgroups_info[name]["total_physical"] = hgroups[hgroup].space.total_physical - hgroups_info[name]["total_provisioned"] = hgroups[ - hgroup - ].space.total_provisioned - hgroups_info[name]["total_reduction"] = hgroups[ - hgroup - ].space.total_reduction - hgroups_info[name]["unique"] = hgroups[hgroup].space.unique - hgroups_info[name]["virtual"] = hgroups[hgroup].space.virtual - hgroups_info[name]["used_provisioned"] = getattr( - hgroups[hgroup].space, "used_provisioned", None - ) + if hgroups[hgroup].is_local: + name = hgroups[hgroup].name + hgroups_info[name]["snapshots"] = getattr( + hgroups[hgroup].space, "snapshots", None + ) + hgroups_info[name]["data_reduction"] = getattr( + hgroups[hgroup].space, "data_reduction", None + ) + hgroups_info[name]["thin_provisioning"] = getattr( + hgroups[hgroup].space, "thin_provisioning", None + ) + hgroups_info[name]["total_physical"] = getattr( + hgroups[hgroup].space, "total_physical", None + ) + hgroups_info[name]["total_provisioned"] = getattr( + hgroups[hgroup].space, "total_provisioned", None + ) + hgroups_info[name]["total_reduction"] = getattr( + hgroups[hgroup].space, "total_reduction", None + ) + hgroups_info[name]["unique"] = getattr( + hgroups[hgroup].space, "unique", None + ) + hgroups_info[name]["virtual"] = getattr( + hgroups[hgroup].space, "virtual", None + ) + hgroups_info[name]["used_provisioned"] = getattr( + hgroups[hgroup].space, "used_provisioned", None + ) + if SUBS_API_VERSION in api_version: + hgroups_info[name]["total_used"] = hgroups[hgroup].space.total_used return hgroups_info @@ -2150,6 +2596,17 @@ def generate_vmsnap_dict(array): return vmsnap_info +def generate_subs_dict(array): + subs_info = {} + subs = list(array.get_subscription_assets().items) + for sub in range(0, len(subs)): + name = subs[sub].name + subs_info[name] = { + "subscription_id": subs[sub].subscription.id, + } + return subs_info + + def main(): argument_spec = purefa_argument_spec() argument_spec.update( @@ -2188,7 +2645,9 @@ def main(): "policies", "dir_snaps", "filesystems", + "alerts", "virtual_machines", + "subscriptions", ) subset_test = (test in valid_subsets for test in subset) if not all(subset_test): @@ -2225,6 +2684,7 @@ def main(): info["hgroups"] = generate_hgroups_dict(module, array) if "pgroups" in subset or "all" in subset: info["pgroups"] = generate_pgroups_dict(module, array) + info["deleted_pgroups"] = generate_del_pgroups_dict(module, array) if "pods" in subset or "all" in subset or "replication" in subset: info["replica_links"] = generate_rl_dict(module, array) info["pods"] = generate_pods_dict(module, array) @@ -2264,7 +2724,13 @@ def main(): quota = True else: quota = False - info["policies"] = generate_policies_dict(array_v6, quota, user_map) + if AUTODIR_API_VERSION in api_version: + autodir = True + else: + autodir = False + info["policies"] = generate_policies_dict( + array_v6, quota, autodir, user_map + ) if "clients" in subset or "all" in subset: info["clients"] = generate_clients_dict(array_v6) if "dir_snaps" in subset or "all" in subset: @@ -2273,6 +2739,10 @@ def main(): info["pg_snapshots"] = generate_pgsnaps_dict(array_v6) if "alerts" in subset or "all" in subset: info["alerts"] = generate_alerts_dict(array_v6) + if SUBS_API_VERSION in api_version and ( + "subscriptions" in subset or "all" in subset + ): + info["subscriptions"] = generate_subs_dict(array_v6) if VM_VERSION in api_version and ( "virtual_machines" in subset or "all" in subset ): diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_inventory.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_inventory.py index 8e65ee07e..396699b58 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_inventory.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_inventory.py @@ -48,17 +48,18 @@ purefa_inventory: from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, get_array, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) -NEW_API_VERSION = "2.2" SFP_API_VERSION = "2.16" -def generate_new_hardware_dict(array, versions): +def generate_new_hardware_dict(array): hw_info = { "fans": {}, "controllers": {}, @@ -137,216 +138,137 @@ def generate_new_hardware_dict(array, versions): "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[ + api_version = array.get_rest_version() + if LooseVersion(SFP_API_VERSION) <= LooseVersion(api_version): + try: + 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 - ].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[ + ].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.rx_power_thresholds.warn_high, - "warn_low": port_details[ + ].static.vendor_serial_number, + "vendor_part_number": port_details[ port_detail - ].static.rx_power_thresholds.warn_low, - }, - "tx_bias_thresholds": { - "alarm_high": port_details[ + ].static.vendor_part_number, + "vendor_date_code": port_details[ port_detail - ].static.tx_bias_thresholds.alarm_high, - "alarm_low": port_details[ + ].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.tx_bias_thresholds.alarm_low, - "warn_high": port_details[ + ].static.fc_transmission_media, + "extended_identifier": 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"] - + ].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 + ) + except AttributeError: + pass return hw_info @@ -354,13 +276,8 @@ 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) + array = get_array(module) + inv_info = generate_new_hardware_dict(array) module.exit_json(changed=False, purefa_inv=inv_info) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_kmip.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_kmip.py index 8774abe87..b422f6f1e 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_kmip.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_kmip.py @@ -97,10 +97,12 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, get_array, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) MIN_REQUIRED_API_VERSION = "2.2" @@ -222,16 +224,15 @@ def main(): 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() + array = get_array(module) + api_version = array.get_rest_version() - if MIN_REQUIRED_API_VERSION not in api_version: + if LooseVersion(MIN_REQUIRED_API_VERSION) > LooseVersion(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: diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_logging.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_logging.py index a2f8e136d..c16818498 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_logging.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_logging.py @@ -66,10 +66,12 @@ 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, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) AUDIT_API_VERSION = "2.2" @@ -87,13 +89,12 @@ def main(): 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() + array = get_array(module) + api_version = array.get_rest_version() audits = [] changed = False - if AUDIT_API_VERSION in api_version: + if LooseVersion(AUDIT_API_VERSION) <= LooseVersion(api_version): changed = True - array = get_array(module) if not module.check_mode: if module.params["log_type"] == "audit": all_audits = list( @@ -151,7 +152,7 @@ def main(): "command": all_audits[audit].command, "subcommand": all_audits[audit].subcommand, "user": all_audits[audit].user, - "origin": all_audits[audit].origin.name, + "origin": getattr(all_audits[audit].origin, "name", None), } audits.append(data) else: diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_messages.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_messages.py index a28bd56b2..131b7971a 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_messages.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_messages.py @@ -27,15 +27,14 @@ options: severity: description: - severity of the alerts to show - type: list - elements: str - choices: [ all, critical, warning, info ] - default: [ all ] + type: str + choices: [ critical, warning, info ] + default: info state: description: - State of alerts to show default: open - choices: [ all, open, closed ] + choices: [ open, closed ] type: str flagged: description: @@ -57,8 +56,7 @@ EXAMPLES = r""" purefa_messages: history: 4w flagged : false - severity: - - critical + severity: critical fa_url: 10.10.10.2 api_token: 89a9356f-c203-d263-8a89-c229486a13ba """ @@ -70,10 +68,12 @@ 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, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) MIN_REQUIRED_API_VERSION = "2.2" ALLOWED_PERIODS = ["h", "d", "w", "y"] @@ -101,28 +101,26 @@ def main(): argument_spec = purefa_argument_spec() argument_spec.update( dict( - state=dict(type="str", default="open", choices=["all", "open", "closed"]), + state=dict(type="str", default="open", choices=["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"], + type="str", + default="info", + choices=["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: + array = get_array(module) + api_version = array.get_rest_version() + if LooseVersion(MIN_REQUIRED_API_VERSION) > LooseVersion(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())) @@ -131,30 +129,12 @@ def main(): 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"] + "'" + severity = " and severity='" + module.params["severity"] + "'" + state = " and state='" + module.params["state"] + "'" filter_string = "notified>" + since_time + state + flagged + severity try: - res = array_v6.get_alerts(filter=filter_string) + res = array.get_alerts(filter=filter_string) alerts = list(res.items) except Exception: module.fail_json( diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_network.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_network.py index e5004568a..c296707d0 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_network.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_network.py @@ -67,6 +67,39 @@ options: type: list choices: [ "replication", "management", "ds", "file", "iscsi", "scsi-fc", "nvme-fc", "nvme-tcp", "nvme-roce", "system"] version_added: '1.15.0' + interface: + description: + - Type of interface to create if subinterfaces is supplied + type: str + choices: [ "vif", "lacp" ] + version_added: '1.22.0' + subordinates: + description: + - List of one or more child devices to be added to a LACP interface + - Subordinates must be on the same controller, therefore the full device needs + to be provided. + type: list + elements: str + version_added: '1.22.0' + subinterfaces: + description: + - List of one or more child devices to be added to a VIF interface + - Only the 'eth' name needs to be provided, such as 'eth6'. This interface on + all controllers will be assigned to the interface. + type: list + elements: str + version_added: '1.22.0' + subnet: + description: + - Name of the subnet which interface is to be attached + type: str + version_added: '1.22.0' + enabled: + description: + - State of the network interface + type: bool + default: true + version_added: '1.22.0' extends_documentation_fragment: - purestorage.flasharray.purestorage.fa """ @@ -117,14 +150,21 @@ RETURN = """ """ try: - from netaddr import IPAddress, IPNetwork + from netaddr import IPAddress, IPNetwork, valid_ipv4, valid_ipv6 HAS_NETADDR = True except ImportError: HAS_NETADDR = False try: - from pypureclient.flasharray import NetworkInterfacePatch + from pypureclient.flasharray import ( + NetworkInterfacePatch, + NetworkInterfacePost, + NetworkinterfacepostEth, + NetworkinterfacepatchEth, + FixedReferenceNoId, + ReferenceNoId, + ) HAS_PYPURECLIENT = True except ImportError: @@ -154,8 +194,7 @@ def _get_fc_interface(module, array): if interface_list.status_code == 200: interface = list(interface_list.items)[0] return interface - else: - return None + return None def _get_interface(module, array): @@ -227,35 +266,107 @@ def update_fc_interface(module, array, interface, api_version): module.exit_json(changed=changed) +def _create_subordinates(module, array): + subordinates_v1 = [] + subordinates_v2 = [] + all_children = True + if module.params["subordinates"]: + for inter in sorted(module.params["subordinates"]): + if array.get_network_interfaces(names=[inter]).status_code != 200: + all_children = False + if not all_children: + module.fail_json( + msg="Subordinate {0} does not exist. Ensure you have specified the controller.".format( + inter + ) + ) + subordinates_v2.append(FixedReferenceNoId(name=inter)) + subordinates_v1.append(inter) + return subordinates_v1, subordinates_v2 + + +def _create_subinterfaces(module, array): + subinterfaces_v1 = [] + subinterfaces_v2 = [] + all_children = True + purity_vm = bool(len(array.get_controllers().items) == 1) + if module.params["subinterfaces"]: + for inter in sorted(module.params["subinterfaces"]): + # As we may be on a single controller device, only check for the ct0 version of the interface + if array.get_network_interfaces(names=["ct0." + inter]).status_code != 200: + all_children = False + if not all_children: + module.fail_json( + msg="Child subinterface {0} does not exist".format(inter) + ) + subinterfaces_v2.append(FixedReferenceNoId(name="ct0." + inter)) + subinterfaces_v1.append("ct0." + inter) + if not purity_vm: + subinterfaces_v2.append(FixedReferenceNoId(name="ct1." + inter)) + subinterfaces_v1.append("ct1." + inter) + return subinterfaces_v1, subinterfaces_v2 + + def update_interface(module, array, interface): """Modify Interface settings""" changed = False current_state = { + "enabled": interface["enabled"], "mtu": interface["mtu"], "gateway": interface["gateway"], "address": interface["address"], "netmask": interface["netmask"], "services": sorted(interface["services"]), + "slaves": sorted(interface["slaves"]), } + array6 = get_array(module) + subinterfaces = sorted(current_state["slaves"]) + if module.params["subinterfaces"]: + new_subinterfaces, dummy = _create_subinterfaces(module, array6) + if new_subinterfaces != subinterfaces: + subinterfaces = new_subinterfaces + else: + subinterfaces = current_state["slaves"] + if module.params["subordinates"]: + new_subordinates, dummy = _create_subordinates(module, array6) + if new_subordinates != subinterfaces: + subinterfaces = new_subordinates + else: + subinterfaces = current_state["slaves"] + if module.params["enabled"] != current_state["enabled"]: + enabled = module.params["enabled"] + else: + enabled = current_state["enabled"] + if not current_state["gateway"]: + try: + if valid_ipv4(interface["address"]): + current_state["gateway"] = None + elif valid_ipv6(interface["address"]): + current_state["gateway"] = None + except AttributeError: + current_state["gateway"] = None if not module.params["servicelist"]: services = sorted(interface["services"]) else: services = sorted(module.params["servicelist"]) if not module.params["address"]: address = interface["address"] + netmask = interface["netmask"] 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"]), - ]: + if module.params["gateway"] and module.params["gateway"] not in [ + "0.0.0.0", + "::", + ]: + if module.params["gateway"] not in IPNetwork(module.params["address"]): module.fail_json(msg="Gateway and subnet are not compatible.") + if 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 address in ["0.0.0.0", "::"]: + address = None if not module.params["mtu"]: mtu = interface["mtu"] else: @@ -268,36 +379,87 @@ def update_interface(module, array, interface): else: mtu = module.params["mtu"] if module.params["address"]: - netmask = str(IPNetwork(module.params["address"]).netmask) + if valid_ipv4(address): + netmask = str(IPNetwork(module.params["address"]).netmask) + else: + netmask = str(module.params["address"].split("/", 1)[1]) + if netmask in ["0.0.0.0", "0"]: + netmask = None else: netmask = interface["netmask"] if not module.params["gateway"]: gateway = interface["gateway"] - else: + elif module.params["gateway"] in ["0.0.0.0", "::"]: + gateway = None + elif valid_ipv4(address): 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()) + else: + gateway = module.params["gateway"] + new_state = { + "enabled": enabled, "address": address, "mtu": mtu, "gateway": gateway, "netmask": netmask, - "services": services, + "services": sorted(services), + "slaves": sorted(subinterfaces), } + if new_state["address"]: + if ( + current_state["address"] + and IPAddress(new_state["address"]).version + != IPAddress(current_state["address"]).version + ): + if new_state["gateway"]: + if ( + IPAddress(new_state["gateway"]).version + != IPAddress(new_state["address"]).version + ): + module.fail_json( + msg="Changing IP protocol requires gateway to change as well." + ) if new_state != current_state: changed = True 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: + if not module.check_mode: + network = NetworkInterfacePatch( + services=module.params["servicelist"] + ) + res = array6.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 updated as pypureclient module is required" + ) + if ( "management" in interface["services"] or "app" in interface["services"] - ) and address == "0.0.0.0/0": + ) and address in ["0.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: + array.set_network_interface( + interface["name"], enabled=new_state["enabled"] + ) if new_state["gateway"] is not None: array.set_network_interface( interface["name"], @@ -306,67 +468,200 @@ def update_interface(module, array, interface): netmask=new_state["netmask"], gateway=new_state["gateway"], ) + if ( + current_state["slaves"] != new_state["slaves"] + and new_state["slaves"] != [] + ): + array.set_network_interface( + interface["name"], + subinterfacelist=new_state["slaves"], + ) else: + if valid_ipv4(new_state["address"]): + empty_gateway = "0.0.0.0" + else: + empty_gateway = "::" array.set_network_interface( interface["name"], address=new_state["address"], mtu=new_state["mtu"], netmask=new_state["netmask"], + gateway=empty_gateway, ) + if ( + current_state["slaves"] != new_state["slaves"] + and new_state["slaves"] != [] + ): + array.set_network_interface( + interface["name"], + subinterfacelist=new_state["slaves"], + ) 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.exit_json(changed=changed) + + +def create_interface(module, array): + changed = True + subnet_exists = bool( + array.get_subnets(names=[module.params["subnet"]]).status_code == 200 + ) + if module.params["subnet"] and not subnet_exists: + module.fail_json( + msg="Subnet {0} does not exist".format(module.params["subnet"]) + ) + + if module.params["interface"] == "vif": + dummy, subinterfaces = _create_subinterfaces(module, array) + else: + dummy, subinterfaces = _create_subordinates(module, array) + + if not module.check_mode: + if module.params["address"]: + address = str(module.params["address"].strip("[]").split("/", 1)[0]) + if valid_ipv4(address): + netmask = str(IPNetwork(module.params["address"]).netmask) + else: + netmask = str(module.params["address"].strip("[]").split("/", 1)[1]) + else: + netmask = None + address = None + if module.params["gateway"]: + gateway = str(module.params["gateway"].strip("[]")) + if gateway not in ["0.0.0.0", "::"]: + if address and gateway not in IPNetwork(module.params["address"]): + module.fail_json(msg="Gateway and subnet are not compatible.") + else: + gateway = None + if module.params["interface"] == "vif": + res = array.post_network_interfaces( + names=[module.params["name"]], + network=NetworkInterfacePost( + eth=NetworkinterfacepostEth(subtype="vif") + ), + ) + else: + res = array.post_network_interfaces( + names=[module.params["name"]], + network=NetworkInterfacePost( + eth=NetworkinterfacepostEth( + subtype="lacpbond", subinterfaces=subinterfaces + ), + ), + ) + + if res.status_code != 200: + module.fail_json( + msg="Failed to create interface {0}. Error: {1}".format( + module.params["name"], res.errors[0].message + ) + ) + + if module.params["subinterfaces"] and module.params["subnet"]: + res = array.patch_network_interfaces( + names=[module.params["name"]], + network=NetworkInterfacePatch( + enabled=module.params["enabled"], + eth=NetworkinterfacepatchEth( + subinterfaces=subinterfaces, + address=address, + gateway=gateway, + mtu=module.params["mtu"], + netmask=netmask, + subnet=ReferenceNoId(name=module.params["subnet"]), + ), + ), + ) + if res.status_code != 200: + array.delete_network_interfaces(names=[module.params["name"]]) module.fail_json( - msg="Failed to enable interface {0}.".format(interface["name"]) + msg="Failed to create interface {0}. Error: {1}".format( + module.params["name"], res.errors[0].message + ) ) - if interface["enabled"] and module.params["state"] == "absent": - changed = True - if not module.check_mode: - try: - array.disable_network_interface(interface["name"]) - except Exception: + elif module.params["subinterfaces"] and not module.params["subnet"]: + res = array.patch_network_interfaces( + names=[module.params["name"]], + network=NetworkInterfacePatch( + enabled=module.params["enabled"], + eth=NetworkinterfacepatchEth( + subinterfaces=subinterfaces, + address=address, + gateway=gateway, + mtu=module.params["mtu"], + netmask=netmask, + ), + ), + ) + if res.status_code != 200: + array.delete_network_interfaces(names=[module.params["name"]]) module.fail_json( - msg="Failed to disable interface {0}.".format(interface["name"]) + msg="Failed to create interface {0}. Error: {1}".format( + module.params["name"], res.errors[0].message + ) ) - 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"] + elif not module.params["subinterfaces"] and module.params["subnet"]: + res = array.patch_network_interfaces( + names=[module.params["name"]], + network=NetworkInterfacePatch( + enabled=module.params["enabled"], + eth=NetworkinterfacepatchEth( + address=address, + gateway=gateway, + mtu=module.params["mtu"], + netmask=netmask, + subnet=ReferenceNoId(name=module.params["subnet"]), + ), + ), + ) + if res.status_code != 200: + array.delete_network_interfaces(names=[module.params["name"]]) + module.fail_json( + msg="Failed to create interface {0}. Error: {1}".format( + module.params["name"], res.errors[0].message ) - res = array.patch_network_interfaces( - names=[module.params["name"]], network=network + ) + else: + res = array.patch_network_interfaces( + names=[module.params["name"]], + network=NetworkInterfacePatch( + enabled=module.params["enabled"], + eth=NetworkinterfacepatchEth( + address=address, + gateway=gateway, + mtu=module.params["mtu"], + netmask=netmask, + ), + ), + ) + if res.status_code != 200: + array.delete_network_interfaces(names=[module.params["name"]]) + module.fail_json( + msg="Failed to create interface {0}. Error: {1}".format( + module.params["name"], res.errors[0].message ) - 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 delete_interface(module, array): + changed = True + if not module.check_mode: + res = array.delete_network_interfaces(names=[module.params["name"]]) + if res.status_code != 200: + module.fail_json( + msg="Failed to delete network interface {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( @@ -392,15 +687,34 @@ def main(): "system", ], ), + interface=dict(type="str", choices=["vif", "lacp"]), + subinterfaces=dict(type="list", elements="str"), + subordinates=dict(type="list", elements="str"), + subnet=dict(type="str"), + enabled=dict(type="bool", default=True), ) ) module = AnsibleModule(argument_spec, supports_check_mode=True) + if module.params["state"] == "present": + if module.params["interface"] == "lacp" and not module.params["subordinates"]: + module.fail_json( + msg="interface is lacp but all of the following are missing: subordinates" + ) + + creating_new_if = bool(module.params["interface"]) + if not HAS_NETADDR: module.fail_json(msg="netaddr module is required") array = get_system(module) + if module.params["address"]: + module.params["address"] = module.params["address"].strip("[]") + if module.params["gateway"]: + module.params["gateway"] = module.params["gateway"].strip("[]") + if "/" not in module.params["address"]: + module.fail_json(msg="address must include valid netmask bits") api_version = array._list_available_rest_versions() if not _is_cbs(array): if module.params["servicelist"] and "system" in module.params["servicelist"]: @@ -424,11 +738,29 @@ def main(): else: update_interface(module, array, interface) else: + if (module.params["interface"] == "vif" and module.params["subordinates"]) or ( + module.params["interface"] == "lacp" and module.params["subinterfaces"] + ): + module.fail_json( + msg="interface type not compatable with provided subinterfaces | subordinates" + ) interface = _get_interface(module, array) - if not interface: - module.fail_json(msg="Invalid network interface specified.") - else: + array6 = get_array(module) + if not creating_new_if: + if not interface: + module.fail_json(msg="Invalid network interface specified.") + elif module.params["state"] == "present": + update_interface(module, array, interface) + else: + delete_interface(module, array6) + elif not interface and module.params["state"] == "present": + create_interface(module, array6) + elif interface and module.params["state"] == "absent": + delete_interface(module, array6) + elif module.params["state"] == "present": update_interface(module, array, interface) + else: + module.exit_json(changed=False) module.exit_json(changed=False) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_ntp.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_ntp.py index e2a5c8f18..348d1fed8 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_ntp.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_ntp.py @@ -40,6 +40,15 @@ options: - 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. + ntp_key: + type: str + description: + - The NTP symmetric key to be used for NTP authentication. + - If it is an ASCII string, it cannot contain the character "#" + and cannot be longer than 20 characters. + - If it is a hex-encoded string, it cannot be longer than 64 characters. + - Setting this parameter is not idempotent. + version_added: "1.22.0" extends_documentation_fragment: - purestorage.flasharray.purestorage.fa """ @@ -68,14 +77,26 @@ RETURN = r""" from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, + get_array, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) + +HAS_PURESTORAGE = True +try: + from pypureclient.flasharray import Arrays +except ImportError: + HAS_PURESTORAGE = False + + +KEY_API_VERSION = "2.26" def _is_cbs(array, is_cbs=False): """Is the selected array a Cloud Block Store""" - model = array.get(controllers=True)[0]["model"] + model = list(array.get_controllers().items)[0].model is_cbs = bool("CBS" in model) return is_cbs @@ -94,7 +115,7 @@ def delete_ntp(module, array): changed = True if not module.check_mode: try: - array.set(ntpserver=[]) + array.patch_arrays(array=Arrays(ntp_servers=[])) except Exception: module.fail_json(msg="Deletion of NTP servers failed") else: @@ -109,41 +130,85 @@ def create_ntp(module, array): if not module.params["ntp_servers"]: module.params["ntp_servers"] = ["0.pool.ntp.org"] try: - array.set(ntpserver=module.params["ntp_servers"][0:4]) + array.patch_arrays( + array=Arrays(ntp_servers=module.params["ntp_servers"][0:4]) + ) except Exception: module.fail_json(msg="Update of NTP servers failed") module.exit_json(changed=changed) +def update_ntp_key(module, array): + """Update NTP Symmetric Key""" + if module.params["ntp_key"] == "" and not getattr( + list(array.get_arrays().items)[0], "ntp_symmetric_key", None + ): + changed = False + else: + try: + int(module.params["ntp_key"], 16) + if len(module.params["ntp_key"]) > 64: + module.fail_json(msg="HEX string cannot be longer than 64 characters") + except ValueError: + if len(module.params["ntp_key"]) > 20: + module.fail_json(msg="ASCII string cannot be longer than 20 characters") + if "#" in module.params["ntp_key"]: + module.fail_json(msg="ASCII string cannot contain # character") + if not all(ord(c) < 128 for c in module.params["ntp_key"]): + module.fail_json(msg="NTP key is non-ASCII") + changed = True + res = array.patch_arrays( + array=Arrays(ntp_symmetric_key=module.params["ntp_key"]) + ) + if res.status_code != 200: + module.fail_json( + msg="Failed to update NTP Symmetric Key. Error: {0}".format( + res.errors[0].message + ) + ) + module.exit_json(changed=changed) + + if len(module.params["ntp_key"]) > 20: + # Must be HEX string is greter than 20 characters + try: + int(module.params["ntp_key"], 16) + except ValueError: + module.fail_json(msg="NTP key is not HEX") + + def main(): argument_spec = purefa_argument_spec() argument_spec.update( dict( ntp_servers=dict(type="list", elements="str"), + ntp_key=dict(type="str", no_log=True), 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 - ) + 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) + array = get_array(module) + api_version = array.get_rest_version() 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: + elif module.params["ntp_servers"]: module.params["ntp_servers"] = remove(module.params["ntp_servers"]) - if sorted(array.get(ntpserver=True)["ntpserver"]) != sorted( + if sorted(list(array.get_arrays().items)[0].ntp_servers) != sorted( module.params["ntp_servers"][0:4] ): create_ntp(module, array) - + if module.params["ntp_key"] or module.params["ntp_key"] == "": + if LooseVersion(KEY_API_VERSION) > LooseVersion(api_version): + module.fail_json(msg="REST API does not support setting NTP Symmetric Key") + else: + update_ntp_key(module, array) module.exit_json(changed=False) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_offload.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_offload.py index 1265911fe..5b911a24d 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_offload.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_offload.py @@ -40,6 +40,7 @@ options: protocol: description: - Define which protocol the offload engine uses + - NFS is not a supported protocl from Purity//FA 6.6.0 and higher default: nfs choices: [ nfs, s3, azure, gcp ] type: str @@ -91,7 +92,14 @@ options: type: str choices: ['retention-based', 'aws-standard-class'] default: retention-based - + profile: + description: + - The Offload target profile that will be selected for this target. + - This option allows more granular configuration for the target on top + of the protocol parameter + type: str + version_added: '1.21.0' + choices: ['azure', 'gcp', 'nfs', 'nfs-flashblade', 's3-aws', 's3-flashblade', 's3-scality-ring', 's3-wasabi-pay-as-you-go', 's3-wasabi-rcs', 's3-other'] extends_documentation_fragment: - purestorage.flasharray.purestorage.fa """ @@ -143,137 +151,129 @@ RETURN = r""" HAS_PURESTORAGE = True try: - from pypureclient import flasharray + from pypureclient.flasharray import ( + OffloadAzure, + OffloadGoogleCloud, + OffloadNfs, + OffloadPost, + OffloadS3, + ) 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, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) -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 +MULTIOFFLOAD_LIMIT = 1 +PROFILE_API_VERSION = "2.25" +NO_SNAP2NFS_VERSION = "2.27" def get_target(module, array): """Return target or None""" - try: - return array.get_offload(module.params["name"]) - except Exception: + res = array.get_offloads(names=[module.params["name"]]) + if res.status_code == 200: + return list(res.items)[0] + else: return None def create_offload(module, array): """Create offload target""" changed = True - api_version = array._list_available_rest_versions() + api_version = array.get_rest_version() # 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: + res = array.get_network_interfaces(names=["@offload.data0"]) + if res.status != 200: + module.fail_json(msg="Offload Network interface doesn't exist. Please resolve.") + if not list(res.items)[0].enabled: 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"], + if module.params["protocol"] == "gcp": + if PROFILE_API_VERSION in api_version and module.params["profile"]: + bucket = OffloadGoogleCloud( + access_key_id=module.params["access_key"], + bucket=module.params["bucket"], + secret_access_key=module.params["secret"], + profile=module.params["profile"], ) - except Exception: - module.fail_json( - msg="Failed to create NFS offload {0}. " - "Please perform diagnostic checks.".format(module.params["name"]) + else: + bucket = OffloadGoogleCloud( + access_key_id=module.params["access_key"], + bucket=module.params["bucket"], + secret_access_key=module.params["secret"], + ) + offload = OffloadPost(google_cloud=bucket) + if module.params["protocol"] == "azure" and module.params["profile"]: + if PROFILE_API_VERSION in api_version: + bucket = OffloadAzure( + container_name=module.params["container"], + secret_access_key=module.params["secret"], + account_name=module.params[".bucket"], + profile=module.params["profile"], ) - 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"], + bucket = OffloadAzure( 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"]) + offload = OffloadPost(azure=bucket) + if module.params["protocol"] == "s3" and module.params["profile"]: + if PROFILE_API_VERSION in api_version: + bucket = OffloadS3( + access_key_id=module.params["access_key"], + bucket=module.params["bucket"], + secret_access_key=module.params["secret"], + profile=module.params["profile"], ) - 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 - ) + else: + bucket = OffloadS3( + access_key_id=module.params["access_key"], + bucket=module.params["bucket"], + secret_access_key=module.params["secret"], ) + offload = OffloadPost(s3=bucket) + if module.params["protocol"] == "nfs" and module.params["profile"]: + if PROFILE_API_VERSION in api_version: + bucket = OffloadNfs( + mount_point=module.params["share"], + address=module.params["address"], + mount_options=module.params["options"], + profile=module.params["profile"], + ) + else: + bucket = OffloadNfs( + mount_point=module.params["share"], + address=module.params["address"], + mount_options=module.params["options"], + ) + offload = OffloadPost(nfs=bucket) + res = array.post_offloads( + offload=offload, + initialize=module.params["initialize"], + names=[module.params["name"]], + ) + if res.status_code != 200: + module.fail_json( + msg="Failed to create {0} offload {1}. Error: {2}" + "Please perform diagnostic checks.".format( + module.params["protocol"].upper(), + module.params["name"], + res.errors[0].message, + ) + ) module.exit_json(changed=changed) @@ -286,33 +286,14 @@ def update_offload(module, array): 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"] - ) + res = array.delete_offloads(names=[module.params["name"]]) + if res.status_code != 200: + module.fail_json( + msg="Failed to delete offload {0}. Error: {1}".format( + module.params["name"], res.errors[0].message ) + ) module.exit_json(changed=changed) @@ -329,6 +310,21 @@ def main(): default="retention-based", choices=["retention-based", "aws-standard-class"], ), + profile=dict( + type="str", + choices=[ + "azure", + "gcp", + "nfs", + "nfs-flashblade", + "s3-aws", + "s3-flashblade", + "s3-scality-ring", + "s3-wasabi-pay-as-you-go", + "s3-wasabi-rcs", + "s3-other", + ], + ), name=dict(type="str", required=True), initialize=dict(default=True, type="bool"), access_key=dict(type="str", no_log=False), @@ -356,19 +352,47 @@ def main(): 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": + 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() + array = get_array(module) + api_version = array.get_rest_version() - if MIN_REQUIRED_API_VERSION not in api_version: + if ( + LooseVersion(NO_SNAP2NFS_VERSION) <= LooseVersion(api_version) + and module.params["protocol"] == "nfs" + ): module.fail_json( - msg="FlashArray REST version not supported. " - "Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION) + msg="NFS offload target is not supported from Purity//FA 6.6.0 and higher" + ) + if ( + ( + module.params["protocol"].lower() == "azure" + and module.params["profile"] != "azure" + ) + or ( + module.params["protocol"].lower() == "gcp" + and module.params["profile"] != "gcp" ) + or ( + module.params["protocol"].lower() == "nfs" + and module.params["profile"] not in ["nfs", "nfs-flashblade"] + ) + or ( + module.params["protocol"].lower() == "s3" + and module.params["profile"] + not in [ + "s3-aws", + "s3-flashblade", + "s3-scality-ring", + "s3-wasabi-pay-as-you-go", + "s3-wasabi-rcs", + "s3-other", + ] + ) + ): + module.warn("Specified profile not valid, ignoring...") + module.params["profile"] = None if ( not re.match(r"^[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9]$", module.params["name"]) @@ -391,45 +415,29 @@ def main(): "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: + res = array.get_apps(names=["offload"]) + if res.status_code != 200: module.fail_json( msg="Correct Offload app not installed or incorrectly configured" ) else: - if version.parse(array.get()["version"]) != version.parse(app_version): + app_state = list(res.items)[0] + if LooseVersion(app_state.version) != LooseVersion(array.get_rest_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: + offloads = list(array.get_offloads().items) + if len(offloads) >= 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.") + 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) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_pg.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_pg.py index 3fa51ebbb..3344c0895 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_pg.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_pg.py @@ -374,68 +374,65 @@ def make_pgroup(module, array): module.fail_json( msg="Creation of pgroup {0} failed.".format(module.params["name"]) ) + if not module.check_mode: + 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: - 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"] - ) + array.set_pgroup(module.params["name"], vollist=module.params["volume"]) 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"] + msg="Adding volumes to pgroup {0} failed.".format( + module.params["name"] ) - except Exception: - module.fail_json( - msg="Adding hostgroups 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["safe_mode"]: - arrayv6 = get_array(module) - try: - arrayv6.patch_protection_groups( - names=[module.params["name"]], - protection_group=flasharray.ProtectionGroup( - retention_lock="ratcheted" - ), + ) + 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"] ) - except Exception: - module.fail_json( - msg="Failed to set SafeMode on pgroup {0}".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) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_pgsched.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_pgsched.py index dc0a488d4..c3ebcb09f 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_pgsched.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_pgsched.py @@ -48,8 +48,9 @@ options: default: true replicate_at: description: - - Specifies the preferred time as HH:MM:SS, using 24-hour clock, at which to generate snapshots. - type: int + - Provide a time in 12-hour AM/PM format, eg. 11AM + - Only valid if I(replicate_frequency) is an exact multiple of 86400, ie 1 day. + type: str blackout_start: description: - Specifies the time at which to suspend replication. @@ -68,9 +69,9 @@ options: type: int snap_at: description: - - Specifies the preferred time as HH:MM:SS, using 24-hour clock, at which to generate snapshots. + - Provide a time in 12-hour AM/PM format, eg. 11AM - Only valid if I(snap_frequency) is an exact multiple of 86400, ie 1 day. - type: int + type: str snap_frequency: description: - Specifies the snapshot frequency in seconds. @@ -120,7 +121,7 @@ EXAMPLES = r""" schedule: snapshot enabled: true snap_frequency: 86400 - snap_at: 15:30:00 + snap_at: 3PM per_day: 5 all_for: 5 fa_url: 10.10.10.2 @@ -132,7 +133,7 @@ EXAMPLES = r""" schedule: replication enabled: true replicate_frequency: 86400 - replicate_at: 15:30:00 + replicate_at: 3PM target_per_day: 5 target_all_for: 5 blackout_start: 2AM @@ -217,7 +218,7 @@ def _convert_to_minutes(hour): return (int(hour[:-2]) + 12) * 3600 -def update_schedule(module, array): +def update_schedule(module, array, snap_time, repl_time): """Update Protection Group Schedule""" changed = False try: @@ -260,10 +261,15 @@ def update_schedule(module, array): else: snap_frequency = module.params["snap_frequency"] + if module.params["enabled"] is None: + snap_enabled = current_snap["snap_enabled"] + else: + snap_enabled = module.params["enabled"] + if not module.params["snap_at"]: snap_at = current_snap["snap_at"] else: - snap_at = module.params["snap_at"] + snap_at = _convert_to_minutes(module.params["snap_at"].upper()) if not module.params["days"]: if isinstance(module.params["days"], int): @@ -294,11 +300,12 @@ def update_schedule(module, array): new_snap = { "days": days, "snap_frequency": snap_frequency, - "snap_enabled": module.params["enabled"], + "snap_enabled": snap_enabled, "snap_at": snap_at, "per_day": per_day, "all_for": all_for, } + module.warn("current {0}; new: {1}".format(current_snap, new_snap)) if current_snap != new_snap: changed = True if not module.check_mode: @@ -306,11 +313,17 @@ def update_schedule(module, array): 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, - ) + if snap_time: + array.set_pgroup( + module.params["name"], + snap_frequency=snap_frequency, + snap_at=snap_at, + ) + else: + array.set_pgroup( + module.params["name"], + snap_frequency=snap_frequency, + ) array.set_pgroup( module.params["name"], days=days, @@ -343,10 +356,15 @@ def update_schedule(module, array): else: replicate_frequency = module.params["replicate_frequency"] + if module.params["enabled"] is None: + replicate_enabled = current_repl["replicate_enabled"] + else: + replicate_enabled = module.params["enabled"] + if not module.params["replicate_at"]: replicate_at = current_repl["replicate_at"] else: - replicate_at = module.params["replicate_at"] + replicate_at = _convert_to_minutes(module.params["replicate_at"].upper()) if not module.params["target_days"]: if isinstance(module.params["target_days"], int): @@ -380,15 +398,17 @@ def update_schedule(module, array): if not module.params["blackout_end"]: blackout_end = current_repl["blackout_start"] else: - blackout_end = _convert_to_minutes(module.params["blackout_end"]) + blackout_end = _convert_to_minutes(module.params["blackout_end"].upper()) if not module.params["blackout_start"]: blackout_start = current_repl["blackout_start"] else: - blackout_start = _convert_to_minutes(module.params["blackout_start"]) + blackout_start = _convert_to_minutes( + module.params["blackout_start"].upper() + ) new_repl = { "replicate_frequency": replicate_frequency, - "replicate_enabled": module.params["enabled"], + "replicate_enabled": replicate_enabled, "target_days": target_days, "replicate_at": replicate_at, "target_per_day": target_per_day, @@ -405,11 +425,17 @@ def update_schedule(module, array): module.params["name"], replicate_enabled=module.params["enabled"], ) - array.set_pgroup( - module.params["name"], - replicate_frequency=replicate_frequency, - replicate_at=replicate_at, - ) + if repl_time: + array.set_pgroup( + module.params["name"], + replicate_frequency=replicate_frequency, + replicate_at=replicate_at, + ) + else: + array.set_pgroup( + module.params["name"], + replicate_frequency=replicate_frequency, + ) if blackout_start == 0: array.set_pgroup(module.params["name"], replicate_blackout=None) else: @@ -482,8 +508,8 @@ def main(): ), blackout_start=dict(type="str"), blackout_end=dict(type="str"), - snap_at=dict(type="int"), - replicate_at=dict(type="int"), + snap_at=dict(type="str"), + replicate_at=dict(type="str"), replicate_frequency=dict(type="int"), snap_frequency=dict(type="int"), all_for=dict(type="int"), @@ -506,13 +532,22 @@ def main(): array = get_system(module) pgroup = get_pgroup(module, array) + repl_time = False + if module.params["replicate_at"] and module.params["replicate_frequency"]: + if not module.params["replicate_frequency"] % 86400 == 0: + module.fail_json( + msg="replicate_at not valid unless replicate frequency is measured in days, ie. a multiple of 86400" + ) + repl_time = True + snap_time = False 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" ) + snap_time = True if pgroup and state == "present": - update_schedule(module, array) + update_schedule(module, array, snap_time, repl_time) elif pgroup and state == "absent": delete_schedule(module, array) elif pgroup is None: diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_pgsnap.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_pgsnap.py index 822b0491f..4f3f6f16c 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_pgsnap.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_pgsnap.py @@ -42,7 +42,7 @@ options: Copy (added in 2.7) will create a full read/write clone of the snapshot. type: str - choices: [ absent, present, copy ] + choices: [ absent, present, copy, rename ] default: present eradicate: description: @@ -64,6 +64,7 @@ options: description: - Volume to restore a specified volume to. - If not supplied this will default to the volume defined in I(restore) + - Name of new snapshot suffix if renaming a snapshot type: str offload: description: @@ -85,6 +86,27 @@ options: - Force immeadiate snapshot to remote targets type: bool default: false + throttle: + description: + - If set to true, allows snapshot to fail if array health is not optimal. + type: bool + default: false + version_added: '1.21.0' + with_default_protection: + description: + - Whether to add the default container protection groups to + those specified in I(add_to_pgs) as the inital protection + of a volume created from a snapshot. + type: bool + default: true + version_added: '1.27.0' + add_to_pgs: + description: + - A volume created from a snapshot will be added to the specified + protection groups + type: list + elements: str + version_added: '1.27.0' extends_documentation_fragment: - purestorage.flasharray.purestorage.fa """ @@ -114,6 +136,7 @@ EXAMPLES = r""" restore: data target: data2 overwrite: true + with_default_protection: false fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 state: copy @@ -127,7 +150,7 @@ EXAMPLES = r""" api_token: e31060a7-21fc-e277-6240-25983c6c4592 state: copy -- name: Restore AC pod protection group snapshot pod1::pgname.snap.data to pdo1::data2 +- name: Restore AC pod protection group snapshot pod1::pgname.snap.data to pod1::data2 purestorage.flasharray.purefa_pgsnap: name: pod1::pgname suffix: snap @@ -156,28 +179,53 @@ EXAMPLES = r""" fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 state: absent + +- name: Rename protection group snapshot foo.fred to foo.dave + purestorage.flasharray.purefa_pgsnap: + name: foo + suffix: fred + target: dave + fa_url: 10.10.10.2 + api_token: e31060a7-21fc-e277-6240-25983c6c4592 + state: rename """ RETURN = r""" """ +HAS_PURESTORAGE = True +try: + from pypureclient.flasharray import ( + ProtectionGroupSnapshot, + ProtectionGroupSnapshotPatch, + VolumePost, + Reference, + FixedReference, + DestroyedPatchPost, + ) +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, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) from datetime import datetime -OFFLOAD_API = "1.16" -POD_SNAPSHOT = "2.4" +THROTTLE_API = "2.25" +DEFAULT_API = "2.16" def _check_offload(module, array): try: - offload = array.get_offload(module.params["offload"]) - if offload["status"] == "connected": + offload = list(array.get_offloads(names=[module.params["offload"]]).items)[0] + if offload.status == "connected": return True return False except Exception: @@ -187,7 +235,7 @@ def _check_offload(module, array): def get_pgroup(module, array): """Return Protection Group or None""" try: - return array.get_pgroup(module.params["name"]) + return list(array.get_protection_groups(names=[module.params["name"]]).items)[0] except Exception: return None @@ -195,14 +243,71 @@ def get_pgroup(module, array): def get_pgroupvolume(module, array): """Return Protection Group Volume or None""" try: - pgroup = array.get_pgroup(module.params["name"]) + volumes = [] + pgroup = list(array.get_protection_groups(names=[module.params["name"]]).items)[ + 0 + ] + if pgroup.host_count > 0: # We have a host PG + host_dict = list( + array.get_protection_groups_hosts( + group_names=[module.params["name"]] + ).items + ) + for host in range(0, len(host_dict)): + hostvols = list( + array.get_connections( + host_names=[host_dict[host].member["name"]] + ).items + ) + for hvol in range(0, len(hostvols)): + volumes.append(hostvols[hvol].volume["name"]) + elif pgroup.host_group_count > 0: # We have a hostgroup PG + hgroup_dict = list( + array.get_protection_groups_host_groups( + group_names=[module.params["name"]] + ).items + ) + hgroups = [] + # First check if there are any volumes in the host groups + for hgentry in range(0, len(hgroup_dict)): + hgvols = list( + array.get_connections( + host_group_names=[hgroup_dict[hgentry].member["name"]] + ).items + ) + for hgvol in range(0, len(hgvols)): + volumes.append(hgvols[hgvol].volume["name"]) + # Second check for host specific volumes + for hgroup in range(0, len(hgroup_dict)): + hg_hosts = list( + array.get_host_groups_hosts( + group_names=[hgroup_dict[hgroup].member["name"]] + ).items + ) + for hg_host in range(0, len(hg_hosts)): + host_vols = list( + array.get_connections( + host_names=[hg_hosts[hg_host].member["name"]] + ).items + ) + for host_vol in range(0, len(host_vols)): + volumes.append(host_vols[host_vol].volume["name"]) + else: # We have a volume PG + vol_dict = list( + array.get_protection_groups_volumes( + group_names=[module.params["name"]] + ).items + ) + for entry in range(0, len(vol_dict)): + volumes.append(vol_dict[entry].member["name"]) + volumes = list(set(volumes)) 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"]: + for volume in volumes: if volume == restore_volume: return volume except Exception: @@ -210,7 +315,7 @@ def get_pgroupvolume(module, array): def get_rpgsnapshot(module, array): - """Return iReplicated Snapshot or None""" + """Return Replicated Snapshot or None""" try: snapname = ( module.params["name"] @@ -219,83 +324,103 @@ def get_rpgsnapshot(module, array): + "." + 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: + array.get_volume_snapshots(names=[snapname]) + return snapname + except AttributeError: 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: + snapname = module.params["name"] + "." + module.params["suffix"] + res = array.get_protection_group_snapshots(names=[snapname]) + if res.status_code == 200: + return list(res.items)[0] + else: return None def create_pgsnapshot(module, array): """Create Protection Group Snapshot""" + api_version = array.get_rest_version() changed = True if not module.check_mode: - try: + suffix = ProtectionGroupSnapshot(suffix=module.params["suffix"]) + if LooseVersion(THROTTLE_API) >= LooseVersion(api_version): if ( - module.params["now"] - and array.get_pgroup(module.params["name"])["targets"] is not None + list(array.get_protection_groups(names=[module.params["name"]]).items)[ + 0 + ].target_count + > 0 ): - array.create_pgroup_snapshot( - source=module.params["name"], - suffix=module.params["suffix"], - snap=True, + if module.params["now"]: + res = array.post_protection_group_snapshots( + source_names=[module.params["name"]], + apply_retention=module.params["apply_retention"], + replicate_now=True, + protection_group_snapshot=suffix, + ) + else: + res = array.post_protection_group_snapshots( + source_names=[module.params["name"]], + apply_retention=module.params["apply_retention"], + protection_group_snapshot=suffix, + replicate=module.params["remote"], + ) + else: + res = array.post_protection_group_snapshots( + source_names=[module.params["name"]], apply_retention=module.params["apply_retention"], - replicate_now=module.params["remote"], + protection_group_snapshot=suffix, ) + else: + if ( + list(array.get_protection_groups(names=[module.params["name"]]).items)[ + 0 + ].target_count + > 0 + ): + if module.params["now"]: + res = array.post_protection_group_snapshots( + source_names=[module.params["name"]], + apply_retention=module.params["apply_retention"], + replicate_now=True, + allow_throttle=module.params["throttle"], + protection_group_snapshot=suffix, + ) + else: + res = array.post_protection_group_snapshots( + source_names=[module.params["name"]], + apply_retention=module.params["apply_retention"], + allow_throttle=module.params["throttle"], + protection_group_snapshot=suffix, + replicate=module.params["remote"], + ) else: - array.create_pgroup_snapshot( - source=module.params["name"], - suffix=module.params["suffix"], - snap=True, + res = array.post_protection_group_snapshots( + source_names=[module.params["name"]], apply_retention=module.params["apply_retention"], + allow_throttle=module.params["throttle"], + protection_group_snapshot=suffix, ) - except Exception: + + if res.status_code != 200: module.fail_json( - msg="Snapshot of pgroup {0} failed.".format(module.params["name"]) + msg="Snapshot of pgroup {0} failed. Error: {1}".format( + module.params["name"], res.errors[0].message + ) ) 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.") + latest_snapshot = list( + array.get_protection_group_snapshots(names=[module.params["name"]]).items + )[-1].suffix + module.params["suffix"] = latest_snapshot if ":" in module.params["name"] and "::" not in module.params["name"]: if get_rpgsnapshot(module, array) is None: module.fail_json( @@ -310,7 +435,7 @@ def restore_pgsnapvolume(module, array): module.params["restore"] ) ) - volume = ( + source_volume = ( module.params["name"] + "." + module.params["suffix"] @@ -324,20 +449,49 @@ def restore_pgsnapvolume(module, array): 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 - ): + if list(array.get_pods(names=[target_pod_name]).items)[0].array_count > 1: 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"] + if LooseVersion(DEFAULT_API) <= LooseVersion(array.get_rest_version()): + if module.params["add_to_pgs"]: + add_to_pgs = [] + for add_pg in range(0, len(module.params["add_to_pgs"])): + add_to_pgs.append( + FixedReference(name=module.params["add_to_pgs"][add_pg]) + ) + res = array.post_volumes( + names=[module.params["target"]], + volume=VolumePost(source=Reference(name=source_volume)), + with_default_protection=module.params["with_default_protection"], + add_to_protection_group_names=add_to_pgs, + ) + else: + if module.params["overwrite"]: + res = array.post_volumes( + names=[module.params["target"]], + volume=VolumePost(source=Reference(name=source_volume)), + overwrite=module.params["overwrite"], + ) + else: + res = array.post_volumes( + names=[module.params["target"]], + volume=VolumePost(source=Reference(name=source_volume)), + with_default_protection=module.params[ + "with_default_protection" + ], + ) + else: + res = array.post_volumes( + names=[module.params["target"]], + overwrite=module.params["overwrite"], + volume=VolumePost(source=Reference(name=source_volume)), ) - except Exception: + if res.status_code != 200: module.fail_json( - msg="Failed to restore {0} from pgroup {1}".format( - volume, module.params["name"] + msg="Failed to restore {0} from pgroup {1}. Error: {2}".format( + module.params["restore"], + module.params["name"], + res.errors[0].message, ) ) module.exit_json(changed=changed) @@ -349,23 +503,61 @@ def delete_offload_snapshot(module, array): snapname = module.params["name"] + "." + module.params["suffix"] if ":" in module.params["name"] and module.params["offload"]: if _check_offload(module, array): - changed = True + res = array.get_remote_protection_group_snapshots( + names=[snapname], on=module.params["offload"] + ) + if res.status_code != 200: + module.fail_json( + msg="Offload snapshot {0} does not exist on {1}".format( + snapname, module.params["offload"] + ) + ) + + rpg_destroyed = list(res.items)[0].destroyed if not module.check_mode: - try: - array.destroy_pgroup(snapname, on=module.params["offload"]) + if not rpg_destroyed: + changed = True + res = array.patch_remote_protection_group_snapshots( + names=[snapname], + on=module.params["offload"], + remote_protection_group_snapshot=DestroyedPatchPost( + destroyed=True + ), + ) + if res.status_code != 200: + module.fail_json( + msg="Failed to delete offloaded snapshot {0} on target {1}. Error: {2}".format( + snapname, + module.params["offload"], + res.errors[0].message, + ) + ) if module.params["eradicate"]: - try: - array.eradicate_pgroup( - snapname, on=module.params["offload"] + res = array.delete_remote_protection_group_snapshots( + names=[snapname], on=module.params["offload"] + ) + if res.status_code != 200: + module.fail_json( + msg="Failed to eradicate offloaded snapshot {0} on target {1}. Error: {2}".format( + snapname, + module.params["offload"], + res.errors[0].message, + ) ) - except Exception: + else: + if module.params["eradicate"]: + changed = True + res = array.delete_remote_protection_group_snapshots( + names=[snapname], on=module.params["offload"] + ) + if res.status_code != 200: module.fail_json( - msg="Failed to eradicate offloaded snapshot {0} on target {1}".format( - snapname, module.params["offload"] + msg="Failed to eradicate offloaded snapshot {0} on target {1}. Error: {2}".format( + snapname, + module.params["offload"], + res.errors[0].message, ) ) - except Exception: - pass else: module.fail_json( msg="Offload target {0} does not exist or not connected".format( @@ -383,17 +575,58 @@ def delete_pgsnapshot(module, array): 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) + res = array.patch_protection_group_snapshots( + names=[snapname], + protection_group_snapshot=ProtectionGroupSnapshotPatch(destroyed=True), + ) + if res.status_code != 200: + module.fail_json( + msg="Failed to delete pgroup {0}. Error {1}".format( + snapname, res.errors[0].message + ) + ) + if module.params["eradicate"]: + res = array.delete_protection_group_snapshots(names=[snapname]) + if res.status_code != 200: + module.fail_json( + msg="Failed to delete pgroup {0}. Error {1}".format( + snapname, res.errors[0].message ) - except Exception: - module.fail_json(msg="Failed to delete pgroup {0}".format(snapname)) + ) + module.exit_json(changed=changed) + + +def eradicate_pgsnapshot(module, array): + """Eradicate Protection Group Snapshot""" + changed = True + if not module.check_mode: + snapname = module.params["name"] + "." + module.params["suffix"] + res = array.delete_protection_group_snapshots(names=[snapname]) + if res.status_code != 200: + module.fail_json( + msg="Failed to delete pgroup {0}. Error {1}".format( + snapname, res.errors[0].message + ) + ) + module.exit_json(changed=changed) + + +def update_pgsnapshot(module, array): + """Update Protection Group Snapshot - basically just rename...""" + changed = True + if not module.check_mode: + current_name = module.params["name"] + "." + module.params["suffix"] + new_name = module.params["name"] + "." + module.params["target"] + res = array.patch_protection_group_snapshots( + names=[current_name], + protection_group_snapshot=ProtectionGroupSnapshotPatch(name=new_name), + ) + if res.status_code != 200: + module.fail_json( + msg="Failed to rename {0} to {1}. Error: {2}".format( + current_name, new_name, res.errors[0].message + ) + ) module.exit_json(changed=changed) @@ -405,6 +638,7 @@ def main(): suffix=dict(type="str"), restore=dict(type="str"), offload=dict(type="str"), + throttle=dict(type="bool", default=False), overwrite=dict(type="bool", default=False), target=dict(type="str"), eradicate=dict(type="bool", default=False), @@ -412,18 +646,26 @@ def main(): apply_retention=dict(type="bool", default=False), remote=dict(type="bool", default=False), state=dict( - type="str", default="present", choices=["absent", "present", "copy"] + type="str", + default="present", + choices=["absent", "present", "copy", "rename"], ), + with_default_protection=dict(type="bool", default=True), + add_to_pgs=dict(type="list", elements="str"), ) ) required_if = [("state", "copy", ["suffix", "restore"])] + mutually_exclusive = [["now", "remote"]] module = AnsibleModule( - argument_spec, required_if=required_if, supports_check_mode=True + argument_spec, + required_if=required_if, + mutually_exclusive=mutually_exclusive, + 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"] + pattern = re.compile("^(?=.*[a-zA-Z-])[a-zA-Z0-9]([a-zA-Z0-9-]{0,63}[a-zA-Z0-9])?$") if state == "present": if module.params["suffix"] is None: suffix = "snap-" + str( @@ -431,6 +673,10 @@ def main(): ) module.params["suffix"] = suffix.replace(".", "") else: + if module.params["restore"]: + pattern = re.compile( + "^[0-9]{0,63}$|^(?=.*[a-zA-Z-])[a-zA-Z0-9]([a-zA-Z0-9-]{0,63}[a-zA-Z0-9])?$" + ) if not pattern.match(module.params["suffix"]): module.fail_json( msg="Suffix name {0} does not conform to suffix name rules".format( @@ -441,11 +687,18 @@ def main(): 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) + if state == "rename" and module.params["target"] is not None: + if not pattern.match(module.params["target"]): + module.fail_json( + msg="Suffix target {0} does not conform to suffix name rules".format( + module.params["target"] + ) + ) + array = get_array(module) + api_version = array.get_rest_version() + if not HAS_PURESTORAGE and module.params["throttle"]: + module.warn( + "Throttle capability disable as py-pure-client sdk is not installed" ) pgroup = get_pgroup(module, array) if pgroup is None: @@ -453,24 +706,34 @@ def main(): msg="Protection Group {0} does not exist.".format(module.params["name"]) ) pgsnap = get_pgsnapshot(module, array) + if pgsnap: + pgsnap_deleted = pgsnap.destroyed if state != "absent" and module.params["offload"]: module.fail_json( msg="offload parameter not supported for state {0}".format(state) ) elif state == "copy": + if module.params["overwrite"] and ( + module.params["add_to_pgs"] or module.params["with_default_protection"] + ): + module.fail_json( + msg="overwrite and add_to_pgs or with_default_protection are incompatable" + ) 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) + state == "absent" and module.params["offload"] and get_pgsnapshot(module, array) ): delete_offload_snapshot(module, array) - elif state == "absent" and pgsnap: + elif state == "rename" and pgsnap: + update_pgsnapshot(module, array) + elif state == "absent" and pgsnap and not pgsnap_deleted: delete_pgsnapshot(module, array) + elif state == "absent" and pgsnap and pgsnap_deleted and module.params["eradicate"]: + eradicate_pgsnapshot(module, array) elif state == "absent" and not pgsnap: module.exit_json(changed=False) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_pod.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_pod.py index 75c4eb6c9..a41e346eb 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_pod.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_pod.py @@ -179,38 +179,15 @@ from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa impo get_array, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.common import ( + human_to_bytes, +) POD_API_VERSION = "1.13" POD_QUOTA_VERSION = "2.23" -def human_to_bytes(size): - """Given a human-readable byte string (e.g. 2G, 30M), - return the number of bytes. Will return 0 if the argument has - unexpected form. - """ - bytes = size[:-1] - unit = size[-1].upper() - if bytes.isdigit(): - bytes = int(bytes) - if unit == "P": - bytes *= 1125899906842624 - elif unit == "T": - bytes *= 1099511627776 - elif unit == "G": - bytes *= 1073741824 - elif unit == "M": - bytes *= 1048576 - elif unit == "K": - bytes *= 1024 - else: - bytes = 0 - else: - bytes = 0 - return bytes - - def get_pod(module, array): """Return Pod or None""" try: diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_policy.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_policy.py index 37017e4df..7247d376f 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_policy.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_policy.py @@ -38,7 +38,7 @@ options: policy: description: - The type of policy to use - choices: [ nfs, smb, snapshot, quota ] + choices: [ nfs, smb, snapshot, quota, autodir ] required: true type: str enabled: @@ -73,10 +73,18 @@ options: choices: [ ro, rw ] default: rw type: str + nfs_version: + description: + - NFS protocol version allowed for the export + type: list + elements: str + choices: [ nfsv3, nfsv4 ] + version_added: "1.22.0" user_mapping: description: - Defines if user mapping is enabled type: bool + default: true version_added: 1.14.0 snap_at: description: @@ -174,6 +182,21 @@ options: type: str default: "65534" version_added: 1.14.0 + security: + description: + - The security flavors to use for accessing files on a mount point. + - If the server does not support the requested flavor, the mount operation fails. + - This operation updates all rules of the specified policy. + type: list + elements: str + choices: [ auth_sys, krb5, krb5i, krb5p ] + version_added: 1.25.0 + access_based_enumeration: + description: + - Defines if access based enumeration for SMB is enabled + type: bool + default: false + version_added: 1.26.0 extends_documentation_fragment: - purestorage.flasharray.purestorage.fa """ @@ -332,60 +355,29 @@ try: except ImportError: HAS_PURESTORAGE = False -HAS_PACKAGING = True -try: - from packaging import version -except ImportError: - HAS_PACKAGING = 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, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) +from ansible_collections.purestorage.flasharray.plugins.module_utils.common import ( + human_to_bytes, + convert_to_millisecs, +) MIN_REQUIRED_API_VERSION = "2.3" MIN_QUOTA_API_VERSION = "2.7" MIN_SUFFIX_API_VERSION = "2.9" USER_MAP_VERSION = "2.15" ALL_SQUASH_VERSION = "2.16" - - -def _human_to_bytes(size): - """Given a human-readable byte string (e.g. 2G, 30M), - return the number of bytes. Will return 0 if the argument has - unexpected form. - """ - bytes = size[:-1] - unit = size[-1].upper() - if bytes.isdigit(): - bytes = int(bytes) - if unit == "P": - bytes *= 1125899906842624 - elif unit == "T": - bytes *= 1099511627776 - elif unit == "G": - bytes *= 1073741824 - elif unit == "M": - bytes *= 1048576 - elif unit == "K": - bytes *= 1024 - else: - bytes = 0 - else: - bytes = 0 - return bytes - - -def _convert_to_millisecs(hour): - if hour[-2:].upper() == "AM" and hour[:2] == "12": - return 0 - elif hour[-2:].upper() == "AM": - return int(hour[:-2]) * 3600000 - elif hour[-2:].upper() == "PM" and hour[:2] == "12": - return 43200000 - return (int(hour[:-2]) + 12) * 3600000 +AUTODIR_VERSION = "2.24" +NFS_VERSION = "2.26" +SECURITY_VERSION = "2.29" +ABE_VERSION = "2.4" def rename_policy(module, array): @@ -560,7 +552,7 @@ def delete_policy(module, array): if module.params["directory"][old_dir] in dirs: old_dirs.append(module.params["directory"][old_dir]) else: - old_dirs = module.params["directory"] + pass if old_dirs: changed = True for rem_dir in range(0, len(old_dirs)): @@ -607,9 +599,53 @@ def delete_policy(module, array): deleted.errors[0].message, ) ) - else: + elif module.params["policy"] == "autodir": + if not module.params["directory"]: + res = array.delete_policies_autodir(names=[module.params["name"]]) + if res.status_code == 200: + changed = True + else: + module.fail_json( + msg="Deletion of Autodir policy {0} failed. Error: {1}".format( + module.params["name"], res.errors[0].message + ) + ) + if module.params["directory"]: + dirs = [] + old_dirs = [] + current_dirs = list( + array.get_directories_policies_autodir( + policy_names=[module.params["name"]] + ).items + ) + if current_dirs: + for current_dir in range(0, len(current_dirs)): + dirs.append(current_dirs[current_dir].member.name) + for old_dir in range(0, len(module.params["directory"])): + if module.params["directory"][old_dir] in dirs: + old_dirs.append(module.params["directory"][old_dir]) + else: + pass + if old_dirs: + changed = True + for rem_dir in range(0, len(old_dirs)): + if not module.check_mode: + directory_removed = ( + array.delete_directories_policies_autodir( + member_names=[old_dirs[rem_dir]], + policy_names=module.params["name"], + ) + ) + if directory_removed.status_code != 200: + module.fail_json( + msg="Failed to remove directory from Autodir policy {0}. Error: {1}".format( + module.params["name"], + directory_removed.errors[0].message, + ) + ) + else: # quota if module.params["quota_limit"]: - quota_limit = _human_to_bytes(module.params["quota_limit"]) + quota_limit = human_to_bytes(module.params["quota_limit"]) rules = list( array.get_policies_quota_rules( policy_names=[module.params["name"]] @@ -704,18 +740,7 @@ def create_policy(module, array, all_squash): ) if created.status_code == 200: - policy = flasharray.PolicyNfsPost( - user_mapping_enabled=module.params["user_mapping"], - ) - res = array.patch_policies_nfs( - names=[module.params["name"]], policy=policy - ) - if res.status_code != 200: - module.fail_json( - msg="Failed to set NFS policy {0}. Error: {1}".format( - module.params["name"], res.errors[0].message - ) - ) + changed = True if module.params["client"]: if all_squash: rules = flasharray.PolicyrulenfsclientpostRules( @@ -741,7 +766,52 @@ def create_policy(module, array, all_squash): module.params["name"], rule_created.errors[0].message ) ) - changed = True + policy = flasharray.PolicyNfsPatch( + user_mapping_enabled=module.params["user_mapping"], + ) + res = array.patch_policies_nfs( + names=[module.params["name"]], policy=policy + ) + if res.status_code != 200: + module.fail_json( + msg="Failed to set NFS policy user_mapping {0}. Error: {1}".format( + module.params["name"], res.errors[0].message + ) + ) + if ( + LooseVersion(array.get_rest_version()) >= LooseVersion(NFS_VERSION) + and module.params["client"] + and module.params["nfs_version"] + ): + policy = flasharray.PolicyNfsPatch( + nfs_version=module.params["nfs_version"], + ) + res = array.patch_policies_nfs( + names=[module.params["name"]], policy=policy + ) + if res.status_code != 200: + module.fail_json( + msg="Failed to set NFS policy version {0}. Error: {1}".format( + module.params["name"], res.errors[0].message + ) + ) + if ( + LooseVersion(array.get_rest_version()) + >= LooseVersion(SECURITY_VERSION) + and module.params["security"] + ): + policy = flasharray.PolicyNfsPatch( + security=module.params["security"], + ) + res = array.patch_policies_nfs( + names=[module.params["name"]], policy=policy + ) + if res.status_code != 200: + module.fail_json( + msg="Failed to set NFS policy security {0}. Error: {1}".format( + module.params["name"], res.errors[0].message + ) + ) else: module.fail_json( msg="Failed to create NFS policy {0}. Error: {1}".format( @@ -754,7 +824,21 @@ def create_policy(module, array, all_squash): policy=flasharray.PolicyPost(enabled=module.params["enabled"]), ) if created.status_code == 200: - changed = True + if LooseVersion(ABE_VERSION) <= LooseVersion(array.get_rest_version()): + res = array.patch_policies_smb( + names=[module.params["name"]], + policy=flasharray.PolicySmbPatch( + access_based_enumeration_enabled=module.params[ + "access_based_enumeration" + ] + ), + ) + if res.status_code != 200: + module.fail_json( + msg="Failed to set SMB policy {0}. Error: {1}".format( + module.params["name"], res.errors[0].message + ) + ) if module.params["client"]: rules = flasharray.PolicyrulesmbclientpostRules( anonymous_access_allowed=module.params["smb_anon_allowed"], @@ -771,6 +855,7 @@ def create_policy(module, array, all_squash): module.params["name"], rule_created.errors[0].message ) ) + changed = True else: module.fail_json( msg="Failed to create SMB policy {0}. Error: {1}".format( @@ -778,12 +863,10 @@ def create_policy(module, array, all_squash): ) ) elif module.params["policy"] == "snapshot": - if HAS_PACKAGING: - suffix_enabled = version.parse( - array.get_rest_version() - ) >= version.parse(MIN_SUFFIX_API_VERSION) - else: - suffix_enabled = False + suffix_enabled = bool( + LooseVersion(array.get_rest_version()) + >= LooseVersion(MIN_SUFFIX_API_VERSION) + ) created = array.post_policies_snapshot( names=[module.params["name"]], policy=flasharray.PolicyPost(enabled=module.params["enabled"]), @@ -802,7 +885,7 @@ def create_policy(module, array, all_squash): ) if suffix_enabled: rules = flasharray.PolicyrulesnapshotpostRules( - at=_convert_to_millisecs(module.params["snap_at"]), + at=convert_to_millisecs(module.params["snap_at"]), client_name=module.params["snap_client_name"], every=module.params["snap_every"] * 60000, keep_for=module.params["snap_keep_for"] * 60000, @@ -810,7 +893,7 @@ def create_policy(module, array, all_squash): ) else: rules = flasharray.PolicyrulesnapshotpostRules( - at=_convert_to_millisecs(module.params["snap_at"]), + at=convert_to_millisecs(module.params["snap_at"]), client_name=module.params["snap_client_name"], every=module.params["snap_every"] * 60000, keep_for=module.params["snap_keep_for"] * 60000, @@ -863,7 +946,38 @@ def create_policy(module, array, all_squash): module.params["name"], created.errors[0].message ) ) - else: + elif module.params["policy"] == "autodir": + created = array.post_policies_autodir( + names=[module.params["name"]], + policy=flasharray.PolicyPost(enabled=module.params["enabled"]), + ) + if created.status_code == 200: + changed = True + if module.params["directory"]: + policies = flasharray.DirectoryPolicyPost( + policies=[ + flasharray.DirectorypolicypostPolicies( + policy=flasharray.Reference(name=module.params["name"]) + ) + ] + ) + directory_added = array.post_directories_policies_autodir( + member_names=module.params["directory"], policies=policies + ) + if directory_added.status_code != 200: + module.fail_json( + msg="Failed to add directory for Autodir policy {0}. Error: {1}".format( + module.params["name"], + directory_added.errors[0].message, + ) + ) + else: + module.fail_json( + msg="Failed to create Autodir policy {0}. Error: {1}".format( + module.params["name"], created.errors[0].message + ) + ) + else: # quota created = array.post_policies_quota( names=[module.params["name"]], policy=flasharray.PolicyPost(enabled=module.params["enabled"]), @@ -871,7 +985,7 @@ def create_policy(module, array, all_squash): if created.status_code == 200: changed = True if module.params["quota_limit"]: - quota = _human_to_bytes(module.params["quota_limit"]) + quota = human_to_bytes(module.params["quota_limit"]) rules = flasharray.PolicyrulequotapostRules( enforced=module.params["quota_enforced"], quota_limit=quota, @@ -924,16 +1038,15 @@ def create_policy(module, array, all_squash): def update_policy(module, array, api_version, all_squash): """Update an existing policy including add/remove rules""" - changed = ( - changed_dir - ) = ( - changed_rule - ) = changed_enable = changed_quota = changed_member = changed_user_map = False + changed = changed_dir = changed_rule = changed_enable = changed_quota = ( + changed_member + ) = changed_user_map = changed_abe = changed_nfs = False if module.params["policy"] == "nfs": + current_policy = list( + array.get_policies_nfs(names=[module.params["name"]]).items + )[0] try: - current_enabled = list( - array.get_policies_nfs(names=[module.params["name"]]).items - )[0].enabled + current_enabled = current_policy.enabled if USER_MAP_VERSION in api_version: current_user_map = list( array.get_policies_nfs(names=[module.params["name"]]).items @@ -944,6 +1057,23 @@ def update_policy(module, array, api_version, all_squash): module.params["name"] ) ) + if module.params["nfs_version"] and sorted( + module.params["nfs_version"] + ) != sorted(getattr(current_policy, "nfs_version", [])): + changed_nfs = True + if not module.check_mode: + res = array.patch_policies_nfs( + names=[module.params["name"]], + policy=flasharray.PolicyNfsPatch( + nfs_version=module.params["nfs_version"] + ), + ) + if res.status_code != 200: + module.exit_json( + msg="Failed to change NFS version for NFS policy {0}".format( + module.params["name"] + ) + ) if ( module.params["user_mapping"] and current_user_map != module.params["user_mapping"] @@ -988,20 +1118,79 @@ def update_policy(module, array, api_version, all_squash): rule_name = rules[rule].name break if not rule_name: - if all_squash: - rules = flasharray.PolicyrulenfsclientpostRules( - permission=module.params["nfs_permission"], - client=module.params["client"], - anongid=module.params["anongid"], - anonuid=module.params["anonuid"], - access=module.params["nfs_access"], - ) + if LooseVersion(NFS_VERSION) > LooseVersion( + array.get_rest_version() + ): + if all_squash: + rules = flasharray.PolicyrulenfsclientpostRules( + permission=module.params["nfs_permission"], + client=module.params["client"], + anongid=module.params["anongid"], + anonuid=module.params["anonuid"], + access=module.params["nfs_access"], + ) + else: + rules = flasharray.PolicyrulenfsclientpostRules( + permission=module.params["nfs_permission"], + client=module.params["client"], + access=module.params["nfs_access"], + nfs_version=module.params["nfs_version"], + ) + elif ( + LooseVersion(SECURITY_VERSION) + > LooseVersion(array.get_rest_version()) + <= LooseVersion(NFS_VERSION) + ): + if all_squash: + rules = flasharray.PolicyrulenfsclientpostRules( + permission=module.params["nfs_permission"], + client=module.params["client"], + anongid=module.params["anongid"], + anonuid=module.params["anonuid"], + access=module.params["nfs_access"], + nfs_version=module.params["nfs_version"], + ) + else: + rules = flasharray.PolicyrulenfsclientpostRules( + permission=module.params["nfs_permission"], + client=module.params["client"], + access=module.params["nfs_access"], + ) else: - rules = flasharray.PolicyrulenfsclientpostRules( - permission=module.params["nfs_permission"], - client=module.params["client"], - access=module.params["nfs_access"], - ) + if module.params["security"]: + if all_squash: + rules = flasharray.PolicyrulenfsclientpostRules( + permission=module.params["nfs_permission"], + client=module.params["client"], + anongid=module.params["anongid"], + anonuid=module.params["anonuid"], + access=module.params["nfs_access"], + nfs_version=module.params["nfs_version"], + security=module.params["security"], + ) + else: + rules = flasharray.PolicyrulenfsclientpostRules( + permission=module.params["nfs_permission"], + client=module.params["client"], + access=module.params["nfs_access"], + security=module.params["security"], + ) + else: + if all_squash: + rules = flasharray.PolicyrulenfsclientpostRules( + permission=module.params["nfs_permission"], + client=module.params["client"], + anongid=module.params["anongid"], + anonuid=module.params["anonuid"], + access=module.params["nfs_access"], + nfs_version=module.params["nfs_version"], + ) + else: + rules = flasharray.PolicyrulenfsclientpostRules( + permission=module.params["nfs_permission"], + client=module.params["client"], + access=module.params["nfs_access"], + ) rule = flasharray.PolicyRuleNfsClientPost(rules=[rules]) changed_rule = True if not module.check_mode: @@ -1044,15 +1233,38 @@ def update_policy(module, array, api_version, all_squash): ) elif module.params["policy"] == "smb": try: - current_enabled = list( - array.get_policies_smb(names=[module.params["name"]]).items - )[0].enabled + current = list(array.get_policies_smb(names=[module.params["name"]]).items)[ + 0 + ] + current_enabled = current.enabled + current_access_based_enumeration = current.access_based_enumeration_enabled except Exception: module.fail_json( msg="Incorrect policy type specified for existing policy {0}".format( module.params["name"] ) ) + if ( + "access_based_enumeration" in module.params + and current_access_based_enumeration + != module.params["access_based_enumeration"] + ): + changed_abe = True + if not module.check_mode: + res = array.patch_policies_smb( + names=[module.params["name"]], + policy=flasharray.PolicySmbPatch( + access_based_enumeration_enabled=module.params[ + "access_based_enumeration" + ] + ), + ) + if res.status_code != 200: + module.exit_json( + msg="Failed to enable/disable Access based enueration for SMB policy {0}".format( + module.params["name"] + ) + ) if current_enabled != module.params["enabled"]: changed_enable = True if not module.check_mode: @@ -1116,12 +1328,10 @@ def update_policy(module, array, api_version, all_squash): ) ) elif module.params["policy"] == "snapshot": - if HAS_PACKAGING: - suffix_enabled = version.parse(array.get_rest_version()) >= version.parse( - MIN_SUFFIX_API_VERSION - ) - else: - suffix_enabled = False + suffix_enabled = bool( + LooseVersion(array.get_rest_version()) + >= LooseVersion(MIN_SUFFIX_API_VERSION) + ) try: current_enabled = list( array.get_policies_snapshot(names=[module.params["name"]]).items @@ -1223,7 +1433,7 @@ def update_policy(module, array, api_version, all_squash): ) if suffix_enabled: rules = flasharray.PolicyrulesnapshotpostRules( - at=_convert_to_millisecs(module.params["snap_at"]), + at=convert_to_millisecs(module.params["snap_at"]), client_name=module.params["snap_client_name"], every=module.params["snap_every"] * 60000, keep_for=module.params["snap_keep_for"] * 60000, @@ -1231,7 +1441,7 @@ def update_policy(module, array, api_version, all_squash): ) else: rules = flasharray.PolicyrulesnapshotpostRules( - at=_convert_to_millisecs(module.params["snap_at"]), + at=convert_to_millisecs(module.params["snap_at"]), client_name=module.params["snap_client_name"], every=module.params["snap_every"] * 60000, keep_for=module.params["snap_keep_for"] * 60000, @@ -1276,7 +1486,7 @@ def update_policy(module, array, api_version, all_squash): ) if suffix_enabled: rules = flasharray.PolicyrulesnapshotpostRules( - at=_convert_to_millisecs(module.params["snap_at"]), + at=convert_to_millisecs(module.params["snap_at"]), client_name=module.params["snap_client_name"], every=module.params["snap_every"] * 60000, keep_for=module.params["snap_keep_for"] * 60000, @@ -1284,7 +1494,7 @@ def update_policy(module, array, api_version, all_squash): ) else: rules = flasharray.PolicyrulesnapshotpostRules( - at=_convert_to_millisecs(module.params["snap_at"]), + at=convert_to_millisecs(module.params["snap_at"]), client_name=module.params["snap_client_name"], every=module.params["snap_every"] * 60000, keep_for=module.params["snap_keep_for"] * 60000, @@ -1317,7 +1527,69 @@ def update_policy(module, array, api_version, all_squash): rule_created.errors[err_no].message, ) ) - else: + elif module.params["policy"] == "autodir": + try: + current_enabled = list( + array.get_policies_autodir(names=[module.params["name"]]).items + )[0].enabled + except Exception: + module.fail_json( + msg="Incorrect policy type specified for existing policy {0}".format( + module.params["name"] + ) + ) + if current_enabled != module.params["enabled"]: + changed_enable = True + if not module.check_mode: + res = array.patch_policies_autodir( + names=[module.params["name"]], + policy=flasharray.PolicyPatch(enabled=module.params["enabled"]), + ) + if res.status_code != 200: + module.exit_json( + msg="Failed to enable/disable autodir policy {0}".format( + module.params["name"] + ) + ) + if module.params["directory"]: + dirs = [] + new_dirs = [] + current_dirs = list( + array.get_directories_policies_autodir( + policy_names=[module.params["name"]] + ).items + ) + if current_dirs: + for current_dir in range(0, len(current_dirs)): + dirs.append(current_dirs[current_dir].member.name) + for new_dir in range(0, len(module.params["directory"])): + if module.params["directory"][new_dir] not in dirs: + changed_dir = True + new_dirs.append(module.params["directory"][new_dir]) + else: + new_dirs = module.params["directory"] + if new_dirs: + policies = flasharray.DirectoryPolicyPost( + policies=[ + flasharray.DirectorypolicypostPolicies( + policy=flasharray.Reference(name=module.params["name"]) + ) + ] + ) + changed_dir = True + for add_dir in range(0, len(new_dirs)): + if not module.check_mode: + directory_added = array.post_directories_policies_autodir( + member_names=[new_dirs[add_dir]], policies=policies + ) + if directory_added.status_code != 200: + module.fail_json( + msg="Failed to add new directory to Autodir policy {0}. Error: {1}".format( + module.params["name"], + directory_added.errors[0].message, + ) + ) + else: # quota current_enabled = list( array.get_policies_quota(names=[module.params["name"]]).items )[0].enabled @@ -1419,7 +1691,7 @@ def update_policy(module, array, api_version, all_squash): ) ) if module.params["quota_limit"]: - quota = _human_to_bytes(module.params["quota_limit"]) + quota = human_to_bytes(module.params["quota_limit"]) current_rules = list( array.get_policies_quota_rules( policy_names=[module.params["name"]] @@ -1502,6 +1774,8 @@ def update_policy(module, array, api_version, all_squash): or changed_member or changed_dir or changed_user_map + or changed_abe + or changed_nfs ): changed = True module.exit_json(changed=changed) @@ -1519,7 +1793,9 @@ def main(): ), nfs_permission=dict(type="str", default="rw", choices=["rw", "ro"]), policy=dict( - type="str", required=True, choices=["nfs", "smb", "snapshot", "quota"] + type="str", + required=True, + choices=["nfs", "smb", "snapshot", "quota", "autodir"], ), name=dict(type="str", required=True), rename=dict(type="str"), @@ -1540,8 +1816,19 @@ def main(): quota_notifications=dict( type="list", elements="str", choices=["user", "group"] ), - user_mapping=dict(type="bool"), + user_mapping=dict(type="bool", default=True), directory=dict(type="list", elements="str"), + nfs_version=dict( + type="list", + elements="str", + choices=["nfsv3", "nfsv4"], + ), + security=dict( + type="list", + elements="str", + choices=["auth_sys", "krb5", "krb5i", "krb5p"], + ), + access_based_enumeration=dict(type="bool", default=False), ) ) @@ -1565,6 +1852,11 @@ def main(): msg="FlashArray REST version not supportedi for directory quotas. " "Minimum version required: {0}".format(MIN_QUOTA_API_VERSION) ) + if module.params["policy"] == "autodir" and AUTODIR_VERSION not in api_version: + module.fail_json( + msg="FlashArray REST version not supported for autodir policies. " + "Minimum version required: {0}".format(AUTODIR_VERSION) + ) array = get_array(module) state = module.params["state"] if module.params["quota_notifications"]: diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_proxy.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_proxy.py index 37dd7ac6a..c97615ced 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_proxy.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_proxy.py @@ -30,6 +30,13 @@ options: default: present type: str choices: [ absent, present ] + protocol: + description: + - The proxy protocol. + choices: [http, https ] + default: https + type: str + version_added: '1.20.0' host: description: - The proxy host name. @@ -87,7 +94,11 @@ def create_proxy(module, array): current_proxy = array.get(proxy=True) if current_proxy is not None: new_proxy = ( - "https://" + module.params["host"] + ":" + str(module.params["port"]) + module.params["protocol"] + + "://" + + module.params["host"] + + ":" + + str(module.params["port"]) ) if new_proxy != current_proxy["proxy"]: changed = True @@ -105,6 +116,7 @@ def main(): argument_spec.update( dict( state=dict(type="str", default="present", choices=["absent", "present"]), + protocol=dict(type="str", default="https", choices=["http", "https"]), host=dict(type="str"), port=dict(type="int"), ) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_ra.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_ra.py index 4899b0797..19c192828 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_ra.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_ra.py @@ -30,8 +30,8 @@ options: - When set to I(enable) the RA port can be exposed using the I(debug) module. type: str - default: enable - choices: [ enable, disable ] + default: present + choices: [ enable, disable, absent, present ] extends_documentation_fragment: - purestorage.flasharray.purestorage.fa """ @@ -58,43 +58,75 @@ RETURN = r""" 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.flasharray import SupportPatch +except ImportError: + HAS_PURESTORAGE = False + def enable_ra(module, array): """Enable Remote Assist""" changed = False ra_facts = {} - if not array.get_remote_assist_status()["status"] in ["connected", "enabled"]: + if not list(array.get_support().items)[0].remote_assist_status in [ + "connected", + "connecting", + "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") + res = array.patch_support(support=SupportPatch(remote_assist_active=True)) + if res.status_code == 200: + ra_data = list(res.items)[0] + ra_facts["fa_ra"] = { + "name": ra_data.remote_assist_paths[0].component_name, + "port": None, + } + else: + module.fail_json( + msg="Enabling Remote Assist failed. Error: {0}".format( + res.errors[0].message + ) + ) 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") + res = array.get_support() + if res.status_code == 200: + ra_data = list(res.items)[0] + ra_facts["fa_ra"] = { + "name": ra_data.remote_assist_paths[0].component_name, + "port": None, + } + else: + module.fail_json( + msg="Getting Remote Assist failed. Error: {0}".format( + res.errors[0].message + ) + ) 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"]: + if list(array.get_support().items)[0].remote_assist_status in [ + "connected", + "connecting", + "enabled", + ]: changed = True if not module.check_mode: - try: - array.disable_remote_assist() - except Exception: - module.fail_json(msg="Disabling Remote Assist failed") + res = array.patch_support(support=SupportPatch(remote_assist_active=False)) + if res.status_code != 200: + module.fail_json( + msg="Disabling Remote Assist failed. Error: {0}".format( + res.errors[0].message + ) + ) module.exit_json(changed=changed) @@ -102,15 +134,21 @@ def main(): argument_spec = purefa_argument_spec() argument_spec.update( dict( - state=dict(type="str", default="enable", choices=["enable", "disable"]), + state=dict( + type="str", + default="present", + choices=["enable", "disable", "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_system(module) + array = get_array(module) - if module.params["state"] == "enable": + if module.params["state"] in ["enable", "present"]: enable_ra(module, array) else: disable_ra(module, array) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_saml.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_saml.py index 9d5fc7443..3acf3f748 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_saml.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_saml.py @@ -121,10 +121,12 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, get_array, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) MIN_REQUIRED_API_VERSION = "2.11" @@ -310,15 +312,14 @@ def main(): 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() + array = get_array(module) + api_version = array.get_rest_version() - if MIN_REQUIRED_API_VERSION not in api_version: + if LooseVersion(MIN_REQUIRED_API_VERSION) > LooseVersion(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: diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_smis.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_smis.py index f752cb950..b92c65bcb 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_smis.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_smis.py @@ -65,10 +65,12 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, get_array, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) MIN_REQUIRED_API_VERSION = "2.2" @@ -115,15 +117,14 @@ def main(): 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() + array = get_array(module) + api_version = array.get_rest_version() - if MIN_REQUIRED_API_VERSION not in api_version: + if LooseVersion(MIN_REQUIRED_API_VERSION) > LooseVersion(api_version): module.fail_json( msg="FlashArray REST version not supported. " "Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION) ) - array = get_array(module) update_smis(module, array) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_snap.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_snap.py index db567a398..1b3207878 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_snap.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_snap.py @@ -32,7 +32,6 @@ options: suffix: description: - Suffix of snapshot name. - - Not used during creation if I(offload) is provided. type: str target: description: @@ -51,7 +50,6 @@ options: - Target can be either another FlashArray or an Offload Target - This is only applicable for creation, deletion and eradication of snapshots - I(state) of I(copy) is not supported. - - I(suffix) is not supported for offload snapshots. type: str state: description: @@ -71,6 +69,12 @@ options: - If set to false, allow destruction/eradication of snapshots not in use by replication type: bool default: false + throttle: + description: + - If set to true, allows snapshot to fail if array health is not optimal. + type: bool + default: false + version_added: '1.21.0' extends_documentation_fragment: - purestorage.flasharray.purestorage.fa """ @@ -173,6 +177,8 @@ from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa impo from datetime import datetime GET_SEND_API = "2.4" +THROTTLE_API = "2.25" +SNAPSHOT_SUFFIX_API = "2.28" def _check_offload(module, array): @@ -258,10 +264,14 @@ def get_deleted_snapshot(module, array, arrayv6): def get_snapshot(module, array): - """Return Snapshot or None""" + """Return True if snapshot exists, False otherwise""" try: snapname = module.params["name"] + "." + module.params["suffix"] - for snaps in array.get_volume(module.params["name"], snap=True, pending=False): + name = module.params["name"] + if len(name.split(":")) == 2: + # API 1.x raises exception if name is a remote snap + name = module.params["name"] + "*" + for snaps in array.get_volume(name, snap=True, pending=False): if snaps["name"] == snapname: return True except Exception: @@ -271,12 +281,18 @@ def get_snapshot(module, array): def create_snapshot(module, array, arrayv6): """Create Snapshot""" changed = False + api_version = array._list_available_rest_versions() if module.params["offload"]: - module.params["suffix"] = None + if SNAPSHOT_SUFFIX_API not in api_version: + module.params["suffix"] = None changed = True if not module.check_mode: res = arrayv6.post_remote_volume_snapshots( - source_names=[module.params["name"]], on=module.params["offload"] + source_names=[module.params["name"]], + on=module.params["offload"], + remote_volume_snapshot=flasharray.RemoteVolumeSnapshotPost( + suffix=module.params["suffix"] + ), ) if res.status_code != 200: module.fail_json( @@ -285,21 +301,37 @@ def create_snapshot(module, array, arrayv6): ) ) else: - remote_snap = list(res.items)[0].name - module.params["suffix"] = remote_snap.split(".")[1] + if SNAPSHOT_SUFFIX_API not in api_version: + remote_snap = list(res.items)[0].name + module.params["suffix"] = remote_snap.split(".")[1] else: changed = True if not module.check_mode: - try: - array.create_snapshot( - module.params["name"], suffix=module.params["suffix"] + if THROTTLE_API in api_version: + res = arrayv6.post_volume_snapshots( + allow_throttle=module.params["throttle"], + volume_snapshot=flasharray.VolumeSnapshotPost( + suffix=module.params["suffix"] + ), + source_names=[module.params["name"]], ) - except Exception: - module.fail_json( - msg="Failed to create snapshot for volume {0}".format( - module.params["name"] + if res.status_code != 200: + module.fail_json( + msg="Failed to create snapshot for volume {0}. Error: {1}".format( + module.params["name"], res.errors[0].message + ) + ) + else: + try: + array.create_snapshot( + module.params["name"], suffix=module.params["suffix"] + ) + except Exception: + module.fail_json( + msg="Failed to create snapshot for volume {0}".format( + module.params["name"] + ) ) - ) module.exit_json(changed=changed, suffix=module.params["suffix"]) @@ -518,6 +550,7 @@ def main(): suffix=dict(type="str"), target=dict(type="str"), offload=dict(type="str"), + throttle=dict(type="bool", default=False), ignore_repl=dict(type="bool", default=False), overwrite=dict(type="bool", default=False), eradicate=dict(type="bool", default=False), diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_snmp_agent.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_snmp_agent.py index b9dc8ca94..c3ecb2e64 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_snmp_agent.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_snmp_agent.py @@ -120,10 +120,12 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, get_array, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) MIN_REQUIRED_API_VERSION = "2.1" @@ -238,10 +240,10 @@ def main(): supports_check_mode=True, ) - array = get_system(module) - api_version = array._list_available_rest_versions() + array = get_array(module) + api_version = array.get_rest_version() - if MIN_REQUIRED_API_VERSION not in api_version: + if LooseVersion(MIN_REQUIRED_API_VERSION) > LooseVersion(api_version): module.fail_json( msg="FlashArray REST version not supported. " "Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_sso.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_sso.py index c1199215f..404a2c044 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_sso.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_sso.py @@ -66,10 +66,12 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, get_array, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) SSO_API_VERSION = "2.2" @@ -88,11 +90,10 @@ def main(): module.fail_json(msg="py-pure-client sdk is required for this module") state = module.params["state"] - array = get_system(module) - api_version = array._list_available_rest_versions() + array = get_array(module) + api_version = array.get_rest_version() changed = False - if SSO_API_VERSION in api_version: - array = get_array(module) + if LooseVersion(SSO_API_VERSION) <= LooseVersion(api_version): current_sso = list(array.get_admins_settings().items)[0].single_sign_on_enabled if (state == "present" and not current_sso) or ( state == "absent" and current_sso diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_subnet.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_subnet.py index efce8db9e..84083c522 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_subnet.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_subnet.py @@ -97,7 +97,7 @@ RETURN = """ """ try: - from netaddr import IPNetwork + from netaddr import IPNetwork, valid_ipv4, valid_ipv6 HAS_NETADDR = True except ImportError: @@ -130,22 +130,38 @@ def update_subnet(module, array, subnet): "prefix": subnet["prefix"], "gateway": subnet["gateway"], } + address = str(subnet["prefix"].split("/", 1)[0]) + if not current_state["vlan"]: + current_state["vlan"] = 0 + if not current_state["gateway"]: + if valid_ipv4(address): + current_state["gateway"] = "0.0.0.0" + elif valid_ipv6(address): + current_state["gateway"] = "::" + else: + module.fail_json(msg="Prefix address is not valid IPv4 or IPv6") + if not module.params["prefix"]: prefix = subnet["prefix"] else: - if module.params["gateway"] and module.params["gateway"] not in IPNetwork( - module.params["prefix"] - ): - module.fail_json(msg="Gateway and subnet are not compatible.") - elif ( - not module.params["gateway"] - and subnet["gateway"] - and subnet["gateway"] not in IPNetwork(module.params["prefix"]) + if module.params["gateway"] and not ( + module.params["gateway"] in ["0.0.0.0", "::"] ): - module.fail_json(msg="Gateway and subnet are not compatible.") + if module.params["gateway"] and module.params["gateway"] not in IPNetwork( + module.params["prefix"] + ): + module.fail_json(msg="Gateway and subnet are not compatible.") + elif ( + not module.params["gateway"] + and subnet["gateway"] + and subnet["gateway"] not in IPNetwork(module.params["prefix"]) + ): + module.fail_json(msg="Gateway and subnet are not compatible.") prefix = module.params["prefix"] if not module.params["vlan"]: vlan = subnet["vlan"] + if not vlan: + vlan = 0 else: if not 0 <= module.params["vlan"] <= 4094: module.fail_json( @@ -165,8 +181,11 @@ def update_subnet(module, array, subnet): if not module.params["gateway"]: gateway = subnet["gateway"] else: - if module.params["gateway"] not in IPNetwork(prefix): - module.fail_json(msg="Gateway and subnet are not compatible.") + if module.params["gateway"] and not ( + module.params["gateway"] in ["0.0.0.0", "::"] + ): + if module.params["gateway"] not in IPNetwork(prefix): + module.fail_json(msg="Gateway and subnet are not compatible.") gateway = module.params["gateway"] new_state = {"prefix": prefix, "mtu": mtu, "gateway": gateway, "vlan": vlan} if new_state != current_state: @@ -214,10 +233,13 @@ def create_subnet(module, array): if not module.params["prefix"]: module.fail_json(msg="Prefix required when creating subnet.") else: - if module.params["gateway"] and module.params["gateway"] not in IPNetwork( - module.params["prefix"] + if module.params["gateway"] and not ( + module.params["gateway"] in ["0.0.0.0", "::"] ): - module.fail_json(msg="Gateway and subnet are not compatible.") + if module.params["gateway"] and module.params["gateway"] not in IPNetwork( + module.params["prefix"] + ): + module.fail_json(msg="Gateway and subnet are not compatible.") prefix = module.params["prefix"] if module.params["vlan"]: if not 0 <= module.params["vlan"] <= 4094: @@ -235,7 +257,7 @@ def create_subnet(module, array): ) else: mtu = module.params["mtu"] - if module.params["gateway"]: + if module.params["gateway"] and not (module.params["gateway"] in ["0.0.0.0", "::"]): if module.params["gateway"] not in IPNetwork(prefix): module.fail_json(msg="Gateway and subnet are not compatible.") gateway = module.params["gateway"] @@ -313,6 +335,10 @@ def main(): state = module.params["state"] array = get_system(module) subnet = _get_subnet(module, array) + if module.params["prefix"]: + module.params["prefix"] = module.params["prefix"].strip("[]") + if module.params["gateway"]: + module.params["gateway"] = module.params["gateway"].strip("[]") if state == "present" and not subnet: create_subnet(module, array) if state == "present" and subnet: diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_syslog.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_syslog.py index adb385ca4..20b3104fe 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_syslog.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_syslog.py @@ -21,14 +21,13 @@ version_added: '1.0.0' short_description: Configure Pure Storage FlashArray syslog settings description: - Configure syslog configuration for Pure Storage FlashArrays. -- Add or delete an individual syslog server to the existing - list of serves. +- Manage individual syslog servers. author: - Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com> options: state: description: - - Create or delete syslog servers configuration + - Create, update or delete syslog servers configuration default: present type: str choices: [ absent, present ] @@ -55,14 +54,14 @@ options: description: - A user-specified name. The name must be locally unique and cannot be changed. - - Only applicable with FlashArrays running Purity//FA 6.0 or higher. type: str + required: true extends_documentation_fragment: - purestorage.flasharray.purestorage.fa """ EXAMPLES = r""" -- name: Delete exisitng syslog server entries +- name: Delete existing syslog server entry purestorage.flasharray.purefa_syslog: address: syslog1.com protocol: tcp @@ -70,13 +69,23 @@ EXAMPLES = r""" fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 -- name: Set array syslog servers +- name: Add syslog server entry purestorage.flasharray.purefa_syslog: state: present address: syslog1.com + port: 8081 protocol: udp fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 + +- name: Update syslog server entry + purestorage.flasharray.purefa_syslog: + state: present + address: syslog1.com + port: 8081 + protocol: tcp + fa_url: 10.10.10.2 + api_token: e31060a7-21fc-e277-6240-25983c6c4592 """ RETURN = r""" @@ -85,7 +94,7 @@ RETURN = r""" HAS_PURESTORAGE = True try: - from pypureclient import flasharray + from pypureclient.flasharray import SyslogServer except ImportError: HAS_PURESTORAGE = False @@ -93,90 +102,71 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( get_array, - get_system, purefa_argument_spec, ) -SYSLOG_NAME_API = "2.4" - - def delete_syslog(module, array): """Delete Syslog Server""" - changed = False + changed = True + if not module.check_mode: + res = array.delete_syslog_servers(names=[module.params["name"]]) + if res.status_code != 200: + module.fail_json( + msg="Failed to remove syslog server {0}. Error: {1}".format( + module.params["name"], res.errors[0].message + ) + ) + module.exit_json(changed=changed) + + +def add_syslog(module, array): + """Add Syslog Server""" + changed = True noport_address = module.params["protocol"] + "://" + module.params["address"] if module.params["port"]: full_address = noport_address + ":" + module.params["port"] else: full_address = noport_address - - address_list = array.get(syslogserver=True)["syslogserver"] - - if address_list: - for address in range(0, len(address_list)): - if address_list[address] == full_address: - del address_list[address] - changed = True - if not module.check_mode: - try: - array.set(syslogserver=address_list) - break - except Exception: - module.fail_json( - msg="Failed to remove syslog server: {0}".format( - full_address - ) - ) - + if not module.check_mode: + res = array.post_syslog_servers( + names=[module.params["name"]], + syslog_server=SyslogServer(name=module.params["name"], uri=full_address), + ) + if res.status_code != 200: + module.fail_json( + msg="Adding syslog server {0} failed. Error: {1}".format( + module.params["name"], res.errors[0].message + ) + ) module.exit_json(changed=changed) -def add_syslog(module, array, arrayv6): - """Add Syslog Server""" +def update_syslog(module, array): + """Update Syslog Server""" changed = False + syslog_config = list(array.get_syslog_servers(names=[module.params["name"]]).items)[ + 0 + ] noport_address = module.params["protocol"] + "://" + module.params["address"] if module.params["port"]: full_address = noport_address + ":" + module.params["port"] else: full_address = noport_address - - address_list = array.get(syslogserver=True)["syslogserver"] - exists = False - - if address_list: - for address in range(0, len(address_list)): - if address_list[address] == full_address: - exists = True - break - if not exists: - if arrayv6 and module.params["name"]: - changed = True - if not module.check_mode: - res = arrayv6.post_syslog_servers( - names=[module.params["name"]], - syslog_server=flasharray.SyslogServer( - name=module.params["name"], uri=full_address - ), + if full_address != syslog_config.uri: + changed = True + res = array.patch_syslog_servers( + names=[module.params["name"]], + syslog_server=SyslogServer(uri=full_address), + ) + if res.status_code != 200: + module.fail_json( + msg="Updating syslog server {0} failed. Error: {1}".format( + module.params["name"], res.errors[0].message ) - if res.status_code != 200: - module.fail_json( - msg="Adding syslog server {0} failed. Error: {1}".format( - module.params["name"], res.errors[0].message - ) - ) - else: - changed = True - if not module.check_mode: - try: - address_list.append(full_address) - array.set(syslogserver=address_list) - except Exception: - module.fail_json( - msg="Failed to add syslog server: {0}".format(full_address) - ) - + ) module.exit_json(changed=changed) @@ -187,29 +177,30 @@ def main(): address=dict(type="str", required=True), protocol=dict(type="str", choices=["tcp", "tls", "udp"], required=True), port=dict(type="str"), - name=dict(type="str"), + name=dict(type="str", required=True), state=dict(type="str", default="present", choices=["absent", "present"]), ) ) module = AnsibleModule(argument_spec, supports_check_mode=True) - array = get_system(module) + array = get_array(module) - if module.params["name"] and not HAS_PURESTORAGE: + if not HAS_PURESTORAGE: module.fail_json(msg="py-pure-client sdk is required for this module") - api_version = array._list_available_rest_versions() - - if SYSLOG_NAME_API in api_version and module.params["name"]: - arrayv6 = get_array(module) + res = array.get_syslog_servers(names=[module.params["name"]]) + if res.status_code == 200: + exists = True else: - arrayv6 = None + exists = False - if module.params["state"] == "absent": + if module.params["state"] == "absent" and exists: delete_syslog(module, array) - else: - add_syslog(module, array, arrayv6) + elif module.params["state"] == "present" and not exists: + add_syslog(module, array) + elif module.params["state"] == "present" and exists: + update_syslog(module, array) module.exit_json(changed=False) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_syslog_settings.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_syslog_settings.py index fce6dffa3..735930e08 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_syslog_settings.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_syslog_settings.py @@ -76,10 +76,12 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( - get_system, get_array, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) MIN_REQUIRED_API_VERSION = "2.9" @@ -106,15 +108,14 @@ def main(): 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() + array = get_array(module) + api_version = array.get_rest_version() - if MIN_REQUIRED_API_VERSION not in api_version: + if LooseVersion(MIN_REQUIRED_API_VERSION) > LooseVersion(api_version): module.fail_json( msg="Purity//FA version not supported. Minimum version required: 6.2.0" ) - array = get_array(module) changed = cert_change = False if module.params["ca_certificate"] and len(module.params["ca_certificate"]) > 3000: module.fail_json(msg="Certificate exceeds 3000 characters") diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_token.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_token.py index fa66fe308..f7acb8889 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_token.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_token.py @@ -89,13 +89,22 @@ from ansible.module_utils.basic import AnsibleModule from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( get_array, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.common import ( + convert_time_to_millisecs, +) from os import environ import platform -VERSION = 1.0 +VERSION = 1.5 USER_AGENT_BASE = "Ansible_token" TIMEOUT_API_VERSION = "2.2" +HAS_DISTRO = True +try: + import distro +except ImportError: + HAS_DISTRO = False + HAS_PURESTORAGE = True try: from purestorage import purestorage @@ -103,30 +112,22 @@ except ImportError: HAS_PURESTORAGE = False -def _convert_time_to_millisecs(timeout): - if timeout[-1:].lower() not in ["w", "d", "h", "m", "s"]: - return 0 - try: - if timeout[-1:].lower() == "w": - return int(timeout[:-1]) * 7 * 86400000 - elif timeout[-1:].lower() == "d": - return int(timeout[:-1]) * 86400000 - elif timeout[-1:].lower() == "h": - return int(timeout[:-1]) * 3600000 - elif timeout[-1:].lower() == "m": - return int(timeout[:-1]) * 60000 - except Exception: - return 0 - - def get_session(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(), - } + if HAS_DISTRO: + user_agent = "%(base)s %(class)s/%(version)s (%(platform)s)" % { + "base": USER_AGENT_BASE, + "class": __name__, + "version": VERSION, + "platform": distro.name(pretty=True), + } + else: + 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"] username = module.params["username"] @@ -205,7 +206,7 @@ def main(): ): module.params["api_token"] = api_token array6 = get_array(module) - ttl = _convert_time_to_millisecs(module.params["timeout"]) + ttl = convert_time_to_millisecs(module.params["timeout"]) if ttl != 0: changed = True array6.delete_admins_api_tokens(names=[username]) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_vg.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_vg.py index febb0d5a2..3720ee7cd 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_vg.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_vg.py @@ -99,6 +99,11 @@ options: choices: [ 0, 10 ] default: 0 version_added: '1.13.0' + rename: + description: + - Value to rename the specified volume group to + type: str + version_added: '1.22.0' extends_documentation_fragment: - purestorage.flasharray.purestorage.fa """ @@ -160,6 +165,13 @@ EXAMPLES = r""" fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 state: absent + +- name: Rename volume group foo to bar + purestorage.flasharray.purefa_vg: + name: foo + rename: bar + fa_url: 10.10.10.2 + api_token: e31060a7-21fc-e277-6240-25983c6c4592 """ RETURN = r""" @@ -177,6 +189,10 @@ from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa impo get_system, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.common import ( + human_to_bytes, + human_to_real, +) VGROUP_API_VERSION = "1.13" @@ -185,52 +201,15 @@ MULTI_VG_VERSION = "2.2" PRIORITY_API_VERSION = "2.11" -def human_to_bytes(size): - """Given a human-readable byte string (e.g. 2G, 30M), - return the number of bytes. Will return 0 if the argument has - unexpected form. - """ - bytes = size[:-1] - unit = size[-1].upper() - if bytes.isdigit(): - bytes = int(bytes) - if unit == "P": - bytes *= 1125899906842624 - elif unit == "T": - bytes *= 1099511627776 - elif unit == "G": - bytes *= 1073741824 - elif unit == "M": - bytes *= 1048576 - elif unit == "K": - bytes *= 1024 - else: - bytes = 0 - else: - bytes = 0 - return bytes - - -def human_to_real(iops): - """Given a human-readable IOPs string (e.g. 2K, 30M), - return the real number. Will return 0 if the argument has - unexpected form. - """ - digit = iops[:-1] - unit = iops[-1].upper() - if unit.isdigit(): - digit = iops - elif digit.isdigit(): - digit = int(digit) - if unit == "M": - digit *= 1000000 - elif unit == "K": - digit *= 1000 - else: - digit = 0 - else: - digit = 0 - return digit +def rename_exists(module, array): + """Determine if rename target already exists""" + exists = False + new_name = module.params["rename"] + for vgroup in array.list_vgroups(): + if vgroup["name"].casefold() == new_name.casefold(): + exists = True + break + return exists def get_multi_vgroups(module, destroyed=False): @@ -272,6 +251,25 @@ def get_vgroup(module, array): return vgroup +def rename_vgroup(module, array): + changed = True + if not rename_exists(module, array): + try: + if not module.check_mode: + array.rename_vgroup(module.params["name"], module.params["rename"]) + except Exception: + module.fail_json( + msg="Rename to {0} failed.".format(module.params["rename"]) + ) + else: + module.warn( + "Rename failed. Volume Group {0} already exists.".format( + module.params["rename"] + ) + ) + module.exit_json(changed=changed) + + def make_vgroup(module, array): """Create Volume Group""" changed = True @@ -630,6 +628,7 @@ def main(): priority_operator=dict(type="str", choices=["+", "-"], default="+"), priority_value=dict(type="int", choices=[0, 10], default=0), eradicate=dict(type="bool", default=False), + rename=dict(type="str"), ) ) @@ -673,6 +672,8 @@ def main(): eradicate_vgroup(module, array) elif not vgroup and not xvgroup and state == "present": make_vgroup(module, array) + elif state == "present" and vgroup and module.params["rename"] and not xvgroup: + rename_vgroup(module, array) elif vgroup and state == "present": update_vgroup(module, array) elif vgroup is None and state == "absent": diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_vnc.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_vnc.py index 48e154c77..f9dd627a3 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_vnc.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_vnc.py @@ -32,7 +32,7 @@ options: choices: [ present, absent ] name: description: - - Name od app + - Name of app type: str required: true extends_documentation_fragment: @@ -80,44 +80,65 @@ vnc: type: str """ +HAS_PURESTORAGE = True +try: + from pypureclient.flasharray import App +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 = "1.17" - def enable_vnc(module, array, app): """Enable VNC port""" changed = False vnc_fact = [] - if not app["vnc_enabled"]: - try: - if not module.check_mode: - array.enable_app_vnc(module.params["name"]) - vnc_fact = array.get_app_node(module.params["name"]) - changed = True - except Exception: - module.fail_json( - msg="Enabling VNC for {0} failed".format(module.params["name"]) + if not app.vnc_enabled: + changed = True + if not module.check_mode: + res = array.patch_apps( + names=[module.params["name"]], app=App(vnc_enabled=True) ) + if res.status_code == 200: + vnc_nodes = list( + array.get_apps_nodes(app_names=[module.params["name"]]).items + )[0] + vnc_fact = { + "status": vnc_nodes.status, + "index": vnc_nodes.index, + "version": vnc_nodes.version, + "vnc": vnc_nodes.vnc, + "name": module.params["name"], + } + else: + module.fail_json( + msg="Enabling VNC for {0} failed. Error: {1}".format( + module.params["name"], res.errors[0].message + ) + ) module.exit_json(changed=changed, vnc=vnc_fact) def disable_vnc(module, array, app): """Disable VNC port""" changed = False - if app["vnc_enabled"]: - try: - if not module.check_mode: - array.disable_app_vnc(module.params["name"]) - changed = True - except Exception: - module.fail_json( - msg="Disabling VNC for {0} failed".format(module.params["name"]) + if app.vnc_enabled: + changed = True + if not module.check_mode: + res = array.patch_apps( + names=[module.params["name"]], app=App(vnc_enabled=False) ) + if res.status_code != 200: + module.fail_json( + msg="Disabling VNC for {0} failed. Error: {1}".format( + module.params["name"], res.errors[0].message + ) + ) module.exit_json(changed=changed) @@ -132,21 +153,18 @@ def main(): 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 for this module") - 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) - ) - try: - app = array.get_app(module.params["name"]) - except Exception: + array = get_array(module) + + res = array.get_apps(names=[module.params["name"]]) + if res.status_code != 200: module.fail_json( msg="Selected application {0} does not exist".format(module.params["name"]) ) - if not app["enabled"]: + app = list(res.items)[0] + if not app.enabled: module.fail_json( msg="Application {0} is not enabled".format(module.params["name"]) ) diff --git a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_volume.py b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_volume.py index c3c92f6d4..877af7f74 100644 --- a/ansible_collections/purestorage/flasharray/plugins/modules/purefa_volume.py +++ b/ansible_collections/purestorage/flasharray/plugins/modules/purefa_volume.py @@ -86,6 +86,7 @@ options: See associated descriptions - Only supported from Purity//FA v6.0.0 and higher type: str + default: "" bw_qos: description: - Bandwidth limit for volume in M or G units. @@ -326,6 +327,10 @@ from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa impo get_system, purefa_argument_spec, ) +from ansible_collections.purestorage.flasharray.plugins.module_utils.common import ( + human_to_bytes, + human_to_real, +) QOS_API_VERSION = "1.14" @@ -402,54 +407,6 @@ def get_pgroup(module, array): return pgroup -def human_to_bytes(size): - """Given a human-readable byte string (e.g. 2G, 30M), - return the number of bytes. Will return 0 if the argument has - unexpected form. - """ - bytes = size[:-1] - unit = size[-1].upper() - if bytes.isdigit(): - bytes = int(bytes) - if unit == "P": - bytes *= 1125899906842624 - elif unit == "T": - bytes *= 1099511627776 - elif unit == "G": - bytes *= 1073741824 - elif unit == "M": - bytes *= 1048576 - elif unit == "K": - bytes *= 1024 - else: - bytes = 0 - else: - bytes = 0 - return bytes - - -def human_to_real(iops): - """Given a human-readable IOPs string (e.g. 2K, 30M), - return the real number. Will return 0 if the argument has - unexpected form. - """ - digit = iops[:-1] - unit = iops[-1].upper() - if unit.isdigit(): - digit = iops - elif digit.isdigit(): - digit = int(digit) - if unit == "M": - digit *= 1000000 - elif unit == "K": - digit *= 1000 - else: - digit = 0 - else: - digit = 0 - return digit - - def get_multi_volumes(module, destroyed=False): """Return True is all volumes exist or None""" names = [] @@ -1553,7 +1510,7 @@ def main(): count=dict(type="int"), start=dict(type="int", default=0), digits=dict(type="int", default=1), - suffix=dict(type="str"), + suffix=dict(type="str", default=""), priority_operator=dict(type="str", choices=["+", "-", "="]), priority_value=dict(type="int", choices=[-10, 0, 10]), size=dict(type="str"), |