488 lines
20 KiB
Python
488 lines
20 KiB
Python
# Copyright 2017 Infinidat Ltd.
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
"""
|
|
INFINIDAT InfiniBox Share Driver
|
|
"""
|
|
|
|
import functools
|
|
|
|
import ipaddress
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_utils import units
|
|
import six
|
|
|
|
from manila.common import constants
|
|
from manila import exception
|
|
from manila.i18n import _
|
|
from manila.share import driver
|
|
from manila.share import utils
|
|
from manila import version
|
|
|
|
try:
|
|
import capacity
|
|
import infinisdk
|
|
except ImportError:
|
|
capacity = None
|
|
infinisdk = None
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
infinidat_connection_opts = [
|
|
cfg.HostAddressOpt('infinibox_hostname',
|
|
help='The name (or IP address) for the INFINIDAT '
|
|
'Infinibox storage system.'), ]
|
|
|
|
infinidat_auth_opts = [
|
|
cfg.StrOpt('infinibox_login',
|
|
help=('Administrative user account name used to access the '
|
|
'INFINIDAT Infinibox storage system.')),
|
|
cfg.StrOpt('infinibox_password',
|
|
help=('Password for the administrative user account '
|
|
'specified in the infinibox_login option.'),
|
|
secret=True), ]
|
|
|
|
infinidat_general_opts = [
|
|
cfg.StrOpt('infinidat_pool_name',
|
|
help='Name of the pool from which volumes are allocated.'),
|
|
cfg.StrOpt('infinidat_nas_network_space_name',
|
|
help='Name of the NAS network space on the INFINIDAT '
|
|
'InfiniBox.'),
|
|
cfg.BoolOpt('infinidat_thin_provision', help='Use thin provisioning.',
|
|
default=True)]
|
|
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(infinidat_connection_opts)
|
|
CONF.register_opts(infinidat_auth_opts)
|
|
CONF.register_opts(infinidat_general_opts)
|
|
|
|
_MANILA_TO_INFINIDAT_ACCESS_LEVEL = {
|
|
constants.ACCESS_LEVEL_RW: 'RW',
|
|
constants.ACCESS_LEVEL_RO: 'RO',
|
|
}
|
|
|
|
# Max retries for the REST API client in case of a failure:
|
|
_API_MAX_RETRIES = 5
|
|
# Identifier used as the REST API User-Agent string:
|
|
_INFINIDAT_MANILA_IDENTIFIER = (
|
|
"manila/%s" % version.version_info.release_string())
|
|
|
|
|
|
def infinisdk_to_manila_exceptions(func):
|
|
@functools.wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except infinisdk.core.exceptions.InfiniSDKException as ex:
|
|
# string formatting of 'ex' includes http code and url
|
|
msg = _('Caught exception from infinisdk: %s') % ex
|
|
LOG.exception(msg)
|
|
raise exception.ShareBackendException(msg=msg)
|
|
return wrapper
|
|
|
|
|
|
class InfiniboxShareDriver(driver.ShareDriver):
|
|
|
|
VERSION = '1.0' # driver version
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(InfiniboxShareDriver, self).__init__(False, *args, **kwargs)
|
|
self.configuration.append_config_values(infinidat_connection_opts)
|
|
self.configuration.append_config_values(infinidat_auth_opts)
|
|
self.configuration.append_config_values(infinidat_general_opts)
|
|
|
|
def _setup_and_get_system_object(self, management_address, auth):
|
|
system = infinisdk.InfiniBox(management_address, auth=auth)
|
|
system.api.add_auto_retry(
|
|
lambda e: isinstance(
|
|
e, infinisdk.core.exceptions.APITransportFailure) and
|
|
"Interrupted system call" in e.error_desc, _API_MAX_RETRIES)
|
|
system.api.set_source_identifier(_INFINIDAT_MANILA_IDENTIFIER)
|
|
system.login()
|
|
return system
|
|
|
|
def do_setup(self, context):
|
|
"""Driver initialization"""
|
|
if infinisdk is None:
|
|
msg = _("Missing 'infinisdk' python module, ensure the library"
|
|
" is installed and available.")
|
|
raise exception.ManilaException(message=msg)
|
|
|
|
infinibox_login = self._safe_get_from_config_or_fail('infinibox_login')
|
|
infinibox_password = (
|
|
self._safe_get_from_config_or_fail('infinibox_password'))
|
|
auth = (infinibox_login, infinibox_password)
|
|
|
|
management_address = (
|
|
self._safe_get_from_config_or_fail('infinibox_hostname'))
|
|
|
|
self._pool_name = (
|
|
self._safe_get_from_config_or_fail('infinidat_pool_name'))
|
|
|
|
self._network_space_name = (
|
|
self._safe_get_from_config_or_fail(
|
|
'infinidat_nas_network_space_name'))
|
|
|
|
self._system = (
|
|
self._setup_and_get_system_object(management_address, auth))
|
|
|
|
backend_name = self.configuration.safe_get('share_backend_name')
|
|
self._backend_name = backend_name or self.__class__.__name__
|
|
|
|
thin_provisioning = self.configuration.infinidat_thin_provision
|
|
self._provtype = "THIN" if thin_provisioning else "THICK"
|
|
|
|
LOG.debug('setup complete')
|
|
|
|
def _update_share_stats(self):
|
|
"""Retrieve stats info from share group."""
|
|
(free_capacity_bytes, physical_capacity_bytes,
|
|
provisioned_capacity_gb) = self._get_available_capacity()
|
|
|
|
max_over_subscription_ratio = (
|
|
self.configuration.max_over_subscription_ratio)
|
|
|
|
data = dict(
|
|
share_backend_name=self._backend_name,
|
|
vendor_name='INFINIDAT',
|
|
driver_version=self.VERSION,
|
|
storage_protocol='NFS',
|
|
total_capacity_gb=float(physical_capacity_bytes) / units.Gi,
|
|
free_capacity_gb=float(free_capacity_bytes) / units.Gi,
|
|
reserved_percentage=self.configuration.reserved_share_percentage,
|
|
thin_provisioning=self.configuration.infinidat_thin_provision,
|
|
max_over_subscription_ratio=max_over_subscription_ratio,
|
|
provisioned_capacity_gb=provisioned_capacity_gb,
|
|
snapshot_support=True,
|
|
create_share_from_snapshot_support=True,
|
|
mount_snapshot_support=True,
|
|
revert_to_snapshot_support=True)
|
|
|
|
super(InfiniboxShareDriver, self)._update_share_stats(data)
|
|
|
|
def _get_available_capacity(self):
|
|
# pylint: disable=no-member
|
|
pool = self._get_infinidat_pool()
|
|
free_capacity_bytes = (pool.get_free_physical_capacity() /
|
|
capacity.byte)
|
|
physical_capacity_bytes = (pool.get_physical_capacity() /
|
|
capacity.byte)
|
|
provisioned_capacity_gb = (
|
|
(pool.get_virtual_capacity() - pool.get_free_virtual_capacity()) /
|
|
capacity.GB)
|
|
# pylint: enable=no-member
|
|
return (free_capacity_bytes, physical_capacity_bytes,
|
|
provisioned_capacity_gb)
|
|
|
|
def _safe_get_from_config_or_fail(self, config_parameter):
|
|
config_value = self.configuration.safe_get(config_parameter)
|
|
if not config_value: # None or empty string
|
|
reason = (_("%(config_parameter)s configuration parameter "
|
|
"must be specified") %
|
|
{'config_parameter': config_parameter})
|
|
LOG.error(reason)
|
|
raise exception.BadConfigurationException(reason=reason)
|
|
return config_value
|
|
|
|
def _verify_share_protocol(self, share):
|
|
if share['share_proto'] != 'NFS':
|
|
reason = (_('Unsupported share protocol: %(proto)s.') %
|
|
{'proto': share['share_proto']})
|
|
LOG.error(reason)
|
|
raise exception.InvalidShare(reason=reason)
|
|
|
|
def _verify_access_type(self, access):
|
|
if access['access_type'] != 'ip':
|
|
reason = _('Only "ip" access type allowed for the NFS protocol.')
|
|
LOG.error(reason)
|
|
raise exception.InvalidShareAccess(reason=reason)
|
|
return True
|
|
|
|
def _make_share_name(self, manila_share):
|
|
return 'openstack-shr-%s' % manila_share['id']
|
|
|
|
def _make_snapshot_name(self, manila_snapshot):
|
|
return 'openstack-snap-%s' % manila_snapshot['id']
|
|
|
|
def _set_manila_object_metadata(self, infinidat_object, manila_object):
|
|
data = {"system": "openstack",
|
|
"openstack_version": version.version_info.release_string(),
|
|
"manila_id": manila_object['id'],
|
|
"manila_name": manila_object['name'],
|
|
"host.created_by": _INFINIDAT_MANILA_IDENTIFIER}
|
|
infinidat_object.set_metadata_from_dict(data)
|
|
|
|
@infinisdk_to_manila_exceptions
|
|
def _get_infinidat_pool(self):
|
|
pool = self._system.pools.safe_get(name=self._pool_name)
|
|
if pool is None:
|
|
msg = _('Pool "%s" not found') % self._pool_name
|
|
LOG.error(msg)
|
|
raise exception.ShareBackendException(msg=msg)
|
|
return pool
|
|
|
|
@infinisdk_to_manila_exceptions
|
|
def _get_infinidat_nas_network_space_ips(self):
|
|
network_space = self._system.network_spaces.safe_get(
|
|
name=self._network_space_name)
|
|
if network_space is None:
|
|
msg = _('INFINIDAT InfiniBox NAS network space "%s" '
|
|
'not found') % self._network_space_name
|
|
LOG.error(msg)
|
|
raise exception.ShareBackendException(msg=msg)
|
|
network_space_ips = network_space.get_ips()
|
|
if not network_space_ips:
|
|
msg = _('INFINIDAT InfiniBox NAS network space "%s" has no IP '
|
|
'addresses defined') % self._network_space_name
|
|
LOG.error(msg)
|
|
raise exception.ShareBackendException(msg=msg)
|
|
ip_addresses = (
|
|
[ip_munch.ip_address for ip_munch in network_space_ips if
|
|
ip_munch.enabled])
|
|
if not ip_addresses:
|
|
msg = _('INFINIDAT InfiniBox NAS network space "%s" has no '
|
|
'enabled IP addresses') % self._network_space_name
|
|
LOG.error(msg)
|
|
raise exception.ShareBackendException(msg=msg)
|
|
return ip_addresses
|
|
|
|
def _get_full_nfs_export_paths(self, export_path):
|
|
network_space_ips = self._get_infinidat_nas_network_space_ips()
|
|
return ['{network_space_ip}:{export_path}'.format(
|
|
network_space_ip=network_space_ip,
|
|
export_path=export_path) for network_space_ip in network_space_ips]
|
|
|
|
@infinisdk_to_manila_exceptions
|
|
def _get_infinidat_filesystem_by_name(self, name):
|
|
filesystem = self._system.filesystems.safe_get(name=name)
|
|
if filesystem is None:
|
|
msg = (_('Filesystem not found on the Infinibox by its name: %s') %
|
|
name)
|
|
LOG.error(msg)
|
|
raise exception.ShareResourceNotFound(share_id=name)
|
|
return filesystem
|
|
|
|
def _get_infinidat_filesystem(self, manila_share):
|
|
filesystem_name = self._make_share_name(manila_share)
|
|
return self._get_infinidat_filesystem_by_name(filesystem_name)
|
|
|
|
def _get_infinidat_snapshot_by_name(self, name):
|
|
snapshot = self._system.filesystems.safe_get(name=name)
|
|
if snapshot is None:
|
|
msg = (_('Snapshot not found on the Infinibox by its name: %s') %
|
|
name)
|
|
LOG.error(msg)
|
|
raise exception.ShareSnapshotNotFound(snapshot_id=name)
|
|
return snapshot
|
|
|
|
def _get_infinidat_snapshot(self, manila_snapshot):
|
|
snapshot_name = self._make_snapshot_name(manila_snapshot)
|
|
return self._get_infinidat_snapshot_by_name(snapshot_name)
|
|
|
|
def _get_infinidat_dataset(self, manila_object, is_snapshot):
|
|
return (self._get_infinidat_snapshot(manila_object) if is_snapshot
|
|
else self._get_infinidat_filesystem(manila_object))
|
|
|
|
@infinisdk_to_manila_exceptions
|
|
def _get_export(self, infinidat_filesystem):
|
|
infinidat_exports = infinidat_filesystem.get_exports()
|
|
if len(infinidat_exports) == 0:
|
|
msg = _("Could not find share export")
|
|
raise exception.ShareBackendException(msg=msg)
|
|
elif len(infinidat_exports) > 1:
|
|
msg = _("INFINIDAT filesystem has more than one active export; "
|
|
"possibly not a Manila share")
|
|
LOG.error(msg)
|
|
raise exception.ShareBackendException(msg=msg)
|
|
return infinidat_exports[0]
|
|
|
|
def _get_infinidat_access_level(self, access):
|
|
"""Translates between Manila access levels to INFINIDAT API ones"""
|
|
access_level = access['access_level']
|
|
try:
|
|
return _MANILA_TO_INFINIDAT_ACCESS_LEVEL[access_level]
|
|
except KeyError:
|
|
raise exception.InvalidShareAccessLevel(level=access_level)
|
|
|
|
def _get_ip_address_range(self, ip_address):
|
|
"""Parse single IP address or subnet into a range.
|
|
|
|
If the IP address string is in subnet mask format, returns a
|
|
<start ip>-<end-ip> string. If the IP address contains a single IP
|
|
address, returns only that IP address.
|
|
"""
|
|
|
|
ip_address = six.text_type(ip_address)
|
|
|
|
# try treating the ip_address parameter as a range of IP addresses:
|
|
ip_network = ipaddress.ip_network(ip_address, strict=False)
|
|
ip_network_hosts = list(ip_network.hosts())
|
|
if len(ip_network_hosts) < 2: # /32, single IP address
|
|
return ip_address.split('/')[0]
|
|
return "{}-{}".format(ip_network_hosts[0], ip_network_hosts[-1])
|
|
|
|
@infinisdk_to_manila_exceptions
|
|
def _create_filesystem_export(self, infinidat_filesystem):
|
|
infinidat_export = infinidat_filesystem.add_export(permissions=[])
|
|
export_paths = self._get_full_nfs_export_paths(
|
|
infinidat_export.get_export_path())
|
|
export_locations = [{
|
|
'path': export_path,
|
|
'is_admin_only': False,
|
|
'metadata': {},
|
|
} for export_path in export_paths]
|
|
return export_locations
|
|
|
|
@infinisdk_to_manila_exceptions
|
|
def _delete_share(self, share, is_snapshot):
|
|
if is_snapshot:
|
|
dataset_name = self._make_snapshot_name(share)
|
|
else:
|
|
dataset_name = self._make_share_name(share)
|
|
try:
|
|
infinidat_filesystem = (
|
|
self._get_infinidat_filesystem_by_name(dataset_name))
|
|
except exception.ShareResourceNotFound:
|
|
message = ("share %(share)s not found on Infinibox, skipping "
|
|
"delete")
|
|
LOG.warning(message, {"share": share})
|
|
return # filesystem not found
|
|
try:
|
|
infinidat_export = self._get_export(infinidat_filesystem)
|
|
infinidat_export.safe_delete()
|
|
except exception.ShareBackendException:
|
|
# it is possible that the export has been deleted
|
|
pass
|
|
infinidat_filesystem.safe_delete()
|
|
|
|
@infinisdk_to_manila_exceptions
|
|
def _extend_share(self, infinidat_filesystem, share, new_size):
|
|
# pylint: disable=no-member
|
|
new_size_capacity_units = new_size * capacity.GiB
|
|
# pylint: enable=no-member
|
|
old_size = infinidat_filesystem.get_size()
|
|
infinidat_filesystem.resize(new_size_capacity_units - old_size)
|
|
|
|
@infinisdk_to_manila_exceptions
|
|
def _update_access(self, manila_object, access_rules, is_snapshot):
|
|
infinidat_filesystem = self._get_infinidat_dataset(
|
|
manila_object, is_snapshot=is_snapshot)
|
|
infinidat_export = self._get_export(infinidat_filesystem)
|
|
permissions = [
|
|
{'access': self._get_infinidat_access_level(access_rule),
|
|
'client': self._get_ip_address_range(access_rule['access_to']),
|
|
'no_root_squash': True} for access_rule in access_rules if
|
|
self._verify_access_type(access_rule)]
|
|
infinidat_export.update_permissions(permissions)
|
|
|
|
@infinisdk_to_manila_exceptions
|
|
def create_share(self, context, share, share_server=None):
|
|
self._verify_share_protocol(share)
|
|
|
|
pool = self._get_infinidat_pool()
|
|
size = share['size'] * capacity.GiB # pylint: disable=no-member
|
|
share_name = self._make_share_name(share)
|
|
|
|
infinidat_filesystem = self._system.filesystems.create(
|
|
pool=pool, name=share_name, size=size, provtype=self._provtype)
|
|
self._set_manila_object_metadata(infinidat_filesystem, share)
|
|
return self._create_filesystem_export(infinidat_filesystem)
|
|
|
|
@infinisdk_to_manila_exceptions
|
|
def create_share_from_snapshot(self, context, share, snapshot,
|
|
share_server=None, parent_share=None):
|
|
name = self._make_share_name(share)
|
|
infinidat_snapshot = self._get_infinidat_snapshot(snapshot)
|
|
infinidat_new_share = infinidat_snapshot.create_snapshot(
|
|
name=name, write_protected=False)
|
|
self._extend_share(infinidat_new_share, share, share['size'])
|
|
return self._create_filesystem_export(infinidat_new_share)
|
|
|
|
@infinisdk_to_manila_exceptions
|
|
def create_snapshot(self, context, snapshot, share_server=None):
|
|
"""Creates a snapshot."""
|
|
share = snapshot['share']
|
|
infinidat_filesystem = self._get_infinidat_filesystem(share)
|
|
name = self._make_snapshot_name(snapshot)
|
|
infinidat_snapshot = infinidat_filesystem.create_snapshot(name=name)
|
|
# snapshot is created in the same size as the original share, so no
|
|
# extending is needed
|
|
self._set_manila_object_metadata(infinidat_snapshot, snapshot)
|
|
return {'export_locations':
|
|
self._create_filesystem_export(infinidat_snapshot)}
|
|
|
|
def delete_share(self, context, share, share_server=None):
|
|
try:
|
|
self._verify_share_protocol(share)
|
|
except exception.InvalidShare:
|
|
# cleanup shouldn't fail on wrong protocol or missing share:
|
|
message = ("failed to delete share %(share)s; unsupported share "
|
|
"protocol %(share_proto)s, only NFS is supported")
|
|
LOG.warning(message, {"share": share,
|
|
"share_proto": share['share_proto']})
|
|
return
|
|
self._delete_share(share, is_snapshot=False)
|
|
|
|
def delete_snapshot(self, context, snapshot, share_server=None):
|
|
self._delete_share(snapshot, is_snapshot=True)
|
|
|
|
def ensure_share(self, context, share, share_server=None):
|
|
# will raise ShareResourceNotFound if the share was not found:
|
|
infinidat_filesystem = self._get_infinidat_filesystem(share)
|
|
try:
|
|
infinidat_export = self._get_export(infinidat_filesystem)
|
|
return self._get_full_nfs_export_paths(
|
|
infinidat_export.get_export_path())
|
|
except exception.ShareBackendException:
|
|
# export not found, need to re-export
|
|
message = ("missing export for share %(share)s, trying to "
|
|
"re-export")
|
|
LOG.info(message, {"share": share})
|
|
return self._create_filesystem_export(infinidat_filesystem)
|
|
|
|
def update_access(self, context, share, access_rules, add_rules,
|
|
delete_rules, share_server=None):
|
|
# As the Infinibox API can bulk update export access rules, we will try
|
|
# to use the access_rules list
|
|
self._verify_share_protocol(share)
|
|
self._update_access(share, access_rules, is_snapshot=False)
|
|
|
|
def get_network_allocations_number(self):
|
|
return 0
|
|
|
|
@infinisdk_to_manila_exceptions
|
|
def revert_to_snapshot(self, context, snapshot, share_access_rules,
|
|
snapshot_access_rules, share_server=None):
|
|
infinidat_snapshot = self._get_infinidat_snapshot(snapshot)
|
|
infinidat_parent_share = self._get_infinidat_filesystem(
|
|
snapshot['share'])
|
|
infinidat_parent_share.restore(infinidat_snapshot)
|
|
|
|
def extend_share(self, share, new_size, share_server=None):
|
|
infinidat_filesystem = self._get_infinidat_filesystem(share)
|
|
self._extend_share(infinidat_filesystem, share, new_size)
|
|
|
|
def snapshot_update_access(self, context, snapshot, access_rules,
|
|
add_rules, delete_rules, share_server=None):
|
|
# snapshots are to be mounted in read-only mode, see:
|
|
# "Add mountable snapshots" on openstack specs.
|
|
access_rules, _, _ = utils.change_rules_to_readonly(
|
|
access_rules, [], [])
|
|
try:
|
|
self._update_access(snapshot, access_rules, is_snapshot=True)
|
|
except exception.InvalidShareAccess as e:
|
|
raise exception.InvalidSnapshotAccess(e)
|