Merge "Add Pure Storage FlashBlade driver"
This commit is contained in:
commit
1a39505039
@ -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::
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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.
|
@ -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,
|
||||
|
0
manila/share/drivers/purestorage/__init__.py
Normal file
0
manila/share/drivers/purestorage/__init__.py
Normal file
467
manila/share/drivers/purestorage/flashblade.py
Normal file
467
manila/share/drivers/purestorage/flashblade.py
Normal file
@ -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)
|
0
manila/tests/share/drivers/purestorage/__init__.py
Normal file
0
manila/tests/share/drivers/purestorage/__init__.py
Normal file
359
manila/tests/share/drivers/purestorage/test_flashblade.py
Normal file
359
manila/tests/share/drivers/purestorage/test_flashblade.py
Normal file
@ -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),
|
||||
),
|
||||
)
|
@ -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.
|
Loading…
x
Reference in New Issue
Block a user