From ee9f3e2c9358e89349f92bf27ba9590637854961 Mon Sep 17 00:00:00 2001 From: huangpeng5 <1298695987@qq.com> Date: Wed, 16 Aug 2023 17:45:33 +0800 Subject: [PATCH] 1.use new query storagepool interface instead old one to improve effiency, 2.the reload_qos interface customized for ZTE cloud platform is added to dynamically update,delete and add qos 3.the timeout interval for invoking Huawei storage RESTful interface --- Cinder/Newton/customization_driver.py | 55 ++++++++++++++++++ Cinder/Newton/dsware.py | 24 +++++--- Cinder/Newton/fs_client.py | 14 ++++- Cinder/Newton/fs_qos.py | 2 +- Cinder/Newton/fs_utils.py | 18 +++--- Cinder/Ocata/customization_driver.py | 55 ++++++++++++++++++ Cinder/Ocata/dsware.py | 24 +++++--- Cinder/Ocata/fs_client.py | 14 ++++- Cinder/Ocata/fs_qos.py | 2 +- Cinder/Ocata/fs_utils.py | 18 +++--- Cinder/Pike/customization_driver.py | 55 ++++++++++++++++++ Cinder/Pike/dsware.py | 24 +++++--- Cinder/Pike/fs_client.py | 14 ++++- Cinder/Pike/fs_qos.py | 2 +- Cinder/Pike/fs_utils.py | 18 +++--- Cinder/Queens/customization_driver.py | 55 ++++++++++++++++++ Cinder/Queens/dsware.py | 24 +++++--- Cinder/Queens/fs_client.py | 14 ++++- Cinder/Queens/fs_qos.py | 2 +- Cinder/Queens/fs_utils.py | 18 +++--- Cinder/Rocky/customization_driver.py | 55 ++++++++++++++++++ Cinder/Rocky/dsware.py | 24 +++++--- Cinder/Rocky/fs_client.py | 14 ++++- Cinder/Rocky/fs_qos.py | 2 +- Cinder/Rocky/fs_utils.py | 18 +++--- Cinder/Stein/customization_driver.py | 55 ++++++++++++++++++ Cinder/Stein/dsware.py | 24 +++++--- Cinder/Stein/fs_client.py | 14 ++++- Cinder/Stein/fs_qos.py | 2 +- Cinder/Stein/fs_utils.py | 18 +++--- Cinder/Train/customization_driver.py | 55 ++++++++++++++++++ Cinder/Train/dsware.py | 24 +++++--- Cinder/Train/fs_client.py | 14 ++++- Cinder/Train/fs_qos.py | 2 +- Cinder/Train/fs_utils.py | 18 +++--- Cinder/Ussuri/customization_driver.py | 55 ++++++++++++++++++ Cinder/Ussuri/dsware.py | 24 +++++--- Cinder/Ussuri/fs_client.py | 14 ++++- Cinder/Ussuri/fs_qos.py | 2 +- Cinder/Ussuri/fs_utils.py | 18 +++--- Cinder/Victoria/customization_driver.py | 55 ++++++++++++++++++ Cinder/Victoria/dsware.py | 24 +++++--- Cinder/Victoria/fs_client.py | 14 ++++- Cinder/Victoria/fs_qos.py | 2 +- Cinder/Victoria/fs_utils.py | 18 +++--- Cinder/Wallaby/customization_driver.py | 55 ++++++++++++++++++ Cinder/Wallaby/dsware.py | 24 +++++--- Cinder/Wallaby/fs_client.py | 14 ++++- Cinder/Wallaby/fs_qos.py | 2 +- Cinder/Wallaby/fs_utils.py | 18 +++--- Cinder/Xena/customization_driver.py | 55 ++++++++++++++++++ Cinder/Xena/dsware.py | 24 +++++--- Cinder/Xena/fs_client.py | 14 ++++- Cinder/Xena/fs_qos.py | 2 +- Cinder/Xena/fs_utils.py | 18 +++--- Cinder/Yoga/customization_driver.py | 55 ++++++++++++++++++ Cinder/Yoga/dsware.py | 24 +++++--- Cinder/Yoga/fs_client.py | 14 ++++- Cinder/Yoga/fs_qos.py | 2 +- Cinder/Yoga/fs_utils.py | 18 +++--- Cinder/Zed/customization_driver.py | 55 ++++++++++++++++++ Cinder/Zed/dsware.py | 24 +++++--- Cinder/Zed/fs_client.py | 14 ++++- Cinder/Zed/fs_qos.py | 2 +- Cinder/Zed/fs_utils.py | 18 +++--- ...5\347\275\256\346\214\207\345\215\227.pdf" | Bin 386667 -> 390375 bytes 66 files changed, 1248 insertions(+), 221 deletions(-) create mode 100644 Cinder/Newton/customization_driver.py create mode 100644 Cinder/Ocata/customization_driver.py create mode 100644 Cinder/Pike/customization_driver.py create mode 100644 Cinder/Queens/customization_driver.py create mode 100644 Cinder/Rocky/customization_driver.py create mode 100644 Cinder/Stein/customization_driver.py create mode 100644 Cinder/Train/customization_driver.py create mode 100644 Cinder/Ussuri/customization_driver.py create mode 100644 Cinder/Victoria/customization_driver.py create mode 100644 Cinder/Wallaby/customization_driver.py create mode 100644 Cinder/Xena/customization_driver.py create mode 100644 Cinder/Yoga/customization_driver.py create mode 100644 Cinder/Zed/customization_driver.py diff --git a/Cinder/Newton/customization_driver.py b/Cinder/Newton/customization_driver.py new file mode 100644 index 0000000..8709ee8 --- /dev/null +++ b/Cinder/Newton/customization_driver.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023 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 logging + +from cinder.volume.drivers.fusionstorage import fs_utils + +LOG = logging.getLogger(__name__) + + +class DriverForZTE(object): + """ + ZTE Cloud Platform Customization Class + """ + def __init__(self, *args, **kwargs): + super(DriverForZTE, self).__init__(*args, **kwargs) + + def reload_qos(self, volume, qos_vals=None): + """ + ZTE Cloud Platform Customization Interface + QoS policies can be dynamically modified,remove,add + and take effect on volumes in real time. + """ + self._check_volume_exist_on_array(volume) + volume_name = self._get_vol_name(volume) + if not qos_vals: + LOG.info("qos_vals is None, remove qos from volume %s", volume_name) + self.fs_qos.remove(volume_name) + return + + qos_vals = fs_utils.get_qos_param(qos_vals, self.client) + vol_qos = self.client.get_qos_by_vol_name(volume_name) + qos_name = vol_qos.get("qosName") + if qos_name: + LOG.info("volume already had qos, " + "update qos:%s of volume %s", qos_name, volume_name) + self.client.modify_qos(qos_name, qos_vals) + return + + LOG.info("volume did not have qos, " + "add qos to volume %s", volume_name) + self.fs_qos.add(qos_vals, volume_name) + return diff --git a/Cinder/Newton/dsware.py b/Cinder/Newton/dsware.py index 48645c6..7c4b913 100644 --- a/Cinder/Newton/dsware.py +++ b/Cinder/Newton/dsware.py @@ -37,6 +37,7 @@ from cinder.volume.drivers.fusionstorage import fs_flow from cinder.volume.drivers.fusionstorage import fs_qos from cinder.volume.drivers.fusionstorage import fs_utils +from cinder.volume.drivers.fusionstorage import customization_driver from cinder.volume.drivers.san import san from cinder.volume import utils as volume_utils @@ -110,6 +111,9 @@ cfg.BoolOpt('full_clone', default=False, help='Whether use full clone.'), + cfg.IntOpt('rest_timeout', + default=constants.DEFAULT_TIMEOUT, + help='timeout when call storage restful api.'), ] CONF = cfg.CONF @@ -117,7 +121,8 @@ @interface.volumedriver -class DSWAREBaseDriver(driver.VolumeDriver): +class DSWAREBaseDriver(customization_driver.DriverForZTE, + driver.VolumeDriver): VERSION = "2.6.2" CI_WIKI_NAME = 'Huawei_FusionStorage_CI' @@ -156,7 +161,8 @@ def do_setup(self, context): } extend_conf = { - "mutual_authentication": mutual_authentication + "mutual_authentication": mutual_authentication, + "rest_timeout": self.configuration.rest_timeout } self.client = fs_client.RestCommon(fs_address=url_str, @@ -167,7 +173,7 @@ def do_setup(self, context): self.fs_qos = fs_qos.FusionStorageQoS(self.client) def check_for_setup_error(self): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() all_pools_name = [p['poolName'] for p in all_pools if p.get('poolName')] @@ -186,7 +192,7 @@ def _update_pool_stats(self): "pools": [], "vendor_name": "Huawei" } - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool['poolName'] in self.configuration.pools_name: @@ -260,7 +266,7 @@ def _get_pool_id(self, volume): def _get_pool_id_by_name(self, pool_name): pool_id_list = [] - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool_name == pool['poolName']: pool_id_list.append(pool['poolId']) @@ -712,6 +718,7 @@ def retype(self, context, volume, new_type, diff, host): volume, new_type, host, vol_name) if migrate: src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) LOG.debug("Begin to migrate LUN(id: %(lun_id)s) with " "change %(change_opts)s.", {"lun_id": src_lun_id, "change_opts": change_opts}) @@ -731,6 +738,7 @@ def migrate_volume(self, context, volume, host): {"volume": volume.id, "host": host}) src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) moved = self._migrate_volume(volume, host, src_lun_id) return moved, {} @@ -856,7 +864,6 @@ def _check_migration_valid(self, host): return True def _check_volume_exist_on_array(self, volume): - volume_name = self._get_vol_name(volume) result = self._check_volume_exist(volume) if not result: msg = _("Volume %s does not exist on the array." @@ -873,11 +880,14 @@ def _check_volume_exist_on_array(self, volume): ) % volume.id self._raise_exception(msg) + return lun_id + + def _check_volume_snapshot_exist(self, volume): + volume_name = self._get_vol_name(volume) if self.client.get_volume_snapshot(volume_name): msg = _("Volume %s which have snapshot cannot do lun migration" ) % volume.id self._raise_exception(msg) - return lun_id def _rollback_snapshot(self, vol_name, snap_name): def _snapshot_rollback_finish(): diff --git a/Cinder/Newton/fs_client.py b/Cinder/Newton/fs_client.py index 3ebf26f..ec2fb06 100644 --- a/Cinder/Newton/fs_client.py +++ b/Cinder/Newton/fs_client.py @@ -44,6 +44,8 @@ def __init__(self, fs_address, fs_user, fs_password, **extend_conf): self.token = None self.version = None self.esn = None + self.rest_timeout = extend_conf.get( + "rest_timeout", constants.DEFAULT_TIMEOUT) mutual_authentication = extend_conf.get("mutual_authentication", {}) self.init_http_head(mutual_authentication) @@ -93,11 +95,13 @@ def _deal_call_result(result, filter_flag, json_flag, req_dict): return result.json() if json_flag else result def call(self, url, method, data=None, - call_timeout=constants.DEFAULT_TIMEOUT, **input_kwargs): + call_timeout=None, **input_kwargs): filter_flag = input_kwargs.get("filter_flag") json_flag = input_kwargs.get("json_flag", True) get_version = input_kwargs.get("get_version") get_system_time = input_kwargs.get("get_system_time") + if call_timeout is None: + call_timeout = self.rest_timeout kwargs = {'timeout': call_timeout} if data is not None: @@ -110,7 +114,7 @@ def call(self, url, method, data=None, result = func(call_url, **kwargs) except Exception as err: LOG.error('Bad response from server: %(url)s. ' - 'Error: %(err)s'), {'url': call_url, 'err': err} + 'Error: %(err)s', {'url': call_url, 'err': err}) return {"error": { "code": constants.CONNECT_ERROR, "description": "Connect to server error."}} @@ -201,6 +205,12 @@ def query_pool_info(self, pool_id=None): self._assert_rest_result(result, _("Query pool session error.")) return result['storagePools'] + def query_storage_pool_info(self): + url = "/cluster/storagepool/queryStoragePool" + result = self.call(url, 'GET', get_version=True, filter_flag=True) + self._assert_rest_result(result, _("Query pool session error.")) + return result.get('storagePools', []) + def _get_volume_num_by_pool(self, pool_id): pool_info = self.query_pool_info(pool_id) return pool_info[0].get('volumeNum', 0) diff --git a/Cinder/Newton/fs_qos.py b/Cinder/Newton/fs_qos.py index 286dbc3..6ed8bb3 100644 --- a/Cinder/Newton/fs_qos.py +++ b/Cinder/Newton/fs_qos.py @@ -42,7 +42,7 @@ def add(self, qos, vol_name): raise def _is_qos_associate_to_volume(self, qos_name): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() volumes = None for pool in all_pools: volumes = self.client.get_qos_volume_info( diff --git a/Cinder/Newton/fs_utils.py b/Cinder/Newton/fs_utils.py index f9a95d6..0e3b765 100644 --- a/Cinder/Newton/fs_utils.py +++ b/Cinder/Newton/fs_utils.py @@ -215,16 +215,20 @@ def _get_qos_specs(qos_specs_id, client): kvs = specs.get('specs', {}) LOG.info('The QoS specs is: %s.', kvs) - qos = dict() - for k, v in kvs.items(): + return get_qos_param(kvs, client) + + +def get_qos_param(qos_vals, client): + qos_param = dict() + for k, v in qos_vals.items(): _raise_qos_is_invalid(k) - qos = _set_qos(qos, k, v) + qos_param = _set_qos(qos_param, k, v) - _raise_qos_not_set(qos) - _set_default_qos(qos) - qos = _get_trigger_qos(qos, client) + _raise_qos_not_set(qos_param) + _set_default_qos(qos_param) + qos_param = _get_trigger_qos(qos_param, client) - return qos + return qos_param def _deal_date_increase_or_decrease(is_date_decrease, is_date_increase, qos): diff --git a/Cinder/Ocata/customization_driver.py b/Cinder/Ocata/customization_driver.py new file mode 100644 index 0000000..8709ee8 --- /dev/null +++ b/Cinder/Ocata/customization_driver.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023 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 logging + +from cinder.volume.drivers.fusionstorage import fs_utils + +LOG = logging.getLogger(__name__) + + +class DriverForZTE(object): + """ + ZTE Cloud Platform Customization Class + """ + def __init__(self, *args, **kwargs): + super(DriverForZTE, self).__init__(*args, **kwargs) + + def reload_qos(self, volume, qos_vals=None): + """ + ZTE Cloud Platform Customization Interface + QoS policies can be dynamically modified,remove,add + and take effect on volumes in real time. + """ + self._check_volume_exist_on_array(volume) + volume_name = self._get_vol_name(volume) + if not qos_vals: + LOG.info("qos_vals is None, remove qos from volume %s", volume_name) + self.fs_qos.remove(volume_name) + return + + qos_vals = fs_utils.get_qos_param(qos_vals, self.client) + vol_qos = self.client.get_qos_by_vol_name(volume_name) + qos_name = vol_qos.get("qosName") + if qos_name: + LOG.info("volume already had qos, " + "update qos:%s of volume %s", qos_name, volume_name) + self.client.modify_qos(qos_name, qos_vals) + return + + LOG.info("volume did not have qos, " + "add qos to volume %s", volume_name) + self.fs_qos.add(qos_vals, volume_name) + return diff --git a/Cinder/Ocata/dsware.py b/Cinder/Ocata/dsware.py index 48645c6..7c4b913 100644 --- a/Cinder/Ocata/dsware.py +++ b/Cinder/Ocata/dsware.py @@ -37,6 +37,7 @@ from cinder.volume.drivers.fusionstorage import fs_flow from cinder.volume.drivers.fusionstorage import fs_qos from cinder.volume.drivers.fusionstorage import fs_utils +from cinder.volume.drivers.fusionstorage import customization_driver from cinder.volume.drivers.san import san from cinder.volume import utils as volume_utils @@ -110,6 +111,9 @@ cfg.BoolOpt('full_clone', default=False, help='Whether use full clone.'), + cfg.IntOpt('rest_timeout', + default=constants.DEFAULT_TIMEOUT, + help='timeout when call storage restful api.'), ] CONF = cfg.CONF @@ -117,7 +121,8 @@ @interface.volumedriver -class DSWAREBaseDriver(driver.VolumeDriver): +class DSWAREBaseDriver(customization_driver.DriverForZTE, + driver.VolumeDriver): VERSION = "2.6.2" CI_WIKI_NAME = 'Huawei_FusionStorage_CI' @@ -156,7 +161,8 @@ def do_setup(self, context): } extend_conf = { - "mutual_authentication": mutual_authentication + "mutual_authentication": mutual_authentication, + "rest_timeout": self.configuration.rest_timeout } self.client = fs_client.RestCommon(fs_address=url_str, @@ -167,7 +173,7 @@ def do_setup(self, context): self.fs_qos = fs_qos.FusionStorageQoS(self.client) def check_for_setup_error(self): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() all_pools_name = [p['poolName'] for p in all_pools if p.get('poolName')] @@ -186,7 +192,7 @@ def _update_pool_stats(self): "pools": [], "vendor_name": "Huawei" } - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool['poolName'] in self.configuration.pools_name: @@ -260,7 +266,7 @@ def _get_pool_id(self, volume): def _get_pool_id_by_name(self, pool_name): pool_id_list = [] - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool_name == pool['poolName']: pool_id_list.append(pool['poolId']) @@ -712,6 +718,7 @@ def retype(self, context, volume, new_type, diff, host): volume, new_type, host, vol_name) if migrate: src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) LOG.debug("Begin to migrate LUN(id: %(lun_id)s) with " "change %(change_opts)s.", {"lun_id": src_lun_id, "change_opts": change_opts}) @@ -731,6 +738,7 @@ def migrate_volume(self, context, volume, host): {"volume": volume.id, "host": host}) src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) moved = self._migrate_volume(volume, host, src_lun_id) return moved, {} @@ -856,7 +864,6 @@ def _check_migration_valid(self, host): return True def _check_volume_exist_on_array(self, volume): - volume_name = self._get_vol_name(volume) result = self._check_volume_exist(volume) if not result: msg = _("Volume %s does not exist on the array." @@ -873,11 +880,14 @@ def _check_volume_exist_on_array(self, volume): ) % volume.id self._raise_exception(msg) + return lun_id + + def _check_volume_snapshot_exist(self, volume): + volume_name = self._get_vol_name(volume) if self.client.get_volume_snapshot(volume_name): msg = _("Volume %s which have snapshot cannot do lun migration" ) % volume.id self._raise_exception(msg) - return lun_id def _rollback_snapshot(self, vol_name, snap_name): def _snapshot_rollback_finish(): diff --git a/Cinder/Ocata/fs_client.py b/Cinder/Ocata/fs_client.py index 3ebf26f..ec2fb06 100644 --- a/Cinder/Ocata/fs_client.py +++ b/Cinder/Ocata/fs_client.py @@ -44,6 +44,8 @@ def __init__(self, fs_address, fs_user, fs_password, **extend_conf): self.token = None self.version = None self.esn = None + self.rest_timeout = extend_conf.get( + "rest_timeout", constants.DEFAULT_TIMEOUT) mutual_authentication = extend_conf.get("mutual_authentication", {}) self.init_http_head(mutual_authentication) @@ -93,11 +95,13 @@ def _deal_call_result(result, filter_flag, json_flag, req_dict): return result.json() if json_flag else result def call(self, url, method, data=None, - call_timeout=constants.DEFAULT_TIMEOUT, **input_kwargs): + call_timeout=None, **input_kwargs): filter_flag = input_kwargs.get("filter_flag") json_flag = input_kwargs.get("json_flag", True) get_version = input_kwargs.get("get_version") get_system_time = input_kwargs.get("get_system_time") + if call_timeout is None: + call_timeout = self.rest_timeout kwargs = {'timeout': call_timeout} if data is not None: @@ -110,7 +114,7 @@ def call(self, url, method, data=None, result = func(call_url, **kwargs) except Exception as err: LOG.error('Bad response from server: %(url)s. ' - 'Error: %(err)s'), {'url': call_url, 'err': err} + 'Error: %(err)s', {'url': call_url, 'err': err}) return {"error": { "code": constants.CONNECT_ERROR, "description": "Connect to server error."}} @@ -201,6 +205,12 @@ def query_pool_info(self, pool_id=None): self._assert_rest_result(result, _("Query pool session error.")) return result['storagePools'] + def query_storage_pool_info(self): + url = "/cluster/storagepool/queryStoragePool" + result = self.call(url, 'GET', get_version=True, filter_flag=True) + self._assert_rest_result(result, _("Query pool session error.")) + return result.get('storagePools', []) + def _get_volume_num_by_pool(self, pool_id): pool_info = self.query_pool_info(pool_id) return pool_info[0].get('volumeNum', 0) diff --git a/Cinder/Ocata/fs_qos.py b/Cinder/Ocata/fs_qos.py index 286dbc3..6ed8bb3 100644 --- a/Cinder/Ocata/fs_qos.py +++ b/Cinder/Ocata/fs_qos.py @@ -42,7 +42,7 @@ def add(self, qos, vol_name): raise def _is_qos_associate_to_volume(self, qos_name): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() volumes = None for pool in all_pools: volumes = self.client.get_qos_volume_info( diff --git a/Cinder/Ocata/fs_utils.py b/Cinder/Ocata/fs_utils.py index 0856038..5bcfdf3 100644 --- a/Cinder/Ocata/fs_utils.py +++ b/Cinder/Ocata/fs_utils.py @@ -217,16 +217,20 @@ def _get_qos_specs(qos_specs_id, client): kvs = specs.get('specs', {}) LOG.info('The QoS specs is: %s.', kvs) - qos = dict() - for k, v in kvs.items(): + return get_qos_param(kvs, client) + + +def get_qos_param(qos_vals, client): + qos_param = dict() + for k, v in qos_vals.items(): _raise_qos_is_invalid(k) - qos = _set_qos(qos, k, v) + qos_param = _set_qos(qos_param, k, v) - _raise_qos_not_set(qos) - _set_default_qos(qos) - qos = _get_trigger_qos(qos, client) + _raise_qos_not_set(qos_param) + _set_default_qos(qos_param) + qos_param = _get_trigger_qos(qos_param, client) - return qos + return qos_param def _deal_date_increase_or_decrease(is_date_decrease, is_date_increase, qos): diff --git a/Cinder/Pike/customization_driver.py b/Cinder/Pike/customization_driver.py new file mode 100644 index 0000000..8709ee8 --- /dev/null +++ b/Cinder/Pike/customization_driver.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023 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 logging + +from cinder.volume.drivers.fusionstorage import fs_utils + +LOG = logging.getLogger(__name__) + + +class DriverForZTE(object): + """ + ZTE Cloud Platform Customization Class + """ + def __init__(self, *args, **kwargs): + super(DriverForZTE, self).__init__(*args, **kwargs) + + def reload_qos(self, volume, qos_vals=None): + """ + ZTE Cloud Platform Customization Interface + QoS policies can be dynamically modified,remove,add + and take effect on volumes in real time. + """ + self._check_volume_exist_on_array(volume) + volume_name = self._get_vol_name(volume) + if not qos_vals: + LOG.info("qos_vals is None, remove qos from volume %s", volume_name) + self.fs_qos.remove(volume_name) + return + + qos_vals = fs_utils.get_qos_param(qos_vals, self.client) + vol_qos = self.client.get_qos_by_vol_name(volume_name) + qos_name = vol_qos.get("qosName") + if qos_name: + LOG.info("volume already had qos, " + "update qos:%s of volume %s", qos_name, volume_name) + self.client.modify_qos(qos_name, qos_vals) + return + + LOG.info("volume did not have qos, " + "add qos to volume %s", volume_name) + self.fs_qos.add(qos_vals, volume_name) + return diff --git a/Cinder/Pike/dsware.py b/Cinder/Pike/dsware.py index 0cd030b..6168b5f 100644 --- a/Cinder/Pike/dsware.py +++ b/Cinder/Pike/dsware.py @@ -37,6 +37,7 @@ from cinder.volume.drivers.fusionstorage import fs_flow from cinder.volume.drivers.fusionstorage import fs_qos from cinder.volume.drivers.fusionstorage import fs_utils +from cinder.volume.drivers.fusionstorage import customization_driver from cinder.volume.drivers.san import san from cinder.volume import utils as volume_utils @@ -114,6 +115,9 @@ cfg.BoolOpt('full_clone', default=False, help='Whether use full clone.'), + cfg.IntOpt('rest_timeout', + default=constants.DEFAULT_TIMEOUT, + help='timeout when call storage restful api.'), ] CONF = cfg.CONF @@ -121,7 +125,8 @@ @interface.volumedriver -class DSWAREBaseDriver(driver.VolumeDriver): +class DSWAREBaseDriver(customization_driver.DriverForZTE, + driver.VolumeDriver): VERSION = "2.6.2" CI_WIKI_NAME = 'Huawei_FusionStorage_CI' @@ -160,7 +165,8 @@ def do_setup(self, context): } extend_conf = { - "mutual_authentication": mutual_authentication + "mutual_authentication": mutual_authentication, + "rest_timeout": self.configuration.rest_timeout } self.client = fs_client.RestCommon(fs_address=url_str, @@ -171,7 +177,7 @@ def do_setup(self, context): self.fs_qos = fs_qos.FusionStorageQoS(self.client) def check_for_setup_error(self): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() all_pools_name = [p['poolName'] for p in all_pools if p.get('poolName')] @@ -190,7 +196,7 @@ def _update_pool_stats(self): "pools": [], "vendor_name": "Huawei" } - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool['poolName'] in self.configuration.pools_name: @@ -264,7 +270,7 @@ def _get_pool_id(self, volume): def _get_pool_id_by_name(self, pool_name): pool_id_list = [] - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool_name == pool['poolName']: pool_id_list.append(pool['poolId']) @@ -716,6 +722,7 @@ def retype(self, context, volume, new_type, diff, host): volume, new_type, host, vol_name) if migrate: src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) LOG.debug("Begin to migrate LUN(id: %(lun_id)s) with " "change %(change_opts)s.", {"lun_id": src_lun_id, "change_opts": change_opts}) @@ -735,6 +742,7 @@ def migrate_volume(self, context, volume, host): {"volume": volume.id, "host": host}) src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) moved = self._migrate_volume(volume, host, src_lun_id) return moved, {} @@ -860,7 +868,6 @@ def _check_migration_valid(self, host): return True def _check_volume_exist_on_array(self, volume): - volume_name = self._get_vol_name(volume) result = self._check_volume_exist(volume) if not result: msg = _("Volume %s does not exist on the array." @@ -877,11 +884,14 @@ def _check_volume_exist_on_array(self, volume): ) % volume.id self._raise_exception(msg) + return lun_id + + def _check_volume_snapshot_exist(self, volume): + volume_name = self._get_vol_name(volume) if self.client.get_volume_snapshot(volume_name): msg = _("Volume %s which have snapshot cannot do lun migration" ) % volume.id self._raise_exception(msg) - return lun_id def _rollback_snapshot(self, vol_name, snap_name): def _snapshot_rollback_finish(): diff --git a/Cinder/Pike/fs_client.py b/Cinder/Pike/fs_client.py index 3ebf26f..ec2fb06 100644 --- a/Cinder/Pike/fs_client.py +++ b/Cinder/Pike/fs_client.py @@ -44,6 +44,8 @@ def __init__(self, fs_address, fs_user, fs_password, **extend_conf): self.token = None self.version = None self.esn = None + self.rest_timeout = extend_conf.get( + "rest_timeout", constants.DEFAULT_TIMEOUT) mutual_authentication = extend_conf.get("mutual_authentication", {}) self.init_http_head(mutual_authentication) @@ -93,11 +95,13 @@ def _deal_call_result(result, filter_flag, json_flag, req_dict): return result.json() if json_flag else result def call(self, url, method, data=None, - call_timeout=constants.DEFAULT_TIMEOUT, **input_kwargs): + call_timeout=None, **input_kwargs): filter_flag = input_kwargs.get("filter_flag") json_flag = input_kwargs.get("json_flag", True) get_version = input_kwargs.get("get_version") get_system_time = input_kwargs.get("get_system_time") + if call_timeout is None: + call_timeout = self.rest_timeout kwargs = {'timeout': call_timeout} if data is not None: @@ -110,7 +114,7 @@ def call(self, url, method, data=None, result = func(call_url, **kwargs) except Exception as err: LOG.error('Bad response from server: %(url)s. ' - 'Error: %(err)s'), {'url': call_url, 'err': err} + 'Error: %(err)s', {'url': call_url, 'err': err}) return {"error": { "code": constants.CONNECT_ERROR, "description": "Connect to server error."}} @@ -201,6 +205,12 @@ def query_pool_info(self, pool_id=None): self._assert_rest_result(result, _("Query pool session error.")) return result['storagePools'] + def query_storage_pool_info(self): + url = "/cluster/storagepool/queryStoragePool" + result = self.call(url, 'GET', get_version=True, filter_flag=True) + self._assert_rest_result(result, _("Query pool session error.")) + return result.get('storagePools', []) + def _get_volume_num_by_pool(self, pool_id): pool_info = self.query_pool_info(pool_id) return pool_info[0].get('volumeNum', 0) diff --git a/Cinder/Pike/fs_qos.py b/Cinder/Pike/fs_qos.py index 286dbc3..6ed8bb3 100644 --- a/Cinder/Pike/fs_qos.py +++ b/Cinder/Pike/fs_qos.py @@ -42,7 +42,7 @@ def add(self, qos, vol_name): raise def _is_qos_associate_to_volume(self, qos_name): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() volumes = None for pool in all_pools: volumes = self.client.get_qos_volume_info( diff --git a/Cinder/Pike/fs_utils.py b/Cinder/Pike/fs_utils.py index 0856038..5bcfdf3 100644 --- a/Cinder/Pike/fs_utils.py +++ b/Cinder/Pike/fs_utils.py @@ -217,16 +217,20 @@ def _get_qos_specs(qos_specs_id, client): kvs = specs.get('specs', {}) LOG.info('The QoS specs is: %s.', kvs) - qos = dict() - for k, v in kvs.items(): + return get_qos_param(kvs, client) + + +def get_qos_param(qos_vals, client): + qos_param = dict() + for k, v in qos_vals.items(): _raise_qos_is_invalid(k) - qos = _set_qos(qos, k, v) + qos_param = _set_qos(qos_param, k, v) - _raise_qos_not_set(qos) - _set_default_qos(qos) - qos = _get_trigger_qos(qos, client) + _raise_qos_not_set(qos_param) + _set_default_qos(qos_param) + qos_param = _get_trigger_qos(qos_param, client) - return qos + return qos_param def _deal_date_increase_or_decrease(is_date_decrease, is_date_increase, qos): diff --git a/Cinder/Queens/customization_driver.py b/Cinder/Queens/customization_driver.py new file mode 100644 index 0000000..8709ee8 --- /dev/null +++ b/Cinder/Queens/customization_driver.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023 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 logging + +from cinder.volume.drivers.fusionstorage import fs_utils + +LOG = logging.getLogger(__name__) + + +class DriverForZTE(object): + """ + ZTE Cloud Platform Customization Class + """ + def __init__(self, *args, **kwargs): + super(DriverForZTE, self).__init__(*args, **kwargs) + + def reload_qos(self, volume, qos_vals=None): + """ + ZTE Cloud Platform Customization Interface + QoS policies can be dynamically modified,remove,add + and take effect on volumes in real time. + """ + self._check_volume_exist_on_array(volume) + volume_name = self._get_vol_name(volume) + if not qos_vals: + LOG.info("qos_vals is None, remove qos from volume %s", volume_name) + self.fs_qos.remove(volume_name) + return + + qos_vals = fs_utils.get_qos_param(qos_vals, self.client) + vol_qos = self.client.get_qos_by_vol_name(volume_name) + qos_name = vol_qos.get("qosName") + if qos_name: + LOG.info("volume already had qos, " + "update qos:%s of volume %s", qos_name, volume_name) + self.client.modify_qos(qos_name, qos_vals) + return + + LOG.info("volume did not have qos, " + "add qos to volume %s", volume_name) + self.fs_qos.add(qos_vals, volume_name) + return diff --git a/Cinder/Queens/dsware.py b/Cinder/Queens/dsware.py index e53053a..72fc4cf 100644 --- a/Cinder/Queens/dsware.py +++ b/Cinder/Queens/dsware.py @@ -37,6 +37,7 @@ from cinder.volume.drivers.fusionstorage import fs_flow from cinder.volume.drivers.fusionstorage import fs_qos from cinder.volume.drivers.fusionstorage import fs_utils +from cinder.volume.drivers.fusionstorage import customization_driver from cinder.volume.drivers.san import san from cinder.volume import utils as volume_utils @@ -114,6 +115,9 @@ cfg.BoolOpt('full_clone', default=False, help='Whether use full clone.'), + cfg.IntOpt('rest_timeout', + default=constants.DEFAULT_TIMEOUT, + help='timeout when call storage restful api.'), ] CONF = cfg.CONF @@ -121,7 +125,8 @@ @interface.volumedriver -class DSWAREBaseDriver(driver.VolumeDriver): +class DSWAREBaseDriver(customization_driver.DriverForZTE, + driver.VolumeDriver): VERSION = "2.6.2" CI_WIKI_NAME = 'Huawei_FusionStorage_CI' @@ -160,7 +165,8 @@ def do_setup(self, context): } extend_conf = { - "mutual_authentication": mutual_authentication + "mutual_authentication": mutual_authentication, + "rest_timeout": self.configuration.rest_timeout } self.client = fs_client.RestCommon(fs_address=url_str, @@ -171,7 +177,7 @@ def do_setup(self, context): self.fs_qos = fs_qos.FusionStorageQoS(self.client) def check_for_setup_error(self): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() all_pools_name = [p['poolName'] for p in all_pools if p.get('poolName')] @@ -190,7 +196,7 @@ def _update_pool_stats(self): "pools": [], "vendor_name": "Huawei" } - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool['poolName'] in self.configuration.pools_name: @@ -264,7 +270,7 @@ def _get_pool_id(self, volume): def _get_pool_id_by_name(self, pool_name): pool_id_list = [] - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool_name == pool['poolName']: pool_id_list.append(pool['poolId']) @@ -716,6 +722,7 @@ def retype(self, context, volume, new_type, diff, host): volume, new_type, host, vol_name) if migrate: src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) LOG.debug("Begin to migrate LUN(id: %(lun_id)s) with " "change %(change_opts)s.", {"lun_id": src_lun_id, "change_opts": change_opts}) @@ -735,6 +742,7 @@ def migrate_volume(self, context, volume, host): {"volume": volume.id, "host": host}) src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) moved = self._migrate_volume(volume, host, src_lun_id) return moved, {} @@ -860,7 +868,6 @@ def _check_migration_valid(self, host): return True def _check_volume_exist_on_array(self, volume): - volume_name = self._get_vol_name(volume) result = self._check_volume_exist(volume) if not result: msg = _("Volume %s does not exist on the array." @@ -877,11 +884,14 @@ def _check_volume_exist_on_array(self, volume): ) % volume.id self._raise_exception(msg) + return lun_id + + def _check_volume_snapshot_exist(self, volume): + volume_name = self._get_vol_name(volume) if self.client.get_volume_snapshot(volume_name): msg = _("Volume %s which have snapshot cannot do lun migration" ) % volume.id self._raise_exception(msg) - return lun_id def _rollback_snapshot(self, vol_name, snap_name): def _snapshot_rollback_finish(): diff --git a/Cinder/Queens/fs_client.py b/Cinder/Queens/fs_client.py index 3ebf26f..ec2fb06 100644 --- a/Cinder/Queens/fs_client.py +++ b/Cinder/Queens/fs_client.py @@ -44,6 +44,8 @@ def __init__(self, fs_address, fs_user, fs_password, **extend_conf): self.token = None self.version = None self.esn = None + self.rest_timeout = extend_conf.get( + "rest_timeout", constants.DEFAULT_TIMEOUT) mutual_authentication = extend_conf.get("mutual_authentication", {}) self.init_http_head(mutual_authentication) @@ -93,11 +95,13 @@ def _deal_call_result(result, filter_flag, json_flag, req_dict): return result.json() if json_flag else result def call(self, url, method, data=None, - call_timeout=constants.DEFAULT_TIMEOUT, **input_kwargs): + call_timeout=None, **input_kwargs): filter_flag = input_kwargs.get("filter_flag") json_flag = input_kwargs.get("json_flag", True) get_version = input_kwargs.get("get_version") get_system_time = input_kwargs.get("get_system_time") + if call_timeout is None: + call_timeout = self.rest_timeout kwargs = {'timeout': call_timeout} if data is not None: @@ -110,7 +114,7 @@ def call(self, url, method, data=None, result = func(call_url, **kwargs) except Exception as err: LOG.error('Bad response from server: %(url)s. ' - 'Error: %(err)s'), {'url': call_url, 'err': err} + 'Error: %(err)s', {'url': call_url, 'err': err}) return {"error": { "code": constants.CONNECT_ERROR, "description": "Connect to server error."}} @@ -201,6 +205,12 @@ def query_pool_info(self, pool_id=None): self._assert_rest_result(result, _("Query pool session error.")) return result['storagePools'] + def query_storage_pool_info(self): + url = "/cluster/storagepool/queryStoragePool" + result = self.call(url, 'GET', get_version=True, filter_flag=True) + self._assert_rest_result(result, _("Query pool session error.")) + return result.get('storagePools', []) + def _get_volume_num_by_pool(self, pool_id): pool_info = self.query_pool_info(pool_id) return pool_info[0].get('volumeNum', 0) diff --git a/Cinder/Queens/fs_qos.py b/Cinder/Queens/fs_qos.py index 286dbc3..6ed8bb3 100644 --- a/Cinder/Queens/fs_qos.py +++ b/Cinder/Queens/fs_qos.py @@ -42,7 +42,7 @@ def add(self, qos, vol_name): raise def _is_qos_associate_to_volume(self, qos_name): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() volumes = None for pool in all_pools: volumes = self.client.get_qos_volume_info( diff --git a/Cinder/Queens/fs_utils.py b/Cinder/Queens/fs_utils.py index 0856038..5bcfdf3 100644 --- a/Cinder/Queens/fs_utils.py +++ b/Cinder/Queens/fs_utils.py @@ -217,16 +217,20 @@ def _get_qos_specs(qos_specs_id, client): kvs = specs.get('specs', {}) LOG.info('The QoS specs is: %s.', kvs) - qos = dict() - for k, v in kvs.items(): + return get_qos_param(kvs, client) + + +def get_qos_param(qos_vals, client): + qos_param = dict() + for k, v in qos_vals.items(): _raise_qos_is_invalid(k) - qos = _set_qos(qos, k, v) + qos_param = _set_qos(qos_param, k, v) - _raise_qos_not_set(qos) - _set_default_qos(qos) - qos = _get_trigger_qos(qos, client) + _raise_qos_not_set(qos_param) + _set_default_qos(qos_param) + qos_param = _get_trigger_qos(qos_param, client) - return qos + return qos_param def _deal_date_increase_or_decrease(is_date_decrease, is_date_increase, qos): diff --git a/Cinder/Rocky/customization_driver.py b/Cinder/Rocky/customization_driver.py new file mode 100644 index 0000000..8709ee8 --- /dev/null +++ b/Cinder/Rocky/customization_driver.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023 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 logging + +from cinder.volume.drivers.fusionstorage import fs_utils + +LOG = logging.getLogger(__name__) + + +class DriverForZTE(object): + """ + ZTE Cloud Platform Customization Class + """ + def __init__(self, *args, **kwargs): + super(DriverForZTE, self).__init__(*args, **kwargs) + + def reload_qos(self, volume, qos_vals=None): + """ + ZTE Cloud Platform Customization Interface + QoS policies can be dynamically modified,remove,add + and take effect on volumes in real time. + """ + self._check_volume_exist_on_array(volume) + volume_name = self._get_vol_name(volume) + if not qos_vals: + LOG.info("qos_vals is None, remove qos from volume %s", volume_name) + self.fs_qos.remove(volume_name) + return + + qos_vals = fs_utils.get_qos_param(qos_vals, self.client) + vol_qos = self.client.get_qos_by_vol_name(volume_name) + qos_name = vol_qos.get("qosName") + if qos_name: + LOG.info("volume already had qos, " + "update qos:%s of volume %s", qos_name, volume_name) + self.client.modify_qos(qos_name, qos_vals) + return + + LOG.info("volume did not have qos, " + "add qos to volume %s", volume_name) + self.fs_qos.add(qos_vals, volume_name) + return diff --git a/Cinder/Rocky/dsware.py b/Cinder/Rocky/dsware.py index e53053a..72fc4cf 100644 --- a/Cinder/Rocky/dsware.py +++ b/Cinder/Rocky/dsware.py @@ -37,6 +37,7 @@ from cinder.volume.drivers.fusionstorage import fs_flow from cinder.volume.drivers.fusionstorage import fs_qos from cinder.volume.drivers.fusionstorage import fs_utils +from cinder.volume.drivers.fusionstorage import customization_driver from cinder.volume.drivers.san import san from cinder.volume import utils as volume_utils @@ -114,6 +115,9 @@ cfg.BoolOpt('full_clone', default=False, help='Whether use full clone.'), + cfg.IntOpt('rest_timeout', + default=constants.DEFAULT_TIMEOUT, + help='timeout when call storage restful api.'), ] CONF = cfg.CONF @@ -121,7 +125,8 @@ @interface.volumedriver -class DSWAREBaseDriver(driver.VolumeDriver): +class DSWAREBaseDriver(customization_driver.DriverForZTE, + driver.VolumeDriver): VERSION = "2.6.2" CI_WIKI_NAME = 'Huawei_FusionStorage_CI' @@ -160,7 +165,8 @@ def do_setup(self, context): } extend_conf = { - "mutual_authentication": mutual_authentication + "mutual_authentication": mutual_authentication, + "rest_timeout": self.configuration.rest_timeout } self.client = fs_client.RestCommon(fs_address=url_str, @@ -171,7 +177,7 @@ def do_setup(self, context): self.fs_qos = fs_qos.FusionStorageQoS(self.client) def check_for_setup_error(self): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() all_pools_name = [p['poolName'] for p in all_pools if p.get('poolName')] @@ -190,7 +196,7 @@ def _update_pool_stats(self): "pools": [], "vendor_name": "Huawei" } - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool['poolName'] in self.configuration.pools_name: @@ -264,7 +270,7 @@ def _get_pool_id(self, volume): def _get_pool_id_by_name(self, pool_name): pool_id_list = [] - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool_name == pool['poolName']: pool_id_list.append(pool['poolId']) @@ -716,6 +722,7 @@ def retype(self, context, volume, new_type, diff, host): volume, new_type, host, vol_name) if migrate: src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) LOG.debug("Begin to migrate LUN(id: %(lun_id)s) with " "change %(change_opts)s.", {"lun_id": src_lun_id, "change_opts": change_opts}) @@ -735,6 +742,7 @@ def migrate_volume(self, context, volume, host): {"volume": volume.id, "host": host}) src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) moved = self._migrate_volume(volume, host, src_lun_id) return moved, {} @@ -860,7 +868,6 @@ def _check_migration_valid(self, host): return True def _check_volume_exist_on_array(self, volume): - volume_name = self._get_vol_name(volume) result = self._check_volume_exist(volume) if not result: msg = _("Volume %s does not exist on the array." @@ -877,11 +884,14 @@ def _check_volume_exist_on_array(self, volume): ) % volume.id self._raise_exception(msg) + return lun_id + + def _check_volume_snapshot_exist(self, volume): + volume_name = self._get_vol_name(volume) if self.client.get_volume_snapshot(volume_name): msg = _("Volume %s which have snapshot cannot do lun migration" ) % volume.id self._raise_exception(msg) - return lun_id def _rollback_snapshot(self, vol_name, snap_name): def _snapshot_rollback_finish(): diff --git a/Cinder/Rocky/fs_client.py b/Cinder/Rocky/fs_client.py index 3ebf26f..ec2fb06 100644 --- a/Cinder/Rocky/fs_client.py +++ b/Cinder/Rocky/fs_client.py @@ -44,6 +44,8 @@ def __init__(self, fs_address, fs_user, fs_password, **extend_conf): self.token = None self.version = None self.esn = None + self.rest_timeout = extend_conf.get( + "rest_timeout", constants.DEFAULT_TIMEOUT) mutual_authentication = extend_conf.get("mutual_authentication", {}) self.init_http_head(mutual_authentication) @@ -93,11 +95,13 @@ def _deal_call_result(result, filter_flag, json_flag, req_dict): return result.json() if json_flag else result def call(self, url, method, data=None, - call_timeout=constants.DEFAULT_TIMEOUT, **input_kwargs): + call_timeout=None, **input_kwargs): filter_flag = input_kwargs.get("filter_flag") json_flag = input_kwargs.get("json_flag", True) get_version = input_kwargs.get("get_version") get_system_time = input_kwargs.get("get_system_time") + if call_timeout is None: + call_timeout = self.rest_timeout kwargs = {'timeout': call_timeout} if data is not None: @@ -110,7 +114,7 @@ def call(self, url, method, data=None, result = func(call_url, **kwargs) except Exception as err: LOG.error('Bad response from server: %(url)s. ' - 'Error: %(err)s'), {'url': call_url, 'err': err} + 'Error: %(err)s', {'url': call_url, 'err': err}) return {"error": { "code": constants.CONNECT_ERROR, "description": "Connect to server error."}} @@ -201,6 +205,12 @@ def query_pool_info(self, pool_id=None): self._assert_rest_result(result, _("Query pool session error.")) return result['storagePools'] + def query_storage_pool_info(self): + url = "/cluster/storagepool/queryStoragePool" + result = self.call(url, 'GET', get_version=True, filter_flag=True) + self._assert_rest_result(result, _("Query pool session error.")) + return result.get('storagePools', []) + def _get_volume_num_by_pool(self, pool_id): pool_info = self.query_pool_info(pool_id) return pool_info[0].get('volumeNum', 0) diff --git a/Cinder/Rocky/fs_qos.py b/Cinder/Rocky/fs_qos.py index 286dbc3..6ed8bb3 100644 --- a/Cinder/Rocky/fs_qos.py +++ b/Cinder/Rocky/fs_qos.py @@ -42,7 +42,7 @@ def add(self, qos, vol_name): raise def _is_qos_associate_to_volume(self, qos_name): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() volumes = None for pool in all_pools: volumes = self.client.get_qos_volume_info( diff --git a/Cinder/Rocky/fs_utils.py b/Cinder/Rocky/fs_utils.py index 0856038..5bcfdf3 100644 --- a/Cinder/Rocky/fs_utils.py +++ b/Cinder/Rocky/fs_utils.py @@ -217,16 +217,20 @@ def _get_qos_specs(qos_specs_id, client): kvs = specs.get('specs', {}) LOG.info('The QoS specs is: %s.', kvs) - qos = dict() - for k, v in kvs.items(): + return get_qos_param(kvs, client) + + +def get_qos_param(qos_vals, client): + qos_param = dict() + for k, v in qos_vals.items(): _raise_qos_is_invalid(k) - qos = _set_qos(qos, k, v) + qos_param = _set_qos(qos_param, k, v) - _raise_qos_not_set(qos) - _set_default_qos(qos) - qos = _get_trigger_qos(qos, client) + _raise_qos_not_set(qos_param) + _set_default_qos(qos_param) + qos_param = _get_trigger_qos(qos_param, client) - return qos + return qos_param def _deal_date_increase_or_decrease(is_date_decrease, is_date_increase, qos): diff --git a/Cinder/Stein/customization_driver.py b/Cinder/Stein/customization_driver.py new file mode 100644 index 0000000..8709ee8 --- /dev/null +++ b/Cinder/Stein/customization_driver.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023 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 logging + +from cinder.volume.drivers.fusionstorage import fs_utils + +LOG = logging.getLogger(__name__) + + +class DriverForZTE(object): + """ + ZTE Cloud Platform Customization Class + """ + def __init__(self, *args, **kwargs): + super(DriverForZTE, self).__init__(*args, **kwargs) + + def reload_qos(self, volume, qos_vals=None): + """ + ZTE Cloud Platform Customization Interface + QoS policies can be dynamically modified,remove,add + and take effect on volumes in real time. + """ + self._check_volume_exist_on_array(volume) + volume_name = self._get_vol_name(volume) + if not qos_vals: + LOG.info("qos_vals is None, remove qos from volume %s", volume_name) + self.fs_qos.remove(volume_name) + return + + qos_vals = fs_utils.get_qos_param(qos_vals, self.client) + vol_qos = self.client.get_qos_by_vol_name(volume_name) + qos_name = vol_qos.get("qosName") + if qos_name: + LOG.info("volume already had qos, " + "update qos:%s of volume %s", qos_name, volume_name) + self.client.modify_qos(qos_name, qos_vals) + return + + LOG.info("volume did not have qos, " + "add qos to volume %s", volume_name) + self.fs_qos.add(qos_vals, volume_name) + return diff --git a/Cinder/Stein/dsware.py b/Cinder/Stein/dsware.py index e53053a..72fc4cf 100644 --- a/Cinder/Stein/dsware.py +++ b/Cinder/Stein/dsware.py @@ -37,6 +37,7 @@ from cinder.volume.drivers.fusionstorage import fs_flow from cinder.volume.drivers.fusionstorage import fs_qos from cinder.volume.drivers.fusionstorage import fs_utils +from cinder.volume.drivers.fusionstorage import customization_driver from cinder.volume.drivers.san import san from cinder.volume import utils as volume_utils @@ -114,6 +115,9 @@ cfg.BoolOpt('full_clone', default=False, help='Whether use full clone.'), + cfg.IntOpt('rest_timeout', + default=constants.DEFAULT_TIMEOUT, + help='timeout when call storage restful api.'), ] CONF = cfg.CONF @@ -121,7 +125,8 @@ @interface.volumedriver -class DSWAREBaseDriver(driver.VolumeDriver): +class DSWAREBaseDriver(customization_driver.DriverForZTE, + driver.VolumeDriver): VERSION = "2.6.2" CI_WIKI_NAME = 'Huawei_FusionStorage_CI' @@ -160,7 +165,8 @@ def do_setup(self, context): } extend_conf = { - "mutual_authentication": mutual_authentication + "mutual_authentication": mutual_authentication, + "rest_timeout": self.configuration.rest_timeout } self.client = fs_client.RestCommon(fs_address=url_str, @@ -171,7 +177,7 @@ def do_setup(self, context): self.fs_qos = fs_qos.FusionStorageQoS(self.client) def check_for_setup_error(self): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() all_pools_name = [p['poolName'] for p in all_pools if p.get('poolName')] @@ -190,7 +196,7 @@ def _update_pool_stats(self): "pools": [], "vendor_name": "Huawei" } - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool['poolName'] in self.configuration.pools_name: @@ -264,7 +270,7 @@ def _get_pool_id(self, volume): def _get_pool_id_by_name(self, pool_name): pool_id_list = [] - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool_name == pool['poolName']: pool_id_list.append(pool['poolId']) @@ -716,6 +722,7 @@ def retype(self, context, volume, new_type, diff, host): volume, new_type, host, vol_name) if migrate: src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) LOG.debug("Begin to migrate LUN(id: %(lun_id)s) with " "change %(change_opts)s.", {"lun_id": src_lun_id, "change_opts": change_opts}) @@ -735,6 +742,7 @@ def migrate_volume(self, context, volume, host): {"volume": volume.id, "host": host}) src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) moved = self._migrate_volume(volume, host, src_lun_id) return moved, {} @@ -860,7 +868,6 @@ def _check_migration_valid(self, host): return True def _check_volume_exist_on_array(self, volume): - volume_name = self._get_vol_name(volume) result = self._check_volume_exist(volume) if not result: msg = _("Volume %s does not exist on the array." @@ -877,11 +884,14 @@ def _check_volume_exist_on_array(self, volume): ) % volume.id self._raise_exception(msg) + return lun_id + + def _check_volume_snapshot_exist(self, volume): + volume_name = self._get_vol_name(volume) if self.client.get_volume_snapshot(volume_name): msg = _("Volume %s which have snapshot cannot do lun migration" ) % volume.id self._raise_exception(msg) - return lun_id def _rollback_snapshot(self, vol_name, snap_name): def _snapshot_rollback_finish(): diff --git a/Cinder/Stein/fs_client.py b/Cinder/Stein/fs_client.py index 3ebf26f..ec2fb06 100644 --- a/Cinder/Stein/fs_client.py +++ b/Cinder/Stein/fs_client.py @@ -44,6 +44,8 @@ def __init__(self, fs_address, fs_user, fs_password, **extend_conf): self.token = None self.version = None self.esn = None + self.rest_timeout = extend_conf.get( + "rest_timeout", constants.DEFAULT_TIMEOUT) mutual_authentication = extend_conf.get("mutual_authentication", {}) self.init_http_head(mutual_authentication) @@ -93,11 +95,13 @@ def _deal_call_result(result, filter_flag, json_flag, req_dict): return result.json() if json_flag else result def call(self, url, method, data=None, - call_timeout=constants.DEFAULT_TIMEOUT, **input_kwargs): + call_timeout=None, **input_kwargs): filter_flag = input_kwargs.get("filter_flag") json_flag = input_kwargs.get("json_flag", True) get_version = input_kwargs.get("get_version") get_system_time = input_kwargs.get("get_system_time") + if call_timeout is None: + call_timeout = self.rest_timeout kwargs = {'timeout': call_timeout} if data is not None: @@ -110,7 +114,7 @@ def call(self, url, method, data=None, result = func(call_url, **kwargs) except Exception as err: LOG.error('Bad response from server: %(url)s. ' - 'Error: %(err)s'), {'url': call_url, 'err': err} + 'Error: %(err)s', {'url': call_url, 'err': err}) return {"error": { "code": constants.CONNECT_ERROR, "description": "Connect to server error."}} @@ -201,6 +205,12 @@ def query_pool_info(self, pool_id=None): self._assert_rest_result(result, _("Query pool session error.")) return result['storagePools'] + def query_storage_pool_info(self): + url = "/cluster/storagepool/queryStoragePool" + result = self.call(url, 'GET', get_version=True, filter_flag=True) + self._assert_rest_result(result, _("Query pool session error.")) + return result.get('storagePools', []) + def _get_volume_num_by_pool(self, pool_id): pool_info = self.query_pool_info(pool_id) return pool_info[0].get('volumeNum', 0) diff --git a/Cinder/Stein/fs_qos.py b/Cinder/Stein/fs_qos.py index 286dbc3..6ed8bb3 100644 --- a/Cinder/Stein/fs_qos.py +++ b/Cinder/Stein/fs_qos.py @@ -42,7 +42,7 @@ def add(self, qos, vol_name): raise def _is_qos_associate_to_volume(self, qos_name): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() volumes = None for pool in all_pools: volumes = self.client.get_qos_volume_info( diff --git a/Cinder/Stein/fs_utils.py b/Cinder/Stein/fs_utils.py index 0856038..5bcfdf3 100644 --- a/Cinder/Stein/fs_utils.py +++ b/Cinder/Stein/fs_utils.py @@ -217,16 +217,20 @@ def _get_qos_specs(qos_specs_id, client): kvs = specs.get('specs', {}) LOG.info('The QoS specs is: %s.', kvs) - qos = dict() - for k, v in kvs.items(): + return get_qos_param(kvs, client) + + +def get_qos_param(qos_vals, client): + qos_param = dict() + for k, v in qos_vals.items(): _raise_qos_is_invalid(k) - qos = _set_qos(qos, k, v) + qos_param = _set_qos(qos_param, k, v) - _raise_qos_not_set(qos) - _set_default_qos(qos) - qos = _get_trigger_qos(qos, client) + _raise_qos_not_set(qos_param) + _set_default_qos(qos_param) + qos_param = _get_trigger_qos(qos_param, client) - return qos + return qos_param def _deal_date_increase_or_decrease(is_date_decrease, is_date_increase, qos): diff --git a/Cinder/Train/customization_driver.py b/Cinder/Train/customization_driver.py new file mode 100644 index 0000000..8709ee8 --- /dev/null +++ b/Cinder/Train/customization_driver.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023 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 logging + +from cinder.volume.drivers.fusionstorage import fs_utils + +LOG = logging.getLogger(__name__) + + +class DriverForZTE(object): + """ + ZTE Cloud Platform Customization Class + """ + def __init__(self, *args, **kwargs): + super(DriverForZTE, self).__init__(*args, **kwargs) + + def reload_qos(self, volume, qos_vals=None): + """ + ZTE Cloud Platform Customization Interface + QoS policies can be dynamically modified,remove,add + and take effect on volumes in real time. + """ + self._check_volume_exist_on_array(volume) + volume_name = self._get_vol_name(volume) + if not qos_vals: + LOG.info("qos_vals is None, remove qos from volume %s", volume_name) + self.fs_qos.remove(volume_name) + return + + qos_vals = fs_utils.get_qos_param(qos_vals, self.client) + vol_qos = self.client.get_qos_by_vol_name(volume_name) + qos_name = vol_qos.get("qosName") + if qos_name: + LOG.info("volume already had qos, " + "update qos:%s of volume %s", qos_name, volume_name) + self.client.modify_qos(qos_name, qos_vals) + return + + LOG.info("volume did not have qos, " + "add qos to volume %s", volume_name) + self.fs_qos.add(qos_vals, volume_name) + return diff --git a/Cinder/Train/dsware.py b/Cinder/Train/dsware.py index e6d6c40..e1b61ce 100644 --- a/Cinder/Train/dsware.py +++ b/Cinder/Train/dsware.py @@ -37,6 +37,7 @@ from cinder.volume.drivers.fusionstorage import fs_flow from cinder.volume.drivers.fusionstorage import fs_qos from cinder.volume.drivers.fusionstorage import fs_utils +from cinder.volume.drivers.fusionstorage import customization_driver from cinder.volume.drivers.san import san from cinder.volume import volume_utils @@ -114,6 +115,9 @@ cfg.BoolOpt('full_clone', default=False, help='Whether use full clone.'), + cfg.IntOpt('rest_timeout', + default=constants.DEFAULT_TIMEOUT, + help='timeout when call storage restful api.'), ] CONF = cfg.CONF @@ -121,7 +125,8 @@ @interface.volumedriver -class DSWAREBaseDriver(driver.VolumeDriver): +class DSWAREBaseDriver(customization_driver.DriverForZTE, + driver.VolumeDriver): VERSION = "2.6.2" CI_WIKI_NAME = 'Huawei_FusionStorage_CI' @@ -160,7 +165,8 @@ def do_setup(self, context): } extend_conf = { - "mutual_authentication": mutual_authentication + "mutual_authentication": mutual_authentication, + "rest_timeout": self.configuration.rest_timeout } self.client = fs_client.RestCommon(fs_address=url_str, @@ -171,7 +177,7 @@ def do_setup(self, context): self.fs_qos = fs_qos.FusionStorageQoS(self.client) def check_for_setup_error(self): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() all_pools_name = [p['poolName'] for p in all_pools if p.get('poolName')] @@ -190,7 +196,7 @@ def _update_pool_stats(self): "pools": [], "vendor_name": "Huawei" } - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool['poolName'] in self.configuration.pools_name: @@ -264,7 +270,7 @@ def _get_pool_id(self, volume): def _get_pool_id_by_name(self, pool_name): pool_id_list = [] - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool_name == pool['poolName']: pool_id_list.append(pool['poolId']) @@ -716,6 +722,7 @@ def retype(self, context, volume, new_type, diff, host): volume, new_type, host, vol_name) if migrate: src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) LOG.debug("Begin to migrate LUN(id: %(lun_id)s) with " "change %(change_opts)s.", {"lun_id": src_lun_id, "change_opts": change_opts}) @@ -735,6 +742,7 @@ def migrate_volume(self, context, volume, host): {"volume": volume.id, "host": host}) src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) moved = self._migrate_volume(volume, host, src_lun_id) return moved, {} @@ -860,7 +868,6 @@ def _check_migration_valid(self, host): return True def _check_volume_exist_on_array(self, volume): - volume_name = self._get_vol_name(volume) result = self._check_volume_exist(volume) if not result: msg = _("Volume %s does not exist on the array." @@ -877,11 +884,14 @@ def _check_volume_exist_on_array(self, volume): ) % volume.id self._raise_exception(msg) + return lun_id + + def _check_volume_snapshot_exist(self, volume): + volume_name = self._get_vol_name(volume) if self.client.get_volume_snapshot(volume_name): msg = _("Volume %s which have snapshot cannot do lun migration" ) % volume.id self._raise_exception(msg) - return lun_id def _rollback_snapshot(self, vol_name, snap_name): def _snapshot_rollback_finish(): diff --git a/Cinder/Train/fs_client.py b/Cinder/Train/fs_client.py index 3ebf26f..ec2fb06 100644 --- a/Cinder/Train/fs_client.py +++ b/Cinder/Train/fs_client.py @@ -44,6 +44,8 @@ def __init__(self, fs_address, fs_user, fs_password, **extend_conf): self.token = None self.version = None self.esn = None + self.rest_timeout = extend_conf.get( + "rest_timeout", constants.DEFAULT_TIMEOUT) mutual_authentication = extend_conf.get("mutual_authentication", {}) self.init_http_head(mutual_authentication) @@ -93,11 +95,13 @@ def _deal_call_result(result, filter_flag, json_flag, req_dict): return result.json() if json_flag else result def call(self, url, method, data=None, - call_timeout=constants.DEFAULT_TIMEOUT, **input_kwargs): + call_timeout=None, **input_kwargs): filter_flag = input_kwargs.get("filter_flag") json_flag = input_kwargs.get("json_flag", True) get_version = input_kwargs.get("get_version") get_system_time = input_kwargs.get("get_system_time") + if call_timeout is None: + call_timeout = self.rest_timeout kwargs = {'timeout': call_timeout} if data is not None: @@ -110,7 +114,7 @@ def call(self, url, method, data=None, result = func(call_url, **kwargs) except Exception as err: LOG.error('Bad response from server: %(url)s. ' - 'Error: %(err)s'), {'url': call_url, 'err': err} + 'Error: %(err)s', {'url': call_url, 'err': err}) return {"error": { "code": constants.CONNECT_ERROR, "description": "Connect to server error."}} @@ -201,6 +205,12 @@ def query_pool_info(self, pool_id=None): self._assert_rest_result(result, _("Query pool session error.")) return result['storagePools'] + def query_storage_pool_info(self): + url = "/cluster/storagepool/queryStoragePool" + result = self.call(url, 'GET', get_version=True, filter_flag=True) + self._assert_rest_result(result, _("Query pool session error.")) + return result.get('storagePools', []) + def _get_volume_num_by_pool(self, pool_id): pool_info = self.query_pool_info(pool_id) return pool_info[0].get('volumeNum', 0) diff --git a/Cinder/Train/fs_qos.py b/Cinder/Train/fs_qos.py index 286dbc3..6ed8bb3 100644 --- a/Cinder/Train/fs_qos.py +++ b/Cinder/Train/fs_qos.py @@ -42,7 +42,7 @@ def add(self, qos, vol_name): raise def _is_qos_associate_to_volume(self, qos_name): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() volumes = None for pool in all_pools: volumes = self.client.get_qos_volume_info( diff --git a/Cinder/Train/fs_utils.py b/Cinder/Train/fs_utils.py index 0856038..5bcfdf3 100644 --- a/Cinder/Train/fs_utils.py +++ b/Cinder/Train/fs_utils.py @@ -217,16 +217,20 @@ def _get_qos_specs(qos_specs_id, client): kvs = specs.get('specs', {}) LOG.info('The QoS specs is: %s.', kvs) - qos = dict() - for k, v in kvs.items(): + return get_qos_param(kvs, client) + + +def get_qos_param(qos_vals, client): + qos_param = dict() + for k, v in qos_vals.items(): _raise_qos_is_invalid(k) - qos = _set_qos(qos, k, v) + qos_param = _set_qos(qos_param, k, v) - _raise_qos_not_set(qos) - _set_default_qos(qos) - qos = _get_trigger_qos(qos, client) + _raise_qos_not_set(qos_param) + _set_default_qos(qos_param) + qos_param = _get_trigger_qos(qos_param, client) - return qos + return qos_param def _deal_date_increase_or_decrease(is_date_decrease, is_date_increase, qos): diff --git a/Cinder/Ussuri/customization_driver.py b/Cinder/Ussuri/customization_driver.py new file mode 100644 index 0000000..8709ee8 --- /dev/null +++ b/Cinder/Ussuri/customization_driver.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023 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 logging + +from cinder.volume.drivers.fusionstorage import fs_utils + +LOG = logging.getLogger(__name__) + + +class DriverForZTE(object): + """ + ZTE Cloud Platform Customization Class + """ + def __init__(self, *args, **kwargs): + super(DriverForZTE, self).__init__(*args, **kwargs) + + def reload_qos(self, volume, qos_vals=None): + """ + ZTE Cloud Platform Customization Interface + QoS policies can be dynamically modified,remove,add + and take effect on volumes in real time. + """ + self._check_volume_exist_on_array(volume) + volume_name = self._get_vol_name(volume) + if not qos_vals: + LOG.info("qos_vals is None, remove qos from volume %s", volume_name) + self.fs_qos.remove(volume_name) + return + + qos_vals = fs_utils.get_qos_param(qos_vals, self.client) + vol_qos = self.client.get_qos_by_vol_name(volume_name) + qos_name = vol_qos.get("qosName") + if qos_name: + LOG.info("volume already had qos, " + "update qos:%s of volume %s", qos_name, volume_name) + self.client.modify_qos(qos_name, qos_vals) + return + + LOG.info("volume did not have qos, " + "add qos to volume %s", volume_name) + self.fs_qos.add(qos_vals, volume_name) + return diff --git a/Cinder/Ussuri/dsware.py b/Cinder/Ussuri/dsware.py index e6d6c40..e1b61ce 100644 --- a/Cinder/Ussuri/dsware.py +++ b/Cinder/Ussuri/dsware.py @@ -37,6 +37,7 @@ from cinder.volume.drivers.fusionstorage import fs_flow from cinder.volume.drivers.fusionstorage import fs_qos from cinder.volume.drivers.fusionstorage import fs_utils +from cinder.volume.drivers.fusionstorage import customization_driver from cinder.volume.drivers.san import san from cinder.volume import volume_utils @@ -114,6 +115,9 @@ cfg.BoolOpt('full_clone', default=False, help='Whether use full clone.'), + cfg.IntOpt('rest_timeout', + default=constants.DEFAULT_TIMEOUT, + help='timeout when call storage restful api.'), ] CONF = cfg.CONF @@ -121,7 +125,8 @@ @interface.volumedriver -class DSWAREBaseDriver(driver.VolumeDriver): +class DSWAREBaseDriver(customization_driver.DriverForZTE, + driver.VolumeDriver): VERSION = "2.6.2" CI_WIKI_NAME = 'Huawei_FusionStorage_CI' @@ -160,7 +165,8 @@ def do_setup(self, context): } extend_conf = { - "mutual_authentication": mutual_authentication + "mutual_authentication": mutual_authentication, + "rest_timeout": self.configuration.rest_timeout } self.client = fs_client.RestCommon(fs_address=url_str, @@ -171,7 +177,7 @@ def do_setup(self, context): self.fs_qos = fs_qos.FusionStorageQoS(self.client) def check_for_setup_error(self): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() all_pools_name = [p['poolName'] for p in all_pools if p.get('poolName')] @@ -190,7 +196,7 @@ def _update_pool_stats(self): "pools": [], "vendor_name": "Huawei" } - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool['poolName'] in self.configuration.pools_name: @@ -264,7 +270,7 @@ def _get_pool_id(self, volume): def _get_pool_id_by_name(self, pool_name): pool_id_list = [] - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool_name == pool['poolName']: pool_id_list.append(pool['poolId']) @@ -716,6 +722,7 @@ def retype(self, context, volume, new_type, diff, host): volume, new_type, host, vol_name) if migrate: src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) LOG.debug("Begin to migrate LUN(id: %(lun_id)s) with " "change %(change_opts)s.", {"lun_id": src_lun_id, "change_opts": change_opts}) @@ -735,6 +742,7 @@ def migrate_volume(self, context, volume, host): {"volume": volume.id, "host": host}) src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) moved = self._migrate_volume(volume, host, src_lun_id) return moved, {} @@ -860,7 +868,6 @@ def _check_migration_valid(self, host): return True def _check_volume_exist_on_array(self, volume): - volume_name = self._get_vol_name(volume) result = self._check_volume_exist(volume) if not result: msg = _("Volume %s does not exist on the array." @@ -877,11 +884,14 @@ def _check_volume_exist_on_array(self, volume): ) % volume.id self._raise_exception(msg) + return lun_id + + def _check_volume_snapshot_exist(self, volume): + volume_name = self._get_vol_name(volume) if self.client.get_volume_snapshot(volume_name): msg = _("Volume %s which have snapshot cannot do lun migration" ) % volume.id self._raise_exception(msg) - return lun_id def _rollback_snapshot(self, vol_name, snap_name): def _snapshot_rollback_finish(): diff --git a/Cinder/Ussuri/fs_client.py b/Cinder/Ussuri/fs_client.py index 3ebf26f..ec2fb06 100644 --- a/Cinder/Ussuri/fs_client.py +++ b/Cinder/Ussuri/fs_client.py @@ -44,6 +44,8 @@ def __init__(self, fs_address, fs_user, fs_password, **extend_conf): self.token = None self.version = None self.esn = None + self.rest_timeout = extend_conf.get( + "rest_timeout", constants.DEFAULT_TIMEOUT) mutual_authentication = extend_conf.get("mutual_authentication", {}) self.init_http_head(mutual_authentication) @@ -93,11 +95,13 @@ def _deal_call_result(result, filter_flag, json_flag, req_dict): return result.json() if json_flag else result def call(self, url, method, data=None, - call_timeout=constants.DEFAULT_TIMEOUT, **input_kwargs): + call_timeout=None, **input_kwargs): filter_flag = input_kwargs.get("filter_flag") json_flag = input_kwargs.get("json_flag", True) get_version = input_kwargs.get("get_version") get_system_time = input_kwargs.get("get_system_time") + if call_timeout is None: + call_timeout = self.rest_timeout kwargs = {'timeout': call_timeout} if data is not None: @@ -110,7 +114,7 @@ def call(self, url, method, data=None, result = func(call_url, **kwargs) except Exception as err: LOG.error('Bad response from server: %(url)s. ' - 'Error: %(err)s'), {'url': call_url, 'err': err} + 'Error: %(err)s', {'url': call_url, 'err': err}) return {"error": { "code": constants.CONNECT_ERROR, "description": "Connect to server error."}} @@ -201,6 +205,12 @@ def query_pool_info(self, pool_id=None): self._assert_rest_result(result, _("Query pool session error.")) return result['storagePools'] + def query_storage_pool_info(self): + url = "/cluster/storagepool/queryStoragePool" + result = self.call(url, 'GET', get_version=True, filter_flag=True) + self._assert_rest_result(result, _("Query pool session error.")) + return result.get('storagePools', []) + def _get_volume_num_by_pool(self, pool_id): pool_info = self.query_pool_info(pool_id) return pool_info[0].get('volumeNum', 0) diff --git a/Cinder/Ussuri/fs_qos.py b/Cinder/Ussuri/fs_qos.py index 286dbc3..6ed8bb3 100644 --- a/Cinder/Ussuri/fs_qos.py +++ b/Cinder/Ussuri/fs_qos.py @@ -42,7 +42,7 @@ def add(self, qos, vol_name): raise def _is_qos_associate_to_volume(self, qos_name): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() volumes = None for pool in all_pools: volumes = self.client.get_qos_volume_info( diff --git a/Cinder/Ussuri/fs_utils.py b/Cinder/Ussuri/fs_utils.py index 0856038..5bcfdf3 100644 --- a/Cinder/Ussuri/fs_utils.py +++ b/Cinder/Ussuri/fs_utils.py @@ -217,16 +217,20 @@ def _get_qos_specs(qos_specs_id, client): kvs = specs.get('specs', {}) LOG.info('The QoS specs is: %s.', kvs) - qos = dict() - for k, v in kvs.items(): + return get_qos_param(kvs, client) + + +def get_qos_param(qos_vals, client): + qos_param = dict() + for k, v in qos_vals.items(): _raise_qos_is_invalid(k) - qos = _set_qos(qos, k, v) + qos_param = _set_qos(qos_param, k, v) - _raise_qos_not_set(qos) - _set_default_qos(qos) - qos = _get_trigger_qos(qos, client) + _raise_qos_not_set(qos_param) + _set_default_qos(qos_param) + qos_param = _get_trigger_qos(qos_param, client) - return qos + return qos_param def _deal_date_increase_or_decrease(is_date_decrease, is_date_increase, qos): diff --git a/Cinder/Victoria/customization_driver.py b/Cinder/Victoria/customization_driver.py new file mode 100644 index 0000000..8709ee8 --- /dev/null +++ b/Cinder/Victoria/customization_driver.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023 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 logging + +from cinder.volume.drivers.fusionstorage import fs_utils + +LOG = logging.getLogger(__name__) + + +class DriverForZTE(object): + """ + ZTE Cloud Platform Customization Class + """ + def __init__(self, *args, **kwargs): + super(DriverForZTE, self).__init__(*args, **kwargs) + + def reload_qos(self, volume, qos_vals=None): + """ + ZTE Cloud Platform Customization Interface + QoS policies can be dynamically modified,remove,add + and take effect on volumes in real time. + """ + self._check_volume_exist_on_array(volume) + volume_name = self._get_vol_name(volume) + if not qos_vals: + LOG.info("qos_vals is None, remove qos from volume %s", volume_name) + self.fs_qos.remove(volume_name) + return + + qos_vals = fs_utils.get_qos_param(qos_vals, self.client) + vol_qos = self.client.get_qos_by_vol_name(volume_name) + qos_name = vol_qos.get("qosName") + if qos_name: + LOG.info("volume already had qos, " + "update qos:%s of volume %s", qos_name, volume_name) + self.client.modify_qos(qos_name, qos_vals) + return + + LOG.info("volume did not have qos, " + "add qos to volume %s", volume_name) + self.fs_qos.add(qos_vals, volume_name) + return diff --git a/Cinder/Victoria/dsware.py b/Cinder/Victoria/dsware.py index e6d6c40..e1b61ce 100644 --- a/Cinder/Victoria/dsware.py +++ b/Cinder/Victoria/dsware.py @@ -37,6 +37,7 @@ from cinder.volume.drivers.fusionstorage import fs_flow from cinder.volume.drivers.fusionstorage import fs_qos from cinder.volume.drivers.fusionstorage import fs_utils +from cinder.volume.drivers.fusionstorage import customization_driver from cinder.volume.drivers.san import san from cinder.volume import volume_utils @@ -114,6 +115,9 @@ cfg.BoolOpt('full_clone', default=False, help='Whether use full clone.'), + cfg.IntOpt('rest_timeout', + default=constants.DEFAULT_TIMEOUT, + help='timeout when call storage restful api.'), ] CONF = cfg.CONF @@ -121,7 +125,8 @@ @interface.volumedriver -class DSWAREBaseDriver(driver.VolumeDriver): +class DSWAREBaseDriver(customization_driver.DriverForZTE, + driver.VolumeDriver): VERSION = "2.6.2" CI_WIKI_NAME = 'Huawei_FusionStorage_CI' @@ -160,7 +165,8 @@ def do_setup(self, context): } extend_conf = { - "mutual_authentication": mutual_authentication + "mutual_authentication": mutual_authentication, + "rest_timeout": self.configuration.rest_timeout } self.client = fs_client.RestCommon(fs_address=url_str, @@ -171,7 +177,7 @@ def do_setup(self, context): self.fs_qos = fs_qos.FusionStorageQoS(self.client) def check_for_setup_error(self): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() all_pools_name = [p['poolName'] for p in all_pools if p.get('poolName')] @@ -190,7 +196,7 @@ def _update_pool_stats(self): "pools": [], "vendor_name": "Huawei" } - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool['poolName'] in self.configuration.pools_name: @@ -264,7 +270,7 @@ def _get_pool_id(self, volume): def _get_pool_id_by_name(self, pool_name): pool_id_list = [] - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool_name == pool['poolName']: pool_id_list.append(pool['poolId']) @@ -716,6 +722,7 @@ def retype(self, context, volume, new_type, diff, host): volume, new_type, host, vol_name) if migrate: src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) LOG.debug("Begin to migrate LUN(id: %(lun_id)s) with " "change %(change_opts)s.", {"lun_id": src_lun_id, "change_opts": change_opts}) @@ -735,6 +742,7 @@ def migrate_volume(self, context, volume, host): {"volume": volume.id, "host": host}) src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) moved = self._migrate_volume(volume, host, src_lun_id) return moved, {} @@ -860,7 +868,6 @@ def _check_migration_valid(self, host): return True def _check_volume_exist_on_array(self, volume): - volume_name = self._get_vol_name(volume) result = self._check_volume_exist(volume) if not result: msg = _("Volume %s does not exist on the array." @@ -877,11 +884,14 @@ def _check_volume_exist_on_array(self, volume): ) % volume.id self._raise_exception(msg) + return lun_id + + def _check_volume_snapshot_exist(self, volume): + volume_name = self._get_vol_name(volume) if self.client.get_volume_snapshot(volume_name): msg = _("Volume %s which have snapshot cannot do lun migration" ) % volume.id self._raise_exception(msg) - return lun_id def _rollback_snapshot(self, vol_name, snap_name): def _snapshot_rollback_finish(): diff --git a/Cinder/Victoria/fs_client.py b/Cinder/Victoria/fs_client.py index 3ebf26f..ec2fb06 100644 --- a/Cinder/Victoria/fs_client.py +++ b/Cinder/Victoria/fs_client.py @@ -44,6 +44,8 @@ def __init__(self, fs_address, fs_user, fs_password, **extend_conf): self.token = None self.version = None self.esn = None + self.rest_timeout = extend_conf.get( + "rest_timeout", constants.DEFAULT_TIMEOUT) mutual_authentication = extend_conf.get("mutual_authentication", {}) self.init_http_head(mutual_authentication) @@ -93,11 +95,13 @@ def _deal_call_result(result, filter_flag, json_flag, req_dict): return result.json() if json_flag else result def call(self, url, method, data=None, - call_timeout=constants.DEFAULT_TIMEOUT, **input_kwargs): + call_timeout=None, **input_kwargs): filter_flag = input_kwargs.get("filter_flag") json_flag = input_kwargs.get("json_flag", True) get_version = input_kwargs.get("get_version") get_system_time = input_kwargs.get("get_system_time") + if call_timeout is None: + call_timeout = self.rest_timeout kwargs = {'timeout': call_timeout} if data is not None: @@ -110,7 +114,7 @@ def call(self, url, method, data=None, result = func(call_url, **kwargs) except Exception as err: LOG.error('Bad response from server: %(url)s. ' - 'Error: %(err)s'), {'url': call_url, 'err': err} + 'Error: %(err)s', {'url': call_url, 'err': err}) return {"error": { "code": constants.CONNECT_ERROR, "description": "Connect to server error."}} @@ -201,6 +205,12 @@ def query_pool_info(self, pool_id=None): self._assert_rest_result(result, _("Query pool session error.")) return result['storagePools'] + def query_storage_pool_info(self): + url = "/cluster/storagepool/queryStoragePool" + result = self.call(url, 'GET', get_version=True, filter_flag=True) + self._assert_rest_result(result, _("Query pool session error.")) + return result.get('storagePools', []) + def _get_volume_num_by_pool(self, pool_id): pool_info = self.query_pool_info(pool_id) return pool_info[0].get('volumeNum', 0) diff --git a/Cinder/Victoria/fs_qos.py b/Cinder/Victoria/fs_qos.py index 286dbc3..6ed8bb3 100644 --- a/Cinder/Victoria/fs_qos.py +++ b/Cinder/Victoria/fs_qos.py @@ -42,7 +42,7 @@ def add(self, qos, vol_name): raise def _is_qos_associate_to_volume(self, qos_name): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() volumes = None for pool in all_pools: volumes = self.client.get_qos_volume_info( diff --git a/Cinder/Victoria/fs_utils.py b/Cinder/Victoria/fs_utils.py index 0856038..5bcfdf3 100644 --- a/Cinder/Victoria/fs_utils.py +++ b/Cinder/Victoria/fs_utils.py @@ -217,16 +217,20 @@ def _get_qos_specs(qos_specs_id, client): kvs = specs.get('specs', {}) LOG.info('The QoS specs is: %s.', kvs) - qos = dict() - for k, v in kvs.items(): + return get_qos_param(kvs, client) + + +def get_qos_param(qos_vals, client): + qos_param = dict() + for k, v in qos_vals.items(): _raise_qos_is_invalid(k) - qos = _set_qos(qos, k, v) + qos_param = _set_qos(qos_param, k, v) - _raise_qos_not_set(qos) - _set_default_qos(qos) - qos = _get_trigger_qos(qos, client) + _raise_qos_not_set(qos_param) + _set_default_qos(qos_param) + qos_param = _get_trigger_qos(qos_param, client) - return qos + return qos_param def _deal_date_increase_or_decrease(is_date_decrease, is_date_increase, qos): diff --git a/Cinder/Wallaby/customization_driver.py b/Cinder/Wallaby/customization_driver.py new file mode 100644 index 0000000..8709ee8 --- /dev/null +++ b/Cinder/Wallaby/customization_driver.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023 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 logging + +from cinder.volume.drivers.fusionstorage import fs_utils + +LOG = logging.getLogger(__name__) + + +class DriverForZTE(object): + """ + ZTE Cloud Platform Customization Class + """ + def __init__(self, *args, **kwargs): + super(DriverForZTE, self).__init__(*args, **kwargs) + + def reload_qos(self, volume, qos_vals=None): + """ + ZTE Cloud Platform Customization Interface + QoS policies can be dynamically modified,remove,add + and take effect on volumes in real time. + """ + self._check_volume_exist_on_array(volume) + volume_name = self._get_vol_name(volume) + if not qos_vals: + LOG.info("qos_vals is None, remove qos from volume %s", volume_name) + self.fs_qos.remove(volume_name) + return + + qos_vals = fs_utils.get_qos_param(qos_vals, self.client) + vol_qos = self.client.get_qos_by_vol_name(volume_name) + qos_name = vol_qos.get("qosName") + if qos_name: + LOG.info("volume already had qos, " + "update qos:%s of volume %s", qos_name, volume_name) + self.client.modify_qos(qos_name, qos_vals) + return + + LOG.info("volume did not have qos, " + "add qos to volume %s", volume_name) + self.fs_qos.add(qos_vals, volume_name) + return diff --git a/Cinder/Wallaby/dsware.py b/Cinder/Wallaby/dsware.py index e6d6c40..e1b61ce 100644 --- a/Cinder/Wallaby/dsware.py +++ b/Cinder/Wallaby/dsware.py @@ -37,6 +37,7 @@ from cinder.volume.drivers.fusionstorage import fs_flow from cinder.volume.drivers.fusionstorage import fs_qos from cinder.volume.drivers.fusionstorage import fs_utils +from cinder.volume.drivers.fusionstorage import customization_driver from cinder.volume.drivers.san import san from cinder.volume import volume_utils @@ -114,6 +115,9 @@ cfg.BoolOpt('full_clone', default=False, help='Whether use full clone.'), + cfg.IntOpt('rest_timeout', + default=constants.DEFAULT_TIMEOUT, + help='timeout when call storage restful api.'), ] CONF = cfg.CONF @@ -121,7 +125,8 @@ @interface.volumedriver -class DSWAREBaseDriver(driver.VolumeDriver): +class DSWAREBaseDriver(customization_driver.DriverForZTE, + driver.VolumeDriver): VERSION = "2.6.2" CI_WIKI_NAME = 'Huawei_FusionStorage_CI' @@ -160,7 +165,8 @@ def do_setup(self, context): } extend_conf = { - "mutual_authentication": mutual_authentication + "mutual_authentication": mutual_authentication, + "rest_timeout": self.configuration.rest_timeout } self.client = fs_client.RestCommon(fs_address=url_str, @@ -171,7 +177,7 @@ def do_setup(self, context): self.fs_qos = fs_qos.FusionStorageQoS(self.client) def check_for_setup_error(self): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() all_pools_name = [p['poolName'] for p in all_pools if p.get('poolName')] @@ -190,7 +196,7 @@ def _update_pool_stats(self): "pools": [], "vendor_name": "Huawei" } - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool['poolName'] in self.configuration.pools_name: @@ -264,7 +270,7 @@ def _get_pool_id(self, volume): def _get_pool_id_by_name(self, pool_name): pool_id_list = [] - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool_name == pool['poolName']: pool_id_list.append(pool['poolId']) @@ -716,6 +722,7 @@ def retype(self, context, volume, new_type, diff, host): volume, new_type, host, vol_name) if migrate: src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) LOG.debug("Begin to migrate LUN(id: %(lun_id)s) with " "change %(change_opts)s.", {"lun_id": src_lun_id, "change_opts": change_opts}) @@ -735,6 +742,7 @@ def migrate_volume(self, context, volume, host): {"volume": volume.id, "host": host}) src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) moved = self._migrate_volume(volume, host, src_lun_id) return moved, {} @@ -860,7 +868,6 @@ def _check_migration_valid(self, host): return True def _check_volume_exist_on_array(self, volume): - volume_name = self._get_vol_name(volume) result = self._check_volume_exist(volume) if not result: msg = _("Volume %s does not exist on the array." @@ -877,11 +884,14 @@ def _check_volume_exist_on_array(self, volume): ) % volume.id self._raise_exception(msg) + return lun_id + + def _check_volume_snapshot_exist(self, volume): + volume_name = self._get_vol_name(volume) if self.client.get_volume_snapshot(volume_name): msg = _("Volume %s which have snapshot cannot do lun migration" ) % volume.id self._raise_exception(msg) - return lun_id def _rollback_snapshot(self, vol_name, snap_name): def _snapshot_rollback_finish(): diff --git a/Cinder/Wallaby/fs_client.py b/Cinder/Wallaby/fs_client.py index 3ebf26f..ec2fb06 100644 --- a/Cinder/Wallaby/fs_client.py +++ b/Cinder/Wallaby/fs_client.py @@ -44,6 +44,8 @@ def __init__(self, fs_address, fs_user, fs_password, **extend_conf): self.token = None self.version = None self.esn = None + self.rest_timeout = extend_conf.get( + "rest_timeout", constants.DEFAULT_TIMEOUT) mutual_authentication = extend_conf.get("mutual_authentication", {}) self.init_http_head(mutual_authentication) @@ -93,11 +95,13 @@ def _deal_call_result(result, filter_flag, json_flag, req_dict): return result.json() if json_flag else result def call(self, url, method, data=None, - call_timeout=constants.DEFAULT_TIMEOUT, **input_kwargs): + call_timeout=None, **input_kwargs): filter_flag = input_kwargs.get("filter_flag") json_flag = input_kwargs.get("json_flag", True) get_version = input_kwargs.get("get_version") get_system_time = input_kwargs.get("get_system_time") + if call_timeout is None: + call_timeout = self.rest_timeout kwargs = {'timeout': call_timeout} if data is not None: @@ -110,7 +114,7 @@ def call(self, url, method, data=None, result = func(call_url, **kwargs) except Exception as err: LOG.error('Bad response from server: %(url)s. ' - 'Error: %(err)s'), {'url': call_url, 'err': err} + 'Error: %(err)s', {'url': call_url, 'err': err}) return {"error": { "code": constants.CONNECT_ERROR, "description": "Connect to server error."}} @@ -201,6 +205,12 @@ def query_pool_info(self, pool_id=None): self._assert_rest_result(result, _("Query pool session error.")) return result['storagePools'] + def query_storage_pool_info(self): + url = "/cluster/storagepool/queryStoragePool" + result = self.call(url, 'GET', get_version=True, filter_flag=True) + self._assert_rest_result(result, _("Query pool session error.")) + return result.get('storagePools', []) + def _get_volume_num_by_pool(self, pool_id): pool_info = self.query_pool_info(pool_id) return pool_info[0].get('volumeNum', 0) diff --git a/Cinder/Wallaby/fs_qos.py b/Cinder/Wallaby/fs_qos.py index 286dbc3..6ed8bb3 100644 --- a/Cinder/Wallaby/fs_qos.py +++ b/Cinder/Wallaby/fs_qos.py @@ -42,7 +42,7 @@ def add(self, qos, vol_name): raise def _is_qos_associate_to_volume(self, qos_name): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() volumes = None for pool in all_pools: volumes = self.client.get_qos_volume_info( diff --git a/Cinder/Wallaby/fs_utils.py b/Cinder/Wallaby/fs_utils.py index 0856038..5bcfdf3 100644 --- a/Cinder/Wallaby/fs_utils.py +++ b/Cinder/Wallaby/fs_utils.py @@ -217,16 +217,20 @@ def _get_qos_specs(qos_specs_id, client): kvs = specs.get('specs', {}) LOG.info('The QoS specs is: %s.', kvs) - qos = dict() - for k, v in kvs.items(): + return get_qos_param(kvs, client) + + +def get_qos_param(qos_vals, client): + qos_param = dict() + for k, v in qos_vals.items(): _raise_qos_is_invalid(k) - qos = _set_qos(qos, k, v) + qos_param = _set_qos(qos_param, k, v) - _raise_qos_not_set(qos) - _set_default_qos(qos) - qos = _get_trigger_qos(qos, client) + _raise_qos_not_set(qos_param) + _set_default_qos(qos_param) + qos_param = _get_trigger_qos(qos_param, client) - return qos + return qos_param def _deal_date_increase_or_decrease(is_date_decrease, is_date_increase, qos): diff --git a/Cinder/Xena/customization_driver.py b/Cinder/Xena/customization_driver.py new file mode 100644 index 0000000..8709ee8 --- /dev/null +++ b/Cinder/Xena/customization_driver.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023 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 logging + +from cinder.volume.drivers.fusionstorage import fs_utils + +LOG = logging.getLogger(__name__) + + +class DriverForZTE(object): + """ + ZTE Cloud Platform Customization Class + """ + def __init__(self, *args, **kwargs): + super(DriverForZTE, self).__init__(*args, **kwargs) + + def reload_qos(self, volume, qos_vals=None): + """ + ZTE Cloud Platform Customization Interface + QoS policies can be dynamically modified,remove,add + and take effect on volumes in real time. + """ + self._check_volume_exist_on_array(volume) + volume_name = self._get_vol_name(volume) + if not qos_vals: + LOG.info("qos_vals is None, remove qos from volume %s", volume_name) + self.fs_qos.remove(volume_name) + return + + qos_vals = fs_utils.get_qos_param(qos_vals, self.client) + vol_qos = self.client.get_qos_by_vol_name(volume_name) + qos_name = vol_qos.get("qosName") + if qos_name: + LOG.info("volume already had qos, " + "update qos:%s of volume %s", qos_name, volume_name) + self.client.modify_qos(qos_name, qos_vals) + return + + LOG.info("volume did not have qos, " + "add qos to volume %s", volume_name) + self.fs_qos.add(qos_vals, volume_name) + return diff --git a/Cinder/Xena/dsware.py b/Cinder/Xena/dsware.py index e6d6c40..e1b61ce 100644 --- a/Cinder/Xena/dsware.py +++ b/Cinder/Xena/dsware.py @@ -37,6 +37,7 @@ from cinder.volume.drivers.fusionstorage import fs_flow from cinder.volume.drivers.fusionstorage import fs_qos from cinder.volume.drivers.fusionstorage import fs_utils +from cinder.volume.drivers.fusionstorage import customization_driver from cinder.volume.drivers.san import san from cinder.volume import volume_utils @@ -114,6 +115,9 @@ cfg.BoolOpt('full_clone', default=False, help='Whether use full clone.'), + cfg.IntOpt('rest_timeout', + default=constants.DEFAULT_TIMEOUT, + help='timeout when call storage restful api.'), ] CONF = cfg.CONF @@ -121,7 +125,8 @@ @interface.volumedriver -class DSWAREBaseDriver(driver.VolumeDriver): +class DSWAREBaseDriver(customization_driver.DriverForZTE, + driver.VolumeDriver): VERSION = "2.6.2" CI_WIKI_NAME = 'Huawei_FusionStorage_CI' @@ -160,7 +165,8 @@ def do_setup(self, context): } extend_conf = { - "mutual_authentication": mutual_authentication + "mutual_authentication": mutual_authentication, + "rest_timeout": self.configuration.rest_timeout } self.client = fs_client.RestCommon(fs_address=url_str, @@ -171,7 +177,7 @@ def do_setup(self, context): self.fs_qos = fs_qos.FusionStorageQoS(self.client) def check_for_setup_error(self): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() all_pools_name = [p['poolName'] for p in all_pools if p.get('poolName')] @@ -190,7 +196,7 @@ def _update_pool_stats(self): "pools": [], "vendor_name": "Huawei" } - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool['poolName'] in self.configuration.pools_name: @@ -264,7 +270,7 @@ def _get_pool_id(self, volume): def _get_pool_id_by_name(self, pool_name): pool_id_list = [] - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool_name == pool['poolName']: pool_id_list.append(pool['poolId']) @@ -716,6 +722,7 @@ def retype(self, context, volume, new_type, diff, host): volume, new_type, host, vol_name) if migrate: src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) LOG.debug("Begin to migrate LUN(id: %(lun_id)s) with " "change %(change_opts)s.", {"lun_id": src_lun_id, "change_opts": change_opts}) @@ -735,6 +742,7 @@ def migrate_volume(self, context, volume, host): {"volume": volume.id, "host": host}) src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) moved = self._migrate_volume(volume, host, src_lun_id) return moved, {} @@ -860,7 +868,6 @@ def _check_migration_valid(self, host): return True def _check_volume_exist_on_array(self, volume): - volume_name = self._get_vol_name(volume) result = self._check_volume_exist(volume) if not result: msg = _("Volume %s does not exist on the array." @@ -877,11 +884,14 @@ def _check_volume_exist_on_array(self, volume): ) % volume.id self._raise_exception(msg) + return lun_id + + def _check_volume_snapshot_exist(self, volume): + volume_name = self._get_vol_name(volume) if self.client.get_volume_snapshot(volume_name): msg = _("Volume %s which have snapshot cannot do lun migration" ) % volume.id self._raise_exception(msg) - return lun_id def _rollback_snapshot(self, vol_name, snap_name): def _snapshot_rollback_finish(): diff --git a/Cinder/Xena/fs_client.py b/Cinder/Xena/fs_client.py index 3ebf26f..ec2fb06 100644 --- a/Cinder/Xena/fs_client.py +++ b/Cinder/Xena/fs_client.py @@ -44,6 +44,8 @@ def __init__(self, fs_address, fs_user, fs_password, **extend_conf): self.token = None self.version = None self.esn = None + self.rest_timeout = extend_conf.get( + "rest_timeout", constants.DEFAULT_TIMEOUT) mutual_authentication = extend_conf.get("mutual_authentication", {}) self.init_http_head(mutual_authentication) @@ -93,11 +95,13 @@ def _deal_call_result(result, filter_flag, json_flag, req_dict): return result.json() if json_flag else result def call(self, url, method, data=None, - call_timeout=constants.DEFAULT_TIMEOUT, **input_kwargs): + call_timeout=None, **input_kwargs): filter_flag = input_kwargs.get("filter_flag") json_flag = input_kwargs.get("json_flag", True) get_version = input_kwargs.get("get_version") get_system_time = input_kwargs.get("get_system_time") + if call_timeout is None: + call_timeout = self.rest_timeout kwargs = {'timeout': call_timeout} if data is not None: @@ -110,7 +114,7 @@ def call(self, url, method, data=None, result = func(call_url, **kwargs) except Exception as err: LOG.error('Bad response from server: %(url)s. ' - 'Error: %(err)s'), {'url': call_url, 'err': err} + 'Error: %(err)s', {'url': call_url, 'err': err}) return {"error": { "code": constants.CONNECT_ERROR, "description": "Connect to server error."}} @@ -201,6 +205,12 @@ def query_pool_info(self, pool_id=None): self._assert_rest_result(result, _("Query pool session error.")) return result['storagePools'] + def query_storage_pool_info(self): + url = "/cluster/storagepool/queryStoragePool" + result = self.call(url, 'GET', get_version=True, filter_flag=True) + self._assert_rest_result(result, _("Query pool session error.")) + return result.get('storagePools', []) + def _get_volume_num_by_pool(self, pool_id): pool_info = self.query_pool_info(pool_id) return pool_info[0].get('volumeNum', 0) diff --git a/Cinder/Xena/fs_qos.py b/Cinder/Xena/fs_qos.py index 286dbc3..6ed8bb3 100644 --- a/Cinder/Xena/fs_qos.py +++ b/Cinder/Xena/fs_qos.py @@ -42,7 +42,7 @@ def add(self, qos, vol_name): raise def _is_qos_associate_to_volume(self, qos_name): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() volumes = None for pool in all_pools: volumes = self.client.get_qos_volume_info( diff --git a/Cinder/Xena/fs_utils.py b/Cinder/Xena/fs_utils.py index 0856038..5bcfdf3 100644 --- a/Cinder/Xena/fs_utils.py +++ b/Cinder/Xena/fs_utils.py @@ -217,16 +217,20 @@ def _get_qos_specs(qos_specs_id, client): kvs = specs.get('specs', {}) LOG.info('The QoS specs is: %s.', kvs) - qos = dict() - for k, v in kvs.items(): + return get_qos_param(kvs, client) + + +def get_qos_param(qos_vals, client): + qos_param = dict() + for k, v in qos_vals.items(): _raise_qos_is_invalid(k) - qos = _set_qos(qos, k, v) + qos_param = _set_qos(qos_param, k, v) - _raise_qos_not_set(qos) - _set_default_qos(qos) - qos = _get_trigger_qos(qos, client) + _raise_qos_not_set(qos_param) + _set_default_qos(qos_param) + qos_param = _get_trigger_qos(qos_param, client) - return qos + return qos_param def _deal_date_increase_or_decrease(is_date_decrease, is_date_increase, qos): diff --git a/Cinder/Yoga/customization_driver.py b/Cinder/Yoga/customization_driver.py new file mode 100644 index 0000000..8709ee8 --- /dev/null +++ b/Cinder/Yoga/customization_driver.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023 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 logging + +from cinder.volume.drivers.fusionstorage import fs_utils + +LOG = logging.getLogger(__name__) + + +class DriverForZTE(object): + """ + ZTE Cloud Platform Customization Class + """ + def __init__(self, *args, **kwargs): + super(DriverForZTE, self).__init__(*args, **kwargs) + + def reload_qos(self, volume, qos_vals=None): + """ + ZTE Cloud Platform Customization Interface + QoS policies can be dynamically modified,remove,add + and take effect on volumes in real time. + """ + self._check_volume_exist_on_array(volume) + volume_name = self._get_vol_name(volume) + if not qos_vals: + LOG.info("qos_vals is None, remove qos from volume %s", volume_name) + self.fs_qos.remove(volume_name) + return + + qos_vals = fs_utils.get_qos_param(qos_vals, self.client) + vol_qos = self.client.get_qos_by_vol_name(volume_name) + qos_name = vol_qos.get("qosName") + if qos_name: + LOG.info("volume already had qos, " + "update qos:%s of volume %s", qos_name, volume_name) + self.client.modify_qos(qos_name, qos_vals) + return + + LOG.info("volume did not have qos, " + "add qos to volume %s", volume_name) + self.fs_qos.add(qos_vals, volume_name) + return diff --git a/Cinder/Yoga/dsware.py b/Cinder/Yoga/dsware.py index e6d6c40..e1b61ce 100644 --- a/Cinder/Yoga/dsware.py +++ b/Cinder/Yoga/dsware.py @@ -37,6 +37,7 @@ from cinder.volume.drivers.fusionstorage import fs_flow from cinder.volume.drivers.fusionstorage import fs_qos from cinder.volume.drivers.fusionstorage import fs_utils +from cinder.volume.drivers.fusionstorage import customization_driver from cinder.volume.drivers.san import san from cinder.volume import volume_utils @@ -114,6 +115,9 @@ cfg.BoolOpt('full_clone', default=False, help='Whether use full clone.'), + cfg.IntOpt('rest_timeout', + default=constants.DEFAULT_TIMEOUT, + help='timeout when call storage restful api.'), ] CONF = cfg.CONF @@ -121,7 +125,8 @@ @interface.volumedriver -class DSWAREBaseDriver(driver.VolumeDriver): +class DSWAREBaseDriver(customization_driver.DriverForZTE, + driver.VolumeDriver): VERSION = "2.6.2" CI_WIKI_NAME = 'Huawei_FusionStorage_CI' @@ -160,7 +165,8 @@ def do_setup(self, context): } extend_conf = { - "mutual_authentication": mutual_authentication + "mutual_authentication": mutual_authentication, + "rest_timeout": self.configuration.rest_timeout } self.client = fs_client.RestCommon(fs_address=url_str, @@ -171,7 +177,7 @@ def do_setup(self, context): self.fs_qos = fs_qos.FusionStorageQoS(self.client) def check_for_setup_error(self): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() all_pools_name = [p['poolName'] for p in all_pools if p.get('poolName')] @@ -190,7 +196,7 @@ def _update_pool_stats(self): "pools": [], "vendor_name": "Huawei" } - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool['poolName'] in self.configuration.pools_name: @@ -264,7 +270,7 @@ def _get_pool_id(self, volume): def _get_pool_id_by_name(self, pool_name): pool_id_list = [] - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool_name == pool['poolName']: pool_id_list.append(pool['poolId']) @@ -716,6 +722,7 @@ def retype(self, context, volume, new_type, diff, host): volume, new_type, host, vol_name) if migrate: src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) LOG.debug("Begin to migrate LUN(id: %(lun_id)s) with " "change %(change_opts)s.", {"lun_id": src_lun_id, "change_opts": change_opts}) @@ -735,6 +742,7 @@ def migrate_volume(self, context, volume, host): {"volume": volume.id, "host": host}) src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) moved = self._migrate_volume(volume, host, src_lun_id) return moved, {} @@ -860,7 +868,6 @@ def _check_migration_valid(self, host): return True def _check_volume_exist_on_array(self, volume): - volume_name = self._get_vol_name(volume) result = self._check_volume_exist(volume) if not result: msg = _("Volume %s does not exist on the array." @@ -877,11 +884,14 @@ def _check_volume_exist_on_array(self, volume): ) % volume.id self._raise_exception(msg) + return lun_id + + def _check_volume_snapshot_exist(self, volume): + volume_name = self._get_vol_name(volume) if self.client.get_volume_snapshot(volume_name): msg = _("Volume %s which have snapshot cannot do lun migration" ) % volume.id self._raise_exception(msg) - return lun_id def _rollback_snapshot(self, vol_name, snap_name): def _snapshot_rollback_finish(): diff --git a/Cinder/Yoga/fs_client.py b/Cinder/Yoga/fs_client.py index 3ebf26f..ec2fb06 100644 --- a/Cinder/Yoga/fs_client.py +++ b/Cinder/Yoga/fs_client.py @@ -44,6 +44,8 @@ def __init__(self, fs_address, fs_user, fs_password, **extend_conf): self.token = None self.version = None self.esn = None + self.rest_timeout = extend_conf.get( + "rest_timeout", constants.DEFAULT_TIMEOUT) mutual_authentication = extend_conf.get("mutual_authentication", {}) self.init_http_head(mutual_authentication) @@ -93,11 +95,13 @@ def _deal_call_result(result, filter_flag, json_flag, req_dict): return result.json() if json_flag else result def call(self, url, method, data=None, - call_timeout=constants.DEFAULT_TIMEOUT, **input_kwargs): + call_timeout=None, **input_kwargs): filter_flag = input_kwargs.get("filter_flag") json_flag = input_kwargs.get("json_flag", True) get_version = input_kwargs.get("get_version") get_system_time = input_kwargs.get("get_system_time") + if call_timeout is None: + call_timeout = self.rest_timeout kwargs = {'timeout': call_timeout} if data is not None: @@ -110,7 +114,7 @@ def call(self, url, method, data=None, result = func(call_url, **kwargs) except Exception as err: LOG.error('Bad response from server: %(url)s. ' - 'Error: %(err)s'), {'url': call_url, 'err': err} + 'Error: %(err)s', {'url': call_url, 'err': err}) return {"error": { "code": constants.CONNECT_ERROR, "description": "Connect to server error."}} @@ -201,6 +205,12 @@ def query_pool_info(self, pool_id=None): self._assert_rest_result(result, _("Query pool session error.")) return result['storagePools'] + def query_storage_pool_info(self): + url = "/cluster/storagepool/queryStoragePool" + result = self.call(url, 'GET', get_version=True, filter_flag=True) + self._assert_rest_result(result, _("Query pool session error.")) + return result.get('storagePools', []) + def _get_volume_num_by_pool(self, pool_id): pool_info = self.query_pool_info(pool_id) return pool_info[0].get('volumeNum', 0) diff --git a/Cinder/Yoga/fs_qos.py b/Cinder/Yoga/fs_qos.py index 286dbc3..6ed8bb3 100644 --- a/Cinder/Yoga/fs_qos.py +++ b/Cinder/Yoga/fs_qos.py @@ -42,7 +42,7 @@ def add(self, qos, vol_name): raise def _is_qos_associate_to_volume(self, qos_name): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() volumes = None for pool in all_pools: volumes = self.client.get_qos_volume_info( diff --git a/Cinder/Yoga/fs_utils.py b/Cinder/Yoga/fs_utils.py index 0856038..5bcfdf3 100644 --- a/Cinder/Yoga/fs_utils.py +++ b/Cinder/Yoga/fs_utils.py @@ -217,16 +217,20 @@ def _get_qos_specs(qos_specs_id, client): kvs = specs.get('specs', {}) LOG.info('The QoS specs is: %s.', kvs) - qos = dict() - for k, v in kvs.items(): + return get_qos_param(kvs, client) + + +def get_qos_param(qos_vals, client): + qos_param = dict() + for k, v in qos_vals.items(): _raise_qos_is_invalid(k) - qos = _set_qos(qos, k, v) + qos_param = _set_qos(qos_param, k, v) - _raise_qos_not_set(qos) - _set_default_qos(qos) - qos = _get_trigger_qos(qos, client) + _raise_qos_not_set(qos_param) + _set_default_qos(qos_param) + qos_param = _get_trigger_qos(qos_param, client) - return qos + return qos_param def _deal_date_increase_or_decrease(is_date_decrease, is_date_increase, qos): diff --git a/Cinder/Zed/customization_driver.py b/Cinder/Zed/customization_driver.py new file mode 100644 index 0000000..8709ee8 --- /dev/null +++ b/Cinder/Zed/customization_driver.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023 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 logging + +from cinder.volume.drivers.fusionstorage import fs_utils + +LOG = logging.getLogger(__name__) + + +class DriverForZTE(object): + """ + ZTE Cloud Platform Customization Class + """ + def __init__(self, *args, **kwargs): + super(DriverForZTE, self).__init__(*args, **kwargs) + + def reload_qos(self, volume, qos_vals=None): + """ + ZTE Cloud Platform Customization Interface + QoS policies can be dynamically modified,remove,add + and take effect on volumes in real time. + """ + self._check_volume_exist_on_array(volume) + volume_name = self._get_vol_name(volume) + if not qos_vals: + LOG.info("qos_vals is None, remove qos from volume %s", volume_name) + self.fs_qos.remove(volume_name) + return + + qos_vals = fs_utils.get_qos_param(qos_vals, self.client) + vol_qos = self.client.get_qos_by_vol_name(volume_name) + qos_name = vol_qos.get("qosName") + if qos_name: + LOG.info("volume already had qos, " + "update qos:%s of volume %s", qos_name, volume_name) + self.client.modify_qos(qos_name, qos_vals) + return + + LOG.info("volume did not have qos, " + "add qos to volume %s", volume_name) + self.fs_qos.add(qos_vals, volume_name) + return diff --git a/Cinder/Zed/dsware.py b/Cinder/Zed/dsware.py index e6d6c40..e1b61ce 100644 --- a/Cinder/Zed/dsware.py +++ b/Cinder/Zed/dsware.py @@ -37,6 +37,7 @@ from cinder.volume.drivers.fusionstorage import fs_flow from cinder.volume.drivers.fusionstorage import fs_qos from cinder.volume.drivers.fusionstorage import fs_utils +from cinder.volume.drivers.fusionstorage import customization_driver from cinder.volume.drivers.san import san from cinder.volume import volume_utils @@ -114,6 +115,9 @@ cfg.BoolOpt('full_clone', default=False, help='Whether use full clone.'), + cfg.IntOpt('rest_timeout', + default=constants.DEFAULT_TIMEOUT, + help='timeout when call storage restful api.'), ] CONF = cfg.CONF @@ -121,7 +125,8 @@ @interface.volumedriver -class DSWAREBaseDriver(driver.VolumeDriver): +class DSWAREBaseDriver(customization_driver.DriverForZTE, + driver.VolumeDriver): VERSION = "2.6.2" CI_WIKI_NAME = 'Huawei_FusionStorage_CI' @@ -160,7 +165,8 @@ def do_setup(self, context): } extend_conf = { - "mutual_authentication": mutual_authentication + "mutual_authentication": mutual_authentication, + "rest_timeout": self.configuration.rest_timeout } self.client = fs_client.RestCommon(fs_address=url_str, @@ -171,7 +177,7 @@ def do_setup(self, context): self.fs_qos = fs_qos.FusionStorageQoS(self.client) def check_for_setup_error(self): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() all_pools_name = [p['poolName'] for p in all_pools if p.get('poolName')] @@ -190,7 +196,7 @@ def _update_pool_stats(self): "pools": [], "vendor_name": "Huawei" } - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool['poolName'] in self.configuration.pools_name: @@ -264,7 +270,7 @@ def _get_pool_id(self, volume): def _get_pool_id_by_name(self, pool_name): pool_id_list = [] - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() for pool in all_pools: if pool_name == pool['poolName']: pool_id_list.append(pool['poolId']) @@ -716,6 +722,7 @@ def retype(self, context, volume, new_type, diff, host): volume, new_type, host, vol_name) if migrate: src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) LOG.debug("Begin to migrate LUN(id: %(lun_id)s) with " "change %(change_opts)s.", {"lun_id": src_lun_id, "change_opts": change_opts}) @@ -735,6 +742,7 @@ def migrate_volume(self, context, volume, host): {"volume": volume.id, "host": host}) src_lun_id = self._check_volume_exist_on_array(volume) + self._check_volume_snapshot_exist(volume) moved = self._migrate_volume(volume, host, src_lun_id) return moved, {} @@ -860,7 +868,6 @@ def _check_migration_valid(self, host): return True def _check_volume_exist_on_array(self, volume): - volume_name = self._get_vol_name(volume) result = self._check_volume_exist(volume) if not result: msg = _("Volume %s does not exist on the array." @@ -877,11 +884,14 @@ def _check_volume_exist_on_array(self, volume): ) % volume.id self._raise_exception(msg) + return lun_id + + def _check_volume_snapshot_exist(self, volume): + volume_name = self._get_vol_name(volume) if self.client.get_volume_snapshot(volume_name): msg = _("Volume %s which have snapshot cannot do lun migration" ) % volume.id self._raise_exception(msg) - return lun_id def _rollback_snapshot(self, vol_name, snap_name): def _snapshot_rollback_finish(): diff --git a/Cinder/Zed/fs_client.py b/Cinder/Zed/fs_client.py index 3ebf26f..ec2fb06 100644 --- a/Cinder/Zed/fs_client.py +++ b/Cinder/Zed/fs_client.py @@ -44,6 +44,8 @@ def __init__(self, fs_address, fs_user, fs_password, **extend_conf): self.token = None self.version = None self.esn = None + self.rest_timeout = extend_conf.get( + "rest_timeout", constants.DEFAULT_TIMEOUT) mutual_authentication = extend_conf.get("mutual_authentication", {}) self.init_http_head(mutual_authentication) @@ -93,11 +95,13 @@ def _deal_call_result(result, filter_flag, json_flag, req_dict): return result.json() if json_flag else result def call(self, url, method, data=None, - call_timeout=constants.DEFAULT_TIMEOUT, **input_kwargs): + call_timeout=None, **input_kwargs): filter_flag = input_kwargs.get("filter_flag") json_flag = input_kwargs.get("json_flag", True) get_version = input_kwargs.get("get_version") get_system_time = input_kwargs.get("get_system_time") + if call_timeout is None: + call_timeout = self.rest_timeout kwargs = {'timeout': call_timeout} if data is not None: @@ -110,7 +114,7 @@ def call(self, url, method, data=None, result = func(call_url, **kwargs) except Exception as err: LOG.error('Bad response from server: %(url)s. ' - 'Error: %(err)s'), {'url': call_url, 'err': err} + 'Error: %(err)s', {'url': call_url, 'err': err}) return {"error": { "code": constants.CONNECT_ERROR, "description": "Connect to server error."}} @@ -201,6 +205,12 @@ def query_pool_info(self, pool_id=None): self._assert_rest_result(result, _("Query pool session error.")) return result['storagePools'] + def query_storage_pool_info(self): + url = "/cluster/storagepool/queryStoragePool" + result = self.call(url, 'GET', get_version=True, filter_flag=True) + self._assert_rest_result(result, _("Query pool session error.")) + return result.get('storagePools', []) + def _get_volume_num_by_pool(self, pool_id): pool_info = self.query_pool_info(pool_id) return pool_info[0].get('volumeNum', 0) diff --git a/Cinder/Zed/fs_qos.py b/Cinder/Zed/fs_qos.py index 286dbc3..6ed8bb3 100644 --- a/Cinder/Zed/fs_qos.py +++ b/Cinder/Zed/fs_qos.py @@ -42,7 +42,7 @@ def add(self, qos, vol_name): raise def _is_qos_associate_to_volume(self, qos_name): - all_pools = self.client.query_pool_info() + all_pools = self.client.query_storage_pool_info() volumes = None for pool in all_pools: volumes = self.client.get_qos_volume_info( diff --git a/Cinder/Zed/fs_utils.py b/Cinder/Zed/fs_utils.py index 0856038..5bcfdf3 100644 --- a/Cinder/Zed/fs_utils.py +++ b/Cinder/Zed/fs_utils.py @@ -217,16 +217,20 @@ def _get_qos_specs(qos_specs_id, client): kvs = specs.get('specs', {}) LOG.info('The QoS specs is: %s.', kvs) - qos = dict() - for k, v in kvs.items(): + return get_qos_param(kvs, client) + + +def get_qos_param(qos_vals, client): + qos_param = dict() + for k, v in qos_vals.items(): _raise_qos_is_invalid(k) - qos = _set_qos(qos, k, v) + qos_param = _set_qos(qos_param, k, v) - _raise_qos_not_set(qos) - _set_default_qos(qos) - qos = _get_trigger_qos(qos, client) + _raise_qos_not_set(qos_param) + _set_default_qos(qos_param) + qos_param = _get_trigger_qos(qos_param, client) - return qos + return qos_param def _deal_date_increase_or_decrease(is_date_decrease, is_date_increase, qos): diff --git "a/ReleaseDoc/zh/FusionStorage OpenStack Driver\351\205\215\347\275\256\346\214\207\345\215\227.pdf" "b/ReleaseDoc/zh/FusionStorage OpenStack Driver\351\205\215\347\275\256\346\214\207\345\215\227.pdf" index 48e1bcfc562c8c2d7def73c2a2d189d6d95951e8..a7fb45445e3e228c51c714280b5d0242ce5a7ca3 100644 GIT binary patch delta 255316 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 Xp17z!@B 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