Coverage for manila/share/drivers/purestorage/flashblade.py: 66%
239 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright 2021 Pure Storage Inc.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15"""
16Pure Storage FlashBlade Share Driver
17"""
19import functools
20import platform
22from oslo_config import cfg
23from oslo_log import log as logging
24from oslo_utils import units
26from manila import exception
27from manila.i18n import _
28from manila.share import driver
30try:
31 import purity_fb
32except ImportError:
33 purity_fb = None
35LOG = logging.getLogger(__name__)
37flashblade_connection_opts = [
38 cfg.HostAddressOpt(
39 "flashblade_mgmt_vip",
40 help="The name (or IP address) for the Pure Storage "
41 "FlashBlade storage system management VIP.",
42 ),
43 cfg.ListOpt(
44 "flashblade_data_vip",
45 help="The names (or IP address) for the Pure Storage "
46 "FlashBlade storage system data VIPs. "
47 "The first listed name or IP address will be considered "
48 "to be the preferred IP address, although is not "
49 "enforced.",
50 ),
51]
53flashblade_auth_opts = [
54 cfg.StrOpt(
55 "flashblade_api",
56 help=("API token for an administrative user account"),
57 secret=True,
58 ),
59]
61flashblade_extra_opts = [
62 cfg.BoolOpt(
63 "flashblade_eradicate",
64 default=True,
65 help="When enabled, all FlashBlade file systems and snapshots "
66 "will be eradicated at the time of deletion in Manila. "
67 "Data will NOT be recoverable after a delete with this "
68 "set to True! When disabled, file systems and snapshots "
69 "will go into pending eradication state and can be "
70 "recovered.)",
71 ),
72]
74CONF = cfg.CONF
75CONF.register_opts(flashblade_connection_opts)
76CONF.register_opts(flashblade_auth_opts)
77CONF.register_opts(flashblade_extra_opts)
80def purity_fb_to_manila_exceptions(func):
81 @functools.wraps(func)
82 def wrapper(*args, **kwargs):
83 try:
84 return func(*args, **kwargs)
85 except purity_fb.rest.ApiException as ex:
86 msg = _("Caught exception from purity_fb: %s") % ex
87 LOG.exception(msg)
88 raise exception.ShareBackendException(msg=msg)
90 return wrapper
93class FlashBladeShareDriver(driver.ShareDriver):
94 """Version hisotry:
96 1.0.0 - Initial version
97 2.0.0 - Xena release
98 3.0.0 - Yoga release
99 4.0.0 - Zed release
100 5.0.0 - Antelope release
101 6.0.0 - Bobcat release
102 7.0.0 - 2024.1 (Caracal) release
103 8.0.0 - 2025.1 (Epoxy) release
104 9.0.0 - 2025.2 (Flamingo) release
106 """
108 VERSION = "9.0" # driver version
109 USER_AGENT_BASE = "OpenStack Manila"
111 def __init__(self, *args, **kwargs):
112 super(FlashBladeShareDriver, self).__init__(False, *args, **kwargs)
113 self.configuration.append_config_values(flashblade_connection_opts)
114 self.configuration.append_config_values(flashblade_auth_opts)
115 self.configuration.append_config_values(flashblade_extra_opts)
116 self._user_agent = "%(base)s %(class)s/%(version)s (%(platform)s)" % {
117 "base": self.USER_AGENT_BASE,
118 "class": self.__class__.__name__,
119 "version": self.VERSION,
120 "platform": platform.platform(),
121 }
123 def do_setup(self, context):
124 """Driver initialization"""
125 if purity_fb is None:
126 msg = _(
127 "Missing 'purity_fb' python module, ensure the library"
128 " is installed and available."
129 )
130 raise exception.ManilaException(message=msg)
132 self.api = self._safe_get_from_config_or_fail("flashblade_api")
133 self.management_address = self._safe_get_from_config_or_fail(
134 "flashblade_mgmt_vip"
135 )
136 self.data_address = self._safe_get_from_config_or_fail(
137 "flashblade_data_vip"
138 )
139 self._sys = purity_fb.PurityFb(self.management_address)
140 self._sys.disable_verify_ssl()
141 try:
142 self._sys.login(self.api)
143 self._sys._api_client.user_agent = self._user_agent
144 except purity_fb.rest.ApiException as ex:
145 msg = _("Exception when logging into the array: %s\n") % ex
146 LOG.exception(msg)
147 raise exception.ManilaException(message=msg)
149 backend_name = self.configuration.safe_get("share_backend_name")
150 self._backend_name = backend_name or self.__class__.__name__
152 LOG.debug("setup complete")
154 def _update_share_stats(self, data=None):
155 """Retrieve stats info from share group."""
156 (
157 free_capacity_bytes,
158 physical_capacity_bytes,
159 provisioned_cap_bytes,
160 data_reduction,
161 ) = self._get_available_capacity()
163 reserved_share_percentage = self.configuration.safe_get(
164 "reserved_share_percentage"
165 )
166 if reserved_share_percentage is None:
167 reserved_share_percentage = 0
169 reserved_share_from_snapshot_percentage = self.configuration.safe_get(
170 "reserved_share_from_snapshot_percentage"
171 )
172 if reserved_share_from_snapshot_percentage is None:
173 reserved_share_from_snapshot_percentage = reserved_share_percentage
175 reserved_share_extend_percentage = self.configuration.safe_get(
176 "reserved_share_extend_percentage"
177 )
178 if reserved_share_extend_percentage is None:
179 reserved_share_extend_percentage = reserved_share_percentage
181 data = dict(
182 share_backend_name=self._backend_name,
183 vendor_name="PURE STORAGE",
184 driver_version=self.VERSION,
185 storage_protocol="NFS",
186 data_reduction=data_reduction,
187 reserved_percentage=reserved_share_percentage,
188 reserved_snapshot_percentage=(
189 reserved_share_from_snapshot_percentage),
190 reserved_share_extend_percentage=(
191 reserved_share_extend_percentage),
192 total_capacity_gb=float(physical_capacity_bytes) / units.Gi,
193 free_capacity_gb=float(free_capacity_bytes) / units.Gi,
194 provisioned_capacity_gb=float(provisioned_cap_bytes) / units.Gi,
195 snapshot_support=True,
196 create_share_from_snapshot_support=False,
197 mount_snapshot_support=False,
198 revert_to_snapshot_support=True,
199 thin_provisioning=True,
200 )
202 super(FlashBladeShareDriver, self)._update_share_stats(data)
204 def _get_available_capacity(self):
205 try:
206 space = self._sys.arrays.list_arrays_space()
207 except purity_fb.rest.ApiException:
208 message = "Connection failure. Retrying login..."
209 LOG.warning(message)
210 try:
211 self._sys.login(self.api)
212 self._sys._api_client.user_agent = self._user_agent
213 except purity_fb.rest.ApiException as ex:
214 msg = _("Exception when logging into the array: %s\n") % ex
215 LOG.exception(msg)
216 raise exception.ManilaException(message=msg)
217 space = self._sys.arrays.list_arrays_space()
218 array_space = space.items[0]
219 data_reduction = array_space.space.data_reduction
220 physical_capacity_bytes = array_space.capacity
221 used_capacity_bytes = array_space.space.total_physical
222 free_capacity_bytes = physical_capacity_bytes - used_capacity_bytes
223 provisioned_capacity_bytes = array_space.space.unique
224 return (
225 free_capacity_bytes,
226 physical_capacity_bytes,
227 provisioned_capacity_bytes,
228 data_reduction,
229 )
231 def _safe_get_from_config_or_fail(self, config_parameter):
232 config_value = self.configuration.safe_get(config_parameter)
233 if not config_value:
234 reason = _(
235 "%(config_parameter)s configuration parameter "
236 "must be specified"
237 ) % {"config_parameter": config_parameter}
238 LOG.exception(reason)
239 raise exception.BadConfigurationException(reason=reason)
240 return config_value
242 def _make_source_name(self, snapshot):
243 base_name = CONF.share_name_template + "-manila"
244 return base_name % snapshot["share_id"]
246 def _make_share_name(self, manila_share):
247 base_name = CONF.share_name_template + "-manila"
248 return base_name % manila_share["id"]
250 def _get_full_nfs_export_path(self, export_path, location):
251 return "{subnet_ip}:/{export_path}".format(
252 subnet_ip=location, export_path=export_path
253 )
255 def _get_flashblade_filesystem_by_name(self, name):
256 filesys = []
257 filesys.append(name)
258 try:
259 res = self._sys.file_systems.list_file_systems(names=filesys)
260 except purity_fb.rest.ApiException as ex:
261 msg = _("Share not found on FlashBlade: %s\n") % ex
262 LOG.exception(msg)
263 raise exception.ManilaException(message=msg)
264 message = "Filesystem %(share_name)s exists. Continuing..."
265 LOG.debug(message, {"share_name": res.items[0].name})
267 def _get_flashblade_snapshot_by_name(self, name):
268 try:
269 self._sys.file_system_snapshots.list_file_system_snapshots(
270 filter=name
271 )
272 except purity_fb.rest.ApiException as ex:
273 msg = _("Snapshot not found on FlashBlade: %s\n") % ex
274 LOG.exception(msg)
275 raise exception.ManilaException(message=msg)
277 @purity_fb_to_manila_exceptions
278 def _resize_share(self, share, new_size):
279 dataset_name = self._make_share_name(share)
280 self._get_flashblade_filesystem_by_name(dataset_name)
281 consumed_size = (
282 self._sys.file_systems.list_file_systems(names=[dataset_name])
283 .items[0]
284 .space.virtual
285 )
286 attr = {}
287 if consumed_size >= new_size * units.Gi:
288 raise exception.ShareShrinkingPossibleDataLoss(
289 share_id=share["id"]
290 )
291 attr["provisioned"] = new_size * units.Gi
292 n_attr = purity_fb.FileSystem(**attr)
293 LOG.debug("Resizing filesystem...")
294 self._sys.file_systems.update_file_systems(
295 name=dataset_name, attributes=n_attr
296 )
298 def _update_nfs_access(self, share, access_rules):
299 dataset_name = self._make_share_name(share)
300 self._get_flashblade_filesystem_by_name(dataset_name)
301 nfs_rules = ""
302 rule_state = {}
303 for access in access_rules:
304 if access["access_type"] == "ip":
305 line = (
306 access["access_to"]
307 + "("
308 + access["access_level"]
309 + ",no_root_squash) "
310 )
311 rule_state[access["access_id"]] = {"state": "active"}
312 nfs_rules += line
313 else:
314 message = _(
315 'Only "ip" access type is allowed for NFS protocol.'
316 )
317 LOG.error(message)
318 rule_state[access["access_id"]] = {"state": "error"}
319 try:
320 self._sys.file_systems.update_file_systems(
321 name=dataset_name,
322 attributes=purity_fb.FileSystem(
323 nfs=purity_fb.NfsRule(rules=nfs_rules)
324 ),
325 )
326 message = "Set nfs rules %(nfs_rules)s for %(share_name)s"
327 LOG.debug(
328 message, {"nfs_rules": nfs_rules, "share_name": dataset_name}
329 )
330 except purity_fb.rest.ApiException as ex:
331 msg = _("Failed to set NFS access rules: %s\n") % ex
332 LOG.exception(msg)
333 raise exception.ManilaException(message=msg)
334 return rule_state
336 @purity_fb_to_manila_exceptions
337 def create_share(self, context, share, share_server=None):
338 """Create a share and export it based on protocol used."""
339 size = share["size"] * units.Gi
340 share_name = self._make_share_name(share)
342 if share["share_proto"] == "NFS":
343 flashblade_fs = purity_fb.FileSystem(
344 name=share_name,
345 provisioned=size,
346 hard_limit_enabled=True,
347 fast_remove_directory_enabled=True,
348 snapshot_directory_enabled=True,
349 nfs=purity_fb.NfsRule(
350 v3_enabled=True, rules="", v4_1_enabled=True
351 ),
352 )
353 self._sys.file_systems.create_file_systems(flashblade_fs)
354 locations = []
355 preferred = True
356 for address in self.data_address:
357 export_location = {
358 "path": self._get_full_nfs_export_path(
359 share_name,
360 address,
361 ),
362 "is_admin_only": False,
363 "metadata": {
364 "preferred": preferred,
365 },
366 }
367 LOG.debug("pref %(pref)s", {"pref": preferred})
368 preferred = False
369 locations.append(export_location)
370 else:
371 message = _("Unsupported share protocol: %(proto)s.") % {
372 "proto": share["share_proto"]
373 }
374 LOG.exception(message)
375 raise exception.InvalidShare(reason=message)
376 LOG.info("FlashBlade created share %(name)s", {"name": share_name})
378 return locations
380 def create_snapshot(self, context, snapshot, share_server=None):
381 """Called to create a snapshot"""
382 source = []
383 flashblade_filesystem = self._make_source_name(snapshot)
384 source.append(flashblade_filesystem)
385 try:
386 self._sys.file_system_snapshots.create_file_system_snapshots(
387 sources=source, suffix=purity_fb.SnapshotSuffix(snapshot["id"])
388 )
389 except purity_fb.rest.ApiException as ex:
390 msg = (
391 _("Snapshot failed. Share not found on FlashBlade: %s\n") % ex
392 )
393 LOG.exception(msg)
394 raise exception.ManilaException(message=msg)
396 def delete_share(self, context, share, share_server=None):
397 """Called to delete a share"""
398 dataset_name = self._make_share_name(share)
399 try:
400 self._get_flashblade_filesystem_by_name(dataset_name)
401 except purity_fb.rest.ApiException:
402 message = (
403 "share %(dataset_name)s not found on FlashBlade, skip "
404 "delete"
405 )
406 LOG.warning(message, {"dataset_name": dataset_name})
407 return
408 self._sys.file_systems.update_file_systems(
409 name=dataset_name,
410 attributes=purity_fb.FileSystem(
411 nfs=purity_fb.NfsRule(v3_enabled=False, v4_1_enabled=False),
412 smb=purity_fb.ProtocolRule(enabled=False),
413 destroyed=True,
414 ),
415 )
416 if self.configuration.flashblade_eradicate:
417 self._sys.file_systems.delete_file_systems(name=dataset_name)
418 LOG.info(
419 "FlashBlade eradicated share %(name)s", {"name": dataset_name}
420 )
422 @purity_fb_to_manila_exceptions
423 def delete_snapshot(self, context, snapshot, share_server=None):
424 """Called to delete a snapshot"""
425 dataset_name = self._make_source_name(snapshot)
426 filt = "source_display_name='{0}' and suffix='{1}'".format(
427 dataset_name, snapshot["id"]
428 )
429 name = "{0}.{1}".format(dataset_name, snapshot["id"])
430 LOG.debug("FlashBlade filter %(name)s", {"name": filt})
431 try:
432 self._get_flashblade_snapshot_by_name(filt)
433 except exception.ShareResourceNotFound:
434 message = (
435 "snapshot %(snapshot)s not found on FlashBlade, skip delete"
436 )
437 LOG.warning(
438 message, {"snapshot": dataset_name + "." + snapshot["id"]}
439 )
440 return
441 self._sys.file_system_snapshots.update_file_system_snapshots(
442 name=name, attributes=purity_fb.FileSystemSnapshot(destroyed=True)
443 )
444 LOG.debug(
445 "Snapshot %(name)s deleted successfully",
446 {"name": dataset_name + "." + snapshot["id"]},
447 )
448 if self.configuration.flashblade_eradicate:
449 self._sys.file_system_snapshots.delete_file_system_snapshots(
450 name=name
451 )
452 LOG.debug(
453 "Snapshot %(name)s eradicated successfully",
454 {"name": dataset_name + "." + snapshot["id"]},
455 )
457 def ensure_share(self, context, share, share_server=None):
458 """Dummy - called to ensure share is exported.
460 All shares created on a FlashBlade are guaranteed to
461 be exported so this check is redundant
462 """
464 def update_access(
465 self,
466 context,
467 share,
468 access_rules,
469 add_rules,
470 delete_rules,
471 update_rules,
472 share_server=None,
473 ):
474 """Update access of share"""
475 # We will use the access_rules list to bulk update access
476 state_map = self._update_nfs_access(share, access_rules)
477 return state_map
479 def extend_share(self, share, new_size, share_server=None):
480 """uses resize_share to extend a share"""
481 self._resize_share(share, new_size)
483 def shrink_share(self, share, new_size, share_server=None):
484 """uses resize_share to shrink a share"""
485 self._resize_share(share, new_size)
487 @purity_fb_to_manila_exceptions
488 def revert_to_snapshot(
489 self,
490 context,
491 snapshot,
492 share_access_rules,
493 snapshot_access_rules,
494 share_server=None,
495 ):
496 dataset_name = self._make_source_name(snapshot)
497 filt = "source_display_name='{0}' and suffix='{1}'".format(
498 dataset_name, snapshot["id"]
499 )
500 LOG.debug("FlashBlade filter %(name)s", {"name": filt})
501 name = "{0}.{1}".format(dataset_name, snapshot["id"])
502 self._get_flashblade_snapshot_by_name(filt)
503 fs_attr = purity_fb.FileSystem(
504 name=dataset_name, source=purity_fb.Reference(name=name)
505 )
506 try:
507 self._sys.file_systems.create_file_systems(
508 overwrite=True,
509 discard_non_snapshotted_data=True,
510 file_system=fs_attr,
511 )
512 except purity_fb.rest.ApiException as ex:
513 msg = _("Failed to revert snapshot: %s\n") % ex
514 LOG.exception(msg)
515 raise exception.ManilaException(message=msg)