566 lines
19 KiB
Python
566 lines
19 KiB
Python
# Copyright (c) 2014 Huawei Technologies Co., Ltd.
|
|
# 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
|
|
from xml.etree import ElementTree as ET
|
|
|
|
from oslo_log import log
|
|
from oslo_serialization import jsonutils
|
|
import six
|
|
from six.moves import http_cookiejar
|
|
from six.moves.urllib import request as urlreq # pylint: disable=E0611
|
|
|
|
from manila import exception
|
|
from manila.i18n import _
|
|
from manila.i18n import _LE
|
|
from manila.share.drivers.huawei import constants
|
|
from manila import utils
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
|
|
class RestHelper(object):
|
|
"""Helper class for Huawei OceanStor V3 storage system."""
|
|
|
|
def __init__(self, configuration):
|
|
self.configuration = configuration
|
|
self.cookie = http_cookiejar.CookieJar()
|
|
self.url = None
|
|
self.headers = {
|
|
"Connection": "keep-alive",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
def call(self, url, data=None, method=None):
|
|
"""Send requests to server.
|
|
|
|
Send HTTPS call, get response in JSON.
|
|
Convert response into Python Object and return it.
|
|
"""
|
|
if "xx/sessions" not in url:
|
|
LOG.debug('Request URL: %(url)s\n'
|
|
'Call Method: %(method)s\n'
|
|
'Request Data: %(data)s\n',
|
|
{'url': url,
|
|
'method': method,
|
|
'data': data})
|
|
opener = urlreq.build_opener(urlreq.HTTPCookieProcessor(self.cookie))
|
|
urlreq.install_opener(opener)
|
|
|
|
try:
|
|
req = urlreq.Request(url, data, self.headers)
|
|
if method:
|
|
req.get_method = lambda: method
|
|
res_temp = urlreq.urlopen(req, timeout=constants.SOCKET_TIMEOUT)
|
|
res = res_temp.read().decode("utf-8")
|
|
|
|
LOG.debug('Response Data: %(res)s.', {'res': res})
|
|
|
|
except Exception as err:
|
|
LOG.error(_LE('Bad response from server: %s.') % err)
|
|
raise err
|
|
|
|
try:
|
|
res_json = jsonutils.loads(res)
|
|
except Exception as err:
|
|
err_msg = (_('JSON transfer error: %s.') % err)
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidShare(reason=err_msg)
|
|
|
|
return res_json
|
|
|
|
def login(self):
|
|
"""Log in huawei array."""
|
|
login_info = self._get_login_info()
|
|
url = login_info['RestURL'] + "xx/sessions"
|
|
data = jsonutils.dumps({"username": login_info['UserName'],
|
|
"password": login_info['UserPassword'],
|
|
"scope": "0"})
|
|
result = self.call(url, data)
|
|
if (result['error']['code'] != 0) or ("data" not in result):
|
|
err_msg = (_("Login error, reason is %s.") % result)
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidShare(reason=err_msg)
|
|
|
|
deviceid = result['data']['deviceid']
|
|
self.url = login_info['RestURL'] + deviceid
|
|
self.headers['iBaseToken'] = result['data']['iBaseToken']
|
|
return deviceid
|
|
|
|
def _create_filesystem(self, fs_param):
|
|
"""Create file system."""
|
|
url = self.url + "/filesystem"
|
|
data = jsonutils.dumps(fs_param)
|
|
result = self.call(url, data)
|
|
|
|
msg = 'Create filesystem error.'
|
|
self._assert_rest_result(result, msg)
|
|
self._assert_data_in_result(result, msg)
|
|
|
|
return result['data']['ID']
|
|
|
|
def _assert_rest_result(self, result, err_str):
|
|
if result['error']['code'] != 0:
|
|
err_msg = (_('%(err)s\nresult: %(res)s.') % {'err': err_str,
|
|
'res': result})
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidShare(reason=err_msg)
|
|
|
|
def _assert_data_in_result(self, result, msg):
|
|
if "data" not in result:
|
|
err_msg = (_('%s "data" was not in result.') % msg)
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidShare(reason=err_msg)
|
|
|
|
def _get_login_info(self):
|
|
"""Get login IP, username and password from config file."""
|
|
logininfo = {}
|
|
filename = self.configuration.manila_huawei_conf_file
|
|
tree = ET.parse(filename)
|
|
root = tree.getroot()
|
|
RestURL = root.findtext('Storage/RestURL')
|
|
logininfo['RestURL'] = RestURL.strip()
|
|
|
|
# Prefix !$$$ means encoded already.
|
|
prefix_name = '!$$$'
|
|
need_encode = False
|
|
for key in ['UserName', 'UserPassword']:
|
|
node = root.find('Storage/%s' % key)
|
|
node_text = node.text
|
|
if node_text.find(prefix_name) > -1:
|
|
logininfo[key] = base64.b64decode(node_text[4:])
|
|
else:
|
|
logininfo[key] = node_text
|
|
node.text = prefix_name + base64.b64encode(node_text)
|
|
need_encode = True
|
|
if need_encode:
|
|
self._change_file_mode(filename)
|
|
try:
|
|
tree.write(filename, 'UTF-8')
|
|
except Exception as err:
|
|
err_msg = (_('File write error %s.') % err)
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidShare(reason=err_msg)
|
|
|
|
return logininfo
|
|
|
|
def _change_file_mode(self, filepath):
|
|
try:
|
|
utils.execute('chmod', '666', filepath, run_as_root=True)
|
|
|
|
except Exception as err:
|
|
LOG.error(_LE('Bad response from change file: %s.') % err)
|
|
raise err
|
|
|
|
def _create_share(self, share_name, fs_id, share_proto):
|
|
"""Create a share."""
|
|
share_type = self._get_share_type(share_proto)
|
|
share_path = self._get_share_path(share_name)
|
|
|
|
filepath = {}
|
|
if share_proto == 'NFS':
|
|
filepath = {
|
|
"DESCRIPTION": "",
|
|
"FSID": fs_id,
|
|
"SHAREPATH": share_path,
|
|
}
|
|
elif share_proto == 'CIFS':
|
|
filepath = {
|
|
"SHAREPATH": share_path,
|
|
"DESCRIPTION": "",
|
|
"ABEENABLE": "false",
|
|
"ENABLENOTIFY": "true",
|
|
"ENABLEOPLOCK": "true",
|
|
"NAME": share_name.replace("-", "_"),
|
|
"FSID": fs_id,
|
|
"TENANCYID": "0",
|
|
}
|
|
else:
|
|
raise exception.InvalidShare(
|
|
reason=(_('Invalid NAS protocol supplied: %s.')
|
|
% share_proto))
|
|
|
|
url = self.url + "/" + share_type
|
|
data = jsonutils.dumps(filepath)
|
|
|
|
result = self.call(url, data, "POST")
|
|
|
|
msg = 'Create share error.'
|
|
self._assert_rest_result(result, msg)
|
|
self._assert_data_in_result(result, msg)
|
|
|
|
return result['data']['ID']
|
|
|
|
def _delete_share_by_id(self, share_id, share_type):
|
|
"""Delete share by share id."""
|
|
url = self.url + "/" + share_type + "/" + share_id
|
|
|
|
result = self.call(url, None, "DELETE")
|
|
self._assert_rest_result(result, 'Delete share error.')
|
|
|
|
def _delete_fs(self, fs_id):
|
|
"""Delete file system."""
|
|
# Get available file system
|
|
url = self.url + "/filesystem/" + fs_id
|
|
|
|
result = self.call(url, None, "DELETE")
|
|
self._assert_rest_result(result, 'Delete file system error.')
|
|
|
|
def _get_cifs_service_status(self):
|
|
url = self.url + "/CIFSSERVICE"
|
|
result = self.call(url, None, "GET")
|
|
|
|
msg = 'Get CIFS service status error.'
|
|
self._assert_rest_result(result, msg)
|
|
self._assert_data_in_result(result, msg)
|
|
|
|
return result['data']['RUNNINGSTATUS']
|
|
|
|
def _get_nfs_service_status(self):
|
|
url = self.url + "/NFSSERVICE"
|
|
result = self.call(url, None, "GET")
|
|
|
|
msg = 'Get NFS service status error.'
|
|
self._assert_rest_result(result, msg)
|
|
self._assert_data_in_result(result, msg)
|
|
|
|
service = {}
|
|
|
|
service['RUNNINGSTATUS'] = result['data']['RUNNINGSTATUS']
|
|
service['SUPPORTV3'] = result['data']['SUPPORTV3']
|
|
service['SUPPORTV4'] = result['data']['SUPPORTV4']
|
|
return service
|
|
|
|
def _start_nfs_service_status(self):
|
|
url = self.url + "/NFSSERVICE"
|
|
nfsserviceinfo = {
|
|
"NFSV4DOMAIN": "localdomain",
|
|
"RUNNINGSTATUS": "2",
|
|
"SUPPORTV3": 'true',
|
|
"SUPPORTV4": 'true',
|
|
"TYPE": "16452",
|
|
}
|
|
|
|
data = jsonutils.dumps(nfsserviceinfo)
|
|
result = self.call(url, data, "PUT")
|
|
|
|
self._assert_rest_result(result, 'Start NFS service error.')
|
|
|
|
def _start_cifs_service_status(self):
|
|
url = self.url + "/CIFSSERVICE"
|
|
cifsserviceinfo = {
|
|
"ENABLENOTIFY": "true",
|
|
"ENABLEOPLOCK": "true",
|
|
"ENABLEOPLOCKLEASE": "false",
|
|
"GUESTENABLE": "false",
|
|
"OPLOCKTIMEOUT": "35",
|
|
"RUNNINGSTATUS": "2",
|
|
"SECURITYMODEL": "3",
|
|
"SIGNINGENABLE": "false",
|
|
"SIGNINGREQUIRED": "false",
|
|
"TYPE": "16453",
|
|
}
|
|
|
|
data = jsonutils.dumps(cifsserviceinfo)
|
|
result = self.call(url, data, "PUT")
|
|
|
|
self._assert_rest_result(result, 'Start CIFS service error.')
|
|
|
|
def _find_pool_info(self):
|
|
root = self._read_xml()
|
|
pool_name = root.findtext('Filesystem/StoragePool')
|
|
if not pool_name:
|
|
err_msg = (_("Invalid resource pool: %s.") % pool_name)
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidInput(err_msg)
|
|
|
|
url = self.url + "/storagepool"
|
|
result = self.call(url, None)
|
|
self._assert_rest_result(result, 'Query resource pool error.')
|
|
|
|
poolinfo = {}
|
|
pool_name = pool_name.strip()
|
|
for item in result.get('data', []):
|
|
if pool_name == item['NAME']:
|
|
poolinfo['name'] = pool_name
|
|
poolinfo['ID'] = item['ID']
|
|
poolinfo['CAPACITY'] = item['USERFREECAPACITY']
|
|
poolinfo['TOTALCAPACITY'] = item['USERTOTALCAPACITY']
|
|
break
|
|
|
|
return poolinfo
|
|
|
|
def _read_xml(self):
|
|
"""Open xml file and parse the content."""
|
|
filename = self.configuration.manila_huawei_conf_file
|
|
try:
|
|
tree = ET.parse(filename)
|
|
root = tree.getroot()
|
|
except Exception as err:
|
|
message = (_('Read Huawei config file(%(filename)s)'
|
|
' for Manila error: %(err)s')
|
|
% {'filename': filename,
|
|
'err': err})
|
|
LOG.error(message)
|
|
raise exception.InvalidInput(reason=message)
|
|
return root
|
|
|
|
def _remove_access_from_share(self, access_id, access_type):
|
|
url = self.url + "/" + access_type + "/" + access_id
|
|
result = self.call(url, None, "DELETE")
|
|
self._assert_rest_result(result, 'delete access from share error!')
|
|
|
|
def _get_access_from_count(self, share_id, share_client_type):
|
|
url_subfix = ("/" + share_client_type + "/count?"
|
|
+ "filter=PARENTID::" + share_id)
|
|
url = self.url + url_subfix
|
|
result = self.call(url, None, "GET")
|
|
|
|
msg = "Get access count by share error!"
|
|
self._assert_rest_result(result, msg)
|
|
self._assert_data_in_result(result, msg)
|
|
|
|
return int(result['data']['COUNT'])
|
|
|
|
def _get_access_from_share(self, share_id, access_to, share_client_type):
|
|
"""Segments to find access for a period of 100."""
|
|
count = self._get_access_from_count(share_id, share_client_type)
|
|
|
|
access_id = None
|
|
range_begin = 0
|
|
while True:
|
|
if count < 0 or access_id:
|
|
break
|
|
access_id = self._get_access_from_share_range(share_id,
|
|
access_to,
|
|
range_begin,
|
|
share_client_type)
|
|
range_begin += 100
|
|
count -= 100
|
|
|
|
return access_id
|
|
|
|
def _get_access_from_share_range(self, share_id,
|
|
access_to, range_begin,
|
|
share_client_type):
|
|
range_end = range_begin + 100
|
|
url = (self.url + "/" + share_client_type + "?filter=PARENTID::"
|
|
+ share_id + "&range=[" + six.text_type(range_begin)
|
|
+ "-" + six.text_type(range_end) + "]")
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result, 'Get access id by share error!')
|
|
|
|
for item in result.get('data', []):
|
|
if access_to == item['NAME']:
|
|
return item['ID']
|
|
|
|
def _allow_access_rest(self, share_id, access_to,
|
|
share_proto, access_level):
|
|
"""Allow access to the share."""
|
|
access_type = self._get_share_client_type(share_proto)
|
|
url = self.url + "/" + access_type
|
|
|
|
access = {}
|
|
if access_type == "NFS_SHARE_AUTH_CLIENT":
|
|
access = {
|
|
"TYPE": "16409",
|
|
"NAME": access_to,
|
|
"PARENTID": share_id,
|
|
"ACCESSVAL": access_level,
|
|
"SYNC": "0",
|
|
"ALLSQUASH": "1",
|
|
"ROOTSQUASH": "0",
|
|
}
|
|
elif access_type == "CIFS_SHARE_AUTH_CLIENT":
|
|
access = {
|
|
"NAME": access_to,
|
|
"PARENTID": share_id,
|
|
"PERMISSION": access_level,
|
|
"DOMAINTYPE": "2",
|
|
}
|
|
data = jsonutils.dumps(access)
|
|
result = self.call(url, data, "POST")
|
|
|
|
msg = 'Allow access error.'
|
|
self._assert_rest_result(result, msg)
|
|
|
|
def _get_share_client_type(self, share_proto):
|
|
share_client_type = None
|
|
if share_proto == 'NFS':
|
|
share_client_type = "NFS_SHARE_AUTH_CLIENT"
|
|
elif share_proto == 'CIFS':
|
|
share_client_type = "CIFS_SHARE_AUTH_CLIENT"
|
|
else:
|
|
raise exception.InvalidInput(
|
|
reason=(_('Invalid NAS protocol supplied: %s.')
|
|
% share_proto))
|
|
|
|
return share_client_type
|
|
|
|
def _check_snapshot_id_exist(self, snap_id):
|
|
"""Check the snapshot id exists."""
|
|
url_subfix = "/FSSNAPSHOT/" + snap_id
|
|
|
|
url = self.url + url_subfix
|
|
result = self.call(url, None, "GET")
|
|
|
|
if result['error']['code'] == constants.MSG_SNAPSHOT_NOT_FOUND:
|
|
return False
|
|
elif result['error']['code'] == 0:
|
|
return True
|
|
else:
|
|
err_str = "Check the snapshot id exists error!"
|
|
err_msg = (_('%(err)s\nresult: %(res)s.') % {'err': err_str,
|
|
'res': result})
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidShare(reason=err_msg)
|
|
|
|
def _delete_snapshot(self, snap_id):
|
|
"""Deletes snapshot."""
|
|
url = self.url + "/FSSNAPSHOT/%s" % snap_id
|
|
data = jsonutils.dumps({"TYPE": "48", "ID": snap_id})
|
|
result = self.call(url, data, "DELETE")
|
|
self._assert_rest_result(result, 'Delete snapshot error.')
|
|
|
|
def _create_snapshot(self, sharefsid, snapshot_name):
|
|
"""Create a snapshot."""
|
|
filepath = {
|
|
"PARENTTYPE": "40",
|
|
"TYPE": "48",
|
|
"PARENTID": sharefsid,
|
|
"NAME": snapshot_name.replace("-", "_"),
|
|
"DESCRIPTION": "",
|
|
}
|
|
|
|
url = self.url + "/FSSNAPSHOT"
|
|
data = jsonutils.dumps(filepath)
|
|
|
|
result = self.call(url, data, "POST")
|
|
|
|
msg = 'Create a snapshot error.'
|
|
self._assert_rest_result(result, msg)
|
|
self._assert_data_in_result(result, msg)
|
|
|
|
return result['data']['ID']
|
|
|
|
def _get_share_by_name(self, share_name, share_type):
|
|
"""Segments to find share for a period of 100."""
|
|
count = self._get_share_count(share_type)
|
|
|
|
share = {}
|
|
range_begin = 0
|
|
while True:
|
|
if count < 0 or share:
|
|
break
|
|
share = self._get_share_by_name_range(share_name,
|
|
range_begin,
|
|
share_type)
|
|
range_begin += 100
|
|
count -= 100
|
|
|
|
return share
|
|
|
|
def _get_share_count(self, share_type):
|
|
"""Get share count."""
|
|
url = self.url + "/" + share_type + "/count"
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result, 'Get share count error!')
|
|
|
|
return int(result['data']['COUNT'])
|
|
|
|
def _get_share_by_name_range(self, share_name,
|
|
range_begin, share_type):
|
|
"""Get share by share name."""
|
|
range_end = range_begin + 100
|
|
url = (self.url + "/" + share_type + "?range=["
|
|
+ six.text_type(range_begin) + "-"
|
|
+ six.text_type(range_end) + "]")
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result, 'Get share by name error!')
|
|
|
|
share_path = self._get_share_path(share_name)
|
|
|
|
share = {}
|
|
for item in result.get('data', []):
|
|
if share_path == item['SHAREPATH']:
|
|
share['ID'] = item['ID']
|
|
share['FSID'] = item['FSID']
|
|
break
|
|
|
|
return share
|
|
|
|
def _get_share_type(self, share_proto):
|
|
share_type = None
|
|
if share_proto == 'NFS':
|
|
share_type = "NFSHARE"
|
|
elif share_proto == 'CIFS':
|
|
share_type = "CIFSHARE"
|
|
else:
|
|
raise exception.InvalidInput(
|
|
reason=(_('Invalid NAS protocol supplied: %s.')
|
|
% share_proto))
|
|
|
|
return share_type
|
|
|
|
def _get_fsid_by_name(self, share_name):
|
|
url = self.url + "/FILESYSTEM?range=[0-8191]"
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result, 'Get filesystem by name error!')
|
|
sharename = share_name.replace("-", "_")
|
|
|
|
for item in result.get('data', []):
|
|
if sharename == item['NAME']:
|
|
return item['ID']
|
|
|
|
def _get_fs_info_by_id(self, fsid):
|
|
url = self.url + "/filesystem/%s" % fsid
|
|
result = self.call(url, None, "GET")
|
|
|
|
msg = "Get filesystem info by id error!"
|
|
self._assert_rest_result(result, msg)
|
|
self._assert_data_in_result(result, msg)
|
|
|
|
fs = {}
|
|
fs['HEALTHSTATUS'] = result['data']['HEALTHSTATUS']
|
|
fs['RUNNINGSTATUS'] = result['data']['RUNNINGSTATUS']
|
|
return fs
|
|
|
|
def _get_share_path(self, share_name):
|
|
share_path = "/" + share_name.replace("-", "_") + "/"
|
|
return share_path
|
|
|
|
def _get_share_name_by_id(self, share_id):
|
|
share_name = "share_" + share_id
|
|
return share_name
|
|
|
|
def _get_snapshot_id(self, fs_id, snap_name):
|
|
snapshot_id = (fs_id + "@" + "share_snapshot_"
|
|
+ snap_name.replace("-", "_"))
|
|
return snapshot_id
|
|
|
|
def _extend_share(self, fsid, new_size):
|
|
url = self.url + "/filesystem/%s" % fsid
|
|
|
|
capacityinfo = {
|
|
"CAPACITY": new_size,
|
|
}
|
|
|
|
data = jsonutils.dumps(capacityinfo)
|
|
result = self.call(url, data, "PUT")
|
|
|
|
msg = "Extend a share error!"
|
|
self._assert_rest_result(result, msg)
|
|
self._assert_data_in_result(result, msg)
|