# Copyright (c) 2015 Hitachi Data Systems, Inc. # 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 os from oslo_config import cfg from oslo_log import log from oslo_utils import excutils from oslo_utils import importutils import six from manila.common import constants from manila import exception from manila.i18n import _, _LI, _LW from manila.share import driver from manila.share import utils LOG = log.getLogger(__name__) hitachi_hnas_opts = [ cfg.StrOpt('hitachi_hnas_ip', deprecated_name='hds_hnas_ip', help="HNAS management interface IP for communication " "between Manila controller and HNAS."), cfg.StrOpt('hitachi_hnas_user', deprecated_name='hds_hnas_user', help="HNAS username Base64 String in order to perform tasks " "such as create file-systems and network interfaces."), cfg.StrOpt('hitachi_hnas_password', deprecated_name='hds_hnas_password', secret=True, help="HNAS user password. Required only if private key is not " "provided."), cfg.IntOpt('hitachi_hnas_evs_id', deprecated_name='hds_hnas_evs_id', help="Specify which EVS this backend is assigned to."), cfg.StrOpt('hitachi_hnas_evs_ip', deprecated_name='hds_hnas_evs_ip', help="Specify IP for mounting shares."), cfg.StrOpt('hitachi_hnas_admin_network_ip', help="Specify IP for mounting shares in the Admin network."), cfg.StrOpt('hitachi_hnas_file_system_name', deprecated_name='hds_hnas_file_system_name', help="Specify file-system name for creating shares."), cfg.StrOpt('hitachi_hnas_ssh_private_key', deprecated_name='hds_hnas_ssh_private_key', secret=True, help="RSA/DSA private key value used to connect into HNAS. " "Required only if password is not provided."), cfg.StrOpt('hitachi_hnas_cluster_admin_ip0', deprecated_name='hds_hnas_cluster_admin_ip0', help="The IP of the clusters admin node. Only set in HNAS " "multinode clusters."), cfg.IntOpt('hitachi_hnas_stalled_job_timeout', deprecated_name='hds_hnas_stalled_job_timeout', default=30, help="The time (in seconds) to wait for stalled HNAS jobs " "before aborting."), cfg.StrOpt('hitachi_hnas_driver_helper', deprecated_name='hds_hnas_driver_helper', default='manila.share.drivers.hitachi.hnas.ssh.HNASSSHBackend', help="Python class to be used for driver helper."), cfg.BoolOpt('hitachi_hnas_allow_cifs_snapshot_while_mounted', deprecated_name='hds_hnas_allow_cifs_snapshot_while_mounted', default=False, help="By default, CIFS snapshots are not allowed to be taken " "when the share has clients connected because consistent " "point-in-time replica cannot be guaranteed for all " "files. Enabling this might cause inconsistent snapshots " "on CIFS shares."), ] CONF = cfg.CONF CONF.register_opts(hitachi_hnas_opts) class HitachiHNASDriver(driver.ShareDriver): """Manila HNAS Driver implementation. 1.0.0 - Initial Version. 2.0.0 - Refactoring, bugfixes, implemented Share Shrink and Update Access. 3.0.0 - New driver location, implemented support for CIFS protocol. 3.1.0 - Added admin network export location support. """ def __init__(self, *args, **kwargs): """Do initialization.""" LOG.debug("Invoking base constructor for Manila Hitachi HNAS Driver.") super(HitachiHNASDriver, self).__init__(False, *args, **kwargs) LOG.debug("Setting up attributes for Manila Hitachi HNAS Driver.") self.configuration.append_config_values(hitachi_hnas_opts) LOG.debug("Reading config parameters for Manila Hitachi HNAS Driver.") self.backend_name = self.configuration.safe_get('share_backend_name') hnas_helper = self.configuration.safe_get('hitachi_hnas_driver_helper') hnas_ip = self.configuration.safe_get('hitachi_hnas_ip') hnas_username = self.configuration.safe_get('hitachi_hnas_user') hnas_password = self.configuration.safe_get('hitachi_hnas_password') hnas_evs_id = self.configuration.safe_get('hitachi_hnas_evs_id') self.hnas_evs_ip = self.configuration.safe_get('hitachi_hnas_evs_ip') self.hnas_admin_network_ip = self.configuration.safe_get( 'hitachi_hnas_admin_network_ip') self.fs_name = self.configuration.safe_get( 'hitachi_hnas_file_system_name') self.cifs_snapshot = self.configuration.safe_get( 'hitachi_hnas_allow_cifs_snapshot_while_mounted') ssh_private_key = self.configuration.safe_get( 'hitachi_hnas_ssh_private_key') cluster_admin_ip0 = self.configuration.safe_get( 'hitachi_hnas_cluster_admin_ip0') self.private_storage = kwargs.get('private_storage') job_timeout = self.configuration.safe_get( 'hitachi_hnas_stalled_job_timeout') if hnas_helper is None: msg = _("The config parameter hitachi_hnas_driver_helper is not " "set.") raise exception.InvalidParameterValue(err=msg) if hnas_evs_id is None: msg = _("The config parameter hitachi_hnas_evs_id is not set.") raise exception.InvalidParameterValue(err=msg) if self.hnas_evs_ip is None: msg = _("The config parameter hitachi_hnas_evs_ip is not set.") raise exception.InvalidParameterValue(err=msg) if hnas_ip is None: msg = _("The config parameter hitachi_hnas_ip is not set.") raise exception.InvalidParameterValue(err=msg) if hnas_username is None: msg = _("The config parameter hitachi_hnas_user is not set.") raise exception.InvalidParameterValue(err=msg) if hnas_password is None and ssh_private_key is None: msg = _("Credentials configuration parameters missing: " "you need to set hitachi_hnas_password or " "hitachi_hnas_ssh_private_key.") raise exception.InvalidParameterValue(err=msg) LOG.debug("Initializing HNAS Layer.") helper = importutils.import_class(hnas_helper) self.hnas = helper(hnas_ip, hnas_username, hnas_password, ssh_private_key, cluster_admin_ip0, hnas_evs_id, self.hnas_evs_ip, self.fs_name, job_timeout) def update_access(self, context, share, access_rules, add_rules, delete_rules, share_server=None): """Update access rules for given share. :param context: The `context.RequestContext` object for the request :param share: Share that will have its access rules updated. :param access_rules: All access rules for given share. :param add_rules: Empty List or List of access rules which should be added. access_rules already contains these rules. :param delete_rules: Empty List or List of access rules which should be removed. access_rules doesn't contain these rules. :param share_server: Data structure with share server information. Not used by this driver. """ hnas_share_id = self._get_hnas_share_id(share['id']) try: self._ensure_share(share, hnas_share_id) except exception.HNASItemNotFoundException: raise exception.ShareResourceNotFound(share_id=share['id']) self._check_protocol(share['id'], share['share_proto']) if share['share_proto'].lower() == 'nfs': self._nfs_update_access(share, hnas_share_id, access_rules) else: if not (add_rules or delete_rules): # recovery mode self._clean_cifs_access_list(hnas_share_id) self._cifs_allow_access(share, hnas_share_id, access_rules) else: self._cifs_deny_access(share, hnas_share_id, delete_rules) self._cifs_allow_access(share, hnas_share_id, add_rules) def _nfs_update_access(self, share, hnas_share_id, access_rules): host_list = [] for rule in access_rules: if rule['access_type'].lower() != 'ip': msg = _("Only IP access type currently supported for NFS. " "Share provided %(share)s with rule type " "%(type)s.") % {'share': share['id'], 'type': rule['access_type']} raise exception.InvalidShareAccess(reason=msg) if rule['access_level'] == constants.ACCESS_LEVEL_RW: host_list.append(rule['access_to'] + '(' + rule['access_level'] + ',norootsquash)') else: host_list.append(rule['access_to'] + '(' + rule['access_level'] + ')') self.hnas.update_nfs_access_rule(host_list, share_id=hnas_share_id) if host_list: LOG.debug("Share %(share)s has the rules: %(rules)s", {'share': share['id'], 'rules': ', '.join(host_list)}) else: LOG.debug("Share %(share)s has no rules.", {'share': share['id']}) def _cifs_allow_access(self, share_or_snapshot, hnas_id, add_rules, is_snapshot=False): entity_type = "share" if is_snapshot: entity_type = "snapshot" for rule in add_rules: if rule['access_type'].lower() != 'user': msg = _("Only USER access type currently supported for CIFS. " "%(entity_type)s provided %(share)s with " "rule %(r_id)s type %(type)s allowing permission " "to %(to)s.") % { 'entity_type': entity_type.capitalize(), 'share': share_or_snapshot['id'], 'type': rule['access_type'], 'r_id': rule['id'], 'to': rule['access_to'], } raise exception.InvalidShareAccess(reason=msg) if rule['access_level'] == constants.ACCESS_LEVEL_RW: # Adding permission acr = Allow Change&Read permission = 'acr' else: # Adding permission ar = Allow Read permission = 'ar' formatted_user = rule['access_to'].replace('\\', '\\\\') self.hnas.cifs_allow_access(hnas_id, formatted_user, permission, is_snapshot=is_snapshot) LOG.debug("Added %(rule)s rule for user/group %(user)s " "to %(entity_type)s %(share)s.", {'rule': rule['access_level'], 'user': rule['access_to'], 'entity_type': entity_type, 'share': share_or_snapshot['id']}) def _cifs_deny_access(self, share_or_snapshot, hnas_id, delete_rules, is_snapshot=False): if is_snapshot: entity_type = "snapshot" share_proto = share_or_snapshot['share']['share_proto'] else: entity_type = "share" share_proto = share_or_snapshot['share_proto'] for rule in delete_rules: if rule['access_type'].lower() != 'user': LOG.warning(_LW('Only USER access type is allowed for ' 'CIFS. %(entity_type)s ' 'provided %(share)s with ' 'protocol %(proto)s.'), {'entity_type': entity_type.capitalize(), 'share': share_or_snapshot['id'], 'proto': share_proto}) continue formatted_user = rule['access_to'].replace('\\', '\\\\') self.hnas.cifs_deny_access(hnas_id, formatted_user, is_snapshot=is_snapshot) LOG.debug("Access denied for user/group %(user)s " "to %(entity_type)s %(share)s.", {'user': rule['access_to'], 'entity_type': entity_type, 'share': share_or_snapshot['id']}) def _clean_cifs_access_list(self, hnas_id, is_snapshot=False): permission_list = self.hnas.list_cifs_permissions(hnas_id) for permission in permission_list: formatted_user = r'"\{1}{0}\{1}"'.format(permission[0], '"') self.hnas.cifs_deny_access(hnas_id, formatted_user, is_snapshot=is_snapshot) def create_share(self, context, share, share_server=None): """Creates share. :param context: The `context.RequestContext` object for the request :param share: Share that will be created. :param share_server: Data structure with share server information. Not used by this driver. :returns: Returns a list of dicts containing the EVS IP concatenated with the path of share in the filesystem. Example for NFS:: [ { 'path': '172.24.44.10:/shares/id', 'metadata': {}, 'is_admin_only': False }, { 'path': '192.168.0.10:/shares/id', 'metadata': {}, 'is_admin_only': True } ] Example for CIFS:: [ { 'path': '\\172.24.44.10\id', 'metadata': {}, 'is_admin_only': False }, { 'path': '\\192.168.0.10\id', 'metadata': {}, 'is_admin_only': True } ] """ LOG.debug("Creating share in HNAS: %(shr)s.", {'shr': share['id']}) self._check_protocol(share['id'], share['share_proto']) export_list = self._create_share(share['id'], share['size'], share['share_proto']) LOG.debug("Share %(share)s created successfully on path(s): " "%(paths)s.", {'paths': ', '.join([x['path'] for x in export_list]), 'share': share['id']}) return export_list def delete_share(self, context, share, share_server=None): """Deletes share. :param context: The `context.RequestContext` object for the request :param share: Share that will be deleted. :param share_server: Data structure with share server information. Not used by this driver. """ hnas_share_id = self._get_hnas_share_id(share['id']) LOG.debug("Deleting share in HNAS: %(shr)s.", {'shr': share['id']}) self._delete_share(hnas_share_id, share['share_proto']) LOG.debug("Export and share successfully deleted: %(shr)s.", {'shr': share['id']}) def create_snapshot(self, context, snapshot, share_server=None): """Creates snapshot. :param context: The `context.RequestContext` object for the request :param snapshot: Snapshot that will be created. :param share_server: Data structure with share server information. Not used by this driver. """ hnas_share_id = self._get_hnas_share_id(snapshot['share_id']) LOG.debug("The snapshot of share %(snap_share_id)s will be created " "with id %(snap_id)s.", {'snap_share_id': snapshot['share_id'], 'snap_id': snapshot['id']}) export_locations = self._create_snapshot(hnas_share_id, snapshot) LOG.info(_LI("Snapshot %(id)s successfully created."), {'id': snapshot['id']}) output = { 'provider_location': os.path.join( '/snapshots', hnas_share_id, snapshot['id']) } if export_locations: output['export_locations'] = export_locations return output def delete_snapshot(self, context, snapshot, share_server=None): """Deletes snapshot. :param context: The `context.RequestContext` object for the request :param snapshot: Snapshot that will be deleted. :param share_server: Data structure with share server information. Not used by this driver. """ hnas_share_id = self._get_hnas_share_id(snapshot['share_id']) hnas_snapshot_id = self._get_hnas_snapshot_id(snapshot) LOG.debug("The snapshot %(snap_id)s will be deleted. The related " "share ID is %(snap_share_id)s.", {'snap_id': snapshot['id'], 'snap_share_id': snapshot['share_id']}) self._delete_snapshot(snapshot['share'], hnas_share_id, hnas_snapshot_id) LOG.info(_LI("Snapshot %(id)s successfully deleted."), {'id': snapshot['id']}) def create_share_from_snapshot(self, context, share, snapshot, share_server=None): """Creates a new share from snapshot. :param context: The `context.RequestContext` object for the request :param share: Information about the new share. :param snapshot: Information about the snapshot that will be copied to new share. :param share_server: Data structure with share server information. Not used by this driver. :returns: Returns a list of dicts containing the EVS IP concatenated with the path of share in the filesystem. Example for NFS:: [ { 'path': '172.24.44.10:/shares/id', 'metadata': {}, 'is_admin_only': False }, { 'path': '192.168.0.10:/shares/id', 'metadata': {}, 'is_admin_only': True } ] Example for CIFS:: [ { 'path': '\\172.24.44.10\id', 'metadata': {}, 'is_admin_only': False }, { 'path': '\\192.168.0.10\id', 'metadata': {}, 'is_admin_only': True } ] """ LOG.debug("Creating a new share from snapshot: %(ss_id)s.", {'ss_id': snapshot['id']}) hnas_src_share_id = self._get_hnas_share_id(snapshot['share_id']) hnas_src_snap_id = self._get_hnas_snapshot_id(snapshot) export_list = self._create_share_from_snapshot( share, hnas_src_share_id, hnas_src_snap_id) LOG.debug("Share %(share)s created successfully on path(s): " "%(paths)s.", {'paths': ', '.join([x['path'] for x in export_list]), 'share': share['id']}) return export_list def ensure_share(self, context, share, share_server=None): """Ensure that share is exported. :param context: The `context.RequestContext` object for the request :param share: Share that will be checked. :param share_server: Data structure with share server information. Not used by this driver. :returns: Returns a list of dicts containing the EVS IP concatenated with the path of share in the filesystem. Example for NFS:: [ { 'path': '172.24.44.10:/shares/id', 'metadata': {}, 'is_admin_only': False }, { 'path': '192.168.0.10:/shares/id', 'metadata': {}, 'is_admin_only': True } ] Example for CIFS:: [ { 'path': '\\172.24.44.10\id', 'metadata': {}, 'is_admin_only': False }, { 'path': '\\192.168.0.10\id', 'metadata': {}, 'is_admin_only': True } ] """ LOG.debug("Ensuring share in HNAS: %(shr)s.", {'shr': share['id']}) hnas_share_id = self._get_hnas_share_id(share['id']) export_list = self._ensure_share(share, hnas_share_id) LOG.debug("Share ensured in HNAS: %(shr)s, protocol %(proto)s.", {'shr': share['id'], 'proto': share['share_proto']}) return export_list def extend_share(self, share, new_size, share_server=None): """Extends a share to new size. :param share: Share that will be extended. :param new_size: New size of share. :param share_server: Data structure with share server information. Not used by this driver. """ hnas_share_id = self._get_hnas_share_id(share['id']) LOG.debug("Expanding share in HNAS: %(shr_id)s.", {'shr_id': share['id']}) self._extend_share(hnas_share_id, share, new_size) LOG.info(_LI("Share %(shr_id)s successfully extended to " "%(shr_size)s."), {'shr_id': share['id'], 'shr_size': six.text_type(new_size)}) # TODO(alyson): Implement in DHSS = true mode def get_network_allocations_number(self): """Track allocations_number in DHSS = true. When using the setting driver_handles_share_server = false does not require to track allocations_number because we do not handle network stuff. """ return 0 def _update_share_stats(self, data=None): """Updates the Capability of Backend.""" LOG.debug("Updating Backend Capability Information - Hitachi HNAS.") self._check_fs_mounted() total_space, free_space, dedupe = self.hnas.get_stats() reserved = self.configuration.safe_get('reserved_share_percentage') data = { 'share_backend_name': self.backend_name, 'driver_handles_share_servers': self.driver_handles_share_servers, 'vendor_name': 'Hitachi', 'driver_version': '3.0.0', 'storage_protocol': 'NFS_CIFS', 'total_capacity_gb': total_space, 'free_capacity_gb': free_space, 'reserved_percentage': reserved, 'qos': False, 'thin_provisioning': True, 'dedupe': dedupe, 'revert_to_snapshot_support': True, 'mount_snapshot_support': True, } LOG.info(_LI("HNAS Capabilities: %(data)s."), {'data': six.text_type(data)}) super(HitachiHNASDriver, self)._update_share_stats(data) def manage_existing(self, share, driver_options): """Manages a share that exists on backend. :param share: Share that will be managed. :param driver_options: Empty dict or dict with 'volume_id' option. :returns: Returns a dict with size of the share managed and a list of dicts containing its export locations. Example for NFS:: { 'size': 10, 'export_locations': [ { 'path': '172.24.44.10:/shares/id', 'metadata': {}, 'is_admin_only': False }, { 'path': '192.168.0.10:/shares/id', 'metadata': {}, 'is_admin_only': True } ] } Example for CIFS:: { 'size': 10, 'export_locations': [ { 'path': '\\172.24.44.10\id', 'metadata': {}, 'is_admin_only': False }, { 'path': '\\192.168.0.10\id', 'metadata': {}, 'is_admin_only': True } ] } """ hnas_share_id = self._get_hnas_share_id(share['id']) # Make sure returned value is the same as provided, # confirming it does not exist. if hnas_share_id != share['id']: msg = _("Share ID %s already exists, cannot manage.") % share['id'] raise exception.HNASBackendException(msg=msg) self._check_protocol(share['id'], share['share_proto']) if share['share_proto'].lower() == 'nfs': # 10.0.0.1:/shares/example LOG.info(_LI("Share %(shr_path)s will be managed with ID " "%(shr_id)s."), {'shr_path': share['export_locations'][0]['path'], 'shr_id': share['id']}) old_path_info = share['export_locations'][0]['path'].split( ':/shares/') if len(old_path_info) == 2: evs_ip = old_path_info[0] hnas_share_id = old_path_info[1] else: msg = _("Incorrect path. It should have the following format: " "IP:/shares/share_id.") raise exception.ShareBackendException(msg=msg) else: # then its CIFS # \\10.0.0.1\example old_path = share['export_locations'][0]['path'].split('\\') if len(old_path) == 4: evs_ip = old_path[2] hnas_share_id = old_path[3] else: msg = _("Incorrect path. It should have the following format: " "\\\\IP\\share_id.") raise exception.ShareBackendException(msg=msg) if evs_ip != self.hnas_evs_ip: msg = _("The EVS IP %(evs)s is not " "configured.") % {'evs': evs_ip} raise exception.ShareBackendException(msg=msg) if self.backend_name not in share['host']: msg = _("The backend passed in the host parameter (%(shr)s) is " "not configured.") % {'shr': share['host']} raise exception.ShareBackendException(msg=msg) output = self._manage_existing(share, hnas_share_id) self.private_storage.update( share['id'], {'hnas_id': hnas_share_id}) LOG.debug("HNAS ID %(hnas_id)s has been saved to private storage for " "Share ID %(share_id)s", {'hnas_id': hnas_share_id, 'share_id': share['id']}) LOG.info(_LI("Share %(shr_path)s was successfully managed with ID " "%(shr_id)s."), {'shr_path': share['export_locations'][0]['path'], 'shr_id': share['id']}) return output def unmanage(self, share): """Unmanages a share. :param share: Share that will be unmanaged. """ self.private_storage.delete(share['id']) if len(share['export_locations']) == 0: LOG.info(_LI("The share with ID %(shr_id)s is no longer being " "managed."), {'shr_id': share['id']}) else: LOG.info(_LI("The share with current path %(shr_path)s and ID " "%(shr_id)s is no longer being managed."), {'shr_path': share['export_locations'][0]['path'], 'shr_id': share['id']}) def shrink_share(self, share, new_size, share_server=None): """Shrinks a share to new size. :param share: Share that will be shrunk. :param new_size: New size of share. :param share_server: Data structure with share server information. Not used by this driver. """ hnas_share_id = self._get_hnas_share_id(share['id']) LOG.debug("Shrinking share in HNAS: %(shr_id)s.", {'shr_id': share['id']}) self._shrink_share(hnas_share_id, share, new_size) LOG.info(_LI("Share %(shr_id)s successfully shrunk to " "%(shr_size)sG."), {'shr_id': share['id'], 'shr_size': six.text_type(new_size)}) def revert_to_snapshot(self, context, snapshot, access_rules, share_server=None): """Reverts a share to a given snapshot. :param context: The `context.RequestContext` object for the request :param snapshot: The snapshot to which the share is to be reverted to. :param access_rules: List of all access rules for the affected share. Not used by this driver. :param share_server: Data structure with share server information. Not used by this driver. """ self._check_fs_mounted() hnas_share_id = self._get_hnas_share_id(snapshot['share_id']) hnas_snapshot_id = self._get_hnas_snapshot_id(snapshot) dest_path = os.path.join('/shares', hnas_share_id) src_path = os.path.join('/snapshots', hnas_share_id, hnas_snapshot_id) self.hnas.tree_delete(dest_path) self.hnas.vvol_create(hnas_share_id) self.hnas.quota_add(hnas_share_id, snapshot['size']) try: self.hnas.tree_clone(src_path, dest_path) except exception.HNASNothingToCloneException: LOG.warning(_LW("Source directory is empty, creating an empty " "directory.")) LOG.info(_LI("Share %(share)s successfully reverted to snapshot " "%(snapshot)s."), {'share': snapshot['share_id'], 'snapshot': snapshot['id']}) def _get_hnas_share_id(self, share_id): hnas_id = self.private_storage.get(share_id, 'hnas_id') if hnas_id is None: hnas_id = share_id LOG.debug("Share ID is %(shr_id)s and respective HNAS ID " "is %(hnas_id)s.", {'shr_id': share_id, 'hnas_id': hnas_id}) return hnas_id def _get_hnas_snapshot_id(self, snapshot): hnas_snapshot_id = snapshot['id'] if snapshot['provider_location']: LOG.debug("Snapshot %(snap_id)s with provider_location: " "%(p_loc)s.", {'snap_id': hnas_snapshot_id, 'p_loc': snapshot['provider_location']}) hnas_snapshot_id = snapshot['provider_location'].split('/')[-1] return hnas_snapshot_id def _create_share(self, share_id, share_size, share_proto): """Creates share. Creates a virtual-volume, adds a quota limit and exports it. :param share_id: manila's database ID of share that will be created. :param share_size: Size limit of share. :param share_proto: Protocol of share that will be created (NFS or CIFS) :returns: Returns a list of dicts containing the new share's export locations. """ self._check_fs_mounted() self.hnas.vvol_create(share_id) self.hnas.quota_add(share_id, share_size) LOG.debug("Share created with id %(shr)s, size %(size)sG.", {'shr': share_id, 'size': share_size}) self._create_export(share_id, share_proto) export_list = self._get_export_locations(share_proto, share_id) return export_list def _create_export(self, share_id, share_proto, snapshot_id=None): try: if share_proto.lower() == 'nfs': # Create NFS export self.hnas.nfs_export_add(share_id, snapshot_id=snapshot_id) LOG.debug("NFS Export created to %(shr)s.", {'shr': share_id}) else: # Create CIFS share with vvol path self.hnas.cifs_share_add(share_id, snapshot_id=snapshot_id) LOG.debug("CIFS share created to %(shr)s.", {'shr': share_id}) except exception.HNASBackendException: with excutils.save_and_reraise_exception(): if snapshot_id is None: self.hnas.vvol_delete(share_id) def _check_fs_mounted(self): mounted = self.hnas.check_fs_mounted() if not mounted: msg = _("Filesystem %s is not mounted.") % self.fs_name raise exception.HNASBackendException(msg=msg) def _ensure_share(self, share, hnas_share_id): """Ensure that share is exported. :param share: Share that will be checked. :param hnas_share_id: HNAS ID of share that will be checked. :returns: Returns a list of dicts containing the share's export locations. """ self._check_protocol(share['id'], share['share_proto']) self._check_fs_mounted() self.hnas.check_vvol(hnas_share_id) self.hnas.check_quota(hnas_share_id) if share['share_proto'].lower() == 'nfs': self.hnas.check_export(hnas_share_id) else: self.hnas.check_cifs(hnas_share_id) export_list = self._get_export_locations( share['share_proto'], hnas_share_id) return export_list def _shrink_share(self, hnas_share_id, share, new_size): """Shrinks a share to new size. :param hnas_share_id: HNAS ID of share that will be shrunk. :param share: model of share that will be shrunk. :param new_size: New size of share after shrink operation. """ self._ensure_share(share, hnas_share_id) usage = self.hnas.get_share_usage(hnas_share_id) LOG.debug("Usage space in share %(share)s: %(usage)sG", {'share': share['id'], 'usage': usage}) if new_size > usage: self.hnas.modify_quota(hnas_share_id, new_size) else: raise exception.ShareShrinkingPossibleDataLoss( share_id=share['id']) def _extend_share(self, hnas_share_id, share, new_size): """Extends a share to new size. :param hnas_share_id: HNAS ID of share that will be extended. :param share: model of share that will be extended. :param new_size: New size of share after extend operation. """ self._ensure_share(share, hnas_share_id) old_size = share['size'] available_space = self.hnas.get_stats()[1] LOG.debug("Available space in filesystem: %(space)sG.", {'space': available_space}) if (new_size - old_size) < available_space: self.hnas.modify_quota(hnas_share_id, new_size) else: msg = (_("Share %s cannot be extended due to insufficient space.") % share['id']) raise exception.HNASBackendException(msg=msg) def _delete_share(self, hnas_share_id, share_proto): """Deletes share. It uses tree-delete-job-submit to format and delete virtual-volumes. Quota is deleted with virtual-volume. :param hnas_share_id: HNAS ID of share that will be deleted. :param share_proto: Protocol of share that will be deleted. """ self._check_fs_mounted() if share_proto.lower() == 'nfs': self.hnas.nfs_export_del(hnas_share_id) elif share_proto.lower() == 'cifs': self.hnas.cifs_share_del(hnas_share_id) self.hnas.vvol_delete(hnas_share_id) def _manage_existing(self, share, hnas_share_id): """Manages a share that exists on backend. :param share: share that will be managed. :param hnas_share_id: HNAS ID of share that will be managed. :returns: Returns a dict with size of the share managed and a list of dicts containing its export locations. """ self._ensure_share(share, hnas_share_id) share_size = self.hnas.get_share_quota(hnas_share_id) if share_size is None: msg = (_("The share %s trying to be managed does not have a " "quota limit, please set it before manage.") % share['id']) raise exception.ManageInvalidShare(reason=msg) export_list = self._get_export_locations( share['share_proto'], hnas_share_id) return {'size': share_size, 'export_locations': export_list} def _create_snapshot(self, hnas_share_id, snapshot): """Creates a snapshot of share. It copies the directory and all files to a new directory inside /snapshots/share_id/. :param hnas_share_id: HNAS ID of share for snapshot. :param snapshot: Snapshot that will be created. """ self._ensure_share(snapshot['share'], hnas_share_id) saved_list = [] share_proto = snapshot['share']['share_proto'] self._check_protocol(snapshot['share_id'], share_proto) if share_proto.lower() == 'nfs': saved_list = self.hnas.get_nfs_host_list(hnas_share_id) new_list = [] for access in saved_list: for rw in ('read_write', 'readwrite', 'rw'): access = access.replace(rw, 'ro') new_list.append(access) self.hnas.update_nfs_access_rule(new_list, share_id=hnas_share_id) else: # CIFS if (self.hnas.is_cifs_in_use(hnas_share_id) and not self.cifs_snapshot): msg = _("CIFS snapshot when share is mounted is disabled. " "Set hitachi_hnas_allow_cifs_snapshot_while_mounted to" " True or unmount the share to take a snapshot.") raise exception.ShareBackendException(msg=msg) src_path = os.path.join('/shares', hnas_share_id) dest_path = os.path.join('/snapshots', hnas_share_id, snapshot['id']) try: self.hnas.tree_clone(src_path, dest_path) except exception.HNASNothingToCloneException: LOG.warning(_LW("Source directory is empty, creating an empty " "directory.")) self.hnas.create_directory(dest_path) finally: if share_proto.lower() == 'nfs': self.hnas.update_nfs_access_rule(saved_list, share_id=hnas_share_id) export_locations = [] if snapshot['share'].get('mount_snapshot_support'): self._create_export(hnas_share_id, share_proto, snapshot_id=snapshot['id']) export_locations = self._get_export_locations( share_proto, snapshot['id'], is_snapshot=True) return export_locations def _delete_snapshot(self, share, hnas_share_id, snapshot_id): """Deletes snapshot. It receives the hnas_share_id only to join the path for snapshot. :param hnas_share_id: HNAS ID of share from which snapshot was taken. :param snapshot_id: ID of snapshot. """ self._check_fs_mounted() share_proto = share['share_proto'] if share.get('mount_snapshot_support'): if share_proto.lower() == 'nfs': self.hnas.nfs_export_del(snapshot_id=snapshot_id) elif share_proto.lower() == 'cifs': self.hnas.cifs_share_del(snapshot_id) path = os.path.join('/snapshots', hnas_share_id, snapshot_id) self.hnas.tree_delete(path) path = os.path.join('/snapshots', hnas_share_id) self.hnas.delete_directory(path) def _create_share_from_snapshot(self, share, src_hnas_share_id, hnas_snapshot_id): """Creates a new share from snapshot. It copies everything from snapshot directory to a new vvol, set a quota limit for it and export. :param share: a dict from new share. :param src_hnas_share_id: HNAS ID of share from which snapshot was taken. :param hnas_snapshot_id: HNAS ID from snapshot that will be copied to new share. :returns: Returns a list of dicts containing the new share's export locations. """ dest_path = os.path.join('/shares', share['id']) src_path = os.path.join('/snapshots', src_hnas_share_id, hnas_snapshot_id) # Before copying everything to new vvol, we need to create it, # because we only can transform an empty directory into a vvol. self._check_fs_mounted() self.hnas.vvol_create(share['id']) self.hnas.quota_add(share['id'], share['size']) try: self.hnas.tree_clone(src_path, dest_path) except exception.HNASNothingToCloneException: LOG.warning(_LW("Source directory is empty, exporting " "directory.")) self._check_protocol(share['id'], share['share_proto']) try: if share['share_proto'].lower() == 'nfs': self.hnas.nfs_export_add(share['id']) else: self.hnas.cifs_share_add(share['id']) except exception.HNASBackendException: with excutils.save_and_reraise_exception(): self.hnas.vvol_delete(share['id']) return self._get_export_locations( share['share_proto'], share['id']) def _check_protocol(self, share_id, protocol): if protocol.lower() not in ('nfs', 'cifs'): msg = _("Only NFS or CIFS protocol are currently supported. " "Share provided %(share)s with protocol " "%(proto)s.") % {'share': share_id, 'proto': protocol} raise exception.ShareBackendException(msg=msg) def _get_export_locations(self, share_proto, hnas_id, is_snapshot=False): export_list = [] for ip in (self.hnas_evs_ip, self.hnas_admin_network_ip): if ip: path = self._get_export_path(ip, share_proto, hnas_id, is_snapshot) export_list.append({ "path": path, "is_admin_only": ip == self.hnas_admin_network_ip, "metadata": {}, }) return export_list def _get_export_path(self, ip, share_proto, hnas_id, is_snapshot): """Gets and returns export path. :param ip: IP from HNAS EVS configured. :param share_proto: Share or snapshot protocol (NFS or CIFS). :param hnas_id: Entity ID in HNAS, it can be the ID from a share or a snapshot. :param is_snapshot: Boolean to determine if export is related to a share or a snapshot. :return: Complete export path, for example: - In NFS: SHARE: 172.24.44.10:/shares/id SNAPSHOT: 172.24.44.10:/snapshots/id - In CIFS: SHARE and SNAPSHOT: \\172.24.44.10\id """ if share_proto.lower() == 'nfs': if is_snapshot: path = os.path.join('/snapshots', hnas_id) else: path = os.path.join('/shares', hnas_id) export = ':'.join((ip, path)) else: export = r'\\%s\%s' % (ip, hnas_id) return export def _ensure_snapshot(self, snapshot, hnas_snapshot_id): """Ensure that snapshot is exported. :param snapshot: Snapshot that will be checked. :param hnas_snapshot_id: HNAS ID of snapshot that will be checked. :returns: Returns a list of dicts containing the snapshot's export locations or None if mount_snapshot_support is False. """ self._check_protocol(snapshot['share_id'], snapshot['share']['share_proto']) self._check_fs_mounted() export_list = None if snapshot['share'].get('mount_snapshot_support'): if snapshot['share']['share_proto'].lower() == 'nfs': self.hnas.check_export(hnas_snapshot_id, is_snapshot=True) else: self.hnas.check_cifs(hnas_snapshot_id) export_list = self._get_export_locations( snapshot['share']['share_proto'], hnas_snapshot_id, is_snapshot=True) return export_list def ensure_snapshot(self, context, snapshot, share_server=None): """Ensure that snapshot is exported. :param context: The `context.RequestContext` object for the request. :param snapshot: Snapshot that will be checked. :param share_server: Data structure with share server information. Not used by this driver. :returns: Returns a list of dicts containing the EVS IP concatenated with the path of snapshot in the filesystem or None if mount_snapshot_support is False. Example for NFS:: [ { 'path': '172.24.44.10:/snapshots/id', 'metadata': {}, 'is_admin_only': False }, { 'path': '192.168.0.10:/snapshots/id', 'metadata': {}, 'is_admin_only': True } ] Example for CIFS:: [ { 'path': '\\172.24.44.10\id', 'metadata': {}, 'is_admin_only': False }, { 'path': '\\192.168.0.10\id', 'metadata': {}, 'is_admin_only': True } ] """ LOG.debug("Ensuring snapshot in HNAS: %(snap)s.", {'snap': snapshot['id']}) hnas_snapshot_id = self._get_hnas_snapshot_id(snapshot) export_list = self._ensure_snapshot(snapshot, hnas_snapshot_id) LOG.debug("Snapshot ensured in HNAS: %(snap)s, protocol %(proto)s.", {'snap': snapshot['id'], 'proto': snapshot['share']['share_proto']}) return export_list def manage_existing_snapshot(self, snapshot, driver_options): """Manages a snapshot that exists only in HNAS. The snapshot to be managed should be in the path /snapshots/SHARE_ID/SNAPSHOT_ID. Also, the size of snapshot should be provided as --driver_options size=. :param snapshot: snapshot that will be managed. :param driver_options: expects only one key 'size'. It must be provided in order to manage a snapshot. :returns: Returns a dict with size of snapshot managed """ try: snapshot_size = int(driver_options.get("size", 0)) except (ValueError, TypeError): msg = _("The size in driver options to manage snapshot " "%(snap_id)s should be an integer, in format " "driver-options size=. Value passed: " "%(size)s.") % {'snap_id': snapshot['id'], 'size': driver_options.get("size")} raise exception.ManageInvalidShareSnapshot(reason=msg) if snapshot_size == 0: msg = _("Snapshot %(snap_id)s has no size specified for manage. " "Please, provide the size with parameter driver-options " "size=.") % {'snap_id': snapshot['id']} raise exception.ManageInvalidShareSnapshot(reason=msg) hnas_share_id = self._get_hnas_share_id(snapshot['share_id']) LOG.debug("Path provided to manage snapshot: %(path)s.", {'path': snapshot['provider_location']}) path_info = snapshot['provider_location'].split('/') if len(path_info) == 4 and path_info[1] == 'snapshots': path_share_id = path_info[2] hnas_snapshot_id = path_info[3] else: msg = (_("Incorrect path %(path)s for manage snapshot " "%(snap_id)s. It should have the following format: " "/snapshots/SHARE_ID/SNAPSHOT_ID.") % {'path': snapshot['provider_location'], 'snap_id': snapshot['id']}) raise exception.ManageInvalidShareSnapshot(reason=msg) if hnas_share_id != path_share_id: msg = _("The snapshot %(snap_id)s does not belong to share " "%(share_id)s.") % {'snap_id': snapshot['id'], 'share_id': snapshot['share_id']} raise exception.ManageInvalidShareSnapshot(reason=msg) if not self.hnas.check_snapshot(snapshot['provider_location']): msg = _("Snapshot %(snap_id)s does not exist in " "HNAS.") % {'snap_id': hnas_snapshot_id} raise exception.ManageInvalidShareSnapshot(reason=msg) try: self._ensure_snapshot(snapshot, hnas_snapshot_id) except exception.HNASItemNotFoundException: LOG.warning(_LW("Export does not exist for snapshot %s, " "creating a new one."), snapshot['id']) self._create_export(hnas_share_id, snapshot['share']['share_proto'], snapshot_id=hnas_snapshot_id) output = {'size': snapshot_size} if snapshot['share'].get('mount_snapshot_support'): export_locations = self._get_export_locations( snapshot['share']['share_proto'], hnas_snapshot_id, is_snapshot=True) output['export_locations'] = export_locations LOG.info(_LI("Snapshot %(snap_path)s for share %(shr_id)s was " "successfully managed with ID %(snap_id)s."), {'snap_path': snapshot['provider_location'], 'shr_id': snapshot['share_id'], 'snap_id': snapshot['id']}) return output def unmanage_snapshot(self, snapshot): """Unmanage a share snapshot :param snapshot: Snapshot that will be unmanaged. """ LOG.info(_LI("The snapshot with ID %(snap_id)s from share " "%(share_id)s is no longer being managed by Manila. " "However, it is not deleted and can be found in HNAS."), {'snap_id': snapshot['id'], 'share_id': snapshot['share_id']}) def snapshot_update_access(self, context, snapshot, access_rules, add_rules, delete_rules, share_server=None): """Update access rules for given snapshot. Drivers should support 2 different cases in this method: 1. Recovery after error - 'access_rules' contains all access rules, 'add_rules' and 'delete_rules' shall be empty. Driver should clear any existent access rules and apply all access rules for given snapshot. This recovery is made at driver start up. 2. Adding/Deleting of several access rules - 'access_rules' contains all access rules, 'add_rules' and 'delete_rules' contain rules which should be added/deleted. Driver can ignore rules in 'access_rules' and apply only rules from 'add_rules' and 'delete_rules'. All snapshots rules should be read only. :param context: Current context :param snapshot: Snapshot model with snapshot data. :param access_rules: All access rules for given snapshot :param add_rules: Empty List or List of access rules which should be added. access_rules already contains these rules. :param delete_rules: Empty List or List of access rules which should be removed. access_rules doesn't contain these rules. :param share_server: None or Share server model """ hnas_snapshot_id = self._get_hnas_snapshot_id(snapshot) self._ensure_snapshot(snapshot, hnas_snapshot_id) access_rules, add_rules, delete_rules = utils.change_rules_to_readonly( access_rules, add_rules, delete_rules) if snapshot['share']['share_proto'].lower() == 'nfs': host_list = [] for rule in access_rules: if rule['access_type'].lower() != 'ip': msg = _("Only IP access type currently supported for NFS. " "Snapshot provided %(snapshot)s with rule type " "%(type)s.") % {'snapshot': snapshot['id'], 'type': rule['access_type']} raise exception.InvalidSnapshotAccess(reason=msg) host_list.append(rule['access_to'] + '(ro)') self.hnas.update_nfs_access_rule(host_list, snapshot_id=hnas_snapshot_id) if host_list: LOG.debug("Snapshot %(snapshot)s has the rules: %(rules)s", {'snapshot': snapshot['id'], 'rules': ', '.join(host_list)}) else: LOG.debug("Snapshot %(snapshot)s has no rules.", {'snapshot': snapshot['id']}) else: if not (add_rules or delete_rules): # cifs recovery mode self._clean_cifs_access_list(hnas_snapshot_id, is_snapshot=True) self._cifs_allow_access(snapshot, hnas_snapshot_id, access_rules, is_snapshot=True) else: self._cifs_deny_access(snapshot, hnas_snapshot_id, delete_rules, is_snapshot=True) self._cifs_allow_access(snapshot, hnas_snapshot_id, add_rules, is_snapshot=True)