Files
manila/manila/share/drivers/dell_emc/plugins/isilon/isilon.py
Nilesh Thathagar e9386b67d9 Dell PowerScale: Added support of thin provisioning
Implements: blueprint powerscale-thin-provisioning
Change-Id: I197796713304ff6695f39b1b39817bfbb4c7927e
Signed-off-by: Nilesh Thathagar <nilesh.thathagar@dell.com>
2025-08-04 07:37:40 +00:00

582 lines
23 KiB
Python

# Copyright 2015 EMC Corporation
# 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.
"""
Isilon specific NAS backend plugin.
"""
import os
from oslo_config import cfg
from oslo_log import log
from oslo_utils import units
from manila.common import constants as const
from manila import exception
from manila.i18n import _
from manila.share.drivers.dell_emc.plugins import base
from manila.share.drivers.dell_emc.plugins.isilon import isilon_api
"""Version history:
0.1.0 - Initial version
1.0.0 - Fix Http auth issue, SSL verification error and etc
1.0.1 - Add support for update share stats
1.0.2 - Add support for ensure shares
1.0.3 - Add support for thin provisioning
"""
VERSION = "1.0.3"
CONF = cfg.CONF
LOG = log.getLogger(__name__)
POWERSCALE_OPTS = [
cfg.StrOpt('powerscale_dir_permission',
default='0777',
help='Predefined ACL value or POSIX mode '
'for PowerScale directories.'),
cfg.IntOpt('powerscale_threshold_limit',
default=0,
help='Specifies the threshold limit (in percentage) '
'for triggering SmartQuotas alerts in PowerScale')
]
class IsilonStorageConnection(base.StorageConnection):
"""Implements Isilon specific functionality for EMC Manila driver."""
def __init__(self, *args, **kwargs):
super(IsilonStorageConnection, self).__init__(*args, **kwargs)
LOG.debug('Setting up attributes for Manila '
'Dell PowerScale Driver.')
if 'configuration' in kwargs:
kwargs['configuration'].append_config_values(POWERSCALE_OPTS)
self._server = None
self._port = None
self._username = None
self._password = None
self._server_url = None
self._root_dir = None
self._verify_ssl_cert = None
self._ssl_cert_path = None
self._containers = {}
self._shares = {}
self._snapshots = {}
self._isilon_api = None
self.driver_handles_share_servers = False
self.ipv6_implemented = True
# props for share status update
self.reserved_percentage = None
self.reserved_snapshot_percentage = None
self.reserved_share_extend_percentage = None
self.max_over_subscription_ratio = None
self._threshold_limit = 0
def _get_container_path(self, share):
"""Return path to a container."""
return os.path.join(self._root_dir, share['name'])
def create_share(self, context, share, share_server):
"""Is called to create share."""
LOG.debug(f'Creating {share["share_proto"]} share.')
if share['share_proto'] == 'NFS':
location = self._create_nfs_share(share)
elif share['share_proto'] == 'CIFS':
location = self._create_cifs_share(share)
else:
message = (_('Unsupported share protocol: %(proto)s.') %
{'proto': share['share_proto']})
LOG.error(message)
raise exception.InvalidShare(reason=message)
# apply directory quota based on share size
max_share_size = share['size'] * units.Gi
self._isilon_api.quota_create(
self._get_container_path(share), 'directory', max_share_size)
return location
def create_share_from_snapshot(self, context, share, snapshot,
share_server):
"""Creates a share from the snapshot."""
LOG.debug(f'Creating {share["share_proto"]} share from snapshot.')
# Create share at new location
location = self.create_share(context, share, share_server)
# Clone snapshot to new location
fq_target_dir = self._get_container_path(share)
self._isilon_api.clone_snapshot(snapshot['name'], fq_target_dir)
return location
def _create_nfs_share(self, share):
"""Is called to create nfs share."""
LOG.debug(f'Creating NFS share {share["name"]}.')
# Create directory
container_path = self._get_container_path(share)
self._create_directory(container_path)
# Create nfs share
share_created = self._isilon_api.create_nfs_export(container_path)
if not share_created:
message = (
_('The requested NFS share "%(share)s" was not created.') %
{'share': share['name']})
LOG.error(message)
raise exception.ShareBackendException(msg=message)
location = self._get_location(self._format_nfs_path(container_path))
return location
def _create_cifs_share(self, share):
"""Is called to create cifs share."""
LOG.debug(f'Creating CIFS share {share["name"]}.')
# Create directory
container_path = self._get_container_path(share)
self._create_directory(container_path)
# Create smb share
share_created = self._isilon_api.create_smb_share(
share['name'], container_path)
if not share_created:
message = (
_('The requested CIFS share "%(share)s" was not created.') %
{'share': share['name']})
LOG.error(message)
raise exception.ShareBackendException(msg=message)
location = self._get_location(self._format_smb_path(share['name']))
return location
def _create_directory(self, path, recursive=False):
"""Is called to create a directory."""
dir_created = self._isilon_api.create_directory(path, recursive)
if not dir_created:
message = (
_('Failed to create directory "%(dir)s".') %
{'dir': path})
LOG.error(message)
raise exception.ShareBackendException(msg=message)
def create_snapshot(self, context, snapshot, share_server):
"""Is called to create snapshot."""
LOG.debug(f'Creating snapshot {snapshot["name"]}.')
snapshot_path = os.path.join(self._root_dir, snapshot['share_name'])
snap_created = self._isilon_api.create_snapshot(
snapshot['name'], snapshot_path)
if not snap_created:
message = (
_('Failed to create snapshot "%(snap)s".') %
{'snap': snapshot['name']})
LOG.error(message)
raise exception.ShareBackendException(msg=message)
def delete_share(self, context, share, share_server):
"""Is called to remove share."""
LOG.debug(f'Deleting {share["share_proto"]} share.')
if share['share_proto'] == 'NFS':
self._delete_nfs_share(share)
elif share['share_proto'] == 'CIFS':
self._delete_cifs_share(share)
else:
message = (_('Unsupported share type: %(type)s.') %
{'type': share['share_proto']})
LOG.warning(message)
return
dir_path = self._get_container_path(share)
# remove quota
self._delete_quota(dir_path)
# remove directory
self._delete_directory(dir_path)
def _delete_quota(self, path):
"""Is called to remove quota."""
quota = self._isilon_api.quota_get(path, 'directory')
if quota:
LOG.debug(f'Removing quota {quota["id"]}')
deleted = self._isilon_api.delete_quota(quota['id'])
if not deleted:
message = (
_('Failed to delete quota "%(quota_id)s" for '
'directory "%(dir)s".') %
{'quota_id': quota['id'], 'dir': path})
LOG.error(message)
else:
LOG.warning(f'Quota not found for {path}')
def _delete_directory(self, path):
"""Is called to remove directory."""
path_exist = self._isilon_api.is_path_existent(path)
if path_exist:
LOG.debug(f'Removing directory {path}')
deleted = self._isilon_api.delete_path(path, recursive=True)
if not deleted:
message = (
_('Failed to delete directory "%(dir)s".') %
{'dir': path})
LOG.error(message)
else:
LOG.warning(f'Directory not found for {path}')
def _delete_nfs_share(self, share):
"""Is called to remove nfs share."""
share_id = self._isilon_api.lookup_nfs_export(
self._get_container_path(share))
if share_id is None:
lw = ('Attempted to delete NFS Share "%s", but the share does '
'not appear to exist.')
LOG.warning(lw, share['name'])
else:
# attempt to delete the share
export_deleted = self._isilon_api.delete_nfs_share(share_id)
if not export_deleted:
message = _('Error deleting NFS share: %s') % share['name']
LOG.error(message)
raise exception.ShareBackendException(msg=message)
def _delete_cifs_share(self, share):
"""Is called to remove CIFS share."""
smb_share = self._isilon_api.lookup_smb_share(share['name'])
if smb_share is None:
lw = ('Attempted to delete CIFS Share "%s", but the share does '
'not appear to exist.')
LOG.warning(lw, share['name'])
else:
share_deleted = self._isilon_api.delete_smb_share(share['name'])
if not share_deleted:
message = _('Error deleting CIFS share: %s') % share['name']
LOG.error(message)
raise exception.ShareBackendException(msg=message)
def delete_snapshot(self, context, snapshot, share_server):
"""Is called to remove snapshot."""
LOG.debug(f'Deleting snapshot {snapshot["name"]}')
deleted = self._isilon_api.delete_snapshot(snapshot['name'])
if not deleted:
message = (
_('Failed to delete snapshot "%(snap)s".') %
{'snap': snapshot['name']})
LOG.error(message)
raise exception.ShareBackendException(msg=message)
def ensure_share(self, context, share, share_server):
"""Invoked to ensure that share is exported."""
raise NotImplementedError()
def extend_share(self, share, new_size, share_server=None):
"""Extends a share."""
LOG.debug('Extending share %(name)s to %(size)sG.', {
'name': share['name'], 'size': new_size
})
new_quota_size = new_size * units.Gi
self._isilon_api.quota_set(
self._get_container_path(share), 'directory', new_quota_size)
def allow_access(self, context, share, access, share_server):
"""Allow access to the share."""
raise NotImplementedError()
def deny_access(self, context, share, access, share_server):
"""Deny access to the share."""
raise NotImplementedError()
def check_for_setup_error(self):
"""Check for setup error."""
def connect(self, emc_share_driver, context):
"""Connect to an Isilon cluster."""
LOG.debug('Reading configuration parameters for Manila'
' Dell PowerScale Driver.')
config = emc_share_driver.configuration
self._server = config.safe_get("emc_nas_server")
self._port = config.safe_get("emc_nas_server_port")
self._username = config.safe_get("emc_nas_login")
self._password = config.safe_get("emc_nas_password")
self._root_dir = config.safe_get("emc_nas_root_dir")
self._threshold_limit = config.safe_get("powerscale_threshold_limit")
# validate IP, username and password
if not all([self._server,
self._username,
self._password]):
message = _("REST server IP, username and password"
" must be specified.")
raise exception.BadConfigurationException(reason=message)
self._server_url = f'https://{self._server}:{self._port}'
self._verify_ssl_cert = config.safe_get("emc_ssl_cert_verify")
if self._verify_ssl_cert:
self._ssl_cert_path = config.safe_get("emc_ssl_cert_path")
self._dir_permission = config.safe_get("powerscale_dir_permission")
self._isilon_api = isilon_api.IsilonApi(
self._server_url, self._username, self._password,
self._verify_ssl_cert, self._ssl_cert_path,
self._dir_permission,
self._threshold_limit)
if not self._isilon_api.is_path_existent(self._root_dir):
self._create_directory(self._root_dir, recursive=True)
# configuration for share status update
self.reserved_percentage = config.safe_get(
'reserved_share_percentage')
if self.reserved_percentage is None:
self.reserved_percentage = 0
self.reserved_snapshot_percentage = config.safe_get(
'reserved_share_from_snapshot_percentage')
if self.reserved_snapshot_percentage is None:
self.reserved_snapshot_percentage = self.reserved_percentage
self.reserved_share_extend_percentage = config.safe_get(
'reserved_share_extend_percentage')
if self.reserved_share_extend_percentage is None:
self.reserved_share_extend_percentage = self.reserved_percentage
self.max_over_subscription_ratio = config.safe_get(
'max_over_subscription_ratio')
def update_share_stats(self, stats_dict):
"""Retrieve stats info from share."""
stats_dict['driver_version'] = VERSION
stats_dict['storage_protocol'] = 'NFS_CIFS'
# PowerScale does not support pools.
# To align with manila scheduler 'pool-aware' strategic,
# report with one pool structure.
pool_stat = {
'pool_name': stats_dict['share_backend_name'],
'qos': False,
'reserved_percentage': self.reserved_percentage,
'reserved_snapshot_percentage':
self.reserved_snapshot_percentage,
'reserved_share_extend_percentage':
self.reserved_share_extend_percentage,
'max_over_subscription_ratio':
self.max_over_subscription_ratio,
'thin_provisioning': True,
}
spaces = self._isilon_api.get_space_stats()
if spaces:
pool_stat['total_capacity_gb'] = spaces['total'] // units.Gi
pool_stat['free_capacity_gb'] = spaces['free'] // units.Gi
allocated_space = self._isilon_api.get_allocated_space()
pool_stat['allocated_capacity_gb'] = allocated_space
stats_dict['pools'] = [pool_stat]
def get_network_allocations_number(self):
"""Returns number of network allocations for creating VIFs."""
# TODO(Shaun Edwards)
return 0
def setup_server(self, network_info, metadata=None):
"""Set up and configures share server with given network parameters."""
# TODO(Shaun Edwards): Look into supporting share servers
def teardown_server(self, server_details, security_services=None):
"""Teardown share server."""
# TODO(Shaun Edwards): Look into supporting share servers
def update_access(self, context, share, access_rules, add_rules,
delete_rules, share_server=None):
"""Update share access."""
LOG.debug(f'Updaing access for share {share["name"]}.')
if share['share_proto'] == 'NFS':
state_map = self._update_access_nfs(share, access_rules)
if share['share_proto'] == 'CIFS':
state_map = self._update_access_cifs(share, access_rules)
return state_map
def _update_access_nfs(self, share, access_rules):
"""Updates access on a NFS share."""
nfs_rw_ips = set()
nfs_ro_ips = set()
rule_state_map = {}
for rule in access_rules:
rule_state_map[rule['access_id']] = {
'state': 'error'
}
for rule in access_rules:
if rule['access_level'] == const.ACCESS_LEVEL_RW:
nfs_rw_ips.add(rule['access_to'])
elif rule['access_level'] == const.ACCESS_LEVEL_RO:
nfs_ro_ips.add(rule['access_to'])
export_id = self._isilon_api.lookup_nfs_export(
self._get_container_path(share))
if export_id is None:
# share does not exist on backend (set all rules to error state)
message = _('Failed to update access for NFS share %s: '
'share not found.') % share['name']
LOG.error(message)
return rule_state_map
r = self._isilon_api.modify_nfs_export_access(
export_id, ro_ips=list(nfs_ro_ips), rw_ips=list(nfs_rw_ips))
if not r:
return rule_state_map
# if we finish the bulk rule update with no error set rules to active
for rule in access_rules:
rule_state_map[rule['access_id']]['state'] = 'active'
return rule_state_map
def _update_access_cifs(self, share, access_rules):
"""Update access on a CIFS share."""
rule_state_map = {}
ip_access_rules = []
user_access_rules = []
for rule in access_rules:
if rule['access_type'] == 'ip':
ip_access_rules.append(rule)
elif rule['access_type'] == 'user':
user_access_rules.append(rule)
else:
message = (_("Access type %(type)s is not supported for CIFS."
) % {'type': rule['access_type']})
LOG.error(message)
rule_state_map.update({rule['access_id']: {'state': 'error'}})
ips = self._get_cifs_ip_list(ip_access_rules, rule_state_map)
user_permissions = self._get_cifs_user_permissions(
user_access_rules, rule_state_map)
share_updated = self._isilon_api.modify_smb_share_access(
share['name'],
host_acl=ips,
permissions=user_permissions)
if not share_updated:
message = (
_('Failed to update access rules for CIFS share "%(share)s".'
) % {'share': share['name']})
LOG.error(message)
for rule in access_rules:
rule_state_map[rule['access_id']] = {
'state': 'error'
}
return rule_state_map
def _get_cifs_ip_list(self, access_rules, rule_state_map):
"""Get CIFS ip list."""
cifs_ips = []
for rule in access_rules:
if rule['access_level'] != const.ACCESS_LEVEL_RW:
message = ('Only RW access level is supported '
'for CIFS IP access.')
LOG.error(message)
rule_state_map.update({rule['access_id']: {'state': 'error'}})
continue
cifs_ips.append('allow:' + rule['access_to'])
rule_state_map.update({rule['access_id']: {'state': 'active'}})
return cifs_ips
def _get_cifs_user_permissions(self, access_rules, rule_state_map):
"""Get CIFS user permissions."""
cifs_user_permissions = []
for rule in access_rules:
if rule['access_level'] == const.ACCESS_LEVEL_RW:
smb_permission = isilon_api.SmbPermission.rw
elif rule['access_level'] == const.ACCESS_LEVEL_RO:
smb_permission = isilon_api.SmbPermission.ro
else:
message = ('Only RW and RO access levels are supported '
'for CIFS user access.')
LOG.error(message)
rule_state_map.update({rule['access_id']: {'state': 'error'}})
continue
user_sid = self._isilon_api.get_user_sid(rule['access_to'])
if user_sid:
cifs_user_permissions.append({
'permission': smb_permission.value,
'permission_type': 'allow',
'trustee': user_sid
})
rule_state_map.update({rule['access_id']: {'state': 'active'}})
else:
message = _('Failed to get user sid by %(user)s.' %
{'user': rule['access_to']})
LOG.error(message)
rule_state_map.update({rule['access_id']: {'state': 'error'}})
return cifs_user_permissions
def get_backend_info(self, context):
"""Get driver and array configuration parameters.
:returns: A dictionary containing driver-specific info.
"""
LOG.debug("Retrieving PowerScale backend info.")
cluster_version = self._isilon_api.get_cluster_version()
return {'driver_version': VERSION,
'cluster_version': cluster_version,
'rest_server': self._server,
'rest_port': self._port}
def ensure_shares(self, context, shares):
"""Invoked to ensure that shares are exported.
:shares: A list of all shares for updates.
:returns: None or a dictionary of updates in the format.
"""
LOG.debug("Ensuring PowerScale shares.")
updates = {}
for share in shares:
if share['share_proto'] == 'NFS':
container_path = self._get_container_path(share)
share_id = self._isilon_api.lookup_nfs_export(container_path)
if share_id:
location = self._format_nfs_path(container_path)
updates[share['id']] = {
'export_locations': [location],
'status': 'available',
'reapply_access_rules': True,
}
else:
LOG.warning(f'NFS Share {share["name"]} is not found.')
elif share['share_proto'] == 'CIFS':
smb_share = self._isilon_api.lookup_smb_share(share['name'])
if smb_share:
location = self._format_smb_path(share['name'])
updates[share['id']] = {
'export_locations': [location],
'status': 'available',
'reapply_access_rules': True,
}
else:
LOG.warning(f'CIFS Share {share["name"]} is not found.')
if share['id'] not in updates:
updates[share['id']] = {
'export_locations': [],
'status': 'error',
'reapply_access_rules': False,
}
return updates
def _format_smb_path(self, share_name):
return '\\\\{0}\\{1}'.format(self._server, share_name)
def _format_nfs_path(self, container_path):
return '{0}:{1}'.format(self._server, container_path)
def _get_location(self, path):
export_locations = [{'path': path,
'is_admin_only': False,
'metadata': {"preferred": True}}]
return export_locations