Coverage for manila/share/drivers/infinidat/infinibox.py: 100%
314 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 2022 Infinidat Ltd.
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"""
16INFINIDAT InfiniBox Share Driver
17"""
19import functools
21import ipaddress
22from oslo_config import cfg
23from oslo_log import log as logging
24from oslo_utils import units
25import requests
27from manila.common import constants
28from manila import exception
29from manila.i18n import _
30from manila.share import driver
31from manila.share import utils
32from manila import version
34try:
35 import capacity
36except ImportError:
37 capacity = None
39try:
40 import infinisdk
41except ImportError:
42 infinisdk = None
45LOG = logging.getLogger(__name__)
47infinidat_connection_opts = [
48 cfg.HostAddressOpt('infinibox_hostname',
49 help='The name (or IP address) for the INFINIDAT '
50 'Infinibox storage system.'),
51 cfg.BoolOpt('infinidat_use_ssl',
52 help='Use SSL to connect to the INFINIDAT Infinibox storage '
53 'system.',
54 default=False),
55 cfg.BoolOpt('infinidat_suppress_ssl_warnings',
56 help='Suppress requests library SSL certificate warnings.',
57 default=False), ]
59infinidat_auth_opts = [
60 cfg.StrOpt('infinibox_login',
61 help=('Administrative user account name used to access the '
62 'INFINIDAT Infinibox storage system.')),
63 cfg.StrOpt('infinibox_password',
64 help=('Password for the administrative user account '
65 'specified in the infinibox_login option.'),
66 secret=True), ]
68infinidat_general_opts = [
69 cfg.StrOpt('infinidat_pool_name',
70 help='Name of the pool from which volumes are allocated.'),
71 cfg.StrOpt('infinidat_nas_network_space_name',
72 help='Name of the NAS network space on the INFINIDAT '
73 'InfiniBox.'),
74 cfg.BoolOpt('infinidat_thin_provision', help='Use thin provisioning.',
75 default=True),
76 cfg.BoolOpt('infinidat_snapdir_accessible',
77 help=('Controls access to the .snapshot directory. '
78 'By default, each share allows access to its own '
79 '.snapshot directory, which contains files and '
80 'directories of each snapshot taken. To restrict '
81 'access to the .snapshot directory, this option '
82 'should be set to False.'),
83 default=True),
84 cfg.BoolOpt('infinidat_snapdir_visible',
85 help=('Controls visibility of the .snapshot directory. '
86 'By default, each share contains the .snapshot '
87 'directory, which is hidden on the client side. '
88 'To make the .snapshot directory visible, this '
89 'option should be set to True.'),
90 default=False), ]
92CONF = cfg.CONF
93CONF.register_opts(infinidat_connection_opts)
94CONF.register_opts(infinidat_auth_opts)
95CONF.register_opts(infinidat_general_opts)
97_MANILA_TO_INFINIDAT_ACCESS_LEVEL = {
98 constants.ACCESS_LEVEL_RW: 'RW',
99 constants.ACCESS_LEVEL_RO: 'RO',
100}
102# Max retries for the REST API client in case of a failure:
103_API_MAX_RETRIES = 5
104# Identifier used as the REST API User-Agent string:
105_INFINIDAT_MANILA_IDENTIFIER = (
106 "manila/%s" % version.version_info.release_string())
109def infinisdk_to_manila_exceptions(func):
110 @functools.wraps(func)
111 def wrapper(*args, **kwargs):
112 try:
113 return func(*args, **kwargs)
114 except infinisdk.core.exceptions.InfiniSDKException as ex:
115 # string formatting of 'ex' includes http code and url
116 msg = _('Caught exception from infinisdk: %s') % ex
117 LOG.exception(msg)
118 raise exception.ShareBackendException(msg=msg)
119 return wrapper
122class InfiniboxShareDriver(driver.ShareDriver):
123 """INFINIDAT InfiniBox Share driver.
125 Version history:
126 1.0 - initial release
127 1.1 - added support for TLS/SSL communication
128 1.2 - fixed host assisted migration
129 """
131 VERSION = '1.2' # driver version
133 def __init__(self, *args, **kwargs):
134 super(InfiniboxShareDriver, self).__init__(False, *args, **kwargs)
135 self.configuration.append_config_values(infinidat_connection_opts)
136 self.configuration.append_config_values(infinidat_auth_opts)
137 self.configuration.append_config_values(infinidat_general_opts)
139 def _setup_and_get_system_object(self, management_address, auth, use_ssl):
140 system = infinisdk.InfiniBox(management_address, auth=auth,
141 use_ssl=use_ssl)
142 system.api.add_auto_retry(
143 lambda e: isinstance(
144 e, infinisdk.core.exceptions.APITransportFailure) and
145 "Interrupted system call" in e.error_desc, _API_MAX_RETRIES)
146 system.api.set_source_identifier(_INFINIDAT_MANILA_IDENTIFIER)
147 system.login()
148 return system
150 def do_setup(self, context):
151 """Driver initialization"""
152 if capacity is None:
153 msg = _("Missing 'capacity' python module, ensure the library"
154 " is installed and available.")
155 raise exception.ManilaException(message=msg)
156 if infinisdk is None:
157 msg = _("Missing 'infinisdk' python module, ensure the library"
158 " is installed and available.")
159 raise exception.ManilaException(message=msg)
161 if self.configuration.safe_get('infinidat_suppress_ssl_warnings'):
162 LOG.warning('Suppressing requests library SSL Warnings')
163 rpu = requests.packages.urllib3 # pylint: disable=no-member
164 rpu.disable_warnings(rpu.exceptions.InsecureRequestWarning)
165 rpu.disable_warnings(rpu.exceptions.InsecurePlatformWarning)
167 use_ssl = self.configuration.safe_get('infinidat_use_ssl')
168 infinibox_login = self._safe_get_from_config_or_fail('infinibox_login')
169 infinibox_password = (
170 self._safe_get_from_config_or_fail('infinibox_password'))
171 auth = (infinibox_login, infinibox_password)
173 management_address = (
174 self._safe_get_from_config_or_fail('infinibox_hostname'))
176 self._pool_name = (
177 self._safe_get_from_config_or_fail('infinidat_pool_name'))
179 self._network_space_name = (
180 self._safe_get_from_config_or_fail(
181 'infinidat_nas_network_space_name'))
183 self._system = self._setup_and_get_system_object(management_address,
184 auth, use_ssl)
186 backend_name = self.configuration.safe_get('share_backend_name')
187 self._backend_name = backend_name or self.__class__.__name__
189 thin_provisioning = self.configuration.infinidat_thin_provision
190 self._provtype = "THIN" if thin_provisioning else "THICK"
192 LOG.debug('setup complete')
194 def _update_share_stats(self):
195 """Retrieve stats info from share group."""
196 (free_capacity_bytes, physical_capacity_bytes,
197 provisioned_capacity_gb) = self._get_available_capacity()
199 max_over_subscription_ratio = (
200 self.configuration.max_over_subscription_ratio)
202 data = dict(
203 share_backend_name=self._backend_name,
204 vendor_name='INFINIDAT',
205 driver_version=self.VERSION,
206 storage_protocol='NFS',
207 total_capacity_gb=float(physical_capacity_bytes) / units.Gi,
208 free_capacity_gb=float(free_capacity_bytes) / units.Gi,
209 reserved_percentage=self.configuration.reserved_share_percentage,
210 reserved_snapshot_percentage=(
211 self.configuration.reserved_share_from_snapshot_percentage
212 or self.configuration.reserved_share_percentage),
213 reserved_share_extend_percentage=(
214 self.configuration.reserved_share_extend_percentage
215 or self.configuration.reserved_share_percentage),
216 thin_provisioning=self.configuration.infinidat_thin_provision,
217 max_over_subscription_ratio=max_over_subscription_ratio,
218 provisioned_capacity_gb=provisioned_capacity_gb,
219 snapshot_support=True,
220 create_share_from_snapshot_support=True,
221 mount_snapshot_support=True,
222 revert_to_snapshot_support=True)
224 super(InfiniboxShareDriver, self)._update_share_stats(data)
226 def _get_available_capacity(self):
227 # pylint: disable=no-member
228 pool = self._get_infinidat_pool()
229 free_capacity_bytes = (pool.get_free_physical_capacity() /
230 capacity.byte)
231 physical_capacity_bytes = (pool.get_physical_capacity() /
232 capacity.byte)
233 provisioned_capacity_gb = (
234 (pool.get_virtual_capacity() - pool.get_free_virtual_capacity()) /
235 capacity.GB)
236 # pylint: enable=no-member
237 return (free_capacity_bytes, physical_capacity_bytes,
238 provisioned_capacity_gb)
240 def _safe_get_from_config_or_fail(self, config_parameter):
241 config_value = self.configuration.safe_get(config_parameter)
242 if not config_value: # None or empty string
243 reason = (_("%(config_parameter)s configuration parameter "
244 "must be specified") %
245 {'config_parameter': config_parameter})
246 LOG.error(reason)
247 raise exception.BadConfigurationException(reason=reason)
248 return config_value
250 def _verify_share_protocol(self, share):
251 if share['share_proto'] != 'NFS':
252 reason = (_('Unsupported share protocol: %(proto)s.') %
253 {'proto': share['share_proto']})
254 LOG.error(reason)
255 raise exception.InvalidShare(reason=reason)
257 def _verify_access_type(self, access):
258 if access['access_type'] != 'ip':
259 reason = _('Only "ip" access type allowed for the NFS protocol.')
260 LOG.error(reason)
261 raise exception.InvalidShareAccess(reason=reason)
262 return True
264 def _make_share_name(self, manila_share):
265 return 'openstack-shr-%s' % manila_share['id']
267 def _make_snapshot_name(self, manila_snapshot):
268 return 'openstack-snap-%s' % manila_snapshot['id']
270 def _set_manila_object_metadata(self, infinidat_object, manila_object):
271 data = {"system": "openstack",
272 "openstack_version": version.version_info.release_string(),
273 "manila_id": manila_object['id'],
274 "manila_name": manila_object['name'],
275 "host.created_by": _INFINIDAT_MANILA_IDENTIFIER}
276 infinidat_object.set_metadata_from_dict(data)
278 @infinisdk_to_manila_exceptions
279 def _get_infinidat_pool(self):
280 pool = self._system.pools.safe_get(name=self._pool_name)
281 if pool is None:
282 msg = _('Pool "%s" not found') % self._pool_name
283 LOG.error(msg)
284 raise exception.ShareBackendException(msg=msg)
285 return pool
287 @infinisdk_to_manila_exceptions
288 def _get_infinidat_nas_network_space_ips(self):
289 network_space = self._system.network_spaces.safe_get(
290 name=self._network_space_name)
291 if network_space is None:
292 msg = _('INFINIDAT InfiniBox NAS network space "%s" '
293 'not found') % self._network_space_name
294 LOG.error(msg)
295 raise exception.ShareBackendException(msg=msg)
296 network_space_ips = network_space.get_ips()
297 if not network_space_ips:
298 msg = _('INFINIDAT InfiniBox NAS network space "%s" has no IP '
299 'addresses defined') % self._network_space_name
300 LOG.error(msg)
301 raise exception.ShareBackendException(msg=msg)
302 ip_addresses = (
303 [ip_munch.ip_address for ip_munch in network_space_ips if
304 ip_munch.enabled])
305 if not ip_addresses:
306 msg = _('INFINIDAT InfiniBox NAS network space "%s" has no '
307 'enabled IP addresses') % self._network_space_name
308 LOG.error(msg)
309 raise exception.ShareBackendException(msg=msg)
310 return ip_addresses
312 def _get_full_nfs_export_paths(self, export_path):
313 network_space_ips = self._get_infinidat_nas_network_space_ips()
314 return ['{network_space_ip}:{export_path}'.format(
315 network_space_ip=network_space_ip,
316 export_path=export_path) for network_space_ip in network_space_ips]
318 @infinisdk_to_manila_exceptions
319 def _get_infinidat_filesystem_by_name(self, name):
320 filesystem = self._system.filesystems.safe_get(name=name)
321 if filesystem is None:
322 msg = (_('Filesystem not found on the Infinibox by its name: %s') %
323 name)
324 LOG.error(msg)
325 raise exception.ShareResourceNotFound(share_id=name)
326 return filesystem
328 def _get_infinidat_filesystem(self, manila_share):
329 filesystem_name = self._make_share_name(manila_share)
330 return self._get_infinidat_filesystem_by_name(filesystem_name)
332 def _get_infinidat_snapshot_by_name(self, name):
333 snapshot = self._system.filesystems.safe_get(name=name)
334 if snapshot is None:
335 msg = (_('Snapshot not found on the Infinibox by its name: %s') %
336 name)
337 LOG.error(msg)
338 raise exception.ShareSnapshotNotFound(snapshot_id=name)
339 return snapshot
341 def _get_infinidat_snapshot(self, manila_snapshot):
342 snapshot_name = self._make_snapshot_name(manila_snapshot)
343 return self._get_infinidat_snapshot_by_name(snapshot_name)
345 def _get_infinidat_dataset(self, manila_object, is_snapshot):
346 return (self._get_infinidat_snapshot(manila_object) if is_snapshot
347 else self._get_infinidat_filesystem(manila_object))
349 @infinisdk_to_manila_exceptions
350 def _get_export(self, infinidat_filesystem):
351 infinidat_exports = infinidat_filesystem.get_exports()
352 if len(infinidat_exports) == 0:
353 msg = _("Could not find share export")
354 raise exception.ShareBackendException(msg=msg)
355 elif len(infinidat_exports) > 1:
356 msg = _("INFINIDAT filesystem has more than one active export; "
357 "possibly not a Manila share")
358 LOG.error(msg)
359 raise exception.ShareBackendException(msg=msg)
360 return infinidat_exports[0]
362 def _get_infinidat_access_level(self, access):
363 """Translates between Manila access levels to INFINIDAT API ones"""
364 access_level = access['access_level']
365 try:
366 return _MANILA_TO_INFINIDAT_ACCESS_LEVEL[access_level]
367 except KeyError:
368 raise exception.InvalidShareAccessLevel(level=access_level)
370 def _get_ip_address_range(self, ip_address):
371 """Parse single IP address or subnet into a range.
373 If the IP address string is in subnet mask format, returns a
374 <start ip>-<end-ip> string. If the IP address contains a single IP
375 address, returns only that IP address.
376 """
378 ip_address = str(ip_address)
380 # try treating the ip_address parameter as a range of IP addresses:
381 ip_network = ipaddress.ip_network(ip_address, strict=False)
382 ip_network_hosts = list(ip_network.hosts())
383 if len(ip_network_hosts) < 2: # /32, single IP address
384 return ip_address.split('/')[0]
385 return "{}-{}".format(ip_network_hosts[0], ip_network_hosts[-1])
387 @infinisdk_to_manila_exceptions
388 def _create_filesystem_export(self, infinidat_filesystem):
389 snapdir_visible = self.configuration.infinidat_snapdir_visible
390 infinidat_export = infinidat_filesystem.add_export(
391 permissions=[], snapdir_visible=snapdir_visible)
392 return self._make_export_locations(infinidat_export)
394 @infinisdk_to_manila_exceptions
395 def _ensure_filesystem_export(self, infinidat_filesystem):
396 try:
397 infinidat_export = self._get_export(infinidat_filesystem)
398 except exception.ShareBackendException:
399 return self._create_filesystem_export(infinidat_filesystem)
400 actual = infinidat_export.is_snapdir_visible()
401 expected = self.configuration.infinidat_snapdir_visible
402 if actual is not expected:
403 LOG.debug('Update snapdir_visible for %s: %s -> %s',
404 infinidat_filesystem.get_name(), actual, expected)
405 infinidat_export.update_snapdir_visible(expected)
406 return self._make_export_locations(infinidat_export)
408 @infinisdk_to_manila_exceptions
409 def _make_export_locations(self, infinidat_export):
410 export_paths = self._get_full_nfs_export_paths(
411 infinidat_export.get_export_path())
412 export_locations = [{
413 'path': export_path,
414 'is_admin_only': False,
415 'metadata': {},
416 } for export_path in export_paths]
417 return export_locations
419 @infinisdk_to_manila_exceptions
420 def _delete_share(self, share, is_snapshot):
421 if is_snapshot:
422 dataset_name = self._make_snapshot_name(share)
423 else:
424 dataset_name = self._make_share_name(share)
425 try:
426 infinidat_filesystem = (
427 self._get_infinidat_filesystem_by_name(dataset_name))
428 except exception.ShareResourceNotFound:
429 message = ("share %(share)s not found on Infinibox, skipping "
430 "delete")
431 LOG.warning(message, {"share": share})
432 return # filesystem not found
433 try:
434 infinidat_export = self._get_export(infinidat_filesystem)
435 infinidat_export.safe_delete()
436 except exception.ShareBackendException:
437 # it is possible that the export has been deleted
438 pass
439 infinidat_filesystem.safe_delete()
441 @infinisdk_to_manila_exceptions
442 def _extend_share(self, infinidat_filesystem, share, new_size):
443 # pylint: disable=no-member
444 new_size_capacity_units = new_size * capacity.GiB
445 # pylint: enable=no-member
446 old_size = infinidat_filesystem.get_size()
447 infinidat_filesystem.resize(new_size_capacity_units - old_size)
449 @infinisdk_to_manila_exceptions
450 def _update_access(self, manila_object, access_rules, is_snapshot):
451 infinidat_filesystem = self._get_infinidat_dataset(
452 manila_object, is_snapshot=is_snapshot)
453 infinidat_export = self._get_export(infinidat_filesystem)
454 permissions = [
455 {'access': self._get_infinidat_access_level(access_rule),
456 'client': self._get_ip_address_range(access_rule['access_to']),
457 'no_root_squash': True} for access_rule in access_rules if
458 self._verify_access_type(access_rule)]
459 infinidat_export.update_permissions(permissions)
461 @infinisdk_to_manila_exceptions
462 def create_share(self, context, share, share_server=None):
463 self._verify_share_protocol(share)
465 pool = self._get_infinidat_pool()
466 size = share['size'] * capacity.GiB # pylint: disable=no-member
467 name = self._make_share_name(share)
468 snapdir_accessible = self.configuration.infinidat_snapdir_accessible
469 infinidat_filesystem = self._system.filesystems.create(
470 pool=pool, name=name, size=size, provtype=self._provtype,
471 snapdir_accessible=snapdir_accessible)
472 self._set_manila_object_metadata(infinidat_filesystem, share)
473 return self._create_filesystem_export(infinidat_filesystem)
475 @infinisdk_to_manila_exceptions
476 def create_share_from_snapshot(self, context, share, snapshot,
477 share_server=None, parent_share=None):
478 name = self._make_share_name(share)
479 infinidat_snapshot = self._get_infinidat_snapshot(snapshot)
480 snapdir_accessible = self.configuration.infinidat_snapdir_accessible
481 infinidat_new_share = infinidat_snapshot.create_snapshot(
482 name=name, write_protected=False,
483 snapdir_accessible=snapdir_accessible)
484 self._extend_share(infinidat_new_share, share, share['size'])
485 return self._create_filesystem_export(infinidat_new_share)
487 @infinisdk_to_manila_exceptions
488 def create_snapshot(self, context, snapshot, share_server=None):
489 """Creates a snapshot."""
490 share = snapshot['share']
491 infinidat_filesystem = self._get_infinidat_filesystem(share)
492 name = self._make_snapshot_name(snapshot)
493 snapdir_accessible = self.configuration.infinidat_snapdir_accessible
494 infinidat_snapshot = infinidat_filesystem.create_snapshot(
495 name=name, snapdir_accessible=snapdir_accessible)
496 # snapshot is created in the same size as the original share, so no
497 # extending is needed
498 self._set_manila_object_metadata(infinidat_snapshot, snapshot)
499 return {'export_locations':
500 self._create_filesystem_export(infinidat_snapshot)}
502 def delete_share(self, context, share, share_server=None):
503 try:
504 self._verify_share_protocol(share)
505 except exception.InvalidShare:
506 # cleanup shouldn't fail on wrong protocol or missing share:
507 message = ("failed to delete share %(share)s; unsupported share "
508 "protocol %(share_proto)s, only NFS is supported")
509 LOG.warning(message, {"share": share,
510 "share_proto": share['share_proto']})
511 return
512 self._delete_share(share, is_snapshot=False)
514 def delete_snapshot(self, context, snapshot, share_server=None):
515 self._delete_share(snapshot, is_snapshot=True)
517 def ensure_share(self, context, share, share_server=None):
518 """Ensure that share is properly configured and exported."""
519 # will raise ShareResourceNotFound if the share was not found:
520 infinidat_filesystem = self._get_infinidat_filesystem(share)
521 actual = infinidat_filesystem.is_snapdir_accessible()
522 expected = self.configuration.infinidat_snapdir_accessible
523 if actual is not expected:
524 LOG.debug('Update snapdir_accessible for %s: %s -> %s',
525 infinidat_filesystem.get_name(), actual, expected)
526 infinidat_filesystem.update_field('snapdir_accessible', expected)
527 return self._ensure_filesystem_export(infinidat_filesystem)
529 def ensure_shares(self, context, shares):
530 """Invoked to ensure that shares are exported."""
531 updates = {}
532 for share in shares:
533 updates[share['id']] = {
534 'export_locations': self.ensure_share(context, share)}
535 return updates
537 def get_backend_info(self, context):
538 snapdir_accessible = self.configuration.infinidat_snapdir_accessible
539 snapdir_visible = self.configuration.infinidat_snapdir_visible
540 return {
541 'snapdir_accessible': snapdir_accessible,
542 'snapdir_visible': snapdir_visible
543 }
545 def update_access(self, context, share, access_rules, add_rules,
546 delete_rules, update_rules, share_server=None):
547 # As the Infinibox API can bulk update export access rules, we will try
548 # to use the access_rules list
549 self._verify_share_protocol(share)
550 self._update_access(share, access_rules, is_snapshot=False)
552 def get_network_allocations_number(self):
553 return 0
555 @infinisdk_to_manila_exceptions
556 def revert_to_snapshot(self, context, snapshot, share_access_rules,
557 snapshot_access_rules, share_server=None):
558 infinidat_snapshot = self._get_infinidat_snapshot(snapshot)
559 infinidat_parent_share = self._get_infinidat_filesystem(
560 snapshot['share'])
561 infinidat_parent_share.restore(infinidat_snapshot)
563 def extend_share(self, share, new_size, share_server=None):
564 infinidat_filesystem = self._get_infinidat_filesystem(share)
565 self._extend_share(infinidat_filesystem, share, new_size)
567 def snapshot_update_access(self, context, snapshot, access_rules,
568 add_rules, delete_rules, share_server=None):
569 # snapshots are to be mounted in read-only mode, see:
570 # "Add mountable snapshots" on openstack specs.
571 access_rules, _, _ = utils.change_rules_to_readonly(
572 access_rules, [], [])
573 try:
574 self._update_access(snapshot, access_rules, is_snapshot=True)
575 except exception.InvalidShareAccess as e:
576 raise exception.InvalidSnapshotAccess(e)