manila/manila/share/drivers/huawei/v3/helper.py

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)