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

781 lines
28 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.init_http_head()
def init_http_head(self):
self.cookie = http_cookiejar.CookieJar()
self.url = None
self.headers = {
"Connection": "keep-alive",
"Content-Type": "application/json",
}
def do_call(self, url, data=None, method=None,
calltimeout=constants.SOCKET_TIMEOUT):
"""Send requests to server.
Send HTTPS call, get response in JSON.
Convert response into Python Object and return it.
"""
if self.url:
url = self.url + url
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)
result = None
try:
req = urlreq.Request(url, data, self.headers)
if method:
req.get_method = lambda: method
res_temp = urlreq.urlopen(req, timeout=calltimeout)
res = res_temp.read().decode("utf-8")
LOG.debug('Response Data: %(res)s.', {'res': res})
except Exception as err:
LOG.error(_LE('\nBad response from server: %(url)s.'
' Error: %(err)s'), {'url': url, 'err': err})
res = '{"error":{"code":%s,' \
'"description":"Connect server error"}}' \
% constants.ERROR_CONNECT_TO_SERVER
try:
result = jsonutils.loads(res)
except Exception as err:
err_msg = (_('JSON transfer error: %s.') % err)
LOG.error(err_msg)
raise exception.InvalidInput(reason=err_msg)
return result
def login(self):
"""Login huawei array."""
login_info = self._get_login_info()
urlstr = login_info['RestURL']
url_list = urlstr.split(";")
deviceid = None
for item_url in url_list:
url = item_url.strip('').strip('\n') + "xx/sessions"
data = jsonutils.dumps({"username": login_info['UserName'],
"password": login_info['UserPassword'],
"scope": "0"})
self.init_http_head()
result = self.do_call(url, data,
calltimeout=constants.LOGIN_SOCKET_TIMEOUT)
if((result['error']['code'] != 0)
or ("data" not in result)
or (result['data']['deviceid'] is None)):
err_msg = (_("Login to %s failed, try another") % item_url)
LOG.error(err_msg)
continue
LOG.debug('Login success: %(url)s\n',
{'url': item_url})
deviceid = result['data']['deviceid']
self.url = item_url + deviceid
self.headers['iBaseToken'] = result['data']['iBaseToken']
break
if deviceid is None:
err_msg = (_("All url Login fail"))
LOG.error(err_msg)
raise exception.InvalidShare(reason=err_msg)
return deviceid
@utils.synchronized('huawei_manila', external=True)
def call(self, url, data=None, method=None):
"""Send requests to server.
if fail, try another RestURL
"""
deviceid = None
old_url = self.url
result = self.do_call(url, data, method)
error_code = result['error']['code']
if(error_code == constants.ERROR_CONNECT_TO_SERVER
or error_code == constants.ERROR_UNAUTHORIZED_TO_SERVER):
err_msg = (_("Can't open the recent url, re-login."))
LOG.error(err_msg)
deviceid = self.login()
if deviceid is not None:
LOG.debug('Replace URL: \n'
'Old URL: %(old_url)s\n'
'New URL: %(new_url)s\n',
{'old_url': old_url,
'new_url': self.url})
result = self.do_call(url, data, method)
return result
def _create_filesystem(self, fs_param):
"""Create file system."""
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)
if node.text.find(prefix_name) > -1:
logininfo[key] = base64.b64decode(six.b(node.text[4:]))
else:
logininfo[key] = node.text
node.text = prefix_name + six.text_type(
base64.b64encode(six.b(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_url_type = self._get_share_url_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 = "/" + share_url_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_url_type):
"""Delete share by share id."""
url = "/" + share_url_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 = "/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 = "/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 = "/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 = "/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 = "/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_type(self, poolinfo):
root = self._read_xml()
for pool_type in ('Thin', 'Thick'):
pool_name_list = root.findtext(('Filesystem/%s_StoragePool'
% pool_type))
pool_name_list = pool_name_list.split(";")
for pool_name in pool_name_list:
pool_name = pool_name.strip().strip('\n')
if poolinfo['name'] == pool_name:
poolinfo['type'] = pool_type
def _find_pool_info(self, pool_name, result):
if pool_name is None:
return
poolinfo = {}
pool_name = pool_name.strip()
for item in result.get('data', []):
if pool_name == item['NAME'] and '2' == item['USAGETYPE']:
poolinfo['name'] = pool_name
poolinfo['ID'] = item['ID']
poolinfo['CAPACITY'] = item['USERFREECAPACITY']
poolinfo['TOTALCAPACITY'] = item['USERTOTALCAPACITY']
poolinfo['CONSUMEDCAPACITY'] = item['USERCONSUMEDCAPACITY']
self._find_pool_type(poolinfo)
break
return poolinfo
def _find_all_pool_info(self):
url = "/storagepool"
result = self.call(url, None)
msg = "Query resource pool error."
self._assert_rest_result(result, msg)
self._assert_data_in_result(result, msg)
return result
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 = "/" + 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 = 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 = ("/" + 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 = "/" + 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 = 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 = "/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 = "/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_url_type):
"""Segments to find share for a period of 100."""
count = self._get_share_count(share_url_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_url_type)
range_begin += 100
count -= 100
return share
def _get_share_count(self, share_url_type):
"""Get share count."""
url = "/" + share_url_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_url_type):
"""Get share by share name."""
range_end = range_begin + 100
url = ("/" + share_url_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_url_type(self, share_proto):
share_url_type = None
if share_proto == 'NFS':
share_url_type = "NFSHARE"
elif share_proto == 'CIFS':
share_url_type = "CIFSHARE"
else:
raise exception.InvalidInput(
reason=(_('Invalid NAS protocol supplied: %s.')
% share_proto))
return share_url_type
def _get_fsid_by_name(self, share_name):
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 = "/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']
fs['CAPACITY'] = result['data']['CAPACITY']
fs['ALLOCTYPE'] = result['data']['ALLOCTYPE']
fs['POOLNAME'] = result['data']['PARENTNAME']
fs['COMPRESSION'] = result['data']['ENABLECOMPRESSION']
fs['DEDUP'] = result['data']['ENABLEDEDUP']
fs['SMARTPARTITIONID'] = result['data']['CACHEPARTITIONID']
fs['SMARTCACHEID'] = result['data']['SMARTCACHEPARTITIONID']
return fs
def _get_share_path(self, share_name):
share_path = "/" + share_name.replace("-", "_") + "/"
return share_path
def _get_share_name_by_export_location(self, export_location, share_proto):
export_location_split = None
share_name = None
share_ip = None
if export_location:
if share_proto == 'NFS':
export_location_split = export_location.split(':/')
if len(export_location_split) == 2:
share_name = export_location_split[1]
share_ip = export_location_split[0]
elif share_proto == 'CIFS':
export_location_split = export_location.split('\\')
if (len(export_location_split) == 4 and
export_location_split[0] == "" and
export_location_split[1] == ""):
share_ip = export_location_split[2]
share_name = export_location_split[3]
if share_name is None:
raise exception.InvalidInput(
reason=(_('No share with export location %s could be found.')
% export_location))
root = self._read_xml()
target_ip = root.findtext('Storage/LogicalPortIP')
if target_ip:
if share_ip != target_ip.strip():
raise exception.InvalidInput(
reason=(_('The share IP %s is not configured.')
% share_ip))
else:
raise exception.InvalidInput(
reason=(_('The config parameter LogicalPortIP is not set.')))
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 _change_share_size(self, fsid, new_size):
url = "/filesystem/%s" % fsid
capacityinfo = {
"CAPACITY": new_size,
}
data = jsonutils.dumps(capacityinfo)
result = self.call(url, data, "PUT")
msg = "Change a share size error!"
self._assert_rest_result(result, msg)
self._assert_data_in_result(result, msg)
def _change_fs_name(self, fsid, name):
url = "/filesystem/%s" % fsid
fs_param = {
"NAME": name.replace("-", "_"),
}
data = jsonutils.dumps(fs_param)
result = self.call(url, data, "PUT")
msg = _("Change filesystem name error.")
self._assert_rest_result(result, msg)
def _change_extra_specs(self, fsid, extra_specs):
url = "/filesystem/%s" % fsid
fs_param = {
"ENABLEDEDUP": extra_specs['dedupe'],
"ENABLECOMPRESSION": extra_specs['compression']
}
data = jsonutils.dumps(fs_param)
result = self.call(url, data, "PUT")
msg = _("Change extra_specs error.")
self._assert_rest_result(result, msg)
def _get_partition_id_by_name(self, name):
url = "/cachepartition"
result = self.call(url, None, "GET")
self._assert_rest_result(result, _('Get partition by name error.'))
if "data" in result:
for item in result['data']:
if name == item['NAME']:
return item['ID']
return None
def get_partition_info_by_id(self, partitionid):
url = '/cachepartition/' + partitionid
result = self.call(url, None, "GET")
self._assert_rest_result(result,
_('Get partition by partition id error.'))
return result['data']
def _add_fs_to_partition(self, fs_id, partition_id):
url = "/filesystem/associate/cachepartition"
data = jsonutils.dumps({"ID": partition_id,
"ASSOCIATEOBJTYPE": 40,
"ASSOCIATEOBJID": fs_id,
"TYPE": 268})
result = self.call(url, data, "POST")
self._assert_rest_result(result,
_('Add filesystem to partition error.'))
def _remove_fs_from_partition(self, fs_id, partition_id):
url = "/smartPartition/removeFs"
data = jsonutils.dumps({"ID": partition_id,
"ASSOCIATEOBJTYPE": 40,
"ASSOCIATEOBJID": fs_id,
"TYPE": 268})
result = self.call(url, data, "PUT")
self._assert_rest_result(result,
_('Remove filesystem from partition error.'))
def _get_cache_id_by_name(self, name):
url = "/SMARTCACHEPARTITION"
result = self.call(url, None, "GET")
self._assert_rest_result(result, _('Get cache by name error.'))
if "data" in result:
for item in result['data']:
if name == item['NAME']:
return item['ID']
return None
def get_cache_info_by_id(self, cacheid):
url = "/SMARTCACHEPARTITION/" + cacheid
data = jsonutils.dumps({"TYPE": "273",
"ID": cacheid})
result = self.call(url, data, "GET")
self._assert_rest_result(
result, _('Get smartcache by cache id error.'))
return result['data']
def _add_fs_to_cache(self, fs_id, cache_id):
url = "/SMARTCACHEPARTITION/CREATE_ASSOCIATE"
data = jsonutils.dumps({"ID": cache_id,
"ASSOCIATEOBJTYPE": 40,
"ASSOCIATEOBJID": fs_id,
"TYPE": 273})
result = self.call(url, data, "PUT")
self._assert_rest_result(result, _('Add filesystem to cache error.'))
def _remove_fs_from_cache(self, fs_id, cache_id):
url = "/SMARTCACHEPARTITION/REMOVE_ASSOCIATE"
data = jsonutils.dumps({"ID": cache_id,
"ASSOCIATEOBJTYPE": 40,
"ASSOCIATEOBJID": fs_id,
"TYPE": 273})
result = self.call(url, data, "PUT")
self._assert_rest_result(result,
_('Remove filesystem from cache error.'))