Coverage for manila/share/drivers/dell_emc/plugins/powerscale/powerscale.py: 97%
498 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 2015 EMC Corporation
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"""
17PowerScale specific NAS backend plugin.
18"""
19import os
21from oslo_config import cfg
22from oslo_log import log
23from oslo_utils import units
25from manila.common import constants as const
26from manila import exception
27from manila.i18n import _
28from manila.share.drivers.dell_emc.plugins import base
29from manila.share.drivers.dell_emc.plugins.powerscale import powerscale_api
31"""Version history:
32 0.1.0 - Initial version
33 1.0.0 - Fix Http auth issue, SSL verification error and etc
34 1.0.1 - Add support for update share stats
35 1.0.2 - Add support for ensure shares
36 1.0.3 - Add support for thin provisioning
37 1.0.4 - Rename isilon to powerscale
38 1.0.5 - Add support for share shrink
39 1.0.6 - Add support of manage/unmanage share and snapshot
40 1.0.7 - Add support of mount snapshot
41 1.0.8 - Add support of mount point name
43"""
44VERSION = "1.0.8"
46CONF = cfg.CONF
48LOG = log.getLogger(__name__)
50POWERSCALE_OPTS = [
51 cfg.StrOpt('powerscale_dir_permission',
52 default='0777',
53 help='Predefined ACL value or POSIX mode '
54 'for PowerScale directories.'),
55 cfg.IntOpt('powerscale_threshold_limit',
56 default=0,
57 help='Specifies the threshold limit (in percentage) '
58 'for triggering SmartQuotas alerts in PowerScale')
59]
62class PowerScaleStorageConnection(base.StorageConnection):
63 """Implements PowerScale specific functionality for EMC Manila driver."""
65 def __init__(self, *args, **kwargs):
66 super(PowerScaleStorageConnection, self).__init__(*args, **kwargs)
67 LOG.debug('Setting up attributes for Manila '
68 'Dell PowerScale Driver.')
69 if 'configuration' in kwargs: 69 ↛ 70line 69 didn't jump to line 70 because the condition on line 69 was never true
70 kwargs['configuration'].append_config_values(POWERSCALE_OPTS)
72 self._server = None
73 self._port = None
74 self._username = None
75 self._password = None
76 self._server_url = None
77 self._root_dir = None
78 self._verify_ssl_cert = None
79 self._ssl_cert_path = None
80 self._containers = {}
81 self._shares = {}
82 self._snapshots = {}
84 self._powerscale_api = None
85 self.driver_handles_share_servers = False
86 self.ipv6_implemented = True
87 self.manage_existing_snapshot_support = True
88 # props for share status update
89 self.reserved_percentage = None
90 self.reserved_snapshot_percentage = None
91 self.reserved_share_extend_percentage = None
92 self.max_over_subscription_ratio = None
93 self._threshold_limit = 0
94 self.shrink_share_support = True
95 self.manage_existing_support = True
96 self.mount_snapshot_support = True
97 self._snapshot_root_dir = '/ifs/.snapshot'
99 def _get_container_path(self, share):
100 """Return path to a container."""
101 return os.path.join(self._root_dir, share['name'])
103 def _get_snapshot_path(self, snapshot):
104 return os.path.join(self._snapshot_root_dir, snapshot['name'])
106 def create_share(self, context, share, share_server):
107 """Is called to create share."""
108 LOG.debug(f'Creating {share["share_proto"]} share.')
109 if share['share_proto'] == 'NFS':
110 location = self._create_nfs_share(share)
111 elif share['share_proto'] == 'CIFS':
112 location = self._create_cifs_share(share)
113 else:
114 message = (_('Unsupported share protocol: %(proto)s.') %
115 {'proto': share['share_proto']})
116 LOG.error(message)
117 raise exception.InvalidShare(reason=message)
119 # apply directory quota based on share size
120 max_share_size = share['size'] * units.Gi
121 self._powerscale_api.quota_create(
122 self._get_container_path(share), 'directory', max_share_size)
124 return location
126 def create_share_from_snapshot(self, context, share, snapshot,
127 share_server):
128 """Creates a share from the snapshot."""
129 LOG.debug(f'Creating {share["share_proto"]} share from snapshot.')
130 # Create share at new location
131 location = self.create_share(context, share, share_server)
133 # Clone snapshot to new location
134 fq_target_dir = self._get_container_path(share)
135 self._powerscale_api.clone_snapshot(snapshot['name'], fq_target_dir,
136 snapshot['provider_location'])
138 return location
140 def _create_nfs_share(self, share):
141 """Is called to create nfs share."""
142 LOG.debug(f'Creating NFS share {share["name"]}.')
143 # Create directory
144 container_path = self._get_container_path(share)
145 path = {}
146 self._create_directory(container_path)
147 # Create nfs share
148 share_created = self._powerscale_api.create_nfs_export(container_path)
149 if not share_created:
150 message = (
151 _('The requested NFS share "%(share)s" was not created.') %
152 {'share': share['name']})
153 LOG.error(message)
154 raise exception.ShareBackendException(msg=message)
155 format_path = self._format_nfs_path(container_path)
156 path[format_path] = True
157 if share.get('mount_point_name'):
158 path[format_path] = False
159 mount_point_name = (
160 self._format_nfs_mount_point_name(
161 share.get('mount_point_name')))
162 alias_created = (self._powerscale_api.
163 create_nfs_export_aliases(mount_point_name,
164 container_path))
165 format_path = self._format_nfs_path(mount_point_name)
166 path[format_path] = True
167 if not alias_created:
168 message = (
169 _('Failed to create NFS alias for "%(share)s".') %
170 {'share': share['name']})
171 LOG.error(message)
172 raise exception.ShareBackendException(msg=message)
173 return self._get_location(path)
175 def _create_cifs_share(self, share):
176 """Is called to create cifs share."""
177 LOG.debug(f'Creating CIFS share {share["name"]}.')
178 # Create directory
179 container_path = self._get_container_path(share)
180 self._create_directory(container_path)
181 # Create smb share
182 share_name = share['name']
183 if share.get('mount_point_name'):
184 share_name = share.get('mount_point_name')
185 share_created = self._powerscale_api.create_smb_share(
186 share_name, container_path)
187 if not share_created:
188 message = (
189 _('The requested CIFS share "%(share)s" was not created.') %
190 {'share': share['name']})
191 LOG.error(message)
192 raise exception.ShareBackendException(msg=message)
193 location = (self.
194 _get_location({self._format_smb_path(share_name): True}))
195 return location
197 def _create_directory(self, path, recursive=False):
198 """Is called to create a directory."""
199 dir_created = self._powerscale_api.create_directory(path, recursive)
200 if not dir_created:
201 message = (
202 _('Failed to create directory "%(dir)s".') %
203 {'dir': path})
204 LOG.error(message)
205 raise exception.ShareBackendException(msg=message)
207 def create_snapshot(self, context, snapshot, share_server):
208 """Is called to create snapshot."""
209 LOG.debug(f'Creating snapshot {snapshot["name"]}.')
210 snapshot_path = os.path.join(self._root_dir, snapshot['share_name'])
211 snap_id = self._powerscale_api.create_snapshot(
212 snapshot['name'], snapshot_path)
213 result = {}
214 if snap_id is None:
215 message = (
216 _('Failed to create snapshot "%(snap)s".') %
217 {'snap': snapshot['name']})
218 LOG.error(message)
219 raise exception.ShareBackendException(msg=message)
220 result['provider_location'] = snap_id
221 if snapshot['share']['mount_snapshot_support']:
222 snap_result = self._create_snap_export_path(snapshot=snapshot,
223 snap_actual_name=None)
224 result.update(snap_result)
225 return result
227 def delete_share(self, context, share, share_server):
228 """Is called to remove share."""
229 LOG.debug(f'Deleting {share["share_proto"]} share.')
230 if share['share_proto'] == 'NFS':
231 self._delete_nfs_share(share)
232 elif share['share_proto'] == 'CIFS':
233 self._delete_cifs_share(share)
234 else:
235 message = (_('Unsupported share type: %(type)s.') %
236 {'type': share['share_proto']})
237 LOG.warning(message)
238 return
240 dir_path = self._get_container_path(share)
241 # remove quota
242 self._delete_quota(dir_path)
243 # remove directory
244 self._delete_directory(dir_path)
246 def _delete_quota(self, path):
247 """Is called to remove quota."""
248 quota = self._powerscale_api.quota_get(path, 'directory')
249 if quota:
250 LOG.debug(f'Removing quota {quota["id"]}')
251 deleted = self._powerscale_api.delete_quota(quota['id'])
252 if not deleted:
253 message = (
254 _('Failed to delete quota "%(quota_id)s" for '
255 'directory "%(dir)s".') %
256 {'quota_id': quota['id'], 'dir': path})
257 LOG.error(message)
258 else:
259 LOG.warning(f'Quota not found for {path}')
261 def _delete_directory(self, path):
262 """Is called to remove directory."""
263 path_exist = self._powerscale_api.is_path_existent(path)
264 if path_exist:
265 LOG.debug(f'Removing directory {path}')
266 deleted = self._powerscale_api.delete_path(path, recursive=True)
267 if not deleted:
268 message = (
269 _('Failed to delete directory "%(dir)s".') %
270 {'dir': path})
271 LOG.error(message)
272 else:
273 LOG.warning(f'Directory not found for {path}')
275 def _delete_nfs_share(self, share):
276 """Is called to remove nfs share."""
277 container_path = self._get_container_path(share)
278 self._delete_export("NFS", share['name'], container_path)
279 if share.get('mount_point_name'):
280 self._delete_nfs_aliases(share['mount_point_name'],
281 container_path, share['name'])
283 def _delete_nfs_aliases(self, mount_point_name,
284 container_path, share_name):
285 mount_point_name = (
286 self._format_nfs_mount_point_name(mount_point_name))
287 valid_alias = self._check_valid_aliases(mount_point_name,
288 container_path)
289 if valid_alias:
290 alias_deleted = (
291 self._powerscale_api.
292 delete_nfs_export_aliases(mount_point_name))
293 if not alias_deleted:
294 message = (_('Error deleting NFS alias for '
295 'share: %s') % share_name)
296 LOG.error(message)
297 raise exception.ShareBackendException(msg=message)
298 else:
299 message = (_("NFS alias %(alias_name)s is exist "
300 "with another share path.") %
301 {'alias_name': mount_point_name})
302 LOG.warning(message)
304 def _delete_cifs_share(self, share):
305 """Is called to remove CIFS share."""
306 share_name = share['name']
307 container_path = self._get_container_path(share)
308 if share.get('mount_point_name'):
309 share_name = share.get('mount_point_name')
310 self._delete_export("CIFS", share_name, container_path)
312 def delete_snapshot(self, context, snapshot, share_server):
313 """Is called to remove snapshot."""
314 snap_name = snapshot['name']
315 proto = snapshot['share']['share_proto']
316 snap_path = self._get_snapshot_path(snapshot)
317 LOG.debug('Deleting snapshot %s', snap_name)
318 if snapshot.get('provider_location'):
319 deleted = (self._powerscale_api.
320 delete_snapshot_by_id(snapshot['provider_location']))
321 else:
322 deleted = self._powerscale_api.delete_snapshot(snapshot['name'])
323 if not deleted:
324 message = (_('Failed to delete snapshot "%(snap)s".') %
325 {'snap': snapshot['name']})
326 LOG.error(message)
327 raise exception.ShareBackendException(msg=message)
328 self._delete_export(proto, snap_name, snap_path)
330 def ensure_share(self, context, share, share_server):
331 """Invoked to ensure that share is exported."""
332 raise NotImplementedError()
334 def extend_share(self, share, new_size, share_server=None):
335 """Extends a share."""
336 LOG.debug('Extending share %(name)s to %(size)sG.', {
337 'name': share['name'], 'size': new_size
338 })
339 new_quota_size = new_size * units.Gi
340 self._powerscale_api.quota_set(
341 self._get_container_path(share), 'directory', new_quota_size)
343 def manage_existing(self, share, driver_options):
344 """Import an external NFS/CIFS share into Manila."""
345 export_path = (
346 share.get('export_location')
347 or share.get('export_locations', [None])[0]
348 )
349 protocol = share.get('share_proto')
350 LOG.info(
351 "Managing existing share with protocol: %s, export path: %s",
352 protocol,
353 export_path,
354 )
355 if protocol == 'NFS':
356 nfs_path = export_path.split(':', 1)[1]
357 export_id = self._powerscale_api.lookup_nfs_export(nfs_path)
358 if not export_id:
359 raise exception.ShareBackendException(
360 msg=f"NFS export {nfs_path} not found."
361 )
362 backend_quota_path = nfs_path
363 export_location = export_path
364 elif protocol == 'CIFS': 364 ↛ 380line 364 didn't jump to line 380 because the condition on line 364 was always true
365 share_name = export_path.split('\\')[-1]
366 smb_share = self._powerscale_api.lookup_smb_share(share_name)
367 if not smb_share:
368 raise exception.ShareBackendException(
369 msg=f"CIFS share {share_name} not found."
370 )
371 backend_quota_path = smb_share.get('path')
372 if not backend_quota_path:
373 raise exception.ShareBackendException(
374 msg=(
375 "Unable to resolve OneFS path for CIFS share "
376 f"{share_name}."
377 )
378 )
379 export_location = export_path
380 share_quota = self._powerscale_api.quota_get(
381 backend_quota_path, 'directory'
382 )
383 size_bytes = (
384 share_quota.get('thresholds', {}).get('hard')
385 if share_quota else 0
386 )
387 if not size_bytes:
388 raise exception.ManageInvalidShare(
389 reason=(
390 "Managing existing share requires a directory quota hard "
391 "limit on backend path %s." % backend_quota_path
392 )
393 )
394 size_gb = size_bytes // units.Gi
395 return {
396 'size': size_gb,
397 'export_locations': [export_location],
398 }
400 def shrink_share(self, share, new_size, share_server=None):
401 """Shrink a share by lowering its directory hard quota.
403 Rejects shrink if current logical usage exceeds the requested size.
404 """
405 LOG.debug(
406 'Shrinking share %(name)s to %(size)sG.',
407 {'name': share['name'], 'size': new_size}
408 )
409 path = self._get_container_path(share)
410 quota_json = self._powerscale_api.quota_get(path, 'directory')
411 used_bytes = 0
412 if quota_json: 412 ↛ 414line 412 didn't jump to line 414 because the condition on line 412 was always true
413 used_bytes = quota_json.get('usage', {}).get('logical', 0)
414 new_quota_bytes = new_size * units.Gi
415 if used_bytes > new_quota_bytes:
416 message = (_(
417 'Cannot shrink share "%(name)s" to %(size)sG: '
418 'used space (%(used)s bytes) exceeds new size.'
419 ) % {
420 'name': share['name'],
421 'size': new_size,
422 'used': used_bytes
423 })
424 LOG.error(message)
425 raise exception.ShareShrinkingPossibleDataLoss(
426 share_id=share.get('id', share.get('name')),
427 reason=message,
428 )
429 self._powerscale_api.quota_set(path, 'directory', new_quota_bytes)
431 def allow_access(self, context, share, access, share_server):
432 """Allow access to the share."""
433 raise NotImplementedError()
435 def deny_access(self, context, share, access, share_server):
436 """Deny access to the share."""
437 raise NotImplementedError()
439 def check_for_setup_error(self):
440 """Check for setup error."""
442 def connect(self, emc_share_driver, context):
443 """Connect to an PowerScale cluster."""
444 LOG.debug('Reading configuration parameters for Manila'
445 ' Dell PowerScale Driver.')
446 config = emc_share_driver.configuration
447 self._server = config.safe_get("emc_nas_server")
448 self._port = config.safe_get("emc_nas_server_port")
449 self._username = config.safe_get("emc_nas_login")
450 self._password = config.safe_get("emc_nas_password")
451 self._root_dir = config.safe_get("emc_nas_root_dir")
452 self._threshold_limit = config.safe_get("powerscale_threshold_limit")
454 # validate IP, username and password
455 if not all([self._server,
456 self._username,
457 self._password]):
458 message = _("REST server IP, username and password"
459 " must be specified.")
460 raise exception.BadConfigurationException(reason=message)
462 self._server_url = f'https://{self._server}:{self._port}'
464 self._verify_ssl_cert = config.safe_get("emc_ssl_cert_verify")
465 if self._verify_ssl_cert: 465 ↛ 466line 465 didn't jump to line 466 because the condition on line 465 was never true
466 self._ssl_cert_path = config.safe_get("emc_ssl_cert_path")
467 self._dir_permission = config.safe_get("powerscale_dir_permission")
468 self._powerscale_api = powerscale_api.PowerScaleApi(
469 self._server_url, self._username, self._password,
470 self._verify_ssl_cert, self._ssl_cert_path,
471 self._dir_permission,
472 self._threshold_limit)
474 if not self._powerscale_api.is_path_existent(self._root_dir):
475 self._create_directory(self._root_dir, recursive=True)
477 # configuration for share status update
478 self.reserved_percentage = config.safe_get(
479 'reserved_share_percentage')
480 if self.reserved_percentage is None: 480 ↛ 483line 480 didn't jump to line 483 because the condition on line 480 was always true
481 self.reserved_percentage = 0
483 self.reserved_snapshot_percentage = config.safe_get(
484 'reserved_share_from_snapshot_percentage')
485 if self.reserved_snapshot_percentage is None: 485 ↛ 488line 485 didn't jump to line 488 because the condition on line 485 was always true
486 self.reserved_snapshot_percentage = self.reserved_percentage
488 self.reserved_share_extend_percentage = config.safe_get(
489 'reserved_share_extend_percentage')
490 if self.reserved_share_extend_percentage is None: 490 ↛ 493line 490 didn't jump to line 493 because the condition on line 490 was always true
491 self.reserved_share_extend_percentage = self.reserved_percentage
493 self.max_over_subscription_ratio = config.safe_get(
494 'max_over_subscription_ratio')
496 def update_share_stats(self, stats_dict):
497 """Retrieve stats info from share."""
498 stats_dict['driver_version'] = VERSION
499 stats_dict['storage_protocol'] = 'NFS_CIFS'
500 # PowerScale does not support pools.
501 # To align with manila scheduler 'pool-aware' strategic,
502 # report with one pool structure.
503 pool_stat = {
504 'pool_name': stats_dict['share_backend_name'],
505 'qos': False,
506 'reserved_percentage': self.reserved_percentage,
507 'reserved_snapshot_percentage':
508 self.reserved_snapshot_percentage,
509 'reserved_share_extend_percentage':
510 self.reserved_share_extend_percentage,
511 'max_over_subscription_ratio':
512 self.max_over_subscription_ratio,
513 'thin_provisioning': True,
514 'mount_snapshot_support': True,
515 'mount_point_name_support': True,
516 }
517 spaces = self._powerscale_api.get_space_stats()
518 if spaces: 518 ↛ 521line 518 didn't jump to line 521 because the condition on line 518 was always true
519 pool_stat['total_capacity_gb'] = spaces['total'] // units.Gi
520 pool_stat['free_capacity_gb'] = spaces['free'] // units.Gi
521 allocated_space = self._powerscale_api.get_allocated_space()
522 pool_stat['allocated_capacity_gb'] = allocated_space
524 stats_dict['pools'] = [pool_stat]
526 def get_network_allocations_number(self):
527 """Returns number of network allocations for creating VIFs."""
528 # TODO(Shaun Edwards)
529 return 0
531 def setup_server(self, network_info, metadata=None):
532 """Set up and configures share server with given network parameters."""
533 # TODO(Shaun Edwards): Look into supporting share servers
535 def teardown_server(self, server_details, security_services=None):
536 """Teardown share server."""
537 # TODO(Shaun Edwards): Look into supporting share servers
539 def update_access(self, context, share, access_rules, add_rules,
540 delete_rules, share_server=None):
541 """Update share access."""
542 share_name = share["name"]
543 LOG.debug(f'Updaing access for share {share_name}.')
544 state_map = {}
545 if share['share_proto'] == 'NFS':
546 path = self._get_container_path(share)
547 state_map = self._update_access_nfs(share_name, path, access_rules)
548 if share['share_proto'] == 'CIFS':
549 state_map = self._update_access_cifs(share_name, access_rules)
550 return state_map
552 def _update_access_nfs(self, share_name, path, access_rules):
553 """Updates access on a NFS share."""
554 nfs_rw_ips = set()
555 nfs_ro_ips = set()
556 rule_state_map = {}
557 for rule in access_rules:
558 rule_state_map[rule['access_id']] = {
559 'state': 'error'
560 }
562 for rule in access_rules:
563 access_level = const.ACCESS_LEVEL_RO
564 if rule.get('access_level'): 564 ↛ 566line 564 didn't jump to line 566 because the condition on line 564 was always true
565 access_level = rule.get('access_level')
566 if access_level == const.ACCESS_LEVEL_RW:
567 nfs_rw_ips.add(rule['access_to'])
568 elif access_level == const.ACCESS_LEVEL_RO: 568 ↛ 562line 568 didn't jump to line 562 because the condition on line 568 was always true
569 nfs_ro_ips.add(rule['access_to'])
571 export_id = self._powerscale_api.lookup_nfs_export(path)
572 if export_id is None:
573 # share does not exist on backend (set all rules to error state)
574 message = _('Failed to update access for NFS share %s: '
575 'share not found.') % share_name
576 LOG.error(message)
577 return rule_state_map
579 r = self._powerscale_api.modify_nfs_export_access(
580 export_id, ro_ips=list(nfs_ro_ips), rw_ips=list(nfs_rw_ips))
581 if not r:
582 return rule_state_map
584 # if we finish the bulk rule update with no error set rules to active
585 for rule in access_rules:
586 rule_state_map[rule['access_id']]['state'] = 'active'
587 return rule_state_map
589 def _update_access_cifs(self, share_name, access_rules, read_only=False):
590 """Update access on a CIFS share."""
591 rule_state_map = {}
592 ip_access_rules = []
593 user_access_rules = []
594 for rule in access_rules:
595 if rule['access_type'] == 'ip':
596 ip_access_rules.append(rule)
597 elif rule['access_type'] == 'user':
598 user_access_rules.append(rule)
599 else:
600 message = (_("Access type %(type)s is not supported for CIFS."
601 ) % {'type': rule['access_type']})
602 LOG.error(message)
603 rule_state_map.update({rule['access_id']: {'state': 'error'}})
604 ips = self._get_cifs_ip_list(ip_access_rules, rule_state_map,
605 read_only)
606 user_permissions = self._get_cifs_user_permissions(
607 user_access_rules, rule_state_map)
608 if read_only and len(user_permissions) == 0:
609 user_permissions = [{
610 "permission": "read",
611 "permission_type": "allow",
612 "trustee": {
613 "id": "SID:S-1-1-0",
614 "name": "Everyone",
615 "type": "wellknown"
616 }
617 }]
618 share_updated = self._powerscale_api.modify_smb_share_access(
619 share_name,
620 host_acl=ips,
621 permissions=user_permissions)
623 if not share_updated:
624 message = (
625 _(
626 'Failed to update access rules for CIFS share '
627 '"%(share)s".'
628 ) % {'share': share_name}
629 )
630 LOG.error(message)
631 for rule in access_rules:
632 rule_state_map[rule['access_id']] = {
633 'state': 'error'
634 }
636 return rule_state_map
638 def _get_cifs_ip_list(self, access_rules, rule_state_map, read_only):
639 """Get CIFS ip list."""
640 cifs_ips = []
641 for rule in access_rules:
642 if not read_only and rule['access_level'] != const.ACCESS_LEVEL_RW:
643 message = ('Only RW access level is supported '
644 'for CIFS IP access.')
645 LOG.error(message)
646 rule_state_map.update({rule['access_id']: {'state': 'error'}})
647 continue
648 cifs_ips.append('allow:' + rule['access_to'])
649 rule_state_map.update({rule['access_id']: {'state': 'active'}})
650 if len(cifs_ips) > 0:
651 cifs_ips.append('deny:ALL')
652 return cifs_ips
654 def _get_cifs_user_permissions(self, access_rules, rule_state_map):
655 """Get CIFS user permissions."""
656 cifs_user_permissions = []
657 for rule in access_rules:
658 access_level = const.ACCESS_LEVEL_RO
659 smb_permission = powerscale_api.SmbPermission.ro
660 if rule.get('access_level'): 660 ↛ 662line 660 didn't jump to line 662 because the condition on line 660 was always true
661 access_level = rule.get('access_level')
662 if access_level == const.ACCESS_LEVEL_RW:
663 smb_permission = powerscale_api.SmbPermission.rw
664 elif access_level == const.ACCESS_LEVEL_RO:
665 smb_permission = powerscale_api.SmbPermission.ro
666 user_sid = self._powerscale_api.get_user_sid(rule['access_to'])
667 if user_sid:
668 cifs_user_permissions.append({
669 'permission': smb_permission.value,
670 'permission_type': 'allow',
671 'trustee': user_sid
672 })
673 rule_state_map.update({rule['access_id']: {'state': 'active'}})
674 else:
675 message = _('Failed to get user sid by %(user)s.' %
676 {'user': rule['access_to']})
677 LOG.error(message)
678 rule_state_map.update({rule['access_id']: {'state': 'error'}})
679 return cifs_user_permissions
681 def get_backend_info(self, context):
682 """Get driver and array configuration parameters.
684 :returns: A dictionary containing driver-specific info.
685 """
686 LOG.debug("Retrieving PowerScale backend info.")
687 cluster_version = self._powerscale_api.get_cluster_version()
688 return {'driver_version': VERSION,
689 'cluster_version': cluster_version,
690 'rest_server': self._server,
691 'rest_port': self._port}
693 def ensure_shares(self, context, shares):
694 """Invoked to ensure that shares are exported.
696 :shares: A list of all shares for updates.
697 :returns: None or a dictionary of updates in the format.
698 """
699 LOG.debug("Ensuring PowerScale shares.")
700 updates = {}
701 for share in shares:
702 if share['share_proto'] == 'NFS':
703 container_path = self._get_container_path(share)
704 share_id = self._powerscale_api.lookup_nfs_export(
705 container_path)
706 if share_id:
707 location = [self._format_nfs_path(container_path)]
708 if share.get('mount_point_name'):
709 location.append(self._format_nfs_path(
710 self._format_nfs_mount_point_name(
711 share.get('mount_point_name'))
712 ))
713 updates[share['id']] = {
714 'export_locations': location,
715 'status': 'available',
716 'reapply_access_rules': True,
717 }
718 else:
719 LOG.warning(f'NFS Share {share["name"]} is not found.')
720 elif share['share_proto'] == 'CIFS': 720 ↛ 736line 720 didn't jump to line 736 because the condition on line 720 was always true
721 share_name = share['name']
722 if share.get('mount_point_name'):
723 share_name = share.get('mount_point_name')
724 smb_share = self._powerscale_api.lookup_smb_share(
725 share_name)
726 if smb_share:
727 location = self._format_smb_path(share_name)
728 updates[share['id']] = {
729 'export_locations': [location],
730 'status': 'available',
731 'reapply_access_rules': True,
732 }
733 else:
734 LOG.warning(f'CIFS Share {share["name"]} is not found.')
736 if share['id'] not in updates:
737 updates[share['id']] = {
738 'export_locations': [],
739 'status': 'error',
740 'reapply_access_rules': False,
741 }
742 return updates
744 def _format_smb_path(self, share_name):
745 return '\\\\{0}\\{1}'.format(self._server, share_name)
747 def _format_nfs_path(self, container_path):
748 return '{0}:{1}'.format(self._server, container_path)
750 def _get_location(self, paths):
751 return [
752 {
753 'path': path,
754 'is_admin_only': False,
755 'metadata': {'preferred': bool(is_preferred)}
756 }
757 for path, is_preferred in paths.items()
758 ]
760 def _format_nfs_mount_point_name(self, mount_point_name):
761 return '/{0}'.format(mount_point_name)
763 def _check_valid_aliases(self, mount_point_name,
764 container_path):
765 alias_details = (self.
766 _powerscale_api.
767 get_nfs_export_aliases(mount_point_name))
768 return alias_details['path'] == container_path
770 def manage_existing_snapshot(self, snapshot, driver_options):
771 """Brings an existing snapshot under Manila management."""
772 provider_location = snapshot.get('provider_location')
773 snap = self._powerscale_api.get_snapshot_id(provider_location)
774 if not snap:
775 message = ("Could not find a snapshot in the backend with "
776 "ID: %s, please make sure "
777 "the snapshot exists in the backend."
778 % provider_location)
779 LOG.error(message)
780 raise exception.ManageInvalidShareSnapshot(reason=message)
781 elif (snap['path'] !=
782 self._get_container_path(snapshot['share'])):
783 message = ("Snapshot does not belong to the given share %s."
784 % snapshot['share']['name'])
785 LOG.error(message)
786 raise exception.ManageInvalidShareSnapshot(reason=message)
787 try:
788 snapshot_size = int(driver_options.get("size", 0))
789 except (ValueError, TypeError):
790 msg = _("The size in driver options to manage snapshot "
791 "%(snap_id)s should be an integer, in format "
792 "driver-options size=<SIZE>. Value passed: "
793 "%(size)s.") % {'snap_id': snapshot['id'],
794 'size': driver_options.get("size")}
795 LOG.error(msg)
796 raise exception.ManageInvalidShareSnapshot(reason=msg)
798 if not snapshot_size:
799 msg = _("Snapshot %(snap_id)s has no specified size. "
800 "Use default value to share size, "
801 "set size in driver options if you "
802 "want.") % {'snap_id': snapshot['id']}
803 LOG.info(msg)
804 snapshot_size = snapshot['share']['size']
805 LOG.info("Snapshot %(provider_location)s in "
806 "PowerScale will be managed "
807 "with ID %(snapshot_id)s.",
808 {'provider_location': snapshot.get('provider_location'),
809 'snapshot_id': snapshot['id']})
810 manage_snap_result = {"size": snapshot_size,
811 "provider_location": provider_location}
812 if snapshot['share']['mount_snapshot_support']:
813 snap_result = self._create_snap_export_path(
814 snapshot=snapshot,
815 snap_actual_name=snap['name']
816 )
817 manage_snap_result.update(snap_result)
818 return manage_snap_result
820 def _create_snap_export_path(self, snapshot, snap_actual_name=None):
821 share_proto = snapshot['share']['share_proto']
822 snap_export_path = self._get_snapshot_path(snapshot)
823 snap_name = snapshot['name']
824 if snap_actual_name:
825 snap_name = snap_actual_name
826 snap_export_path = os.path.join(self._snapshot_root_dir,
827 snap_name)
828 created = True
829 export_path = None
830 if share_proto == "NFS":
831 share_export = (self._powerscale_api.
832 lookup_nfs_export(snap_export_path))
833 if share_export is None:
834 created = (self.
835 _powerscale_api.
836 create_snapshot_nfs_export(snap_export_path))
837 export_path = self._format_nfs_path(snap_export_path)
838 elif share_proto == "CIFS": 838 ↛ 846line 838 didn't jump to line 846 because the condition on line 838 was always true
839 smb_export = (self._powerscale_api.
840 lookup_smb_share(snap_name))
841 if smb_export is None: 841 ↛ 845line 841 didn't jump to line 845 because the condition on line 841 was always true
842 created = (self._powerscale_api.
843 create_snapshot_smb_export(snap_name,
844 snap_export_path))
845 export_path = self._format_smb_path(snap_name)
846 if not created:
847 msg = _('Failed to create snapshot export path '
848 '"%(snap)s".') % {'snap': snap_name}
849 LOG.error(msg)
850 raise exception.ShareBackendException(msg=msg)
851 return {'export_locations': self._get_location({export_path: True})}
853 def _delete_export(self, proto, name, path=None):
854 if proto == "NFS":
855 share_id = self._powerscale_api.lookup_nfs_export(path)
856 if share_id is None:
857 LOG.warning('Attempted to delete NFS export "%s", '
858 'but it does not exist.', name)
859 return
860 if not self._powerscale_api.delete_nfs_share(share_id):
861 msg = _('Error deleting NFS export: %s') % name
862 LOG.error(msg)
863 raise exception.ShareBackendException(msg=msg)
864 elif proto == "CIFS": 864 ↛ exitline 864 didn't return from function '_delete_export' because the condition on line 864 was always true
865 smb_share = self._powerscale_api.lookup_smb_share(name)
866 if smb_share is None:
867 LOG.warning('Attempted to delete CIFS export "%s", '
868 'but it does not exist.', name)
869 return
870 elif smb_share['path'] == path:
871 share_deleted = self._powerscale_api.delete_smb_share(name)
872 if not share_deleted:
873 message = (_('Error deleting CIFS share: %s') %
874 name)
875 LOG.error(message)
876 raise exception.ShareBackendException(msg=message)
877 else:
878 message = _('CIFS share "%s": the delete operation '
879 'failed because the share path '
880 'does not match the expected path.') % name
881 LOG.warning(message)
883 def snapshot_update_access(self, context, snapshot,
884 access_rules, add_rules=None,
885 delete_rules=None, share_server=None):
886 """Update snapshot access."""
887 snapshot_name = snapshot["name"]
888 LOG.debug(f'Updating access for snapshot {snapshot_name}.')
889 share_proto = snapshot['share']['share_proto']
890 state_map = {}
891 if share_proto == 'NFS':
892 path = self._get_snapshot_path(snapshot)
893 state_map = self._update_access_nfs(snapshot_name,
894 path, access_rules)
895 if share_proto == 'CIFS':
896 state_map = self._update_access_cifs(snapshot_name,
897 access_rules,
898 read_only=True)
899 return state_map