493 lines
18 KiB
Python
493 lines
18 KiB
Python
# 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 hisotry:
|
|
|
|
1.0.0 - Initial version
|
|
2.0.0 - Xena release
|
|
3.0.0 - Yoga release
|
|
4.0.0 - Zed release
|
|
"""
|
|
|
|
VERSION = "4.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_share_percentage"
|
|
)
|
|
if reserved_share_percentage is None:
|
|
reserved_share_percentage = 0
|
|
|
|
reserved_share_from_snapshot_percentage = self.configuration.safe_get(
|
|
"reserved_share_from_snapshot_percentage"
|
|
)
|
|
if reserved_share_from_snapshot_percentage is None:
|
|
reserved_share_from_snapshot_percentage = reserved_share_percentage
|
|
|
|
reserved_share_extend_percentage = self.configuration.safe_get(
|
|
"reserved_share_extend_percentage"
|
|
)
|
|
if reserved_share_extend_percentage is None:
|
|
reserved_share_extend_percentage = reserved_share_percentage
|
|
|
|
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,
|
|
reserved_snapshot_percentage=(
|
|
reserved_share_from_snapshot_percentage),
|
|
reserved_share_extend_percentage=(
|
|
reserved_share_extend_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):
|
|
base_name = CONF.share_name_template + "-manila"
|
|
return base_name % snapshot["share_id"]
|
|
|
|
def _make_share_name(self, manila_share):
|
|
base_name = CONF.share_name_template + "-manila"
|
|
return base_name % 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)
|