Manila share driver for Inspur InStorage series.
Features that Inspur InStorage driver support: share create/delete. extend share size. update_access. protocol: NFS/CIFS. ThirdPartySystem: INSPUR CI Change-Id: I931902901668c295fb12523bf3d414a897c36275 Implements: Blueprint inspur-instorage-manila-driver
This commit is contained in:
parent
86823b5cb6
commit
54d855877b
|
@ -67,7 +67,9 @@ Mapping of share drivers and share features support
|
||||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+
|
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+
|
||||||
| INFINIDAT | Q | \- | Q | \- | Q | Q | \- | Q | Q |
|
| INFINIDAT | Q | \- | Q | \- | Q | Q | \- | Q | Q |
|
||||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+
|
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+
|
||||||
| INSPUR | R | \- | R | \- | R | R | \- | \- | \- |
|
| INSPUR AS13000 | R | \- | R | \- | R | R | \- | \- | \- |
|
||||||
|
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+
|
||||||
|
| INSPUR InStorage | T | \- | T | \- | \- | \- | \- | \- | \- |
|
||||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+
|
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+
|
||||||
| LVM | M | \- | M | \- | M | M | \- | O | O |
|
| LVM | M | \- | M | \- | M | M | \- | O | O |
|
||||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+
|
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+
|
||||||
|
@ -138,7 +140,9 @@ Mapping of share drivers and share access rules support
|
||||||
+----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+
|
+----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+
|
||||||
| INFINIDAT | NFS (Q) | \- | \- | \- | \- | NFS (Q) | \- | \- | \- | \- |
|
| INFINIDAT | NFS (Q) | \- | \- | \- | \- | NFS (Q) | \- | \- | \- | \- |
|
||||||
+----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+
|
+----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+
|
||||||
| INSPUR | NFS (R) | \- | CIFS (R) | \- | \- | NFS (R) | \- | CIFS (R) | \- | \- |
|
| INSPUR AS13000 | NFS (R) | \- | CIFS (R) | \- | \- | NFS (R) | \- | CIFS (R) | \- | \- |
|
||||||
|
+----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+
|
||||||
|
| INSPUR InStorage | NFS (T) | \- | CIFS (T) | \- | \- | NFS (T) | \- | CIFS (T) | \- | \- |
|
||||||
+----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+
|
+----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+
|
||||||
| Oracle ZFSSA | NFS,CIFS(K) | \- | \- | \- | \- | \- | \- | \- | \- | \- |
|
| Oracle ZFSSA | NFS,CIFS(K) | \- | \- | \- | \- | \- | \- | \- | \- | \- |
|
||||||
+----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+
|
+----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+
|
||||||
|
@ -201,7 +205,9 @@ Mapping of share drivers and security services support
|
||||||
+----------------------------------------+------------------+-----------------+------------------+
|
+----------------------------------------+------------------+-----------------+------------------+
|
||||||
| INFINIDAT | \- | \- | \- |
|
| INFINIDAT | \- | \- | \- |
|
||||||
+----------------------------------------+------------------+-----------------+------------------+
|
+----------------------------------------+------------------+-----------------+------------------+
|
||||||
| INSPUR | \- | \- | \- |
|
| INSPUR AS13000 | \- | \- | \- |
|
||||||
|
+----------------------------------------+------------------+-----------------+------------------+
|
||||||
|
| INSPUR InStorage | \- | \- | \- |
|
||||||
+----------------------------------------+------------------+-----------------+------------------+
|
+----------------------------------------+------------------+-----------------+------------------+
|
||||||
| Oracle ZFSSA | \- | \- | \- |
|
| Oracle ZFSSA | \- | \- | \- |
|
||||||
+----------------------------------------+------------------+-----------------+------------------+
|
+----------------------------------------+------------------+-----------------+------------------+
|
||||||
|
@ -280,7 +286,9 @@ More information: :ref:`capabilities_and_extra_specs`
|
||||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+
|
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+
|
||||||
| QNAP | \- | O | Q | Q | O | Q | \- | O | \- | \- | P | \- |
|
| QNAP | \- | O | Q | Q | O | Q | \- | O | \- | \- | P | \- |
|
||||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+
|
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+
|
||||||
| INSPUR | \- | R | \- | \- | R | \- | \- | R | \- | \- | R | \- |
|
| INSPUR AS13000 | \- | R | \- | \- | R | \- | \- | R | \- | \- | R | \- |
|
||||||
|
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+
|
||||||
|
| INSPUR InStorage | \- | T | \- | \- | \- | T | \- | \- | \- | \- | T | \- |
|
||||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+
|
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
|
@ -73,6 +73,7 @@ import manila.share.drivers.huawei.huawei_nas
|
||||||
import manila.share.drivers.ibm.gpfs
|
import manila.share.drivers.ibm.gpfs
|
||||||
import manila.share.drivers.infinidat.infinibox
|
import manila.share.drivers.infinidat.infinibox
|
||||||
import manila.share.drivers.inspur.as13000.as13000_nas
|
import manila.share.drivers.inspur.as13000.as13000_nas
|
||||||
|
import manila.share.drivers.inspur.instorage.instorage
|
||||||
import manila.share.drivers.lvm
|
import manila.share.drivers.lvm
|
||||||
import manila.share.drivers.maprfs.maprfs_native
|
import manila.share.drivers.maprfs.maprfs_native
|
||||||
import manila.share.drivers.netapp.options
|
import manila.share.drivers.netapp.options
|
||||||
|
@ -159,6 +160,7 @@ _global_opt_lists = [
|
||||||
manila.share.drivers.infinidat.infinibox.infinidat_connection_opts,
|
manila.share.drivers.infinidat.infinibox.infinidat_connection_opts,
|
||||||
manila.share.drivers.infinidat.infinibox.infinidat_general_opts,
|
manila.share.drivers.infinidat.infinibox.infinidat_general_opts,
|
||||||
manila.share.drivers.inspur.as13000.as13000_nas.inspur_as13000_opts,
|
manila.share.drivers.inspur.as13000.as13000_nas.inspur_as13000_opts,
|
||||||
|
manila.share.drivers.inspur.instorage.instorage.instorage_opts,
|
||||||
manila.share.drivers.maprfs.maprfs_native.maprfs_native_share_opts,
|
manila.share.drivers.maprfs.maprfs_native.maprfs_native_share_opts,
|
||||||
manila.share.drivers.lvm.share_opts,
|
manila.share.drivers.lvm.share_opts,
|
||||||
manila.share.drivers.netapp.options.netapp_proxy_opts,
|
manila.share.drivers.netapp.options.netapp_proxy_opts,
|
||||||
|
|
|
@ -0,0 +1,476 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
CLI helpers for Inspur InStorage
|
||||||
|
"""
|
||||||
|
|
||||||
|
import paramiko
|
||||||
|
import re
|
||||||
|
import six
|
||||||
|
|
||||||
|
from eventlet import greenthread
|
||||||
|
|
||||||
|
from oslo_concurrency import processutils
|
||||||
|
from oslo_log import log
|
||||||
|
from oslo_utils import excutils
|
||||||
|
|
||||||
|
from manila import exception
|
||||||
|
from manila.i18n import _
|
||||||
|
from manila import utils as manila_utils
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SSHRunner(object):
|
||||||
|
"""SSH runner is used to run ssh command on inspur instorage system."""
|
||||||
|
|
||||||
|
def __init__(self, host, port, login, password, privatekey=None):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.login = login
|
||||||
|
self.password = password
|
||||||
|
self.privatekey = privatekey
|
||||||
|
|
||||||
|
self.ssh_conn_timeout = 60
|
||||||
|
self.ssh_min_pool_size = 1
|
||||||
|
self.ssh_max_pool_size = 10
|
||||||
|
|
||||||
|
self.sshpool = None
|
||||||
|
|
||||||
|
def __call__(self, cmd_list, check_exit_code=True, attempts=1):
|
||||||
|
"""SSH tool"""
|
||||||
|
manila_utils.check_ssh_injection(cmd_list)
|
||||||
|
command = ' '.join(cmd_list)
|
||||||
|
if not self.sshpool:
|
||||||
|
try:
|
||||||
|
self.sshpool = manila_utils.SSHPool(
|
||||||
|
self.host,
|
||||||
|
self.port,
|
||||||
|
self.ssh_conn_timeout,
|
||||||
|
self.login,
|
||||||
|
password=self.password,
|
||||||
|
privatekey=self.privatekey,
|
||||||
|
min_size=self.ssh_min_pool_size,
|
||||||
|
max_size=self.ssh_max_pool_size
|
||||||
|
)
|
||||||
|
except paramiko.SSHException:
|
||||||
|
LOG.error("Unable to create SSHPool")
|
||||||
|
raise
|
||||||
|
try:
|
||||||
|
return self._ssh_execute(self.sshpool, command,
|
||||||
|
check_exit_code, attempts)
|
||||||
|
except Exception:
|
||||||
|
LOG.error("Error running SSH command: %s", command)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _ssh_execute(self, sshpool, command,
|
||||||
|
check_exit_code=True, attempts=1):
|
||||||
|
try:
|
||||||
|
with sshpool.item() as ssh:
|
||||||
|
last_exception = None
|
||||||
|
while attempts > 0:
|
||||||
|
attempts -= 1
|
||||||
|
try:
|
||||||
|
return processutils.ssh_execute(
|
||||||
|
ssh,
|
||||||
|
command,
|
||||||
|
check_exit_code=check_exit_code)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception('Error has occurred')
|
||||||
|
last_exception = e
|
||||||
|
greenthread.sleep(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
raise processutils.ProcessExecutionError(
|
||||||
|
exit_code=last_exception.exit_code,
|
||||||
|
stdout=last_exception.stdout,
|
||||||
|
stderr=last_exception.stderr,
|
||||||
|
cmd=last_exception.cmd)
|
||||||
|
except AttributeError:
|
||||||
|
raise processutils.ProcessExecutionError(
|
||||||
|
exit_code=-1,
|
||||||
|
stdout="",
|
||||||
|
stderr="Error running SSH command",
|
||||||
|
cmd=command)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.error("Error running SSH command: %s", command)
|
||||||
|
|
||||||
|
|
||||||
|
class CLIParser(object):
|
||||||
|
"""Parse MCS CLI output and generate iterable."""
|
||||||
|
|
||||||
|
def __init__(self, raw, ssh_cmd=None, delim='!', with_header=True):
|
||||||
|
super(CLIParser, self).__init__()
|
||||||
|
if ssh_cmd:
|
||||||
|
self.ssh_cmd = ' '.join(ssh_cmd)
|
||||||
|
else:
|
||||||
|
self.ssh_cmd = 'None'
|
||||||
|
self.raw = raw
|
||||||
|
self.delim = delim
|
||||||
|
self.with_header = with_header
|
||||||
|
self.result = self._parse()
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
try:
|
||||||
|
return self.result[key]
|
||||||
|
except KeyError:
|
||||||
|
msg = (_('Did not find the expected key %(key)s in %(fun)s: '
|
||||||
|
'%(raw)s.') % {'key': key, 'fun': self.ssh_cmd,
|
||||||
|
'raw': self.raw})
|
||||||
|
raise exception.ShareBackendException(msg=msg)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
for a in self.result:
|
||||||
|
yield a
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.result)
|
||||||
|
|
||||||
|
def _parse(self):
|
||||||
|
def get_reader(content, delim):
|
||||||
|
for line in content.lstrip().splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line:
|
||||||
|
yield line.split(delim)
|
||||||
|
else:
|
||||||
|
yield []
|
||||||
|
|
||||||
|
if isinstance(self.raw, six.string_types):
|
||||||
|
stdout, stderr = self.raw, ''
|
||||||
|
else:
|
||||||
|
stdout, stderr = self.raw
|
||||||
|
reader = get_reader(stdout, self.delim)
|
||||||
|
result = []
|
||||||
|
|
||||||
|
if self.with_header:
|
||||||
|
hds = tuple()
|
||||||
|
for row in reader:
|
||||||
|
hds = row
|
||||||
|
break
|
||||||
|
for row in reader:
|
||||||
|
cur = dict()
|
||||||
|
if len(hds) != len(row):
|
||||||
|
msg = (_('Unexpected CLI response: header/row mismatch. '
|
||||||
|
'header: %(header)s, row: %(row)s.')
|
||||||
|
% {'header': hds,
|
||||||
|
'row': row})
|
||||||
|
raise exception.ShareBackendException(msg=msg)
|
||||||
|
for k, v in zip(hds, row):
|
||||||
|
CLIParser.append_dict(cur, k, v)
|
||||||
|
result.append(cur)
|
||||||
|
else:
|
||||||
|
cur = dict()
|
||||||
|
for row in reader:
|
||||||
|
if row:
|
||||||
|
CLIParser.append_dict(cur, row[0], ' '.join(row[1:]))
|
||||||
|
elif cur: # start new section
|
||||||
|
result.append(cur)
|
||||||
|
cur = dict()
|
||||||
|
if cur:
|
||||||
|
result.append(cur)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def append_dict(dict_, key, value):
|
||||||
|
key, value = key.strip(), value.strip()
|
||||||
|
obj = dict_.get(key, None)
|
||||||
|
if obj is None:
|
||||||
|
dict_[key] = value
|
||||||
|
elif isinstance(obj, list):
|
||||||
|
obj.append(value)
|
||||||
|
dict_[key] = obj
|
||||||
|
else:
|
||||||
|
dict_[key] = [obj, value]
|
||||||
|
return dict_
|
||||||
|
|
||||||
|
|
||||||
|
class InStorageSSH(object):
|
||||||
|
"""SSH interface to Inspur InStorage systems."""
|
||||||
|
|
||||||
|
def __init__(self, ssh_runner):
|
||||||
|
self._ssh = ssh_runner
|
||||||
|
|
||||||
|
def _run_ssh(self, ssh_cmd):
|
||||||
|
try:
|
||||||
|
return self._ssh(ssh_cmd)
|
||||||
|
except processutils.ProcessExecutionError as e:
|
||||||
|
msg = (_('CLI Exception output:\n command: %(cmd)s\n '
|
||||||
|
'stdout: %(out)s\n stderr: %(err)s.') %
|
||||||
|
{'cmd': ssh_cmd,
|
||||||
|
'out': e.stdout,
|
||||||
|
'err': e.stderr})
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.ShareBackendException(msg=msg)
|
||||||
|
|
||||||
|
def run_ssh_inq(self, ssh_cmd, delim='!', with_header=False):
|
||||||
|
"""Run an SSH command and return parsed output."""
|
||||||
|
raw = self._run_ssh(ssh_cmd)
|
||||||
|
LOG.debug('Response for cmd %s is %s', ssh_cmd, raw)
|
||||||
|
return CLIParser(raw, ssh_cmd=ssh_cmd, delim=delim,
|
||||||
|
with_header=with_header)
|
||||||
|
|
||||||
|
def run_ssh_assert_no_output(self, ssh_cmd):
|
||||||
|
"""Run an SSH command and assert no output returned."""
|
||||||
|
out, err = self._run_ssh(ssh_cmd)
|
||||||
|
if len(out.strip()) != 0:
|
||||||
|
msg = (_('Expected no output from CLI command %(cmd)s, '
|
||||||
|
'got %(out)s.') % {'cmd': ' '.join(ssh_cmd), 'out': out})
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.ShareBackendException(msg=msg)
|
||||||
|
|
||||||
|
def run_ssh_check_created(self, ssh_cmd):
|
||||||
|
"""Run an SSH command and return the ID of the created object."""
|
||||||
|
out, err = self._run_ssh(ssh_cmd)
|
||||||
|
try:
|
||||||
|
match_obj = re.search(r'\[([0-9]+)\],? successfully created', out)
|
||||||
|
return match_obj.group(1)
|
||||||
|
except (AttributeError, IndexError):
|
||||||
|
msg = (_('Failed to parse CLI output:\n command: %(cmd)s\n '
|
||||||
|
'stdout: %(out)s\n stderr: %(err)s.') %
|
||||||
|
{'cmd': ssh_cmd,
|
||||||
|
'out': out,
|
||||||
|
'err': err})
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.ShareBackendException(msg=msg)
|
||||||
|
|
||||||
|
def lsnode(self, node_id=None):
|
||||||
|
with_header = True
|
||||||
|
ssh_cmd = ['mcsinq', 'lsnode', '-delim', '!']
|
||||||
|
if node_id:
|
||||||
|
with_header = False
|
||||||
|
ssh_cmd.append(node_id)
|
||||||
|
return self.run_ssh_inq(ssh_cmd, with_header=with_header)
|
||||||
|
|
||||||
|
def lsnaspool(self, pool_id=None):
|
||||||
|
ssh_cmd = ['mcsinq', 'lsnaspool', '-delim', '!']
|
||||||
|
if pool_id:
|
||||||
|
ssh_cmd.append(pool_id)
|
||||||
|
return self.run_ssh_inq(ssh_cmd, with_header=True)
|
||||||
|
|
||||||
|
def lsfs(self, node_name=None, fsname=None):
|
||||||
|
if fsname and not node_name:
|
||||||
|
msg = _('Node name should be set when file system name is set.')
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.InvalidParameterValue(msg)
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsinq', 'lsfs', '-delim', '!']
|
||||||
|
to_append = []
|
||||||
|
|
||||||
|
if node_name:
|
||||||
|
to_append += ['-node', '"%s"' % node_name]
|
||||||
|
|
||||||
|
if fsname:
|
||||||
|
to_append += ['-name', '"%s"' % fsname]
|
||||||
|
|
||||||
|
if not to_append:
|
||||||
|
to_append += ['-all']
|
||||||
|
|
||||||
|
ssh_cmd += to_append
|
||||||
|
return self.run_ssh_inq(ssh_cmd, with_header=True)
|
||||||
|
|
||||||
|
def addfs(self, fsname, pool_name, size, node_name):
|
||||||
|
"""Create a file system on the storage.
|
||||||
|
|
||||||
|
:param fsname: file system name
|
||||||
|
:param pool_name: pool in which to create the file system
|
||||||
|
:param size: file system size in GB
|
||||||
|
:param node_name: the primary node name
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsop', 'addfs', '-name', '"%s"' % fsname, '-pool',
|
||||||
|
'"%s"' % pool_name, '-size', '%dg' % size,
|
||||||
|
'-node', '"%s"' % node_name]
|
||||||
|
self.run_ssh_assert_no_output(ssh_cmd)
|
||||||
|
|
||||||
|
def rmfs(self, fsname):
|
||||||
|
"""Remove the specific file system.
|
||||||
|
|
||||||
|
:param fsname: file system name to be removed
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsop', 'rmfs', '-name', '"%s"' % fsname]
|
||||||
|
self.run_ssh_assert_no_output(ssh_cmd)
|
||||||
|
|
||||||
|
def expandfs(self, fsname, size):
|
||||||
|
"""Expand the space of the specific file system.
|
||||||
|
|
||||||
|
:param fsname: file system name
|
||||||
|
:param size: the size(GB) to be expanded, origin + size = result
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsop', 'expandfs', '-name', '"%s"' % fsname,
|
||||||
|
'-size', '%dg' % size]
|
||||||
|
self.run_ssh_assert_no_output(ssh_cmd)
|
||||||
|
|
||||||
|
# NAS directory operation
|
||||||
|
def lsnasdir(self, dirpath):
|
||||||
|
"""List the child directory under dirpath.
|
||||||
|
|
||||||
|
:param dirpath: the parent directory to list with
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsinq', 'lsnasdir', '-delim', '!', '"%s"' % dirpath]
|
||||||
|
return self.run_ssh_inq(ssh_cmd, with_header=True)
|
||||||
|
|
||||||
|
def addnasdir(self, dirpath):
|
||||||
|
"""Create a new NAS directory indicated by dirpath."""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsop', 'addnasdir', '"%s"' % dirpath]
|
||||||
|
self.run_ssh_assert_no_output(ssh_cmd)
|
||||||
|
|
||||||
|
def chnasdir(self, old_path, new_path):
|
||||||
|
"""Rename the NAS directory name."""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsop', 'chnasdir', '-oldpath', '"%s"' % old_path,
|
||||||
|
'-newpath', '"%s"' % new_path]
|
||||||
|
self.run_ssh_assert_no_output(ssh_cmd)
|
||||||
|
|
||||||
|
def rmnasdir(self, dirpath):
|
||||||
|
"""Remove the specific dirpath."""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsop', 'rmnasdir', '"%s"' % dirpath]
|
||||||
|
self.run_ssh_assert_no_output(ssh_cmd)
|
||||||
|
|
||||||
|
# NFS operation
|
||||||
|
def rmnfs(self, share_path):
|
||||||
|
"""Remove the NFS indicated by path."""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsop', 'rmnfs', '"%s"' % share_path]
|
||||||
|
self.run_ssh_assert_no_output(ssh_cmd)
|
||||||
|
|
||||||
|
def lsnfslist(self, prefix=None):
|
||||||
|
"""List NFS shares on a system."""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsinq', 'lsnfslist', '-delim', '!']
|
||||||
|
if prefix:
|
||||||
|
ssh_cmd.append('"%s"' % prefix)
|
||||||
|
|
||||||
|
return self.run_ssh_inq(ssh_cmd, with_header=True)
|
||||||
|
|
||||||
|
def lsnfsinfo(self, share_path):
|
||||||
|
"""List a specific NFS share's information."""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsinq', 'lsnfsinfo', '-delim', '!', '"%s"' % share_path]
|
||||||
|
return self.run_ssh_inq(ssh_cmd, with_header=True)
|
||||||
|
|
||||||
|
def addnfsclient(self, share_path, client_spec):
|
||||||
|
"""Add a client access rule to NFS share.
|
||||||
|
|
||||||
|
:param share_path: the NFS share path.
|
||||||
|
:param client_spec: IP/MASK:RIGHTS:ALL_SQUASH:ROOT_SQUASH.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsop', 'addnfsclient', '-path', '"%s"' % share_path,
|
||||||
|
'-client', client_spec]
|
||||||
|
self.run_ssh_assert_no_output(ssh_cmd)
|
||||||
|
|
||||||
|
def chnfsclient(self, share_path, client_spec):
|
||||||
|
"""Change a NFS share's client info."""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsop', 'chnfsclient', '-path', '"%s"' % share_path,
|
||||||
|
'-client', client_spec]
|
||||||
|
self.run_ssh_assert_no_output(ssh_cmd)
|
||||||
|
|
||||||
|
def rmnfsclient(self, share_path, client_spec):
|
||||||
|
"""Remove a client info from the NFS share."""
|
||||||
|
|
||||||
|
# client_spec parameter for rmnfsclient is IP/MASK,
|
||||||
|
# so we need remove the right part
|
||||||
|
client_spec = client_spec.split(':')[0]
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsop', 'rmnfsclient', '-path', '"%s"' % share_path,
|
||||||
|
'-client', client_spec]
|
||||||
|
self.run_ssh_assert_no_output(ssh_cmd)
|
||||||
|
|
||||||
|
# CIFS operation
|
||||||
|
def lscifslist(self, filter=None):
|
||||||
|
"""List CIFS shares on the system."""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsinq', 'lscifslist', '-delim', '!']
|
||||||
|
if filter:
|
||||||
|
ssh_cmd.append('"%s"' % filter)
|
||||||
|
|
||||||
|
return self.run_ssh_inq(ssh_cmd, with_header=True)
|
||||||
|
|
||||||
|
def lscifsinfo(self, share_name):
|
||||||
|
"""List a specific CIFS share's information."""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsinq', 'lscifsinfo', '-delim', '!', '"%s"' % share_name]
|
||||||
|
return self.run_ssh_inq(ssh_cmd, with_header=True)
|
||||||
|
|
||||||
|
def addcifs(self, share_name, dirpath, oplocks='off'):
|
||||||
|
"""Create a CIFS share with given path."""
|
||||||
|
ssh_cmd = ['mcsop', 'addcifs', '-name', share_name, '-path', dirpath,
|
||||||
|
'-oplocks', oplocks]
|
||||||
|
self.run_ssh_assert_no_output(ssh_cmd)
|
||||||
|
|
||||||
|
def rmcifs(self, share_name):
|
||||||
|
"""Remove a CIFS share."""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsop', 'rmcifs', share_name]
|
||||||
|
self.run_ssh_assert_no_output(ssh_cmd)
|
||||||
|
|
||||||
|
def chcifs(self, share_name, oplocks='off'):
|
||||||
|
"""Change a CIFS share's attribute.
|
||||||
|
|
||||||
|
:param share_name: share's name
|
||||||
|
:param oplocks: 'off' or 'on'
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
ssh_cmd = ['mcsop', 'chcifs', '-name', share_name, '-oplocks', oplocks]
|
||||||
|
self.run_ssh_assert_no_output(ssh_cmd)
|
||||||
|
|
||||||
|
def addcifsuser(self, share_name, rights):
|
||||||
|
"""Add a user access rule to CIFS share.
|
||||||
|
|
||||||
|
:param share_name: share's name
|
||||||
|
:param rights: [LU|LG]:xxx:[rw|ro]
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
ssh_cmd = ['mcsop', 'addcifsuser', '-name', share_name,
|
||||||
|
'-rights', rights]
|
||||||
|
self.run_ssh_assert_no_output(ssh_cmd)
|
||||||
|
|
||||||
|
def chcifsuser(self, share_name, rights):
|
||||||
|
"""Change a user access rule."""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsop', 'chcifsuser', '-name', share_name,
|
||||||
|
'-rights', rights]
|
||||||
|
self.run_ssh_assert_no_output(ssh_cmd)
|
||||||
|
|
||||||
|
def rmcifsuser(self, share_name, rights):
|
||||||
|
"""Remove CIFS user from a CIFS share."""
|
||||||
|
|
||||||
|
# the rights parameter for rmcifsuser is LU:NAME
|
||||||
|
rights = ':'.join(rights.split(':')[0:-1])
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsop', 'rmcifsuser', '-name', share_name,
|
||||||
|
'-rights', rights]
|
||||||
|
self.run_ssh_assert_no_output(ssh_cmd)
|
||||||
|
|
||||||
|
# NAS port ip
|
||||||
|
def lsnasportip(self):
|
||||||
|
"""List NAS service port ip address."""
|
||||||
|
|
||||||
|
ssh_cmd = ['mcsinq', 'lsnasportip', '-delim', '!']
|
||||||
|
return self.run_ssh_inq(ssh_cmd, with_header=True)
|
|
@ -0,0 +1,623 @@
|
||||||
|
# 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)
|
|
@ -60,6 +60,11 @@ def set_defaults(conf):
|
||||||
_safe_set_of_opts(conf, 'as13000_nas_password', 'password')
|
_safe_set_of_opts(conf, 'as13000_nas_password', 'password')
|
||||||
_safe_set_of_opts(conf, 'as13000_share_pools', 'pool0')
|
_safe_set_of_opts(conf, 'as13000_share_pools', 'pool0')
|
||||||
|
|
||||||
|
_safe_set_of_opts(conf, 'instorage_nas_ip', '1.1.1.1')
|
||||||
|
_safe_set_of_opts(conf, 'instorage_nas_login', 'admin')
|
||||||
|
_safe_set_of_opts(conf, 'instorage_nas_password', 'password')
|
||||||
|
_safe_set_of_opts(conf, 'instorage_nas_pools', 'pool0')
|
||||||
|
|
||||||
_safe_set_of_opts(conf, 'qnap_management_url', 'http://1.2.3.4:8080')
|
_safe_set_of_opts(conf, 'qnap_management_url', 'http://1.2.3.4:8080')
|
||||||
_safe_set_of_opts(conf, 'qnap_share_ip', '1.2.3.4')
|
_safe_set_of_opts(conf, 'qnap_share_ip', '1.2.3.4')
|
||||||
_safe_set_of_opts(conf, 'qnap_nas_login', 'admin')
|
_safe_set_of_opts(conf, 'qnap_nas_login', 'admin')
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
prelude: >
|
||||||
|
Add Inspur InStorage driver.
|
||||||
|
features:
|
||||||
|
- Add new Inspur InStorage driver, support share create, delete, extend,
|
||||||
|
and access through NFS and CIFS protocol.
|
Loading…
Reference in New Issue