From 2e709aa87d36f737d50fba0adbf5d892d6e74c0a Mon Sep 17 00:00:00 2001 From: Mark Sturdevant Date: Sat, 6 Dec 2014 23:51:25 -0800 Subject: [PATCH] HP 3PAR Driver for Manila Implement share driver for HP 3PAR arrays with File Persona capabilities. NFS and CIFS shares can be created and deleted. Snapshots can be created and deleted. Read-only shares can be created from snapshots. Does not support handling of share servers. A pre-assigned IP address and a pre-configured VFS is used instead of share networks. This is done by setting 'hp3par_share_ip_address' to the desired IP address in the manila.conf. DocImpact Change-Id: I90e19a1d34d568eba6178ca3dbb605f37c598439 Implements: blueprint hp3par-manila-driver --- manila/exception.py | 12 + manila/opts.py | 1 + manila/share/drivers/hp/__init__.py | 0 manila/share/drivers/hp/hp_3par_driver.py | 279 ++++++++ manila/share/drivers/hp/hp_3par_mediator.py | 540 ++++++++++++++++ manila/tests/share/drivers/hp/__init__.py | 0 .../drivers/hp/test_hp_3par_constants.py | 59 ++ .../share/drivers/hp/test_hp_3par_driver.py | 408 ++++++++++++ .../share/drivers/hp/test_hp_3par_mediator.py | 610 ++++++++++++++++++ 9 files changed, 1909 insertions(+) create mode 100644 manila/share/drivers/hp/__init__.py create mode 100644 manila/share/drivers/hp/hp_3par_driver.py create mode 100644 manila/share/drivers/hp/hp_3par_mediator.py create mode 100644 manila/tests/share/drivers/hp/__init__.py create mode 100644 manila/tests/share/drivers/hp/test_hp_3par_constants.py create mode 100644 manila/tests/share/drivers/hp/test_hp_3par_driver.py create mode 100644 manila/tests/share/drivers/hp/test_hp_3par_mediator.py 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)