# Copyright (c) 2019 ShenZhen SandStone Data 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. """SandStone iSCSI Driver.""" import hashlib import json import re import time from oslo_log import log as logging import requests import six from cinder import exception from cinder.i18n import _ from cinder.volume.drivers.sandstone import constants LOG = logging.getLogger(__name__) class RestCmd(object): """Restful api class.""" def __init__(self, address, user, password, suppress_requests_ssl_warnings): """Init RestCmd class. :param address: Restapi uri. :param user: login web username. :param password: login web password. """ self.address = "https://%(address)s" % {"address": address} self.user = user self.password = password self.pagesize = constants.PAGESIZE self.session = None self.short_wait = 10 self.long_wait = 12000 self.debug = True self._init_http_header() def _init_http_header(self): self.session = requests.Session() self.session.headers.update({ "Content-Type": "application/json", "Connection": "keep-alive", "Accept-Encoding": "gzip, deflate", }) self.session.verify = False def run(self, url, method, data=None, json_flag=True, filter_flag=False, om_op_flag=False): """Run rest cmd function. :param url: rest api uri resource. :param data: rest api uri json parameter. :param filter_flag: controller whether filter log. (default 'No') :param om_op_flag: api op have basic and om, use different prefix uri. """ kwargs = {} if data: kwargs["data"] = json.dumps(data) if om_op_flag: rest_url = self.address + constants.OM_URI + url else: rest_url = self.address + constants.BASIC_URI + url func = getattr(self.session, method.lower()) try: result = func(rest_url, **kwargs) except requests.RequestException as err: msg = _('Bad response from server: %(url)s. ' 'Error: %(err)s') % {'url': rest_url, 'err': err} raise exception.VolumeBackendAPIException(msg) try: result.raise_for_status() except requests.HTTPError as exc: if exc.response.status_code == constants.CONNECT_ERROR: try: self.login() except requests.ConnectTimeout as err: msg = (_("Sandstone web server may be abnormal " "or storage may be poweroff. Error: %(err)s") % {'err': err}) raise exception.VolumeBackendAPIException(msg) else: return {"error": {"code": exc.response.status_code, "description": six.text_type(exc)}} if not filter_flag: LOG.info(''' Request URL: %(url)s, Call Method: %(method)s, Request Data: %(data)s, Response Data: %(res)s, Result Data: %(res_json)s.''', {'url': url, 'method': method, 'data': data, 'res': result, 'res_json': result.json()}) if json_flag: return result.json() return result def _assert_restapi_result(self, result, err): if result.get("success") != 1: msg = (_('%(err)s\nresult:%(res)s') % {"err": err, "res": result}) raise exception.VolumeBackendAPIException(data=msg) def login(self): """Login web get with token session.""" url = 'user/login' sha256 = hashlib.sha256() sha256.update(self.password.encode("utf8")) password = sha256.hexdigest() data = {"username": self.user, "password": password} result = self.run(url=url, data=data, method='POST', json_flag=False, om_op_flag=True) self._assert_restapi_result(result.json(), _('Login error.')) cookies = result.cookies set_cookie = result.headers['Set-Cookie'] self.session.headers['Cookie'] = ';'.join( ['XSRF-TOKEN={}'.format(cookies['XSRF-TOKEN']), ' username={}'.format(self.user), ' sdsom_sessionid={}'.format(self._find_sessionid(set_cookie))]) self.session.headers["Referer"] = self.address self.session.headers["X-XSRF-TOKEN"] = cookies["XSRF-TOKEN"] def _find_sessionid(self, headers): sessionid = re.findall("sdsom_sessionid=(\\w+);", headers) if sessionid: return sessionid[0] return "" def _check_special_result(self, result, contain): if result.get("success") == 0 and contain in result.get("data"): return True def logout(self): """Logout release resource.""" url = 'user/logout' data = {"username": self.user} result = self.run(url, 'POST', data=data, om_op_flag=True) self._assert_restapi_result(result, _("Logout out error.")) def query_capacity_info(self): """Query cluster capacity.""" url = 'capacity' capacity_info = {} result = self.run(url, 'POST', filter_flag=True) self._assert_restapi_result(result, _("Query capacity error.")) capacity_info["capacity_bytes"] = result["data"].get( "capacity_bytes", 0) capacity_info["free_bytes"] = result["data"].get("free_bytes", 0) return capacity_info def query_pool_info(self): """Query use pool status.""" url = 'pool/list' result = self.run(url, 'POST') self._assert_restapi_result(result, _("Query pool status error.")) return result["data"] def get_poolid_from_poolname(self): """Use poolname get poolid from pool/list maps.""" data = self.query_pool_info() poolname_map_poolid = {} if data: for pool in data: poolname_map_poolid[pool["realname"]] = pool["pool_id"] return poolname_map_poolid def create_initiator(self, initiator_name): """Create client iqn in storage cluster.""" url = 'resource/initiator/create' data = {"iqn": initiator_name, "type": "iSCSI", "remark": "Cinder iSCSI"} result = self.run(url, 'POST', data=data) # initiator exist, return no err. if self._check_special_result(result, "already exist"): return self._assert_restapi_result(result, _("Create initiator error.")) def _delaytask_list(self, pagesize=20): url = 'delaytask/list' data = {"pageno": 1, "pagesize": pagesize} return self.run(url, 'POST', data=data, om_op_flag=True) def _judge_delaytask_status(self, wait_time, func_name, *args): # wait 10 seconds for task func = getattr(self, func_name.lower()) for wait in range(1, wait_time + 1): try: task_status = func(*args) if self.debug: LOG.info(task_status) except exception.VolumeBackendAPIException as exc: msg = (_("Task: run %(task)s failed, " "err: %(err)s.") % {"task": func_name, "err": exc}) LOG.error(msg) if task_status.get('run_status') == "failed": msg = (_("Task : run %(task)s failed, " "parameter : %(parameter)s, " "progress is %(process)d.") % {"task": func_name, "process": task_status.get('progress'), "parameter": args}) raise exception.VolumeBackendAPIException(data=msg) elif task_status.get('run_status') != "completed": msg = (_("Task : running %(task)s , " "parameter : %(parameter)s, " "progress is %(process)d, " "waited for 1 second, " "total waited %(total)d second.") % {"task": func_name, "process": task_status.get('progress', 0), "parameter": args, "total": wait}) LOG.info(msg) time.sleep(1) elif task_status.get('run_status') == "completed": msg = (_("Task : running %(task)s successfully, " "parameter : %(parameter)s, " "progress is %(process)d, " "total spend %(total)d second.") % {"task": func_name, "process": task_status.get('progress'), "parameter": args, "total": wait}) LOG.info(msg) break def add_initiator_to_target(self, target_name, initiator_name): """Bind client iqn to storage target iqn.""" url = 'resource/target/add_initiator_to_target' data = {"targetName": target_name, "iqns": [{"ip": "", "iqn": initiator_name}]} result = self.run(url, 'POST', data=data) # wait 10 seconds to map initiator self._judge_delaytask_status(self.short_wait, "query_map_initiator_porcess", target_name, initiator_name) self._assert_restapi_result(result, _("Add initiator " "to target error.")) def query_map_initiator_porcess(self, target_name, initiator_name): """Query initiator add to target process.""" result = self._delaytask_list() self._assert_restapi_result(result, _("Query mapping " "initiator process error.")) result = result["data"].get("results", None) or [] expected_parameter = [{"target_name": target_name, "iqns": [{"ip": "", "iqn": initiator_name}]}] task = [map_initiator_task for map_initiator_task in result if map_initiator_task["executor"] == "MapInitiator" and map_initiator_task["parameter"] == expected_parameter] if task: return task[0] return {} def query_initiator_by_name(self, initiator_name): """Query initiator exist or not.""" url = 'resource/initiator/list' data = {"initiatorMark": "", "pageno": 1, "pagesize": self.pagesize, "type": "iSCSI"} result = self.run(url, 'POST', data=data) self._assert_restapi_result(result, _("Query initiator " "by name error.")) result = result["data"].get("results", None) or [] initiator_info = [initiator for initiator in result if initiator.get("iqn", None) == initiator_name] if initiator_info: return initiator_info[0] return None def query_target_initiatoracl(self, target_name, initiator_name): """Query target iqn bind client iqn info.""" url = 'resource/target/get_target_acl_list' data = {"pageno": 1, "pagesize": self.pagesize, "targetName": target_name} result = self.run(url, 'POST', data=data) self._assert_restapi_result(result, _("Query target " "initiatoracl error.")) results = result["data"].get("results", None) acl_info = [acl for acl in results or [] if acl.get("name", None) == initiator_name] return acl_info or None def query_node_by_targetips(self, target_ips): """Query target ip relation with node.""" url = 'block/gateway/server/list' result = self.run(url, 'POST') self._assert_restapi_result(result, _("Query node by " "targetips error.")) targetip_to_hostid = {} for node in result["data"]: for node_access_ip in node.get("networks"): goal_ip = node_access_ip.get("address") if goal_ip in target_ips: targetip_to_hostid[goal_ip] =\ node_access_ip.get("hostid", None) return targetip_to_hostid def query_target_by_name(self, target_name): """Query target iqn exist or not.""" url = 'resource/target/list' data = {"pageno": 1, "pagesize": self.pagesize, "thirdParty": [0, 1], "targetMark": ""} result = self.run(url, 'POST', data=data) self._assert_restapi_result(result, _("Query target by name error.")) result = result["data"].get("results", None) or [] target_info = [target for target in result if target.get("name", None) == target_name] if target_info: return target_info[0] return None def create_target(self, target_name, targetip_to_hostid): """Create target iqn.""" url = 'resource/target/create' data = {"type": "iSCSI", "readOnly": 0, "thirdParty": 1, "targetName": target_name, "networks": [{"hostid": host_id, "address": address} for address, host_id in targetip_to_hostid.items()]} result = self.run(url, 'POST', data=data) # target exist, return no err. if self._check_special_result(result, "already exist"): return self._assert_restapi_result(result, _("Create target error.")) def add_chap_by_target(self, target_name, username, password): """Add chap to target, only support forward.""" url = 'resource/target/add_chap' data = {"password": password, "user": username, "targetName": target_name} result = self.run(url, 'POST', data=data) self._assert_restapi_result(result, _("Add chap by target error.")) def query_chapinfo_by_target(self, target_name, username): """Query chapinfo by target, check chap add target or not.""" url = 'resource/target/get_chap_list' data = {"targetName": target_name} result = self.run(url, 'POST', data=data) self._assert_restapi_result(result, _("Query chapinfo " "by target error.")) result = result.get('data') or [] chapinfo = [c for c in result if c.get("user") == username] if chapinfo: return chapinfo[0] return None def create_lun(self, capacity_bytes, poolid, volume_name): """Create lun resource.""" url = 'resource/lun/add' data = {"capacity_bytes": capacity_bytes, "poolId": poolid, "priority": "normal", "qosSettings": {}, "volumeName": volume_name} result = self.run(url, 'POST', data=data) self._assert_restapi_result(result, _("Create lun error.")) def delete_lun(self, poolid, volume_name): """Delete lun resource.""" url = 'resource/lun/batch_delete' data = {"delayTime": 0, "volumeNameList": [{ "poolId": poolid, "volumeName": volume_name}]} result = self.run(url, 'POST', data=data) # lun deleted, return no err. if self._check_special_result(result, "not found"): return self._assert_restapi_result(result, _("Delete lun error.")) def extend_lun(self, capacity_bytes, poolid, volume_name): """Extend lun, only support enlarge.""" url = 'resource/lun/resize' data = {"capacity_bytes": capacity_bytes, "poolId": poolid, "volumeName": volume_name} result = self.run(url, 'POST', data=data) self._assert_restapi_result(result, _("Extend lun error.")) def unmap_lun(self, target_name, poolid, volume_name, pool_name): """Unbind lun from target iqn.""" url = 'resource/target/unmap_luns' volume_info = self.query_lun_by_name(volume_name, poolid) result = {"success": 0} if volume_info: uuid = volume_info.get("uuid", None) data = {"targetName": target_name, "targetLunList": [uuid], "targetSnapList": []} result = self.run(url, 'POST', data=data) # lun unmaped, return no err. if self._check_special_result(result, "not mapped"): return # wait for 10 seconds to unmap lun. self._judge_delaytask_status(self.short_wait, "query_unmapping_lun_porcess", target_name, volume_name, uuid, pool_name) self._assert_restapi_result(result, _("Unmap lun error.")) else: self._assert_restapi_result(result, _("Unmap lun error, uuid is None.")) def mapping_lun(self, target_name, poolid, volume_name, pool_name): """Bind lun to target iqn.""" url = 'resource/target/map_luns' volume_info = self.query_lun_by_name(volume_name, poolid) result = {"success": 0} if volume_info: uuid = volume_info.get("uuid", None) data = {"targetName": target_name, "targetLunList": [uuid], "targetSnapList": []} result = self.run(url, 'POST', data=data) # lun maped, return no err. if self._check_special_result(result, "already mapped"): return # wait for 10 seconds to map lun. self._judge_delaytask_status(self.short_wait, "query_mapping_lun_porcess", target_name, volume_name, uuid, pool_name) self._assert_restapi_result(result, _("Map lun error.")) else: self._assert_restapi_result(result, _("Map lun error, uuid is None.")) def query_mapping_lun_porcess(self, target_name, volume_name, uuid, pool_name): """Query mapping lun process.""" result = self._delaytask_list() self._assert_restapi_result(result, _("Query mapping " "lun process error.")) expected_parameter = {"target_name": target_name, "image_id": uuid, "target_realname": target_name, "meta_pool": pool_name, "image_realname": volume_name} result = result["data"].get("results", None) or [] task = [map_initiator_task for map_initiator_task in result if map_initiator_task["executor"] == "TargetMap" and map_initiator_task["parameter"] == expected_parameter] if task: return task[0] return {} def query_unmapping_lun_porcess(self, target_name, volume_name, uuid, pool_name): """Query mapping lun process.""" result = self._delaytask_list() self._assert_restapi_result(result, _("Query mapping " "lun process error.")) expected_parameter = {"target_name": target_name, "image_id": uuid, "target_realname": target_name, "meta_pool": pool_name, "image_name": volume_name} result = result["data"].get("results", None) or [] task = [map_initiator_task for map_initiator_task in result if map_initiator_task["executor"] == "TargetUnmap" and map_initiator_task["parameter"] == expected_parameter] if task: return task[0] return {} def query_target_lunacl(self, target_name, poolid, volume_name): """Query target iqn relation with lun.""" url = 'resource/target/get_luns' data = {"pageno": 1, "pagesize": self.pagesize, "pools": [poolid], "targetName": target_name} result = self.run(url, 'POST', data=data) self._assert_restapi_result(result, _("Query target lunacl error.")) # target get_luns use results result = result["data"].get("results", None) or [] lunid = [volume.get("lid", None) for volume in result if volume.get("name", None) == volume_name and volume.get("pool_id") == poolid] if lunid: return lunid[0] return None def query_lun_by_name(self, volume_name, poolid): """Query lun exist or not.""" url = 'resource/lun/list' data = {"pageno": 1, "pagesize": self.pagesize, "volumeMark": volume_name, "sortType": "time", "sortOrder": "desc", "pools": [poolid], "thirdParty": [0, 1]} result = self.run(url, 'POST', data=data) self._assert_restapi_result(result, _("Query lun by name error.")) result = result["data"].get("results", None) or [] volume_info = [volume for volume in result if volume.get("volumeName", None) == volume_name] if volume_info: return volume_info[0] return None def query_target_by_lun(self, volume_name, poolid): """Query lun already mapped target name.""" url = "resource/lun/targets" data = {"poolId": poolid, "volumeName": volume_name} result = self.run(url, 'POST', data=data) self._assert_restapi_result(result, _("Query target by lun error.")) data = result["data"] target_name = data[0].get("name", None) return target_name def create_snapshot(self, poolid, volume_name, snapshot_name): """Create lun snapshot.""" url = 'resource/snapshot/add' data = {"lunName": volume_name, "poolId": poolid, "remark": "Cinder iSCSI snapshot.", "snapName": snapshot_name} result = self.run(url, 'POST', data=data) # snapshot existed, return no err. if self._check_special_result(result, "has exists"): return # wait for 10 seconds to create snapshot self._judge_delaytask_status(self.short_wait, "query_create_snapshot_process", poolid, volume_name, snapshot_name) self._assert_restapi_result(result, _("Create snapshot error.")) def query_create_snapshot_process(self, poolid, volume_name, snapshot_name): """Query create snapshot process.""" result = self._delaytask_list() self._assert_restapi_result(result, _("Query flatten " "lun process error.")) result = result["data"].get("results", None) or [] task = [flatten_task for flatten_task in result if flatten_task["executor"] == "SnapCreate" and flatten_task["parameter"].get("pool_id", None) == poolid and flatten_task["parameter"].get("snap_name", None) == snapshot_name and flatten_task["parameter"].get("lun_name", None) == volume_name] if task: return task[0] return {} def delete_snapshot(self, poolid, volume_name, snapshot_name): """Delete lun snapshot.""" url = 'resource/snapshot/delete' data = {"lunName": volume_name, "poolId": poolid, "snapName": snapshot_name} result = self.run(url, 'POST', data=data) # snapshot deleted, need return no err. if self._check_special_result(result, "not found"): return # wait for 10 seconds to delete snapshot self._judge_delaytask_status(self.short_wait, "query_delete_snapshot_process", poolid, volume_name, snapshot_name) self._assert_restapi_result(result, _("Delete snapshot error.")) def query_delete_snapshot_process(self, poolid, volume_name, snapshot_name): """Query delete snapshot process.""" result = self._delaytask_list() self._assert_restapi_result(result, _("Query delete " "snapshot process error.")) result = result["data"].get("results", None) or [] task = [flatten_task for flatten_task in result if flatten_task["executor"] == "SnapDelete" and flatten_task["parameter"].get("pool_id", None) == poolid and flatten_task["parameter"].get("snap_name", None) == snapshot_name and flatten_task["parameter"].get("lun_name", None) == volume_name] if task: return task[0] return {} def create_lun_from_snapshot(self, snapshot_name, src_volume_name, poolid, dst_volume_name): """Create lun from source lun snapshot.""" url = 'resource/snapshot/clone' data = {"snapshot": {"poolId": poolid, "lunName": src_volume_name, "snapName": snapshot_name}, "cloneLun": {"lunName": dst_volume_name, "poolId": poolid}} result = self.run(url, 'POST', data=data) # clone volume exsited, return no err. if self._check_special_result(result, "already exists"): return # wait for 10 seconds to clone lun self._judge_delaytask_status(self.short_wait, "query_clone_lun_process", poolid, src_volume_name, snapshot_name) self._assert_restapi_result(result, _("Create lun " "from snapshot error.")) self.flatten_lun(dst_volume_name, poolid) def query_clone_lun_process(self, poolid, volume_name, snapshot_name): """Query clone lun process.""" result = self._delaytask_list() self._assert_restapi_result(result, _("Query flatten " "lun process error.")) result = result["data"].get("results", None) or [] task = [flatten_task for flatten_task in result if flatten_task["executor"] == "SnapClone" and flatten_task["parameter"].get("pool_id", None) == poolid and flatten_task["parameter"].get("snap_name", None) == snapshot_name and flatten_task["parameter"].get("lun_name", None) == volume_name] if task: return task[0] return {} def flatten_lun(self, volume_name, poolid): """Flatten lun.""" url = 'resource/lun/flatten' data = {"poolId": poolid, "volumeName": volume_name} result = self.run(url, 'POST', data=data) # volume flattened, return no err. if self._check_special_result(result, "not need flatten"): return # wait for longest 200 min to flatten self._judge_delaytask_status(self.long_wait, "query_flatten_lun_process", poolid, volume_name) self._assert_restapi_result(result, _("Flatten lun error.")) def query_flatten_lun_process(self, poolid, volume_name): """Query flatten lun process.""" result = self._delaytask_list() self._assert_restapi_result(result, _("Query flatten " "lun process error.")) result = result["data"].get("results", None) or [] task = [flatten_task for flatten_task in result if flatten_task["executor"] == "LunFlatten" and flatten_task["parameter"].get("pool_id", None) == poolid and flatten_task["parameter"].get("lun_name", None) == volume_name] if task: return task[0] return {} def create_lun_from_lun(self, dst_volume_name, poolid, src_volume_name): """Clone lun from source lun.""" tmp_snapshot_name = 'temp' + src_volume_name + 'clone' +\ dst_volume_name self.create_snapshot(poolid, src_volume_name, tmp_snapshot_name) self.create_lun_from_snapshot(tmp_snapshot_name, src_volume_name, poolid, dst_volume_name) self.flatten_lun(dst_volume_name, poolid) self.delete_snapshot(poolid, src_volume_name, tmp_snapshot_name) def query_snapshot_by_name(self, volume_name, poolid, snapshot_name): """Query snapshot exist or not.""" url = 'resource/snapshot/list' data = {"lunName": volume_name, "pageno": 1, "pagesize": self.pagesize, "poolId": poolid, "snapMark": ""} result = self.run(url, 'POST', data=data) self._assert_restapi_result(result, _("Query snapshot by name error.")) result = result["data"].get("results", None) or [] snapshot_info = [snapshot for snapshot in result if snapshot.get("snapName", None) == snapshot_name] return snapshot_info