
Cinder already removed support for Python 3.8, thus it no longer has to use the wrapper function from oslo.utils but can use the md5 method from hashlib directly. The md5 function from oslo.utils is being deprecated by [1]. [1] https://review.opendev.org/c/openstack/oslo.utils/+/930879 Change-Id: Ief1f4951b7b3b4fb8e7ba599cd7138f62b39d46f
506 lines
16 KiB
Python
506 lines
16 KiB
Python
# Copyright (c) 2016 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 hashlib
|
|
import json
|
|
import math
|
|
|
|
from oslo_log import log as logging
|
|
from oslo_utils import strutils
|
|
|
|
from cinder import context
|
|
from cinder import exception
|
|
from cinder.i18n import _
|
|
from cinder import objects
|
|
from cinder.objects import fields
|
|
from cinder import utils
|
|
from cinder.volume.drivers.huawei import constants
|
|
from cinder.volume import qos_specs
|
|
from cinder.volume import volume_types
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
def encode_name(name):
|
|
encoded_name = hashlib.md5(name.encode('utf-8'),
|
|
usedforsecurity=False).hexdigest()
|
|
prefix = name.split('-')[0] + '-'
|
|
postfix = encoded_name[:constants.MAX_NAME_LENGTH - len(prefix)]
|
|
return prefix + postfix
|
|
|
|
|
|
def old_encode_name(name):
|
|
pre_name = name.split("-")[0]
|
|
vol_encoded = str(hash(name))
|
|
if vol_encoded.startswith('-'):
|
|
newuuid = pre_name + vol_encoded
|
|
else:
|
|
newuuid = pre_name + '-' + vol_encoded
|
|
return newuuid
|
|
|
|
|
|
def encode_host_name(name):
|
|
if name and len(name) > constants.MAX_NAME_LENGTH:
|
|
encoded_name = hashlib.md5(name.encode('utf-8'),
|
|
usedforsecurity=False).hexdigest()
|
|
return encoded_name[:constants.MAX_NAME_LENGTH]
|
|
return name
|
|
|
|
|
|
def old_encode_host_name(name):
|
|
if name and len(name) > constants.MAX_NAME_LENGTH:
|
|
name = str(hash(name))
|
|
return name
|
|
|
|
|
|
def wait_for_condition(func, interval, timeout):
|
|
"""Wait for ``func`` to return True.
|
|
|
|
This retries running func until it either returns True or raises an
|
|
exception.
|
|
:param func: The function to call.
|
|
:param interval: The interval to wait in seconds between calls.
|
|
:param timeout: The maximum time in seconds to wait.
|
|
"""
|
|
if interval == 0:
|
|
interval = 1
|
|
if timeout == 0:
|
|
timeout = 1
|
|
|
|
@utils.retry(exception.VolumeDriverException,
|
|
interval=interval,
|
|
backoff_rate=1,
|
|
retries=(math.ceil(timeout / interval)))
|
|
def _retry_call():
|
|
result = func()
|
|
if not result:
|
|
raise exception.VolumeDriverException(
|
|
_('Timed out waiting for condition.'))
|
|
|
|
_retry_call()
|
|
|
|
|
|
def _get_volume_type(volume):
|
|
if volume.volume_type:
|
|
return volume.volume_type
|
|
if volume.volume_type_id:
|
|
return volume_types.get_volume_type(None, volume.volume_type_id)
|
|
|
|
|
|
def get_volume_params(volume):
|
|
volume_type = _get_volume_type(volume)
|
|
return get_volume_type_params(volume_type)
|
|
|
|
|
|
def get_volume_type_params(volume_type):
|
|
specs = {}
|
|
if isinstance(volume_type, dict) and volume_type.get('extra_specs'):
|
|
specs = volume_type['extra_specs']
|
|
elif isinstance(volume_type, objects.VolumeType
|
|
) and volume_type.extra_specs:
|
|
specs = volume_type.extra_specs
|
|
|
|
vol_params = get_volume_params_from_specs(specs)
|
|
vol_params['qos'] = None
|
|
|
|
if isinstance(volume_type, dict) and volume_type.get('qos_specs_id'):
|
|
vol_params['qos'] = _get_qos_specs(volume_type['qos_specs_id'])
|
|
elif isinstance(volume_type, objects.VolumeType
|
|
) and volume_type.qos_specs_id:
|
|
vol_params['qos'] = _get_qos_specs(volume_type.qos_specs_id)
|
|
|
|
LOG.info('volume opts %s.', vol_params)
|
|
return vol_params
|
|
|
|
|
|
def get_volume_params_from_specs(specs):
|
|
opts = _get_opts_from_specs(specs)
|
|
|
|
_verify_smartcache_opts(opts)
|
|
_verify_smartpartition_opts(opts)
|
|
_verify_smartthin_opts(opts)
|
|
|
|
return opts
|
|
|
|
|
|
def _get_opts_from_specs(specs):
|
|
"""Get the well defined extra specs."""
|
|
opts = {}
|
|
|
|
def _get_bool_param(k, v):
|
|
words = v.split()
|
|
if len(words) == 2 and words[0] == '<is>':
|
|
return strutils.bool_from_string(words[1], strict=True)
|
|
|
|
msg = _("%(k)s spec must be specified as %(k)s='<is> True' "
|
|
"or '<is> False'.") % {'k': k}
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(reason=msg)
|
|
|
|
def _get_replication_type_param(k, v):
|
|
words = v.split()
|
|
if len(words) == 2 and words[0] == '<in>':
|
|
REPLICA_SYNC_TYPES = {'sync': constants.REPLICA_SYNC_MODEL,
|
|
'async': constants.REPLICA_ASYNC_MODEL}
|
|
sync_type = words[1].lower()
|
|
if sync_type in REPLICA_SYNC_TYPES:
|
|
return REPLICA_SYNC_TYPES[sync_type]
|
|
|
|
msg = _("replication_type spec must be specified as "
|
|
"replication_type='<in> sync' or '<in> async'.")
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(reason=msg)
|
|
|
|
def _get_string_param(k, v):
|
|
if not v:
|
|
msg = _("%s spec must be specified as a string.") % k
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(reason=msg)
|
|
return v
|
|
|
|
opts_capability = {
|
|
'capabilities:smarttier': (_get_bool_param, False),
|
|
'capabilities:smartcache': (_get_bool_param, False),
|
|
'capabilities:smartpartition': (_get_bool_param, False),
|
|
'capabilities:thin_provisioning_support': (_get_bool_param, False),
|
|
'capabilities:thick_provisioning_support': (_get_bool_param, False),
|
|
'capabilities:hypermetro': (_get_bool_param, False),
|
|
'capabilities:replication_enabled': (_get_bool_param, False),
|
|
'replication_type': (_get_replication_type_param,
|
|
constants.REPLICA_ASYNC_MODEL),
|
|
'smarttier:policy': (_get_string_param, None),
|
|
'smartcache:cachename': (_get_string_param, None),
|
|
'smartpartition:partitionname': (_get_string_param, None),
|
|
'huawei_controller:controllername': (_get_string_param, None),
|
|
'capabilities:dedup': (_get_bool_param, None),
|
|
'capabilities:compression': (_get_bool_param, None),
|
|
}
|
|
|
|
def _get_opt_key(spec_key):
|
|
key_split = spec_key.split(':')
|
|
if len(key_split) == 1:
|
|
return key_split[0]
|
|
else:
|
|
return key_split[1]
|
|
|
|
for spec_key in opts_capability:
|
|
opt_key = _get_opt_key(spec_key)
|
|
opts[opt_key] = opts_capability[spec_key][1]
|
|
|
|
for key, value in specs.items():
|
|
if key not in opts_capability:
|
|
continue
|
|
func = opts_capability[key][0]
|
|
opt_key = _get_opt_key(key)
|
|
opts[opt_key] = func(key, value)
|
|
|
|
return opts
|
|
|
|
|
|
def _get_qos_specs(qos_specs_id):
|
|
ctxt = context.get_admin_context()
|
|
specs = qos_specs.get_qos_specs(ctxt, qos_specs_id)
|
|
if specs is None:
|
|
return {}
|
|
|
|
if specs.get('consumer') == 'front-end':
|
|
return {}
|
|
|
|
kvs = specs.get('specs', {})
|
|
LOG.info('The QoS specs is: %s.', kvs)
|
|
|
|
qos = {'IOTYPE': kvs.pop('IOType', None)}
|
|
|
|
if qos['IOTYPE'] not in constants.QOS_IOTYPES:
|
|
msg = _('IOType must be in %(types)s.'
|
|
) % {'types': constants.QOS_IOTYPES}
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(reason=msg)
|
|
|
|
for k, v in kvs.items():
|
|
if k not in constants.QOS_SPEC_KEYS:
|
|
msg = _('QoS key %s is not valid.') % k
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(reason=msg)
|
|
|
|
if int(v) <= 0:
|
|
msg = _('QoS value for %s must > 0.') % k
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(reason=msg)
|
|
|
|
qos[k.upper()] = v
|
|
|
|
if len(qos) < 2:
|
|
msg = _('QoS policy must specify both IOType and one another '
|
|
'qos spec, got policy: %s.') % qos
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(reason=msg)
|
|
|
|
qos_keys = set(qos.keys())
|
|
if (qos_keys & set(constants.UPPER_LIMIT_KEYS) and
|
|
qos_keys & set(constants.LOWER_LIMIT_KEYS)):
|
|
msg = _('QoS policy upper limit and lower limit '
|
|
'conflict, QoS policy: %s.') % qos
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(reason=msg)
|
|
|
|
return qos
|
|
|
|
|
|
def _verify_smartthin_opts(opts):
|
|
if (opts['thin_provisioning_support'] and
|
|
opts['thick_provisioning_support']):
|
|
msg = _('Cannot set thin and thick at the same time.')
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(reason=msg)
|
|
elif opts['thin_provisioning_support']:
|
|
opts['LUNType'] = constants.THIN_LUNTYPE
|
|
elif opts['thick_provisioning_support']:
|
|
opts['LUNType'] = constants.THICK_LUNTYPE
|
|
|
|
|
|
def _verify_smartcache_opts(opts):
|
|
if opts['smartcache'] and not opts['cachename']:
|
|
msg = _('Cache name is not specified, please set '
|
|
'smartcache:cachename in extra specs.')
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(reason=msg)
|
|
|
|
|
|
def _verify_smartpartition_opts(opts):
|
|
if opts['smartpartition'] and not opts['partitionname']:
|
|
msg = _('Partition name is not specified, please set '
|
|
'smartpartition:partitionname in extra specs.')
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(reason=msg)
|
|
|
|
|
|
def wait_lun_online(client, lun_id, wait_interval=None, wait_timeout=None):
|
|
def _lun_online():
|
|
result = client.get_lun_info_by_id(lun_id)
|
|
if result['HEALTHSTATUS'] != constants.STATUS_HEALTH:
|
|
err_msg = _('LUN %s is abnormal.') % lun_id
|
|
LOG.error(err_msg)
|
|
raise exception.VolumeBackendAPIException(data=err_msg)
|
|
|
|
if result['RUNNINGSTATUS'] == constants.LUN_INITIALIZING:
|
|
return False
|
|
|
|
return True
|
|
|
|
if not wait_interval:
|
|
wait_interval = constants.DEFAULT_WAIT_INTERVAL
|
|
if not wait_timeout:
|
|
wait_timeout = wait_interval * 10
|
|
|
|
wait_for_condition(_lun_online, wait_interval, wait_timeout)
|
|
|
|
|
|
def is_not_exist_exc(exc):
|
|
msg = getattr(exc, 'msg', '')
|
|
return 'not exist' in msg
|
|
|
|
|
|
def to_string(**kwargs):
|
|
return json.dumps(kwargs) if kwargs else ''
|
|
|
|
|
|
def to_dict(text):
|
|
return json.loads(text) if text else {}
|
|
|
|
|
|
def get_volume_private_data(volume):
|
|
if not volume.provider_location:
|
|
return {}
|
|
|
|
try:
|
|
info = json.loads(volume.provider_location)
|
|
except Exception:
|
|
LOG.exception("Decode volume provider_location error")
|
|
return {}
|
|
|
|
if isinstance(info, dict):
|
|
return info
|
|
|
|
# To keep compatible with old driver version
|
|
return {'huawei_lun_id': str(info),
|
|
'huawei_lun_wwn': volume.admin_metadata.get('huawei_lun_wwn'),
|
|
'huawei_sn': volume.metadata.get('huawei_sn'),
|
|
'hypermetro_id': volume.metadata.get('hypermetro_id'),
|
|
'remote_lun_id': volume.metadata.get('remote_lun_id')
|
|
}
|
|
|
|
|
|
def get_volume_metadata(volume):
|
|
if isinstance(volume, objects.Volume):
|
|
return volume.metadata
|
|
if volume.get('volume_metadata'):
|
|
return {item['key']: item['value'] for item in
|
|
volume['volume_metadata']}
|
|
return {}
|
|
|
|
|
|
def get_replication_data(volume):
|
|
if not volume.replication_driver_data:
|
|
return {}
|
|
|
|
return json.loads(volume.replication_driver_data)
|
|
|
|
|
|
def get_snapshot_private_data(snapshot):
|
|
if not snapshot.provider_location:
|
|
return {}
|
|
|
|
info = json.loads(snapshot.provider_location)
|
|
if isinstance(info, dict):
|
|
return info
|
|
|
|
# To keep compatible with old driver version
|
|
return {'huawei_snapshot_id': str(info),
|
|
'huawei_snapshot_wwn': snapshot.metadata.get(
|
|
'huawei_snapshot_wwn'),
|
|
}
|
|
|
|
|
|
def get_external_lun_info(client, external_ref):
|
|
lun_info = None
|
|
if 'source-id' in external_ref:
|
|
lun = client.get_lun_info_by_id(external_ref['source-id'])
|
|
lun_info = client.get_lun_info_by_name(lun['NAME'])
|
|
elif 'source-name' in external_ref:
|
|
lun_info = client.get_lun_info_by_name(external_ref['source-name'])
|
|
|
|
return lun_info
|
|
|
|
|
|
def get_external_snapshot_info(client, external_ref):
|
|
snapshot_info = None
|
|
if 'source-id' in external_ref:
|
|
snapshot_info = client.get_snapshot_info_by_id(
|
|
external_ref['source-id'])
|
|
elif 'source-name' in external_ref:
|
|
snapshot_info = client.get_snapshot_info_by_name(
|
|
external_ref['source-name'])
|
|
|
|
return snapshot_info
|
|
|
|
|
|
def get_lun_info(client, volume):
|
|
metadata = get_volume_private_data(volume)
|
|
|
|
volume_name = encode_name(volume.id)
|
|
lun_info = client.get_lun_info_by_name(volume_name)
|
|
|
|
# If new encoded way not found, try the old encoded way.
|
|
if not lun_info:
|
|
volume_name = old_encode_name(volume.id)
|
|
lun_info = client.get_lun_info_by_name(volume_name)
|
|
|
|
if not lun_info and metadata.get('huawei_lun_id'):
|
|
lun_info = client.get_lun_info_by_id(metadata['huawei_lun_id'])
|
|
|
|
if lun_info and ('huawei_lun_wwn' in metadata and
|
|
lun_info.get('WWN') != metadata['huawei_lun_wwn']):
|
|
return None
|
|
|
|
return lun_info
|
|
|
|
|
|
def get_snapshot_info(client, snapshot):
|
|
name = encode_name(snapshot.id)
|
|
snapshot_info = client.get_snapshot_info_by_name(name)
|
|
|
|
# If new encoded way not found, try the old encoded way.
|
|
if not snapshot_info:
|
|
name = old_encode_name(snapshot.id)
|
|
snapshot_info = client.get_snapshot_info_by_name(name)
|
|
|
|
return snapshot_info
|
|
|
|
|
|
def get_host_id(client, host_name):
|
|
encoded_name = encode_host_name(host_name)
|
|
host_id = client.get_host_id_by_name(encoded_name)
|
|
if encoded_name == host_name:
|
|
return host_id
|
|
|
|
if not host_id:
|
|
encoded_name = old_encode_host_name(host_name)
|
|
host_id = client.get_host_id_by_name(encoded_name)
|
|
|
|
return host_id
|
|
|
|
|
|
def get_hypermetro_group(client, group_id):
|
|
encoded_name = encode_name(group_id)
|
|
group = client.get_metrogroup_by_name(encoded_name)
|
|
if not group:
|
|
encoded_name = old_encode_name(group_id)
|
|
group = client.get_metrogroup_by_name(encoded_name)
|
|
return group
|
|
|
|
|
|
def get_replication_group(client, group_id):
|
|
encoded_name = encode_name(group_id)
|
|
group = client.get_replication_group_by_name(encoded_name)
|
|
if not group:
|
|
encoded_name = old_encode_name(group_id)
|
|
group = client.get_replication_group_by_name(encoded_name)
|
|
return group
|
|
|
|
|
|
def get_volume_model_update(volume, **kwargs):
|
|
private_data = get_volume_private_data(volume)
|
|
|
|
if kwargs.get('hypermetro_id'):
|
|
private_data['hypermetro_id'] = kwargs.get('hypermetro_id')
|
|
elif 'hypermetro_id' in private_data:
|
|
private_data.pop('hypermetro_id')
|
|
|
|
if 'huawei_lun_id' in kwargs:
|
|
private_data['huawei_lun_id'] = kwargs['huawei_lun_id']
|
|
if 'huawei_lun_wwn' in kwargs:
|
|
private_data['huawei_lun_wwn'] = kwargs['huawei_lun_wwn']
|
|
if 'huawei_sn' in kwargs:
|
|
private_data['huawei_sn'] = kwargs['huawei_sn']
|
|
|
|
model_update = {'provider_location': to_string(**private_data)}
|
|
|
|
if kwargs.get('replication_id'):
|
|
model_update['replication_driver_data'] = to_string(
|
|
pair_id=kwargs.get('replication_id'))
|
|
model_update['replication_status'] = fields.ReplicationStatus.ENABLED
|
|
else:
|
|
model_update['replication_driver_data'] = None
|
|
model_update['replication_status'] = fields.ReplicationStatus.DISABLED
|
|
|
|
return model_update
|
|
|
|
|
|
def get_group_type_params(group):
|
|
opts = []
|
|
for volume_type in group.volume_types:
|
|
opt = get_volume_type_params(volume_type)
|
|
opts.append(opt)
|
|
return opts
|
|
|
|
|
|
def is_support_clone_pair(client):
|
|
array_info = client.get_array_info()
|
|
version_info = array_info['PRODUCTVERSION']
|
|
if version_info >= constants.SUPPORT_CLONE_PAIR_VERSION:
|
|
return True
|