# Copyright (c) 2016 Clinton Knight. All rights reserved. # Copyright (c) 2017 Jose Porrua. 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. """ Storage service catalog (SSC) functions and classes for NetApp cDOT systems. """ import copy import re from oslo_log import log as logging import six from cinder import exception from cinder.i18n import _ LOG = logging.getLogger(__name__) # NOTE(cknight): The keys in this map are tuples that contain arguments needed # for efficient use of the system-user-capability-get-iter cDOT API. The # values are SSC extra specs associated with the APIs listed in the keys. SSC_API_MAP = { ('storage.aggregate', 'show', 'aggr-options-list-info'): [ 'netapp_raid_type', ], ('storage.disk', 'show', 'storage-disk-get-iter'): [ 'netapp_disk_type', ], ('snapmirror', 'show', 'snapmirror-get-iter'): [ 'netapp_mirrored', ], ('volume.efficiency', 'show', 'sis-get-iter'): [ 'netapp_dedup', 'netapp_compression', ], ('volume', '*show', 'volume-get-iter'): [ 'netapp_flexvol_encryption', ], } class CapabilitiesLibrary(object): def __init__(self, protocol, vserver_name, zapi_client, configuration): self.protocol = protocol.lower() self.vserver_name = vserver_name self.zapi_client = zapi_client self.configuration = configuration self.backend_name = self.configuration.safe_get('volume_backend_name') self.ssc = {} self.invalid_extra_specs = [] def check_api_permissions(self): """Check which APIs that support SSC functionality are available.""" inaccessible_apis = [] invalid_extra_specs = [] for api_tuple, extra_specs in SSC_API_MAP.items(): object_name, operation_name, api = api_tuple if not self.zapi_client.check_cluster_api(object_name, operation_name, api): inaccessible_apis.append(api) invalid_extra_specs.extend(extra_specs) if inaccessible_apis: if 'volume-get-iter' in inaccessible_apis: msg = _('User not permitted to query Data ONTAP volumes.') raise exception.VolumeBackendAPIException(data=msg) else: LOG.warning('The configured user account does not have ' 'sufficient privileges to use all needed ' 'APIs. The following extra specs will fail ' 'or be ignored: %s.', invalid_extra_specs) self.invalid_extra_specs = invalid_extra_specs def cluster_user_supported(self): return not self.invalid_extra_specs def get_ssc(self): """Get a copy of the Storage Service Catalog.""" return copy.deepcopy(self.ssc) def get_ssc_flexvol_names(self): """Get the names of the FlexVols in the Storage Service Catalog.""" ssc = self.get_ssc() return list(ssc.keys()) def get_ssc_for_flexvol(self, flexvol_name): """Get map of Storage Service Catalog entries for a single flexvol.""" return copy.deepcopy(self.ssc.get(flexvol_name, {})) def get_ssc_aggregates(self): """Get a list of aggregates for all SSC flexvols.""" aggregates = set() for __, flexvol_info in self.ssc.items(): if 'netapp_aggregate' in flexvol_info: aggregates.add(flexvol_info['netapp_aggregate']) return list(aggregates) def is_qos_min_supported(self, pool_name): for __, flexvol_info in self.ssc.items(): if ('netapp_qos_min_support' in flexvol_info and 'pool_name' in flexvol_info and flexvol_info['pool_name'] == pool_name): return flexvol_info['netapp_qos_min_support'] == 'true' return False def update_ssc(self, flexvol_map): """Periodically runs to update Storage Service Catalog data. The self.ssc attribute is updated with the following format. { : {: }} """ LOG.info("Updating storage service catalog information for " "backend '%s'", self.backend_name) ssc = {} for flexvol_name, flexvol_info in flexvol_map.items(): ssc_volume = {} # Add metadata passed from the driver, including pool name ssc_volume.update(flexvol_info) # Get volume info ssc_volume.update(self._get_ssc_flexvol_info(flexvol_name)) ssc_volume.update(self._get_ssc_dedupe_info(flexvol_name)) ssc_volume.update(self._get_ssc_mirror_info(flexvol_name)) ssc_volume.update(self._get_ssc_encryption_info(flexvol_name)) # Get aggregate info aggregate_name = ssc_volume.get('netapp_aggregate') aggr_info = self._get_ssc_aggregate_info(aggregate_name) node_name = aggr_info.pop('netapp_node_name') ssc_volume.update(aggr_info) ssc_volume.update(self._get_ssc_qos_min_info(node_name)) ssc[flexvol_name] = ssc_volume self.ssc = ssc def _update_for_failover(self, zapi_client, flexvol_map): self.zapi_client = zapi_client self.update_ssc(flexvol_map) def _get_ssc_flexvol_info(self, flexvol_name): """Gather flexvol info and recast into SSC-style volume stats.""" volume_info = self.zapi_client.get_flexvol(flexvol_name=flexvol_name) netapp_thick = (volume_info.get('space-guarantee-enabled') and (volume_info.get('space-guarantee') == 'file' or volume_info.get('space-guarantee') == 'volume')) thick = self._get_thick_provisioning_support(netapp_thick) return { 'netapp_thin_provisioned': six.text_type(not netapp_thick).lower(), 'thick_provisioning_support': thick, 'thin_provisioning_support': not thick, 'netapp_aggregate': volume_info.get('aggregate'), } def _get_thick_provisioning_support(self, netapp_thick): """Get standard thick/thin values for a flexvol. The values reported for the standard thick_provisioning_support and thin_provisioning_support flags depend on both the flexvol state as well as protocol-specific configuration values. """ if self.protocol == 'nfs': return (netapp_thick and not self.configuration.nfs_sparsed_volumes) else: return (netapp_thick and (self.configuration.netapp_lun_space_reservation == 'enabled')) def _get_ssc_dedupe_info(self, flexvol_name): """Gather dedupe info and recast into SSC-style volume stats.""" if ('netapp_dedup' in self.invalid_extra_specs or 'netapp_compression' in self.invalid_extra_specs): dedupe = False compression = False else: dedupe_info = self.zapi_client.get_flexvol_dedupe_info( flexvol_name) dedupe = dedupe_info.get('dedupe') compression = dedupe_info.get('compression') return { 'netapp_dedup': six.text_type(dedupe).lower(), 'netapp_compression': six.text_type(compression).lower(), } def _get_ssc_encryption_info(self, flexvol_name): """Gather flexvol encryption info and recast into SSC-style stats.""" encrypted = self.zapi_client.is_flexvol_encrypted( flexvol_name, self.vserver_name) return {'netapp_flexvol_encryption': six.text_type(encrypted).lower()} def _get_ssc_qos_min_info(self, node_name): """Gather Qos minimum info and recast into SSC-style stats.""" supported = self.zapi_client.is_qos_min_supported( self.protocol == 'nfs', node_name) return {'netapp_qos_min_support': six.text_type(supported).lower()} def _get_ssc_mirror_info(self, flexvol_name): """Gather SnapMirror info and recast into SSC-style volume stats.""" mirrored = self.zapi_client.is_flexvol_mirrored( flexvol_name, self.vserver_name) return {'netapp_mirrored': six.text_type(mirrored).lower()} def _get_ssc_aggregate_info(self, aggregate_name): """Gather aggregate info and recast into SSC-style volume stats.""" if 'netapp_raid_type' in self.invalid_extra_specs: raid_type = None hybrid = None disk_types = None node_name = None else: aggregate = self.zapi_client.get_aggregate(aggregate_name) node_name = aggregate.get('node-name') raid_type = aggregate.get('raid-type') hybrid = (six.text_type(aggregate.get('is-hybrid')).lower() if 'is-hybrid' in aggregate else None) disk_types = self.zapi_client.get_aggregate_disk_types( aggregate_name) return { 'netapp_raid_type': raid_type, 'netapp_hybrid_aggregate': hybrid, 'netapp_disk_type': disk_types, 'netapp_node_name': node_name, } def get_matching_flexvols_for_extra_specs(self, extra_specs): """Return a list of flexvol names that match a set of extra specs.""" extra_specs = self._modify_extra_specs_for_comparison(extra_specs) matching_flexvols = [] for flexvol_name, flexvol_info in self.get_ssc().items(): if self._flexvol_matches_extra_specs(flexvol_info, extra_specs): matching_flexvols.append(flexvol_name) return matching_flexvols def _flexvol_matches_extra_specs(self, flexvol_info, extra_specs): """Check whether the SSC data for a FlexVol matches extra specs. A set of extra specs is considered a match for a FlexVol if, for each extra spec, the value matches what is in SSC or the key is unknown to SSC. """ for extra_spec_key, extra_spec_value in extra_specs.items(): if extra_spec_key in flexvol_info: if not self._extra_spec_matches(extra_spec_value, flexvol_info[extra_spec_key]): return False return True def _extra_spec_matches(self, extra_spec_value, ssc_flexvol_value): """Check whether an extra spec value matches something in the SSC. The SSC values may be scalars or lists, so the extra spec value must be compared to the SSC value if it is a scalar, or it must be sought in the list. """ if isinstance(ssc_flexvol_value, list): return extra_spec_value in ssc_flexvol_value else: return extra_spec_value == ssc_flexvol_value def _modify_extra_specs_for_comparison(self, extra_specs): """Adjust extra spec values for simple comparison to SSC values. Most extra-spec key-value tuples may be directly compared. But the boolean values that take the form ' True' or ' False' must be modified to allow comparison with the values we keep in the SSC and report to the scheduler. """ modified_extra_specs = copy.deepcopy(extra_specs) for key, value in extra_specs.items(): if isinstance(value, six.string_types): if re.match(r'\s+True', value, re.I): modified_extra_specs[key] = True elif re.match(r'\s+False', value, re.I): modified_extra_specs[key] = False return modified_extra_specs