# Copyright (c) 2016 Dell Inc. or its subsidiaries. # 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. from __future__ import division import contextlib from distutils import version import functools import json from oslo_log import log as logging from oslo_utils import fnmatch from oslo_utils import units import six from cinder import coordination from cinder import exception from cinder.i18n import _ from cinder.objects import fields from cinder.volume import group_types from cinder.volume import volume_types from cinder.volume import volume_utils from cinder.zonemanager import utils as zm_utils LOG = logging.getLogger(__name__) BACKEND_QOS_CONSUMERS = frozenset(['back-end', 'both']) QOS_MAX_IOPS = 'maxIOPS' QOS_MAX_BWS = 'maxBWS' def dump_provider_location(location_dict): sorted_keys = sorted(location_dict.keys()) return '|'.join('%(k)s^%(v)s' % {'k': k, 'v': location_dict[k]} for k in sorted_keys) def build_provider_location(system, lun_type, lun_id, version): """Builds provider_location for volume or snapshot. :param system: Unity serial number :param lun_id: LUN ID in Unity :param lun_type: 'lun' :param version: driver version """ location_dict = {'system': system, 'type': lun_type, 'id': six.text_type(lun_id), 'version': version} return dump_provider_location(location_dict) def extract_provider_location(provider_location, key): """Extracts value of the specified field from provider_location string. :param provider_location: provider_location string :param key: field name of the value that to be extracted :return: value of the specified field if it exists, otherwise, None is returned """ if provider_location: for kvp in provider_location.split('|'): fields = kvp.split('^') if len(fields) == 2 and fields[0] == key: return fields[1] else: LOG.warning('"%(key)s" is not found in provider ' 'location "%(location)s."', {'key': key, 'location': provider_location}) else: LOG.warning('Empty provider location received.') def byte_to_gib(byte): return byte / units.Gi def byte_to_mib(byte): return byte / units.Mi def gib_to_mib(gib): return gib * units.Ki def validate_pool_names(conf_pools, array_pools): if not conf_pools: LOG.debug('No storage pools are specified. This host will manage ' 'all the pools on the Unity system.') return array_pools conf_pools = set(map(lambda i: i.strip(), conf_pools)) array_pools = set(map(lambda i: i.strip(), array_pools)) existed = conf_pools & array_pools if not existed: msg = (_('No storage pools to be managed exist. Please check ' 'your configuration. The available storage pools on the ' 'system are %s.') % array_pools) raise exception.VolumeBackendAPIException(data=msg) return existed def extract_iscsi_uids(connector): if 'initiator' not in connector: msg = _("Host %s doesn't have iSCSI initiator.") % connector['host'] raise exception.VolumeBackendAPIException(data=msg) return [connector['initiator']] def extract_fc_uids(connector): if 'wwnns' not in connector or 'wwpns' not in connector: msg = _("Host %s doesn't have FC initiators.") % connector['host'] LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) wwnns = connector['wwnns'] wwpns = connector['wwpns'] wwns = [(node + port).upper() for node, port in zip(wwnns, wwpns)] def _to_wwn(wwn): # Format the wwn to include the colon # For example, convert 1122200000051E55E100 to # 11:22:20:00:00:05:1E:55:A1:00 return ':'.join(wwn[i:i + 2] for i in range(0, len(wwn), 2)) return list(map(_to_wwn, wwns)) def convert_ip_to_portal(ip): is_ipv6_without_brackets = ':' in ip and ip[-1] != ']' if is_ipv6_without_brackets: return '[%s]:3260' % ip return '%s:3260' % ip def convert_to_itor_tgt_map(zone_mapping): """Function to process data from lookup service. :param zone_mapping: mapping is the data from the zone lookup service with below format { : { 'initiator_port_wwn_list': ('200000051e55a100', '200000051e55a121'..) 'target_port_wwn_list': ('100000051e55a100', '100000051e55a121'..) } } """ target_wwns = [] itor_tgt_map = {} for san_name in zone_mapping: one_map = zone_mapping[san_name] for target in one_map['target_port_wwn_list']: if target not in target_wwns: target_wwns.append(target) for initiator in one_map['initiator_port_wwn_list']: itor_tgt_map[initiator] = one_map['target_port_wwn_list'] LOG.debug("target_wwns: %(tgt_wwns)s\n init_targ_map: %(itor_tgt_map)s", {'tgt_wwns': target_wwns, 'itor_tgt_map': itor_tgt_map}) return target_wwns, itor_tgt_map def get_pool_name(volume): return volume_utils.extract_host(volume.host, 'pool') def get_pool_name_from_host(host): return volume_utils.extract_host(host['host'], 'pool') def get_backend_name_from_volume(volume): return volume_utils.extract_host(volume.host, 'backend') def get_backend_name_from_host(host): return volume_utils.extract_host(host['host'], 'backend') def get_extra_spec(volume, spec_key): spec_value = None type_id = volume.volume_type_id if type_id is not None: extra_specs = volume_types.get_volume_type_extra_specs(type_id) if spec_key in extra_specs: spec_value = extra_specs[spec_key] return spec_value def group_is_cg(group): result = get_group_specs(group, 'consistent_group_snapshot_enabled') return result == ' True' def get_group_specs(group, spec_key): spec_value = None if group.group_type_id: group_specs = group_types.get_group_type_specs(group.group_type_id) if spec_key in group_specs: spec_value = group_specs[spec_key] return spec_value def ignore_exception(func, *args, **kwargs): try: func(*args, **kwargs) except Exception as ex: LOG.warning('Error occurred but ignored. Function: %(func_name)s, ' 'args: %(args)s, kwargs: %(kwargs)s, ' 'exception: %(ex)s.', {'func_name': func, 'args': args, 'kwargs': kwargs, 'ex': ex}) @contextlib.contextmanager def assure_cleanup(enter_func, exit_func, use_enter_return): """Assures the resource is cleaned up. Used as a context. :param enter_func: the function to execute when entering the context. :param exit_func: the function to execute when leaving the context. :param use_enter_return: the flag indicates whether to pass the return value of enter_func in to the exit_func. """ enter_return = None try: if isinstance(enter_func, functools.partial): enter_func_name = enter_func.func.__name__ else: enter_func_name = enter_func.__name__ LOG.debug(('Entering context. Function: %(func_name)s, ' 'use_enter_return: %(use)s.'), {'func_name': enter_func_name, 'use': use_enter_return}) enter_return = enter_func() yield enter_return finally: if isinstance(exit_func, functools.partial): exit_func_name = exit_func.func.__name__ else: exit_func_name = exit_func.__name__ LOG.debug(('Exiting context. Function: %(func_name)s, ' 'use_enter_return: %(use)s.'), {'func_name': exit_func_name, 'use': use_enter_return}) if enter_return is not None: if use_enter_return: ignore_exception(exit_func, enter_return) else: ignore_exception(exit_func) def create_lookup_service(): return zm_utils.create_lookup_service() def get_backend_qos_specs(volume): type_id = volume.volume_type_id if type_id is None: return None qos_specs = volume_types.get_volume_type_qos_specs(type_id) if qos_specs is None: return None qos_specs = qos_specs['qos_specs'] if qos_specs is None: return None consumer = qos_specs['consumer'] # Front end QoS specs are handled by nova. We ignore them here. if consumer not in BACKEND_QOS_CONSUMERS: return None max_iops = qos_specs['specs'].get(QOS_MAX_IOPS) max_bws = qos_specs['specs'].get(QOS_MAX_BWS) if max_iops is None and max_bws is None: return None return { 'id': qos_specs['id'], QOS_MAX_IOPS: max_iops, QOS_MAX_BWS: max_bws, } def remove_empty(option, value_list): if value_list: value_list = list(filter(None, map(str.strip, value_list))) if not value_list: raise exception.InvalidConfigurationValue(option=option, value=value_list) return value_list return None def match_any(full, patterns): matched = list( filter(lambda x: any(fnmatch.fnmatchcase(x, p) for p in patterns), full)) unmatched = list( filter(lambda x: not any(fnmatch.fnmatchcase(x, p) for p in patterns), full)) unmatched_patterns = list( filter(lambda p: not any(fnmatch.fnmatchcase(x, p) for x in full), patterns)) return matched, unmatched, unmatched_patterns def is_before_4_1(ver): return version.LooseVersion(ver) < version.LooseVersion('4.1') def lock_if(condition, lock_name): if condition: return coordination.synchronized(lock_name) else: return functools.partial def append_capabilities(func): capabilities = { 'thin_provisioning_support': True, 'thick_provisioning_support': True, 'consistent_group_snapshot_enabled': True, 'fast_support': True, 'consistent_group_replication_enabled': True } @six.wraps(func) def _inner(*args, **kwargs): output = func(*args, **kwargs) output.update(capabilities) return output return _inner def is_multiattach_to_host(volume_attachment, host_name): # When multiattach is enabled, a volume could be attached to two or more # instances which are hosted on one nova host. # Because unity cannot recognize the volume is attached to two or more # instances, we should keep the volume attached to the nova host until # the volume is detached from the last instance. if not volume_attachment: return False attachment = [a for a in volume_attachment if a.attach_status == fields.VolumeAttachStatus.ATTACHED and a.attached_host == host_name] return len(attachment) > 1 def load_replication_data(rep_data_str): # rep_data_str is string dumped from a dict like: # { # 'default': 'rep_session_name_failed_over', # 'backend_id_1': 'rep_session_name_1', # 'backend_id_2': 'rep_session_name_2' # } return json.loads(rep_data_str) def dump_replication_data(model_update, rep_data): # rep_data is a dict like: # { # 'backend_id_1': 'rep_session_name_1', # 'backend_id_2': 'rep_session_name_2' # } model_update['replication_driver_data'] = json.dumps(rep_data) return model_update def enable_replication_status(model_update, rep_data): model_update['replication_status'] = fields.ReplicationStatus.ENABLED return dump_replication_data(model_update, rep_data) def error_replication_status(model_update): # model_update is a dict like: # { # 'volume_id': volume.id, # 'updates': { # 'provider_id': new_provider_id, # 'provider_location': new_provider_location, # 'replication_status': fields.ReplicationStatus.FAILOVER_ERROR, # ... # } # } model_update['updates']['replication_status'] = ( fields.ReplicationStatus.FAILOVER_ERROR ) return model_update