Coverage for manila/share/drivers/dell_emc/plugins/powerflex/connection.py: 94%
191 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"""
17PowerFlex 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.powerflex import (
28 object_manager as manager)
30"""Version history:
31 1.0 - Initial version
32"""
34VERSION = "1.0"
36CONF = cfg.CONF
38LOG = log.getLogger(__name__)
40POWERFLEX_OPTS = [
41 cfg.StrOpt('powerflex_storage_pool',
42 help='Storage pool used to provision NAS.'),
43 cfg.StrOpt('powerflex_protection_domain',
44 help='Protection domain to use.'),
45 cfg.StrOpt('dell_nas_backend_host',
46 help='Dell NAS backend hostname or IP address.'),
47 cfg.IntOpt('dell_nas_backend_port',
48 default=443,
49 help='Port number to use with the Dell NAS backend.'),
50 cfg.StrOpt('dell_nas_server',
51 help='Root directory or NAS server which owns the shares.'),
52 cfg.StrOpt('dell_nas_login',
53 help='User name for the Dell NAS backend.'),
54 cfg.StrOpt('dell_nas_password',
55 secret=True,
56 help='Password for the Dell NAS backend.')
58]
61class PowerFlexStorageConnection(driver.StorageConnection):
62 """Implements PowerFlex specific functionality for Dell Manila driver."""
64 def __init__(self, *args, **kwargs):
65 """Do initialization"""
67 LOG.debug('Invoking base constructor for Manila \
68 Dell PowerFlex SDNAS Driver.')
69 super(PowerFlexStorageConnection,
70 self).__init__(*args, **kwargs)
72 LOG.debug('Setting up attributes for Manila \
73 Dell PowerFlex SDNAS Driver.')
74 if 'configuration' in kwargs: 74 ↛ 75line 74 didn't jump to line 75 because the condition on line 74 was never true
75 kwargs['configuration'].append_config_values(POWERFLEX_OPTS)
77 self.manager = None
78 self.server = None
79 self._username = None
80 self._password = None
81 self._server_url = None
82 self._root_dir = None
83 self._verify_ssl_cert = None
84 self._shares = {}
85 self.verify_certificate = None
86 self.certificate_path = None
87 self.export_path = None
89 self.driver_handles_share_servers = False
91 self.reserved_percentage = None
92 self.reserved_snapshot_percentage = None
93 self.reserved_share_extend_percentage = None
94 self.max_over_subscription_ratio = None
96 def connect(self, dell_share_driver, context):
97 """Connects to Dell PowerFlex SDNAS server."""
98 LOG.debug('Reading configuration parameters for Manila \
99 Dell PowerFlex SDNAS Driver.')
100 config = dell_share_driver.configuration
101 get_config_value = config.safe_get
102 self.verify_certificate = get_config_value("dell_ssl_cert_verify")
103 self.rest_ip = get_config_value("dell_nas_backend_host")
104 self.rest_port = get_config_value("dell_nas_backend_port")
105 self.nas_server = get_config_value("dell_nas_server")
106 self.storage_pool = get_config_value("powerflex_storage_pool")
107 self.protection_domain = get_config_value(
108 "powerflex_protection_domain")
109 self.rest_username = get_config_value("dell_nas_login")
110 self.rest_password = get_config_value("dell_nas_password")
111 if self.verify_certificate: 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true
112 self.certificate_path = get_config_value(
113 "dell_ssl_certificate_path")
114 if not all([self.rest_ip, 114 ↛ 117line 114 didn't jump to line 117 because the condition on line 114 was never true
115 self.rest_username,
116 self.rest_password]):
117 message = _("REST server IP, username and password"
118 " must be specified.")
119 raise exception.BadConfigurationException(reason=message)
120 # validate certificate settings
121 if self.verify_certificate and not self.certificate_path: 121 ↛ 122line 121 didn't jump to line 122 because the condition on line 121 was never true
122 message = _("Path to REST server's certificate must be specified.")
123 raise exception.BadConfigurationException(reason=message)
125 LOG.debug('Initializing Dell PowerFlex SDNAS Layer.')
126 self.host_url = ("https://%(server_ip)s:%(server_port)s" %
127 {
128 "server_ip": self.rest_ip,
129 "server_port": self.rest_port})
130 LOG.info("REST server IP: %(ip)s, port: %(port)s, "
131 "username: %(user)s. Verify server's certificate: "
132 "%(verify_cert)s.",
133 {
134 "ip": self.rest_ip,
135 "port": self.rest_port,
136 "user": self.rest_username,
137 "verify_cert": self.verify_certificate,
138 })
140 self.manager = manager.StorageObjectManager(self.host_url,
141 self.rest_username,
142 self.rest_password,
143 self.export_path,
144 self.certificate_path,
145 self.verify_certificate)
147 # configuration for share status update
148 self.reserved_percentage = config.safe_get(
149 'reserved_share_percentage')
150 if self.reserved_percentage is None: 150 ↛ 153line 150 didn't jump to line 153 because the condition on line 150 was always true
151 self.reserved_percentage = 0
153 self.reserved_snapshot_percentage = config.safe_get(
154 'reserved_share_from_snapshot_percentage')
155 if self.reserved_snapshot_percentage is None: 155 ↛ 158line 155 didn't jump to line 158 because the condition on line 155 was always true
156 self.reserved_snapshot_percentage = self.reserved_percentage
158 self.reserved_share_extend_percentage = config.safe_get(
159 'reserved_share_extend_percentage')
160 if self.reserved_share_extend_percentage is None: 160 ↛ 163line 160 didn't jump to line 163 because the condition on line 160 was always true
161 self.reserved_share_extend_percentage = self.reserved_percentage
163 self.max_over_subscription_ratio = config.safe_get(
164 'max_over_subscription_ratio')
166 def create_share(self, context, share, share_server):
167 """Is called to create a share."""
168 LOG.debug(f'Creating {share["share_proto"]} share.')
169 location = self._create_nfs_share(share)
171 return location
173 def create_share_from_snapshot(self, context, share, snapshot,
174 share_server=None, parent_share=None):
175 """Is called to create a share from an existing snapshot."""
176 raise NotImplementedError()
178 def allow_access(self, context, share, access, share_server):
179 """Is called to allow access to a share."""
180 raise NotImplementedError()
182 def check_for_setup_error(self):
183 """Is called to check for setup error."""
185 def update_access(self, context, share, access_rules, add_rules,
186 delete_rules, share_server=None):
187 """Is called to update share access."""
188 LOG.debug(f'Updating access to {share["share_proto"]} share.')
189 return self._update_nfs_access(share, access_rules)
191 def create_snapshot(self, context, snapshot, share_server):
192 """Is called to create snapshot."""
193 export_name = snapshot['share_name']
194 LOG.debug(f'Retrieving filesystem ID for share {export_name}')
195 filesystem_id = self.manager.get_fsid_from_export_name(export_name)
196 LOG.debug(f'Retrieving snapshot ID for filesystem {filesystem_id}')
197 snapshot_id = self.manager.create_snapshot(snapshot['name'],
198 filesystem_id)
199 if snapshot_id:
200 LOG.info("Snapshot %(id)s successfully created.",
201 {'id': snapshot['id']})
203 def delete_snapshot(self, context, snapshot, share_server):
204 """Is called to delete snapshot."""
205 snapshot_name = snapshot['name']
206 filesystem_id = self.manager.get_fsid_from_snapshot_name(snapshot_name)
207 LOG.debug(f'Retrieving filesystem ID for snapshot {snapshot_name}')
208 snapshot_deleted = self.manager.delete_filesystem(filesystem_id)
209 if not snapshot_deleted:
210 message = (
211 _('Failed to delete snapshot "%(snapshot)s".') %
212 {'snapshot': snapshot['name']})
213 LOG.error(message)
214 raise exception.ShareBackendException(msg=message)
215 else:
216 LOG.info("Snapshot %(id)s successfully deleted.",
217 {'id': snapshot['id']})
219 def delete_share(self, context, share, share_server):
220 """Is called to delete a share."""
221 LOG.debug(f'Deleting {share["share_proto"]} share.')
222 self._delete_nfs_share(share)
224 def deny_access(self, context, share, access, share_server):
225 """Is called to deny access to a share."""
226 raise NotImplementedError()
228 def ensure_share(self, context, share, share_server):
229 """Is called to ensure a share is exported."""
231 def extend_share(self, share, new_size, share_server=None):
232 """Is called to extend a share."""
233 # Converts the size from GiB to Bytes
234 new_size_in_bytes = new_size * units.Gi
235 LOG.debug(f"Extending {share['name']} to {new_size}GiB")
236 filesystem_id = self.manager.get_filesystem_id(share['name'])
237 self.manager.extend_export(filesystem_id,
238 new_size_in_bytes)
240 def setup_server(self, network_info, metadata=None):
241 """Is called to set up a share server.
243 Requires driver_handles_share_servers to be True.
244 """
245 raise NotImplementedError()
247 def teardown_server(self, server_details, security_services=None):
248 """Is called to teardown a share server.
250 Requires driver_handles_share_servers to be True.
251 """
252 raise NotImplementedError()
254 def _create_nfs_share(self, share):
255 """Creates an NFS share.
257 In PowerFlex, an export (share) belongs to a filesystem.
258 This function creates a filesystem and an export.
259 """
260 LOG.debug(f'Retrieving Storage Pool ID for {self.storage_pool}')
261 storage_pool_id = self.manager.get_storage_pool_id(
262 self.protection_domain,
263 self.storage_pool)
264 nas_server_id = self.manager.get_nas_server_id(self.nas_server)
265 LOG.debug(f"Creating filesystem {share['name']}")
266 size_in_bytes = share['size'] * units.Gi
267 filesystem_id = self.manager.create_filesystem(storage_pool_id,
268 self.nas_server,
269 share['name'],
270 size_in_bytes)
271 if not filesystem_id:
272 message = {
273 _('The requested NFS export "%(export)s"'
274 ' was not created.') %
275 {'export': share['name']}}
276 LOG.error(message)
277 raise exception.ShareBackendException(msg=message)
279 LOG.debug(f"Creating export {share['name']}")
280 export_id = self.manager.create_nfs_export(filesystem_id,
281 share['name'])
282 if not export_id:
283 message = (
284 _('The requested NFS export "%(export)s"'
285 ' was not created.') %
286 {'export': share['name']})
287 LOG.error(message)
288 raise exception.ShareBackendException(msg=message)
289 file_interfaces = self.manager.get_nas_server_interfaces(
290 nas_server_id)
291 export_path = self.manager.get_nfs_export_name(export_id)
292 locations = self._get_nfs_location(file_interfaces,
293 export_path)
294 return locations
296 def _delete_nfs_share(self, share):
297 """Deletes a filesystem and its associated export."""
298 filesystem_id = self.manager.get_filesystem_id(share['name'])
299 LOG.debug(f"Retrieving filesystem ID for filesystem {share['name']}")
300 if filesystem_id is None:
301 message = ('Attempted to delete NFS export "%s",'
302 ' but the export does not appear to exist.')
303 LOG.warning(message, share['name'])
304 else:
305 LOG.debug(f"Deleting filesystem ID {filesystem_id}")
306 share_deleted = self.manager.delete_filesystem(filesystem_id)
307 if not share_deleted:
308 message = (
309 _('Failed to delete NFS export "%(export)s".') %
310 {'export': share['name']})
311 LOG.error(message)
312 raise exception.ShareBackendException(msg=message)
314 def _update_nfs_access(self, share, access_rules):
315 """Updates access rules for NFS share type."""
316 nfs_rw_ips = set()
317 nfs_ro_ips = set()
318 access_updates = {}
320 for rule in access_rules:
321 if rule['access_type'].lower() != 'ip':
322 message = (_("Only IP access type currently supported for "
323 "NFS. Share provided %(share)s with rule type "
324 "%(type)s") % {'share': share['display_name'],
325 'type': rule['access_type']})
326 LOG.error(message)
327 access_updates.update({rule['access_id']: {'state': 'error'}})
329 else:
330 if rule['access_level'] == const.ACCESS_LEVEL_RW:
331 nfs_rw_ips.add(rule['access_to'])
332 elif rule['access_level'] == const.ACCESS_LEVEL_RO: 332 ↛ 334line 332 didn't jump to line 334 because the condition on line 332 was always true
333 nfs_ro_ips.add(rule['access_to'])
334 access_updates.update({rule['access_id']: {'state': 'active'}})
336 share_id = self.manager.get_nfs_export_id(share['name'])
337 share_updated = self.manager.set_export_access(share_id,
338 nfs_rw_ips,
339 nfs_ro_ips)
340 if not share_updated:
341 message = (
342 _('Failed to update NFS access rules for "%(export)s".') %
343 {'export': share['display_name']})
344 LOG.error(message)
345 raise exception.ShareBackendException(msg=message)
346 return access_updates
348 def update_share_stats(self, stats_dict):
349 """Retrieve stats info from share."""
350 stats_dict['driver_version'] = VERSION
351 stats_dict['storage_protocol'] = 'NFS'
352 stats_dict['create_share_from_snapshot_support'] = False
353 stats_dict['pools'] = []
354 storage_pool_id = self.manager.get_storage_pool_id(
355 self.protection_domain,
356 self.storage_pool
357 )
358 total = free = used = provisioned = 0
359 statistic = self.manager.get_storage_pool_statistic(storage_pool_id)
360 if statistic: 360 ↛ 365line 360 didn't jump to line 365 because the condition on line 360 was always true
361 total = statistic.get('maxCapacityInKb') // units.Mi
362 free = statistic.get('netUnusedCapacityInKb') // units.Mi
363 used = statistic.get('capacityInUseInKb') // units.Mi
364 provisioned = statistic.get('primaryVacInKb') // units.Mi
365 pool_stat = {
366 'pool_name': self.storage_pool,
367 'thin_provisioning': True,
368 'total_capacity_gb': total,
369 'free_capacity_gb': free,
370 'allocated_capacity_gb': used,
371 'provisioned_capacity_gb': provisioned,
372 'qos': False,
373 'reserved_percentage': self.reserved_percentage,
374 'reserved_snapshot_percentage':
375 self.reserved_snapshot_percentage,
376 'reserved_share_extend_percentage':
377 self.reserved_share_extend_percentage,
378 'max_over_subscription_ratio':
379 self.max_over_subscription_ratio
380 }
381 stats_dict['pools'].append(pool_stat)
383 def _get_nfs_location(self, file_interfaces, export_path):
384 export_locations = []
385 for interface in file_interfaces:
386 export_locations.append(
387 {'path': f"{interface}:/{export_path}"})
388 return export_locations
390 def get_default_filter_function(self):
391 return 'share.size >= 3'