From 9eb37eca8b228db308b2c07bcecdcca7122b4464 Mon Sep 17 00:00:00 2001 From: Simon Dodsley Date: Mon, 19 Apr 2021 16:31:30 -0400 Subject: [PATCH] Add Pure Storage FlashBlade driver Change-Id: I8de380bca1b55d4d0ee44a5e5d052a7dced467df --- ...hare_back_ends_feature_support_mapping.rst | 8 + .../shared-file-systems/drivers.rst | 1 + .../drivers/purestorage-flashblade-driver.rst | 123 +++++ .../tables/manila-purestorage-flashblade.inc | 18 + manila/opts.py | 4 + manila/share/drivers/purestorage/__init__.py | 0 .../share/drivers/purestorage/flashblade.py | 467 ++++++++++++++++++ .../share/drivers/purestorage/__init__.py | 0 .../drivers/purestorage/test_flashblade.py | 359 ++++++++++++++ ...dd-flashblade-driver-de20b758a8ce2640.yaml | 6 + 10 files changed, 986 insertions(+) create mode 100644 doc/source/configuration/shared-file-systems/drivers/purestorage-flashblade-driver.rst create mode 100644 doc/source/configuration/tables/manila-purestorage-flashblade.inc create mode 100644 manila/share/drivers/purestorage/__init__.py create mode 100644 manila/share/drivers/purestorage/flashblade.py create mode 100644 manila/tests/share/drivers/purestorage/__init__.py create mode 100644 manila/tests/share/drivers/purestorage/test_flashblade.py create mode 100644 releasenotes/notes/add-flashblade-driver-de20b758a8ce2640.yaml diff --git a/doc/source/admin/share_back_ends_feature_support_mapping.rst b/doc/source/admin/share_back_ends_feature_support_mapping.rst index b266cc7a42..f6623192d0 100644 --- a/doc/source/admin/share_back_ends_feature_support_mapping.rst +++ b/doc/source/admin/share_back_ends_feature_support_mapping.rst @@ -95,6 +95,8 @@ Mapping of share drivers and share features support +----------------------------------------+-----------------------+-----------------------+--------------------------+--------------------------+------------------------+-----------------------------------+--------------------------+--------------------+--------------------+ | QNAP | O | O | O | \- | O | O | O | \- | \- | +----------------------------------------+-----------------------+-----------------------+--------------------------+--------------------------+------------------------+-----------------------------------+--------------------------+--------------------+--------------------+ +| Pure Storage FlashBlade | X | \- | X | X | X | \- | \- | X | \- | ++----------------------------------------+-----------------------+-----------------------+--------------------------+--------------------------+------------------------+-----------------------------------+--------------------------+--------------------+--------------------+ Mapping of share drivers and share access rules support ------------------------------------------------------- @@ -164,6 +166,8 @@ Mapping of share drivers and share access rules support +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ | QNAP | NFS (O) | \- | \- | \- | \- | NFS (O) | \- | \- | \- | \- | +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ +| Pure Storage FlashBlade | NFS (X) | \- | \- | \- | \- | NFS (X) | \- | \- | \- | \- | ++----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ Mapping of share drivers and security services support ------------------------------------------------------ @@ -231,6 +235,8 @@ Mapping of share drivers and security services support +----------------------------------------+------------------+-----------------+------------------+ | QNAP | \- | \- | \- | +----------------------------------------+------------------+-----------------+------------------+ +| Pure Storage FlashBlade | \- | \- | \- | ++----------------------------------------+------------------+-----------------+------------------+ Mapping of share drivers and common capabilities ------------------------------------------------ @@ -300,6 +306,8 @@ More information: :ref:`capabilities_and_extra_specs` +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ | INSPUR InStorage | \- | T | \- | \- | \- | T | \- | \- | \- | \- | T | \- | +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ +| Pure Storage FlashBlade | \- | X | \- | \- | X | \- | \- | \- | X | \- | X | \- | ++----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ .. note:: diff --git a/doc/source/configuration/shared-file-systems/drivers.rst b/doc/source/configuration/shared-file-systems/drivers.rst index 2141c1be40..f2edb69009 100644 --- a/doc/source/configuration/shared-file-systems/drivers.rst +++ b/doc/source/configuration/shared-file-systems/drivers.rst @@ -34,6 +34,7 @@ Share drivers drivers/quobyte-driver.rst drivers/windows-smb-driver.rst drivers/nexentastor5-driver.rst + drivers/purestorage-flashblade-driver.rst To use different share drivers for the Shared File Systems service, use the diff --git a/doc/source/configuration/shared-file-systems/drivers/purestorage-flashblade-driver.rst b/doc/source/configuration/shared-file-systems/drivers/purestorage-flashblade-driver.rst new file mode 100644 index 0000000000..66d6e0c7bd --- /dev/null +++ b/doc/source/configuration/shared-file-systems/drivers/purestorage-flashblade-driver.rst @@ -0,0 +1,123 @@ +============================== +Pure Storage FlashBlade driver +============================== + +The Pure Storage FlashBlade driver provides support for managing filesystem shares +on the Pure Storage FlashBlade storage systems. + +The driver is compatible with Pure Storage FlashBlades that support REST API version +1.6 or higher (Purity//FB v2.3.0 or higher). +This section explains how to configure the FlashBlade driver. + +Supported operations +~~~~~~~~~~~~~~~~~~~~ + +- Create and delete NFS shares. + +- Extend/Shrink a share. + +- Create and delete filesystem snapshots (No support for create-from or mount). + +- Revert to Snapshot. + +- Both RW and RO access levels are supported. + +- Set access rights to NFS shares. + + Note the following limitations: + + - Only IP (for NFS shares) access types are supported. + +External package installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The driver requires the ``purity_fb`` package for communicating with +FlashBlade systems. Install the package from PyPI using the following command: + +.. code-block:: console + + $ pip install purity_fb + +Driver configuration +~~~~~~~~~~~~~~~~~~~~ + +Edit the ``manila.conf`` file, which is usually located under the following +path ``/etc/manila/manila.conf``. + +* Add a section for the FlashBlade driver back end. + +* Under the ``[DEFAULT]`` section, set the ``enabled_share_backends`` parameter + with the name of the new back-end section. + +Configure the driver back-end section with the parameters below. + +* Configure the driver name by setting the following parameter: + + .. code-block:: ini + + share_driver = manila.share.drivers.purestorage.flashblade.FlashBladeShareDriver + +* Configure the management and data VIPs of the FlashBlade array by adding the + following parameters: + + .. code-block:: ini + + flashblade_mgmt_vip = FlashBlade management VIP + flashblade_data_vip = FlashBlade data VIP + +* Configure user credentials: + + The driver requires a FlashBlade user with administrative privileges. + We recommend creating a dedicated OpenStack user account + that holds an administrative user role. + Refer to the FlashBlade manuals for details on user account management. + Configure the user credentials by adding the following parameters: + + .. code-block:: ini + + flashblade_api = FlashBlade API token for admin-privileged user + +* (Optional) Configure File System and Snapshot Eradication: + + The option, when enabled, all FlashBlade file systems and snapshots will + be eradicated at the time of deletion in Manila. Data will NOT be + recoverable after a delete with this set to True! When disabled, + file systems and snapshots will go into pending eradication state + and can be recovered. Recovery of these pending eradication snapshots + cannot be accomplished through Manila. These snapshots will self-eradicate + after 24 hours unless manually restored. The default setting is True. + + .. code-block:: ini + + flashblade_eradicate = { True | False } + +* The back-end name is an identifier for the back end. + We recommend using the same name as the name of the section. + Configure the back-end name by adding the following parameter: + + .. code-block:: ini + + share_backend_name = back-end name + +Configuration example +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: ini + + [DEFAULT] + enabled_share_backends = flashblade-1 + + [flashblade-1] + share_driver = manila.share.drivers.purestorage.flashblade.FlashBladeShareDriver + share_backend_name = flashblade-1 + driver_handles_share_servers = false + flashblade_mgmt_vip = 10.1.2.3 + flashblade_data_vip = 10.1.2.4 + flashblade_api = pureuser API + +Driver options +~~~~~~~~~~~~~~ + +Configuration options specific to this driver: + +.. include:: ../../tables/manila-purestorage-flashblade.inc diff --git a/doc/source/configuration/tables/manila-purestorage-flashblade.inc b/doc/source/configuration/tables/manila-purestorage-flashblade.inc new file mode 100644 index 0000000000..6371a48139 --- /dev/null +++ b/doc/source/configuration/tables/manila-purestorage-flashblade.inc @@ -0,0 +1,18 @@ +.. _manila-purestorage-flashblade: + +.. list-table:: Description of Pure Storage FlashBlade share driver configuration options + :header-rows: 1 + :class: config-ref-table + + * - Configuration option = Default value + - Description + * - **[DEFAULT]** + - + * - ``flashblade_mgmt_vip`` = ``None`` + - (String) The name (or IP address) for the Pure Storage FlashBlade storage system management port. + * - ``flashblade_data_vip`` = ``None`` + - (String) The name (or IP address) for the Pure Storage FlashBlade storage system data port. + * - ``flashblade_api`` = ``None`` + - (String) API token for an administrative level user account. + * - ``flashblade_eradicate`` = ``True`` + - (Boolean) Enable or disable filesystem and snapshot eradication on delete. diff --git a/manila/opts.py b/manila/opts.py index 0d0985fd04..c013b9d8d9 100644 --- a/manila/opts.py +++ b/manila/opts.py @@ -81,6 +81,7 @@ import manila.share.drivers.lvm import manila.share.drivers.maprfs.maprfs_native import manila.share.drivers.netapp.options import manila.share.drivers.nexenta.options +import manila.share.drivers.purestorage.flashblade import manila.share.drivers.qnap.qnap import manila.share.drivers.quobyte.quobyte import manila.share.drivers.service_instance @@ -177,6 +178,9 @@ _global_opt_lists = [ manila.share.drivers.nexenta.options.nexenta_connection_opts, manila.share.drivers.nexenta.options.nexenta_dataset_opts, manila.share.drivers.nexenta.options.nexenta_nfs_opts, + manila.share.drivers.purestorage.flashblade.flashblade_auth_opts, + manila.share.drivers.purestorage.flashblade.flashblade_extra_opts, + manila.share.drivers.purestorage.flashblade.flashblade_connection_opts, manila.share.drivers.qnap.qnap.qnap_manila_opts, manila.share.drivers.quobyte.quobyte.quobyte_manila_share_opts, manila.share.drivers.service_instance.common_opts, diff --git a/manila/share/drivers/purestorage/__init__.py b/manila/share/drivers/purestorage/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/share/drivers/purestorage/flashblade.py b/manila/share/drivers/purestorage/flashblade.py new file mode 100644 index 0000000000..06ff638a46 --- /dev/null +++ b/manila/share/drivers/purestorage/flashblade.py @@ -0,0 +1,467 @@ +# Copyright 2021 Pure Storage Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Pure Storage FlashBlade Share Driver +""" + +import functools +import platform + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import units + +from manila import exception +from manila.i18n import _ +from manila.share import driver + +HAS_PURITY_FB = True +try: + import purity_fb +except ImportError: + purity_fb = None + +LOG = logging.getLogger(__name__) + +flashblade_connection_opts = [ + cfg.HostAddressOpt( + "flashblade_mgmt_vip", + help="The name (or IP address) for the Pure Storage " + "FlashBlade storage system management VIP.", + ), + cfg.HostAddressOpt( + "flashblade_data_vip", + help="The name (or IP address) for the Pure Storage " + "FlashBlade storage system data VIP.", + ), +] + +flashblade_auth_opts = [ + cfg.StrOpt( + "flashblade_api", + help=("API token for an administrative user account"), + secret=True, + ), +] + +flashblade_extra_opts = [ + cfg.BoolOpt( + "flashblade_eradicate", + default=True, + help="When enabled, all FlashBlade file systems and snapshots " + "will be eradicated at the time of deletion in Manila. " + "Data will NOT be recoverable after a delete with this " + "set to True! When disabled, file systems and snapshots " + "will go into pending eradication state and can be " + "recovered.)", + ), +] + +CONF = cfg.CONF +CONF.register_opts(flashblade_connection_opts) +CONF.register_opts(flashblade_auth_opts) +CONF.register_opts(flashblade_extra_opts) + + +def purity_fb_to_manila_exceptions(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except purity_fb.rest.ApiException as ex: + msg = _("Caught exception from purity_fb: %s") % ex + LOG.exception(msg) + raise exception.ShareBackendException(msg=msg) + + return wrapper + + +class FlashBladeShareDriver(driver.ShareDriver): + + VERSION = "2.0" # driver version + USER_AGENT_BASE = "OpenStack Manila" + + def __init__(self, *args, **kwargs): + super(FlashBladeShareDriver, self).__init__(False, *args, **kwargs) + self.configuration.append_config_values(flashblade_connection_opts) + self.configuration.append_config_values(flashblade_auth_opts) + self.configuration.append_config_values(flashblade_extra_opts) + self._user_agent = "%(base)s %(class)s/%(version)s (%(platform)s)" % { + "base": self.USER_AGENT_BASE, + "class": self.__class__.__name__, + "version": self.VERSION, + "platform": platform.platform(), + } + + def do_setup(self, context): + """Driver initialization""" + if purity_fb is None: + msg = _( + "Missing 'purity_fb' python module, ensure the library" + " is installed and available." + ) + raise exception.ManilaException(message=msg) + + self.api = self._safe_get_from_config_or_fail("flashblade_api") + self.management_address = self._safe_get_from_config_or_fail( + "flashblade_mgmt_vip" + ) + self.data_address = self._safe_get_from_config_or_fail( + "flashblade_data_vip" + ) + self._sys = purity_fb.PurityFb(self.management_address) + self._sys.disable_verify_ssl() + try: + self._sys.login(self.api) + self._sys._api_client.user_agent = self._user_agent + except purity_fb.rest.ApiException as ex: + msg = _("Exception when logging into the array: %s\n") % ex + LOG.exception(msg) + raise exception.ManilaException(message=msg) + + backend_name = self.configuration.safe_get("share_backend_name") + self._backend_name = backend_name or self.__class__.__name__ + + LOG.debug("setup complete") + + def _update_share_stats(self, data=None): + """Retrieve stats info from share group.""" + ( + free_capacity_bytes, + physical_capacity_bytes, + provisioned_cap_bytes, + data_reduction, + ) = self._get_available_capacity() + + reserved_share_percentage = self.configuration.safe_get( + "reserved_safe_percentage" + ) + if reserved_share_percentage is None: + reserved_share_percentage = 0 + + data = dict( + share_backend_name=self._backend_name, + vendor_name="PURE STORAGE", + driver_version=self.VERSION, + storage_protocol="NFS", + data_reduction=data_reduction, + reserved_percentage=reserved_share_percentage, + total_capacity_gb=float(physical_capacity_bytes) / units.Gi, + free_capacity_gb=float(free_capacity_bytes) / units.Gi, + provisioned_capacity_gb=float(provisioned_cap_bytes) / units.Gi, + snapshot_support=True, + create_share_from_snapshot_support=False, + mount_snapshot_support=False, + revert_to_snapshot_support=True, + thin_provisioning=True, + ) + + super(FlashBladeShareDriver, self)._update_share_stats(data) + + def _get_available_capacity(self): + space = self._sys.arrays.list_arrays_space() + array_space = space.items[0] + data_reduction = array_space.space.data_reduction + physical_capacity_bytes = array_space.capacity + used_capacity_bytes = array_space.space.total_physical + free_capacity_bytes = physical_capacity_bytes - used_capacity_bytes + provisioned_capacity_bytes = array_space.space.unique + return ( + free_capacity_bytes, + physical_capacity_bytes, + provisioned_capacity_bytes, + data_reduction, + ) + + def _safe_get_from_config_or_fail(self, config_parameter): + config_value = self.configuration.safe_get(config_parameter) + if not config_value: + reason = _( + "%(config_parameter)s configuration parameter " + "must be specified" + ) % {"config_parameter": config_parameter} + LOG.exception(reason) + raise exception.BadConfigurationException(reason=reason) + return config_value + + def _make_source_name(self, snapshot): + return "share-%s-manila" % snapshot["share_id"] + + def _make_share_name(self, manila_share): + return "share-%s-manila" % manila_share["id"] + + def _get_full_nfs_export_path(self, export_path): + subnet_ip = self.data_address + return "{subnet_ip}:/{export_path}".format( + subnet_ip=subnet_ip, export_path=export_path + ) + + def _get_flashblade_filesystem_by_name(self, name): + filesys = [] + filesys.append(name) + try: + res = self._sys.file_systems.list_file_systems(names=filesys) + except purity_fb.rest.ApiException as ex: + msg = _("Share not found on FlashBlade: %s\n") % ex + LOG.exception(msg) + raise exception.ManilaException(message=msg) + message = "Filesystem %(share_name)s exists. Continuing..." + LOG.debug(message, {"share_name": res.items[0].name}) + + def _get_flashblade_snapshot_by_name(self, name): + try: + self._sys.file_system_snapshots.list_file_system_snapshots( + filter=name + ) + except purity_fb.rest.ApiException as ex: + msg = _("Snapshot not found on FlashBlade: %s\n") % ex + LOG.exception(msg) + raise exception.ManilaException(message=msg) + + @purity_fb_to_manila_exceptions + def _create_filesystem_export(self, flashblade_filesystem): + flashblade_export = flashblade_filesystem.add_export(permissions=[]) + return { + "path": self._get_full_nfs_export_path( + flashblade_export.get_export_path() + ), + "is_admin_only": False, + "preferred": True, + "metadata": {}, + } + + @purity_fb_to_manila_exceptions + def _resize_share(self, share, new_size): + dataset_name = self._make_share_name(share) + self._get_flashblade_filesystem_by_name(dataset_name) + consumed_size = ( + self._sys.file_systems.list_file_systems(names=[dataset_name]) + .items[0] + .space.virtual + ) + attr = {} + if consumed_size >= new_size * units.Gi: + raise exception.ShareShrinkingPossibleDataLoss( + share_id=share["id"] + ) + attr["provisioned"] = new_size * units.Gi + n_attr = purity_fb.FileSystem(**attr) + LOG.debug("Resizing filesystem...") + self._sys.file_systems.update_file_systems( + name=dataset_name, attributes=n_attr + ) + + def _update_nfs_access(self, share, access_rules): + dataset_name = self._make_share_name(share) + self._get_flashblade_filesystem_by_name(dataset_name) + nfs_rules = "" + rule_state = {} + for access in access_rules: + if access["access_type"] == "ip": + line = ( + access["access_to"] + + "(" + + access["access_level"] + + ",no_root_squash) " + ) + rule_state[access["access_id"]] = {"state": "active"} + nfs_rules += line + else: + message = _( + 'Only "ip" access type is allowed for NFS protocol.' + ) + LOG.error(message) + rule_state[access["access_id"]] = {"state": "error"} + try: + self._sys.file_systems.update_file_systems( + name=dataset_name, + attributes=purity_fb.FileSystem( + nfs=purity_fb.NfsRule(rules=nfs_rules) + ), + ) + message = "Set nfs rules %(nfs_rules)s for %(share_name)s" + LOG.debug( + message, {"nfs_rules": nfs_rules, "share_name": dataset_name} + ) + except purity_fb.rest.ApiException as ex: + msg = _("Failed to set NFS access rules: %s\n") % ex + LOG.exception(msg) + raise exception.ManilaException(message=msg) + return rule_state + + @purity_fb_to_manila_exceptions + def create_share(self, context, share, share_server=None): + """Create a share and export it based on protocol used.""" + size = share["size"] * units.Gi + share_name = self._make_share_name(share) + + if share["share_proto"] == "NFS": + flashblade_fs = purity_fb.FileSystem( + name=share_name, + provisioned=size, + hard_limit_enabled=True, + fast_remove_directory_enabled=True, + snapshot_directory_enabled=True, + nfs=purity_fb.NfsRule( + v3_enabled=True, rules="", v4_1_enabled=True + ), + ) + self._sys.file_systems.create_file_systems(flashblade_fs) + location = self._get_full_nfs_export_path(share_name) + else: + message = _("Unsupported share protocol: %(proto)s.") % { + "proto": share["share_proto"] + } + LOG.exception(message) + raise exception.InvalidShare(reason=message) + LOG.info("FlashBlade created share %(name)s", {"name": share_name}) + + return location + + def create_snapshot(self, context, snapshot, share_server=None): + """Called to create a snapshot""" + source = [] + flashblade_filesystem = self._make_source_name(snapshot) + source.append(flashblade_filesystem) + try: + self._sys.file_system_snapshots.create_file_system_snapshots( + sources=source, suffix=purity_fb.SnapshotSuffix(snapshot["id"]) + ) + except purity_fb.rest.ApiException as ex: + msg = ( + _("Snapshot failed. Share not found on FlashBlade: %s\n") % ex + ) + LOG.exception(msg) + raise exception.ManilaException(message=msg) + + def delete_share(self, context, share, share_server=None): + """Called to delete a share""" + dataset_name = self._make_share_name(share) + try: + self._get_flashblade_filesystem_by_name(dataset_name) + except purity_fb.rest.ApiException: + message = ( + "share %(dataset_name)s not found on FlashBlade, skip " + "delete" + ) + LOG.warning(message, {"dataset_name": dataset_name}) + return + self._sys.file_systems.update_file_systems( + name=dataset_name, + attributes=purity_fb.FileSystem( + nfs=purity_fb.NfsRule(v3_enabled=False, v4_1_enabled=False), + smb=purity_fb.ProtocolRule(enabled=False), + destroyed=True, + ), + ) + if self.configuration.flashblade_eradicate: + self._sys.file_systems.delete_file_systems(name=dataset_name) + LOG.info( + "FlashBlade eradicated share %(name)s", {"name": dataset_name} + ) + + @purity_fb_to_manila_exceptions + def delete_snapshot(self, context, snapshot, share_server=None): + """Called to delete a snapshot""" + dataset_name = self._make_source_name(snapshot) + filt = "source_display_name='{0}' and suffix='{1}'".format( + dataset_name, snapshot["id"] + ) + name = "{0}.{1}".format(dataset_name, snapshot["id"]) + LOG.debug("FlashBlade filter %(name)s", {"name": filt}) + try: + self._get_flashblade_snapshot_by_name(filt) + except exception.ShareResourceNotFound: + message = ( + "snapshot %(snapshot)s not found on FlashBlade, skip delete" + ) + LOG.warning( + message, {"snapshot": dataset_name + "." + snapshot["id"]} + ) + return + self._sys.file_system_snapshots.update_file_system_snapshots( + name=name, attributes=purity_fb.FileSystemSnapshot(destroyed=True) + ) + LOG.debug( + "Snapshot %(name)s deleted successfully", + {"name": dataset_name + "." + snapshot["id"]}, + ) + if self.configuration.flashblade_eradicate: + self._sys.file_system_snapshots.delete_file_system_snapshots( + name=name + ) + LOG.debug( + "Snapshot %(name)s eradicated successfully", + {"name": dataset_name + "." + snapshot["id"]}, + ) + + def ensure_share(self, context, share, share_server=None): + """Dummy - called to ensure share is exported. + + All shares created on a FlashBlade are guaranteed to + be exported so this check is redundant + """ + + def update_access( + self, + context, + share, + access_rules, + add_rules, + delete_rules, + share_server=None, + ): + """Update access of share""" + # We will use the access_rules list to bulk update access + state_map = self._update_nfs_access(share, access_rules) + return state_map + + def extend_share(self, share, new_size, share_server=None): + """uses resize_share to extend a share""" + self._resize_share(share, new_size) + + def shrink_share(self, share, new_size, share_server=None): + """uses resize_share to shrink a share""" + self._resize_share(share, new_size) + + @purity_fb_to_manila_exceptions + def revert_to_snapshot( + self, + context, + snapshot, + share_access_rules, + snapshot_access_rules, + share_server=None, + ): + dataset_name = self._make_source_name(snapshot) + filt = "source_display_name='{0}' and suffix='{1}'".format( + dataset_name, snapshot["id"] + ) + LOG.debug("FlashBlade filter %(name)s", {"name": filt}) + name = "{0}.{1}".format(dataset_name, snapshot["id"]) + self._get_flashblade_snapshot_by_name(filt) + fs_attr = purity_fb.FileSystem( + name=dataset_name, source=purity_fb.Reference(name=name) + ) + try: + self._sys.file_systems.create_file_systems( + overwrite=True, + discard_non_snapshotted_data=True, + file_system=fs_attr, + ) + except purity_fb.rest.ApiException as ex: + msg = _("Failed to revert snapshot: %s\n") % ex + LOG.exception(msg) + raise exception.ManilaException(message=msg) diff --git a/manila/tests/share/drivers/purestorage/__init__.py b/manila/tests/share/drivers/purestorage/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/share/drivers/purestorage/test_flashblade.py b/manila/tests/share/drivers/purestorage/test_flashblade.py new file mode 100644 index 0000000000..e6d44f7ebf --- /dev/null +++ b/manila/tests/share/drivers/purestorage/test_flashblade.py @@ -0,0 +1,359 @@ +# Copyright 2021 Pure Storage Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Unit tests for Pure Storage FlashBlade driver.""" + +import sys +from unittest import mock + +sys.modules["purity_fb"] = mock.Mock() + +from manila.common import constants +from manila import exception +from manila.share.drivers.purestorage import flashblade +from manila import test + + +_MOCK_SHARE_ID = 1 +_MOCK_SNAPSHOT_ID = "snap" +_MOCK_SHARE_SIZE = 4294967296 + + +def _create_mock__getitem__(mock): + def mock__getitem__(self, key, default=None): + return getattr(mock, key, default) + + return mock__getitem__ + + +test_nfs_share = mock.Mock( + id=_MOCK_SHARE_ID, size=_MOCK_SHARE_SIZE, share_proto="NFS" +) +test_nfs_share.__getitem__ = _create_mock__getitem__(test_nfs_share) + +test_snapshot = mock.Mock(id=_MOCK_SNAPSHOT_ID, share=test_nfs_share) +test_snapshot.__getitem__ = _create_mock__getitem__(test_snapshot) + + +class FakePurityFBException(Exception): + def __init__(self, message=None, error_code=None, *args): + self.message = message + self.error_code = error_code + super(FakePurityFBException, self).__init__(message, error_code, *args) + + +class FlashBladeDriverTestCaseBase(test.TestCase): + def setUp(self): + super(FlashBladeDriverTestCaseBase, self).setUp() + self.configuration = mock.Mock() + self.configuration.flashblade_mgmt_vip = "mockfb1" + self.configuration.flashblade_data_vip = "mockfb2" + self.configuration.flashblade_api = "api" + self.configuration.flashblade_eradicate = True + + self.configuration.driver_handles_share_servers = False + self._mock_filesystem = mock.Mock() + self.mock_object(self.configuration, "safe_get", self._fake_safe_get) + self.purity_fb = self._patch( + "manila.share.drivers.purestorage.flashblade.purity_fb" + ) + + self.driver = flashblade.FlashBladeShareDriver( + configuration=self.configuration + ) + + self._sys = self._flashblade_mock() + + self._sys.api_version = mock.Mock() + + self._sys.arrays.list_arrays_space = mock.Mock() + self.purity_fb.rest.ApiException = FakePurityFBException + self.purity_fb.PurityFb.return_value = self._sys + + self.driver.do_setup(None) + self.mock_object( + self.driver, + "_resize_share", + mock.Mock(return_value="fake_dataset"), + ) + self.mock_object( + self.driver, + "_make_source_name", + mock.Mock(return_value="fake_dataset"), + ) + self.mock_object( + self.driver, + "_get_flashblade_filesystem_by_name", + mock.Mock(return_value="fake_dataset"), + ) + self.mock_object( + self.driver, + "_get_flashblade_snapshot_by_name", + mock.Mock(return_value="fake_snapshot.snap"), + ) + + def _flashblade_mock(self): + result = mock.Mock() + self._mock_filesystem = mock.Mock() + result.file_systems.create_file_systems.return_value = ( + self._mock_filesystem + ) + result.file_systems.update_file_systems.return_value = ( + self._mock_filesystem + ) + result.file_systems.delete_file_systems.return_value = ( + self._mock_filesystem + ) + result.file_system_snapshots.create_file_system_snapshots\ + .return_value = (self._mock_filesystem) + return result + + def _raise_purity_fb(self, *args, **kwargs): + raise FakePurityFBException() + + def _fake_safe_get(self, value): + return getattr(self.configuration, value, None) + + def _patch(self, path, *args, **kwargs): + patcher = mock.patch(path, *args, **kwargs) + result = patcher.start() + self.addCleanup(patcher.stop) + return result + + +class FlashBladeDriverTestCase(FlashBladeDriverTestCaseBase): + @mock.patch("manila.share.drivers.purestorage.flashblade.purity_fb", None) + def test_no_purity_fb_module(self): + self.assertRaises(exception.ManilaException, + self.driver.do_setup, None) + + def test_no_auth_parameters(self): + self.configuration.flashblade_api = None + self.assertRaises( + exception.BadConfigurationException, self.driver.do_setup, None + ) + + def test_empty_auth_parameters(self): + self.configuration.flashblade_api = "" + self.assertRaises( + exception.BadConfigurationException, self.driver.do_setup, None + ) + + def test_create_share_incorrect_protocol(self): + test_nfs_share.share_proto = "CIFS" + self.assertRaises( + exception.InvalidShare, + self.driver.create_share, + None, + test_nfs_share, + ) + + def test_create_nfs_share(self): + location = self.driver.create_share(None, test_nfs_share) + self._sys.file_systems.create_file_systems.assert_called_once_with( + self.purity_fb.FileSystem( + name="share-%s-manila" % test_nfs_share["id"], + provisioned=test_nfs_share["size"], + hard_limit_enabled=True, + fast_remove_directory_enabled=True, + snapshot_directory_enabled=True, + nfs=self.purity_fb.NfsRule( + v3_enabled=True, rules="", v4_1_enabled=True + ), + ) + ) + self.assertEqual("mockfb2:/share-1-manila", location) + + def test_delete_share(self): + self.mock_object(self.driver, "_get_flashblade_filesystem_by_name") + + self.driver.delete_share(None, test_nfs_share) + + share_name = "share-%s-manila" % test_nfs_share["id"] + self.driver._get_flashblade_filesystem_by_name.assert_called_once_with( + share_name + ) + self._sys.file_systems.update_file_systems.assert_called_once_with( + name=share_name, + attributes=self.purity_fb.FileSystem( + nfs=self.purity_fb.NfsRule( + v3_enabled=False, v4_1_enabled=False + ), + smb=self.purity_fb.ProtocolRule(enabled=False), + destroyed=True, + ), + ) + self._sys.file_systems.delete_file_systems.assert_called_once_with( + name=share_name + ) + + def test_delete_share_no_eradicate(self): + self.configuration.flashblade_eradicate = False + self.mock_object(self.driver, "_get_flashblade_filesystem_by_name") + + self.driver.delete_share(None, test_nfs_share) + + share_name = "share-%s-manila" % test_nfs_share["id"] + self.driver._get_flashblade_filesystem_by_name.assert_called_once_with( + share_name + ) + self._sys.file_systems.update_file_systems.assert_called_once_with( + name=share_name, + attributes=self.purity_fb.FileSystem( + nfs=self.purity_fb.NfsRule( + v3_enabled=False, v4_1_enabled=False + ), + smb=self.purity_fb.ProtocolRule(enabled=False), + destroyed=True, + ), + ) + assert not self._sys.file_systems.delete_file_systems.called + + def test_delete_share_not_found(self): + self.mock_object( + self.driver, + "_get_flashblade_filesystem_by_name", + mock.Mock(side_effect=self.purity_fb.rest.ApiException), + ) + mock_result = self.driver.delete_share(None, test_nfs_share) + self.assertIsNone(mock_result) + + def test_extend_share(self): + self.driver.extend_share(test_nfs_share, _MOCK_SHARE_SIZE * 2) + self.driver._resize_share.assert_called_once_with( + test_nfs_share, + _MOCK_SHARE_SIZE * 2, + ) + + def test_shrink_share(self): + self.driver.shrink_share(test_nfs_share, _MOCK_SHARE_SIZE / 2) + self.driver._resize_share.assert_called_once_with( + test_nfs_share, + _MOCK_SHARE_SIZE / 2, + ) + + def test_shrink_share_over_consumed(self): + self.mock_object( + self.driver, + "_resize_share", + mock.Mock( + side_effect=exception.ShareShrinkingPossibleDataLoss( + share_id=test_nfs_share["id"] + ) + ), + ) + self.assertRaises( + exception.ShareShrinkingPossibleDataLoss, + self.driver.shrink_share, + test_nfs_share, + _MOCK_SHARE_SIZE / 2, + ) + + def test_create_snapshot(self): + self.mock_object(self.driver, "_get_flashblade_filesystem_by_name") + self.mock_object(self.driver, "_get_flashblade_snapshot_by_name") + self.mock_object(self.driver, "_make_source_name") + self.driver.create_snapshot(None, test_snapshot) + self._sys.file_system_snapshots.create_file_system_snapshots\ + .assert_called_once_with( + suffix=self.purity_fb.SnapshotSuffix(test_snapshot["id"]), + sources=[mock.ANY], + ) + + def test_delete_snapshot_no_eradicate(self): + self.configuration.flashblade_eradicate = False + self.mock_object(self.driver, "_get_flashblade_snapshot_by_name") + self.driver.delete_snapshot(None, test_snapshot) + self._sys.file_system_snapshots.update_file_system_snapshots\ + .assert_called_once_with( + name=mock.ANY, + attributes=self.purity_fb.FileSystemSnapshot(destroyed=True), + ) + assert not self._sys.file_system_snapshots\ + .delete_file_system_snapshots.called + + def test_delete_snapshot(self): + self.mock_object(self.driver, "_get_flashblade_snapshot_by_name") + self.driver.delete_snapshot(None, test_snapshot) + self._sys.file_system_snapshots.update_file_system_snapshots\ + .assert_called_once_with( + name=mock.ANY, + attributes=self.purity_fb.FileSystemSnapshot(destroyed=True), + ) + self._sys.file_system_snapshots.delete_file_system_snapshots\ + .assert_called_once_with( + name=mock.ANY + ) + + def test_delete_snapshot_not_found(self): + self.mock_object( + self.driver, + "_get_flashblade_snapshot_by_name", + mock.Mock( + side_effect=exception.ShareResourceNotFound( + share_id=test_nfs_share["id"] + ) + ), + ) + mock_result = self.driver.delete_snapshot(None, test_snapshot) + self.assertIsNone(mock_result) + + def test_update_access_share(self): + access_rules = [ + { + "access_level": constants.ACCESS_LEVEL_RO, + "access_to": "1.2.3.4", + "access_type": "ip", + "access_id": "09960614-8574-4e03-89cf-7cf267b0bd09", + }, + { + "access_level": constants.ACCESS_LEVEL_RW, + "access_to": "1.2.3.5", + "access_type": "user", + "access_id": "09960614-8574-4e03-89cf-7cf267b0bd08", + }, + ] + + expected_rule_map = { + "09960614-8574-4e03-89cf-7cf267b0bd08": {"state": "error"}, + "09960614-8574-4e03-89cf-7cf267b0bd09": {"state": "active"}, + } + + rule_map = self.driver.update_access( + None, test_nfs_share, access_rules, [], [] + ) + self.assertEqual(expected_rule_map, rule_map) + + def test_revert_to_snapshot_bad_snapshot(self): + self.mock_object( + self.driver, + "_get_flashblade_filesystem_by_name", + mock.Mock(side_effect=self.purity_fb.rest.ApiException), + ) + mock_result = self.driver.revert_to_snapshot( + None, test_snapshot, None, None + ) + self.assertIsNone(mock_result) + + def test_revert_to_snapshot(self): + self.mock_object(self.driver, "_get_flashblade_snapshot_by_name") + self.driver.revert_to_snapshot(None, test_snapshot, [], []) + self._sys.file_systems.create_file_systems.assert_called_once_with( + overwrite=True, + discard_non_snapshotted_data=True, + file_system=self.purity_fb.FileSystem( + name=test_nfs_share, + source=self.purity_fb.Reference(name=mock.ANY), + ), + ) diff --git a/releasenotes/notes/add-flashblade-driver-de20b758a8ce2640.yaml b/releasenotes/notes/add-flashblade-driver-de20b758a8ce2640.yaml new file mode 100644 index 0000000000..eaab9cc335 --- /dev/null +++ b/releasenotes/notes/add-flashblade-driver-de20b758a8ce2640.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added Pure Storage FlashBlade driver. + Driver supports NFS protocol. + Share operations include create, delete, resize, snapshot and revert-to-snapshot.