# Copyright (c) 2015 Clinton Knight. All rights reserved. # Copyright (c) 2015 Tom Barron. 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. """ NetApp Data ONTAP cDOT base storage driver library. This library is the abstract base for subclasses that complete the single-SVM or multi-SVM functionality needed by the cDOT Manila drivers. """ import copy import math import socket import time from oslo_log import log from oslo_service import loopingcall from oslo_utils import units import six from manila import exception from manila.i18n import _, _LE, _LI, _LW from manila.share.drivers.netapp.dataontap.client import client_cmode from manila.share.drivers.netapp.dataontap.protocols import cifs_cmode from manila.share.drivers.netapp.dataontap.protocols import nfs_cmode from manila.share.drivers.netapp import options as na_opts from manila.share.drivers.netapp import utils as na_utils from manila.share import share_types from manila.share import utils as share_utils LOG = log.getLogger(__name__) class NetAppCmodeFileStorageLibrary(object): AUTOSUPPORT_INTERVAL_SECONDS = 3600 # hourly SSC_UPDATE_INTERVAL_SECONDS = 3600 # hourly HOUSEKEEPING_INTERVAL_SECONDS = 600 # ten minutes SUPPORTED_PROTOCOLS = ('nfs', 'cifs') # Maps NetApp qualified extra specs keys to corresponding backend API # client library argument keywords. When we expose more backend # capabilities here, we will add them to this map. BOOLEAN_QUALIFIED_EXTRA_SPECS_MAP = { 'netapp:thin_provisioned': 'thin_provisioned', 'netapp:dedup': 'dedup_enabled', 'netapp:compression': 'compression_enabled', } STRING_QUALIFIED_EXTRA_SPECS_MAP = { 'netapp:snapshot_policy': 'snapshot_policy', 'netapp:language': 'language', 'netapp:max_files': 'max_files', } # Maps standard extra spec keys to legacy NetApp keys STANDARD_BOOLEAN_EXTRA_SPECS_MAP = { 'thin_provisioning': 'netapp:thin_provisioned', 'dedupe': 'netapp:dedup', 'compression': 'netapp:compression', } def __init__(self, driver_name, **kwargs): na_utils.validate_driver_instantiation(**kwargs) self.driver_name = driver_name self.private_storage = kwargs['private_storage'] self.configuration = kwargs['configuration'] self.configuration.append_config_values(na_opts.netapp_connection_opts) self.configuration.append_config_values(na_opts.netapp_basicauth_opts) self.configuration.append_config_values(na_opts.netapp_transport_opts) self.configuration.append_config_values(na_opts.netapp_support_opts) self.configuration.append_config_values(na_opts.netapp_cluster_opts) self.configuration.append_config_values( na_opts.netapp_provisioning_opts) self._licenses = [] self._client = None self._clients = {} self._ssc_stats = {} self._have_cluster_creds = None self._app_version = kwargs.get('app_version', 'unknown') na_utils.setup_tracing(self.configuration.netapp_trace_flags) self._backend_name = self.configuration.safe_get( 'share_backend_name') or driver_name @na_utils.trace def do_setup(self, context): self._client = self._get_api_client() self._have_cluster_creds = self._client.check_for_cluster_credentials() @na_utils.trace def check_for_setup_error(self): self._licenses = self._get_licenses() self._start_periodic_tasks() def _get_vserver(self, share_server=None): raise NotImplementedError() @na_utils.trace def _get_api_client(self, vserver=None): # Use cached value to prevent calls to system-get-ontapi-version. client = self._clients.get(vserver) if not client: client = client_cmode.NetAppCmodeClient( transport_type=self.configuration.netapp_transport_type, username=self.configuration.netapp_login, password=self.configuration.netapp_password, hostname=self.configuration.netapp_server_hostname, port=self.configuration.netapp_server_port, vserver=vserver, trace=na_utils.TRACE_API) self._clients[vserver] = client return client @na_utils.trace def _get_licenses(self): if not self._have_cluster_creds: LOG.debug('License info not available without cluster credentials') return [] self._licenses = self._client.get_licenses() log_data = { 'backend': self._backend_name, 'licenses': ', '.join(self._licenses), } LOG.info(_LI('Available licenses on %(backend)s ' 'are %(licenses)s.'), log_data) if 'nfs' not in self._licenses and 'cifs' not in self._licenses: msg = _LE('Neither NFS nor CIFS is licensed on %(backend)s') msg_args = {'backend': self._backend_name} LOG.error(msg % msg_args) return self._licenses @na_utils.trace def _start_periodic_tasks(self): # Run the task once in the current thread so prevent a race with # the first invocation of get_share_stats. self._update_ssc_info() # Start the task that updates the slow-changing storage service catalog ssc_periodic_task = loopingcall.FixedIntervalLoopingCall( self._update_ssc_info) ssc_periodic_task.start(interval=self.SSC_UPDATE_INTERVAL_SECONDS, initial_delay=self.SSC_UPDATE_INTERVAL_SECONDS) # Start the task that logs autosupport (EMS) data to the controller ems_periodic_task = loopingcall.FixedIntervalLoopingCall( self._handle_ems_logging) ems_periodic_task.start(interval=self.AUTOSUPPORT_INTERVAL_SECONDS, initial_delay=0) # Start the task that runs other housekeeping tasks, such as deletion # of previously soft-deleted storage artifacts. housekeeping_periodic_task = loopingcall.FixedIntervalLoopingCall( self._handle_housekeeping_tasks) housekeeping_periodic_task.start( interval=self.HOUSEKEEPING_INTERVAL_SECONDS, initial_delay=0) def _get_valid_share_name(self, share_id): """Get share name according to share name template.""" return self.configuration.netapp_volume_name_template % { 'share_id': share_id.replace('-', '_')} def _get_valid_snapshot_name(self, snapshot_id): """Get snapshot name according to snapshot name template.""" return 'share_snapshot_' + snapshot_id.replace('-', '_') def _get_valid_cg_snapshot_name(self, snapshot_id): """Get snapshot name according to snapshot name template.""" return 'share_cg_snapshot_' + snapshot_id.replace('-', '_') @na_utils.trace def _get_aggregate_space(self): aggregates = self._find_matching_aggregates() if self._have_cluster_creds: return self._client.get_cluster_aggregate_capacities(aggregates) else: return self._client.get_vserver_aggregate_capacities(aggregates) @na_utils.trace def _get_aggregate_node(self, aggregate_name): """Get home node for the specified aggregate, or None.""" if self._have_cluster_creds: return self._client.get_node_for_aggregate(aggregate_name) else: return None @na_utils.trace def get_share_stats(self): """Retrieve stats info from Data ONTAP backend.""" data = { 'share_backend_name': self._backend_name, 'driver_name': self.driver_name, 'vendor_name': 'NetApp', 'driver_version': '1.0', 'netapp_storage_family': 'ontap_cluster', 'storage_protocol': 'NFS_CIFS', 'total_capacity_gb': 0.0, 'free_capacity_gb': 0.0, 'consistency_group_support': 'host', 'pools': self._get_pools(), } return data @na_utils.trace def get_share_server_pools(self, share_server): """Return list of pools related to a particular share server. Note that the multi-SVM cDOT driver assigns all available pools to each Vserver, so there is no need to filter the pools any further by share_server. :param share_server: ShareServer class instance. """ return self._get_pools() @na_utils.trace def _get_pools(self): """Retrieve list of pools available to this backend.""" pools = [] aggr_space = self._get_aggregate_space() for aggr_name in sorted(aggr_space.keys()): total_capacity_gb = na_utils.round_down(float( aggr_space[aggr_name].get('total', 0)) / units.Gi, '0.01') free_capacity_gb = na_utils.round_down(float( aggr_space[aggr_name].get('available', 0)) / units.Gi, '0.01') allocated_capacity_gb = na_utils.round_down(float( aggr_space[aggr_name].get('used', 0)) / units.Gi, '0.01') if total_capacity_gb == 0.0: total_capacity_gb = 'unknown' pool = { 'pool_name': aggr_name, 'total_capacity_gb': total_capacity_gb, 'free_capacity_gb': free_capacity_gb, 'allocated_capacity_gb': allocated_capacity_gb, 'qos': 'False', 'reserved_percentage': 0, 'dedupe': [True, False], 'compression': [True, False], 'thin_provisioning': [True, False], } # Add storage service catalog data. pool_ssc_stats = self._ssc_stats.get(aggr_name) if pool_ssc_stats: pool.update(pool_ssc_stats) pools.append(pool) return pools @na_utils.trace def _handle_ems_logging(self): """Build and send an EMS log message.""" self._client.send_ems_log_message(self._build_ems_log_message()) @na_utils.trace def _build_ems_log_message(self): """Construct EMS Autosupport log message.""" ems_log = { 'computer-name': socket.getfqdn() or 'Manila_node', 'event-id': '0', 'event-source': 'Manila driver %s' % self.driver_name, 'app-version': self._app_version, 'category': 'provisioning', 'event-description': 'OpenStack Manila connected to cluster node', 'log-level': '6', 'auto-support': 'false', } return ems_log @na_utils.trace def _handle_housekeeping_tasks(self): """Handle various cleanup activities.""" def _find_matching_aggregates(self): """Find all aggregates match pattern.""" raise NotImplementedError() @na_utils.trace def _get_helper(self, share): """Returns driver which implements share protocol.""" share_protocol = share['share_proto'].lower() if share_protocol not in self.SUPPORTED_PROTOCOLS: err_msg = _("Invalid NAS protocol supplied: %s.") % share_protocol raise exception.NetAppException(err_msg) self._check_license_for_protocol(share_protocol) if share_protocol == 'nfs': return nfs_cmode.NetAppCmodeNFSHelper() elif share_protocol == 'cifs': return cifs_cmode.NetAppCmodeCIFSHelper() @na_utils.trace def _check_license_for_protocol(self, share_protocol): """Validates protocol license if cluster APIs are accessible.""" if not self._have_cluster_creds: return if share_protocol.lower() not in self._licenses: current_licenses = self._get_licenses() if share_protocol.lower() not in current_licenses: msg_args = { 'protocol': share_protocol, 'host': self.configuration.netapp_server_hostname } msg = _('The protocol %(protocol)s is not licensed on ' 'controller %(host)s') % msg_args LOG.error(msg) raise exception.NetAppException(msg) @na_utils.trace def get_pool(self, share): pool = share_utils.extract_host(share['host'], level='pool') if pool: return pool share_name = self._get_valid_share_name(share['id']) return self._client.get_aggregate_for_volume(share_name) @na_utils.trace def create_share(self, context, share, share_server): """Creates new share.""" vserver, vserver_client = self._get_vserver(share_server=share_server) self._allocate_container(share, vserver_client) return self._create_export(share, vserver, vserver_client) @na_utils.trace def create_share_from_snapshot(self, context, share, snapshot, share_server=None): """Creates new share from snapshot.""" vserver, vserver_client = self._get_vserver(share_server=share_server) self._allocate_container_from_snapshot(share, snapshot, vserver_client) return self._create_export(share, vserver, vserver_client) @na_utils.trace def _allocate_container(self, share, vserver_client): """Create new share on aggregate.""" share_name = self._get_valid_share_name(share['id']) # Get Data ONTAP aggregate name as pool name. pool_name = share_utils.extract_host(share['host'], level='pool') if pool_name is None: msg = _("Pool is not available in the share host field.") raise exception.InvalidHost(reason=msg) extra_specs = share_types.get_extra_specs_from_share(share) extra_specs = self._remap_standard_boolean_extra_specs(extra_specs) self._check_extra_specs_validity(share, extra_specs) provisioning_options = self._get_provisioning_options(extra_specs) LOG.debug('Creating share %(share)s on pool %(pool)s with ' 'provisioning options %(options)s', {'share': share_name, 'pool': pool_name, 'options': provisioning_options}) vserver_client.create_volume( pool_name, share_name, share['size'], snapshot_reserve=self.configuration. netapp_volume_snapshot_reserve_percent, **provisioning_options) @na_utils.trace def _remap_standard_boolean_extra_specs(self, extra_specs): """Replace standard boolean extra specs with NetApp-specific ones.""" specs = copy.deepcopy(extra_specs) for (key, netapp_key) in self.STANDARD_BOOLEAN_EXTRA_SPECS_MAP.items(): if key in specs: bool_value = share_types.parse_boolean_extra_spec(key, specs[key]) specs[netapp_key] = 'true' if bool_value else 'false' del specs[key] return specs @na_utils.trace def _check_extra_specs_validity(self, share, extra_specs): """Check if the extra_specs have valid values.""" self._check_boolean_extra_specs_validity( share, extra_specs, list(self.BOOLEAN_QUALIFIED_EXTRA_SPECS_MAP)) self._check_string_extra_specs_validity(share, extra_specs) @na_utils.trace def _check_string_extra_specs_validity(self, share, extra_specs): """Check if the string_extra_specs have valid values.""" if 'netapp:max_files' in extra_specs: self._check_if_max_files_is_valid(share, extra_specs['netapp:max_files']) @na_utils.trace def _check_if_max_files_is_valid(self, share, value): """Check if max_files has a valid value.""" if int(value) < 0: args = {'value': value, 'key': 'netapp:max_files', 'type_id': share['share_type_id'], 'share_id': share['id']} msg = _('Invalid value "%(value)s" for extra_spec "%(key)s" ' 'in share_type %(type_id)s for share %(share_id)s.') raise exception.NetAppException(msg % args) @na_utils.trace def _check_boolean_extra_specs_validity(self, share, specs, keys_of_interest): # cDOT compression requires deduplication. dedup = specs.get('netapp:dedup', None) compression = specs.get('netapp:compression', None) if dedup is not None and compression is not None: if dedup.lower() == 'false' and compression.lower() == 'true': spec = {'netapp:dedup': dedup, 'netapp:compression': compression} type_id = share['share_type_id'] share_id = share['id'] args = {'type_id': type_id, 'share_id': share_id, 'spec': spec} msg = _('Invalid combination of extra_specs in share_type ' '%(type_id)s for share %(share_id)s: %(spec)s: ' 'deduplication must be enabled in order for ' 'compression to be enabled.') raise exception.Invalid(msg % args) """Check if the boolean_extra_specs have valid values.""" # Extra spec values must be (ignoring case) 'true' or 'false'. for key in keys_of_interest: value = specs.get(key) if value is not None and value.lower() not in ['true', 'false']: type_id = share['share_type_id'] share_id = share['id'] arg_map = {'value': value, 'key': key, 'type_id': type_id, 'share_id': share_id} msg = _('Invalid value "%(value)s" for extra_spec "%(key)s" ' 'in share_type %(type_id)s for share %(share_id)s.') raise exception.Invalid(msg % arg_map) @na_utils.trace def _get_boolean_provisioning_options(self, specs, boolean_specs_map): """Given extra specs, return corresponding client library kwargs. Build a full set of client library provisioning kwargs, filling in a default value if an explicit value has not been supplied via a corresponding extra spec. Boolean extra spec values are "true" or "false", with missing specs treated as "false". Provisioning kwarg values are True or False. """ # Extract the extra spec keys of concern and their corresponding # kwarg keys as lists. keys_of_interest = list(boolean_specs_map) provisioning_args = [boolean_specs_map[key] for key in keys_of_interest] # Set missing spec values to 'false' for key in keys_of_interest: if key not in specs: specs[key] = 'false' # Build a list of Boolean provisioning arguments from the string # equivalents in the spec values. provisioning_values = [specs[key].lower() == 'true' for key in keys_of_interest] # Combine the list of provisioning args and the list of provisioning # values into a dictionary suitable for use as kwargs when invoking # provisioning methods from the client API library. return dict(zip(provisioning_args, provisioning_values)) @na_utils.trace def _get_string_provisioning_options(self, specs, string_specs_map): """Given extra specs, return corresponding client library kwargs. Build a full set of client library provisioning kwargs, filling in a default value if an explicit value has not been supplied via a corresponding extra spec. """ # Extract the extra spec keys of concern and their corresponding # kwarg keys as lists. keys_of_interest = list(string_specs_map) provisioning_args = [string_specs_map[key] for key in keys_of_interest] # Set missing spec values to 'false' for key in keys_of_interest: if key not in specs: specs[key] = None provisioning_values = [specs[key] for key in keys_of_interest] # Combine the list of provisioning args and the list of provisioning # values into a dictionary suitable for use as kwargs when invoking # provisioning methods from the client API library. return dict(zip(provisioning_args, provisioning_values)) @na_utils.trace def _get_provisioning_options(self, specs): """Return a merged result of string and binary provisioning options.""" boolean_args = self._get_boolean_provisioning_options( specs, self.BOOLEAN_QUALIFIED_EXTRA_SPECS_MAP) string_args = self._get_string_provisioning_options( specs, self.STRING_QUALIFIED_EXTRA_SPECS_MAP) result = boolean_args.copy() result.update(string_args) return result @na_utils.trace def _check_aggregate_extra_specs_validity(self, aggregate_name, specs): for specs_key in ('netapp_disk_type', 'netapp_raid_type'): aggr_value = self._ssc_stats.get(aggregate_name, {}).get(specs_key) specs_value = specs.get(specs_key) if aggr_value and specs_value and aggr_value != specs_value: msg = _('Invalid value "%(value)s" for extra_spec "%(key)s" ' 'in aggregate %(aggr)s.') msg_args = { 'value': specs_value, 'key': specs_key, 'aggr': aggregate_name } raise exception.NetAppException(msg % msg_args) @na_utils.trace def _allocate_container_from_snapshot( self, share, snapshot, vserver_client, snapshot_name_func=_get_valid_snapshot_name): """Clones existing share.""" share_name = self._get_valid_share_name(share['id']) parent_share_name = self._get_valid_share_name(snapshot['share_id']) parent_snapshot_name = snapshot_name_func(self, snapshot['id']) LOG.debug('Creating share from snapshot %s', snapshot['id']) vserver_client.create_volume_clone(share_name, parent_share_name, parent_snapshot_name) vserver_client.split_volume_clone(share_name) @na_utils.trace def _share_exists(self, share_name, vserver_client): return vserver_client.volume_exists(share_name) @na_utils.trace def delete_share(self, context, share, share_server=None): """Deletes share.""" try: vserver, vserver_client = self._get_vserver( share_server=share_server) except (exception.InvalidInput, exception.VserverNotSpecified, exception.VserverNotFound) as error: LOG.warning(_LW("Could not determine share server for share being " "deleted: %(share)s. Deletion of share record " "will proceed anyway. Error: %(error)s"), {'share': share['id'], 'error': error}) return share_name = self._get_valid_share_name(share['id']) if self._share_exists(share_name, vserver_client): self._remove_export(share, vserver_client) self._deallocate_container(share_name, vserver_client) else: LOG.info(_LI("Share %s does not exist."), share['id']) @na_utils.trace def _deallocate_container(self, share_name, vserver_client): """Free share space.""" vserver_client.unmount_volume(share_name, force=True) vserver_client.offline_volume(share_name) vserver_client.delete_volume(share_name) @na_utils.trace def _create_export(self, share, vserver, vserver_client): """Creates NAS storage.""" helper = self._get_helper(share) helper.set_client(vserver_client) share_name = self._get_valid_share_name(share['id']) interfaces = vserver_client.get_network_interfaces( protocols=[share['share_proto']]) if not interfaces: msg = _('Cannot find network interfaces for Vserver %(vserver)s ' 'and protocol %(proto)s.') msg_args = {'vserver': vserver, 'proto': share['share_proto']} raise exception.NetAppException(msg % msg_args) # Get LIF addresses with metadata export_addresses = self._get_export_addresses_with_metadata(share, interfaces) # Create the share and get a callback for generating export locations callback = helper.create_share(share, share_name) # Generate export locations using addresses, metadata and callback export_locations = [ { 'path': callback(export_address), 'is_admin_only': metadata.pop('is_admin_only', False), 'metadata': metadata, } for export_address, metadata in copy.deepcopy(export_addresses).items() ] # Sort the export locations to report preferred paths first export_locations = self._sort_export_locations_by_preferred_paths( export_locations) return export_locations @na_utils.trace def _get_export_addresses_with_metadata(self, share, interfaces): """Return interface addresses with locality and other metadata.""" aggregate_name = share_utils.extract_host(share['host'], level='pool') home_node = self._get_aggregate_node(aggregate_name) addresses = {} for interface in interfaces: address = interface['address'] is_admin_only = False if home_node: preferred = interface.get('home-node') == home_node else: preferred = None addresses[address] = { 'is_admin_only': is_admin_only, 'preferred': preferred, } return addresses @na_utils.trace def _sort_export_locations_by_preferred_paths(self, export_locations): """Sort the export locations to report preferred paths first.""" sort_key = lambda location: location.get( 'metadata', {}).get('preferred') is not True return sorted(export_locations, key=sort_key) @na_utils.trace def _remove_export(self, share, vserver_client): """Deletes NAS storage.""" helper = self._get_helper(share) helper.set_client(vserver_client) share_name = self._get_valid_share_name(share['id']) target = helper.get_target(share) # Share may be in error state, so there's no share and target. if target: helper.delete_share(share, share_name) @na_utils.trace def create_snapshot(self, context, snapshot, share_server=None): """Creates a snapshot of a share.""" vserver, vserver_client = self._get_vserver(share_server=share_server) share_name = self._get_valid_share_name(snapshot['share_id']) snapshot_name = self._get_valid_snapshot_name(snapshot['id']) LOG.debug('Creating snapshot %s', snapshot_name) vserver_client.create_snapshot(share_name, snapshot_name) @na_utils.trace def delete_snapshot(self, context, snapshot, share_server=None): """Deletes a snapshot of a share.""" try: vserver, vserver_client = self._get_vserver( share_server=share_server) except (exception.InvalidInput, exception.VserverNotSpecified, exception.VserverNotFound) as error: LOG.warning(_LW("Could not determine share server for snapshot " "being deleted: %(snap)s. Deletion of snapshot " "record will proceed anyway. Error: %(error)s"), {'snap': snapshot['id'], 'error': error}) return share_name = self._get_valid_share_name(snapshot['share_id']) snapshot_name = self._get_valid_snapshot_name(snapshot['id']) try: self._handle_busy_snapshot(vserver_client, share_name, snapshot_name) except exception.SnapshotNotFound: LOG.info(_LI("Snapshot %s does not exist."), snapshot_name) return LOG.debug('Deleting snapshot %(snap)s for share %(share)s.', {'snap': snapshot_name, 'share': share_name}) vserver_client.delete_snapshot(share_name, snapshot_name) @na_utils.trace def _handle_busy_snapshot(self, vserver_client, share_name, snapshot_name, wait_seconds=60): """Checks for and handles a busy snapshot. If a snapshot is not busy, take no action. If a snapshot is busy for reasons other than a clone dependency, raise immediately. Otherwise, since we always start a clone split operation after cloning a share, wait up to a minute for a clone dependency to clear before giving up. """ snapshot = vserver_client.get_snapshot(share_name, snapshot_name) if not snapshot['busy']: return # Fail fast if snapshot is not busy due to a clone dependency if snapshot['owners'] != {'volume clone'}: raise exception.ShareSnapshotIsBusy(snapshot_name=snapshot_name) # Wait for clone dependency to clear. retry_interval = 3 # seconds for retry in range(int(wait_seconds / retry_interval)): LOG.debug('Snapshot %(snap)s for share %(share)s is busy, waiting ' 'for volume clone dependency to clear.', {'snap': snapshot_name, 'share': share_name}) time.sleep(retry_interval) snapshot = vserver_client.get_snapshot(share_name, snapshot_name) if not snapshot['busy']: return raise exception.ShareSnapshotIsBusy(snapshot_name=snapshot_name) @na_utils.trace def manage_existing(self, share, driver_options): vserver, vserver_client = self._get_vserver(share_server=None) share_size = self._manage_container(share, vserver_client) export_locations = self._create_export(share, vserver, vserver_client) return {'size': share_size, 'export_locations': export_locations} @na_utils.trace def unmanage(self, share): pass @na_utils.trace def _manage_container(self, share, vserver_client): """Bring existing volume under management as a share.""" protocol_helper = self._get_helper(share) protocol_helper.set_client(vserver_client) volume_name = protocol_helper.get_share_name_for_share(share) if not volume_name: msg = _('Volume could not be determined from export location ' '%(export)s.') msg_args = {'export': share['export_location']} raise exception.ManageInvalidShare(reason=msg % msg_args) share_name = self._get_valid_share_name(share['id']) aggregate_name = share_utils.extract_host(share['host'], level='pool') # Get existing volume info volume = vserver_client.get_volume_to_manage(aggregate_name, volume_name) if not volume: msg = _('Volume %(volume)s not found on aggregate %(aggr)s.') msg_args = {'volume': volume_name, 'aggr': aggregate_name} raise exception.ManageInvalidShare(reason=msg % msg_args) # Ensure volume is manageable self._validate_volume_for_manage(volume, vserver_client) # Validate extra specs extra_specs = share_types.get_extra_specs_from_share(share) try: self._check_extra_specs_validity(share, extra_specs) self._check_aggregate_extra_specs_validity(aggregate_name, extra_specs) except exception.ManilaException as ex: raise exception.ManageExistingShareTypeMismatch( reason=six.text_type(ex)) provisioning_options = self._get_provisioning_options(extra_specs) debug_args = { 'share': share_name, 'aggr': aggregate_name, 'options': provisioning_options } LOG.debug('Managing share %(share)s on aggregate %(aggr)s with ' 'provisioning options %(options)s', debug_args) # Rename & remount volume on new path vserver_client.unmount_volume(volume_name) vserver_client.set_volume_name(volume_name, share_name) vserver_client.mount_volume(share_name) # Modify volume to match extra specs vserver_client.manage_volume(aggregate_name, share_name, **provisioning_options) # Save original volume info to private storage original_data = { 'original_name': volume['name'], 'original_junction_path': volume['junction-path'] } self.private_storage.update(share['id'], original_data) # When calculating the size, round up to the next GB. return int(math.ceil(float(volume['size']) / units.Gi)) @na_utils.trace def _validate_volume_for_manage(self, volume, vserver_client): """Ensure volume is a candidate for becoming a share.""" # Check volume info, extra specs validity if volume['type'] != 'rw' or volume['style'] != 'flex': msg = _('Volume %(volume)s must be a read-write flexible volume.') msg_args = {'volume': volume['name']} raise exception.ManageInvalidShare(reason=msg % msg_args) if vserver_client.volume_has_luns(volume['name']): msg = _('Volume %(volume)s must not contain LUNs.') msg_args = {'volume': volume['name']} raise exception.ManageInvalidShare(reason=msg % msg_args) if vserver_client.volume_has_junctioned_volumes(volume['name']): msg = _('Volume %(volume)s must not have junctioned volumes.') msg_args = {'volume': volume['name']} raise exception.ManageInvalidShare(reason=msg % msg_args) @na_utils.trace def create_consistency_group(self, context, cg_dict, share_server=None): """Creates a consistency group. cDOT has no persistent CG object, so apart from validating the share_server info is passed correctly, this method has nothing to do. """ vserver, vserver_client = self._get_vserver(share_server=share_server) @na_utils.trace def create_consistency_group_from_cgsnapshot( self, context, cg_dict, cgsnapshot_dict, share_server=None): """Creates a consistency group from an existing CG snapshot.""" vserver, vserver_client = self._get_vserver(share_server=share_server) # Ensure there is something to do if not cgsnapshot_dict['cgsnapshot_members']: return None, None clone_list = self._collate_cg_snapshot_info(cg_dict, cgsnapshot_dict) share_update_list = [] LOG.debug('Creating consistency group from CG snapshot %s.', cgsnapshot_dict['id']) for clone in clone_list: self._allocate_container_from_snapshot( clone['share'], clone['snapshot'], vserver_client, NetAppCmodeFileStorageLibrary._get_valid_cg_snapshot_name) export_locations = self._create_export(clone['share'], vserver, vserver_client) share_update_list.append({ 'id': clone['share']['id'], 'export_locations': export_locations, }) return None, share_update_list def _collate_cg_snapshot_info(self, cg_dict, cgsnapshot_dict): """Collate the data for a clone of a CG snapshot. Given two data structures, a CG snapshot (cgsnapshot_dict) and a new CG to be cloned from the snapshot (cg_dict), match up both structures into a list of dicts (share & snapshot) suitable for use by existing driver methods that clone individual share snapshots. """ clone_list = list() for share in cg_dict['shares']: clone_info = {'share': share} for cgsnapshot_member in cgsnapshot_dict['cgsnapshot_members']: if (share['source_cgsnapshot_member_id'] == cgsnapshot_member['id']): clone_info['snapshot'] = { 'share_id': cgsnapshot_member['share_id'], 'id': cgsnapshot_member['cgsnapshot_id'] } break else: msg = _("Invalid data supplied for creating consistency group " "from CG snapshot %s.") % cgsnapshot_dict['id'] raise exception.InvalidConsistencyGroup(reason=msg) clone_list.append(clone_info) return clone_list @na_utils.trace def delete_consistency_group(self, context, cg_dict, share_server=None): """Deletes a consistency group. cDOT has no persistent CG object, so apart from validating the share_server info is passed correctly, this method has nothing to do. """ try: vserver, vserver_client = self._get_vserver( share_server=share_server) except (exception.InvalidInput, exception.VserverNotSpecified, exception.VserverNotFound) as error: LOG.warning(_LW("Could not determine share server for consistency " "group being deleted: %(cg)s. Deletion of CG " "record will proceed anyway. Error: %(error)s"), {'cg': cg_dict['id'], 'error': error}) @na_utils.trace def create_cgsnapshot(self, context, snap_dict, share_server=None): """Creates a consistency group snapshot.""" vserver, vserver_client = self._get_vserver(share_server=share_server) share_names = [self._get_valid_share_name(member['share_id']) for member in snap_dict.get('cgsnapshot_members', [])] snapshot_name = self._get_valid_cg_snapshot_name(snap_dict['id']) if share_names: LOG.debug('Creating CG snapshot %s.', snapshot_name) vserver_client.create_cg_snapshot(share_names, snapshot_name) return None, None @na_utils.trace def delete_cgsnapshot(self, context, snap_dict, share_server=None): """Deletes a consistency group snapshot.""" try: vserver, vserver_client = self._get_vserver( share_server=share_server) except (exception.InvalidInput, exception.VserverNotSpecified, exception.VserverNotFound) as error: LOG.warning(_LW("Could not determine share server for CG snapshot " "being deleted: %(snap)s. Deletion of CG snapshot " "record will proceed anyway. Error: %(error)s"), {'snap': snap_dict['id'], 'error': error}) return None, None share_names = [self._get_valid_share_name(member['share_id']) for member in snap_dict.get('cgsnapshot_members', [])] snapshot_name = self._get_valid_cg_snapshot_name(snap_dict['id']) for share_name in share_names: try: self._handle_busy_snapshot(vserver_client, share_name, snapshot_name) except exception.SnapshotNotFound: LOG.info(_LI("Snapshot %(snap)s does not exist for share " "%(share)s."), {'snap': snapshot_name, 'share': share_name}) continue LOG.debug("Deleting snapshot %(snap)s for share %(share)s.", {'snap': snapshot_name, 'share': share_name}) vserver_client.delete_snapshot(share_name, snapshot_name) return None, None @na_utils.trace def extend_share(self, share, new_size, share_server=None): """Extends size of existing share.""" vserver, vserver_client = self._get_vserver(share_server=share_server) share_name = self._get_valid_share_name(share['id']) LOG.debug('Extending share %(name)s to %(size)s GB.', {'name': share_name, 'size': new_size}) vserver_client.set_volume_size(share_name, new_size) @na_utils.trace def shrink_share(self, share, new_size, share_server=None): """Shrinks size of existing share.""" vserver, vserver_client = self._get_vserver(share_server=share_server) share_name = self._get_valid_share_name(share['id']) LOG.debug('Shrinking share %(name)s to %(size)s GB.', {'name': share_name, 'size': new_size}) vserver_client.set_volume_size(share_name, new_size) @na_utils.trace def update_access(self, context, share, access_rules, add_rules=None, delete_rules=None, share_server=None): """Updates access rules for a share.""" try: vserver, vserver_client = self._get_vserver( share_server=share_server) except (exception.InvalidInput, exception.VserverNotSpecified, exception.VserverNotFound) as error: LOG.warning(_LW("Could not determine share server for share " "%(share)s during access rules update. " "Error: %(error)s"), {'share': share['id'], 'error': error}) return share_name = self._get_valid_share_name(share['id']) if self._share_exists(share_name, vserver_client): helper = self._get_helper(share) helper.set_client(vserver_client) helper.update_access(share, share_name, access_rules) else: LOG.warning(_LW("Could not update access rules, share %s does " "not exist."), share['id']) def setup_server(self, network_info, metadata=None): raise NotImplementedError() def teardown_server(self, server_details, security_services=None): raise NotImplementedError() def get_network_allocations_number(self): """Get number of network interfaces to be created.""" raise NotImplementedError() @na_utils.trace def _update_ssc_info(self): """Periodically runs to update Storage Service Catalog data. The self._ssc_stats attribute is updated with the following format. { : {: }} """ LOG.info(_LI("Updating storage service catalog information for " "backend '%s'"), self._backend_name) # Work on a copy and update the ssc data atomically before returning. ssc_stats = copy.deepcopy(self._ssc_stats) aggregate_names = self._find_matching_aggregates() # Initialize entries for each aggregate. for aggregate_name in aggregate_names: if aggregate_name not in ssc_stats: ssc_stats[aggregate_name] = {} if aggregate_names: self._update_ssc_aggr_info(aggregate_names, ssc_stats) self._ssc_stats = ssc_stats @na_utils.trace def _update_ssc_aggr_info(self, aggregate_names, ssc_stats): """Updates the given SSC dictionary with new disk type information. :param aggregate_names: The aggregates this driver cares about :param ssc_stats: The dictionary to update """ if not self._have_cluster_creds: return raid_types = self._client.get_aggregate_raid_types(aggregate_names) for aggregate_name, raid_type in raid_types.items(): ssc_stats[aggregate_name]['netapp_raid_type'] = raid_type disk_types = self._client.get_aggregate_disk_types(aggregate_names) for aggregate_name, disk_type in disk_types.items(): ssc_stats[aggregate_name]['netapp_disk_type'] = disk_type