manila/manila/share/drivers/inspur/instorage/instorage.py

624 lines
21 KiB
Python

# Copyright 2019 Inspur Corp.
# 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.
"""
Driver for Inspur InStorage
"""
import ipaddress
import itertools
import six
from oslo_config import cfg
from oslo_log import log
from oslo_utils import units
from manila import coordination
from manila import exception
from manila.i18n import _
from manila.share import driver
from manila.share import utils as share_utils
from manila.share.drivers.inspur.instorage.cli_helper import InStorageSSH
from manila.share.drivers.inspur.instorage.cli_helper import SSHRunner
instorage_opts = [
cfg.HostAddressOpt(
'instorage_nas_ip',
required=True,
help='IP address for the InStorage.'
),
cfg.PortOpt(
'instorage_nas_port',
default=22,
help='Port number for the InStorage.'
),
cfg.StrOpt(
'instorage_nas_login',
required=True,
help='Username for the InStorage.'
),
cfg.StrOpt(
'instorage_nas_password',
required=True,
secret=True,
help='Password for the InStorage.'
),
cfg.ListOpt(
'instorage_nas_pools',
required=True,
help='The Storage Pools Manila should use, a comma separated list.'
)
]
CONF = cfg.CONF
CONF.register_opts(instorage_opts)
LOG = log.getLogger(__name__)
class InStorageShareDriver(driver.ShareDriver):
"""Inspur InStorage NAS driver. Allows for NFS and CIFS NAS.
.. code::none
Version history:
1.0.0 - Initial driver.
Driver support:
share create/delete
extend size
update_access
protocol: NFS/CIFS
"""
VENDOR = 'INSPUR'
VERSION = '1.0.0'
PROTOCOL = 'NFS_CIFS'
def __init__(self, *args, **kwargs):
super(InStorageShareDriver, self).__init__(False, *args, **kwargs)
self.configuration.append_config_values(instorage_opts)
self.backend_name = self.configuration.safe_get('share_backend_name')
self.backend_pools = self.configuration.instorage_nas_pools
self.ssh_runner = SSHRunner(**{
'host': self.configuration.instorage_nas_ip,
'port': 22,
'login': self.configuration.instorage_nas_login,
'password': self.configuration.instorage_nas_password
})
self.assistant = InStorageAssistant(self.ssh_runner)
def check_for_setup_error(self):
nodes = self.assistant.get_nodes_info()
if len(nodes) == 0:
msg = _('No valid node, be sure the NAS Port IP is configured')
raise exception.ShareBackendException(msg=msg)
pools = self.assistant.get_available_pools()
not_exist = set(self.backend_pools).difference(set(pools))
if not_exist:
msg = _('Pool %s not exist on the storage system') % not_exist
raise exception.InvalidParameterValue(msg)
def _update_share_stats(self, **kwargs):
"""Retrieve share stats information."""
try:
stats = {
'share_backend_name': self.backend_name,
'vendor_name': self.VENDOR,
'driver_version': self.VERSION,
'storage_protocol': 'NFS_CIFS',
'reserved_percentage':
self.configuration.reserved_share_percentage,
'max_over_subscription_ratio':
self.configuration.max_over_subscription_ratio,
'snapshot_support': False,
'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': False,
'qos': False,
'total_capacity_gb': 0.0,
'free_capacity_gb': 0.0,
'pools': []
}
pools = self.assistant.get_pools_attr(self.backend_pools)
total_capacity_gb = 0
free_capacity_gb = 0
for pool in pools.values():
total_capacity_gb += pool['total_capacity_gb']
free_capacity_gb += pool['free_capacity_gb']
stats['pools'].append(pool)
stats['total_capacity_gb'] = total_capacity_gb
stats['free_capacity_gb'] = free_capacity_gb
LOG.debug('share status %s', stats)
super(InStorageShareDriver, self)._update_share_stats(stats)
except Exception:
msg = _('Unexpected error while trying to get the '
'usage stats from array.')
LOG.exception(msg)
raise
@staticmethod
def generate_share_name(share):
# Generate a name with id of the share as base, and do follows:
# 1. Remove the '-' in the id string.
# 2. Transform all alpha to lower case.
# 3. If the first char of the id is a num,
# transform it to an Upper case alpha start from 'A',
# such as '0' -> 'A', '1' -> 'B'.
# e.g.
# generate_share_name({
# 'id': '46CF5E85-D618-4023-8727-6A1EA9292954',
# ...
# })
# returns 'E6cf5e85d618402387276a1ea9292954'
name = share['id'].replace('-', '').lower()
if name[0] in '0123456789':
name = chr(ord('A') + ord(name[0]) - ord('0')) + name[1:]
return name
def get_network_allocations_number(self):
"""Get the number of network interfaces to be created."""
return 0
def create_share(self, context, share, share_server=None):
"""Create a new share instance."""
share_name = self.generate_share_name(share)
share_size = share['size']
share_proto = share['share_proto']
pool_name = share_utils.extract_host(share['host'], level='pool')
self.assistant.create_share(
share_name,
pool_name,
share_size,
share_proto
)
return self.assistant.get_export_locations(share_name, share_proto)
def delete_share(self, context, share, share_server=None):
"""Delete the given share instance."""
share_name = self.generate_share_name(share)
share_proto = share['share_proto']
self.assistant.delete_share(share_name, share_proto)
def extend_share(self, share, new_size, share_server=None):
"""Extend the share instance's size to new size."""
share_name = self.generate_share_name(share)
self.assistant.extend_share(share_name, new_size)
def ensure_share(self, context, share, share_server=None):
"""Ensure that the share instance is exported."""
share_name = self.generate_share_name(share)
share_proto = share['share_proto']
return self.assistant.get_export_locations(share_name, share_proto)
def update_access(self, context, share, access_rules, add_rules,
delete_rules, share_server=None):
"""Update the share instance's access rule."""
share_name = self.generate_share_name(share)
share_proto = share['share_proto']
@coordination.synchronized('inspur-instorage-access-' + share_name)
def _update_access(name, proto, rules, add_rules, delete_rules):
self.assistant.update_access(
name, proto, rules, add_rules, delete_rules
)
_update_access(
share_name, share_proto, access_rules, add_rules, delete_rules
)
class InStorageAssistant(object):
NFS_CLIENT_SPEC_PATTERN = (
'%(ip)s/%(mask)s:%(rights)s:%(all_squash)s:%(root_squash)s'
)
CIFS_CLIENT_RIGHT_PATTERN = (
'%(type)s:%(name)s:%(rights)s'
)
def __init__(self, ssh_runner):
self.ssh = InStorageSSH(ssh_runner)
@staticmethod
def handle_keyerror(cmd, out):
msg = (_('Could not find key in output of command %(cmd)s: %(out)s.')
% {'out': out, 'cmd': cmd})
raise exception.ShareBackendException(msg=msg)
def size_to_gb(self, size):
new_size = 0
if 'P' in size:
new_size = int(float(size.rstrip('PB')) * units.Mi)
elif 'T' in size:
new_size = int(float(size.rstrip('TB')) * units.Ki)
elif 'G' in size:
new_size = int(float(size.rstrip('GB')) * 1)
elif 'M' in size:
mb_size = float(size.rstrip('MB'))
new_size = int((mb_size + units.Ki - 1) / units.Ki)
return new_size
def get_available_pools(self):
nas_pools = self.ssh.lsnaspool()
return [pool['pool_name'] for pool in nas_pools]
def get_pools_attr(self, backend_pools):
pools = {}
fs_attr = self.ssh.lsfs()
nas_pools = self.ssh.lsnaspool()
for pool_attr in nas_pools:
pool_name = pool_attr['pool_name']
if pool_name not in backend_pools:
continue
total_used_capacity = 0
total_allocated_capacity = 0
for fs in fs_attr:
if fs['pool_name'] != pool_name:
continue
allocated = self.size_to_gb(fs['total_capacity'])
used = self.size_to_gb(fs['used_capacity'])
total_allocated_capacity += allocated
total_used_capacity += used
available = self.size_to_gb(pool_attr['available_capacity'])
pool = {
'pool_name': pool_name,
'total_capacity_gb': total_allocated_capacity + available,
'free_capacity_gb': available,
'allocated_capacity_gb': total_allocated_capacity,
'reserved_percentage': 0,
'qos': False,
'dedupe': False,
'compression': False,
'thin_provisioning': False,
'max_over_subscription_ratio': 0
}
pools[pool_name] = pool
return pools
def get_nodes_info(self):
"""Return a dictionary containing information of system's nodes."""
nodes = {}
resp = self.ssh.lsnasportip()
for port in resp:
try:
# Port is invalid if it has no IP configured.
if port['ip'] == '':
continue
node_name = port['node_name']
if node_name not in nodes:
nodes[node_name] = {}
node = nodes[node_name]
node[port['id']] = port
except KeyError:
self.handle_keyerror('lsnasportip', port)
return nodes
@staticmethod
def get_fsname_by_name(name):
return ('%(fsname)s' % {'fsname': name})[0:32]
@staticmethod
def get_dirname_by_name(name):
return ('%(dirname)s' % {'dirname': name})[0:32]
def get_dirpath_by_name(self, name):
fsname = self.get_fsname_by_name(name)
dirname = self.get_dirname_by_name(name)
return '/fs/%(fsname)s/%(dirname)s' % {
'fsname': fsname, 'dirname': dirname
}
def create_share(self, name, pool, size, proto):
"""Create a share with given info."""
# use one available node as the primary node
nodes = self.get_nodes_info()
if len(nodes) == 0:
msg = _('No valid node, be sure the NAS Port IP is configured')
raise exception.ShareBackendException(msg=msg)
node_name = [key for key in nodes.keys()][0]
# first create the file system on which share will be created
fsname = self.get_fsname_by_name(name)
self.ssh.addfs(fsname, pool, size, node_name)
# then create the directory used for the share
dirpath = self.get_dirpath_by_name(name)
self.ssh.addnasdir(dirpath)
# For CIFS, we need to create a CIFS share.
# For NAS, the share is automatically added when the first
# 'access spec' is added on it.
if proto == 'CIFS':
self.ssh.addcifs(name, dirpath)
def check_share_exist(self, name):
"""Check whether the specified share exist on backend."""
fsname = self.get_fsname_by_name(name)
for fs in self.ssh.lsfs():
if fs['fs_name'] == fsname:
return True
return False
def delete_share(self, name, proto):
"""Delete the given share."""
if not self.check_share_exist(name):
LOG.warning('Share %s does not exist on the backend.', name)
return
# For CIFS, we have to delete the share first.
# For NAS, when the last client access spec is removed from
# it, the share is automatically deleted.
if proto == 'CIFS':
self.ssh.rmcifs(name)
# then delete the directory
dirpath = self.get_dirpath_by_name(name)
self.ssh.rmnasdir(dirpath)
# at last delete the file system
fsname = self.get_fsname_by_name(name)
self.ssh.rmfs(fsname)
def extend_share(self, name, new_size):
"""Extend a given share to a new size.
:param name: the name of the share.
:param new_size: the new size the share should be.
:return:
"""
# first get the original capacity
old_size = None
fsname = self.get_fsname_by_name(name)
for fs in self.ssh.lsfs():
if fs['fs_name'] == fsname:
old_size = self.size_to_gb(fs['total_capacity'])
break
if old_size is None:
msg = _('share %s is not available') % name
raise exception.ShareBackendException(msg=msg)
LOG.debug('Extend fs %s from %dGB to %dGB', fsname, old_size, new_size)
self.ssh.expandfs(fsname, new_size - old_size)
def get_export_locations(self, name, share_proto):
"""Get the export locations of a given share.
:param name: the name of the share.
:param share_proto: the protocol of the share.
:return: a list of export locations.
"""
if share_proto == 'NFS':
dirpath = self.get_dirpath_by_name(name)
pattern = '%(ip)s:' + dirpath
elif share_proto == 'CIFS':
pattern = '\\\\%(ip)s\\' + name
else:
msg = _('share protocol %s is not supported') % share_proto
raise exception.ShareBackendException(msg=msg)
# we need get the node so that we know which port ip we can use
node_name = None
fsname = self.get_fsname_by_name(name)
for node in self.ssh.lsnode():
for fs in self.ssh.lsfs(node['name']):
if fs['fs_name'] == fsname:
node_name = node['name']
break
if node_name:
break
if node_name is None:
msg = _('share %s is not available') % name
raise exception.ShareBackendException(msg=msg)
locations = []
ports = self.ssh.lsnasportip()
for port in ports:
if port['node_name'] == node_name and port['ip'] != '':
location = pattern % {'ip': port['ip']}
locations.append({
'path': location,
'is_admin_only': False,
'metadata': {}
})
return locations
def classify_nfs_client_spec(self, client_spec, dirpath):
nfslist = self.ssh.lsnfslist(dirpath)
if len(nfslist):
nfsinfo = self.ssh.lsnfsinfo(dirpath)
spec_set = set([
self.NFS_CLIENT_SPEC_PATTERN % i for i in nfsinfo
])
else:
spec_set = set()
client_spec_set = set(client_spec)
del_spec = spec_set.difference(client_spec_set)
add_spec = client_spec_set.difference(spec_set)
return list(add_spec), list(del_spec)
def access_rule_to_client_spec(self, access_rule):
if access_rule['access_type'] != 'ip':
msg = _('only ip access type is supported when using NFS protocol')
raise exception.ShareBackendException(msg=msg)
network = ipaddress.ip_network(six.text_type(access_rule['access_to']))
if network.version != 4:
msg = _('only IPV4 is accepted when using NFS protocol')
raise exception.ShareBackendException(msg=msg)
client_spec = self.NFS_CLIENT_SPEC_PATTERN % {
'ip': six.text_type(network.network_address),
'mask': six.text_type(network.netmask),
'rights': access_rule['access_level'],
'all_squash': 'all_squash',
'root_squash': 'root_squash'
}
return client_spec
def update_nfs_access(self, share_name, access_rules, add_rules,
delete_rules):
"""Update a NFS share's access rule."""
dirpath = self.get_dirpath_by_name(share_name)
if add_rules or delete_rules:
add_spec = [
self.access_rule_to_client_spec(r) for r in add_rules
]
del_spec = [
self.access_rule_to_client_spec(r) for r in delete_rules
]
_, can_del_spec = self.classify_nfs_client_spec(
[], dirpath
)
to_del_set = set(del_spec)
can_del_set = set(can_del_spec)
will_del_set = to_del_set.intersection(can_del_set)
del_spec = list(will_del_set)
else:
access_spec = [
self.access_rule_to_client_spec(r) for r in access_rules
]
add_spec, del_spec = self.classify_nfs_client_spec(
access_spec, dirpath
)
for spec in del_spec:
self.ssh.rmnfsclient(dirpath, spec)
for spec in add_spec:
self.ssh.addnfsclient(dirpath, spec)
def classify_cifs_rights(self, access_rights, share_name):
cifsinfo = self.ssh.lscifsinfo(share_name)
rights_set = set([
self.CIFS_CLIENT_RIGHT_PATTERN % i for i in cifsinfo
])
access_rights_set = set(access_rights)
del_rights = rights_set.difference(access_rights_set)
add_rights = access_rights_set.difference(rights_set)
return list(add_rights), list(del_rights)
def access_rule_to_rights(self, access_rule):
if access_rule['access_type'] != 'user':
msg = _('only user access type is supported'
' when using CIFS protocol')
raise exception.ShareBackendException(msg=msg)
rights = self.CIFS_CLIENT_RIGHT_PATTERN % {
'type': 'LU',
'name': access_rule['access_to'],
'rights': access_rule['access_level']
}
return rights
def update_cifs_access(self, share_name, access_rules, add_rules,
delete_rules):
"""Update a CIFS share's access rule."""
if add_rules or delete_rules:
add_rights = [
self.access_rule_to_rights(r) for r in add_rules
]
del_rights = [
self.access_rule_to_rights(r) for r in delete_rules
]
else:
access_rights = [
self.access_rule_to_rights(r) for r in access_rules
]
add_rights, del_rights = self.classify_cifs_rights(
access_rights, share_name
)
for rights in del_rights:
self.ssh.rmcifsuser(share_name, rights)
for rights in add_rights:
self.ssh.addcifsuser(share_name, rights)
@staticmethod
def check_access_type(access_type, *rules):
rule_chain = itertools.chain(*rules)
if all([r['access_type'] == access_type for r in rule_chain]):
return True
else:
return False
def update_access(self, share_name, share_proto,
access_rules, add_rules, delete_rules):
if share_proto == 'CIFS':
if self.check_access_type('user', access_rules,
add_rules, delete_rules):
self.update_cifs_access(share_name, access_rules,
add_rules, delete_rules)
else:
msg = _("Only %s access type allowed.") % "user"
raise exception.InvalidShareAccess(reason=msg)
elif share_proto == 'NFS':
if self.check_access_type('ip', access_rules,
add_rules, delete_rules):
self.update_nfs_access(share_name, access_rules,
add_rules, delete_rules)
else:
msg = _("Only %s access type allowed.") % "ip"
raise exception.InvalidShareAccess(reason=msg)
else:
msg = _('share protocol %s is not supported') % share_proto
raise exception.ShareBackendException(msg=msg)