Coverage for manila/share/drivers/dell_emc/plugins/powerstore/connection.py: 96%
269 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 (c) 2023 Dell Inc. or its subsidiaries.
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.
16"""
17PowerStore specific NAS backend plugin.
18"""
19from oslo_config import cfg
20from oslo_log import log
21from oslo_utils import units
23from manila.common import constants as const
24from manila import exception
25from manila.i18n import _
26from manila.share.drivers.dell_emc.plugins import base as driver
27from manila.share.drivers.dell_emc.plugins.powerstore import client
29"""Version history:
30 1.0 - Initial version
31"""
32VERSION = "1.0"
34CONF = cfg.CONF
36LOG = log.getLogger(__name__)
38POWERSTORE_OPTS = [
39 cfg.StrOpt('dell_nas_backend_host',
40 help='Dell NAS backend hostname or IP address.'),
41 cfg.StrOpt('dell_nas_server',
42 help='Root directory or NAS server which owns the shares.'),
43 cfg.StrOpt('dell_ad_domain',
44 help='Domain name of the active directory '
45 'joined by the NAS server.'),
46 cfg.StrOpt('dell_nas_login',
47 help='User name for the Dell NAS backend.'),
48 cfg.StrOpt('dell_nas_password',
49 secret=True,
50 help='Password for the Dell NAS backend.'),
51 cfg.BoolOpt('dell_ssl_cert_verify',
52 default=False,
53 help='If set to False the https client will not validate the '
54 'SSL certificate of the backend endpoint.'),
55 cfg.StrOpt('dell_ssl_cert_path',
56 help='Can be used to specify a non default path to a '
57 'CA_BUNDLE file or directory with certificates of trusted '
58 'CAs, which will be used to validate the backend.')
59]
62class PowerStoreStorageConnection(driver.StorageConnection):
63 """Implements PowerStore specific functionality for Dell Manila driver."""
65 def __init__(self, *args, **kwargs):
66 """Do initialization"""
68 LOG.debug('Invoking base constructor for Manila'
69 ' Dell PowerStore Driver.')
70 super(PowerStoreStorageConnection,
71 self).__init__(*args, **kwargs)
73 LOG.debug('Setting up attributes for Manila'
74 ' Dell PowerStore Driver.')
75 if 'configuration' in kwargs: 75 ↛ 76line 75 didn't jump to line 76 because the condition on line 75 was never true
76 kwargs['configuration'].append_config_values(POWERSTORE_OPTS)
78 self.client = None
79 self.verify_certificate = None
80 self.certificate_path = None
81 self.ipv6_implemented = True
82 self.revert_to_snap_support = True
83 self.shrink_share_support = True
85 # props from super class
86 self.driver_handles_share_servers = False
87 # props for share status update
88 self.reserved_percentage = None
89 self.reserved_snapshot_percentage = None
90 self.reserved_share_extend_percentage = None
91 self.max_over_subscription_ratio = None
93 def connect(self, dell_share_driver, context):
94 """Connects to Dell PowerStore"""
95 LOG.debug('Reading configuration parameters for Manila'
96 ' Dell PowerStore Driver.')
97 config = dell_share_driver.configuration
98 get_config_value = config.safe_get
99 self.rest_ip = get_config_value("dell_nas_backend_host")
100 self.rest_username = get_config_value("dell_nas_login")
101 self.rest_password = get_config_value("dell_nas_password")
102 # validate IP, username and password
103 if not all([self.rest_ip, 103 ↛ 106line 103 didn't jump to line 106 because the condition on line 103 was never true
104 self.rest_username,
105 self.rest_password]):
106 message = _("REST server IP, username and password"
107 " must be specified.")
108 raise exception.BadConfigurationException(reason=message)
109 self.nas_server = get_config_value("dell_nas_server")
110 self.ad_domain = get_config_value("dell_ad_domain")
111 self.verify_certificate = (get_config_value("dell_ssl_cert_verify") or
112 False)
113 if self.verify_certificate: 113 ↛ 117line 113 didn't jump to line 117 because the condition on line 113 was always true
114 self.certificate_path = get_config_value(
115 "dell_ssl_cert_path")
117 LOG.debug('Initializing Dell PowerStore REST Client.')
118 LOG.info("REST server IP: %(ip)s, username: %(user)s. "
119 "Verify server's certificate: %(verify_cert)s.",
120 {
121 "ip": self.rest_ip,
122 "user": self.rest_username,
123 "verify_cert": self.verify_certificate,
124 })
126 self.client = client.PowerStoreClient(self.rest_ip,
127 self.rest_username,
128 self.rest_password,
129 self.verify_certificate,
130 self.certificate_path)
132 # configuration for share status update
133 self.reserved_percentage = config.safe_get(
134 'reserved_share_percentage')
135 if self.reserved_percentage is None: 135 ↛ 138line 135 didn't jump to line 138 because the condition on line 135 was always true
136 self.reserved_percentage = 0
138 self.reserved_snapshot_percentage = config.safe_get(
139 'reserved_share_from_snapshot_percentage')
140 if self.reserved_snapshot_percentage is None: 140 ↛ 143line 140 didn't jump to line 143 because the condition on line 140 was always true
141 self.reserved_snapshot_percentage = self.reserved_percentage
143 self.reserved_share_extend_percentage = config.safe_get(
144 'reserved_share_extend_percentage')
145 if self.reserved_share_extend_percentage is None: 145 ↛ 148line 145 didn't jump to line 148 because the condition on line 145 was always true
146 self.reserved_share_extend_percentage = self.reserved_percentage
148 self.max_over_subscription_ratio = config.safe_get(
149 'max_over_subscription_ratio')
151 def create_share(self, context, share, share_server):
152 """Is called to create a share."""
153 LOG.debug(f'Creating {share["share_proto"]} share.')
154 locations = self._create_share(share)
155 return locations
157 def _create_share(self, share):
158 """Creates a NFS or SMB share.
160 In PowerStore, an export (share) belongs to a filesystem.
161 This function creates a filesystem and an export.
162 """
163 share_name = share['name']
164 size_in_bytes = share['size'] * units.Gi
165 # create a filesystem
166 nas_server_id = self.client.get_nas_server_id(self.nas_server)
167 LOG.debug(f"Creating filesystem {share_name}")
168 filesystem_id = self.client.create_filesystem(nas_server_id,
169 share_name,
170 size_in_bytes)
171 if not filesystem_id:
172 message = {
173 _('The filesystem "%(export)s" was not created.') %
174 {'export': share_name}}
175 LOG.error(message)
176 raise exception.ShareBackendException(msg=message)
177 # create a share
178 locations = self._create_share_NFS_CIFS(nas_server_id, filesystem_id,
179 share_name,
180 share['share_proto'].upper())
181 return locations
183 def _create_share_NFS_CIFS(self, nas_server_id, filesystem_id, share_name,
184 protocol):
185 LOG.debug(f"Get file interfaces of {nas_server_id}")
186 file_interfaces = self.client.get_nas_server_interfaces(
187 nas_server_id)
188 LOG.debug(f"Creating {protocol} export {share_name}")
189 if protocol == 'NFS':
190 export_id = self.client.create_nfs_export(filesystem_id,
191 share_name)
192 if not export_id:
193 message = (
194 _('The requested NFS export "%(export)s"'
195 ' was not created.') %
196 {'export': share_name})
197 LOG.error(message)
198 raise exception.ShareBackendException(msg=message)
199 locations = self._get_nfs_location(file_interfaces, share_name)
200 elif protocol == 'CIFS': 200 ↛ 212line 200 didn't jump to line 212 because the condition on line 200 was always true
201 export_id = self.client.create_smb_share(filesystem_id,
202 share_name)
203 if not export_id:
204 message = (
205 _('The requested SMB share "%(export)s"'
206 ' was not created.') %
207 {'export': share_name})
208 LOG.error(message)
209 raise exception.ShareBackendException(msg=message)
210 locations = self._get_cifs_location(file_interfaces,
211 share_name)
212 return locations
214 def _get_nfs_location(self, file_interfaces, share_name):
215 export_locations = []
216 for interface in file_interfaces:
217 export_locations.append(
218 {'path': f"{interface['ip']}:/{share_name}",
219 'metadata': {
220 'preferred': interface['preferred']
221 }
222 })
223 return export_locations
225 def _get_cifs_location(self, file_interfaces, share_name):
226 export_locations = []
227 for interface in file_interfaces:
228 export_locations.append(
229 {'path': f"\\\\{interface['ip']}\\{share_name}",
230 'metadata': {
231 'preferred': interface['preferred']
232 }
233 })
234 return export_locations
236 def delete_share(self, context, share, share_server):
237 """Is called to delete a share."""
238 LOG.debug(f'Deleting {share["share_proto"]} share.')
239 self._delete_share(share)
241 def _delete_share(self, share):
242 """Deletes a filesystem and its associated export."""
243 LOG.debug(f"Retrieving filesystem ID for filesystem {share['name']}")
244 filesystem_id = self.client.get_filesystem_id(share['name'])
245 if not filesystem_id:
246 LOG.warning(
247 f'Filesystem with share name {share["name"]} is not found.')
248 else:
249 LOG.debug(f"Deleting filesystem ID {filesystem_id}")
250 share_deleted = self.client.delete_filesystem(filesystem_id)
251 if not share_deleted:
252 message = (
253 _('Failed to delete share "%(export)s".') %
254 {'export': share['name']})
255 LOG.error(message)
256 raise exception.ShareBackendException(msg=message)
258 def extend_share(self, share, new_size, share_server):
259 """Is called to extend a share."""
260 LOG.debug(f"Extending {share['name']} to {new_size}GiB")
261 self._resize_filesystem(share, new_size)
263 def shrink_share(self, share, new_size, share_server):
264 """Is called to shrink a share."""
265 LOG.debug(f"Shrinking {share['name']} to {new_size}GiB")
266 self._resize_filesystem(share, new_size)
268 def _resize_filesystem(self, share, new_size):
269 """Is called to resize a filesystem"""
271 # Converts the size from GiB to Bytes
272 new_size_in_bytes = new_size * units.Gi
273 filesystem_id = self.client.get_filesystem_id(share['name'])
274 is_success, detail = self.client.resize_filesystem(filesystem_id,
275 new_size_in_bytes)
276 if not is_success:
277 message = (_('Failed to resize share "%(export)s".') %
278 {'export': share['name']})
279 LOG.error(message)
280 if detail:
281 raise exception.ShareShrinkingPossibleDataLoss(
282 share_id=share['id'])
283 raise exception.ShareBackendException(msg=message)
285 def allow_access(self, context, share, access, share_server):
286 """Allow access to the share."""
287 raise NotImplementedError()
289 def deny_access(self, context, share, access, share_server):
290 """Deny access to the share."""
291 raise NotImplementedError()
293 def update_access(self, context, share, access_rules, add_rules,
294 delete_rules, share_server=None):
295 """Is called to update share access."""
296 protocol = share['share_proto'].upper()
297 LOG.debug(f'Updating access to {protocol} share.')
298 if protocol == 'NFS':
299 return self._update_nfs_access(share, access_rules)
300 elif protocol == 'CIFS': 300 ↛ exitline 300 didn't return from function 'update_access' because the condition on line 300 was always true
301 return self._update_cifs_access(share, access_rules)
303 def _update_nfs_access(self, share, access_rules):
304 """Updates access rules for NFS share type."""
305 nfs_rw_ips = set()
306 nfs_ro_ips = set()
307 access_updates = {}
309 for rule in access_rules:
310 if rule['access_type'].lower() != 'ip':
311 message = (_("Only IP access type currently supported for "
312 "NFS. Share provided %(share)s with rule type "
313 "%(type)s") % {'share': share['display_name'],
314 'type': rule['access_type']})
315 LOG.error(message)
316 access_updates.update({rule['access_id']: {'state': 'error'}})
318 else:
319 if rule['access_level'] == const.ACCESS_LEVEL_RW:
320 nfs_rw_ips.add(rule['access_to'])
321 elif rule['access_level'] == const.ACCESS_LEVEL_RO: 321 ↛ 323line 321 didn't jump to line 323 because the condition on line 321 was always true
322 nfs_ro_ips.add(rule['access_to'])
323 access_updates.update({rule['access_id']: {'state': 'active'}})
325 share_id = self.client.get_nfs_export_id(share['name'])
326 share_updated = self.client.set_export_access(share_id,
327 nfs_rw_ips,
328 nfs_ro_ips)
329 if not share_updated:
330 message = (
331 _('Failed to update NFS access rules for "%(export)s".') %
332 {'export': share['display_name']})
333 LOG.error(message)
334 raise exception.ShareBackendException(msg=message)
335 return access_updates
337 def _update_cifs_access(self, share, access_rules):
338 """Updates access rules for CIFS share type."""
339 cifs_rw_users = set()
340 cifs_ro_users = set()
341 access_updates = {}
343 for rule in access_rules:
344 if rule['access_type'].lower() != 'user':
345 message = (_("Only user access type currently supported for "
346 "CIFS. Share provided %(share)s with rule type "
347 "%(type)s") % {'share': share['display_name'],
348 'type': rule['access_type']})
349 LOG.error(message)
350 access_updates.update({rule['access_id']: {'state': 'error'}})
352 else:
353 prefix = (
354 self.ad_domain or
355 self.client.get_nas_server_smb_netbios(self.nas_server)
356 )
357 if not prefix:
358 message = (
359 _('Failed to get daomain/netbios name of '
360 '"%(nas_server)s".'
361 ) % {'nas_server': self.nas_server})
362 LOG.error(message)
363 access_updates.update({rule['access_id']:
364 {'state': 'error'}})
365 continue
367 prefix = prefix + '\\'
368 if rule['access_level'] == const.ACCESS_LEVEL_RW:
369 cifs_rw_users.add(prefix + rule['access_to'])
370 elif rule['access_level'] == const.ACCESS_LEVEL_RO: 370 ↛ 372line 370 didn't jump to line 372 because the condition on line 370 was always true
371 cifs_ro_users.add(prefix + rule['access_to'])
372 access_updates.update({rule['access_id']: {'state': 'active'}})
374 share_id = self.client.get_smb_share_id(share['name'])
375 share_updated = self.client.set_acl(share_id,
376 cifs_rw_users,
377 cifs_ro_users)
378 if not share_updated:
379 message = (
380 _('Failed to update NFS access rules for "%(export)s".') %
381 {'export': share['display_name']})
382 LOG.error(message)
383 raise exception.ShareBackendException(msg=message)
384 return access_updates
386 def update_share_stats(self, stats_dict):
387 """Retrieve stats info from share."""
388 stats_dict['driver_version'] = VERSION
389 stats_dict['storage_protocol'] = 'NFS_CIFS'
390 stats_dict['reserved_percentage'] = self.reserved_percentage
391 stats_dict['reserved_snapshot_percentage'] = (
392 self.reserved_snapshot_percentage)
393 stats_dict['reserved_share_extend_percentage'] = (
394 self.reserved_share_extend_percentage)
395 stats_dict['max_over_subscription_ratio'] = (
396 self.max_over_subscription_ratio)
398 cluster_id = self.client.get_cluster_id()
399 total, used = self.client.retreive_cluster_capacity_metrics(cluster_id)
400 if total and used:
401 free = total - used
402 stats_dict['total_capacity_gb'] = total // units.Gi
403 stats_dict['free_capacity_gb'] = free // units.Gi
405 def create_snapshot(self, context, snapshot, share_server):
406 """Is called to create snapshot."""
407 export_name = snapshot['share_name']
408 LOG.debug(f'Retrieving filesystem ID for share {export_name}')
409 filesystem_id = self.client.get_filesystem_id(export_name)
410 if not filesystem_id:
411 message = (
412 _('Failed to get filesystem id for export "%(export)s".') %
413 {'export': export_name})
414 LOG.error(message)
415 raise exception.ShareBackendException(msg=message)
416 snapshot_name = snapshot['name']
417 LOG.debug(
418 f'Creating snapshot {snapshot_name} for filesystem {filesystem_id}'
419 )
420 snapshot_id = self.client.create_snapshot(filesystem_id,
421 snapshot_name)
422 if not snapshot_id:
423 message = (
424 _('Failed to create snapshot "%(snapshot)s".') %
425 {'snapshot': snapshot_name})
426 LOG.error(message)
427 raise exception.ShareBackendException(msg=message)
428 else:
429 LOG.info("Snapshot %(snapshot)s successfully created.",
430 {'snapshot': snapshot_name})
432 def delete_snapshot(self, context, snapshot, share_server):
433 """Is called to delete snapshot."""
434 snapshot_name = snapshot['name']
435 LOG.debug(f'Retrieving filesystem ID for snapshot {snapshot_name}')
436 filesystem_id = self.client.get_filesystem_id(snapshot_name)
437 LOG.debug(f'Deleting filesystem ID {filesystem_id}')
438 snapshot_deleted = self.client.delete_filesystem(filesystem_id)
439 if not snapshot_deleted:
440 message = (
441 _('Failed to delete snapshot "%(snapshot)s".') %
442 {'snapshot': snapshot_name})
443 LOG.error(message)
444 raise exception.ShareBackendException(msg=message)
445 else:
446 LOG.info("Snapshot %(snapshot)s successfully deleted.",
447 {'snapshot': snapshot_name})
449 def revert_to_snapshot(self, context, snapshot, share_access_rules,
450 snapshot_access_rules, share_server=None):
451 """Reverts a share (in place) to the specified snapshot."""
452 snapshot_name = snapshot['name']
453 snapshot_id = self.client.get_filesystem_id(snapshot_name)
454 snapshot_restored = self.client.restore_snapshot(snapshot_id)
455 if not snapshot_restored:
456 message = (
457 _('Failed to restore snapshot "%(snapshot)s".') %
458 {'snapshot': snapshot_name})
459 LOG.error(message)
460 raise exception.ShareBackendException(msg=message)
461 else:
462 LOG.info("Snapshot %(snapshot)s successfully restored.",
463 {'snapshot': snapshot_name})
465 def create_share_from_snapshot(self, context, share, snapshot,
466 share_server=None, parent_share=None):
467 """Create a share from a snapshot - clone a snapshot."""
468 LOG.debug(f'Creating {share["share_proto"]} share.')
469 locations = self._create_share_from_snapshot(share, snapshot)
471 if share['size'] != snapshot['size']: 471 ↛ 475line 471 didn't jump to line 475 because the condition on line 471 was always true
472 LOG.debug(f"Resizing {share['name']} to {share['size']}GiB")
473 self._resize_filesystem(share, share['size'])
475 return locations
477 def _create_share_from_snapshot(self, share, snapshot):
478 LOG.debug(f"Retrieving snapshot id of snapshot {snapshot['name']}")
479 snapshot_id = self.client.get_filesystem_id(snapshot['name'])
480 share_name = share['name']
481 LOG.debug(
482 f"Cloning filesystem {share_name} from snapshot {snapshot_id}"
483 )
484 filesystem_id = self.client.clone_snapshot(snapshot_id,
485 share_name)
486 if not filesystem_id:
487 message = {
488 _('The filesystem "%(export)s" was not created.') %
489 {'export': share_name}}
490 LOG.error(message)
491 raise exception.ShareBackendException(msg=message)
492 # create a share
493 nas_server_id = self.client.get_nas_server_id(self.nas_server)
494 locations = self._create_share_NFS_CIFS(nas_server_id, filesystem_id,
495 share_name,
496 share['share_proto'].upper())
497 return locations
499 def ensure_share(self, context, share, share_server):
500 """Invoked to ensure that share is exported."""
502 def setup_server(self, network_info, metadata=None):
503 """Set up and configures share server with given network parameters."""
505 def teardown_server(self, server_details, security_services=None):
506 """Teardown share server."""
508 def check_for_setup_error(self):
509 """Is called to check for setup error."""
511 def get_default_filter_function(self):
512 return 'share.size >= 3'