Add Pure Storage FlashBlade driver

Change-Id: I8de380bca1b55d4d0ee44a5e5d052a7dced467df
This commit is contained in:
Simon Dodsley 2021-04-19 16:31:30 -04:00
parent faf44b54dc
commit 9eb37eca8b
10 changed files with 986 additions and 0 deletions

View File

@ -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::

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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,

View 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)

View 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),
),
)

View File

@ -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.