Add QNAP Manila Driver

This driver supports below features:
 - Create NFS Share
 - Delete NFS Share
 - Allow NFS Share access (IP access type)
 - Deny NFS Share access
 - Create snapshot
 - Delete snapshot
 - Create share from snapshot
 - Extend share
 - Manage share
 - Unmanage share
 - Manage snapshot
 - Unmanage snapshot

DocImpact
Implements: blueprint qnap-manila-driver

Change-Id: I4e4278a870af7be1c026385b85ea309b2d1773a9
This commit is contained in:
Pony Chou 2016-10-01 02:28:51 +08:00
parent dc43f741f8
commit cf182947b7
11 changed files with 3799 additions and 2 deletions

View File

@ -81,7 +81,8 @@ Mapping of share drivers and share features support
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+ +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| MapRFS | O | O | O | O | O | O | O | \- | | MapRFS | O | O | O | O | O | O | O | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+ +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| QNAP | O | O | O | \- | O | O | O | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
Mapping of share drivers and share access rules support Mapping of share drivers and share access rules support
------------------------------------------------------- -------------------------------------------------------
@ -139,6 +140,8 @@ Mapping of share drivers and share access rules support
+----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+ +----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+
| MapRFS | \- | MapRFS(O) | \- | \- | \- | MapRFS(O) | \- | \- | | MapRFS | \- | MapRFS(O) | \- | \- | \- | MapRFS(O) | \- | \- |
+----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+ +----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+
| QNAP | NFS (O) | \- | \- | \- | NFS (O) | \- | \- | \- |
+----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+
Mapping of share drivers and security services support Mapping of share drivers and security services support
------------------------------------------------------ ------------------------------------------------------
@ -194,7 +197,8 @@ Mapping of share drivers and security services support
+----------------------------------------+------------------+-----------------+------------------+ +----------------------------------------+------------------+-----------------+------------------+
| MapRFS | \- | \- | \- | | MapRFS | \- | \- | \- |
+----------------------------------------+------------------+-----------------+------------------+ +----------------------------------------+------------------+-----------------+------------------+
| QNAP | \- | \- | \- |
+----------------------------------------+------------------+-----------------+------------------+
Mapping of share drivers and common capabilities Mapping of share drivers and common capabilities
------------------------------------------------ ------------------------------------------------
@ -252,6 +256,8 @@ More information: :ref:`capabilities_and_extra_specs`
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+ +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| MapRFS | \- | N | \- | \- | \- | N | \- | O | \- | | MapRFS | \- | N | \- | \- | \- | N | \- | O | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+ +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| QNAP | \- | O | \- | \- | O | \- | \- | O | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
.. note:: .. note::

View File

@ -70,6 +70,7 @@ 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
import manila.share.drivers.nexenta.options import manila.share.drivers.nexenta.options
import manila.share.drivers.qnap.qnap
import manila.share.drivers.quobyte.quobyte import manila.share.drivers.quobyte.quobyte
import manila.share.drivers.service_instance import manila.share.drivers.service_instance
import manila.share.drivers.tegile.tegile import manila.share.drivers.tegile.tegile
@ -147,6 +148,7 @@ _global_opt_lists = [
manila.share.drivers.nexenta.options.nexenta_connection_opts, manila.share.drivers.nexenta.options.nexenta_connection_opts,
manila.share.drivers.nexenta.options.nexenta_dataset_opts, manila.share.drivers.nexenta.options.nexenta_dataset_opts,
manila.share.drivers.nexenta.options.nexenta_nfs_opts, manila.share.drivers.nexenta.options.nexenta_nfs_opts,
manila.share.drivers.qnap.qnap.qnap_manila_opts,
manila.share.drivers.quobyte.quobyte.quobyte_manila_share_opts, manila.share.drivers.quobyte.quobyte.quobyte_manila_share_opts,
manila.share.drivers.service_instance.common_opts, manila.share.drivers.service_instance.common_opts,
manila.share.drivers.service_instance.no_share_servers_handling_mode_opts, manila.share.drivers.service_instance.no_share_servers_handling_mode_opts,

View File

View File

@ -0,0 +1,646 @@
# Copyright (c) 2016 QNAP Systems, 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.
"""
API for QNAP Storage.
"""
import base64
import functools
import re
import ssl
try:
import xml.etree.cElementTree as ET
except ImportError:
import xml.etree.ElementTree as ET
from oslo_log import log as logging
import six
from six.moves import http_client
from six.moves import urllib
from manila import exception
from manila.i18n import _
from manila import utils
LOG = logging.getLogger(__name__)
MSG_SESSION_EXPIRED = _("Session ID expired")
MSG_UNEXPECT_RESP = _("Unexpected response from QNAP API")
@utils.retry(exception=exception.ShareBackendException,
retries=5)
def _connection_checker(func):
"""Decorator to check session has expired or not."""
@functools.wraps(func)
def inner_connection_checker(self, *args, **kwargs):
LOG.debug('in _connection_checker')
pattern = re.compile(r".*Session ID expired.$")
try:
return func(self, *args, **kwargs)
except exception.ShareBackendException as e:
matches = pattern.match(six.text_type(e))
if matches:
LOG.debug('Session might have expired.'
' Trying to relogin')
self._login()
raise
return inner_connection_checker
class QnapAPIExecutor(object):
"""Makes QNAP API calls for ES NAS."""
def __init__(self, *args, **kwargs):
self.sid = None
self.username = kwargs['username']
self.password = kwargs['password']
self.ip, self.port, self.ssl = (
self._parse_management_url(kwargs['management_url']))
self._login()
def _parse_management_url(self, management_url):
pattern = re.compile(r"(http|https)\:\/\/(\S+)\:(\d+)")
matches = pattern.match(management_url)
if matches.group(1) == 'http':
management_ssl = False
else:
management_ssl = True
management_ip = matches.group(2)
management_port = matches.group(3)
return management_ip, management_port, management_ssl
def _prepare_connection(self, isSSL, ip, port):
if isSSL:
if hasattr(ssl, '_create_unverified_context'):
context = ssl._create_unverified_context()
connection = http_client.HTTPSConnection(ip,
port=port,
context=context)
else:
connection = http_client.HTTPSConnection(ip,
port=port)
else:
connection = http_client.HTTPConnection(ip, port)
return connection
def get_basic_info(self, management_url):
"""Get the basic information of NAS."""
LOG.debug('in get_basic_info')
management_ip, management_port, management_ssl = (
self._parse_management_url(management_url))
connection = self._prepare_connection(management_ssl,
management_ip,
management_port)
connection.request('GET', '/cgi-bin/authLogin.cgi')
response = connection.getresponse()
data = response.read()
LOG.debug('response data: %s', data)
root = ET.fromstring(data)
display_model_name = root.find('model/displayModelName').text
internal_model_name = root.find('model/internalModelName').text
fw_version = root.find('firmware/version').text
connection.close()
return display_model_name, internal_model_name, fw_version
def _execute_and_get_response_details(self, nas_ip, url):
"""Will prepare response after executing a http request."""
LOG.debug('port: %(port)s, ssl: %(ssl)s',
{'port': self.port, 'ssl': self.ssl})
res_details = {}
# Prepare the connection
connection = self._prepare_connection(self.ssl,
nas_ip,
self.port)
# Make the connection
LOG.debug('url : %s', url)
connection.request('GET', url)
# Extract the response as the connection was successful
response = connection.getresponse()
# Read the response
data = response.read()
LOG.debug('response data: %s', data)
res_details['data'] = data
res_details['error'] = None
res_details['http_status'] = response.status
connection.close()
return res_details
def execute_login(self):
"""Login and return sid."""
params = {
'user': self.username,
'pwd': base64.b64encode(self.password.encode("utf-8")),
'serviceKey': '1',
}
sanitized_params = self._sanitize_params(params)
sanitized_params = urllib.parse.urlencode(sanitized_params)
url = ('/cgi-bin/authLogin.cgi?%s' % sanitized_params)
res_details = self._execute_and_get_response_details(self.ip, url)
root = ET.fromstring(res_details['data'])
if root.find('authPassed').text == '0':
raise exception.ShareBackendException(msg=MSG_SESSION_EXPIRED)
session_id = root.find('authSid').text
return session_id
def _login(self):
"""Execute Https Login API."""
self.sid = self.execute_login()
LOG.debug('sid: %s', self.sid)
def _sanitize_params(self, params):
sanitized_params = {}
for key in params:
value = params[key]
if value is not None:
sanitized_params[key] = six.text_type(value)
return sanitized_params
@_connection_checker
def create_share(self, share, pool_name, create_share_name, share_proto):
"""Create share."""
LOG.debug('create_share_name: %s', create_share_name)
params = {
'wiz_func': 'share_create',
'action': 'add_share',
'vol_name': create_share_name,
'vol_size': six.text_type(share['size']) + 'GB',
'threshold': '80',
'dedup': 'off',
'compression': '1',
'thin_pro': '0',
'cache': '0',
'cifs_enable': '0' if share_proto == 'NFS' else '1',
'nfs_enable': '0' if share_proto == 'CIFS' else '1',
'afp_enable': '0',
'ftp_enable': '0',
'encryption': '0',
'hidden': '0',
'oplocks': '1',
'sync': 'always',
'userrw0': 'admin',
'userrd_len': '0',
'userrw_len': '1',
'userno_len': '0',
'access_r': 'setup_users',
'path_type': 'auto',
'recycle_bin': '1',
'recycle_bin_administrators_only': '0',
'pool_name': pool_name,
'sid': self.sid,
}
sanitized_params = self._sanitize_params(params)
sanitized_params = urllib.parse.urlencode(sanitized_params)
url = ('/cgi-bin/wizReq.cgi?%s' % sanitized_params)
res_details = self._execute_and_get_response_details(self.ip, url)
root = ET.fromstring(res_details['data'])
if root.find('authPassed').text == '0':
raise exception.ShareBackendException(msg=MSG_SESSION_EXPIRED)
if root.find('ES_RET_CODE').text < '0':
msg = _('Create share %s failed') % share['display_name']
raise exception.ShareBackendException(msg=msg)
vol_list = root.find('func').find('ownContent').find('volumeList')
vol_info_tree = vol_list.findall('volume')
for vol in vol_info_tree:
LOG.debug('Iterating vol name: %(name)s, index: %(id)s',
{'name': vol.find('volumeLabel').text,
'id': vol.find('volumeValue').text})
if (create_share_name == vol.find('volumeLabel').text):
LOG.debug('volumeLabel:%s', vol.find('volumeLabel').text)
return vol.find('volumeValue').text
return res_details['data']
@_connection_checker
def delete_share(self, vol_id, *args, **kwargs):
"""Execute delete share API."""
params = {
'func': 'volume_mgmt',
'vol_remove': '1',
'volumeID': vol_id,
'stop_service': 'no',
'sid': self.sid,
}
sanitized_params = self._sanitize_params(params)
sanitized_params = urllib.parse.urlencode(sanitized_params)
url = ('/cgi-bin/disk/disk_manage.cgi?%s' % sanitized_params)
res_details = self._execute_and_get_response_details(self.ip, url)
root = ET.fromstring(res_details['data'])
if root.find('authPassed').text == '0':
raise exception.ShareBackendException(msg=MSG_SESSION_EXPIRED)
if root.find('result').text < '0':
msg = _('Delete share id: %s failed') % vol_id
raise exception.ShareBackendException(msg=msg)
@_connection_checker
def get_specific_poolinfo(self, pool_id):
"""Execute get_specific_poolinfo API."""
params = {
'store': 'poolInfo',
'func': 'extra_get',
'poolID': pool_id,
'Pool_Info': '1',
'sid': self.sid,
}
sanitized_params = self._sanitize_params(params)
sanitized_params = urllib.parse.urlencode(sanitized_params)
url = ('/cgi-bin/disk/disk_manage.cgi?%s' % sanitized_params)
res_details = self._execute_and_get_response_details(self.ip, url)
root = ET.fromstring(res_details['data'])
if root.find('authPassed').text == '0':
raise exception.ShareBackendException(msg=MSG_SESSION_EXPIRED)
if root.find('result').text < '0':
msg = _('get_specific_poolinfo failed')
raise exception.ShareBackendException(msg=msg)
pool_list = root.find('Pool_Index')
pool_info_tree = pool_list.findall('row')
for pool in pool_info_tree:
if pool_id == pool.find('poolID').text:
LOG.debug('poolID: %s', pool.find('poolID').text)
return pool
@_connection_checker
def get_share_info(self, pool_id, **kwargs):
"""Execute get_share_info API."""
for key, value in six.iteritems(kwargs):
LOG.debug('%(key)s = %(val)s',
{'key': key, 'val': value})
params = {
'store': 'poolVolumeList',
'poolID': pool_id,
'func': 'extra_get',
'Pool_Vol_Info': '1',
'sid': self.sid,
}
sanitized_params = self._sanitize_params(params)
sanitized_params = urllib.parse.urlencode(sanitized_params)
url = ('/cgi-bin/disk/disk_manage.cgi?%s' % sanitized_params)
res_details = self._execute_and_get_response_details(self.ip, url)
root = ET.fromstring(res_details['data'])
if root.find('authPassed').text == '0':
raise exception.ShareBackendException(msg=MSG_SESSION_EXPIRED)
if ('vol_no' in kwargs) or ('vol_label' in kwargs):
vol_list = root.find('Volume_Info')
vol_info_tree = vol_list.findall('row')
for vol in vol_info_tree:
LOG.debug('Iterating vol name: %(name)s, index: %(id)s',
{'name': vol.find('vol_label').text,
'id': vol.find('vol_no').text})
if 'vol_no' in kwargs:
if kwargs['vol_no'] == vol.find('vol_no').text:
LOG.debug('vol_no:%s',
vol.find('vol_no').text)
return vol
elif 'vol_label' in kwargs:
if kwargs['vol_label'] == vol.find('vol_label').text:
LOG.debug('vol_label:%s', vol.find('vol_label').text)
return vol
if vol is vol_info_tree[-1]:
return None
else:
return res_details['data']
@_connection_checker
def get_specific_volinfo(self, vol_id, **kwargs):
"""Execute get_specific_volinfo API."""
params = {
'store': 'volumeInfo',
'volumeID': vol_id,
'func': 'extra_get',
'Volume_Info': '1',
'sid': self.sid,
}
sanitized_params = self._sanitize_params(params)
sanitized_params = urllib.parse.urlencode(sanitized_params)
url = ('/cgi-bin/disk/disk_manage.cgi?%s' % sanitized_params)
res_details = self._execute_and_get_response_details(self.ip, url)
root = ET.fromstring(res_details['data'])
if root.find('authPassed').text == '0':
raise exception.ShareBackendException(msg=MSG_SESSION_EXPIRED)
vol_list = root.find('Volume_Info')
vol_info_tree = vol_list.findall('row')
for vol in vol_info_tree:
if vol_id == vol.find('vol_no').text:
LOG.debug('vol_no: %s', vol.find('vol_no').text)
return vol
@_connection_checker
def get_snapshot_info(self, **kwargs):
"""Execute get_snapshot_info API."""
params = {
'func': 'extra_get',
'volumeID': kwargs['volID'],
'snapshot_list': '1',
'snap_start': '0',
'snap_count': '100',
'sid': self.sid,
}
sanitized_params = self._sanitize_params(params)
sanitized_params = urllib.parse.urlencode(sanitized_params)
url = ('/cgi-bin/disk/snapshot.cgi?%s' % sanitized_params)
res_details = self._execute_and_get_response_details(self.ip, url)
root = ET.fromstring(res_details['data'])
if root.find('authPassed').text == '0':
raise exception.ShareBackendException(msg=MSG_SESSION_EXPIRED)
if root.find('result').text < '0':
raise exception.ShareBackendException(msg=MSG_UNEXPECT_RESP)
snapshot_list = root.find('SnapshotList')
# if snapshot_list is None:
if not snapshot_list:
return None
if ('snapshot_name' in kwargs):
snapshot_tree = snapshot_list.findall('row')
for snapshot in snapshot_tree:
if (kwargs['snapshot_name'] ==
snapshot.find('snapshot_name').text):
LOG.debug('snapshot_name:%s', kwargs['snapshot_name'])
return snapshot
if (snapshot is snapshot_tree[-1]):
return None
return res_details['data']
@_connection_checker
def create_snapshot_api(self, volumeID, snapshot_name):
"""Execute CGI to create snapshot from source share."""
LOG.debug('volumeID: %s', volumeID)
LOG.debug('snapshot_name: %s', snapshot_name)
params = {
'func': 'create_snapshot',
'volumeID': volumeID,
'snapshot_name': snapshot_name,
'expire_min': '0',
'vital': '1',
'sid': self.sid,
}
sanitized_params = self._sanitize_params(params)
sanitized_params = urllib.parse.urlencode(sanitized_params)
url = ('/cgi-bin/disk/snapshot.cgi?%s' % sanitized_params)
res_details = self._execute_and_get_response_details(self.ip, url)
root = ET.fromstring(res_details['data'])
if root.find('authPassed').text == '0':
raise exception.ShareBackendException(msg=MSG_SESSION_EXPIRED)
if root.find('ES_RET_CODE').text < '0':
msg = _('Create snapshot failed')
raise exception.ShareBackendException(msg=msg)
@_connection_checker
def delete_snapshot_api(self, snapshot_id):
"""Execute CGI to delete snapshot from snapshot_id."""
params = {
'func': 'del_snapshots',
'snapshotID': snapshot_id,
'sid': self.sid,
}
sanitized_params = self._sanitize_params(params)
sanitized_params = urllib.parse.urlencode(sanitized_params)
url = ('/cgi-bin/disk/snapshot.cgi?%s' % sanitized_params)
res_details = self._execute_and_get_response_details(self.ip, url)
root = ET.fromstring(res_details['data'])
if root.find('authPassed').text == '0':
raise exception.ShareBackendException(msg=MSG_SESSION_EXPIRED)
# snapshot not exist
if root.find('result').text == '-206021':
return
# lun not exist
if root.find('result').text == '-200005':
return
if root.find('result').text < '0':
msg = _('Failed to delete snapshot.')
raise exception.ShareBackendException(msg=msg)
@_connection_checker
def clone_snapshot(self, snapshot_id, new_sharename):
"""Execute CGI to clone snapshot as share."""
params = {
'func': 'clone_qsnapshot',
'by_vol': '1',
'snapshotID': snapshot_id,
'new_name': new_sharename,
'sid': self.sid,
}
sanitized_params = self._sanitize_params(params)
sanitized_params = urllib.parse.urlencode(sanitized_params)
url = ('/cgi-bin/disk/snapshot.cgi?%s' % sanitized_params)
res_details = self._execute_and_get_response_details(self.ip, url)
root = ET.fromstring(res_details['data'])
if root.find('authPassed').text == '0':
raise exception.ShareBackendException(msg=MSG_SESSION_EXPIRED)
if root.find('result').text < '0':
msg = _('Failed to clone snapshot.')
raise exception.ShareBackendException(msg=msg)
@_connection_checker
def edit_share(self, share_dict):
"""Edit share properties."""
LOG.debug('share_dict[sharename]: %s', share_dict['sharename'])
params = {
'wiz_func': 'share_property',
'action': 'share_property',
'sharename': share_dict['sharename'],
'old_sharename': share_dict['old_sharename'],
'vol_size': six.text_type(share_dict['new_size']) + 'GB',
'dedup': '0',
'compression': '1',
'thin_pro': '0',
'cache': '0',
'afp_enable': '0',
'ftp_enable': '1',
'hidden': '0',
'oplocks': '1',
'sync': 'always',
'recycle_bin': '1',
'recycle_bin_administrators_only': '0',
'sid': self.sid,
}
sanitized_params = self._sanitize_params(params)
sanitized_params = urllib.parse.urlencode(sanitized_params)
url = ('/cgi-bin/priv/privWizard.cgi?%s' % sanitized_params)
res_details = self._execute_and_get_response_details(self.ip, url)
root = ET.fromstring(res_details['data'])
if root.find('authPassed').text == '0':
raise exception.ShareBackendException(msg=MSG_SESSION_EXPIRED)
if root.find('ES_RET_CODE').text < '0':
msg = _('Edit sharename %s failed') % share_dict['sharename']
raise exception.ShareBackendException(msg=msg)
@_connection_checker
def get_host_list(self, **kwargs):
"""Execute get_host_list API."""
params = {
'module': 'hosts',
'func': 'get_hostlist',
'sid': self.sid,
}
sanitized_params = self._sanitize_params(params)
sanitized_params = urllib.parse.urlencode(sanitized_params)
url = ('/cgi-bin/accessrights/accessrightsRequest.cgi?%s' %
sanitized_params)
res_details = self._execute_and_get_response_details(self.ip, url)
root = ET.fromstring(res_details['data'])
if root.find('authPassed').text == '0':
raise exception.ShareBackendException(msg=MSG_SESSION_EXPIRED)
if root.find('result').text < '0':
raise exception.ShareBackendException(msg=MSG_UNEXPECT_RESP)
host_list = root.find('content').find('host_list')
# if host_list is None:
if not host_list:
return None
return_hosts = []
host_tree = host_list.findall('host')
for host in host_tree:
LOG.debug('host:%s', host)
return_hosts.append(host)
return return_hosts
@_connection_checker
def add_host(self, hostname, ipv4):
"""Execute add_host API."""
params = {
'module': 'hosts',
'func': 'apply_addhost',
'name': hostname,
'ipaddr_v4': ipv4,
'sid': self.sid,
}
sanitized_params = self._sanitize_params(params)
sanitized_params = urllib.parse.urlencode(sanitized_params)
url = ('/cgi-bin/accessrights/accessrightsRequest.cgi?%s' %
sanitized_params)
res_details = self._execute_and_get_response_details(self.ip, url)
root = ET.fromstring(res_details['data'])
if root.find('authPassed').text == '0':
raise exception.ShareBackendException(msg=MSG_SESSION_EXPIRED)
if root.find('result').text < '0':
raise exception.ShareBackendException(msg=MSG_UNEXPECT_RESP)
@_connection_checker
def set_nfs_access(self, sharename, access, host_name):
"""Execute set_nfs_access API."""
params = {
'wiz_func': 'share_nfs_control',
'action': 'share_nfs_control',
'sharename': sharename,
'access': access,
'host_name': host_name,
'sid': self.sid,
}
sanitized_params = self._sanitize_params(params)
sanitized_params = urllib.parse.urlencode(sanitized_params)
url = ('/cgi-bin/priv/privWizard.cgi?%s' % sanitized_params)
res_details = self._execute_and_get_response_details(self.ip, url)
root = ET.fromstring(res_details['data'])
if root.find('authPassed').text == '0':
raise exception.ShareBackendException(msg=MSG_SESSION_EXPIRED)
if root.find('result').text < '0':
raise exception.ShareBackendException(msg=MSG_UNEXPECT_RESP)
class QnapAPIExecutorTS(QnapAPIExecutor):
"""Makes QNAP API calls for TS NAS."""
@_connection_checker
def get_snapshot_info(self, **kwargs):
"""Execute get_snapshot_info API."""
for key, value in six.iteritems(kwargs):
LOG.debug('%(key)s = %(val)s',
{'key': key, 'val': value})
params = {
'func': 'extra_get',
'LUNIndex': kwargs['lun_index'],
'smb_snapshot_list': '1',
'smb_snapshot': '1',
'snapshot_list': '1',
'sid': self.sid,
}
sanitized_params = self._sanitize_params(params)
sanitized_params = urllib.parse.urlencode(sanitized_params)
url = ('/cgi-bin/disk/snapshot.cgi?%s' % sanitized_params)
res_details = self._execute_and_get_response_details(self.ip, url)
root = ET.fromstring(res_details['data'])
if root.find('authPassed').text == '0':
raise exception.ShareBackendException(msg=MSG_SESSION_EXPIRED)
if root.find('result').text < '0':
raise exception.ShareBackendException(msg=MSG_UNEXPECT_RESP)
snapshot_list = root.find('SnapshotList')
if snapshot_list is None:
return None
snapshot_tree = snapshot_list.findall('row')
for snapshot in snapshot_tree:
if (kwargs['snapshot_name'] ==
snapshot.find('snapshot_name').text):
LOG.debug('snapshot_name:%s', kwargs['snapshot_name'])
return snapshot
return None

View File

@ -0,0 +1,707 @@
# Copyright (c) 2016 QNAP Systems, 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.
"""
Share driver for QNAP Storage.
This driver supports QNAP Storage for NFS.
"""
import re
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import timeutils
from oslo_utils import units
from manila.common import constants
from manila import exception
from manila import share
from manila.i18n import _, _LE, _LI, _LW
from manila.share import driver
from manila.share.drivers.qnap import api
from manila import utils
LOG = logging.getLogger(__name__)
qnap_manila_opts = [
cfg.StrOpt('qnap_management_url',
required=True,
help='The URL to manage QNAP Storage.'),
cfg.StrOpt('qnap_share_ip',
required=True,
help='NAS share IP for mounting shares.'),
cfg.StrOpt('qnap_nas_login',
required=True,
help='Username for QNAP storage.'),
cfg.StrOpt('qnap_nas_password',
required=True,
secret=True,
help='Password for QNAP storage.'),
cfg.StrOpt('qnap_poolname',
required=True,
help='Pool within which QNAP shares must be created.'),
]
CONF = cfg.CONF
CONF.register_opts(qnap_manila_opts)
class QnapShareDriver(driver.ShareDriver):
"""OpenStack driver to enable QNAP Storage.
Version history:
1.0.0 - Initial driver (Only NFS)
"""
DRIVER_VERSION = '1.0.0'
def __init__(self, *args, **kwargs):
"""Initialize QnapShareDriver."""
super(QnapShareDriver, self).__init__(False, *args, **kwargs)
self.private_storage = kwargs.get('private_storage')
self.api_executor = None
self.group_stats = {}
self.configuration.append_config_values(qnap_manila_opts)
self.share_api = share.API()
def do_setup(self, context):
"""Setup the QNAP Manila share driver."""
self.ctxt = context
LOG.debug('context: %s', context)
# Setup API Executor
try:
self.api_executor = self._create_api_executor()
except Exception:
LOG.exception(_LE('Failed to create HTTP client. Check IP '
'address, port, username, password and make '
'sure the array version is compatible.'))
raise
def check_for_setup_error(self):
"""Check the status of setup."""
if self.api_executor is None:
msg = _("Failed to instantiate API client to communicate with "
"QNAP storage systems.")
raise exception.ShareBackendException(msg=msg)
def _create_api_executor(self):
"""Create API executor by NAS model."""
"""LOG.debug('CONF.qnap_nas_login=%(conf)s',
{'conf': CONF.qnap_nas_login})
LOG.debug('self.configuration.qnap_nas_login=%(conf)s',
{'conf': self.configuration.qnap_nas_login})"""
self.api_executor = api.QnapAPIExecutor(
username=self.configuration.qnap_nas_login,
password=self.configuration.qnap_nas_password,
management_url=self.configuration.qnap_management_url)
display_model_name, internal_model_name, fw_version = (
self.api_executor.get_basic_info(
self.configuration.qnap_management_url))
pattern = re.compile(r"^([A-Z]+)-?[A-Z]{0,2}(\d+)\d{2}(U|[a-z]*)")
matches = pattern.match(display_model_name)
if not matches:
return None
model_type = matches.group(1)
ts_model_types = (
"TS", "SS", "IS", "TVS", "TDS", "TBS"
)
tes_model_types = (
"TES",
)
es_model_types = (
"ES",
)
if model_type in ts_model_types:
if (fw_version.startswith("4.2") or fw_version.startswith("4.3")):
LOG.debug('Create TS API Executor')
# modify the pool name to pool index
self.configuration.qnap_poolname = (
self._get_ts_model_pool_id(
self.configuration.qnap_poolname))
return api.QnapAPIExecutorTS(
username=self.configuration.qnap_nas_login,
password=self.configuration.qnap_nas_password,
management_url=self.configuration.qnap_management_url)
elif model_type in tes_model_types:
if 'TS' in internal_model_name:
if (fw_version.startswith("4.2") or
fw_version.startswith("4.3")):
LOG.debug('Create TS API Executor')
# modify the pool name to pool index
self.configuration.qnap_poolname = (
self._get_ts_model_pool_id(
self.configuration.qnap_poolname))
return api.QnapAPIExecutorTS(
username=self.configuration.qnap_nas_login,
password=self.configuration.qnap_nas_password,
management_url=self.configuration.qnap_management_url)
if (fw_version.startswith("1.1.2") or
fw_version.startswith("1.1.3")):
LOG.debug('Create ES API Executor')
return api.QnapAPIExecutor(
username=self.configuration.qnap_nas_login,
password=self.configuration.qnap_nas_password,
management_url=self.configuration.qnap_management_url)
elif model_type in es_model_types:
if (fw_version.startswith("1.1.2") or
fw_version.startswith("1.1.3")):
LOG.debug('Create ES API Executor')
return api.QnapAPIExecutor(
username=self.configuration.qnap_nas_login,
password=self.configuration.qnap_nas_password,
management_url=self.configuration.qnap_management_url)
msg = _('QNAP Storage model is not supported by this driver.')
raise exception.ShareBackendException(msg=msg)
def _get_ts_model_pool_id(self, pool_name):
"""Modify the pool name to pool index."""
pattern = re.compile(r"^(\d+)+|^Storage Pool (\d+)+")
matches = pattern.match(pool_name)
if matches.group(1):
return matches.group(1)
else:
return matches.group(2)
@utils.synchronized('qnap-gen_name')
def _gen_random_name(self, type):
if type == 'share':
infix = "shr-"
elif type == 'snapshot':
infix = "snp-"
elif type == 'host':
infix = "hst-"
else:
infix = ""
return ("manila-%(ifx)s%(time)s" %
{'ifx': infix,
'time': timeutils.utcnow().strftime('%Y%m%d%H%M%S%f')})
def _get_location_path(self, share_name, share_proto, ip):
if share_proto == 'NFS':
created_share = self.api_executor.get_share_info(
self.configuration.qnap_poolname,
vol_label=share_name)
vol_no = created_share.find('vol_no').text
vol = self.api_executor.get_specific_volinfo(vol_no)
vol_mount_path = vol.find('vol_mount_path').text
location = '%s:%s' % (ip, vol_mount_path)
else:
msg = _('Invalid NAS protocol: %s') % share_proto
raise exception.InvalidInput(reason=msg)
export_location = {
'path': location,
'is_admin_only': False,
}
return export_location
def _update_share_stats(self):
"""Get latest share stats."""
backend_name = (self.configuration.safe_get(
'share_backend_name') or
self.__class__.__name__)
LOG.debug('backend_name=%(backend_name)s',
{'backend_name': backend_name})
selected_pool = self.api_executor.get_specific_poolinfo(
self.configuration.qnap_poolname)
total_capacity_gb = (int(selected_pool.find('capacity_bytes').text) /
units.Gi)
LOG.debug('total_capacity_gb: %s GB', total_capacity_gb)
free_capacity_gb = (int(selected_pool.find('freesize_bytes').text) /
units.Gi)
LOG.debug('free_capacity_gb: %s GB', free_capacity_gb)
alloc_capacity_gb = (int(selected_pool.find('allocated_bytes').text) /
units.Gi)
LOG.debug('allocated_capacity_gb: %s GB', alloc_capacity_gb)
reserved_percentage = self.configuration.safe_get(
'reserved_share_percentage')
# single pool now, need support multiple pools in the future
single_pool = {
"pool_name": self.configuration.qnap_poolname,
"total_capacity_gb": total_capacity_gb,
"free_capacity_gb": free_capacity_gb,
"allocated_capacity_gb": alloc_capacity_gb,
"reserved_percentage": reserved_percentage,
"qos": False,
}
data = {
"share_backend_name": backend_name,
"vendor_name": "QNAP",
"driver_version": self.DRIVER_VERSION,
"storage_protocol": "NFS",
"snapshot_support": True,
"create_share_from_snapshot_support": True,
"driver_handles_share_servers": self.configuration.safe_get(
'driver_handles_share_servers'),
'pools': [single_pool],
}
super(self.__class__, self)._update_share_stats(data)
@utils.retry(exception=exception.ShareBackendException,
interval=3,
retries=200)
def create_share(self, context, share, share_server=None):
"""Create a new share."""
LOG.debug('share: %s', share.__dict__)
share_proto = share['share_proto']
# User could create two shares with the same name on horizon.
# Therefore, we should not use displayname to create shares on NAS.
create_share_name = self._gen_random_name("share")
# If share name exists, need to change to another name.
created_share = self.api_executor.get_share_info(
self.configuration.qnap_poolname,
vol_label=create_share_name)
if created_share is not None:
msg = _("Failed to create an unused share name.")
raise exception.ShareBackendException(msg=msg)
create_volID = self.api_executor.create_share(
share,
self.configuration.qnap_poolname,
create_share_name,
share_proto)
# Use private_storage to record volume ID and Name created in the NAS.
_metadata = {'volID': create_volID, 'volName': create_share_name}
self.private_storage.update(share['id'], _metadata)
return self._get_location_path(create_share_name,
share['share_proto'],
self.configuration.qnap_share_ip)
def delete_share(self, context, share, share_server=None):
"""Delete the specified share."""
# Use private_storage to retreive volume ID created in the NAS.
volID = self.private_storage.get(share['id'], 'volID')
if not volID:
LOG.warning(_LW('volID for Share %s does not exist'), share['id'])
return
LOG.debug('volID: %s', volID)
del_share = self.api_executor.get_share_info(
self.configuration.qnap_poolname,
vol_no=volID)
if del_share is None:
LOG.warning(_LW('Share %s does not exist'), share['id'])
return
vol_no = del_share.find('vol_no').text
self.api_executor.delete_share(vol_no)
self.private_storage.delete(share['id'])
def extend_share(self, share, new_size, share_server=None):
"""Extend an existing share."""
LOG.debug('Entering extend_share share=%(share)s '
'new_size=%(size)s',
{'share': share['display_name'], 'size': new_size})
# Use private_storage to retrieve volume Name created in the NAS.
volName = self.private_storage.get(share['id'], 'volName')
if not volName:
LOG.debug('Share %s does not exist', share['id'])
raise exception.ShareResourceNotFound(share_id=share['id'])
LOG.debug('volName: %s', volName)
share_dict = {
"sharename": volName,
"old_sharename": volName,
"new_size": new_size,
}
self.api_executor.edit_share(share_dict)
@utils.retry(exception=exception.ShareBackendException,
interval=3,
retries=200)
def create_snapshot(self, context, snapshot, share_server=None):
"""Create a snapshot."""
LOG.debug('snapshot[share][share_id]: %s',
snapshot['share']['share_id'])
LOG.debug('snapshot id: %s', snapshot['id'])
# Use private_storage to retrieve volume ID created in the NAS.
volID = self.private_storage.get(snapshot['share']['id'], 'volID')
if not volID:
LOG.warning(
_LW('volID for Share %s does not exist'),
snapshot['share']['id'])
raise exception.ShareResourceNotFound(
share_id=snapshot['share']['id'])
LOG.debug('volID: %s', volID)
# User could create two snapshot with the same name on horizon.
# Therefore, we should not use displayname to create snapshot on NAS.
# if snapshot exist, need to change another
create_snapshot_name = self._gen_random_name("snapshot")
LOG.debug('create_snapshot_name: %s', create_snapshot_name)
check_snapshot = self.api_executor.get_snapshot_info(
volID=volID, snapshot_name=create_snapshot_name)
if check_snapshot is not None:
msg = _("Failed to create an unused snapshot name.")
raise exception.ShareBackendException(msg=msg)
LOG.debug('create_snapshot_name: %s', create_snapshot_name)
self.api_executor.create_snapshot_api(volID, create_snapshot_name)
snapshot_id = ""
created_snapshot = self.api_executor.get_snapshot_info(
volID=volID, snapshot_name=create_snapshot_name)
if created_snapshot is not None:
snapshot_id = created_snapshot.find('snapshot_id').text
else:
msg = _("Failed to get snapshot information.")
raise exception.ShareBackendException(msg=msg)
LOG.debug('created_snapshot: %s', created_snapshot)
LOG.debug('snapshot_id: %s', snapshot_id)
# Use private_storage to record data instead of metadata.
_metadata = {'snapshot_id': snapshot_id}
self.private_storage.update(snapshot['id'], _metadata)
# Test to get value from private_storage.
snapshot_id = self.private_storage.get(snapshot['id'], 'snapshot_id')
LOG.debug('snapshot_id: %s', snapshot_id)
return {'provider_location': snapshot_id}
def delete_snapshot(self, context, snapshot, share_server=None):
"""Delete a snapshot."""
LOG.debug('Entering delete_snapshot. The deleted snapshot=%(snap)s',
{'snap': snapshot['id']})
snapshot_id = (snapshot.get('provider_location') or
self.private_storage.get(snapshot['id'], 'snapshot_id'))
if not snapshot_id:
LOG.warning(_LW('Snapshot %s does not exist'), snapshot['id'])
return
LOG.debug('snapshot_id: %s', snapshot_id)
self.api_executor.delete_snapshot_api(snapshot_id)
self.private_storage.delete(snapshot['id'])
@utils.retry(exception=exception.ShareBackendException,
interval=3,
retries=200)
def create_share_from_snapshot(self, context, share, snapshot,
share_server=None):
"""Create a share from a snapshot."""
LOG.debug('Entering create_share_from_snapshot. The source '
'snapshot=%(snap)s. The created share=%(share)s',
{'snap': snapshot['id'], 'share': share['id']})
snapshot_id = (snapshot.get('provider_location') or
self.private_storage.get(snapshot['id'], 'snapshot_id'))
if not snapshot_id:
LOG.warning(_LW('Snapshot %s does not exist'), snapshot['id'])
raise exception.SnapshotResourceNotFound(name=snapshot['id'])
LOG.debug('snapshot_id: %s', snapshot_id)
create_share_name = self._gen_random_name("share")
# if sharename exist, need to change another
created_share = self.api_executor.get_share_info(
self.configuration.qnap_poolname,
vol_label=create_share_name)
if created_share is not None:
msg = _("Failed to create an unused share name.")
raise exception.ShareBackendException(msg=msg)
self.api_executor.clone_snapshot(snapshot_id, create_share_name)
create_volID = ""
created_share = self.api_executor.get_share_info(
self.configuration.qnap_poolname,
vol_label=create_share_name)
if created_share.find('vol_no') is not None:
create_volID = created_share.find('vol_no').text
else:
msg = _("Failed to clone a snapshot in time.")
raise exception.ShareBackendException(msg=msg)
snap_share = self.share_api.get(context,
snapshot['share_instance']['share_id'])
LOG.debug('snap_share[size]: %s', snap_share['size'])
if (share['size'] > snap_share['size']):
share_dict = {'sharename': create_share_name,
'old_sharename': create_share_name,
'new_size': share['size']}
self.api_executor.edit_share(share_dict)
# Use private_storage to record volume ID and Name created in the NAS.
_metadata = {
'volID': create_volID,
'volName': create_share_name,
}
self.private_storage.update(share['id'], _metadata)
# Test to get value from private_storage.
volName = self.private_storage.get(share['id'], 'volName')
LOG.debug('volName: %s', volName)
return self._get_location_path(create_share_name,
share['share_proto'],
self.configuration.qnap_share_ip)
def _get_manila_hostIPv4s(self, hostlist):
host_dict_IPs = []
if hostlist is None:
return host_dict_IPs
for host in hostlist:
# Check host alias name with prefix "manila-hst-" to verify this
# host is created/managed by Manila or not.
if (re.match("^manila-hst-[0-9]+$", host.find('name').text)
is not None):
LOG.debug('host netaddrs text: %s', host.find('netaddrs').text)
if host.find('netaddrs').text is not None:
# Because Manila supports only IPv4 now, check "netaddrs"
# have "ipv4" tag to verify this host is created/managed
# by Manila or not.
if host.find('netaddrs/ipv4').text is not None:
host_dict = {
'index': host.find('index').text,
'hostid': host.find('hostid').text,
'name': host.find('name').text,
'netaddrs': host.find('netaddrs').find('ipv4').text
}
host_dict_IPs.append(host_dict)
return host_dict_IPs
def update_access(self, context, share, access_rules, add_rules,
delete_rules, share_server=None):
if not (add_rules or delete_rules):
volName = self.private_storage.get(share['id'], 'volName')
LOG.debug('volName: %s', volName)
if volName is None:
LOG.debug('Share %s does not exist', share['id'])
raise exception.ShareResourceNotFound(share_id=share['id'])
# Clear all current ACLs
self.api_executor.set_nfs_access(volName, 2, "all")
# Add each one through all rules.
for access in access_rules:
self._allow_access(context, share, access, share_server)
else:
# Adding/Deleting specific rules
for access in delete_rules:
self._deny_access(context, share, access, share_server)
for access in add_rules:
self._allow_access(context, share, access, share_server)
def _allow_access(self, context, share, access, share_server=None):
"""Allow access to the share."""
share_proto = share['share_proto']
access_type = access['access_type']
access_level = access['access_level']
access_to = access['access_to']
self._check_share_access(share_proto, access_type)
hostlist = self.api_executor.get_host_list()
host_dict_IPs = self._get_manila_hostIPv4s(hostlist)
LOG.debug('host_dict_IPs: %s', host_dict_IPs)
if len(host_dict_IPs) == 0:
host_name = self._gen_random_name("host")
self.api_executor.add_host(host_name, access_to)
else:
for host in host_dict_IPs:
LOG.debug('host[netaddrs]: %s', host['netaddrs'])
LOG.debug('access_to: %s', access_to)
if host['netaddrs'] == access_to:
LOG.debug('in match ip')
host_name = host['name']
break
if host is host_dict_IPs[-1]:
host_name = self._gen_random_name("host")
self.api_executor.add_host(host_name, access_to)
volName = self.private_storage.get(share['id'], 'volName')
LOG.debug('volName: %(volName)s for share: %(share)s',
{'volName': volName, 'share': share['id']})
LOG.debug('access_level: %(access)s for share: %(share)s',
{'access': access_level, 'share': share['id']})
LOG.debug('host_name: %(host)s for share: %(share)s',
{'host': host_name, 'share': share['id']})
if access_level == constants.ACCESS_LEVEL_RO:
self.api_executor.set_nfs_access(volName, 1, host_name)
elif access_level == constants.ACCESS_LEVEL_RW:
self.api_executor.set_nfs_access(volName, 0, host_name)
def _deny_access(self, context, share, access, share_server=None):
"""Deny access to the share."""
share_proto = share['share_proto']
access_type = access['access_type']
access_to = access['access_to']
try:
self._check_share_access(share_proto, access_type)
except exception.InvalidShareAccess:
LOG.warning(_LW('The denied rule is invalid and does not exist.'))
return
hostlist = self.api_executor.get_host_list()
host_dict_IPs = self._get_manila_hostIPv4s(hostlist)
LOG.debug('host_dict_IPs: %s', host_dict_IPs)
if len(host_dict_IPs) == 0:
return
else:
for host in host_dict_IPs:
if (host['netaddrs'] == access_to):
host_name = host['name']
break
if (host is host_dict_IPs[-1]):
return
volName = self.private_storage.get(share['id'], 'volName')
LOG.debug('volName: %s', volName)
self.api_executor.set_nfs_access(volName, 2, host_name)
def _check_share_access(self, share_proto, access_type):
if share_proto == 'NFS' and access_type != 'ip':
reason = _('Only "ip" access type is allowed for '
'NFS shares.')
LOG.warning(reason)
raise exception.InvalidShareAccess(reason=reason)
elif share_proto != 'NFS':
reason = _('Invalid NAS protocol: %s') % share_proto
raise exception.InvalidShareAccess(reason=reason)
def manage_existing(self, share, driver_options):
"""Manages a share that exists on backend."""
if share['share_proto'].lower() == 'nfs':
# 10.0.0.1:/share/example
LOG.info(_LI("Share %(shr_path)s will be managed with ID "
"%(shr_id)s."),
{'shr_path': share['export_locations'][0]['path'],
'shr_id': share['id']})
old_path_info = share['export_locations'][0]['path'].split(
':/share/')
if len(old_path_info) == 2:
ip = old_path_info[0]
share_name = old_path_info[1]
else:
msg = _("Incorrect path. It should have the following format: "
"IP:/share/share_name.")
raise exception.ShareBackendException(msg=msg)
else:
msg = _('Invalid NAS protocol: %s') % share['share_proto']
raise exception.InvalidInput(reason=msg)
if ip != self.configuration.qnap_share_ip:
msg = _("The NAS IP %(ip)s is not configured.") % {'ip': ip}
raise exception.ShareBackendException(msg=msg)
existing_share = self.api_executor.get_share_info(
self.configuration.qnap_poolname,
vol_label=share_name)
if existing_share is None:
msg = _("The share %s trying to be managed was not found on "
"backend.") % share['id']
raise exception.ManageInvalidShare(reason=msg)
_metadata = {}
vol_no = existing_share.find('vol_no').text
_metadata['volID'] = vol_no
_metadata['volName'] = share_name
self.private_storage.update(share['id'], _metadata)
# Test to get value from private_storage.
volID = self.private_storage.get(share['id'], 'volID')
LOG.debug('volID: %s', volID)
volName = self.private_storage.get(share['id'], 'volName')
LOG.debug('volName: %s', volName)
LOG.info(_LI("Share %(shr_path)s was successfully managed with ID "
"%(shr_id)s."),
{'shr_path': share['export_locations'][0]['path'],
'shr_id': share['id']})
vol = self.api_executor.get_specific_volinfo(vol_no)
vol_size_gb = int(vol.find('size').text) / units.Gi
export_locations = self._get_location_path(
share_name,
share['share_proto'],
self.configuration.qnap_share_ip)
return {'size': vol_size_gb, 'export_locations': export_locations}
def unmanage(self, share):
"""Remove the specified share from Manila management."""
self.private_storage.delete(share['id'])
def manage_existing_snapshot(self, snapshot, driver_options):
"""Manage existing share snapshot with manila."""
volID = self.private_storage.get(snapshot['share']['id'], 'volID')
LOG.debug('volID: %s', volID)
existing_share = self.api_executor.get_share_info(
self.configuration.qnap_poolname,
vol_no=volID)
if existing_share is None:
msg = _("The share id %s was not found on backend.") % volID
LOG.error(msg)
raise exception.ShareNotFound(reason=msg)
snapshot_id = snapshot.get('provider_location')
snapshot_id_info = snapshot_id.split('@')
if len(snapshot_id_info) == 2:
share_name = snapshot_id_info[0]
else:
msg = _("Incorrect provider_location format. It should have the "
"following format: share_name@snapshot_name.")
LOG.error(msg)
raise exception.InvalidParameterValue(reason=msg)
if share_name != existing_share.find('vol_label').text:
msg = (_("The assigned share %(share_name)s was not matched "
"%(vol_label)s on backend.") %
{'share_name': share_name,
'vol_label': existing_share.find('vol_label').text})
LOG.error(msg)
raise exception.ShareNotFound(reason=msg)
_metadata = {
'snapshot_id': snapshot_id,
}
self.private_storage.update(snapshot['id'], _metadata)
def unmanage_snapshot(self, snapshot):
"""Remove the specified snapshot from Manila management."""
self.private_storage.delete(snapshot['id'])

View File

@ -53,6 +53,12 @@ def set_defaults(conf):
_safe_set_of_opts(conf, 'hitachi_hsp_username', 'hsp_user') _safe_set_of_opts(conf, 'hitachi_hsp_username', 'hsp_user')
_safe_set_of_opts(conf, 'hitachi_hsp_password', 'hsp_password') _safe_set_of_opts(conf, 'hitachi_hsp_password', 'hsp_password')
_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_nas_login', 'admin')
_safe_set_of_opts(conf, 'qnap_nas_password', 'qnapadmin')
_safe_set_of_opts(conf, 'qnap_poolname', 'Storage Pool 1')
def _safe_set_of_opts(conf, *args, **kwargs): def _safe_set_of_opts(conf, *args, **kwargs):
try: try:

View File

@ -0,0 +1,527 @@
# Copyright (c) 2016 QNAP Systems, 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.
FAKE_RES_DETAIL_DATA_LOGIN = """
<QDocRoot version="1.0">
<authPassed><![CDATA[1]]></authPassed>
<authSid><![CDATA[fakeSid]]></authSid>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_GETBASIC_INFO_ES = """
<QDocRoot version="1.0">
<model>
<displayModelName><![CDATA[ES1640dc]]></displayModelName>
<internalModelName><![CDATA[ES1640dc]]></internalModelName>
</model>
<firmware>
<version><![CDATA[1.1.3]]></version>
</firmware>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_GETBASIC_INFO_TS = """
<QDocRoot version="1.0">
<model>
<displayModelName><![CDATA[TS-870U]]></displayModelName>
<internalModelName><![CDATA[TS-870U]]></internalModelName>
</model>
<firmware>
<version><![CDATA[4.3.0]]></version>
</firmware>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_GETBASIC_INFO_TES_TS = """
<QDocRoot version="1.0">
<model>
<displayModelName><![CDATA[TES-1885U]]></displayModelName>
<internalModelName><![CDATA[TS-1885U]]></internalModelName>
</model>
<firmware>
<version><![CDATA[4.3.0]]></version>
</firmware>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_GETBASIC_INFO_TES_ES = """
<QDocRoot version="1.0">
<model>
<displayModelName><![CDATA[TES-1885U]]></displayModelName>
<internalModelName><![CDATA[ES-1885U]]></internalModelName>
</model>
<firmware>
<version><![CDATA[1.1.3]]></version>
</firmware>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_GETBASIC_INFO_ERROR = """
<QDocRoot version="1.0">
<model>
<displayModelName><![CDATA[TTS-1885U]]></displayModelName>
<internalModelName><![CDATA[TTS-1885U]]></internalModelName>
</model>
<firmware>
<version><![CDATA[1.1.3]]></version>
</firmware>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_SHARE_INFO = """
<QDocRoot version="1.0">
<authPassed><![CDATA[1]]></authPassed>
<Volume_Info>
<row>
<vol_no><![CDATA[fakeNo]]></vol_no>
<vol_label><![CDATA[fakeShareName]]></vol_label>
</row>
</Volume_Info>
<result><![CDATA[0]]></result>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_VOLUME_INFO = """
<QDocRoot version="1.0">
<authPassed><![CDATA[1]]></authPassed>
<Volume_Info>
<row>
<vol_no><![CDATA[fakeNo]]></vol_no>
<size><![CDATA[10]]></size>
<vol_mount_path>fakeMountPath</vol_mount_path>
</row>
</Volume_Info>
<result><![CDATA[0]]></result>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_SNAPSHOT = """
<QDocRoot version="1.0">
<authPassed><![CDATA[1]]></authPassed>
<SnapshotList>
<row>
<snapshot_id><![CDATA[fakeSnapshotId]]></snapshot_id>
<snapshot_name><![CDATA[fakeSnapshotName]]></snapshot_name>
</row>
</SnapshotList>
<result><![CDATA[0]]></result>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_SPECIFIC_POOL_INFO = """
<QDocRoot version="1.0">
<authPassed><![CDATA[1]]></authPassed>
<Pool_Index>
<row>
<poolIndex><![CDATA[fakePoolIndex]]></poolIndex>
<poolID><![CDATA[fakePoolId]]></poolID>
<pool_status><![CDATA[0]]></pool_status>
<capacity_bytes><![CDATA[930213412209]]></capacity_bytes>
<allocated_bytes><![CDATA[1480470528]]></allocated_bytes>
<freesize_bytes><![CDATA[928732941681]]></freesize_bytes>
<lun_meta_reserve_ratio><![CDATA[0.0315]]></lun_meta_reserve_ratio>
<pool_capacity><![CDATA[866 GB]]></pool_capacity>
<pool_allocated><![CDATA[1.38 GB]]></pool_allocated>
<pool_freesize><![CDATA[865 GB]]></pool_freesize>
<pool_threshold><![CDATA[80 %]]></pool_threshold>
<pool_used><![CDATA[0 %]]></pool_used>
<pool_available><![CDATA[100 %]]></pool_available>
<pool_owner><![CDATA[SCA]]></pool_owner>
<pool_type><![CDATA[mirror]]></pool_type>
<pool_dedup><![CDATA[1.00]]></pool_dedup>
<pool_bound><![CDATA[0]]></pool_bound>
<pool_progress><![CDATA[0]]></pool_progress>
<pool_scrub><![CDATA[0]]></pool_scrub>
</row>
</Pool_Index>
<result><![CDATA[0]]></result>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_GET_HOST_LIST = """
<QDocRoot version="1.0">
<authPassed><![CDATA[1]]></authPassed>
<ES_RET_CODE><![CDATA[1]]></ES_RET_CODE>
<host_list>
<host>
<index><![CDATA[fakeHostIndex]]></index>
<hostid><![CDATA[fakeHostId]]></hostid>
<name><![CDATA[manila-hst-123]]></name>
<netaddrs>
<ipv4>
<![CDATA[fakeIp]]>
</ipv4>
</netaddrs>
</host>
</host_list>
<result><![CDATA[0]]></result>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_CREATE_SHARE = """
<QDocRoot version="1.0">
<authPassed><![CDATA[1]]></authPassed>
<ES_RET_CODE><![CDATA[1]]></ES_RET_CODE>
<func>
<ownContent>
<volumeList>
<volume>
<volumeStatus><![CDATA[fakeStatus]]></volumeStatus>
<volumeLabel><![CDATA[fakeLabel]]></volumeLabel>
<volumeValue><![CDATA[faleValue]]></volumeValue>
</volume>
</volumeList>
</ownContent>
</func>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_ES_RET_CODE_NEGATIVE = """
<QDocRoot version="1.0">
<authPassed><![CDATA[1]]></authPassed>
<ES_RET_CODE><![CDATA[-1]]></ES_RET_CODE>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_RESULT_NEGATIVE = """
<QDocRoot version="1.0">
<authPassed><![CDATA[1]]></authPassed>
<result><![CDATA[-1]]></result>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_AUTHPASS_FAIL = """
<QDocRoot version="1.0">
<authPassed><![CDATA[0]]></authPassed>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_DELETE_SHARE = """
<QDocRoot version="1.0">
<authPassed><![CDATA[1]]></authPassed>
<ES_RET_CODE><![CDATA[1]]></ES_RET_CODE>
<result>0</result>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_DELETE_SNAPSHOT = """
<QDocRoot version="1.0">
<authPassed><![CDATA[1]]></authPassed>
<ES_RET_CODE><![CDATA[1]]></ES_RET_CODE>
<result>0</result>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_GET_HOST_LIST_API = """
<QDocRoot version="1.0">
<authPassed><![CDATA[1]]></authPassed>
<ES_RET_CODE><![CDATA[1]]></ES_RET_CODE>
<content>
<host_list>
<host>
<index><![CDATA[fakeHostIndex]]></index>
<hostid><![CDATA[fakeHostId]]></hostid>
<name><![CDATA[manila-hst-123]]></name>
<netaddrs>
<ipv4>
<![CDATA[fakeIp]]>
</ipv4>
</netaddrs>
</host>
</host_list>
</content>
<result><![CDATA[0]]></result>
</QDocRoot>"""
FAKE_RES_DETAIL_DATA_CREATE_SNAPSHOT = """
<QDocRoot version="1.0">
<authPassed><![CDATA[1]]></authPassed>
<ES_RET_CODE><![CDATA[1]]></ES_RET_CODE>
</QDocRoot>"""
class SnapshotClass(object):
"""Snapshot Class."""
size = 0
provider_location = 'fakeShareName@fakeSnapshotName'
def __init__(self, size, provider_location=None):
"""Init."""
self.size = size
self.provider_location = provider_location
def get(self, provider_location):
"""Get function."""
return self.provider_location
def __getitem__(self, arg):
"""Getitem."""
return {
'display_name': 'fakeSnapshotDisplayName',
'id': 'fakeSnapshotId',
'share': {'share_id': 'fakeShareId', 'id': 'fakeId'},
'share_instance': {'share_id': 'fakeShareId', 'id': 'fakeId'},
'size': self.size
}[arg]
def __setitem__(self, key, value):
"""Setitem."""
if key == 'provider_location':
self.provider_location = value
class ShareNfsClass(object):
"""Share Class."""
share_proto = 'NFS'
id = ''
size = 0
def __init__(self, share_id, size):
"""Init."""
self.id = share_id
self.size = size
def __getitem__(self, arg):
"""Getitem."""
return {
'share_proto': self.share_proto,
'id': self.id,
'display_name': 'fakeDisplayName',
'export_locations': [{'path': '1.2.3.4:/share/fakeShareName'}],
'host': 'QnapShareDriver',
'size': self.size
}[arg]
def __setitem__(self, key, value):
"""Setitem."""
if key == 'share_proto':
self.share_proto = value
class ShareCifsClass(object):
"""Share Class."""
share_proto = 'CIFS'
id = ''
size = 0
def __init__(self, share_id, size):
"""Init."""
self.id = share_id
self.size = size
def __getitem__(self, arg):
"""Getitem."""
return {
'share_proto': self.share_proto,
'id': self.id,
'display_name': 'fakeDisplayName',
'export_locations': [{'path': '\\\\1.2.3.4\\fakeShareName'}],
'host': 'QnapShareDriver',
'size': self.size
}[arg]
def __setitem__(self, key, value):
"""Setitem."""
if key == 'share_proto':
self.share_proto = value
class AccessClass(object):
"""Access Class."""
access_type = 'fakeAccessType'
access_level = 'ro'
access_to = 'fakeIp'
def __init__(self, access_type, access_level, access_to):
"""Init."""
self.access_type = access_type
self.access_level = access_level
self.access_to = access_to
def __getitem__(self, arg):
"""Getitem."""
return {
'access_type': self.access_type,
'access_level': self.access_level,
'access_to': self.access_to,
}[arg]
class FakeGetBasicInfoResponseEs(object):
"""Fake GetBasicInfo response from ES nas."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_GETBASIC_INFO_ES
class FakeGetBasicInfoResponseTs(object):
"""Fake GetBasicInfoTS response from TS nas."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_GETBASIC_INFO_TS
class FakeGetBasicInfoResponseTesTs(object):
"""Fake GetBasicInfoTS response from TS nas."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_GETBASIC_INFO_TES_TS
class FakeGetBasicInfoResponseTesEs(object):
"""Fake GetBasicInfoTS response from TS nas."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_GETBASIC_INFO_TES_ES
class FakeGetBasicInfoResponseError(object):
"""Fake GetBasicInfoTS response from TS nas."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_GETBASIC_INFO_ERROR
class FakeCreateShareResponse(object):
"""Fake login response."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_CREATE_SHARE
class FakeDeleteShareResponse(object):
"""Fake login response."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_DELETE_SHARE
class FakeDeleteSnapshotResponse(object):
"""Fake pool info response."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_DELETE_SNAPSHOT
class FakeGetHostListResponse(object):
"""Fake pool info response."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_GET_HOST_LIST_API
class FakeAuthPassFailResponse(object):
"""Fake pool info response."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_AUTHPASS_FAIL
class FakeEsResCodeNegativeResponse(object):
"""Fake pool info response."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_ES_RET_CODE_NEGATIVE
class FakeResultNegativeResponse(object):
"""Fake pool info response."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_RESULT_NEGATIVE
class FakeLoginResponse(object):
"""Fake login response."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_LOGIN
class FakeSpecificPoolInfoResponse(object):
"""Fake pool info response."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_SPECIFIC_POOL_INFO
class FakeShareInfoResponse(object):
"""Fake pool info response."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_SHARE_INFO
class FakeSnapshotInfoResponse(object):
"""Fake pool info response."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_SNAPSHOT
class FakeSpecificVolInfoResponse(object):
"""Fake pool info response."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_VOLUME_INFO
class FakeCreateSnapshotResponse(object):
"""Fake pool info response."""
status = 'fackStatus'
def read(self):
"""Mock response.read."""
return FAKE_RES_DETAIL_DATA_CREATE_SNAPSHOT

View File

@ -0,0 +1,788 @@
# Copyright (c) 2016 QNAP Systems, 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 base64
import ddt
import mock
import six
from six.moves import urllib
from manila import exception
from manila.share.drivers.qnap import qnap
from manila import test
from manila.tests import fake_share
from manila.tests.share.drivers.qnap import fakes
def create_configuration(management_url, qnap_share_ip, qnap_nas_login,
qnap_nas_password, qnap_poolname):
"""Create configuration."""
configuration = mock.Mock()
configuration.qnap_management_url = management_url
configuration.qnap_share_ip = qnap_share_ip
configuration.qnap_nas_login = qnap_nas_login
configuration.qnap_nas_password = qnap_nas_password
configuration.qnap_poolname = qnap_poolname
configuration.safe_get.return_value = False
return configuration
class QnapShareDriverBaseTestCase(test.TestCase):
"""Base Class for the QnapShareDriver Tests."""
def setUp(self):
"""Setup the Qnap Driver Base TestCase."""
super(QnapShareDriverBaseTestCase, self).setUp()
self.driver = None
self.share_api = None
def _do_setup(self, management_url, share_ip, nas_login,
nas_password, poolname, **kwargs):
"""Config do setup configurations."""
self.driver = qnap.QnapShareDriver(
configuration=create_configuration(
management_url,
share_ip,
nas_login,
nas_password,
poolname),
private_storage=kwargs.get('private_storage'))
self.driver.do_setup('context')
@ddt.ddt
class QnapAPITestCase(QnapShareDriverBaseTestCase):
"""Tests QNAP api functions."""
login_url = ('/cgi-bin/authLogin.cgi?')
get_basic_info_url = ('/cgi-bin/authLogin.cgi')
fake_password = 'qnapadmin'
def setUp(self):
"""Setup the Qnap API TestCase."""
super(QnapAPITestCase, self).setUp()
fake_parms = {}
fake_parms['user'] = 'admin'
fake_parms['pwd'] = base64.b64encode(
self.fake_password.encode("utf-8"))
fake_parms['serviceKey'] = 1
sanitized_params = self._sanitize_params(fake_parms)
self.login_url = ('/cgi-bin/authLogin.cgi?%s' % sanitized_params)
self.mock_object(six.moves.http_client, 'HTTPConnection')
self.share = fake_share.fake_share(
share_proto='NFS',
id='shareId',
display_name='fakeDisplayName',
export_locations=[{'path': '1.2.3.4:/share/fakeShareName'}],
host='QnapShareDriver',
size=10)
def _sanitize_params(self, params):
sanitized_params = {}
for key in params:
value = params[key]
if value is not None:
sanitized_params[key] = six.text_type(value)
sanitized_params = urllib.parse.urlencode(sanitized_params)
return sanitized_params
@ddt.data('fake_share_name', 'fakeLabel')
def test_create_share_api(self, fake_name):
"""Test create share api."""
mock_http_connection = six.moves.http_client.HTTPConnection
mock_http_connection.return_value.getresponse.side_effect = [
fakes.FakeLoginResponse(),
fakes.FakeGetBasicInfoResponseEs(),
fakes.FakeLoginResponse(),
fakes.FakeCreateShareResponse()]
self._do_setup('http://1.2.3.4:8080', '1.2.3.4', 'admin',
'qnapadmin', 'Storage Pool 1')
self.driver.api_executor.create_share(
self.share,
'Storage Pool 1',
fake_name,
'NFS')
fake_params = {
'wiz_func': 'share_create',
'action': 'add_share',
'vol_name': fake_name,
'vol_size': '10' + 'GB',
'threshold': '80',
'dedup': 'off',
'compression': '1',
'thin_pro': '0',
'cache': '0',
'cifs_enable': '0',
'nfs_enable': '1',
'afp_enable': '0',
'ftp_enable': '0',
'encryption': '0',
'hidden': '0',
'oplocks': '1',
'sync': 'always',
'userrw0': 'admin',
'userrd_len': '0',
'userrw_len': '1',
'userno_len': '0',
'access_r': 'setup_users',
'path_type': 'auto',
'recycle_bin': '1',
'recycle_bin_administrators_only': '0',
'pool_name': 'Storage Pool 1',
'sid': 'fakeSid',
}
sanitized_params = self._sanitize_params(fake_params)
fake_url = ('/cgi-bin/wizReq.cgi?%s' % sanitized_params)
expected_call_list = [
mock.call('GET', self.login_url),
mock.call('GET', self.get_basic_info_url),
mock.call('GET', self.login_url),
mock.call('GET', fake_url)]
self.assertEqual(
expected_call_list,
mock_http_connection.return_value.request.call_args_list)
def test_api_delete_share(self):
"""Test delete share api."""
mock_http_connection = six.moves.http_client.HTTPConnection
mock_http_connection.return_value.getresponse.side_effect = [
fakes.FakeLoginResponse(),
fakes.FakeGetBasicInfoResponseEs(),
fakes.FakeLoginResponse(),
fakes.FakeDeleteShareResponse()]
self._do_setup('http://1.2.3.4:8080', '1.2.3.4', 'admin',
'qnapadmin', 'Storage Pool 1')
self.driver.api_executor.delete_share(
'fakeId')
fake_params = {
'func': 'volume_mgmt',
'vol_remove': '1',
'volumeID': 'fakeId',
'stop_service': 'no',
'sid': 'fakeSid',
}
sanitized_params = self._sanitize_params(fake_params)
fake_url = (
'/cgi-bin/disk/disk_manage.cgi?%s' % sanitized_params)
expected_call_list = [
mock.call('GET', self.login_url),
mock.call('GET', self.get_basic_info_url),
mock.call('GET', self.login_url),
mock.call('GET', fake_url)]
self.assertEqual(
expected_call_list,
mock_http_connection.return_value.request.call_args_list)
def test_get_specific_poolinfo(self):
"""Test get specific poolinfo api."""
mock_http_connection = six.moves.http_client.HTTPConnection
mock_http_connection.return_value.getresponse.side_effect = [
fakes.FakeLoginResponse(),
fakes.FakeGetBasicInfoResponseEs(),
fakes.FakeLoginResponse(),
fakes.FakeSpecificPoolInfoResponse()]
self._do_setup('http://1.2.3.4:8080', '1.2.3.4', 'admin',
'qnapadmin', 'Storage Pool 1')
self.driver.api_executor.get_specific_poolinfo(
'fakePoolId')
fake_params = {
'store': 'poolInfo',
'func': 'extra_get',
'poolID': 'fakePoolId',
'Pool_Info': '1',
'sid': 'fakeSid',
}
sanitized_params = self._sanitize_params(fake_params)
fake_url = (
'/cgi-bin/disk/disk_manage.cgi?%s' % sanitized_params)
expected_call_list = [
mock.call('GET', self.login_url),
mock.call('GET', self.get_basic_info_url),
mock.call('GET', self.login_url),
mock.call('GET', fake_url)]
self.assertEqual(
expected_call_list,
mock_http_connection.return_value.request.call_args_list)
@ddt.data({'pool_id': "Storage Pool 1"},
{'pool_id': "Storage Pool 1", 'vol_no': 'fakeNo'},
{'pool_id': "Storage Pool 1", 'vol_label': 'fakeShareName'})
def test_get_share_info(self, dict_parm):
"""Test get share info api."""
mock_http_connection = six.moves.http_client.HTTPConnection
mock_http_connection.return_value.getresponse.side_effect = [
fakes.FakeLoginResponse(),
fakes.FakeGetBasicInfoResponseEs(),
fakes.FakeLoginResponse(),
fakes.FakeShareInfoResponse()]
self._do_setup('http://1.2.3.4:8080', '1.2.3.4', 'admin',
'qnapadmin', 'Storage Pool 1')
self.driver.api_executor.get_share_info(**dict_parm)
fake_params = {
'store': 'poolVolumeList',
'poolID': 'Storage Pool 1',
'func': 'extra_get',
'Pool_Vol_Info': '1',
'sid': 'fakeSid',
}
sanitized_params = self._sanitize_params(fake_params)
fake_url = (
'/cgi-bin/disk/disk_manage.cgi?%s' % sanitized_params)
expected_call_list = [
mock.call('GET', self.login_url),
mock.call('GET', self.get_basic_info_url),
mock.call('GET', self.login_url),
mock.call('GET', fake_url)]
self.assertEqual(
expected_call_list,
mock_http_connection.return_value.request.call_args_list)
def test_get_specific_volinfo(self):
"""Test get specific volume info api."""
mock_http_connection = six.moves.http_client.HTTPConnection
mock_http_connection.return_value.getresponse.side_effect = [
fakes.FakeLoginResponse(),
fakes.FakeGetBasicInfoResponseEs(),
fakes.FakeLoginResponse(),
fakes.FakeSpecificVolInfoResponse()]
self._do_setup('http://1.2.3.4:8080', '1.2.3.4', 'admin',
'qnapadmin', 'Storage Pool 1')
self.driver.api_executor.get_specific_volinfo(
'fakeNo')
fake_params = {
'store': 'volumeInfo',
'volumeID': 'fakeNo',
'func': 'extra_get',
'Volume_Info': '1',
'sid': 'fakeSid',
}
sanitized_params = self._sanitize_params(fake_params)
fake_url = (
'/cgi-bin/disk/disk_manage.cgi?%s' % sanitized_params)
expected_call_list = [
mock.call('GET', self.login_url),
mock.call('GET', self.get_basic_info_url),
mock.call('GET', self.login_url),
mock.call('GET', fake_url)]
self.assertEqual(
expected_call_list,
mock_http_connection.return_value.request.call_args_list)
def test_get_snapshot_info_es(self):
"""Test get snapsho info api."""
mock_http_connection = six.moves.http_client.HTTPConnection
mock_http_connection.return_value.getresponse.side_effect = [
fakes.FakeLoginResponse(),
fakes.FakeGetBasicInfoResponseEs(),
fakes.FakeLoginResponse(),
fakes.FakeSnapshotInfoResponse()]
self._do_setup('http://1.2.3.4:8080', '1.2.3.4', 'admin',
'qnapadmin', 'Storage Pool 1')
self.driver.api_executor.get_snapshot_info(
volID='volId', snapshot_name='fakeSnapshotName')
fake_params = {
'func': 'extra_get',
'volumeID': 'volId',
'snapshot_list': '1',
'snap_start': '0',
'snap_count': '100',
'sid': 'fakeSid',
}
sanitized_params = self._sanitize_params(fake_params)
fake_url = (
'/cgi-bin/disk/snapshot.cgi?%s' % sanitized_params)
expected_call_list = [
mock.call('GET', self.login_url),
mock.call('GET', self.get_basic_info_url),
mock.call('GET', self.login_url),
mock.call('GET', fake_url)]
self.assertEqual(
expected_call_list,
mock_http_connection.return_value.request.call_args_list)
def test_create_snapshot_api(self):
"""Test create snapshot api."""
mock_http_connection = six.moves.http_client.HTTPConnection
mock_http_connection.return_value.getresponse.side_effect = [
fakes.FakeLoginResponse(),
fakes.FakeGetBasicInfoResponseEs(),
fakes.FakeLoginResponse(),
fakes.FakeCreateSnapshotResponse()]
self._do_setup('http://1.2.3.4:8080', '1.2.3.4', 'admin',
'qnapadmin', 'Storage Pool 1')
self.driver.api_executor.create_snapshot_api(
'fakeVolumeId',
'fakeSnapshotName')
fake_params = {
'func': 'create_snapshot',
'volumeID': 'fakeVolumeId',
'snapshot_name': 'fakeSnapshotName',
'expire_min': '0',
'vital': '1',
'sid': 'fakeSid',
}
sanitized_params = self._sanitize_params(fake_params)
fake_url = (
'/cgi-bin/disk/snapshot.cgi?%s' % sanitized_params)
expected_call_list = [
mock.call('GET', self.login_url),
mock.call('GET', self.get_basic_info_url),
mock.call('GET', self.login_url),
mock.call('GET', fake_url)]
self.assertEqual(
expected_call_list,
mock_http_connection.return_value.request.call_args_list)
def test_delete_snapshot_api(self):
"""Test delete snapshot api."""
mock_http_connection = six.moves.http_client.HTTPConnection
mock_http_connection.return_value.getresponse.side_effect = [
fakes.FakeLoginResponse(),
fakes.FakeGetBasicInfoResponseEs(),
fakes.FakeLoginResponse(),
fakes.FakeDeleteSnapshotResponse()]
self._do_setup('http://1.2.3.4:8080', '1.2.3.4', 'admin',
'qnapadmin', 'Storage Pool 1')
self.driver.api_executor.delete_snapshot_api(
'fakeSnapshotId')
fake_params = {
'func': 'del_snapshots',
'snapshotID': 'fakeSnapshotId',
'sid': 'fakeSid',
}
sanitized_params = self._sanitize_params(fake_params)
fake_url = (
'/cgi-bin/disk/snapshot.cgi?%s' % sanitized_params)
expected_call_list = [
mock.call('GET', self.login_url),
mock.call('GET', self.get_basic_info_url),
mock.call('GET', self.login_url),
mock.call('GET', fake_url)]
self.assertEqual(
expected_call_list,
mock_http_connection.return_value.request.call_args_list)
def test_clone_snapshot_api(self):
"""Test clone snapshot api."""
mock_http_connection = six.moves.http_client.HTTPConnection
mock_http_connection.return_value.getresponse.side_effect = [
fakes.FakeLoginResponse(),
fakes.FakeGetBasicInfoResponseEs(),
fakes.FakeLoginResponse(),
fakes.FakeDeleteSnapshotResponse()]
self._do_setup('http://1.2.3.4:8080', '1.2.3.4', 'admin',
'qnapadmin', 'Storage Pool 1')
self.driver.api_executor.clone_snapshot(
'fakeSnapshotId',
'fakeNewShareName')
fake_params = {
'func': 'clone_qsnapshot',
'by_vol': '1',
'snapshotID': 'fakeSnapshotId',
'new_name': 'fakeNewShareName',
'sid': 'fakeSid',
}
sanitized_params = self._sanitize_params(fake_params)
fake_url = (
'/cgi-bin/disk/snapshot.cgi?%s' % sanitized_params)
expected_call_list = [
mock.call('GET', self.login_url),
mock.call('GET', self.get_basic_info_url),
mock.call('GET', self.login_url),
mock.call('GET', fake_url)]
self.assertEqual(
expected_call_list,
mock_http_connection.return_value.request.call_args_list)
def test_edit_share_api(self):
"""Test edit share api."""
mock_http_connection = six.moves.http_client.HTTPConnection
mock_http_connection.return_value.getresponse.side_effect = [
fakes.FakeLoginResponse(),
fakes.FakeGetBasicInfoResponseTs(),
fakes.FakeLoginResponse(),
fakes.FakeCreateSnapshotResponse()]
self._do_setup('http://1.2.3.4:8080', '1.2.3.4', 'admin',
'qnapadmin', 'Storage Pool 1')
expect_share_dict = {
"sharename": 'fakeVolId',
"old_sharename": 'fakeVolId',
"new_size": 100,
}
self.driver.api_executor.edit_share(
expect_share_dict)
fake_params = {
'wiz_func': 'share_property',
'action': 'share_property',
'sharename': 'fakeVolId',
'old_sharename': 'fakeVolId',
'vol_size': '100GB',
'dedup': '0',
'compression': '1',
'thin_pro': '0',
'cache': '0',
'afp_enable': '0',
'ftp_enable': '1',
'hidden': '0',
'oplocks': '1',
'sync': 'always',
'recycle_bin': '1',
'recycle_bin_administrators_only': '0',
'sid': 'fakeSid',
}
sanitized_params = self._sanitize_params(fake_params)
fake_url = (
'/cgi-bin/priv/privWizard.cgi?%s' % sanitized_params)
expected_call_list = [
mock.call('GET', self.login_url),
mock.call('GET', self.get_basic_info_url),
mock.call('GET', self.login_url),
mock.call('GET', fake_url)]
self.assertEqual(
expected_call_list,
mock_http_connection.return_value.request.call_args_list)
def test_get_host_list(self):
"""Test get host list api."""
mock_http_connection = six.moves.http_client.HTTPConnection
mock_http_connection.return_value.getresponse.side_effect = [
fakes.FakeLoginResponse(),
fakes.FakeGetBasicInfoResponseEs(),
fakes.FakeLoginResponse(),
fakes.FakeGetHostListResponse()]
self._do_setup('http://1.2.3.4:8080', '1.2.3.4', 'admin',
'qnapadmin', 'Storage Pool 1')
self.driver.api_executor.get_host_list()
fake_params = {
'module': 'hosts',
'func': 'get_hostlist',
'sid': 'fakeSid',
}
sanitized_params = self._sanitize_params(fake_params)
fake_url = (
('/cgi-bin/accessrights/accessrightsRequest.cgi?%s') %
sanitized_params)
expected_call_list = [
mock.call('GET', self.login_url),
mock.call('GET', self.get_basic_info_url),
mock.call('GET', self.login_url),
mock.call('GET', fake_url)]
self.assertEqual(
expected_call_list,
mock_http_connection.return_value.request.call_args_list)
def test_add_host(self):
"""Test add host api."""
mock_http_connection = six.moves.http_client.HTTPConnection
mock_http_connection.return_value.getresponse.side_effect = [
fakes.FakeLoginResponse(),
fakes.FakeGetBasicInfoResponseEs(),
fakes.FakeLoginResponse(),
fakes.FakeGetHostListResponse()]
self._do_setup('http://1.2.3.4:8080', '1.2.3.4', 'admin',
'qnapadmin', 'Storage Pool 1')
self.driver.api_executor.add_host(
'fakeHostName', 'fakeIpV4')
fake_params = {
'module': 'hosts',
'func': 'apply_addhost',
'name': 'fakeHostName',
'ipaddr_v4': 'fakeIpV4',
'sid': 'fakeSid',
}
sanitized_params = self._sanitize_params(fake_params)
fake_url = (
('/cgi-bin/accessrights/accessrightsRequest.cgi?%s') %
sanitized_params)
expected_call_list = [
mock.call('GET', self.login_url),
mock.call('GET', self.get_basic_info_url),
mock.call('GET', self.login_url),
mock.call('GET', fake_url)]
self.assertEqual(
expected_call_list,
mock_http_connection.return_value.request.call_args_list)
def test_set_nfs_access(self):
"""Test get host list api."""
mock_http_connection = six.moves.http_client.HTTPConnection
mock_http_connection.return_value.getresponse.side_effect = [
fakes.FakeLoginResponse(),
fakes.FakeGetBasicInfoResponseEs(),
fakes.FakeLoginResponse(),
fakes.FakeGetHostListResponse()]
self._do_setup('http://1.2.3.4:8080', '1.2.3.4', 'admin',
'qnapadmin', 'Storage Pool 1')
self.driver.api_executor.set_nfs_access(
'fakeShareName', 'fakeAccess', 'fakeHostName')
fake_params = {
'wiz_func': 'share_nfs_control',
'action': 'share_nfs_control',
'sharename': 'fakeShareName',
'access': 'fakeAccess',
'host_name': 'fakeHostName',
'sid': 'fakeSid',
}
sanitized_params = self._sanitize_params(fake_params)
fake_url = (
('/cgi-bin/priv/privWizard.cgi?%s') %
sanitized_params)
expected_call_list = [
mock.call('GET', self.login_url),
mock.call('GET', self.get_basic_info_url),
mock.call('GET', self.login_url),
mock.call('GET', fake_url)]
self.assertEqual(
expected_call_list,
mock_http_connection.return_value.request.call_args_list)
def test_get_snapshot_info_ts_api(self):
"""Test get snapshot info api."""
mock_http_connection = six.moves.http_client.HTTPConnection
mock_http_connection.return_value.getresponse.side_effect = [
fakes.FakeLoginResponse(),
fakes.FakeGetBasicInfoResponseTs(),
fakes.FakeLoginResponse(),
fakes.FakeSnapshotInfoResponse()]
self._do_setup('http://1.2.3.4:8080', '1.2.3.4', 'admin',
'qnapadmin', 'Storage Pool 1')
self.driver.api_executor.get_snapshot_info(
snapshot_name='fakeSnapshotName',
lun_index='fakeLunIndex')
fake_params = {
'func': 'extra_get',
'LUNIndex': 'fakeLunIndex',
'smb_snapshot_list': '1',
'smb_snapshot': '1',
'snapshot_list': '1',
'sid': 'fakeSid'}
sanitized_params = self._sanitize_params(fake_params)
fake_url = (
('/cgi-bin/disk/snapshot.cgi?%s') %
sanitized_params)
expected_call_list = [
mock.call('GET', self.login_url),
mock.call('GET', self.get_basic_info_url),
mock.call('GET', self.login_url),
mock.call('GET', fake_url)]
self.assertEqual(
expected_call_list,
mock_http_connection.return_value.request.call_args_list)
@ddt.data(fakes.FakeAuthPassFailResponse(),
fakes.FakeEsResCodeNegativeResponse())
def test_api_create_share_with_fail_response(self, fake_fail_response):
"""Test create share api with fail response."""
mock_http_connection = six.moves.http_client.HTTPConnection
mock_http_connection.return_value.getresponse.side_effect = [
fakes.FakeLoginResponse(),
fakes.FakeGetBasicInfoResponseEs(),
fakes.FakeLoginResponse(),
fake_fail_response,
fake_fail_response,
fake_fail_response,
fake_fail_response,
fake_fail_response]
self._do_setup('http://1.2.3.4:8080', '1.2.3.4', 'admin',
'qnapadmin', 'Storage Pool 1')
self.assertRaises(
exception.ShareBackendException,
self.driver.api_executor.create_share,
share=self.share,
pool_name='Storage Pool 1',
create_share_name='fake_share_name',
share_proto='NFS')
@ddt.unpack
@ddt.data(['self.driver.api_executor.get_share_info',
{'pool_id': 'fakeId'},
fakes.FakeAuthPassFailResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.get_specific_volinfo',
{'vol_id': 'fakeId'},
fakes.FakeAuthPassFailResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.create_snapshot_api',
{'volumeID': 'fakeVolumeId',
'snapshot_name': 'fakeSnapshotName'},
fakes.FakeAuthPassFailResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.create_snapshot_api',
{'volumeID': 'fakeVolumeId',
'snapshot_name': 'fakeSnapshotName'},
fakes.FakeEsResCodeNegativeResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.get_snapshot_info',
{'volID': 'volId'},
fakes.FakeAuthPassFailResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.get_snapshot_info',
{'volID': 'volId'},
fakes.FakeResultNegativeResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.get_specific_poolinfo',
{'pool_id': 'Storage Pool 1'},
fakes.FakeAuthPassFailResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.get_specific_poolinfo',
{'pool_id': 'Storage Pool 1'},
fakes.FakeResultNegativeResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.delete_share',
{'vol_id': 'fakeId'},
fakes.FakeAuthPassFailResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.delete_share',
{'vol_id': 'fakeId'},
fakes.FakeResultNegativeResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.delete_snapshot_api',
{'snapshot_id': 'fakeSnapshotId'},
fakes.FakeAuthPassFailResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.delete_snapshot_api',
{'snapshot_id': 'fakeSnapshotId'},
fakes.FakeResultNegativeResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.clone_snapshot',
{'snapshot_id': 'fakeSnapshotId',
'new_sharename': 'fakeNewShareName'},
fakes.FakeResultNegativeResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.clone_snapshot',
{'snapshot_id': 'fakeSnapshotId',
'new_sharename': 'fakeNewShareName'},
fakes.FakeAuthPassFailResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.edit_share',
{'share_dict': {"sharename": 'fakeVolId',
"old_sharename": 'fakeVolId',
"new_size": 100}},
fakes.FakeEsResCodeNegativeResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.edit_share',
{'share_dict': {"sharename": 'fakeVolId',
"old_sharename": 'fakeVolId',
"new_size": 100}},
fakes.FakeAuthPassFailResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.add_host',
{'hostname': 'fakeHostName',
'ipv4': 'fakeIpV4'},
fakes.FakeResultNegativeResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.add_host',
{'hostname': 'fakeHostName',
'ipv4': 'fakeIpV4'},
fakes.FakeAuthPassFailResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.get_host_list',
{},
fakes.FakeResultNegativeResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.get_host_list',
{},
fakes.FakeAuthPassFailResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.set_nfs_access',
{'sharename': 'fakeShareName',
'access': 'fakeAccess',
'host_name': 'fakeHostName'},
fakes.FakeAuthPassFailResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.set_nfs_access',
{'sharename': 'fakeShareName',
'access': 'fakeAccess',
'host_name': 'fakeHostName'},
fakes.FakeResultNegativeResponse(),
fakes.FakeGetBasicInfoResponseEs()],
['self.driver.api_executor.get_snapshot_info',
{'snapshot_name': 'fakeSnapshoName',
'lun_index': 'fakeLunIndex'},
fakes.FakeAuthPassFailResponse(),
fakes.FakeGetBasicInfoResponseTs()],
['self.driver.api_executor.get_snapshot_info',
{'snapshot_name': 'fakeSnapshoName',
'lun_index': 'fakeLunIndex'},
fakes.FakeResultNegativeResponse(),
fakes.FakeGetBasicInfoResponseTs()])
def test_get_snapshot_info_ts_with_fail_response(
self, api, dict_parm,
fake_fail_response, fake_basic_info):
"""Test get snapshot info api with fail response."""
mock_http_connection = six.moves.http_client.HTTPConnection
mock_http_connection.return_value.getresponse.side_effect = [
fakes.FakeLoginResponse(),
fake_basic_info,
fakes.FakeLoginResponse(),
fake_fail_response,
fake_fail_response,
fake_fail_response,
fake_fail_response,
fake_fail_response]
self._do_setup('http://1.2.3.4:8080', '1.2.3.4', 'admin',
'qnapadmin', 'Storage Pool 1')
self.assertRaises(
exception.ShareBackendException,
eval(api),
**dict_parm)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
---
features:
- Added Manila share driver for QNAP ES series storage systems.