diff --git a/manila/exception.py b/manila/exception.py index caae7006..145b0795 100644 --- a/manila/exception.py +++ b/manila/exception.py @@ -458,6 +458,18 @@ class EMCVnxXMLAPIError(Invalid): message = _("%(err)s") +class HP3ParInvalidClient(Invalid): + message = _("%(err)s") + + +class HP3ParInvalid(Invalid): + message = _("%(err)s") + + +class HP3ParUnexpectedError(ManilaException): + message = _("%(err)s") + + class GPFSException(ManilaException): message = _("GPFS exception occurred.") diff --git a/manila/opts.py b/manila/opts.py index 83fb5a43..4023d307 100644 --- a/manila/opts.py +++ b/manila/opts.py @@ -108,6 +108,7 @@ _global_opt_lists = [ manila.share.drivers.glusterfs.GlusterfsManilaShare_opts, manila.share.drivers.glusterfs_native.glusterfs_native_manila_share_opts, manila.share.drivers.hds.sop.hdssop_share_opts, + manila.share.drivers.hp.hp_3par_driver.HP3PAR_OPTS, manila.share.drivers.huawei.huawei_nas.huawei_opts, manila.share.drivers.ibm.gpfs.gpfs_share_opts, manila.share.drivers.netapp.cluster_mode.NETAPP_NAS_OPTS, diff --git a/manila/share/drivers/hp/__init__.py b/manila/share/drivers/hp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manila/share/drivers/hp/hp_3par_driver.py b/manila/share/drivers/hp/hp_3par_driver.py new file mode 100644 index 00000000..0534709b --- /dev/null +++ b/manila/share/drivers/hp/hp_3par_driver.py @@ -0,0 +1,279 @@ +# Copyright 2015 Hewlett Packard Development Company, L.P. +# +# 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. + +"""HP 3PAR Driver for OpenStack Manila.""" + +import hashlib +import inspect +import logging + +from oslo_config import cfg +import six + +from manila import exception +from manila.i18n import _ +from manila.i18n import _LI +from manila.openstack.common import log +from manila.share import driver +from manila.share.drivers.hp import hp_3par_mediator + +HP3PAR_OPTS = [ + cfg.StrOpt('hp3par_api_url', + default='', + help="3PAR WSAPI Server Url like " + "https://<3par ip>:8080/api/v1"), + cfg.StrOpt('hp3par_username', + default='', + help="3PAR Super user username"), + cfg.StrOpt('hp3par_password', + default='', + help="3PAR Super user password", + secret=True), + cfg.StrOpt('hp3par_san_ip', + default='', + help="IP address of SAN controller"), + cfg.StrOpt('hp3par_san_login', + default='', + help="Username for SAN controller"), + cfg.StrOpt('hp3par_san_password', + default='', + help="Password for SAN controller", + secret=True), + cfg.IntOpt('hp3par_san_ssh_port', + default=22, + help='SSH port to use with SAN'), + cfg.StrOpt('hp3par_fpg', + default="OpenStack", + help="The File Provisioning Group (FPG) to use"), + cfg.StrOpt('hp3par_share_ip_address', + default='', + help="The IP address for shares not using a share server"), + cfg.BoolOpt('hp3par_debug', + default=False, + help="Enable HTTP debugging to 3PAR"), +] + +CONF = cfg.CONF +CONF.register_opts(HP3PAR_OPTS) + +LOG = log.getLogger(__name__) + + +class HP3ParShareDriver(driver.ShareDriver): + """HP 3PAR driver for Manila. + + Supports NFS and CIFS protocols on arrays with File Persona. + """ + + def __init__(self, *args, **kwargs): + super(HP3ParShareDriver, self).__init__(False, *args, **kwargs) + + self.configuration = kwargs.get('configuration', None) + self.configuration.append_config_values(HP3PAR_OPTS) + self.configuration.append_config_values(driver.ssh_opts) + self.fpg = None + self.vfs = None + self.share_ip_address = None + self._hp3par = None # mediator between driver and client + + def do_setup(self, context): + """Any initialization the share driver does while starting.""" + + self.share_ip_address = self.configuration.hp3par_share_ip_address + if not self.share_ip_address: + raise exception.HP3ParInvalid( + _("Unsupported configuration. " + "hp3par_share_ip_address is not set.")) + + mediator = hp_3par_mediator.HP3ParMediator( + hp3par_username=self.configuration.hp3par_username, + hp3par_password=self.configuration.hp3par_password, + hp3par_api_url=self.configuration.hp3par_api_url, + hp3par_debug=self.configuration.hp3par_debug, + hp3par_san_ip=self.configuration.hp3par_san_ip, + hp3par_san_login=self.configuration.hp3par_san_login, + hp3par_san_password=self.configuration.hp3par_san_password, + hp3par_san_ssh_port=self.configuration.hp3par_san_ssh_port, + ssh_conn_timeout=self.configuration.ssh_conn_timeout, + ) + + mediator.do_setup() + + # FPG must be configured and must exist. + self.fpg = self.configuration.safe_get('hp3par_fpg') + # Validate the FPG and discover the VFS + # This also validates the client, connection, firmware, WSAPI, FPG... + self.vfs = mediator.get_vfs_name(self.fpg) + + # Don't set _hp3par until it is ready. Otherwise _update_stats fails. + self._hp3par = mediator + + def check_for_setup_error(self): + + try: + # Log the source SHA for support. Only do this with DEBUG. + if LOG.isEnabledFor(logging.DEBUG): + driver_source = inspect.getsourcelines(HP3ParShareDriver) + driver_sha1 = hashlib.sha1('blob %(source_size)s\0%(' + 'source_string)s' % + { + 'source_size': len( + driver_source), + 'source_string': driver_source, + }) + LOG.debug('HP3ParShareDriver SHA1: %s', + driver_sha1.hexdigest()) + + mediator_source = inspect.getsourcelines( + hp_3par_mediator.HP3ParMediator) + mediator_sha1 = hashlib.sha1( + 'blob %(source_size)s\0%(source_string)s' % + { + 'source_size': len(mediator_source), + 'source_string': mediator_source, + }) + LOG.debug('HP3ParMediator SHA1: %s', mediator_sha1.hexdigest()) + except Exception as e: + # Don't let any exceptions during the SHA1 logging interfere + # with startup. This is just debug info to identify the source + # code. If it doesn't work, just log a debug message. + LOG.debug('Source code SHA1 not logged due to: %s', + six.text_type(e)) + + @staticmethod + def _build_export_location(protocol, ip, path): + if protocol == 'NFS': + location = ':'.join((ip, path)) + elif protocol == 'CIFS': + location = '\\\\%s\%s' % (ip, path) + else: + message = _('Invalid protocol. Expected NFS or CIFS. ' + 'Got %s.') % protocol + raise exception.InvalidInput(message) + return location + + def create_share(self, context, share, share_server=None): + """Is called to create share.""" + + ip = self.share_ip_address + + protocol = share['share_proto'] + path = self._hp3par.create_share( + share['id'], + protocol, + self.fpg, self.vfs, + size=share['size'] + ) + + return self._build_export_location(protocol, ip, path) + + def create_share_from_snapshot(self, context, share, snapshot, + share_server=None): + """Is called to create share from snapshot.""" + + ip = self.share_ip_address + + protocol = share['share_proto'] + path = self._hp3par.create_share_from_snapshot( + share['id'], + protocol, + snapshot['share']['id'], + snapshot['id'], + self.fpg, + self.vfs + ) + + return self._build_export_location(protocol, ip, path) + + def delete_share(self, context, share, share_server=None): + """Deletes share and its fstore.""" + + self._hp3par.delete_share(share['id'], + share['share_proto'], + self.fpg, + self.vfs) + + def create_snapshot(self, context, snapshot, share_server=None): + """Creates a snapshot of a share.""" + + self._hp3par.create_snapshot(snapshot['share']['id'], + snapshot['id'], + self.fpg, + self.vfs) + + def delete_snapshot(self, context, snapshot, share_server=None): + """Deletes a snapshot of a share.""" + + self._hp3par.delete_snapshot(snapshot['share']['id'], + snapshot['id'], + self.fpg, + self.vfs) + + def ensure_share(self, context, share, share_server=None): + pass + + def allow_access(self, context, share, access, share_server=None): + """Allow access to the share.""" + self._hp3par.allow_access(share['id'], + share['share_proto'], + access['access_type'], + access['access_to'], + self.fpg, + self.vfs) + + def deny_access(self, context, share, access, share_server=None): + """Deny access to the share.""" + self._hp3par.deny_access(share['id'], + share['share_proto'], + access['access_type'], + access['access_to'], + self.fpg, + self.vfs) + + def _update_share_stats(self): + """Retrieve stats info from share group.""" + + if not self._hp3par: + LOG.info( + _LI("Skipping share statistics update. Setup has not " + "completed.")) + total_capacity_gb = 0 + free_capacity_gb = 0 + else: + capacity_stats = self._hp3par.get_capacity(self.fpg) + LOG.debug("Share capacity = %s.", capacity_stats) + total_capacity_gb = capacity_stats['total_capacity_gb'] + free_capacity_gb = capacity_stats['free_capacity_gb'] + + backend_name = self.configuration.safe_get( + 'share_backend_name') or "HP_3PAR" + + reserved_share_percentage = self.configuration.safe_get( + 'reserved_share_percentage') + if reserved_share_percentage is None: + reserved_share_percentage = 0 + + stats = { + 'share_backend_name': backend_name, + 'driver_handles_share_servers': self.driver_handles_share_servers, + 'vendor_name': 'HP', + 'driver_version': '1.0', + 'storage_protocol': 'NFS_CIFS', + 'total_capacity_gb': total_capacity_gb, + 'free_capacity_gb': free_capacity_gb, + 'reserved_percentage': reserved_share_percentage, + 'QoS_support': False, + } + + super(HP3ParShareDriver, self)._update_share_stats(stats) diff --git a/manila/share/drivers/hp/hp_3par_mediator.py b/manila/share/drivers/hp/hp_3par_mediator.py new file mode 100644 index 00000000..bed5fcdb --- /dev/null +++ b/manila/share/drivers/hp/hp_3par_mediator.py @@ -0,0 +1,540 @@ +# Copyright 2015 Hewlett Packard Development Company, L.P. +# +# 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. + +"""HP 3PAR Mediator for OpenStack Manila. + +This 'mediator' de-couples the 3PAR focused client from the OpenStack focused +driver. +""" + +from oslo_utils import importutils +from oslo_utils import units +import six + +from manila import exception +from manila.i18n import _ +from manila.i18n import _LI +from manila.openstack.common import log as logging + +hp3parclient = importutils.try_import("hp3parclient") +if hp3parclient: + from hp3parclient import file_client + + +LOG = logging.getLogger(__name__) +DENY = '-' +ALLOW = '+' +OPEN_STACK_MANILA_FSHARE = 'OpenStack Manila fshare' + + +class HP3ParMediator(object): + + def __init__(self, **kwargs): + + self.hp3par_username = kwargs.get('hp3par_username') + self.hp3par_password = kwargs.get('hp3par_password') + self.hp3par_api_url = kwargs.get('hp3par_api_url') + self.hp3par_debug = kwargs.get('hp3par_debug') + self.hp3par_san_ip = kwargs.get('hp3par_san_ip') + self.hp3par_san_login = kwargs.get('hp3par_san_login') + self.hp3par_san_password = kwargs.get('hp3par_san_password') + self.hp3par_san_ssh_port = kwargs.get('hp3par_san_ssh_port') + self.hp3par_san_private_key = kwargs.get('hp3par_san_private_key') + + self.ssh_conn_timeout = kwargs.get('ssh_conn_timeout') + self._client = None + + def do_setup(self): + + if hp3parclient is None: + msg = _('You must install hp3parclient before using the 3PAR ' + 'driver.') + LOG.exception(msg) + raise exception.HP3ParInvalidClient(message=msg) + + try: + self._client = file_client.HP3ParFilePersonaClient( + self.hp3par_api_url) + except Exception as e: + msg = (_('Failed to connect to HP 3PAR File Persona Client: %s') % + six.text_type(e)) + LOG.exception(msg) + raise exception.ShareBackendException(message=msg) + + try: + ssh_kwargs = {} + if self.hp3par_san_ssh_port: + ssh_kwargs['port'] = self.hp3par_san_ssh_port + if self.ssh_conn_timeout: + ssh_kwargs['conn_timeout'] = self.ssh_conn_timeout + if self.hp3par_san_private_key: + ssh_kwargs['privatekey'] = self.hp3par_san_private_key + + self._client.setSSHOptions( + self.hp3par_san_ip, + self.hp3par_san_login, + self.hp3par_san_password, + **ssh_kwargs + ) + + except Exception as e: + msg = (_('Failed to set SSH options for HP 3PAR File Persona ' + 'Client: %s') % six.text_type(e)) + LOG.exception(msg) + raise exception.ShareBackendException(message=msg) + + if self.hp3par_debug: + self._client.ssh.set_debug_flag(True) + + def get_capacity(self, fpg): + try: + result = self._client.getfpg(fpg) + except Exception as e: + msg = (_('Failed to get capacity for fpg %(fpg)s: %(e)s') % + {'fpg': fpg, 'e': six.text_type(e)}) + LOG.exception(msg) + raise exception.ShareBackendException(message=msg) + + if result['total'] != 1: + msg = (_('Failed to get capacity for fpg %s.') % fpg) + LOG.exception(msg) + raise exception.ShareBackendException(message=msg) + else: + member = result['members'][0] + total_capacity_gb = int(member['capacityKiB']) / units.Mi + free_capacity_gb = int(member['availCapacityKiB']) / units.Mi + return { + 'total_capacity_gb': total_capacity_gb, + 'free_capacity_gb': free_capacity_gb + } + + @staticmethod + def ensure_supported_protocol(share_proto): + protocol = share_proto.lower() + if protocol == 'cifs': + protocol = 'smb' + if protocol not in ['smb', 'nfs']: + message = (_('Invalid protocol. Expected nfs or smb. Got %s.') % + protocol) + LOG.exception(message) + raise exception.InvalidInput(message) + return protocol + + @staticmethod + def ensure_prefix(id): + if id.startswith('osf-'): + return id + else: + return 'osf-%s' % id + + def create_share(self, share_id, share_proto, fpg, vfs, + fstore=None, sharedir=None, readonly=False, size=None): + """Create the share and return its path. + + This method can create a share when called by the driver or when + called locally from create_share_from_snapshot(). The optional + parameters allow re-use. + + :param share_id: The share-id with or without osf- prefix. + :param share_proto: The protocol (to map to smb or nfs) + :param fpg: The file provisioning group + :param vfs: The virtual file system + :param fstore: (optional) The file store. When provided, an existing + file store is used. Otherwise one is created. + :param sharedir: (optional) Share directory. + :param readonly: (optional) Create share as read-only. + :param size: (optional) Size limit for file store if creating one. + :return: share path string + """ + + protocol = self.ensure_supported_protocol(share_proto) + share_name = self.ensure_prefix(share_id) + + if not fstore: + fstore = share_name + try: + result = self._client.createfstore( + vfs, fstore, fpg=fpg, + comment='OpenStack Manila fstore') + LOG.debug("createfstore result=%s", result) + except Exception as e: + msg = (_('Failed to create fstore %(fstore)s: %(e)s') % + {'fstore': fstore, 'e': six.text_type(e)}) + LOG.exception(msg) + raise exception.ShareBackendException(msg) + + if size: + try: + size_str = six.text_type(size) + result = self._client.setfsquota( + vfs, fpg=fpg, fstore=fstore, + scapacity=size_str, hcapacity=size_str) + LOG.debug("setfsquota result=%s", result) + except Exception as e: + msg = (_('Failed to setfsquota on %(fstore)s: %(e)s') % + {'fstore': fstore, 'e': six.text_type(e)}) + LOG.exception(msg) + raise exception.ShareBackendException(msg) + + try: + if protocol == 'nfs': + if readonly: + options = 'ro,no_root_squash,insecure' + else: + options = 'rw,no_root_squash,insecure' + + result = self._client.createfshare( + protocol, vfs, share_name, + fpg=fpg, fstore=fstore, sharedir=sharedir, + clientip='127.0.0.1', + options=options, + comment=OPEN_STACK_MANILA_FSHARE) + else: + result = self._client.createfshare( + protocol, vfs, share_name, + fpg=fpg, fstore=fstore, sharedir=sharedir, + allowip='127.0.0.1', + comment=OPEN_STACK_MANILA_FSHARE) + LOG.debug("createfshare result=%s", result) + + except Exception as e: + msg = (_('Failed to create share %(share_name)s: %(e)s') % + {'share_name': share_name, 'e': six.text_type(e)}) + LOG.exception(msg) + raise exception.ShareBackendException(msg) + + try: + result = self._client.getfshare( + protocol, share_name, + fpg=fpg, vfs=vfs, fstore=fstore) + LOG.debug("getfshare result=%s", result) + + except Exception as e: + msg = (_('Failed to get fshare %(share_name)s after creating it: ' + '%(e)s') % {'share_name': share_name, + 'e': six.text_type(e)}) + LOG.exception(msg) + raise exception.ShareBackendException(msg) + + if result['total'] != 1: + msg = (_('Failed to get fshare %(share_name)s after creating it. ' + 'Expected to get 1 fshare. Got %(total)s.') % + {'share_name': share_name, 'total': result['total']}) + LOG.exception(msg) + raise exception.ShareBackendException(msg) + + if protocol == 'nfs': + return result['members'][0]['sharePath'] + else: + return result['members'][0]['shareName'] + + def create_share_from_snapshot(self, share_id, share_proto, orig_share_id, + snapshot_id, fpg, vfs): + + share_name = self.ensure_prefix(share_id) + orig_share_name = self.ensure_prefix(orig_share_id) + fstore = orig_share_name + snapshot_tag = self.ensure_prefix(snapshot_id) + snapshots = self.get_snapshots(fstore, snapshot_tag, fpg, vfs) + + if len(snapshots) != 1: + msg = (_('Failed to create share from snapshot for ' + 'FPG/VFS/fstore/tag %(fpg)s/%(vfs)s/%(fstore)s/%(tag)s.' + ' Expected to find 1 snapshot, found %(count)s.') % + {'fpg': fpg, 'vfs': vfs, 'fstore': fstore, + 'tag': snapshot_tag, 'count': len(snapshots)}) + LOG.exception(msg) + raise exception.ShareBackendException(msg) + + snapshot = snapshots[0] + sharedir = '.snapshot/%s' % snapshot['snapName'] + + return self.create_share( + share_name, + share_proto, + fpg, + vfs, + fstore=fstore, + sharedir=sharedir, + readonly=True, + ) + + def delete_share(self, share_id, share_proto, fpg, vfs): + + share_name = self.ensure_prefix(share_id) + fstore = self.get_fstore(share_id, share_proto, fpg, vfs) + protocol = self.ensure_supported_protocol(share_proto) + + if not fstore: + # Share does not exist. + return + + try: + self._client.removefshare(protocol, vfs, share_name, + fpg=fpg, fstore=fstore) + except Exception as e: + msg = (_('Failed to remove share %(share_name)s: %(e)s') % + {'share_name': share_name, 'e': six.text_type(e)}) + LOG.exception(msg) + raise exception.ShareBackendException(message=msg) + + try: + self._client.removefstore(vfs, fstore, fpg=fpg) + except Exception as e: + msg = (_('Failed to remove fstore %(fstore)s: %(e)s') % + {'fstore': fstore, 'e': six.text_type(e)}) + LOG.exception(msg) + raise exception.ShareBackendException(message=msg) + + def get_vfs_name(self, fpg): + return self.get_vfs(fpg)['vfsname'] + + def get_vfs(self, fpg, vfs=None): + """Get the VFS or raise an exception.""" + + try: + result = self._client.getvfs(fpg=fpg, vfs=vfs) + except Exception as e: + msg = (_('Exception during getvfs %(vfs)s: %(e)s') % + {'vfs': vfs, 'e': six.text_type(e)}) + LOG.exception(msg) + raise exception.ShareBackendException(msg) + + if result['total'] != 1: + error_msg = result.get('message') + if error_msg: + message = (_('Error while validating FPG/VFS ' + '(%(fpg)s/%(vfs)s): %(msg)s') % + {'fpg': fpg, 'vfs': vfs, 'msg': error_msg}) + LOG.error(message) + raise exception.ShareBackendException(message) + else: + message = (_('Error while validating FPG/VFS ' + '(%(fpg)s/%(vfs)s): Expected 1, ' + 'got %(total)s.') % + {'fpg': fpg, 'vfs': vfs, + 'total': result['total']}) + + LOG.error(message) + raise exception.ShareBackendException(message) + + return result['members'][0] + + def create_snapshot(self, orig_share_id, snapshot_id, fpg, vfs): + """Creates a snapshot of a share.""" + + fstore = self.ensure_prefix(orig_share_id) + snapshot_tag = self.ensure_prefix(snapshot_id) + try: + result = self._client.createfsnap( + vfs, fstore, snapshot_tag, fpg=fpg) + + LOG.debug("createfsnap result=%s", result) + + except Exception as e: + msg = (_('Failed to create snapshot for FPG/VFS/fstore ' + '%(fpg)s/%(vfs)s/%(fstore)s: %(e)s') % + {'fpg': fpg, 'vfs': vfs, 'fstore': fstore, + 'e': six.text_type(e)}) + LOG.exception(msg) + raise exception.ShareBackendException(msg) + + def get_snapshots(self, orig_share_id, snapshot_tag, fpg, vfs): + fstore = self.ensure_prefix(orig_share_id) + try: + pattern = '*_%s' % snapshot_tag + result = self._client.getfsnap( + pattern, fpg=fpg, vfs=vfs, fstore=fstore, pat=True) + + LOG.debug("getfsnap result=%s", result) + + except Exception as e: + msg = (_('Failed to get snapshot for FPG/VFS/fstore/tag ' + '%(fpg)s/%(vfs)s/%(fstore)s/%(tag)s: %(e)s') % + {'fpg': fpg, 'vfs': vfs, 'fstore': fstore, + 'tag': snapshot_tag, 'e': six.text_type(e)}) + LOG.exception(msg) + raise exception.ShareBackendException(msg) + + if result['total'] == 0: + LOG.info((_LI('Found zero snapshots for FPG/VFS/fstore/tag ' + '%(fpg)s/%(vfs)s/%(fstore)s/%(tag)s.') % + {'fpg': fpg, 'vfs': vfs, 'fstore': fstore, + 'tag': snapshot_tag})) + + return result['members'] + + def delete_snapshot(self, orig_share_id, snapshot_id, fpg, vfs): + """Deletes a snapshot of a share.""" + + fstore = self.ensure_prefix(orig_share_id) + snapshot_tag = self.ensure_prefix(snapshot_id) + snapshots = self.get_snapshots(fstore, snapshot_tag, fpg, vfs) + + if not snapshots: + return + + for protocol in ('nfs', 'smb'): + try: + shares = self._client.getfshare(protocol, + fpg=fpg, + vfs=vfs, + fstore=fstore) + except Exception as e: + msg = (_('Unexpected exception while getting share list. ' + 'Cannot delete snapshot without checking for ' + 'dependent shares first: %s') % six.text_type(e)) + LOG.exception(msg) + raise exception.ShareBackendException(msg) + + for share in shares['members']: + if protocol == 'nfs': + path = share['sharePath'][1:].split('/') + dot_snapshot_index = 3 + else: + path = share['shareDir'].split('/') + dot_snapshot_index = 0 + + snapshot_index = dot_snapshot_index + if len(path) > snapshot_index + 1: + if (path[dot_snapshot_index] == '.snapshot' and + path[snapshot_index].endswith(snapshot_tag)): + msg = (_('Cannot delete snapshot because it has a ' + 'dependent share.')) + raise exception.Invalid(msg) + + # Tag should be unique enough to only return one, but this method + # doesn't really need to know that. So just loop. + for snapshot in snapshots: + try: + snapname = snapshot['snapName'] + result = self._client.removefsnap( + vfs, fstore, snapname=snapname, fpg=fpg) + + LOG.debug("removefsnap result=%s", result) + + except Exception as e: + msg = (_('Failed to delete snapshot for FPG/VFS/fstore ' + '%(fpg)s/%(vfs)s/%(fstore)s: %(e)s') % + {'fpg': fpg, 'vfs': vfs, 'fstore': fstore, + 'e': six.text_type(e)}) + LOG.exception(msg) + raise exception.ShareBackendException(msg) + + # Try to reclaim the space + try: + self._client.startfsnapclean(fpg, reclaimStrategy='maxspeed') + except Exception as e: + # Remove already happened so only log this. + msg = (_('Unexpected exception calling startfsnapclean for FPG ' + '%(fpg)s: %(e)s') % {'fpg': fpg, 'e': six.text_type(e)}) + LOG.exception(msg) + + @staticmethod + def validate_access_type(protocol, access_type): + + if access_type not in ('ip', 'user'): + msg = (_("Invalid access type. Expected 'ip' or 'user'. " + "Actual '%s'.") % access_type) + LOG.exception(msg) + raise exception.InvalidInput(msg) + + if protocol == 'nfs' and access_type != 'ip': + msg = (_("Invalid NFS access type. HP 3PAR NFS supports 'ip'. " + "Actual '%s'.") % access_type) + LOG.exception(msg) + raise exception.HP3ParInvalid(msg) + + return protocol + + def _change_access(self, plus_or_minus, fstore, share_id, share_proto, + access_type, access_to, fpg, vfs): + """Allow or deny access to a share. + + Plus_or_minus character indicates add to allow list (+) or remove from + allow list (-). + """ + + share_name = self.ensure_prefix(share_id) + fstore = self.ensure_prefix(fstore) + + protocol = self.ensure_supported_protocol(share_proto) + self.validate_access_type(protocol, access_type) + + try: + if protocol == 'nfs': + result = self._client.setfshare( + protocol, vfs, share_name, fpg=fpg, fstore=fstore, + clientip='%s%s' % (plus_or_minus, access_to)) + elif protocol == 'smb': + if access_type == 'ip': + result = self._client.setfshare( + protocol, vfs, share_name, fpg=fpg, fstore=fstore, + allowip='%s%s' % (plus_or_minus, access_to)) + else: + access_str = 'fullcontrol' + perm = '%s%s:%s' % (plus_or_minus, access_to, access_str) + result = self._client.setfshare(protocol, vfs, share_name, + fpg=fpg, fstore=fstore, + allowperm=perm) + else: + msg = (_("Unexpected error: After ensure_supported_protocol " + "only 'nfs' or 'smb' strings are allowed, but found: " + "%s.") % protocol) + raise exception.HP3ParUnexpectedError(msg) + + LOG.debug("setfshare result=%s", result) + except Exception as e: + msg = (_('Failed to change (%(change)s) access to FPG/share ' + '%(fpg)s/%(share)s to %(type)s %(to)s): %(e)s') % + {'change': plus_or_minus, 'fpg': fpg, 'share': share_name, + 'type': access_type, 'to': access_to, + 'e': six.text_type(e)}) + LOG.exception(msg) + raise exception.ShareBackendException(msg) + + def get_fstore(self, share_id, share_proto, fpg, vfs): + + protocol = self.ensure_supported_protocol(share_proto) + share_name = self.ensure_prefix(share_id) + try: + shares = self._client.getfshare(protocol, + share_name, + fpg=fpg, + vfs=vfs) + except Exception as e: + msg = (_('Unexpected exception while getting share list: %s') % + six.text_type(e)) + raise exception.ShareBackendException(msg) + + members = shares['members'] + if members: + return members[0].get('fstoreName') + + def allow_access(self, share_id, share_proto, access_type, access_to, + fpg, vfs): + """Grant access to a share.""" + + fstore = self.get_fstore(share_id, share_proto, fpg, vfs) + self._change_access(ALLOW, fstore, share_id, share_proto, + access_type, access_to, fpg, vfs) + + def deny_access(self, share_id, share_proto, access_type, access_to, + fpg, vfs): + """Deny access to a share.""" + + fstore = self.get_fstore(share_id, share_proto, fpg, vfs) + if fstore: + self._change_access(DENY, fstore, share_id, share_proto, + access_type, access_to, fpg, vfs) diff --git a/manila/tests/share/drivers/hp/__init__.py b/manila/tests/share/drivers/hp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manila/tests/share/drivers/hp/test_hp_3par_constants.py b/manila/tests/share/drivers/hp/test_hp_3par_constants.py new file mode 100644 index 00000000..9861c913 --- /dev/null +++ b/manila/tests/share/drivers/hp/test_hp_3par_constants.py @@ -0,0 +1,59 @@ +# Copyright 2015 Hewlett Packard Development Company, L.P. +# +# 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. +CIFS = 'CIFS' +SMB_LOWER = 'smb' +NFS = 'NFS' +NFS_LOWER = 'nfs' +IP = 'ip' +USER = 'user' +USERNAME = 'USERNAME_0' +PASSWORD = 'PASSWORD_0' +SAN_LOGIN = 'testlogin4san' +SAN_PASSWORD = 'testpassword4san' +API_URL = 'https://1.2.3.4:8080/api/v1' +TIMEOUT = 60 +PORT = 22 + +# Constants to use with Mock and expect in results +EXPECTED_IP_10203040 = '10.20.30.40' +EXPECTED_IP_1234 = '1.2.3.4' +EXPECTED_IP_127 = '127.0.0.1' +EXPECTED_SHARE_ID = 'osf-share-id' +EXPECTED_SHARE_NAME = 'share-name' +EXPECTED_SHARE_PATH = '/share/path' +EXPECTED_SIZE_1 = 1 +EXPECTED_SIZE_2 = 2 +EXPECTED_SNAP_NAME = 'osf-snap-name' +EXPECTED_SNAP_ID = 'osf-snap-id' +EXPECTED_STATS = {'test': 'stats'} +EXPECTED_FPG = 'FPG_1' +EXPECTED_FSTORE = 'osf-test_fstore' +EXPECTED_VFS = 'test_vfs' +EXPECTED_HP_DEBUG = True + +NFS_SHARE_INFO = { + 'id': EXPECTED_SHARE_ID, + 'share_proto': NFS, +} + +ACCESS_INFO = { + 'access_type': IP, + 'access_to': EXPECTED_IP_1234, +} + +SNAPSHOT_INFO = { + 'name': EXPECTED_SNAP_NAME, + 'id': EXPECTED_SNAP_ID, + 'share': {'id': EXPECTED_SHARE_ID}, +} diff --git a/manila/tests/share/drivers/hp/test_hp_3par_driver.py b/manila/tests/share/drivers/hp/test_hp_3par_driver.py new file mode 100644 index 00000000..297e9ed9 --- /dev/null +++ b/manila/tests/share/drivers/hp/test_hp_3par_driver.py @@ -0,0 +1,408 @@ +# Copyright 2015 Hewlett Packard Development Company, L.P. +# +# 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 sys + +import mock +if 'hp3parclient' not in sys.modules: + sys.modules['hp3parclient'] = mock.Mock() + +from manila import exception +from manila.share.drivers.hp import hp_3par_driver as hp3pardriver +from manila.share.drivers.hp import hp_3par_mediator as hp3parmediator +from manila import test +from manila.tests.share.drivers.hp import test_hp_3par_constants as constants + + +class HP3ParDriverTestCase(test.TestCase): + + def setUp(self): + super(HP3ParDriverTestCase, self).setUp() + + # Create a mock configuration with attributes and a safe_get() + self.conf = mock.Mock() + self.conf.driver_handles_share_servers = False + self.conf.hp3par_debug = constants.EXPECTED_HP_DEBUG + self.conf.hp3par_username = constants.USERNAME + self.conf.hp3par_password = constants.PASSWORD + self.conf.hp3par_api_url = constants.API_URL + self.conf.hp3par_san_login = constants.SAN_LOGIN + self.conf.hp3par_san_password = constants.SAN_PASSWORD + self.conf.hp3par.san_ip = constants.EXPECTED_IP_1234 + self.conf.hp3par_fpg = constants.EXPECTED_FPG + self.conf.hp3par_san_ssh_port = constants.PORT + self.conf.ssh_conn_timeout = constants.TIMEOUT + self.conf.hp3par_share_ip_address = constants.EXPECTED_IP_10203040 + self.conf.network_config_group = 'test_network_config_group' + + def safe_get(attr): + try: + return self.conf.__getattribute__(attr) + except AttributeError: + return None + self.conf.safe_get = safe_get + + self.mock_object(hp3parmediator, 'HP3ParMediator') + self.mock_mediator_constructor = hp3parmediator.HP3ParMediator + self.mock_mediator = self.mock_mediator_constructor() + + self.driver = hp3pardriver.HP3ParShareDriver( + configuration=self.conf) + + def test_driver_setup_success(self): + """Driver do_setup without any errors.""" + + self.mock_mediator.get_vfs_name.return_value = constants.EXPECTED_VFS + + self.driver.do_setup(None) + self.mock_mediator_constructor.assert_has_calls([ + mock.call(hp3par_san_ssh_port=self.conf.hp3par_san_ssh_port, + hp3par_san_password=self.conf.hp3par_san_password, + hp3par_username=self.conf.hp3par_username, + hp3par_san_login=self.conf.hp3par_san_login, + hp3par_debug=self.conf.hp3par_debug, + hp3par_api_url=self.conf.hp3par_api_url, + hp3par_password=self.conf.hp3par_password, + hp3par_san_ip=self.conf.hp3par_san_ip, + ssh_conn_timeout=self.conf.ssh_conn_timeout)]) + + self.mock_mediator.assert_has_calls([ + mock.call.do_setup(), + mock.call.get_vfs_name(self.conf.hp3par_fpg)]) + + self.assertEqual(constants.EXPECTED_VFS, self.driver.vfs) + + def test_driver_with_setup_error(self): + """Driver do_setup when the mediator setup fails.""" + + self.mock_mediator.do_setup.side_effect = ( + exception.ShareBackendException('fail')) + + self.assertRaises(exception.ShareBackendException, + self.driver.do_setup, None) + + self.mock_mediator_constructor.assert_has_calls([ + mock.call(hp3par_san_ssh_port=self.conf.hp3par_san_ssh_port, + hp3par_san_password=self.conf.hp3par_san_password, + hp3par_username=self.conf.hp3par_username, + hp3par_san_login=self.conf.hp3par_san_login, + hp3par_debug=self.conf.hp3par_debug, + hp3par_api_url=self.conf.hp3par_api_url, + hp3par_password=self.conf.hp3par_password, + hp3par_san_ip=self.conf.hp3par_san_ip, + ssh_conn_timeout=self.conf.ssh_conn_timeout)]) + + self.mock_mediator.assert_has_calls([mock.call.do_setup()]) + + def test_driver_with_vfs_error(self): + """Driver do_setup when the get_vfs_name fails.""" + + self.mock_mediator.get_vfs_name.side_effect = ( + exception.ShareBackendException('fail')) + + self.assertRaises(exception.ShareBackendException, + self.driver.do_setup, None) + + self.mock_mediator_constructor.assert_has_calls([ + mock.call(hp3par_san_ssh_port=self.conf.hp3par_san_ssh_port, + hp3par_san_password=self.conf.hp3par_san_password, + hp3par_username=self.conf.hp3par_username, + hp3par_san_login=self.conf.hp3par_san_login, + hp3par_debug=self.conf.hp3par_debug, + hp3par_api_url=self.conf.hp3par_api_url, + hp3par_password=self.conf.hp3par_password, + hp3par_san_ip=self.conf.hp3par_san_ip, + ssh_conn_timeout=self.conf.ssh_conn_timeout)]) + + self.mock_mediator.assert_has_calls([ + mock.call.do_setup(), + mock.call.get_vfs_name(self.conf.hp3par_fpg)]) + + def init_driver(self): + """Simple driver setup for re-use with tests that need one.""" + + self.driver._hp3par = self.mock_mediator + self.driver.vfs = constants.EXPECTED_VFS + self.driver.fpg = constants.EXPECTED_FPG + self.driver.share_ip_address = self.conf.hp3par_share_ip_address + + def do_create_share(self, protocol, expected_share_id, expected_size): + """Re-usable code for create share.""" + context = None + share_server = None + share = { + 'id': expected_share_id, + 'share_proto': protocol, + 'size': expected_size, + } + location = self.driver.create_share(context, share, share_server) + return location + + def do_create_share_from_snapshot(self, + protocol, + snapshot_id, + expected_share_id, + expected_size): + """Re-usable code for create share from snapshot.""" + context = None + share_server = None + share = { + 'id': expected_share_id, + 'share_proto': protocol, + 'size': expected_size, + } + location = self.driver.create_share_from_snapshot(context, + share, + snapshot_id, + share_server) + return location + + def test_driver_check_for_setup_error(self): + """check_for_setup_error should not raise any exceptions.""" + + self.mock_object(hp3pardriver, 'LOG') + self.init_driver() + self.driver.check_for_setup_error() + expected_calls = [mock.call.debug(mock.ANY, mock.ANY), + mock.call.debug(mock.ANY, mock.ANY)] + hp3pardriver.LOG.assert_has_calls(expected_calls) + + def test_driver_create_cifs_share(self): + self.init_driver() + + expected_location = '\\\\%s\%s' % (constants.EXPECTED_IP_10203040, + constants.EXPECTED_SHARE_NAME) + + self.mock_mediator.create_share.return_value = ( + constants.EXPECTED_SHARE_NAME) + + location = self.do_create_share(constants.CIFS, + constants.EXPECTED_SHARE_ID, + constants.EXPECTED_SIZE_2) + + self.assertEqual(expected_location, location) + expected_calls = [mock.call.create_share( + constants.EXPECTED_SHARE_ID, + constants.CIFS, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS, + size=constants.EXPECTED_SIZE_2)] + self.mock_mediator.assert_has_calls(expected_calls) + + def test_driver_create_nfs_share(self): + self.init_driver() + + expected_location = ':'.join((constants.EXPECTED_IP_10203040, + constants.EXPECTED_SHARE_PATH)) + + self.mock_mediator.create_share.return_value = ( + constants.EXPECTED_SHARE_PATH) + + location = self.do_create_share(constants.NFS, + constants.EXPECTED_SHARE_ID, + constants.EXPECTED_SIZE_1) + + self.assertEqual(expected_location, location) + expected_calls = [ + mock.call.create_share(constants.EXPECTED_SHARE_ID, + constants.NFS, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS, + size=constants.EXPECTED_SIZE_1)] + + self.mock_mediator.assert_has_calls(expected_calls) + + def test_driver_create_cifs_share_from_snapshot(self): + self.init_driver() + + expected_location = '\\\\%s\%s' % (constants.EXPECTED_IP_10203040, + constants.EXPECTED_SHARE_NAME) + + self.mock_mediator.create_share_from_snapshot.return_value = ( + constants.EXPECTED_SHARE_NAME) + + location = self.do_create_share_from_snapshot( + constants.CIFS, + constants.SNAPSHOT_INFO, + constants.EXPECTED_SHARE_ID, + constants.EXPECTED_SIZE_2) + + self.assertEqual(expected_location, location) + expected_calls = [ + mock.call.create_share_from_snapshot(constants.EXPECTED_SHARE_ID, + constants.CIFS, + constants.EXPECTED_SHARE_ID, + constants.EXPECTED_SNAP_ID, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS)] + self.mock_mediator.assert_has_calls(expected_calls) + + def test_driver_create_nfs_share_from_snapshot(self): + self.init_driver() + + expected_location = ':'.join((constants.EXPECTED_IP_10203040, + constants.EXPECTED_SHARE_PATH)) + + self.mock_mediator.create_share_from_snapshot.return_value = ( + constants.EXPECTED_SHARE_PATH) + + location = self.do_create_share_from_snapshot( + constants.NFS, + constants.SNAPSHOT_INFO, + constants.EXPECTED_SHARE_ID, + constants.EXPECTED_SIZE_1) + + self.assertEqual(expected_location, location) + expected_calls = [ + mock.call.create_share_from_snapshot(constants.EXPECTED_SHARE_ID, + constants.NFS, + constants.EXPECTED_SHARE_ID, + constants.EXPECTED_SNAP_ID, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS)] + + self.mock_mediator.assert_has_calls(expected_calls) + + def test_driver_delete_share(self): + self.init_driver() + + context = None + share_server = None + share = { + 'id': constants.EXPECTED_SHARE_ID, + 'share_proto': constants.CIFS, + 'size': constants.EXPECTED_SIZE_1, + } + + self.driver.delete_share(context, share, share_server) + + expected_calls = [ + mock.call.delete_share(constants.EXPECTED_SHARE_ID, + constants.CIFS, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS)] + + self.mock_mediator.assert_has_calls(expected_calls) + + def test_driver_create_snapshot(self): + self.init_driver() + + context = None + share_server = None + self.driver.create_snapshot(context, + constants.SNAPSHOT_INFO, + share_server) + + expected_calls = [ + mock.call.create_snapshot(constants.EXPECTED_SHARE_ID, + constants.EXPECTED_SNAP_ID, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS)] + self.mock_mediator.assert_has_calls(expected_calls) + + def test_driver_delete_snapshot(self): + self.init_driver() + + context = None + share_server = None + self.driver.delete_snapshot(context, + constants.SNAPSHOT_INFO, + share_server) + + expected_calls = [ + mock.call.delete_snapshot(constants.EXPECTED_SHARE_ID, + constants.EXPECTED_SNAP_ID, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + ] + self.mock_mediator.assert_has_calls(expected_calls) + + def test_driver_allow_access(self): + self.init_driver() + + context = None + self.driver.allow_access(context, + constants.NFS_SHARE_INFO, + constants.ACCESS_INFO) + + expected_calls = [ + mock.call.allow_access(constants.EXPECTED_SHARE_ID, + constants.NFS, + constants.IP, + constants.EXPECTED_IP_1234, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + ] + self.mock_mediator.assert_has_calls(expected_calls) + + def test_driver_deny_access(self): + self.init_driver() + + context = None + self.driver.deny_access(context, + constants.NFS_SHARE_INFO, + constants.ACCESS_INFO) + + expected_calls = [ + mock.call.deny_access(constants.EXPECTED_SHARE_ID, + constants.NFS, + constants.IP, + constants.EXPECTED_IP_1234, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + ] + self.mock_mediator.assert_has_calls(expected_calls) + + def test_driver_get_share_stats_no_refresh(self): + """Driver does not call mediator when refresh=False.""" + + self.init_driver() + self.driver._stats = constants.EXPECTED_STATS + + result = self.driver.get_share_stats(refresh=False) + + self.assertEqual(constants.EXPECTED_STATS, result) + self.assertEqual([], self.mock_mediator.mock_calls) + + def test_driver_get_share_stats_with_refresh(self): + """Driver adds stats from mediator to expected structure.""" + + self.init_driver() + expected_free = constants.EXPECTED_SIZE_1 + expected_capacity = constants.EXPECTED_SIZE_2 + + self.mock_mediator.get_capacity.return_value = { + 'free_capacity_gb': expected_free, + 'total_capacity_gb': expected_capacity + } + + expected_result = { + 'driver_handles_share_servers': False, + 'QoS_support': False, + 'driver_version': '1.0', + 'free_capacity_gb': expected_free, + 'reserved_percentage': 0, + 'share_backend_name': 'HP_3PAR', + 'storage_protocol': 'NFS_CIFS', + 'total_capacity_gb': expected_capacity, + 'vendor_name': 'HP', + } + + result = self.driver.get_share_stats(refresh=True) + self.assertEqual(expected_result, result) + + expected_calls = [ + mock.call.get_capacity(constants.EXPECTED_FPG) + ] + self.mock_mediator.assert_has_calls(expected_calls) diff --git a/manila/tests/share/drivers/hp/test_hp_3par_mediator.py b/manila/tests/share/drivers/hp/test_hp_3par_mediator.py new file mode 100644 index 00000000..e47fe6db --- /dev/null +++ b/manila/tests/share/drivers/hp/test_hp_3par_mediator.py @@ -0,0 +1,610 @@ +# Copyright 2015 Hewlett Packard Development Company, L.P. +# +# 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 sys + +import mock +if 'hp3parclient' not in sys.modules: + sys.modules['hp3parclient'] = mock.Mock() +import six + +from manila import exception +from manila.share.drivers.hp import hp_3par_mediator as hp3parmediator +from manila import test +from manila.tests.share.drivers.hp import test_hp_3par_constants as constants + +from oslo_utils import units + + +class HP3ParMediatorTestCase(test.TestCase): + + def setUp(self): + super(HP3ParMediatorTestCase, self).setUp() + + # This is the fake client to use. + self.mock_client = mock.Mock() + + # Take over the hp3parclient module and stub the constructor. + hp3parclient = sys.modules['hp3parclient'] + + # Need a fake constructor to return the fake client. + # This is also be used for constructor error tests. + self.mock_object(hp3parclient.file_client, 'HP3ParFilePersonaClient') + self.mock_client_constructor = ( + hp3parclient.file_client.HP3ParFilePersonaClient + ) + self.mock_client = self.mock_client_constructor() + + # Set the mediator to use in tests. + self.mediator = hp3parmediator.HP3ParMediator( + hp3par_username=constants.USERNAME, + hp3par_password=constants.PASSWORD, + hp3par_api_url=constants.API_URL, + hp3par_debug=constants.EXPECTED_HP_DEBUG, + hp3par_san_ip=constants.EXPECTED_IP_1234, + hp3par_san_login=constants.SAN_LOGIN, + hp3par_san_password=constants.SAN_PASSWORD, + hp3par_san_ssh_port=constants.PORT, + ssh_conn_timeout=constants.TIMEOUT) + + def test_mediator_setup_client_init_error(self): + """Any client init exceptions should result in a ManilaException.""" + + self.mock_client_constructor.side_effect = ( + Exception('Any exception. E.g., bad version or some other ' + 'non-Manila Exception.')) + self.assertRaises(exception.ManilaException, self.mediator.do_setup) + + def test_mediator_setup_client_ssh_error(self): + + # This could be anything the client comes up with, but the + # mediator should turn it into a ManilaException. + non_manila_exception = Exception('non-manila-except') + self.mock_client.setSSHOptions.side_effect = non_manila_exception + + self.assertRaises(exception.ManilaException, self.mediator.do_setup) + self.mock_client.assert_has_calls( + [mock.call.setSSHOptions(constants.EXPECTED_IP_1234, + constants.SAN_LOGIN, + constants.SAN_PASSWORD, + port=constants.PORT, + conn_timeout=constants.TIMEOUT)]) + + def test_mediator_vfs_exception(self): + """Backend exception during get_vfs_name.""" + + self.mediator.do_setup() + self.mock_client.getvfs.side_effect = Exception('non-manila-except') + self.assertRaises(exception.ManilaException, + self.mediator.get_vfs_name, + fpg=constants.EXPECTED_FPG) + expected_calls = [ + mock.call.getvfs(fpg=constants.EXPECTED_FPG, vfs=None), + ] + self.mock_client.assert_has_calls(expected_calls) + + def test_mediator_vfs_not_found(self): + """VFS not found.""" + self.mediator.do_setup() + self.mock_client.getvfs.return_value = {'total': 0} + self.assertRaises(exception.ManilaException, + self.mediator.get_vfs_name, + fpg=constants.EXPECTED_FPG) + expected_calls = [ + mock.call.getvfs(fpg=constants.EXPECTED_FPG, vfs=None), + ] + self.mock_client.assert_has_calls(expected_calls) + + def init_mediator(self): + """Basic mediator setup for re-use with tests that need one.""" + + self.mock_client.getvfs.return_value = { + 'total': 1, + 'members': [{'vfsname': constants.EXPECTED_VFS}] + } + self.mock_client.getfshare.return_value = { + 'total': 1, + 'members': [ + {'fstoreName': constants.EXPECTED_FSTORE, + 'shareName': constants.EXPECTED_SHARE_ID, + 'shareDir': constants.EXPECTED_SHARE_PATH, + 'sharePath': constants.EXPECTED_SHARE_PATH}] + } + self.mediator.do_setup() + + def test_mediator_setup_success(self): + """Do a mediator setup without errors.""" + + self.init_mediator() + self.assertIsNotNone(self.mediator._client) + + expected_calls = [ + mock.call.setSSHOptions(constants.EXPECTED_IP_1234, + constants.SAN_LOGIN, + constants.SAN_PASSWORD, + port=constants.PORT, + conn_timeout=constants.TIMEOUT), + mock.call.ssh.set_debug_flag(constants.EXPECTED_HP_DEBUG) + ] + self.mock_client.assert_has_calls(expected_calls) + + def get_expected_calls_for_create_share(self, + expected_fpg, + expected_vfsname, + expected_protocol, + expected_share_id, + expected_size): + size = six.text_type(expected_size) + expected_sharedir = None + + if expected_protocol == constants.NFS_LOWER: + expected_calls = [ + mock.call.createfstore(expected_vfsname, expected_share_id, + comment='OpenStack Manila fstore', + fpg=expected_fpg), + mock.call.setfsquota(expected_vfsname, fpg=expected_fpg, + hcapacity=size, + scapacity=size, + fstore=expected_share_id), + mock.call.createfshare(expected_protocol, expected_vfsname, + expected_share_id, + comment='OpenStack Manila fshare', + fpg=expected_fpg, + sharedir=expected_sharedir, + clientip='127.0.0.1', + options='rw,no_root_squash,insecure', + fstore=expected_share_id), + mock.call.getfshare(expected_protocol, expected_share_id, + fpg=expected_fpg, vfs=expected_vfsname, + fstore=expected_share_id)] + else: + expected_calls = [ + mock.call.createfstore(expected_vfsname, expected_share_id, + comment='OpenStack Manila fstore', + fpg=expected_fpg), + mock.call.setfsquota(expected_vfsname, fpg=expected_fpg, + hcapacity=size, + scapacity=size, + fstore=expected_share_id), + mock.call.createfshare(expected_protocol, expected_vfsname, + expected_share_id, + comment='OpenStack Manila fshare', + fpg=expected_fpg, + sharedir=expected_sharedir, + allowip='127.0.0.1', + fstore=expected_share_id), + mock.call.getfshare(expected_protocol, expected_share_id, + fpg=expected_fpg, vfs=expected_vfsname, + fstore=expected_share_id)] + return expected_calls + + def test_mediator_create_cifs_share(self): + self.init_mediator() + + self.mock_client.getfshare.return_value = { + 'message': None, + 'total': 1, + 'members': [{'shareName': constants.EXPECTED_SHARE_NAME}] + } + + location = self.mediator.create_share(constants.EXPECTED_SHARE_ID, + constants.CIFS, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS, + size=constants.EXPECTED_SIZE_2) + + self.assertEqual(constants.EXPECTED_SHARE_NAME, location) + + expected_calls = self.get_expected_calls_for_create_share( + constants.EXPECTED_FPG, + constants.EXPECTED_VFS, + constants.SMB_LOWER, + constants.EXPECTED_SHARE_ID, + constants.EXPECTED_SIZE_2) + + self.mock_client.assert_has_calls(expected_calls) + + def test_mediator_create_nfs_share(self): + self.init_mediator() + + self.mock_client.getfshare.return_value = { + 'message': None, + 'total': 1, + 'members': [{'sharePath': constants.EXPECTED_SHARE_PATH}] + } + + location = self.mediator.create_share(constants.EXPECTED_SHARE_ID, + constants.NFS.lower(), + constants.EXPECTED_FPG, + constants.EXPECTED_VFS, + size=constants.EXPECTED_SIZE_1) + + self.assertEqual(constants.EXPECTED_SHARE_PATH, location) + + expected_calls = self.get_expected_calls_for_create_share( + constants.EXPECTED_FPG, constants.EXPECTED_VFS, + constants.NFS.lower(), + constants.EXPECTED_SHARE_ID, + constants.EXPECTED_SIZE_1) + + self.mock_client.assert_has_calls(expected_calls) + + def test_mediator_create_cifs_share_from_snapshot(self): + self.init_mediator() + + self.mock_client.getfsnap.return_value = { + 'message': None, + 'total': 1, + 'members': [{'snapName': constants.EXPECTED_SNAP_ID}] + } + + location = self.mediator.create_share_from_snapshot( + constants.EXPECTED_SHARE_ID, + constants.CIFS, + constants.EXPECTED_SHARE_ID, + constants.EXPECTED_SNAP_ID, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + self.assertEqual(constants.EXPECTED_SHARE_ID, location) + + expected_calls = [ + mock.call.getfsnap('*_%s' % constants.EXPECTED_SNAP_ID, + vfs=constants.EXPECTED_VFS, + fpg=constants.EXPECTED_FPG, + pat=True, + fstore=constants.EXPECTED_SHARE_ID), + mock.call.createfshare(constants.SMB_LOWER, + constants.EXPECTED_VFS, + constants.EXPECTED_SHARE_ID, + comment=mock.ANY, + fpg=constants.EXPECTED_FPG, + sharedir='.snapshot/%s' % + constants.EXPECTED_SNAP_ID, + fstore=constants.EXPECTED_SHARE_ID, + allowip=constants.EXPECTED_IP_127), + mock.call.getfshare(constants.SMB_LOWER, + constants.EXPECTED_SHARE_ID, + fpg=constants.EXPECTED_FPG, + vfs=constants.EXPECTED_VFS, + fstore=constants.EXPECTED_SHARE_ID)] + + self.mock_client.assert_has_calls(expected_calls) + + def test_mediator_create_nfs_share_from_snapshot(self): + self.init_mediator() + + self.mock_client.getfsnap.return_value = { + 'message': None, + 'total': 1, + 'members': [{'snapName': constants.EXPECTED_SNAP_ID}] + } + + location = self.mediator.create_share_from_snapshot( + constants.EXPECTED_SHARE_ID, + constants.NFS, + constants.EXPECTED_SHARE_ID, + constants.EXPECTED_SNAP_ID, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + self.assertEqual(constants.EXPECTED_SHARE_PATH, location) + + expected_calls = [ + mock.call.getfsnap('*_%s' % constants.EXPECTED_SNAP_ID, + vfs=constants.EXPECTED_VFS, + fpg=constants.EXPECTED_FPG, + pat=True, + fstore=constants.EXPECTED_SHARE_ID), + mock.call.createfshare(constants.NFS_LOWER, + constants.EXPECTED_VFS, + constants.EXPECTED_SHARE_ID, + comment=mock.ANY, + fpg=constants.EXPECTED_FPG, + sharedir='.snapshot/%s' % + constants.EXPECTED_SNAP_ID, + fstore=constants.EXPECTED_SHARE_ID, + clientip=constants.EXPECTED_IP_127, + options='ro,no_root_squash,insecure'), + mock.call.getfshare(constants.NFS_LOWER, + constants.EXPECTED_SHARE_ID, + fpg=constants.EXPECTED_FPG, + vfs=constants.EXPECTED_VFS, + fstore=constants.EXPECTED_SHARE_ID)] + + self.mock_client.assert_has_calls(expected_calls) + + def test_mediator_delete_share(self): + self.init_mediator() + + self.mediator.delete_share(constants.EXPECTED_SHARE_ID, + constants.CIFS, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + expected_calls = [ + mock.call.removefshare(constants.SMB_LOWER, + constants.EXPECTED_VFS, + constants.EXPECTED_SHARE_ID, + fpg=constants.EXPECTED_FPG, + fstore=constants.EXPECTED_FSTORE), + mock.call.removefstore(constants.EXPECTED_VFS, + constants.EXPECTED_FSTORE, + fpg=constants.EXPECTED_FPG) + ] + + self.mock_client.assert_has_calls(expected_calls) + + def test_mediator_create_snapshot(self): + self.init_mediator() + + self.mediator.create_snapshot(constants.EXPECTED_SHARE_ID, + constants.EXPECTED_SNAP_NAME, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + expected_calls = [ + mock.call.createfsnap(constants.EXPECTED_VFS, + constants.EXPECTED_SHARE_ID, + constants.EXPECTED_SNAP_NAME, + fpg=constants.EXPECTED_FPG) + ] + self.mock_client.assert_has_calls(expected_calls) + + def test_mediator_delete_snapshot(self): + self.init_mediator() + + expected_name_from_array = 'name-from-array' + + self.mock_client.getfsnap.return_value = { + 'total': 1, + 'members': [{'snapName': expected_name_from_array}], + 'message': None + } + + self.mediator.delete_snapshot(constants.EXPECTED_SHARE_ID, + constants.EXPECTED_SNAP_NAME, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + expected_calls = [ + mock.call.getfsnap('*_%s' % constants.EXPECTED_SNAP_NAME, + vfs=constants.EXPECTED_VFS, + fpg=constants.EXPECTED_FPG, + pat=True, + fstore=constants.EXPECTED_SHARE_ID), + mock.call.getfshare(constants.NFS_LOWER, + fpg=constants.EXPECTED_FPG, + vfs=constants.EXPECTED_VFS, + fstore=constants.EXPECTED_SHARE_ID), + mock.call.getfshare(constants.SMB_LOWER, + fpg=constants.EXPECTED_FPG, + vfs=constants.EXPECTED_VFS, + fstore=constants.EXPECTED_SHARE_ID), + mock.call.removefsnap(constants.EXPECTED_VFS, + constants.EXPECTED_SHARE_ID, + fpg=constants.EXPECTED_FPG, + snapname=expected_name_from_array), + mock.call.startfsnapclean(constants.EXPECTED_FPG, + reclaimStrategy='maxspeed') + ] + self.mock_client.assert_has_calls(expected_calls) + + def test_mediator_get_capacity(self): + """Mediator converts client stats to capacity result.""" + expected_capacity = constants.EXPECTED_SIZE_2 + expected_free = constants.EXPECTED_SIZE_1 + + self.init_mediator() + self.mock_client.getfpg.return_value = { + 'total': 1, + 'members': [ + { + 'capacityKiB': str(expected_capacity * units.Mi), + 'availCapacityKiB': str(expected_free * units.Mi) + } + ], + 'message': None, + } + + expected_result = { + 'free_capacity_gb': expected_free, + 'total_capacity_gb': expected_capacity, + } + + result = self.mediator.get_capacity(constants.EXPECTED_FPG) + self.assertEqual(expected_result, result) + expected_calls = [ + mock.call.getfpg(constants.EXPECTED_FPG) + ] + self.mock_client.assert_has_calls(expected_calls) + + def test_mediator_allow_user_access_cifs(self): + """"Allow user access to cifs share.""" + self.init_mediator() + + expected_allowperm = '+%s:fullcontrol' % constants.USERNAME + + self.mediator.allow_access(constants.EXPECTED_SHARE_ID, + constants.CIFS, + constants.USER, + constants.USERNAME, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + expected_calls = [ + mock.call.setfshare(constants.SMB_LOWER, + constants.EXPECTED_VFS, + constants.EXPECTED_SHARE_ID, + allowperm=expected_allowperm, + fpg=constants.EXPECTED_FPG, + fstore=constants.EXPECTED_FSTORE) + + ] + self.mock_client.assert_has_calls(expected_calls) + + def test_mediator_deny_user_access_cifs(self): + """"Deny user access to cifs share.""" + self.init_mediator() + + expected_denyperm = '-%s:fullcontrol' % constants.USERNAME + + self.mediator.deny_access(constants.EXPECTED_SHARE_ID, + constants.CIFS, + constants.USER, + constants.USERNAME, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + expected_calls = [ + mock.call.setfshare(constants.SMB_LOWER, + constants.EXPECTED_VFS, + constants.EXPECTED_SHARE_ID, + allowperm=expected_denyperm, + fpg=constants.EXPECTED_FPG, + fstore=constants.EXPECTED_FSTORE) + + ] + self.mock_client.assert_has_calls(expected_calls) + + def test_mediator_allow_ip_access_cifs(self): + """"Allow ip access to cifs share.""" + self.init_mediator() + + expected_allowip = '+%s' % constants.EXPECTED_IP_1234 + + self.mediator.allow_access(constants.EXPECTED_SHARE_ID, + constants.CIFS, + constants.IP, + constants.EXPECTED_IP_1234, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + expected_calls = [ + mock.call.setfshare(constants.SMB_LOWER, + constants.EXPECTED_VFS, + constants.EXPECTED_SHARE_ID, + allowip=expected_allowip, + fpg=constants.EXPECTED_FPG, + fstore=constants.EXPECTED_FSTORE) + ] + self.mock_client.assert_has_calls(expected_calls) + + def test_mediator_deny_ip_access_cifs(self): + """"Deny ip access to cifs share.""" + self.init_mediator() + + expected_denyip = '-%s' % constants.EXPECTED_IP_1234 + + self.mediator.deny_access(constants.EXPECTED_SHARE_ID, + constants.CIFS, + constants.IP, + constants.EXPECTED_IP_1234, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + expected_calls = [ + mock.call.setfshare(constants.SMB_LOWER, + constants.EXPECTED_VFS, + constants.EXPECTED_SHARE_ID, + allowip=expected_denyip, + fpg=constants.EXPECTED_FPG, + fstore=constants.EXPECTED_FSTORE) + ] + self.mock_client.assert_has_calls(expected_calls) + + def test_mediator_allow_ip_access_nfs(self): + """"Allow ip access to nfs share.""" + self.init_mediator() + + expected_clientip = '+%s' % constants.EXPECTED_IP_1234 + + self.mediator.allow_access(constants.EXPECTED_SHARE_ID, + constants.NFS, + constants.IP, + constants.EXPECTED_IP_1234, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + expected_calls = [ + mock.call.setfshare(constants.NFS.lower(), + constants.EXPECTED_VFS, + constants.EXPECTED_SHARE_ID, + clientip=expected_clientip, + fpg=constants.EXPECTED_FPG, + fstore=constants.EXPECTED_FSTORE) + ] + self.mock_client.assert_has_calls(expected_calls) + + def test_mediator_deny_ip_access_nfs(self): + """"Deny ip access to nfs share.""" + self.init_mediator() + + expected_clientip = '-%s' % constants.EXPECTED_IP_1234 + + self.mediator.deny_access(constants.EXPECTED_SHARE_ID, + constants.NFS, + constants.IP, + constants.EXPECTED_IP_1234, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + expected_calls = [ + mock.call.setfshare(constants.NFS.lower(), + constants.EXPECTED_VFS, + constants.EXPECTED_SHARE_ID, + clientip=expected_clientip, + fpg=constants.EXPECTED_FPG, + fstore=constants.EXPECTED_FSTORE) + ] + self.mock_client.assert_has_calls(expected_calls) + + def test_mediator_allow_user_access_nfs(self): + """"Allow user access to nfs share is not supported.""" + self.init_mediator() + + self.assertRaises(exception.HP3ParInvalid, + self.mediator.allow_access, + constants.EXPECTED_SHARE_ID, + constants.NFS, + constants.USER, + constants.USERNAME, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + def test_mediator_allow_access_bad_proto(self): + """"Allow user access to unsupported protocol.""" + self.init_mediator() + + self.assertRaises(exception.InvalidInput, + self.mediator.allow_access, + constants.EXPECTED_SHARE_ID, + 'unsupported_other_protocol', + constants.USER, + constants.USERNAME, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS) + + def test_mediator_allow_access_bad_type(self): + """"Allow user access to unsupported access type.""" + self.init_mediator() + + self.assertRaises(exception.InvalidInput, + self.mediator.allow_access, + constants.EXPECTED_SHARE_ID, + constants.CIFS, + 'unsupported_other_type', + constants.USERNAME, + constants.EXPECTED_FPG, + constants.EXPECTED_VFS)