manila/manila/share/drivers/infortrend/infortrend_nas.py

643 lines
26 KiB
Python

# Copyright (c) 2019 Infortrend Technology, Inc.
# 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.
import json
import re
from oslo_concurrency import processutils
from oslo_log import log
from oslo_utils import units
from manila.common import constants
from manila import exception
from manila.i18n import _
from manila.share import utils as share_utils
from manila import utils as manila_utils
LOG = log.getLogger(__name__)
def _bi_to_gi(bi_size):
return bi_size / units.Gi
class InfortrendNAS(object):
_SSH_PORT = 22
def __init__(self, nas_ip, username, password, ssh_key,
timeout, pool_dict, channel_dict):
self.nas_ip = nas_ip
self.port = self._SSH_PORT
self.username = username
self.password = password
self.ssh_key = ssh_key
self.ssh_timeout = timeout
self.pool_dict = pool_dict
self.channel_dict = channel_dict
self.command = ""
self.ssh = None
self.sshpool = None
self.location = 'a@0'
def _execute(self, command_line):
command_line.extend(['-z', self.location])
commands = ' '.join(command_line)
manila_utils.check_ssh_injection(commands)
LOG.debug('Executing: %(command)s', {'command': commands})
cli_out = self._ssh_execute(commands)
return self._parser(cli_out)
def _ssh_execute(self, commands):
try:
out, err = processutils.ssh_execute(
self.ssh, commands,
timeout=self.ssh_timeout, check_exit_code=True)
except processutils.ProcessExecutionError as pe:
rc = pe.exit_code
out = pe.stdout
out = out.replace('\n', '\\n')
msg = _('Error on execute ssh command. '
'Exit code: %(rc)d, msg: %(out)s') % {
'rc': rc, 'out': out}
raise exception.InfortrendNASException(err=msg)
return out
def _parser(self, content=None):
LOG.debug('parsing data:\n%s', content)
content = content.replace("\r", "")
content = content.strip()
json_string = content.replace("'", "\"")
cli_data = json_string.splitlines()[2]
if cli_data:
try:
data_dict = json.loads(cli_data)
except Exception:
msg = _('Failed to parse data: '
'%(cli_data)s to dictionary.') % {
'cli_data': cli_data}
LOG.error(msg)
raise exception.InfortrendNASException(err=msg)
rc = int(data_dict['cliCode'][0]['Return'], 16)
if rc == 0:
result = data_dict['data']
else:
result = data_dict['cliCode'][0]['CLI']
else:
msg = _('No data is returned from NAS.')
LOG.error(msg)
raise exception.InfortrendNASException(err=msg)
if rc != 0:
msg = _('NASCLI error, returned: %(result)s.') % {
'result': result}
LOG.error(msg)
raise exception.InfortrendCLIException(
err=msg, rc=rc, out=result)
return rc, result
def do_setup(self):
self._init_connect()
self._ensure_service_on('nfs')
self._ensure_service_on('cifs')
def _init_connect(self):
if not (self.sshpool and self.ssh):
self.sshpool = manila_utils.SSHPool(ip=self.nas_ip,
port=self.port,
conn_timeout=None,
login=self.username,
password=self.password,
privatekey=self.ssh_key)
self.ssh = self.sshpool.create()
if not self.ssh.get_transport().is_active():
self.sshpool = manila_utils.SSHPool(ip=self.nas_ip,
port=self.port,
conn_timeout=None,
login=self.username,
password=self.password,
privatekey=self.ssh_key)
self.ssh = self.sshpool.create()
LOG.debug('NAScmd [%s@%s] start!', self.username, self.nas_ip)
def check_for_setup_error(self):
self._check_pools_setup()
self._check_channels_status()
def _ensure_service_on(self, proto, slot='A'):
command_line = ['service', 'status', proto]
rc, service_status = self._execute(command_line)
if not service_status[0][slot][proto.upper()]['enabled']:
command_line = ['service', 'restart', proto]
self._execute(command_line)
def _check_channels_status(self):
channel_list = list(self.channel_dict.keys())
command_line = ['ifconfig', 'inet', 'show']
rc, channels_status = self._execute(command_line)
for channel in channels_status:
if 'CH' in channel['datalink']:
ch = channel['datalink'].strip('CH')
if ch in self.channel_dict.keys():
self.channel_dict[ch] = channel['IP']
channel_list.remove(ch)
if channel['status'] == 'DOWN':
LOG.warning('Channel [%(ch)s] status '
'is down, please check.', {
'ch': ch})
if len(channel_list) != 0:
msg = _('Channel setting %(channel_list)s is invalid!') % {
'channel_list': channel_list}
LOG.error(msg)
raise exception.InfortrendNASException(message=msg)
def _check_pools_setup(self):
pool_list = list(self.pool_dict.keys())
command_line = ['folder', 'status']
rc, pool_data = self._execute(command_line)
for pool in pool_data:
pool_name = self._extract_pool_name(pool)
if pool_name in self.pool_dict.keys():
pool_list.remove(pool_name)
self.pool_dict[pool_name]['id'] = pool['volumeId']
self.pool_dict[pool_name]['path'] = pool['directory'] + '/'
if len(pool_list) == 0:
break
if len(pool_list) != 0:
msg = _('Please create %(pool_list)s pool/s in advance!') % {
'pool_list': pool_list}
LOG.error(msg)
raise exception.InfortrendNASException(message=msg)
def _extract_pool_name(self, pool_info):
return pool_info['directory'].split('/')[1]
def _extract_lv_name(self, pool_info):
return pool_info['path'].split('/')[2]
def update_pools_stats(self):
pools = []
command_line = ['folder', 'status']
rc, pools_data = self._execute(command_line)
for pool_info in pools_data:
pool_name = self._extract_pool_name(pool_info)
if pool_name in self.pool_dict.keys():
total_space = float(pool_info['size'])
pool_quota_used = self._get_pool_quota_used(pool_name)
available_space = total_space - pool_quota_used
total_capacity_gb = round(_bi_to_gi(total_space), 2)
free_capacity_gb = round(_bi_to_gi(available_space), 2)
pool = {
'pool_name': pool_name,
'total_capacity_gb': total_capacity_gb,
'free_capacity_gb': free_capacity_gb,
'reserved_percentage': 0,
'qos': False,
'dedupe': False,
'compression': False,
'snapshot_support': False,
'thin_provisioning': False,
'thick_provisioning': True,
'replication_type': None,
}
pools.append(pool)
return pools
def _get_pool_quota_used(self, pool_name):
pool_quota_used = 0.0
pool_data = self._get_share_pool_data(pool_name)
folder_name = self._extract_lv_name(pool_data)
command_line = ['fquota', 'status', pool_data['id'],
folder_name, '-t', 'folder']
rc, quota_status = self._execute(command_line)
for share_quota in quota_status:
pool_quota_used += int(share_quota['quota'])
return pool_quota_used
def _get_share_pool_data(self, pool_name):
if not pool_name:
msg = _("Pool is not available in the share host.")
raise exception.InvalidHost(reason=msg)
if pool_name in self.pool_dict.keys():
return self.pool_dict[pool_name]
else:
msg = _('Pool [%(pool_name)s] not set in conf.') % {
'pool_name': pool_name}
LOG.error(msg)
raise exception.InfortrendNASException(err=msg)
def create_share(self, share, share_server=None):
pool_name = share_utils.extract_host(share['host'], level='pool')
pool_data = self._get_share_pool_data(pool_name)
folder_name = self._extract_lv_name(pool_data)
share_proto = share['share_proto'].lower()
share_name = share['id'].replace('-', '')
share_path = pool_data['path'] + share_name
command_line = ['folder', 'options', pool_data['id'],
folder_name, '-c', share_name]
self._execute(command_line)
self._set_share_size(
pool_data['id'], pool_name, share_name, share['size'])
self._ensure_protocol_on(share_path, share_proto, share_name)
LOG.info('Create Share [%(share)s] completed.', {
'share': share['id']})
return self._export_location(
share_name, share_proto, pool_data['path'])
def _export_location(self, share_name, share_proto, pool_path=None):
location = []
location_data = {
'pool_path': pool_path,
'share_name': share_name,
}
self._check_channels_status()
for ch in sorted(self.channel_dict.keys()):
ip = self.channel_dict[ch]
if share_proto == 'nfs':
location.append(
ip + ':%(pool_path)s%(share_name)s' % location_data)
elif share_proto == 'cifs':
location.append(
'\\\\' + ip + '\\%(share_name)s' % location_data)
else:
msg = _('Unsupported protocol: [%s].') % share_proto
raise exception.InvalidInput(msg)
return location
def _set_share_size(self, pool_id, pool_name, share_name, share_size):
pool_data = self._get_share_pool_data(pool_name)
folder_name = self._extract_lv_name(pool_data)
command_line = ['fquota', 'create', pool_id, folder_name,
share_name, str(share_size) + 'G', '-t', 'folder']
self._execute(command_line)
LOG.debug('Set Share [%(share_name)s] '
'Size [%(share_size)s G] completed.', {
'share_name': share_name,
'share_size': share_size})
return
def _get_share_size(self, pool_id, pool_name, share_name):
share_size = None
command_line = ['fquota', 'status', pool_id,
share_name, '-t', 'folder']
rc, quota_status = self._execute(command_line)
for share_quota in quota_status:
if share_quota['name'] == share_name:
share_size = round(_bi_to_gi(float(share_quota['quota'])), 2)
break
return share_size
def delete_share(self, share, share_server=None):
pool_name = share_utils.extract_host(share['host'], level='pool')
pool_data = self._get_share_pool_data(pool_name)
folder_name = self._extract_lv_name(pool_data)
share_name = share['id'].replace('-', '')
if self._check_share_exist(pool_name, share_name):
command_line = ['folder', 'options', pool_data['id'],
folder_name, '-d', share_name]
self._execute(command_line)
else:
LOG.warning('Share [%(share_name)s] is already deleted.', {
'share_name': share_name})
LOG.info('Delete Share [%(share)s] completed.', {
'share': share['id']})
def _check_share_exist(self, pool_name, share_name):
path = self.pool_dict[pool_name]['path']
command_line = ['pagelist', 'folder', path]
rc, subfolders = self._execute(command_line)
return any(subfolder['name'] == share_name for subfolder in subfolders)
def update_access(self, share, access_rules, add_rules,
delete_rules, share_server=None):
self._evict_unauthorized_clients(share, access_rules, share_server)
access_dict = {}
for access in access_rules:
try:
self._allow_access(share, access, share_server)
except (exception.InfortrendNASException) as e:
msg = _('Failed to allow access to client %(access)s, '
'reason %(e)s.') % {
'access': access['access_to'], 'e': e}
LOG.error(msg)
access_dict[access['id']] = 'error'
return access_dict
def _evict_unauthorized_clients(self, share, access_rules,
share_server=None):
pool_name = share_utils.extract_host(share['host'], level='pool')
pool_data = self._get_share_pool_data(pool_name)
share_proto = share['share_proto'].lower()
share_name = share['id'].replace('-', '')
share_path = pool_data['path'] + share_name
access_list = []
for access in access_rules:
access_list.append(access['access_to'])
if share_proto == 'nfs':
host_ip_list = []
command_line = ['share', 'status', '-f', share_path]
rc, nfs_status = self._execute(command_line)
host_list = nfs_status[0]['nfs_detail']['hostList']
for host in host_list:
if host['host'] != '*':
host_ip_list.append(host['host'])
for ip in host_ip_list:
if ip not in access_list:
command_line = ['share', 'options', share_path,
'nfs', '-c', ip]
try:
self._execute(command_line)
except exception.InfortrendNASException:
msg = _("Failed to remove share access rule %s") % (ip)
LOG.exception(msg)
pass
elif share_proto == 'cifs':
host_user_list = []
command_line = ['acl', 'get', share_path]
rc, cifs_status = self._execute(command_line)
for cifs_rule in cifs_status:
if cifs_rule['name']:
host_user_list.append(cifs_rule['name'])
for user in host_user_list:
if user not in access_list:
command_line = ['acl', 'delete', share_path, '-u', user]
try:
self._execute(command_line)
except exception.InfortrendNASException:
msg = _("Failed to remove share access rule %s") % (
user)
LOG.exception(msg)
pass
def _allow_access(self, share, access, share_server=None):
pool_name = share_utils.extract_host(share['host'], level='pool')
pool_data = self._get_share_pool_data(pool_name)
share_name = share['id'].replace('-', '')
share_path = pool_data['path'] + share_name
share_proto = share['share_proto'].lower()
access_type = access['access_type']
access_level = access['access_level'] or constants.ACCESS_LEVEL_RW
access_to = access['access_to']
ACCESS_LEVEL_MAP = {access_level: access_level}
msg = self._check_access_legal(share_proto, access_type)
if msg:
raise exception.InvalidShareAccess(reason=msg)
if share_proto == 'nfs':
command_line = ['share', 'options', share_path, 'nfs',
'-h', access_to, '-p', access_level]
self._execute(command_line)
elif share_proto == 'cifs':
if not self._check_user_exist(access_to):
msg = _('Please create user [%(user)s] in advance.') % {
'user': access_to}
LOG.error(msg)
raise exception.InfortrendNASException(err=msg)
if access_level == constants.ACCESS_LEVEL_RW:
cifs_access = 'f'
elif access_level == constants.ACCESS_LEVEL_RO:
cifs_access = 'r'
try:
access_level = ACCESS_LEVEL_MAP[access_level]
except KeyError:
msg = _('Unsupported access_level: [%s].') % access_level
raise exception.InvalidInput(msg)
command_line = ['acl', 'set', share_path,
'-u', access_to, '-a', cifs_access]
self._execute(command_line)
LOG.info('Share [%(share)s] access to [%(access_to)s] '
'level [%(level)s] protocol [%(share_proto)s] completed.', {
'share': share['id'],
'access_to': access_to,
'level': access_level,
'share_proto': share_proto})
def _ensure_protocol_on(self, share_path, share_proto, cifs_name):
if not self._check_proto_enabled(share_path, share_proto):
command_line = ['share', share_path, share_proto, 'on']
if share_proto == 'cifs':
command_line.extend(['-n', cifs_name])
self._execute(command_line)
def _check_proto_enabled(self, share_path, share_proto):
command_line = ['share', 'status', '-f', share_path]
rc, share_status = self._execute(command_line)
if share_status:
check_enabled = share_status[0][share_proto]
if check_enabled:
return True
return False
def _check_user_exist(self, user_name):
command_line = ['useradmin', 'user', 'list']
rc, user_list = self._execute(command_line)
for user in user_list:
if user['Name'] == user_name:
return True
return False
def _check_access_legal(self, share_proto, access_type):
msg = None
if share_proto == 'cifs' and access_type != 'user':
msg = _('Infortrend CIFS share only supports USER access type.')
elif share_proto == 'nfs' and access_type != 'ip':
msg = _('Infortrend NFS share only supports IP access type.')
elif share_proto not in ('nfs', 'cifs'):
msg = _('Unsupported share protocol [%s].') % share_proto
return msg
def get_pool(self, share):
pool_name = share_utils.extract_host(share['host'], level='pool')
if not pool_name:
share_name = share['id'].replace('-', '')
for pool in self.pool_dict.keys():
if self._check_share_exist(pool, share_name):
pool_name = pool
break
return pool_name
def ensure_share(self, share, share_server=None):
share_proto = share['share_proto'].lower()
pool_name = share_utils.extract_host(share['host'], level='pool')
pool_data = self._get_share_pool_data(pool_name)
share_name = share['id'].replace('-', '')
return self._export_location(
share_name, share_proto, pool_data['path'])
def extend_share(self, share, new_size, share_server=None):
pool_name = share_utils.extract_host(share['host'], level='pool')
pool_data = self._get_share_pool_data(pool_name)
share_name = share['id'].replace('-', '')
self._set_share_size(pool_data['id'], pool_name, share_name, new_size)
LOG.info('Successfully Extend Share [%(share)s] '
'to size [%(new_size)s G].', {
'share': share['id'],
'new_size': new_size})
def shrink_share(self, share, new_size, share_server=None):
pool_name = share_utils.extract_host(share['host'], level='pool')
pool_data = self._get_share_pool_data(pool_name)
share_name = share['id'].replace('-', '')
folder_name = self._extract_lv_name(pool_data)
command_line = ['fquota', 'status', pool_data['id'],
folder_name, '-t', 'folder']
rc, quota_status = self._execute(command_line)
for share_quota in quota_status:
if share_quota['name'] == share_name:
used_space = round(_bi_to_gi(float(share_quota['used'])), 2)
if new_size < used_space:
raise exception.ShareShrinkingPossibleDataLoss(
share_id=share['id'])
self._set_share_size(pool_data['id'], pool_name, share_name, new_size)
LOG.info('Successfully Shrink Share [%(share)s] '
'to size [%(new_size)s G].', {
'share': share['id'],
'new_size': new_size})
def manage_existing(self, share, driver_options):
share_proto = share['share_proto'].lower()
pool_name = share_utils.extract_host(share['host'], level='pool')
pool_data = self._get_share_pool_data(pool_name)
volume_name = self._extract_lv_name(pool_data)
input_location = share['export_locations'][0]['path']
share_name = share['id'].replace('-', '')
ch_ip, folder_name = self._parse_location(input_location, share_proto)
if not self._check_channel_ip(ch_ip):
msg = _('Export location ip: [%(ch_ip)s] '
'is incorrect, please use data port ip.') % {
'ch_ip': ch_ip}
LOG.error(msg)
raise exception.InfortrendNASException(err=msg)
if not self._check_share_exist(pool_name, folder_name):
msg = _('Can not find folder [%(folder_name)s] '
'in pool [%(pool_name)s].') % {
'folder_name': folder_name,
'pool_name': pool_name}
LOG.error(msg)
raise exception.InfortrendNASException(err=msg)
share_path = pool_data['path'] + folder_name
self._ensure_protocol_on(share_path, share_proto, share_name)
share_size = self._get_share_size(
pool_data['id'], pool_name, folder_name)
if not share_size:
msg = _('Folder [%(folder_name)s] has no size limitation, '
'please set it first for Openstack management.') % {
'folder_name': folder_name}
LOG.error(msg)
raise exception.InfortrendNASException(err=msg)
# rename folder name
command_line = ['folder', 'options', pool_data['id'], volume_name,
'-k', folder_name, share_name]
self._execute(command_line)
location = self._export_location(
share_name, share_proto, pool_data['path'])
LOG.info('Successfully Manage Infortrend Share [%(folder_name)s], '
'Size: [%(size)s G], Protocol: [%(share_proto)s], '
'new name: [%(share_name)s].', {
'folder_name': folder_name,
'size': share_size,
'share_proto': share_proto,
'share_name': share_name})
return {'size': share_size, 'export_locations': location}
def _parse_location(self, input_location, share_proto):
ip = None
folder_name = None
pattern_ip = r'[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'
if share_proto == 'nfs':
pattern_folder = r'[^\/]+$'
ip = "".join(re.findall(pattern_ip, input_location))
folder_name = "".join(re.findall(pattern_folder, input_location))
elif share_proto == 'cifs':
pattern_folder = r'[^\\]+$'
ip = "".join(re.findall(pattern_ip, input_location))
folder_name = "".join(re.findall(pattern_folder, input_location))
if not (ip and folder_name):
msg = _('Export location error, please check '
'ip: [%(ip)s], folder_name: [%(folder_name)s].') % {
'ip': ip,
'folder_name': folder_name}
LOG.error(msg)
raise exception.InfortrendNASException(err=msg)
return ip, folder_name
def _check_channel_ip(self, channel_ip):
return any(ip == channel_ip for ip in self.channel_dict.values())
def unmanage(self, share):
pool_name = share_utils.extract_host(share['host'], level='pool')
share_name = share['id'].replace('-', '')
if not self._check_share_exist(pool_name, share_name):
LOG.warning('Share [%(share_name)s] does not exist.', {
'share_name': share_name})
return
LOG.info('Successfully Unmanaged Share [%(share)s].', {
'share': share['id']})