diff --git a/doc/source/admin/share_back_ends_feature_support_mapping.rst b/doc/source/admin/share_back_ends_feature_support_mapping.rst index 6b8d19f162..bc16a14cc0 100644 --- a/doc/source/admin/share_back_ends_feature_support_mapping.rst +++ b/doc/source/admin/share_back_ends_feature_support_mapping.rst @@ -65,6 +65,8 @@ Mapping of share drivers and share features support +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+ | IBM GPFS | K | O | L | \- | K | K | \- | \- | \- | +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+ +| INFINIDAT | Q | \- | Q | \- | Q | Q | \- | Q | Q | ++----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+ | LVM | M | \- | M | \- | M | M | \- | O | O | +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+ | Quobyte | K | \- | M | M | \- | \- | \- | \- | \- | @@ -132,6 +134,8 @@ Mapping of share drivers and share access rules support +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ | IBM GPFS | NFS (K) | \- | \- | \- | \- | NFS (K) | \- | \- | \- | \- | +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ +| INFINIDAT | NFS (Q) | \- | \- | \- | \- | NFS (Q) | \- | \- | \- | \- | ++----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ | Oracle ZFSSA | NFS,CIFS(K) | \- | \- | \- | \- | \- | \- | \- | \- | \- | +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ | CephFS | NFS (P) | \- | \- | \- | CEPHFS (M) | NFS (P) | \- | \- | \- | CEPHFS (N) | @@ -191,6 +195,8 @@ Mapping of share drivers and security services support +----------------------------------------+------------------+-----------------+------------------+ | IBM GPFS | \- | \- | \- | +----------------------------------------+------------------+-----------------+------------------+ +| INFINIDAT | \- | \- | \- | ++----------------------------------------+------------------+-----------------+------------------+ | Oracle ZFSSA | \- | \- | \- | +----------------------------------------+------------------+-----------------+------------------+ | CephFS | \- | \- | \- | @@ -244,6 +250,8 @@ More information: :ref:`capabilities_and_extra_specs` +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ | Huawei | M | K | L | L | L | L | M | M | \- | \- | P | \- | +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ +| INFINIDAT | \- | Q | \- | \- | Q | Q | \- | Q | Q | Q | Q | \- | ++----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ | LVM | \- | M | \- | \- | \- | M | \- | K | O | O | P | P | +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ | Quobyte | \- | K | \- | \- | \- | L | \- | M | \- | \- | P | \- | diff --git a/doc/source/configuration/shared-file-systems/drivers.rst b/doc/source/configuration/shared-file-systems/drivers.rst index f4c29fd93a..a213ea8c1a 100644 --- a/doc/source/configuration/shared-file-systems/drivers.rst +++ b/doc/source/configuration/shared-file-systems/drivers.rst @@ -25,6 +25,7 @@ Share drivers drivers/hpe-3par-share-driver.rst drivers/huawei-nas-driver.rst drivers/ibm-spectrumscale-driver.rst + drivers/infinidat-share-driver.rst drivers/maprfs-native-driver.rst drivers/netapp-cluster-mode-driver.rst drivers/quobyte-driver.rst diff --git a/doc/source/configuration/shared-file-systems/drivers/infinidat-share-driver.rst b/doc/source/configuration/shared-file-systems/drivers/infinidat-share-driver.rst new file mode 100644 index 0000000000..5e3151fd2b --- /dev/null +++ b/doc/source/configuration/shared-file-systems/drivers/infinidat-share-driver.rst @@ -0,0 +1,147 @@ +================================ +INFINIDAT InfiniBox Share driver +================================ + +The INFINIDAT Share driver provides support for managing filesystem shares +on the INFINIDAT InfiniBox storage systems. + +This section explains how to configure the INFINIDAT driver. + +Supported operations +~~~~~~~~~~~~~~~~~~~~ + +- Create and delete filesystem shares. + +- Ensure filesystem shares. + +- Extend a share. + +- Create and delete filesystem snapshots. + +- Create a share from a share snapshot. + +- Revert a share to its snapshot. + +- Mount a snapshot. + +- Set access rights to shares and snapshots. + + Note the following limitations: + + - Only IP access type is supported. + + - Both RW & RO access levels are supported. + +External package installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The driver requires the ``infinisdk`` package for communicating with +InfiniBox systems. Install the package from PyPI using the following command: + +.. code-block:: console + + $ pip install infinisdk + +Setting up the storage array +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create a storage pool object on the InfiniBox array in advance. +The storage pool will contain shares managed by OpenStack. +Refer to the InfiniBox manuals for details on pool management. + +Driver configuration +~~~~~~~~~~~~~~~~~~~~ + +Edit the ``manila.conf`` file, which is usually located under the following +path ``/etc/manila/manila.conf``. + +* Add a section for the INFINIDAT driver back end. + +* Under the ``[DEFAULT]`` section, set the ``enabled_share_backends`` parameter + with the name of the new back-end section. + +Configure the driver back-end section with the parameters below. + +* Configure the driver name by setting the following parameter: + + .. code-block:: ini + + share_driver = manila.share.drivers.infinidat.infinibox.InfiniboxShareDriver + +* Configure the management IP of the InfiniBox array by adding the following + parameter: + + .. code-block:: ini + + infinibox_hostname = InfiniBox management IP + +* Configure user credentials: + + The driver requires an InfiniBox user with administrative privileges. + We recommend creating a dedicated OpenStack user account + that holds an administrative user role. + Refer to the InfiniBox manuals for details on user account management. + Configure the user credentials by adding the following parameters: + + .. code-block:: ini + + infinibox_login = Infinibox management login + infinibox_password = Infinibox management password + +* Configure the name of the InfiniBox pool by adding the following parameter: + + .. code-block:: ini + + infinidat_pool_name = Pool as defined in the InfiniBox + +* Configure the name of the InfiniBox NAS network space by adding the following + parameter: + + .. code-block:: ini + + infinidat_nas_network_space_name = Network space as defined in the InfiniBox + +* The back-end name is an identifier for the back end. + We recommend using the same name as the name of the section. + Configure the back-end name by adding the following parameter: + + .. code-block:: ini + + share_backend_name = back-end name + +* Thin provisioning: + + The INFINIDAT driver supports creating thin or thick provisioned filesystems. + Configure thin or thick provisioning by adding the following parameter: + + .. code-block:: ini + + infinidat_thin_provision = true/false + + This parameter defaults to ``true``. + +Configuration example +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: ini + + [DEFAULT] + enabled_share_backends = infinidat-pool-a + + [infinidat-pool-a] + share_driver = manila.share.drivers.infinidat.infinibox.InfiniboxShareDriver + share_backend_name = infinidat-pool-a + driver_handles_share_servers = false + infinibox_hostname = 10.1.2.3 + infinibox_login = openstackuser + infinibox_password = openstackpass + infinidat_pool_name = pool-a + infinidat_nas_network_space_name = nas_space + infinidat_thin_provision = true + +Driver options +~~~~~~~~~~~~~~ + +Configuration options specific to this driver: + +.. include:: ../../tables/manila-infinidat.inc diff --git a/doc/source/configuration/tables/manila-infinidat.inc b/doc/source/configuration/tables/manila-infinidat.inc new file mode 100644 index 0000000000..5491978475 --- /dev/null +++ b/doc/source/configuration/tables/manila-infinidat.inc @@ -0,0 +1,22 @@ +.. _manila-infinidat: + +.. list-table:: Description of INFINIDAT InfiniBox share driver configuration options + :header-rows: 1 + :class: config-ref-table + + * - Configuration option = Default value + - Description + * - **[DEFAULT]** + - + * - ``infinibox_hostname`` = ``None`` + - (String) The name (or IP address) for the INFINIDAT Infinibox storage system. + * - ``infinibox_login`` = ``None`` + - (String) Administrative user account name used to access the INFINIDAT Infinibox storage system. + * - ``infinibox_password`` = ``None`` + - (String) Password for the administrative user account specified in the infinibox_login option. + * - ``infinidat_pool_name`` = ``None`` + - (String) Name of the pool from which volumes are allocated. + * - ``infinidat_nas_network_space_name`` = ``None`` + - (String) Name of the NAS network space on the INFINIDAT InfiniBox. + * - ``infinidat_thin_provision`` = ``True`` + - (Boolean) Use thin provisioning diff --git a/manila/opts.py b/manila/opts.py index eee4878110..6876c52c1f 100644 --- a/manila/opts.py +++ b/manila/opts.py @@ -70,6 +70,7 @@ import manila.share.drivers.hitachi.hsp.driver import manila.share.drivers.hpe.hpe_3par_driver import manila.share.drivers.huawei.huawei_nas import manila.share.drivers.ibm.gpfs +import manila.share.drivers.infinidat.infinibox import manila.share.drivers.lvm import manila.share.drivers.maprfs.maprfs_native import manila.share.drivers.netapp.options @@ -151,6 +152,9 @@ _global_opt_lists = [ manila.share.drivers.hpe.hpe_3par_driver.HPE3PAR_OPTS, manila.share.drivers.huawei.huawei_nas.huawei_opts, manila.share.drivers.ibm.gpfs.gpfs_share_opts, + manila.share.drivers.infinidat.infinibox.infinidat_auth_opts, + manila.share.drivers.infinidat.infinibox.infinidat_connection_opts, + manila.share.drivers.infinidat.infinibox.infinidat_general_opts, manila.share.drivers.maprfs.maprfs_native.maprfs_native_share_opts, manila.share.drivers.lvm.share_opts, manila.share.drivers.netapp.options.netapp_proxy_opts, diff --git a/manila/share/drivers/infinidat/__init__.py b/manila/share/drivers/infinidat/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/share/drivers/infinidat/infinibox.py b/manila/share/drivers/infinidat/infinibox.py new file mode 100644 index 0000000000..303804da0f --- /dev/null +++ b/manila/share/drivers/infinidat/infinibox.py @@ -0,0 +1,466 @@ +# Copyright 2017 Infinidat Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +INFINIDAT InfiniBox Share Driver +""" + +import functools + +import ipaddress +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import units +import six + +from manila.common import constants +from manila import exception +from manila.i18n import _ +from manila.share import driver +from manila.share import utils +from manila import version + +try: + import capacity + import infinisdk +except ImportError: + capacity = None + infinisdk = None + + +LOG = logging.getLogger(__name__) + +infinidat_connection_opts = [ + cfg.HostAddressOpt('infinibox_hostname', + help='The name (or IP address) for the INFINIDAT ' + 'Infinibox storage system.'), ] + +infinidat_auth_opts = [ + cfg.StrOpt('infinibox_login', + help=('Administrative user account name used to access the ' + 'INFINIDAT Infinibox storage system.')), + cfg.StrOpt('infinibox_password', + help=('Password for the administrative user account ' + 'specified in the infinibox_login option.'), + secret=True), ] + +infinidat_general_opts = [ + cfg.StrOpt('infinidat_pool_name', + help='Name of the pool from which volumes are allocated.'), + cfg.StrOpt('infinidat_nas_network_space_name', + help='Name of the NAS network space on the INFINIDAT ' + 'InfiniBox.'), + cfg.BoolOpt('infinidat_thin_provision', help='Use thin provisioning.', + default=True)] + +CONF = cfg.CONF +CONF.register_opts(infinidat_connection_opts) +CONF.register_opts(infinidat_auth_opts) +CONF.register_opts(infinidat_general_opts) + +_MANILA_TO_INFINIDAT_ACCESS_LEVEL = { + constants.ACCESS_LEVEL_RW: 'RW', + constants.ACCESS_LEVEL_RO: 'RO', +} + + +def infinisdk_to_manila_exceptions(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except infinisdk.core.exceptions.InfiniSDKException as ex: + # string formatting of 'ex' includes http code and url + msg = _('Caught exception from infinisdk: %s') % ex + LOG.exception(msg) + raise exception.ShareBackendException(msg=msg) + return wrapper + + +class InfiniboxShareDriver(driver.ShareDriver): + + VERSION = '1.0' # driver version + + def __init__(self, *args, **kwargs): + super(InfiniboxShareDriver, self).__init__(False, *args, **kwargs) + self.configuration.append_config_values(infinidat_connection_opts) + self.configuration.append_config_values(infinidat_auth_opts) + self.configuration.append_config_values(infinidat_general_opts) + + def do_setup(self, context): + """Driver initialization""" + if infinisdk is None: + msg = _("Missing 'infinisdk' python module, ensure the library" + " is installed and available.") + raise exception.ManilaException(message=msg) + + infinibox_login = self._safe_get_from_config_or_fail('infinibox_login') + infinibox_password = ( + self._safe_get_from_config_or_fail('infinibox_password')) + auth = (infinibox_login, infinibox_password) + + self.management_address = ( + self._safe_get_from_config_or_fail('infinibox_hostname')) + + self._pool_name = ( + self._safe_get_from_config_or_fail('infinidat_pool_name')) + + self._network_space_name = ( + self._safe_get_from_config_or_fail( + 'infinidat_nas_network_space_name')) + + self._system = infinisdk.InfiniBox(self.management_address, auth=auth) + self._system.login() + + backend_name = self.configuration.safe_get('share_backend_name') + self._backend_name = backend_name or self.__class__.__name__ + + thin_provisioning = self.configuration.infinidat_thin_provision + self._provtype = "THIN" if thin_provisioning else "THICK" + + LOG.debug('setup complete') + + def _update_share_stats(self): + """Retrieve stats info from share group.""" + (free_capacity_bytes, physical_capacity_bytes, + provisioned_capacity_gb) = self._get_available_capacity() + + max_over_subscription_ratio = ( + self.configuration.max_over_subscription_ratio) + + data = dict( + share_backend_name=self._backend_name, + vendor_name='INFINIDAT', + driver_version=self.VERSION, + storage_protocol='NFS', + total_capacity_gb=float(physical_capacity_bytes) / units.Gi, + free_capacity_gb=float(free_capacity_bytes) / units.Gi, + reserved_percentage=self.configuration.reserved_share_percentage, + thin_provisioning=self.configuration.infinidat_thin_provision, + max_over_subscription_ratio=max_over_subscription_ratio, + provisioned_capacity_gb=provisioned_capacity_gb, + snapshot_support=True, + create_share_from_snapshot_support=True, + mount_snapshot_support=True, + revert_to_snapshot_support=True) + + super(InfiniboxShareDriver, self)._update_share_stats(data) + + def _get_available_capacity(self): + pool = self._get_infinidat_pool() + free_capacity_bytes = (pool.get_free_physical_capacity() / + capacity.byte) + physical_capacity_bytes = (pool.get_physical_capacity() / + capacity.byte) + provisioned_capacity_gb = ( + (pool.get_virtual_capacity() - pool.get_free_virtual_capacity()) / + capacity.GB) + return (free_capacity_bytes, physical_capacity_bytes, + provisioned_capacity_gb) + + def _safe_get_from_config_or_fail(self, config_parameter): + config_value = self.configuration.safe_get(config_parameter) + if not config_value: # None or empty string + reason = (_("%(config_parameter)s configuration parameter " + "must be specified") % + {'config_parameter': config_parameter}) + LOG.error(reason) + raise exception.BadConfigurationException(reason=reason) + return config_value + + def _verify_share_protocol(self, share): + if share['share_proto'] != 'NFS': + reason = (_('Unsupported share protocol: %(proto)s.') % + {'proto': share['share_proto']}) + LOG.error(reason) + raise exception.InvalidShare(reason=reason) + + def _verify_access_type(self, access): + if access['access_type'] != 'ip': + reason = _('Only "ip" access type allowed for the NFS protocol.') + LOG.error(reason) + raise exception.InvalidShareAccess(reason=reason) + return True + + def _make_share_name(self, manila_share): + return 'openstack-shr-%s' % manila_share['id'] + + def _make_snapshot_name(self, manila_snapshot): + return 'openstack-snap-%s' % manila_snapshot['id'] + + def _set_manila_object_metadata(self, infinidat_object, manila_object): + data = dict(system="openstack", + openstack_version=version.version_info.release_string(), + manila_id=manila_object['id'], + manila_name=manila_object['name']) + infinidat_object.set_metadata_from_dict(data) + + @infinisdk_to_manila_exceptions + def _get_infinidat_pool(self): + pool = self._system.pools.safe_get(name=self._pool_name) + if pool is None: + msg = _('Pool "%s" not found') % self._pool_name + LOG.error(msg) + raise exception.ShareBackendException(msg=msg) + return pool + + @infinisdk_to_manila_exceptions + def _get_infinidat_nas_network_space_ips(self): + network_space = self._system.network_spaces.safe_get( + name=self._network_space_name) + if network_space is None: + msg = _('Network space "%s" not found') % self._network_space_name + LOG.error(msg) + raise exception.ShareBackendException(msg=msg) + network_space_ips = network_space.get_ips() + if not network_space_ips: + msg = _('INFINIDAT InfiniBox NAS Network Space "%s" has no IPs ' + 'defined') % self._network_space_name + LOG.error(msg) + raise exception.ShareBackendException(msg=msg) + return [ip_munch.ip_address for ip_munch in network_space_ips] + + @infinisdk_to_manila_exceptions + def _get_full_nfs_export_path(self, export_path): + network_space_ips = self._get_infinidat_nas_network_space_ips() + return '{network_space_ip}:{export_path}'.format( + network_space_ip=network_space_ips[0], + export_path=export_path) + + @infinisdk_to_manila_exceptions + def _get_infinidat_filesystem_by_name(self, name): + filesystem = self._system.filesystems.safe_get(name=name) + if filesystem is None: + msg = (_('Filesystem not found on the Infinibox by its name: %s') % + name) + LOG.error(msg) + raise exception.ShareResourceNotFound(share_id=name) + return filesystem + + def _get_infinidat_filesystem(self, manila_share): + filesystem_name = self._make_share_name(manila_share) + return self._get_infinidat_filesystem_by_name(filesystem_name) + + def _get_infinidat_snapshot_by_name(self, name): + snapshot = self._system.filesystems.safe_get(name=name) + if snapshot is None: + msg = (_('Snapshot not found on the Infinibox by its name: %s') % + name) + LOG.error(msg) + raise exception.ShareSnapshotNotFound(snapshot_id=name) + return snapshot + + def _get_infinidat_snapshot(self, manila_snapshot): + snapshot_name = self._make_snapshot_name(manila_snapshot) + return self._get_infinidat_snapshot_by_name(snapshot_name) + + def _get_infinidat_dataset(self, manila_object, is_snapshot): + return (self._get_infinidat_snapshot(manila_object) if is_snapshot + else self._get_infinidat_filesystem(manila_object)) + + @infinisdk_to_manila_exceptions + def _get_export(self, infinidat_filesystem): + infinidat_exports = infinidat_filesystem.get_exports() + if len(infinidat_exports) == 0: + msg = _("Could not find share export") + raise exception.ShareBackendException(msg=msg) + elif len(infinidat_exports) > 1: + msg = _("INFINIDAT filesystem has more than one active export; " + "possibly not a Manila share") + LOG.error(msg) + raise exception.ShareBackendException(msg=msg) + return infinidat_exports[0] + + def _get_infinidat_access_level(self, access): + """Translates between Manila access levels to INFINIDAT API ones""" + access_level = access['access_level'] + try: + return _MANILA_TO_INFINIDAT_ACCESS_LEVEL[access_level] + except KeyError: + raise exception.InvalidShareAccessLevel(level=access_level) + + def _get_ip_address_range(self, ip_address): + """Parse single IP address or subnet into a range. + + If the IP address string is in subnet mask format, returns a + - string. If the IP address contains a single IP + address, returns only that IP address. + """ + + ip_address = six.text_type(ip_address) + + # try treating the ip_address parameter as a range of IP addresses: + ip_network = ipaddress.ip_network(ip_address, strict=False) + ip_network_hosts = list(ip_network.hosts()) + if len(ip_network_hosts) == 0: # /32, single IP address + return ip_address.split('/')[0] + return "{}-{}".format(ip_network_hosts[0], ip_network_hosts[-1]) + + @infinisdk_to_manila_exceptions + def _create_filesystem_export(self, infinidat_filesystem): + infinidat_export = infinidat_filesystem.add_export(permissions=[]) + return { + 'path': self._get_full_nfs_export_path( + infinidat_export.get_export_path()), + 'is_admin_only': False, + 'metadata': {}, + } + + @infinisdk_to_manila_exceptions + def _delete_share(self, share, is_snapshot): + if is_snapshot: + dataset_name = self._make_snapshot_name(share) + else: + dataset_name = self._make_share_name(share) + try: + infinidat_filesystem = ( + self._get_infinidat_filesystem_by_name(dataset_name)) + except exception.ShareResourceNotFound: + message = ("share %(share)s not found on Infinibox, skipping " + "delete") + LOG.warning(message, {"share": share}) + return # filesystem not found + if infinidat_filesystem.has_children(): + # can't delete a filesystem that has a live snapshot + if is_snapshot: + raise exception.ShareSnapshotIsBusy(snapshot_name=dataset_name) + else: + reason = _("share has snapshots and cannot be deleted") + raise exception.ShareBusyException(reason=reason) + try: + infinidat_export = self._get_export(infinidat_filesystem) + except exception.ShareBackendException: + # it's possible that the export has been deleted + pass + else: + infinidat_export.safe_delete() + infinidat_filesystem.safe_delete() + + @infinisdk_to_manila_exceptions + def _extend_share(self, infinidat_filesystem, share, new_size): + new_size_capacity_units = new_size * capacity.GiB + old_size = infinidat_filesystem.get_size() + infinidat_filesystem.resize(new_size_capacity_units - old_size) + + @infinisdk_to_manila_exceptions + def _update_access(self, manila_object, access_rules, is_snapshot): + infinidat_filesystem = self._get_infinidat_dataset( + manila_object, is_snapshot=is_snapshot) + infinidat_export = self._get_export(infinidat_filesystem) + permissions = [ + {'access': self._get_infinidat_access_level(access_rule), + 'client': self._get_ip_address_range(access_rule['access_to']), + 'no_root_squash': True} for access_rule in access_rules if + self._verify_access_type(access_rule)] + infinidat_export.update_permissions(permissions) + + @infinisdk_to_manila_exceptions + def create_share(self, context, share, share_server=None): + self._verify_share_protocol(share) + + pool = self._get_infinidat_pool() + size = share['size'] * capacity.GiB + share_name = self._make_share_name(share) + + infinidat_filesystem = self._system.filesystems.create( + pool=pool, name=share_name, size=size, provtype=self._provtype) + self._set_manila_object_metadata(infinidat_filesystem, share) + return self._create_filesystem_export(infinidat_filesystem) + + @infinisdk_to_manila_exceptions + def create_share_from_snapshot(self, context, share, snapshot, + share_server=None): + name = self._make_share_name(share) + infinidat_snapshot = self._get_infinidat_snapshot(snapshot) + infinidat_new_share = infinidat_snapshot.create_child( + name=name, write_protected=False) + self._extend_share(infinidat_new_share, share, share['size']) + return self._create_filesystem_export(infinidat_new_share) + + @infinisdk_to_manila_exceptions + def create_snapshot(self, context, snapshot, share_server=None): + """Creates a snapshot.""" + share = snapshot['share'] + infinidat_filesystem = self._get_infinidat_filesystem(share) + name = self._make_snapshot_name(snapshot) + infinidat_snapshot = infinidat_filesystem.create_child(name=name) + # snapshot is created in the same size as the original share, so no + # extending is needed + self._set_manila_object_metadata(infinidat_snapshot, snapshot) + return {'export_locations': + [self._create_filesystem_export(infinidat_snapshot)]} + + def delete_share(self, context, share, share_server=None): + try: + self._verify_share_protocol(share) + except exception.InvalidShare: + # cleanup shouldn't fail on wrong protocol or missing share: + message = ("failed to delete share %(share)s; unsupported share " + "protocol %(share_proto)s, only NFS is supported") + LOG.warning(message, {"share": share, + "share_proto": share['share_proto']}) + return + self._delete_share(share, is_snapshot=False) + + def delete_snapshot(self, context, snapshot, share_server=None): + self._delete_share(snapshot, is_snapshot=True) + + def ensure_share(self, context, share, share_server=None): + # will raise ShareResourceNotFound if the share was not found: + infinidat_filesystem = self._get_infinidat_filesystem(share) + try: + infinidat_export = self._get_export(infinidat_filesystem) + except exception.ShareBackendException: + # export not found, need to re-export + message = ("missing export for share %(share)s, trying to " + "re-export") + LOG.info(message, {"share": share}) + infinidat_export = ( + self._create_filesystem_export(infinidat_filesystem)) + return [self._get_full_nfs_export_path(infinidat_export['path'])] + return [self._get_full_nfs_export_path( + infinidat_export.get_export_path())] + + def update_access(self, context, share, access_rules, add_rules, + delete_rules, share_server=None): + # As the Infinibox API can bulk update export access rules, we will try + # to use the access_rules list + self._verify_share_protocol(share) + self._update_access(share, access_rules, is_snapshot=False) + + def get_network_allocations_number(self): + return 0 + + @infinisdk_to_manila_exceptions + def revert_to_snapshot(self, context, snapshot, share_access_rules, + snapshot_access_rules, share_server=None): + infinidat_snapshot = self._get_infinidat_snapshot(snapshot) + infinidat_parent_share = self._get_infinidat_filesystem( + snapshot['share']) + infinidat_parent_share.restore(infinidat_snapshot) + + def extend_share(self, share, new_size, share_server=None): + infinidat_filesystem = self._get_infinidat_filesystem(share) + self._extend_share(infinidat_filesystem, share, new_size) + + def snapshot_update_access(self, context, snapshot, access_rules, + add_rules, delete_rules, share_server=None): + # snapshots are to be mounted in read-only mode, see: + # "Add mountable snapshots" on openstack specs. + access_rules, _, _ = utils.change_rules_to_readonly( + access_rules, [], []) + try: + self._update_access(snapshot, access_rules, is_snapshot=True) + except exception.InvalidShareAccess as e: + raise exception.InvalidSnapshotAccess(e) diff --git a/manila/tests/share/drivers/infinidat/__init__.py b/manila/tests/share/drivers/infinidat/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/share/drivers/infinidat/test_infinidat.py b/manila/tests/share/drivers/infinidat/test_infinidat.py new file mode 100644 index 0000000000..0376921525 --- /dev/null +++ b/manila/tests/share/drivers/infinidat/test_infinidat.py @@ -0,0 +1,708 @@ +# Copyright 2017 Infinidat Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Unit tests for INFINIDAT InfiniBox share driver.""" + +import copy +import mock +from oslo_utils import units + +from manila.common import constants +from manila import exception +from manila.share import configuration +from manila.share.drivers.infinidat import infinibox +from manila import test + + +_MOCK_SHARE_ID = 1 +_MOCK_SNAPSHOT_ID = 2 +_MOCK_CLONE_ID = 3 + +_MOCK_SHARE_SIZE = 4 + + +def _create_mock__getitem__(mock): + def mock__getitem__(self, key, default=None): + return getattr(mock, key, default) + return mock__getitem__ + +test_share = mock.Mock(id=_MOCK_SHARE_ID, size=_MOCK_SHARE_SIZE, + share_proto='NFS') +test_share.__getitem__ = _create_mock__getitem__(test_share) + +test_snapshot = mock.Mock(id=_MOCK_SNAPSHOT_ID, size=test_share.size, + share=test_share, share_proto='NFS') +test_snapshot.__getitem__ = _create_mock__getitem__(test_snapshot) + +original_test_clone = mock.Mock(id=_MOCK_CLONE_ID, size=test_share.size, + share=test_snapshot, share_proto='NFS') +original_test_clone.__getitem__ = _create_mock__getitem__(original_test_clone) + + +class FakeInfinisdkException(Exception): + def __init__(self, message=None, error_code=None, *args): + self.message = message + self.error_code = error_code + super(FakeInfinisdkException, self).__init__( + message, error_code, *args) + + +class FakeInfinisdkPermission(object): + def __init__(self, permission): + self._permission = permission + + def __getattr__(self, attr): + return self._permission[attr] + + def __getitem__(self, key): + return self._permission[key] + + +class InfiniboxDriverTestCaseBase(test.TestCase): + def setUp(self): + super(InfiniboxDriverTestCaseBase, self).setUp() + + # create mock configuration + self.configuration = mock.Mock(spec=configuration.Configuration) + self.configuration.infinibox_hostname = 'mockbox' + self.configuration.infinidat_pool_name = 'mockpool' + self.configuration.infinidat_nas_network_space_name = 'mockspace' + self.configuration.infinidat_thin_provision = True + self.configuration.infinibox_login = 'user' + self.configuration.infinibox_password = 'pass' + + self.configuration.network_config_group = 'test_network_config_group' + self.configuration.admin_network_config_group = ( + 'test_admin_network_config_group') + self.configuration.reserved_share_percentage = 0 + self.configuration.filter_function = None + self.configuration.goodness_function = None + self.configuration.driver_handles_share_servers = False + self.configuration.max_over_subscription_ratio = 2 + self.mock_object(self.configuration, 'safe_get', self._fake_safe_get) + + self.driver = infinibox.InfiniboxShareDriver( + configuration=self.configuration) + + # mock external library dependencies + infinisdk = self._patch( + "manila.share.drivers.infinidat.infinibox.infinisdk") + self._capacity_module = ( + self._patch("manila.share.drivers.infinidat.infinibox.capacity")) + self._capacity_module.byte = 1 + self._capacity_module.GiB = units.Gi + + self._system = self._infinibox_mock() + + infinisdk.core.exceptions.InfiniSDKException = FakeInfinisdkException + infinisdk.InfiniBox.return_value = self._system + + self.driver.do_setup(None) + + def _infinibox_mock(self): + result = mock.Mock() + + self._mock_export_permissions = [] + + self._mock_export = mock.Mock() + self._mock_export.get_export_path.return_value = '/mock_export' + self._mock_export.get_permissions = self._fake_get_permissions + self._mock_export.update_permissions = self._fake_update_permissions + + self._mock_filesystem = mock.Mock() + self._mock_filesystem.has_children.return_value = False + self._mock_filesystem.create_child.return_value = self._mock_filesystem + self._mock_filesystem.get_exports.return_value = [self._mock_export, ] + + self._mock_filesystem.size = 4 * self._capacity_module.GiB + self._mock_filesystem.get_size.return_value = ( + self._mock_filesystem.size) + + self._mock_pool = mock.Mock() + self._mock_pool.get_free_physical_capacity.return_value = units.Gi + self._mock_pool.get_physical_capacity.return_value = units.Gi + self._mock_pool.get_virtual_capacity.return_value = units.Gi + self._mock_pool.get_free_virtual_capacity.return_value = units.Gi + + self._mock_network_space = mock.Mock() + self._mock_network_space.get_ips.return_value = ( + [mock.Mock(ip_address='1.2.3.4'), mock.Mock(ip_address='1.2.3.5')]) + + result.network_spaces.safe_get.return_value = self._mock_network_space + result.pools.safe_get.return_value = self._mock_pool + result.filesystems.safe_get.return_value = self._mock_filesystem + result.filesystems.create.return_value = self._mock_filesystem + result.components.nodes.get_all.return_value = [] + return result + + def _raise_infinisdk(self, *args, **kwargs): + raise FakeInfinisdkException() + + def _fake_safe_get(self, value): + return getattr(self.configuration, value, None) + + def _fake_get_permissions(self): + return self._mock_export_permissions + + def _fake_update_permissions(self, new_export_permissions): + self._mock_export_permissions = [ + FakeInfinisdkPermission(permission) for permission in + new_export_permissions] + + def _patch(self, path, *args, **kwargs): + patcher = mock.patch(path, *args, **kwargs) + result = patcher.start() + self.addCleanup(patcher.stop) + return result + + +class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase): + @mock.patch("manila.share.drivers.infinidat.infinibox.infinisdk", None) + def test_no_infinisdk_module(self): + self.assertRaises(exception.ManilaException, + self.driver.do_setup, None) + + def test_no_auth_parameters(self): + self.configuration.infinibox_login = None + self.configuration.infinibox_password = None + self.assertRaises(exception.BadConfigurationException, + self.driver.do_setup, None) + + def test_empty_auth_parameters(self): + self.configuration.infinibox_login = "" + self.configuration.infinibox_password = "" + self.assertRaises(exception.BadConfigurationException, + self.driver.do_setup, None) + + def test_get_share_stats_refreshes(self): + self.driver._update_share_stats() + result = self.driver.get_share_stats() + self.assertEqual(1, result["free_capacity_gb"]) + # change the "free space" in the pool + self._mock_pool.get_free_physical_capacity.return_value = 0 + # no refresh - free capacity should stay the same + result = self.driver.get_share_stats(refresh=False) + self.assertEqual(1, result["free_capacity_gb"]) + # refresh - free capacity should change to 0 + result = self.driver.get_share_stats(refresh=True) + self.assertEqual(0, result["free_capacity_gb"]) + + def test_get_share_stats_pool_not_found(self): + self._system.pools.safe_get.return_value = None + self.assertRaises(exception.ManilaException, + self.driver._update_share_stats) + + def test__verify_share_protocol(self): + # test_share is NFS by default: + self.driver._verify_share_protocol(test_share) + + def test__verify_share_protocol_fails_for_non_nfs(self): + # set test_share protocol for non-NFS (CIFS, for that matter) and see + # that we fail: + cifs_share = copy.deepcopy(test_share) + cifs_share.share_proto = 'CIFS' + # also need to re-define getitem, otherwise we'll get attributes from + # test_share: + cifs_share.__getitem__ = _create_mock__getitem__(cifs_share) + self.assertRaises(exception.InvalidShare, + self.driver._verify_share_protocol, cifs_share) + + def test__verify_access_type_ip(self): + self.assertTrue(self.driver._verify_access_type({'access_type': 'ip'})) + + def test__verify_access_type_fails_for_type_user(self): + self.assertRaises( + exception.InvalidShareAccess, self.driver._verify_access_type, + {'access_type': 'user'}) + + def test__verify_access_type_fails_for_type_cert(self): + self.assertRaises( + exception.InvalidShareAccess, self.driver._verify_access_type, + {'access_type': 'cert'}) + + def test__get_ip_address_range_single_ip(self): + ip_address = self.driver._get_ip_address_range('1.2.3.4') + self.assertEqual('1.2.3.4', ip_address) + + def test__get_ip_address_range_ip_range(self): + ip_address_range = self.driver._get_ip_address_range('5.6.7.8/28') + self.assertEqual('5.6.7.1-5.6.7.14', ip_address_range) + + def test__get_ip_address_range_invalid_address(self): + self.assertRaises(ValueError, self.driver._get_ip_address_range, + 'invalid') + + def test__get_infinidat_pool(self): + self.driver._get_infinidat_pool() + self._system.pools.safe_get.assert_called_once() + + def test__get_infinidat_pool_no_pools(self): + self._system.pools.safe_get.return_value = None + self.assertRaises(exception.ShareBackendException, + self.driver._get_infinidat_pool) + + def test__get_infinidat_pool_api_error(self): + self._system.pools.safe_get.side_effect = ( + self._raise_infinisdk) + self.assertRaises(exception.ShareBackendException, + self.driver._get_infinidat_pool) + + def test__get_infinidat_nas_network_space_ips(self): + self.driver._get_infinidat_nas_network_space_ips() + self._system.network_spaces.safe_get.assert_called_once() + self._mock_network_space.get_ips.assert_called_once() + + def test__get_infinidat_nas_network_space_ips_no_network_space(self): + self._system.network_spaces.safe_get.return_value = None + self.assertRaises(exception.ShareBackendException, + self.driver._get_infinidat_nas_network_space_ips) + + def test__get_infinidat_nas_network_space_ips_no_ips(self): + self._mock_network_space.get_ips.return_value = [] + self.assertRaises(exception.ShareBackendException, + self.driver._get_infinidat_nas_network_space_ips) + + def test__get_infinidat_nas_network_space_api_error(self): + self._system.network_spaces.safe_get.side_effect = ( + self._raise_infinisdk) + self.assertRaises(exception.ShareBackendException, + self.driver._get_infinidat_nas_network_space_ips) + + def test__get_export(self): + # The default return value of get_exports is [mock_export, ]: + export = self.driver._get_export(self._mock_filesystem) + self._mock_filesystem.get_exports.assert_called_once() + self.assertEqual(self._mock_export, export) + + def test__get_export_no_filesystem_exports(self): + self._mock_filesystem.get_exports.return_value = [] + self.assertRaises(exception.ShareBackendException, + self.driver._get_export, self._mock_filesystem) + + def test__get_export_too_many_filesystem_exports(self): + self._mock_filesystem.get_exports.return_value = [ + self._mock_export, self._mock_export] + self.assertRaises(exception.ShareBackendException, + self.driver._get_export, self._mock_filesystem) + + def test__get_export_api_error(self): + self._mock_filesystem.get_exports.side_effect = self._raise_infinisdk + self.assertRaises(exception.ShareBackendException, + self.driver._get_export, self._mock_filesystem) + + def test__get_infinidat_access_level_rw(self): + access_level = ( + self.driver._get_infinidat_access_level( + {'access_level': constants.ACCESS_LEVEL_RW})) + self.assertEqual('RW', access_level) + + def test__get_infinidat_access_level_ro(self): + access_level = ( + self.driver._get_infinidat_access_level( + {'access_level': constants.ACCESS_LEVEL_RO})) + self.assertEqual('RO', access_level) + + def test__get_infinidat_access_level_fails_for_invalid_level(self): + self.assertRaises(exception.InvalidShareAccessLevel, + self.driver._get_infinidat_access_level, + {'access_level': 'invalid'}) + + def test_create_share(self): + # This test uses the default infinidat_thin_provision = True setting: + self.driver.create_share(None, test_share) + self._system.filesystems.create.assert_called_once() + self._mock_filesystem.set_metadata_from_dict.assert_called_once() + self._mock_filesystem.add_export.assert_called_once_with( + permissions=[]) + + def test_create_share_thick_provisioning(self): + self.configuration.infinidat_thin_provision = False + self.driver.create_share(None, test_share) + self._system.filesystems.create.assert_called_once() + self._mock_filesystem.set_metadata_from_dict.assert_called_once() + self._mock_filesystem.add_export.assert_called_once_with( + permissions=[]) + + def test_create_share_pool_not_found(self): + self._system.pools.safe_get.return_value = None + self.assertRaises(exception.ManilaException, + self.driver.create_share, None, test_share) + + def test_create_share_fails_non_nfs(self): + # set test_share protocol for non-NFS (CIFS, for that matter) and see + # that we fail: + cifs_share = copy.deepcopy(test_share) + cifs_share.share_proto = 'CIFS' + # also need to re-define getitem, otherwise we'll get attributes from + # test_share: + cifs_share.__getitem__ = _create_mock__getitem__(cifs_share) + self.assertRaises(exception.InvalidShare, + self.driver.create_share, None, cifs_share) + + def test_create_share_pools_api_fail(self): + # will fail when trying to get pool for share creation: + self._system.pools.safe_get.side_effect = self._raise_infinisdk + self.assertRaises(exception.ShareBackendException, + self.driver.create_share, None, test_share) + + def test_create_share_network_spaces_api_fail(self): + # will fail when trying to get full export path to the new share: + self._system.network_spaces.safe_get.side_effect = ( + self._raise_infinisdk) + self.assertRaises(exception.ShareBackendException, + self.driver.create_share, None, test_share) + + def test_delete_share(self): + self.driver.delete_share(None, test_share) + self._mock_filesystem.safe_delete.assert_called_once() + self._mock_export.safe_delete.assert_called_once() + + def test_delete_share_doesnt_exist(self): + self._system.shares.safe_get.return_value = None + # should not raise an exception + self.driver.delete_share(None, test_share) + + def test_delete_share_export_doesnt_exist(self): + self._mock_filesystem.get_exports.return_value = [] + # should not raise an exception + self.driver.delete_share(None, test_share) + + def test_delete_share_with_children(self): + self._mock_filesystem.has_children.return_value = True + self.assertRaises(exception.ShareBusyException, + self.driver.delete_share, None, test_share) + + def test_delete_share_wrong_share_protocol(self): + # set test_share protocol for non-NFS (CIFS, for that matter) and see + # that delete_share doesn't fail: + cifs_share = copy.deepcopy(test_share) + cifs_share.share_proto = 'CIFS' + # also need to re-define getitem, otherwise we'll get attributes from + # test_share: + cifs_share.__getitem__ = _create_mock__getitem__(cifs_share) + self.driver.delete_share(None, cifs_share) + + def test_extend_share(self): + self.driver.extend_share(test_share, _MOCK_SHARE_SIZE * 2) + self._mock_filesystem.resize.assert_called_once() + + def test_extend_share_api_fail(self): + self._mock_filesystem.resize.side_effect = self._raise_infinisdk + self.assertRaises(exception.ShareBackendException, + self.driver.extend_share, test_share, 8) + + def test_create_snapshot(self): + self.driver.create_snapshot(None, test_snapshot) + self._mock_filesystem.create_child.assert_called_once() + self._mock_filesystem.set_metadata_from_dict.assert_called_once() + self._mock_filesystem.add_export.assert_called_once_with( + permissions=[]) + + def test_create_snapshot_metadata(self): + self._mock_filesystem.create_child.return_value = ( + self._mock_filesystem) + self.driver.create_snapshot(None, test_snapshot) + self._mock_filesystem.set_metadata_from_dict.assert_called_once() + + def test_create_snapshot_share_doesnt_exist(self): + self._system.filesystems.safe_get.return_value = None + self.assertRaises(exception.ShareResourceNotFound, + self.driver.create_snapshot, None, test_snapshot) + + def test_create_snapshot_create_child_api_fail(self): + # will fail when trying to create a child to the original share: + self._mock_filesystem.create_child.side_effect = ( + self._raise_infinisdk) + self.assertRaises(exception.ShareBackendException, + self.driver.create_snapshot, None, test_snapshot) + + def test_create_snapshot_network_spaces_api_fail(self): + # will fail when trying to get full export path to the new snapshot: + self._system.network_spaces.safe_get.side_effect = ( + self._raise_infinisdk) + self.assertRaises(exception.ShareBackendException, + self.driver.create_snapshot, None, test_snapshot) + + def test_create_share_from_snapshot(self): + self.driver.create_share_from_snapshot(None, original_test_clone, + test_snapshot) + self._mock_filesystem.create_child.assert_called_once() + self._mock_filesystem.add_export.assert_called_once_with( + permissions=[]) + + def test_create_share_from_snapshot_bigger_size(self): + test_clone = copy.copy(original_test_clone) + test_clone.size = test_share.size * 2 + # also need to re-define getitem, otherwise we'll get attributes from + # original_get_clone: + test_clone.__getitem__ = _create_mock__getitem__(test_clone) + + self.driver.create_share_from_snapshot(None, test_clone, test_snapshot) + + def test_create_share_from_snapshot_doesnt_exist(self): + self._system.filesystems.safe_get.return_value = None + self.assertRaises(exception.ShareSnapshotNotFound, + self.driver.create_share_from_snapshot, + None, original_test_clone, test_snapshot) + + def test_create_share_from_snapshot_create_fails(self): + self._mock_filesystem.create_child.side_effect = ( + self._raise_infinisdk) + self.assertRaises(exception.ShareBackendException, + self.driver.create_share_from_snapshot, + None, original_test_clone, test_snapshot) + + def test_delete_snapshot(self): + self.driver.delete_snapshot(None, test_snapshot) + self._mock_filesystem.safe_delete.assert_called_once() + self._mock_export.safe_delete.assert_called_once() + + def test_delete_snapshot_with_children(self): + self._mock_filesystem.has_children.return_value = True + self.assertRaises(exception.ShareSnapshotIsBusy, + self.driver.delete_snapshot, None, test_snapshot) + + def test_delete_snapshot_doesnt_exist(self): + self._system.filesystems.safe_get.return_value = None + # should not raise an exception + self.driver.delete_snapshot(None, test_snapshot) + + def test_delete_snapshot_api_fail(self): + self._mock_filesystem.safe_delete.side_effect = self._raise_infinisdk + self.assertRaises(exception.ShareBackendException, + self.driver.delete_snapshot, None, test_snapshot) + + def test_ensure_share(self): + self.driver.ensure_share(None, test_share) + self._mock_filesystem.get_exports.assert_called_once() + self._mock_export.get_export_path.assert_called_once() + + def test_ensure_share_export_missing(self): + self._mock_filesystem.get_exports.return_value = [] + self.driver.ensure_share(None, test_share) + self._mock_filesystem.get_exports.assert_called_once() + self._mock_filesystem.add_export.assert_called_once_with( + permissions=[]) + + def test_ensure_share_share_doesnt_exist(self): + self._system.filesystems.safe_get.return_value = None + self.assertRaises(exception.ShareResourceNotFound, + self.driver.ensure_share, None, test_share) + + def test_ensure_share_get_exports_api_fail(self): + self._mock_filesystem.get_exports.side_effect = self._raise_infinisdk + self._mock_filesystem.add_export.side_effect = self._raise_infinisdk + self.assertRaises(exception.ShareBackendException, + self.driver.ensure_share, None, test_share) + + def test_ensure_share_network_spaces_api_fail(self): + self._system.network_spaces.safe_get.side_effect = ( + self._raise_infinisdk) + self.assertRaises(exception.ShareBackendException, + self.driver.ensure_share, None, test_share) + + def test_get_network_allocations_number(self): + # Mostly to increase test coverage. The return value should always be 0 + # for our driver (see method documentation in base class code): + self.assertEqual(0, self.driver.get_network_allocations_number()) + + def test_revert_to_snapshot(self): + self.driver.revert_to_snapshot(None, test_snapshot, [], []) + self._mock_filesystem.restore.assert_called_once() + + def test_revert_to_snapshot_snapshot_doesnt_exist(self): + self._system.filesystems.safe_get.return_value = None + self.assertRaises(exception.ShareSnapshotNotFound, + self.driver.revert_to_snapshot, None, test_snapshot, + [], []) + + def test_revert_to_snapshot_api_fail(self): + self._mock_filesystem.restore.side_effect = self._raise_infinisdk + self.assertRaises(exception.ShareBackendException, + self.driver.revert_to_snapshot, None, test_snapshot, + [], []) + + def test_update_access(self): + access_rules = [ + {'access_level': constants.ACCESS_LEVEL_RO, + 'access_to': '1.2.3.4', + 'access_type': 'ip'}, + {'access_level': constants.ACCESS_LEVEL_RW, + 'access_to': '1.2.3.5', + 'access_type': 'ip'}, + {'access_level': constants.ACCESS_LEVEL_RO, + 'access_to': '5.6.7.8/28', + 'access_type': 'ip'}] + self.driver.update_access(None, test_share, access_rules, [], []) + + permissions = self._mock_filesystem.get_exports()[0].get_permissions() + # now we are supposed to have three permissions: + # 1. for 1.2.3.4 + # 2. for 1.2.3.5 + # 3. for 5.6.7.1-5.6.7.14 + self.assertEqual(3, len(permissions)) + + # sorting according to clients, to avoid mismatch errors: + permissions = sorted(permissions, + key=lambda permission: permission.client) + + self.assertEqual('RO', permissions[0].access) + self.assertEqual('1.2.3.4', permissions[0].client) + self.assertTrue(permissions[0].no_root_squash) + + self.assertEqual('RW', permissions[1].access) + self.assertEqual('1.2.3.5', permissions[1].client) + self.assertTrue(permissions[1].no_root_squash) + + self.assertEqual('RO', permissions[2].access) + self.assertEqual('5.6.7.1-5.6.7.14', permissions[2].client) + self.assertTrue(permissions[2].no_root_squash) + + def test_update_access_share_doesnt_exist(self): + self._system.filesystems.safe_get.return_value = None + access_rules = [ + {'access_level': constants.ACCESS_LEVEL_RO, + 'access_to': '1.2.3.4', + 'access_type': 'ip'}, + {'access_level': constants.ACCESS_LEVEL_RW, + 'access_to': '1.2.3.5', + 'access_type': 'ip'}, + {'access_level': constants.ACCESS_LEVEL_RO, + 'access_to': '5.6.7.8/28', + 'access_type': 'ip'}] + self.assertRaises(exception.ShareResourceNotFound, + self.driver.update_access, None, test_share, + access_rules, [], []) + + def test_update_access_api_fail(self): + self._mock_filesystem.get_exports.side_effect = self._raise_infinisdk + access_rules = [ + {'access_level': constants.ACCESS_LEVEL_RO, + 'access_to': '1.2.3.4', + 'access_type': 'ip'}, + {'access_level': constants.ACCESS_LEVEL_RW, + 'access_to': '1.2.3.5', + 'access_type': 'ip'}, + {'access_level': constants.ACCESS_LEVEL_RO, + 'access_to': '5.6.7.8/28', + 'access_type': 'ip'}] + self.assertRaises(exception.ShareBackendException, + self.driver.update_access, None, test_share, + access_rules, [], []) + + def test_update_access_fails_non_ip_access_type(self): + access_rules = [ + {'access_level': constants.ACCESS_LEVEL_RO, + 'access_to': '1.2.3.4', + 'access_type': 'user'}] + self.assertRaises(exception.InvalidShareAccess, + self.driver.update_access, None, test_share, + access_rules, [], []) + + def test_update_access_fails_invalid_ip(self): + access_rules = [ + {'access_level': constants.ACCESS_LEVEL_RO, + 'access_to': 'invalid', + 'access_type': 'ip'}] + self.assertRaises(ValueError, + self.driver.update_access, None, test_share, + access_rules, [], []) + + def test_snapshot_update_access(self): + access_rules = [ + {'access_level': constants.ACCESS_LEVEL_RO, + 'access_to': '1.2.3.4', + 'access_type': 'ip'}, + {'access_level': constants.ACCESS_LEVEL_RW, + 'access_to': '1.2.3.5', + 'access_type': 'ip'}, + {'access_level': constants.ACCESS_LEVEL_RO, + 'access_to': '5.6.7.8/28', + 'access_type': 'ip'}] + self.driver.snapshot_update_access(None, test_snapshot, access_rules, + [], []) + + permissions = self._mock_filesystem.get_exports()[0].get_permissions() + # now we are supposed to have three permissions: + # 1. for 1.2.3.4 + # 2. for 1.2.3.5 + # 3. for 5.6.7.1-5.6.7.14 + self.assertEqual(3, len(permissions)) + + # sorting according to clients, to avoid mismatch errors: + permissions = sorted(permissions, + key=lambda permission: permission.client) + + self.assertEqual('RO', permissions[0].access) + self.assertEqual('1.2.3.4', permissions[0].client) + self.assertTrue(permissions[0].no_root_squash) + + # despite sending a RW rule, all rules are converted to RO: + self.assertEqual('RO', permissions[1].access) + self.assertEqual('1.2.3.5', permissions[1].client) + self.assertTrue(permissions[1].no_root_squash) + + self.assertEqual('RO', permissions[2].access) + self.assertEqual('5.6.7.1-5.6.7.14', permissions[2].client) + self.assertTrue(permissions[2].no_root_squash) + + def test_snapshot_update_access_snapshot_doesnt_exist(self): + self._system.filesystems.safe_get.return_value = None + access_rules = [ + {'access_level': constants.ACCESS_LEVEL_RO, + 'access_to': '1.2.3.4', + 'access_type': 'ip'}, + {'access_level': constants.ACCESS_LEVEL_RW, + 'access_to': '1.2.3.5', + 'access_type': 'ip'}, + {'access_level': constants.ACCESS_LEVEL_RO, + 'access_to': '5.6.7.8/28', + 'access_type': 'ip'}] + self.assertRaises(exception.ShareSnapshotNotFound, + self.driver.snapshot_update_access, None, + test_snapshot, access_rules, [], []) + + def test_snapshot_update_access_api_fail(self): + self._mock_filesystem.get_exports.side_effect = self._raise_infinisdk + access_rules = [ + {'access_level': constants.ACCESS_LEVEL_RO, + 'access_to': '1.2.3.4', + 'access_type': 'ip'}, + {'access_level': constants.ACCESS_LEVEL_RW, + 'access_to': '1.2.3.5', + 'access_type': 'ip'}, + {'access_level': constants.ACCESS_LEVEL_RO, + 'access_to': '5.6.7.8/28', + 'access_type': 'ip'}] + self.assertRaises(exception.ShareBackendException, + self.driver.snapshot_update_access, None, + test_snapshot, access_rules, [], []) + + def test_snapshot_update_access_fails_non_ip_access_type(self): + access_rules = [ + {'access_level': constants.ACCESS_LEVEL_RO, + 'access_to': '1.2.3.4', + 'access_type': 'user'}] + self.assertRaises(exception.InvalidSnapshotAccess, + self.driver.snapshot_update_access, None, test_share, + access_rules, [], []) + + def test_snapshot_update_access_fails_invalid_ip(self): + access_rules = [ + {'access_level': constants.ACCESS_LEVEL_RO, + 'access_to': 'invalid', + 'access_type': 'ip'}] + self.assertRaises(ValueError, + self.driver.snapshot_update_access, None, test_share, + access_rules, [], []) diff --git a/releasenotes/notes/infinidat-add-infinibox-driver-ec652258e710d6a0.yaml b/releasenotes/notes/infinidat-add-infinibox-driver-ec652258e710d6a0.yaml new file mode 100644 index 0000000000..b2770559d6 --- /dev/null +++ b/releasenotes/notes/infinidat-add-infinibox-driver-ec652258e710d6a0.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added a new driver for the INFINIDAT InfiniBox storage array.