From ac8a9a2380c155f61093c61e3b7b21d375fab3d0 Mon Sep 17 00:00:00 2001 From: Yian Zong Date: Thu, 20 Apr 2023 07:23:43 +0000 Subject: [PATCH] Adds a new Manila driver for Dell PowerFlex storage backend Adds a new Manila driver to support Dell PowerFlex storage backend. It will include the minimum set of Manila features. Implements: blueprint dell-powerflex-manila-driver Change-Id: I4dc81bf75135b32f1971ca21eee298bca33441cf --- doc/source/admin/index.rst | 1 + ...hare_back_ends_feature_support_mapping.rst | 8 + .../shared-file-systems/drivers.rst | 1 + .../drivers/dell-emc-powerflex-driver.rst | 160 +++++ manila/share/drivers/dell_emc/driver.py | 5 +- .../dell_emc/plugins/powerflex/__init__.py | 0 .../dell_emc/plugins/powerflex/connection.py | 390 ++++++++++++ .../plugins/powerflex/object_manager.py | 409 +++++++++++++ .../dell_emc/plugins/powerflex/__init__.py | 0 .../mockup/create_filesystem_response.json | 3 + .../mockup/create_nfs_export_response.json | 3 + .../mockup/create_nfs_snapshot_response.json | 3 + .../mockup/get_fileystem_id_response.json | 5 + .../get_fsid_from_export_name_response.json | 5 + .../get_fsid_from_snapshot_name_response.json | 5 + .../mockup/get_nas_server_id_response.json | 5 + .../mockup/get_nfs_export_id_response.json | 5 + .../mockup/get_nfs_export_name_response.json | 20 + .../mockup/get_storage_pool_id_response.json | 1 + .../get_storage_pool_spare_percentage.json | 98 +++ .../mockup/get_storage_pool_statistic.json | 381 ++++++++++++ .../powerflex/mockup/login_response.json | 10 + .../plugins/powerflex/test_connection.py | 558 ++++++++++++++++++ .../plugins/powerflex/test_object_manager.py | 458 ++++++++++++++ ...erflex-manila-driver-2c496483242e555a.yaml | 5 + setup.cfg | 1 + 26 files changed, 2539 insertions(+), 1 deletion(-) create mode 100644 doc/source/configuration/shared-file-systems/drivers/dell-emc-powerflex-driver.rst create mode 100644 manila/share/drivers/dell_emc/plugins/powerflex/__init__.py create mode 100644 manila/share/drivers/dell_emc/plugins/powerflex/connection.py create mode 100644 manila/share/drivers/dell_emc/plugins/powerflex/object_manager.py create mode 100644 manila/tests/share/drivers/dell_emc/plugins/powerflex/__init__.py create mode 100644 manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/create_filesystem_response.json create mode 100644 manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/create_nfs_export_response.json create mode 100644 manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/create_nfs_snapshot_response.json create mode 100644 manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_fileystem_id_response.json create mode 100644 manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_fsid_from_export_name_response.json create mode 100644 manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_fsid_from_snapshot_name_response.json create mode 100644 manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_nas_server_id_response.json create mode 100644 manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_nfs_export_id_response.json create mode 100644 manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_nfs_export_name_response.json create mode 100644 manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_storage_pool_id_response.json create mode 100644 manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_storage_pool_spare_percentage.json create mode 100644 manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_storage_pool_statistic.json create mode 100644 manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/login_response.json create mode 100644 manila/tests/share/drivers/dell_emc/plugins/powerflex/test_connection.py create mode 100644 manila/tests/share/drivers/dell_emc/plugins/powerflex/test_object_manager.py create mode 100644 releasenotes/notes/bp-dell-powerflex-manila-driver-2c496483242e555a.yaml diff --git a/doc/source/admin/index.rst b/doc/source/admin/index.rst index d01b2f1593..745776e149 100644 --- a/doc/source/admin/index.rst +++ b/doc/source/admin/index.rst @@ -86,6 +86,7 @@ each back end. emc_vnx_driver ../configuration/shared-file-systems/drivers/dell-emc-unity-driver ../configuration/shared-file-systems/drivers/dell-emc-powerstore-driver + ../configuration/shared-file-systems/drivers/dell-emc-powerflex-driver generic_driver glusterfs_driver glusterfs_native_driver 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 0804559e0c..9b1261ebb6 100644 --- a/doc/source/admin/share_back_ends_feature_support_mapping.rst +++ b/doc/source/admin/share_back_ends_feature_support_mapping.rst @@ -53,6 +53,8 @@ Mapping of share drivers and share features support +----------------------------------------+-----------------------+-----------------------+--------------------------+--------------------------+------------------------+-----------------------------------+--------------------------+--------------------+--------------------+ | Dell EMC PowerStore | B | \- | B | B | B | B | \- | B | \- | +----------------------------------------+-----------------------+-----------------------+--------------------------+--------------------------+------------------------+-----------------------------------+--------------------------+--------------------+--------------------+ +| Dell EMC PowerFlex | B | \- | B | \- | B | \- | \- | \- | \- | ++----------------------------------------+-----------------------+-----------------------+--------------------------+--------------------------+------------------------+-----------------------------------+--------------------------+--------------------+--------------------+ | GlusterFS | J | \- | directory layout (T) | directory layout (T) | volume layout (L) | volume layout (L) | \- | \- | \- | +----------------------------------------+-----------------------+-----------------------+--------------------------+--------------------------+------------------------+-----------------------------------+--------------------------+--------------------+--------------------+ | GlusterFS-Native | J | \- | \- | \- | K | L | \- | \- | \- | @@ -128,6 +130,8 @@ Mapping of share drivers and share access rules support +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ | Dell EMC PowerStore | NFS (B) | \- | CIFS (B) | \- | \- | NFS (B) | \- | CIFS (B) | \- | \- | +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ +| Dell EMC PowerFlex | NFS (B) | \- | \- | \- | \- | NFS (B) | \- | \- | \- | \- | ++----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ | GlusterFS | NFS (J) | \- | \- | \- | \- | \- | \- | \- | \- | \- | +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ | GlusterFS-Native | \- | \- | \- | J | \- | \- | \- | \- | \- | \- | @@ -201,6 +205,8 @@ Mapping of share drivers and security services support +----------------------------------------+------------------+-----------------+------------------+ | Dell EMC PowerStore | B | \- | \- | +----------------------------------------+------------------+-----------------+------------------+ +| Dell EMC PowerFlex | \- | \- | \- | ++----------------------------------------+------------------+-----------------+------------------+ | GlusterFS | \- | \- | \- | +----------------------------------------+------------------+-----------------+------------------+ | GlusterFS-Native | \- | \- | \- | @@ -276,6 +282,8 @@ More information: :ref:`capabilities_and_extra_specs` +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+-------------------------+ | Dell EMC PowerStore | \- | B | \- | \- | B | \- | \- | B | B | \- | B | \- | \- | +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+-------------------------+ +| Dell EMC PowerFlex | \- | B | \- | \- | B | \- | \- | \- | \- | \- | B | \- | \- | ++----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+-------------------------+ | GlusterFS | \- | J | \- | \- | \- | L | \- | volume layout (L) | \- | \- | P | \- | \- | +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+-------------------------+ | GlusterFS-Native | \- | J | \- | \- | \- | L | \- | L | \- | \- | P | \- | \- | diff --git a/doc/source/configuration/shared-file-systems/drivers.rst b/doc/source/configuration/shared-file-systems/drivers.rst index 09ade410f2..4fbbe24c38 100644 --- a/doc/source/configuration/shared-file-systems/drivers.rst +++ b/doc/source/configuration/shared-file-systems/drivers.rst @@ -12,6 +12,7 @@ Share drivers drivers/generic-driver.rst drivers/cephfs-native-driver.rst + drivers/dell-emc-powerflex-driver.rst drivers/dell-emc-powermax-driver.rst drivers/dell-emc-unity-driver.rst drivers/dell-emc-vnx-driver.rst diff --git a/doc/source/configuration/shared-file-systems/drivers/dell-emc-powerflex-driver.rst b/doc/source/configuration/shared-file-systems/drivers/dell-emc-powerflex-driver.rst new file mode 100644 index 0000000000..ddfde986f1 --- /dev/null +++ b/doc/source/configuration/shared-file-systems/drivers/dell-emc-powerflex-driver.rst @@ -0,0 +1,160 @@ +========================= +Dell EMC PowerFlex driver +========================= + +The Dell EMC Shared File Systems service driver framework (EMCShareDriver) +utilizes the Dell EMC storage products to provide the shared file systems to +OpenStack. The Dell EMC driver is a plug-in based driver which is designed to +use different plug-ins to manage different Dell EMC storage products. + +The PowerFlex SDNAS plug-in manages the PowerFlex system to provide shared filesystems. +The Dell EMC driver framework with the PowerFlex SDNAS plug-in is referred to as the +PowerFlex SDNAS driver in this document. + +The PowerFlex SDNAS driver can be used to provide functions such as share and +snapshot for instances. + +The PowerFlex SDNAS driver enables the PowerFlex 4.x storage system to provide +file system management through REST API operations to OpenStack. + + +Requirements +------------ + +- PowerFlex 4.x storage system +- SDNAS cluster registrated with SDNAS Gateway. + + +Supported shared filesystems and operations +------------------------------------------- + +The driver suppors NFS shares only. + +The following operations are supported: + +* Create a share. +* Delete a share. +* Allow share access. +* Deny share access. +* Extend a share. +* Create a snapshot. +* Delete a snapshot. + + +Driver configuration +-------------------- + +Edit the ``manila.conf`` file, which is usually located under the following +path ``/etc/manila/manila.conf``. + +* Add a section for the PowerFlex SDNAS driver backend. + +* Under the ``[DEFAULT]`` section, set the ``enabled_share_backends`` parameter + with the name of the new backend section. + +* Configure the driver backend section with the parameters below. + + .. code-block:: ini + + share_driver = manila.share.drivers.dell_emc.driver.EMCShareDriver + emc_share_backend = powerflex + dell_nas_backend_host = + dell_nas_backend_port = + dell_nas_server = + dell_nas_login = + dell_nas_password = + powerflex_storage_pool = + powerflex_protection_domain = + share_backend_name = powerflex + dell_ssl_cert_verify = + dell_ssl_certificate_path = + + Where: + + +---------------------------------+----------------------------------------------------+ + | **Parameter** | **Description** | + +=================================+====================================================+ + | ``share_driver`` | Full path of the EMCShareDriver used to enable | + | | the plugin. | + +---------------------------------+----------------------------------------------------+ + | ``emc_share_backend`` | The plugin name. Set it to `powerflex` to | + | | enable the PowerFlex SDNAS driver. | + +---------------------------------+----------------------------------------------------+ + | ``dell_nas_backend_host`` | The management IP of the PowerFlex system. | + +---------------------------------+----------------------------------------------------+ + | ``dell_nas_backend_port`` | The port number used for secured connection. | + | | 443 by default if not provided. | + +---------------------------------+----------------------------------------------------+ + | ``dell_nas_server`` | The name of the NAS server within the | + | | PowerFlex system. | + +---------------------------------+----------------------------------------------------+ + | ``dell_nas_login`` | The login to use to connect to the PowerFlex | + | | system. It must have administrator privileges. | + +---------------------------------+----------------------------------------------------+ + | ``dell_nas_password`` | The password associated with the login. | + +---------------------------------+----------------------------------------------------+ + | ``powerflex_storage_pool`` | The name of the storage pool within the | + | | PowerFlex system. | + +---------------------------------+----------------------------------------------------+ + | ``powerflex_protection_domain`` | The name of the protection domain within the | + | | PowerFlex system. | + +---------------------------------+----------------------------------------------------+ + | ``share_backend_name`` | The name of the backend which provides shares. | + | | Must be set to powerflex | + +---------------------------------+----------------------------------------------------+ + | ``dell_ssl_cert_verify`` | Boolean to enable the usage of SSL certificates. | + | | False is the default value. | + +---------------------------------+----------------------------------------------------+ + | ``dell_ssl_certificate_path`` | Full path to SSL certificates. | + | | Applies only when the usage of SSL certificate is | + | | enabled. | + +---------------------------------+----------------------------------------------------+ + +Restart of manila-share service is needed for the configuration +changes to take effect. + +Required operations prior to any usage +-------------------------------------- + +A new share type needs to be created before going further. + +.. code-block:: console + + $ openstack share type create powerflex False + +Map this share type to the backend section configured in Manila + +.. code-block:: console + + $ openstack share type set --extra_specs share_backend_name=powerflex powerflex + + +Specific configuration for Snapshot support +------------------------------------------- + +The following extra specifications need to be configured with share type. + +- snapshot_support = True + +For new share type, these extra specifications can be set directly when +creating share type: + +.. code-block:: console + + $ openstack share type create --extra_specs snapshot_support=True ${share_type_name} False + +Or you can update already existing share type with command: + +.. code-block:: console + + $ openstack share type set --extra_specs snapshot_support=True ${share_type_name} + + +Known restrictions +------------------ + +The PowerFlex SDNAS driver has the following restrictions. + +- Minimum size 3GiB. +- Only NFS protocol is supported. +- Only DHSS=False is supported diff --git a/manila/share/drivers/dell_emc/driver.py b/manila/share/drivers/dell_emc/driver.py index 13d2756dd2..cb1e50dfa6 100644 --- a/manila/share/drivers/dell_emc/driver.py +++ b/manila/share/drivers/dell_emc/driver.py @@ -42,7 +42,7 @@ EMC_NAS_OPTS = [ cfg.StrOpt('emc_share_backend', ignore_case=True, choices=['isilon', 'vnx', 'unity', 'vmax', 'powermax', - 'powerstore'], + 'powerstore', 'powerflex'], help='Share backend.'), cfg.StrOpt('emc_nas_root_dir', help='The root directory where shares will be located.'), @@ -82,9 +82,11 @@ class EMCShareDriver(driver.ShareDriver): "OpenStack. After that, only " "'emc_share_backend=powermax' will be excepted.") self.backend_name = 'powermax' + LOG.info("BACKEND IS: %s", self.backend_name) self.plugin = self.plugin_manager.load_plugin( self.backend_name, configuration=self.configuration) + LOG.info(f"PLUGIN HAS: {self.plugin.__dict__}") super(EMCShareDriver, self).__init__( self.plugin.driver_handles_share_servers, *args, **kwargs) @@ -284,6 +286,7 @@ class EMCShareDriver(driver.ShareDriver): revert_to_snapshot_support=self.revert_to_snap_support) self.plugin.update_share_stats(data) super(EMCShareDriver, self)._update_share_stats(data) + LOG.info(f"Updated share stats: {self._stats}") def get_network_allocations_number(self): """Returns number of network allocations for creating VIFs.""" diff --git a/manila/share/drivers/dell_emc/plugins/powerflex/__init__.py b/manila/share/drivers/dell_emc/plugins/powerflex/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/share/drivers/dell_emc/plugins/powerflex/connection.py b/manila/share/drivers/dell_emc/plugins/powerflex/connection.py new file mode 100644 index 0000000000..e4c3e73450 --- /dev/null +++ b/manila/share/drivers/dell_emc/plugins/powerflex/connection.py @@ -0,0 +1,390 @@ +# Copyright (c) 2023 Dell Inc. or its subsidiaries. +# 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. + +""" +PowerFlex specific NAS backend plugin. +""" +from oslo_config import cfg +from oslo_log import log +from oslo_utils import units + +from manila.common import constants as const +from manila import exception +from manila.i18n import _ +from manila.share.drivers.dell_emc.plugins import base as driver +from manila.share.drivers.dell_emc.plugins.powerflex import ( + object_manager as manager) + +"""Version history: + 1.0 - Initial version +""" + +VERSION = "1.0" + +CONF = cfg.CONF + +LOG = log.getLogger(__name__) + +POWERFLEX_OPTS = [ + cfg.StrOpt('powerflex_storage_pool', + help='Storage pool used to provision NAS.'), + cfg.StrOpt('powerflex_protection_domain', + help='Protection domain to use.'), + cfg.StrOpt('dell_nas_backend_host', + help='Dell NAS backend hostname or IP address.'), + cfg.StrOpt('dell_nas_backend_port', + help='Port number to use with the Dell NAS backend.'), + cfg.StrOpt('dell_nas_server', + help='Root directory or NAS server which owns the shares.'), + cfg.StrOpt('dell_nas_login', + help='User name for the Dell NAS backend.'), + cfg.StrOpt('dell_nas_password', + secret=True, + help='Password for the Dell NAS backend.') + +] + + +class PowerFlexStorageConnection(driver.StorageConnection): + """Implements PowerFlex specific functionality for Dell Manila driver.""" + + def __init__(self, *args, **kwargs): + """Do initialization""" + + LOG.debug('Invoking base constructor for Manila \ + Dell PowerFlex SDNAS Driver.') + super(PowerFlexStorageConnection, + self).__init__(*args, **kwargs) + + LOG.debug('Setting up attributes for Manila \ + Dell PowerFlex SDNAS Driver.') + if 'configuration' in kwargs: + kwargs['configuration'].append_config_values(POWERFLEX_OPTS) + + self.manager = None + self.server = None + self._username = None + self._password = None + self._server_url = None + self._root_dir = None + self._verify_ssl_cert = None + self._shares = {} + self.verify_certificate = None + self.certificate_path = None + self.export_path = None + + self.driver_handles_share_servers = False + + self.reserved_percentage = None + self.reserved_snapshot_percentage = None + self.reserved_share_extend_percentage = None + self.max_over_subscription_ratio = None + + def connect(self, dell_share_driver, context): + """Connects to Dell PowerFlex SDNAS server.""" + LOG.debug('Reading configuration parameters for Manila \ + Dell PowerFlex SDNAS Driver.') + config = dell_share_driver.configuration + get_config_value = config.safe_get + self.verify_certificate = get_config_value("dell_ssl_cert_verify") + self.rest_ip = get_config_value("dell_nas_backend_host") + self.rest_port = (int(get_config_value("dell_nas_backend_port")) or + 443) + self.nas_server = get_config_value("dell_nas_server") + self.storage_pool = get_config_value("powerflex_storage_pool") + self.protection_domain = get_config_value( + "powerflex_protection_domain") + self.rest_username = get_config_value("dell_nas_login") + self.rest_password = get_config_value("dell_nas_password") + if self.verify_certificate: + self.certificate_path = get_config_value( + "dell_ssl_certificate_path") + if not all([self.rest_ip, + self.rest_username, + self.rest_password]): + message = _("REST server IP, username and password" + " must be specified.") + raise exception.BadConfigurationException(reason=message) + # validate certificate settings + if self.verify_certificate and not self.certificate_path: + message = _("Path to REST server's certificate must be specified.") + raise exception.BadConfigurationException(reason=message) + + LOG.debug('Initializing Dell PowerFlex SDNAS Layer.') + self.host_url = ("https://%(server_ip)s:%(server_port)s" % + { + "server_ip": self.rest_ip, + "server_port": self.rest_port}) + LOG.info("REST server IP: %(ip)s, port: %(port)s, " + "username: %(user)s. Verify server's certificate: " + "%(verify_cert)s.", + { + "ip": self.rest_ip, + "port": self.rest_port, + "user": self.rest_username, + "verify_cert": self.verify_certificate, + }) + + self.manager = manager.StorageObjectManager(self.host_url, + self.rest_username, + self.rest_password, + self.export_path, + self.certificate_path, + self.verify_certificate) + + # configuration for share status update + self.reserved_percentage = config.safe_get( + 'reserved_share_percentage') + if self.reserved_percentage is None: + self.reserved_percentage = 0 + + self.reserved_snapshot_percentage = config.safe_get( + 'reserved_share_from_snapshot_percentage') + if self.reserved_snapshot_percentage is None: + self.reserved_snapshot_percentage = self.reserved_percentage + + self.reserved_share_extend_percentage = config.safe_get( + 'reserved_share_extend_percentage') + if self.reserved_share_extend_percentage is None: + self.reserved_share_extend_percentage = self.reserved_percentage + + self.max_over_subscription_ratio = config.safe_get( + 'max_over_subscription_ratio') + + def create_share(self, context, share, share_server): + """Is called to create a share.""" + LOG.debug(f'Creating {share["share_proto"]} share.') + location = self._create_nfs_share(share) + + return location + + def create_share_from_snapshot(self, context, share, snapshot, + share_server=None, parent_share=None): + """Is called to create a share from an existing snapshot.""" + raise NotImplementedError() + + def allow_access(self, context, share, access, share_server): + """Is called to allow access to a share.""" + raise NotImplementedError() + + def check_for_setup_error(self): + """Is called to check for setup error.""" + + def update_access(self, context, share, access_rules, add_rules, + delete_rules, share_server=None): + """Is called to update share access.""" + LOG.debug(f'Updating access to {share["share_proto"]} share.') + return self._update_nfs_access(share, access_rules) + + def create_snapshot(self, context, snapshot, share_server): + """Is called to create snapshot.""" + export_name = snapshot['share_name'] + LOG.debug(f'Retrieving filesystem ID for share {export_name}') + filesystem_id = self.manager.get_fsid_from_export_name(export_name) + LOG.debug(f'Retrieving snapshot ID for filesystem {filesystem_id}') + snapshot_id = self.manager.create_snapshot(snapshot['name'], + filesystem_id) + if snapshot_id: + LOG.info("Snapshot %(id)s successfully created.", + {'id': snapshot['id']}) + + def delete_snapshot(self, context, snapshot, share_server): + """Is called to delete snapshot.""" + snapshot_name = snapshot['name'] + filesystem_id = self.manager.get_fsid_from_snapshot_name(snapshot_name) + LOG.debug(f'Retrieving filesystem ID for snapshot {snapshot_name}') + snapshot_deleted = self.manager.delete_filesystem(filesystem_id) + if not snapshot_deleted: + message = ( + _('Failed to delete snapshot "%(snapshot)s".') % + {'snapshot': snapshot['name']}) + LOG.error(message) + raise exception.ShareBackendException(msg=message) + else: + LOG.info("Snapshot %(id)s successfully deleted.", + {'id': snapshot['id']}) + + def delete_share(self, context, share, share_server): + """Is called to delete a share.""" + LOG.debug(f'Deleting {share["share_proto"]} share.') + self._delete_nfs_share(share) + + def deny_access(self, context, share, access, share_server): + """Is called to deny access to a share.""" + raise NotImplementedError() + + def ensure_share(self, context, share, share_server): + """Is called to ensure a share is exported.""" + + def extend_share(self, share, new_size, share_server=None): + """Is called to extend a share.""" + # Converts the size from GiB to Bytes + new_size_in_bytes = new_size * units.Gi + LOG.debug(f"Extending {share['name']} to {new_size}GiB") + filesystem_id = self.manager.get_filesystem_id(share['name']) + self.manager.extend_export(filesystem_id, + new_size_in_bytes) + + def setup_server(self, network_info, metadata=None): + """Is called to set up a share server. + + Requires driver_handles_share_servers to be True. + """ + raise NotImplementedError() + + def teardown_server(self, server_details, security_services=None): + """Is called to teardown a share server. + + Requires driver_handles_share_servers to be True. + """ + raise NotImplementedError() + + def _create_nfs_share(self, share): + """Creates an NFS share. + + In PowerFlex, an export (share) belongs to a filesystem. + This function creates a filesystem and an export. + """ + LOG.debug(f'Retrieving Storage Pool ID for {self.storage_pool}') + storage_pool_id = self.manager.get_storage_pool_id( + self.protection_domain, + self.storage_pool) + nas_server_id = self.manager.get_nas_server_id(self.nas_server) + LOG.debug(f"Creating filesystem {share['name']}") + size_in_bytes = share['size'] * units.Gi + filesystem_id = self.manager.create_filesystem(storage_pool_id, + self.nas_server, + share['name'], + size_in_bytes) + if not filesystem_id: + message = { + _('The requested NFS export "%(export)s"' + ' was not created.') % + {'export': share['name']}} + LOG.error(message) + raise exception.ShareBackendException(msg=message) + + LOG.debug(f"Creating export {share['name']}") + export_id = self.manager.create_nfs_export(filesystem_id, + share['name']) + if not export_id: + message = ( + _('The requested NFS export "%(export)s"' + ' was not created.') % + {'export': share['name']}) + LOG.error(message) + raise exception.ShareBackendException(msg=message) + file_interfaces = self.manager.get_nas_server_interfaces( + nas_server_id) + export_path = self.manager.get_nfs_export_name(export_id) + locations = self._get_nfs_location(file_interfaces, + export_path) + return locations + + def _delete_nfs_share(self, share): + """Deletes a filesystem and its associated export.""" + filesystem_id = self.manager.get_filesystem_id(share['name']) + LOG.debug(f"Retrieving filesystem ID for filesystem {share['name']}") + if filesystem_id is None: + message = ('Attempted to delete NFS export "%s",' + ' but the export does not appear to exist.') + LOG.warning(message, share['name']) + else: + LOG.debug(f"Deleting filesystem ID {filesystem_id}") + share_deleted = self.manager.delete_filesystem(filesystem_id) + if not share_deleted: + message = ( + _('Failed to delete NFS export "%(export)s".') % + {'export': share['name']}) + LOG.error(message) + raise exception.ShareBackendException(msg=message) + + def _update_nfs_access(self, share, access_rules): + """Updates access rules for NFS share type.""" + nfs_rw_ips = set() + nfs_ro_ips = set() + access_updates = {} + + for rule in access_rules: + if rule['access_type'].lower() != 'ip': + message = (_("Only IP access type currently supported for " + "NFS. Share provided %(share)s with rule type " + "%(type)s") % {'share': share['display_name'], + 'type': rule['access_type']}) + LOG.error(message) + access_updates.update({rule['access_id']: {'state': 'error'}}) + + else: + if rule['access_level'] == const.ACCESS_LEVEL_RW: + nfs_rw_ips.add(rule['access_to']) + elif rule['access_level'] == const.ACCESS_LEVEL_RO: + nfs_ro_ips.add(rule['access_to']) + access_updates.update({rule['access_id']: {'state': 'active'}}) + + share_id = self.manager.get_nfs_export_id(share['name']) + share_updated = self.manager.set_export_access(share_id, + nfs_rw_ips, + nfs_ro_ips) + if not share_updated: + message = ( + _('Failed to update NFS access rules for "%(export)s".') % + {'export': share['display_name']}) + LOG.error(message) + raise exception.ShareBackendException(msg=message) + return access_updates + + def update_share_stats(self, stats_dict): + """Retrieve stats info from share.""" + stats_dict['driver_version'] = VERSION + stats_dict['storage_protocol'] = 'NFS' + stats_dict['create_share_from_snapshot_support'] = False + stats_dict['pools'] = [] + storage_pool_id = self.manager.get_storage_pool_id( + self.protection_domain, + self.storage_pool + ) + statistic = self.manager.get_storage_pool_statistic(storage_pool_id) + if statistic: + total = statistic.get('maxCapacityInKb') // units.Mi + free = statistic.get('netUnusedCapacityInKb') // units.Mi + used = statistic.get('capacityInUseInKb') // units.Mi + provisioned = statistic.get('primaryVacInKb') // units.Mi + pool_stat = { + 'pool_name': self.storage_pool, + 'thin_provisioning': True, + 'total_capacity_gb': total, + 'free_capacity_gb': free, + 'allocated_capacity_gb': used, + 'provisioned_capacity_gb': provisioned, + 'qos': False, + 'reserved_percentage': self.reserved_percentage, + 'reserved_snapshot_percentage': + self.reserved_snapshot_percentage, + 'reserved_share_extend_percentage': + self.reserved_share_extend_percentage, + 'max_over_subscription_ratio': + self.max_over_subscription_ratio + } + stats_dict['pools'].append(pool_stat) + + def _get_nfs_location(self, file_interfaces, export_path): + export_locations = [] + for interface in file_interfaces: + export_locations.append( + {'path': f"{interface}:/{export_path}"}) + return export_locations + + def get_default_filter_function(self): + return 'share.size >= 3' diff --git a/manila/share/drivers/dell_emc/plugins/powerflex/object_manager.py b/manila/share/drivers/dell_emc/plugins/powerflex/object_manager.py new file mode 100644 index 0000000000..d4da6d9c5e --- /dev/null +++ b/manila/share/drivers/dell_emc/plugins/powerflex/object_manager.py @@ -0,0 +1,409 @@ +# Copyright (c) 2023 Dell Inc. or its subsidiaries. +# 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. + +from http import client as http_client +import json + +from oslo_log import log as logging +import requests + +from manila import exception + +LOG = logging.getLogger(__name__) + + +class StorageObjectManager(object): + + def __init__(self, + host_url, + username, + password, + export_path, + certificate_path=None, + verify_ssl_cert=False): + self.host_url = host_url + self.base_url = host_url + '/rest' + self.rest_username = username + self.rest_password = password + self.rest_token = None + self.got_token = False + self.export_path = export_path + self.verify_certificate = verify_ssl_cert + self.certificate_path = certificate_path + + def _get_headers(self): + if self.got_token: + return {"Content-type": "application/json", + "Accept": "application/json", + "Authorization": "Bearer " + self.rest_token} + else: + return {"Content-type": "application/json", + "Accept": "application/json"} + + def execute_powerflex_get_request(self, url, **url_params): + request = url % url_params + res = requests.get(request, + headers=self._get_headers(), + verify=self._get_verify_cert()) + res = self._check_response(res, request, "GET") + response = res.json() + + return res, response + + def execute_powerflex_post_request(self, url, params=None, **url_params): + if not params: + params = {} + request = url % url_params + res = requests.post(request, + data=json.dumps(params), + headers=self._get_headers(), + verify=self._get_verify_cert()) + res = self._check_response(res, request, "POST", params) + response = None + try: + response = res.json() + except ValueError: + # Particular case for get_storage_pool_id which is not + # a json object but a string + response = res + return res, response + + def execute_powerflex_delete_request(self, url, **url_params): + request = url % url_params + res = requests.delete(request, + headers=self._get_headers(), + verify=self._get_verify_cert()) + res = self._check_response(res, request, "DELETE") + return res + + def execute_powerflex_patch_request(self, url, params=None, **url_params): + if not params: + params = {} + request = url % url_params + res = requests.patch(request, + data=json.dumps(params), + headers=self._get_headers(), + verify=self._get_verify_cert()) + res = self._check_response(res, request, "PATCH") + return res + + def _check_response(self, + response, + request, + request_type, + params=None): + login_url = "/auth/login" + + if (response.status_code == http_client.UNAUTHORIZED or + response.status_code == http_client.FORBIDDEN): + LOG.info("Dell PowerFlex token is invalid, going to re-login " + "and get a new one.") + login_request = self.base_url + login_url + verify_cert = self._get_verify_cert() + self.got_token = False + payload = json.dumps({"username": self.rest_username, + "password": self.rest_password}) + res = requests.post(login_request, + headers=self._get_headers(), + data=payload, + verify=verify_cert) + if (res.status_code == http_client.UNAUTHORIZED or + res.status_code == http_client.FORBIDDEN): + message = ("PowerFlex REST API access is still forbidden or " + "unauthorized, there might be an issue with your " + "credentials.") + LOG.error(message) + raise exception.NotAuthorized() + else: + token = res.json()["access_token"] + self.rest_token = token + self.got_token = True + LOG.info("Going to perform request again %s with valid token.", + request) + if (request_type == "GET"): + response = requests.get(request, + headers=self._get_headers(), + verify=verify_cert) + elif (request_type == "POST"): + response = requests.post(request, + headers=self._get_headers(), + data=json.dumps(params), + verify=verify_cert) + elif (request_type == "DELETE"): + response = requests.delete(request, + headers=self._get_headers(), + verify=verify_cert) + elif (request_type == "PATCH"): + response = requests.patch(request, + headers=self._get_headers(), + data=json.dumps(params), + verify=verify_cert) + level = logging.DEBUG + if response.status_code != http_client.OK: + level = logging.ERROR + LOG.log(level, + "REST REQUEST: %s with params %s", + request, + json.dumps(params)) + LOG.log(level, + "REST RESPONSE: %s with params %s", + response.status_code, + response.text) + return response + + def _get_verify_cert(self): + verify_cert = False + if self.verify_certificate: + verify_cert = self.certificate_path + return verify_cert + + def create_filesystem(self, storage_pool_id, nas_server, name, size): + """Creates a filesystem. + + :param nas_server: name of the nas_server + :param name: name of the filesystem + :param size: size in GiB + :return: ID of the filesystem if created successfully + """ + nas_server_id = self.get_nas_server_id(nas_server) + params = { + "name": name, + "size_total": size, + "storage_pool_id": storage_pool_id, + "nas_server_id": nas_server_id + } + url = self.base_url + '/v1/file-systems' + res, response = self.execute_powerflex_post_request(url, params) + if res.status_code == 201: + return response["id"] + + def create_nfs_export(self, filesystem_id, name): + """Creates an NFS export. + + :param filesystem_id: ID of the filesystem on which + the export will be created + :param name: name of the NFS export + :return: ID of the export if created successfully + """ + params = { + "file_system_id": filesystem_id, + "path": "/" + str(name), + "name": name + } + url = self.base_url + '/v1/nfs-exports' + res, response = self.execute_powerflex_post_request(url, params) + if res.status_code == 201: + return response["id"] + + def delete_filesystem(self, filesystem_id): + """Deletes a filesystem and all associated export. + + :param filesystem_id: ID of the filesystem to delete + :return: True if deleted successfully + """ + url = self.base_url + \ + '/v1/file-systems/' + \ + filesystem_id + res = self.execute_powerflex_delete_request(url) + return res.status_code == 204 + + def create_snapshot(self, name, filesystem_id): + """Creates a snapshot of a filesystem. + + :param name: name of the snapshot + :param filesystem_id: ID of the filesystem + :return: ID of the snapshot if created successfully + """ + params = { + "name": name + } + url = self.base_url + \ + '/v1/file-systems/' + \ + filesystem_id + \ + '/snapshot' + res, response = self.execute_powerflex_post_request(url, params) + return res.status_code == 201 + + def get_nas_server_id(self, nas_server): + """Retrieves the NAS server ID. + + :param nas_server: NAS server name + :return: ID of the NAS server if success + """ + url = self.base_url + \ + '/v1/nas-servers?select=id&name=eq.' + \ + nas_server + res, response = self.execute_powerflex_get_request(url) + if res.status_code == 200: + return response[0]['id'] + + def get_nfs_export_name(self, export_id): + """Retrieves NFS Export name. + + :param export_id: ID of the NFS export + :return: path of the NFS export if success + """ + url = self.base_url + '/v1/nfs-exports/' + export_id + '?select=*' + res, response = self.execute_powerflex_get_request(url) + if res.status_code == 200: + return response["name"] + + def get_filesystem_id(self, name): + """Retrieves an ID for a filesystem. + + :param name: name of the filesystem + :return: ID of the filesystem if success + """ + url = self.base_url + \ + '/v1/file-systems?select=id&name=eq.' + \ + name + res, response = self.execute_powerflex_get_request(url) + if res.status_code == 200: + return response[0]['id'] + + def get_nfs_export_id(self, name): + """Retrieves NFS Export ID. + + :param name: name of the NFS export + :return: id of the NFS export if success + """ + url = self.base_url + \ + '/v1/nfs-exports?select=id&name=eq.' + \ + name + res, response = self.execute_powerflex_get_request(url) + if res.status_code == 200: + return response[0]['id'] + + def get_storage_pool_id(self, protection_domain, storage_pool): + """Retrieves the Storage Pool ID. + + :param protection_domain: protection domain name + :param storage_pool: storage pool name + :return: ID of the storage pool if success + """ + params = { + "protectionDomainName": protection_domain, + "name": storage_pool + } + url = self.host_url + \ + '/api/types/StoragePool/instances/action/queryIdByKey' + res, response = self.execute_powerflex_post_request(url, params) + if res.status_code == 200: + return response + + def set_export_access(self, export_id, rw_hosts, ro_hosts): + """Sets the authorization access on the export. + + :param export_id: NFS export ID + :param rw_hosts: a set of RW hosts + :param ro_hosts: a set of RO hosts + :return: True if operation succeeded + """ + params = { + "read_only_hosts": list(ro_hosts), + "read_write_root_hosts": list(rw_hosts) + } + url = self.base_url + \ + '/v1/nfs-exports/' + \ + export_id + res = self.execute_powerflex_patch_request(url, params) + return res.status_code == 204 + + def extend_export(self, export_id, new_size): + """Extends the size of a share to a new size. + + :param export_id: ID of the NFS export + :param new_size: new size to allocate in bytes + :return: True if extended successfully + """ + params = { + "size_total": new_size + } + url = self.base_url + \ + '/v1/file-systems/' + \ + export_id + res = self.execute_powerflex_patch_request(url, params) + return res.status_code == 204 + + def get_fsid_from_export_name(self, name): + """Retieves the Filesystem ID used by an export. + + :param name: name of the export + :return: ID of the Filesystem which owns the export + """ + url = self.base_url + \ + '/v1/nfs-exports?select=file_system_id&name=eq.' + \ + name + res, response = self.execute_powerflex_get_request(url) + if res.status_code == 200: + return response[0]['file_system_id'] + + def get_fsid_from_snapshot_name(self, snapshot_name): + """Retrieves the Filesystem ID used by a snapshot. + + :param snapshot_name: Name of the snapshot + :return: ID of the parent filesystem of the snapshot + """ + url = self.base_url + \ + '/v1/file-systems?select=id&name=eq.' + \ + snapshot_name + res, response = self.execute_powerflex_get_request(url) + if res.status_code == 200: + return response[0]['id'] + + def get_storage_pool_spare_percentage(self, storage_pool_id): + """Retrieves the spare capacity percentage of the storage pool. + + :param storage_pool_id: ID of the storage pool + :return: Spare capacity percentage of the storage pool + """ + url = self.host_url + \ + '/api/instances/StoragePool::' + \ + storage_pool_id + res, response = self.execute_powerflex_get_request(url) + if res.status_code == 200: + return response['sparePercentage'] + + def get_storage_pool_statistic(self, storage_pool_id): + """Retrieves the spare capacity percentage of the storage pool. + + :param storage_pool_id: ID of the storage pool + :return: Statistics of the storage pool + """ + url = self.host_url + \ + '/api/instances/StoragePool::' + \ + storage_pool_id + '/relationships/Statistics' + res, response = self.execute_powerflex_get_request(url) + if res.status_code == 200: + statistics = { + "maxCapacityInKb": response['maxCapacityInKb'], + "capacityInUseInKb": response['capacityInUseInKb'], + "netUnusedCapacityInKb": response['netUnusedCapacityInKb'], + "primaryVacInKb": response['primaryVacInKb'], + } + return statistics + + def get_nas_server_interfaces(self, nas_server_id): + """Retrieves the file interfaces for a given na_server. + + :param nas_server_id: ID of the NAS server + :return: file interfaces of the NAS server + """ + url = self.base_url + \ + '/v1/file-interfaces?select=ip_address&nas_server_id=eq.' + \ + nas_server_id + res, response = self.execute_powerflex_get_request(url) + if res.status_code == 200: + return [i['ip_address'] for i in response] diff --git a/manila/tests/share/drivers/dell_emc/plugins/powerflex/__init__.py b/manila/tests/share/drivers/dell_emc/plugins/powerflex/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/create_filesystem_response.json b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/create_filesystem_response.json new file mode 100644 index 0000000000..18d660ef09 --- /dev/null +++ b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/create_filesystem_response.json @@ -0,0 +1,3 @@ +{ + "id": "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3" +} \ No newline at end of file diff --git a/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/create_nfs_export_response.json b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/create_nfs_export_response.json new file mode 100644 index 0000000000..864abaf175 --- /dev/null +++ b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/create_nfs_export_response.json @@ -0,0 +1,3 @@ +{ + "id": "6433a2b2-6d60-f737-9f3b-2a50fb1ccff3" +} \ No newline at end of file diff --git a/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/create_nfs_snapshot_response.json b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/create_nfs_snapshot_response.json new file mode 100644 index 0000000000..17dcf202e6 --- /dev/null +++ b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/create_nfs_snapshot_response.json @@ -0,0 +1,3 @@ +{ + "id": "6433b635-6c1f-878e-6467-2a50fb1ccff3" +} \ No newline at end of file diff --git a/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_fileystem_id_response.json b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_fileystem_id_response.json new file mode 100644 index 0000000000..713fac4be0 --- /dev/null +++ b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_fileystem_id_response.json @@ -0,0 +1,5 @@ +[ + { + "id": "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3" + } +] \ No newline at end of file diff --git a/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_fsid_from_export_name_response.json b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_fsid_from_export_name_response.json new file mode 100644 index 0000000000..78b4c05294 --- /dev/null +++ b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_fsid_from_export_name_response.json @@ -0,0 +1,5 @@ +[ + { + "file_system_id": "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3" + } +] \ No newline at end of file diff --git a/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_fsid_from_snapshot_name_response.json b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_fsid_from_snapshot_name_response.json new file mode 100644 index 0000000000..96c2d2e312 --- /dev/null +++ b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_fsid_from_snapshot_name_response.json @@ -0,0 +1,5 @@ +[ + { + "id": "6433b635-6c1f-878e-6467-2a50fb1ccff3" + } +] \ No newline at end of file diff --git a/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_nas_server_id_response.json b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_nas_server_id_response.json new file mode 100644 index 0000000000..d6742b108d --- /dev/null +++ b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_nas_server_id_response.json @@ -0,0 +1,5 @@ +[ + { + "id": "64132f37-d33e-9d4a-89ba-d625520a4779" + } +] \ No newline at end of file diff --git a/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_nfs_export_id_response.json b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_nfs_export_id_response.json new file mode 100644 index 0000000000..b2f40a6b03 --- /dev/null +++ b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_nfs_export_id_response.json @@ -0,0 +1,5 @@ +[ + { + "id": "6433a2b2-6d60-f737-9f3b-2a50fb1ccff3" + } +] \ No newline at end of file diff --git a/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_nfs_export_name_response.json b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_nfs_export_name_response.json new file mode 100644 index 0000000000..69f2f9a363 --- /dev/null +++ b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_nfs_export_name_response.json @@ -0,0 +1,20 @@ +{ + "id": "6433a2b2-6d60-f737-9f3b-2a50fb1ccff3", + "file_system_id": "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3", + "name": "Manila-UT-filesystem", + "path": "/Manila-UT-filesystem", + "description": null, + "default_access": "NO_ACCESS", + "min_security": "SYS", + "nfs_owner_username": "root", + "no_access_hosts": [], + "read_only_hosts": [], + "read_only_root_hosts": [], + "read_write_hosts": [], + "read_write_root_hosts": [], + "anonymous_UID": -2, + "anonymous_GID": -2, + "is_no_SUID": false, + "default_access_l10n": null, + "min_security_l10n": null +} \ No newline at end of file diff --git a/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_storage_pool_id_response.json b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_storage_pool_id_response.json new file mode 100644 index 0000000000..0beddc07e6 --- /dev/null +++ b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_storage_pool_id_response.json @@ -0,0 +1 @@ +"28515fee00000000" \ No newline at end of file diff --git a/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_storage_pool_spare_percentage.json b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_storage_pool_spare_percentage.json new file mode 100644 index 0000000000..5b49537d5a --- /dev/null +++ b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_storage_pool_spare_percentage.json @@ -0,0 +1,98 @@ +{ + "name": "Env8-SP-SW_SSD-1", + "rebuildIoPriorityPolicy": "limitNumOfConcurrentIos", + "rebalanceIoPriorityPolicy": "favorAppIos", + "vtreeMigrationIoPriorityPolicy": "favorAppIos", + "protectedMaintenanceModeIoPriorityPolicy": "limitNumOfConcurrentIos", + "rebuildIoPriorityNumOfConcurrentIosPerDevice": 1, + "rebalanceIoPriorityNumOfConcurrentIosPerDevice": 1, + "vtreeMigrationIoPriorityNumOfConcurrentIosPerDevice": 1, + "protectedMaintenanceModeIoPriorityNumOfConcurrentIosPerDevice": 1, + "rebuildIoPriorityBwLimitPerDeviceInKbps": 10240, + "rebalanceIoPriorityBwLimitPerDeviceInKbps": 10240, + "vtreeMigrationIoPriorityBwLimitPerDeviceInKbps": 10240, + "protectedMaintenanceModeIoPriorityBwLimitPerDeviceInKbps": 10240, + "rebuildIoPriorityAppIopsPerDeviceThreshold": null, + "rebalanceIoPriorityAppIopsPerDeviceThreshold": null, + "vtreeMigrationIoPriorityAppIopsPerDeviceThreshold": null, + "protectedMaintenanceModeIoPriorityAppIopsPerDeviceThreshold": null, + "rebuildIoPriorityAppBwPerDeviceThresholdInKbps": null, + "rebalanceIoPriorityAppBwPerDeviceThresholdInKbps": null, + "zeroPaddingEnabled": true, + "vtreeMigrationIoPriorityAppBwPerDeviceThresholdInKbps": null, + "protectedMaintenanceModeIoPriorityAppBwPerDeviceThresholdInKbps": null, + "rebuildIoPriorityQuietPeriodInMsec": null, + "rebalanceIoPriorityQuietPeriodInMsec": null, + "vtreeMigrationIoPriorityQuietPeriodInMsec": null, + "protectedMaintenanceModeIoPriorityQuietPeriodInMsec": null, + "useRmcache": false, + "backgroundScannerMode": "DataComparison", + "backgroundScannerBWLimitKBps": 3072, + "fglAccpId": null, + "fglMetadataSizeXx100": null, + "fglNvdimmWriteCacheSizeInMb": null, + "fglNvdimmMetadataAmortizationX100": null, + "mediaType": "SSD", + "rmcacheWriteHandlingMode": "Cached", + "checksumEnabled": false, + "rebalanceEnabled": true, + "fragmentationEnabled": true, + "numOfParallelRebuildRebalanceJobsPerDevice": 2, + "bgScannerCompareErrorAction": "ReportAndFix", + "bgScannerReadErrorAction": "ReportAndFix", + "externalAccelerationType": "None", + "compressionMethod": "Invalid", + "fglExtraCapacity": null, + "fglOverProvisioningFactor": null, + "fglWriteAtomicitySize": null, + "fglMaxCompressionRatio": null, + "fglPerfProfile": null, + "replicationCapacityMaxRatio": 35, + "persistentChecksumEnabled": true, + "persistentChecksumState": "Protected", + "persistentChecksumBuilderLimitKb": 3072, + "protectionDomainId": "95c5a8b100000000", + "rebuildEnabled": true, + "dataLayout": "MediumGranularity", + "persistentChecksumValidateOnRead": false, + "spClass": "Nas", + "addressSpaceUsage": "Normal", + "useRfcache": false, + "sparePercentage": 34, + "capacityAlertHighThreshold": 66, + "capacityAlertCriticalThreshold": 83, + "capacityUsageState": "Normal", + "addressSpaceUsageType": "DeviceCapacityLimit", + "capacityUsageType": "NetCapacity", + "id": "28515fee00000000", + "links": [ + { + "rel": "self", + "href": "/api/instances/StoragePool::28515fee00000000" + }, + { + "rel": "/api/StoragePool/relationship/Statistics", + "href": "/api/instances/StoragePool::28515fee00000000/relationships/Statistics" + }, + { + "rel": "/api/StoragePool/relationship/SpSds", + "href": "/api/instances/StoragePool::28515fee00000000/relationships/SpSds" + }, + { + "rel": "/api/StoragePool/relationship/Volume", + "href": "/api/instances/StoragePool::28515fee00000000/relationships/Volume" + }, + { + "rel": "/api/StoragePool/relationship/Device", + "href": "/api/instances/StoragePool::28515fee00000000/relationships/Device" + }, + { + "rel": "/api/StoragePool/relationship/VTree", + "href": "/api/instances/StoragePool::28515fee00000000/relationships/VTree" + }, + { + "rel": "/api/parent/relationship/protectionDomainId", + "href": "/api/instances/ProtectionDomain::95c5a8b100000000" + } + ] +} \ No newline at end of file diff --git a/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_storage_pool_statistic.json b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_storage_pool_statistic.json new file mode 100644 index 0000000000..98f9e60788 --- /dev/null +++ b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/get_storage_pool_statistic.json @@ -0,0 +1,381 @@ +{ + "backgroundScanFixedReadErrorCount": 0, + "pendingMovingOutBckRebuildJobs": 0, + "degradedHealthyCapacityInKb": 0, + "activeMovingOutFwdRebuildJobs": 0, + "bckRebuildWriteBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "netFglUncompressedDataSizeInKb": 0, + "primaryReadFromDevBwc": { + "numSeconds": 5, + "totalWeightInKb": 1188, + "numOccured": 18 + }, + "BackgroundScannedInMB": 233566, + "volumeIds": [ + "2488c46c00000003", + "2488c46d00000004", + "2488c46e00000005", + "2488c46f00000006", + "2488c47000000007", + "2488c47100000008", + "2488c47200000009", + "2488c4730000000a", + "2488eb7e00000001" + ], + "maxUserDataCapacityInKb": 3185378304, + "persistentChecksumBuilderProgress": 100, + "rfcacheReadsSkippedAlignedSizeTooLarge": 0, + "pendingMovingInRebalanceJobs": 0, + "rfcacheWritesSkippedHeavyLoad": 0, + "unusedCapacityInKb": 3132161024, + "userDataSdcReadLatency": { + "numSeconds": 5, + "totalWeightInKb": 31971, + "numOccured": 45 + }, + "totalReadBwc": { + "numSeconds": 5, + "totalWeightInKb": 4644, + "numOccured": 45 + }, + "numOfDeviceAtFaultRebuilds": 0, + "totalWriteBwc": { + "numSeconds": 5, + "totalWeightInKb": 76, + "numOccured": 19 + }, + "persistentChecksumCapacityInKb": 2359296, + "rmPendingAllocatedInKb": 0, + "numOfVolumes": 12, + "rfcacheIosOutstanding": 0, + "numOfMappedToAllVolumes": 0, + "capacityAvailableForVolumeAllocationInKb": 1551892480, + "netThinUserDataCapacityInKb": 26608640, + "backgroundScanFixedCompareErrorCount": 0, + "volMigrationWriteBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "thinAndSnapshotRatio": 6.9356937, + "pendingMovingInEnterProtectedMaintenanceModeJobs": 0, + "fglUserDataCapacityInKb": 0, + "activeMovingInNormRebuildJobs": 0, + "aggregateCompressionLevel": "Uncompressed", + "targetOtherLatency": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "netUserDataCapacityInKb": 26608640, + "pendingMovingOutExitProtectedMaintenanceModeJobs": 0, + "overallUsageRatio": 6.9356937, + "volMigrationReadBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "rfcacheReadsSkippedInternalError": 0, + "netCapacityInUseNoOverheadInKb": 26608640, + "pendingMovingInBckRebuildJobs": 0, + "activeBckRebuildCapacityInKb": 0, + "rebalanceCapacityInKb": 0, + "pendingMovingInExitProtectedMaintenanceModeJobs": 0, + "rfcacheReadsSkippedLowResources": 0, + "rplJournalCapAllowed": 1115684864, + "userDataSdcTrimLatency": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "thinCapacityInUseInKb": 0, + "activeMovingInEnterProtectedMaintenanceModeJobs": 0, + "rfcacheWritesSkippedInternalError": 0, + "netUserDataCapacityNoTrimInKb": 26608640, + "rfcacheWritesSkippedCacheMiss": 0, + "degradedFailedCapacityInKb": 0, + "activeNormRebuildCapacityInKb": 0, + "numOfMigratingVolumes": 0, + "fglSparesInKb": 0, + "snapCapacityInUseInKb": 0, + "compressionRatio": 1, + "rfcacheWriteMiss": 0, + "primaryReadFromRmcacheBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "migratingVtreeIds": [], + "numOfVtrees": 12, + "userDataCapacityNoTrimInKb": 53217280, + "rfacheReadHit": 0, + "rplUsedJournalCap": 21504, + "compressedDataCompressionRatio": 0, + "pendingMovingCapacityInKb": 0, + "numOfSnapshots": 0, + "pendingFwdRebuildCapacityInKb": 0, + "tempCapacityInKb": 0, + "totalFglMigrationSizeInKb": 0, + "normRebuildCapacityInKb": 0, + "logWrittenBlocksInKb": 0, + "numOfThickBaseVolumes": 0, + "primaryWriteBwc": { + "numSeconds": 5, + "totalWeightInKb": 36, + "numOccured": 9 + }, + "enterProtectedMaintenanceModeReadBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "activeRebalanceCapacityInKb": 0, + "numOfReplicationJournalVolumes": 3, + "rfcacheReadsSkippedLockIos": 0, + "unreachableUnusedCapacityInKb": 0, + "netProvisionedAddressesInKb": 26608640, + "trimmedUserDataCapacityInKb": 0, + "provisionedAddressesInKb": 53217280, + "numOfVolumesInDeletion": 0, + "maxCapacityInKb": 4826330112, + "pendingMovingOutFwdRebuildJobs": 0, + "rmPendingThickInKb": 0, + "protectedCapacityInKb": 53217280, + "secondaryWriteBwc": { + "numSeconds": 5, + "totalWeightInKb": 40, + "numOccured": 10 + }, + "normRebuildReadBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "thinCapacityAllocatedInKb": 369098752, + "netFglUserDataCapacityInKb": 0, + "metadataOverheadInKb": 0, + "thinCapacityAllocatedInKm": 369098752, + "rebalanceWriteBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "primaryVacInKb": 184549376, + "deviceIds": [ + "fbdbf0a700000000", + "fbdef0a800010000", + "fbdff0a600020000" + ], + "secondaryVacInKb": 184549376, + "netSnapshotCapacityInKb": 0, + "numOfDevices": 3, + "rplTotalJournalCap": 25165824, + "failedCapacityInKb": 0, + "netMetadataOverheadInKb": 0, + "activeMovingOutBckRebuildJobs": 0, + "rfcacheReadsFromCache": 0, + "pendingMovingInNormRebuildJobs": 0, + "enterProtectedMaintenanceModeCapacityInKb": 0, + "activeMovingOutEnterProtectedMaintenanceModeJobs": 0, + "primaryReadBwc": { + "numSeconds": 5, + "totalWeightInKb": 4644, + "numOccured": 45 + }, + "failedVacInKb": 0, + "fglCompressedDataSizeInKb": 0, + "fglUncompressedDataSizeInKb": 0, + "pendingRebalanceCapacityInKb": 0, + "rfcacheAvgReadTime": 0, + "semiProtectedCapacityInKb": 0, + "pendingMovingOutEnterProtectedMaintenanceModeJobs": 0, + "mgUserDdataCcapacityInKb": 53217280, + "netMgUserDataCapacityInKb": 26608640, + "snapshotCapacityInKb": 0, + "fwdRebuildReadBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "rfcacheWritesReceived": 0, + "netUnusedCapacityInKb": 1566080512, + "thinUserDataCapacityInKb": 53217280, + "protectedVacInKb": 369098752, + "bckRebuildCapacityInKb": 0, + "activeMovingInFwdRebuildJobs": 0, + "activeMovingRebalanceJobs": 0, + "netTrimmedUserDataCapacityInKb": 0, + "pendingMovingRebalanceJobs": 0, + "numOfMarkedVolumesForReplication": 1, + "degradedHealthyVacInKb": 0, + "semiProtectedVacInKb": 0, + "userDataReadBwc": { + "numSeconds": 5, + "totalWeightInKb": 4644, + "numOccured": 45 + }, + "pendingBckRebuildCapacityInKb": 0, + "capacityLimitInKb": 4826330112, + "vtreeIds": [ + "3ad4906800000003", + "3ad4906900000004", + "3ad4906a00000005", + "3ad4906b00000006", + "3ad4906c00000007", + "3ad4906d00000008", + "3ad4906e00000009", + "3ad4906f0000000a", + "3ad4b77700000002", + "3ad4b7780000000b", + "3ad4b7790000000c", + "3ad4b77a00000000" + ], + "activeMovingCapacityInKb": 0, + "targetWriteLatency": { + "numSeconds": 5, + "totalWeightInKb": 11392, + "numOccured": 9 + }, + "pendingExitProtectedMaintenanceModeCapacityInKb": 0, + "rfcacheIosSkipped": 0, + "userDataWriteBwc": { + "numSeconds": 5, + "totalWeightInKb": 40, + "numOccured": 10 + }, + "inMaintenanceVacInKb": 0, + "exitProtectedMaintenanceModeReadBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "netFglSparesInKb": 0, + "rfcacheReadsSkipped": 0, + "activeExitProtectedMaintenanceModeCapacityInKb": 0, + "activeMovingOutExitProtectedMaintenanceModeJobs": 0, + "numOfUnmappedVolumes": 4, + "tempCapacityVacInKb": 0, + "volumeAddressSpaceInKb": 184549376, + "currentFglMigrationSizeInKb": 0, + "rfcacheWritesSkippedMaxIoSize": 0, + "netMaxUserDataCapacityInKb": 1592689152, + "numOfMigratingVtrees": 0, + "atRestCapacityInKb": 26608640, + "rfacheWriteHit": 0, + "bckRebuildReadBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "rfcacheSourceDeviceWrites": 0, + "spareCapacityInKb": 1640951808, + "enterProtectedMaintenanceModeWriteBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "rfcacheIoErrors": 0, + "normRebuildWriteBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "inaccessibleCapacityInKb": 0, + "capacityInUseInKb": 53217280, + "rebalanceReadBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "rfcacheReadsSkippedMaxIoSize": 0, + "activeMovingInExitProtectedMaintenanceModeJobs": 0, + "secondaryReadFromDevBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "secondaryReadBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "rfcacheWritesSkippedStuckIo": 0, + "secondaryReadFromRmcacheBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "inMaintenanceCapacityInKb": 0, + "exposedCapacityInKb": 0, + "netFglCompressedDataSizeInKb": 0, + "userDataSdcWriteLatency": { + "numSeconds": 5, + "totalWeightInKb": 17356, + "numOccured": 10 + }, + "inUseVacInKb": 369098752, + "fwdRebuildCapacityInKb": 0, + "thickCapacityInUseInKb": 0, + "backgroundScanReadErrorCount": 0, + "activeMovingInRebalanceJobs": 0, + "migratingVolumeIds": [], + "rfcacheWritesSkippedLowResources": 0, + "capacityInUseNoOverheadInKb": 53217280, + "exitProtectedMaintenanceModeWriteBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "rfcacheSkippedUnlinedWrite": 0, + "netCapacityInUseInKb": 26608640, + "numOfOutgoingMigrations": 0, + "rfcacheAvgWriteTime": 0, + "pendingNormRebuildCapacityInKb": 0, + "pendingMovingOutNormrebuildJobs": 0, + "rfcacheSourceDeviceReads": 0, + "rfcacheReadsPending": 0, + "volumeAllocationLimitInKb": 15745417216, + "fwdRebuildWriteBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "rfcacheReadsSkippedHeavyLoad": 0, + "rfcacheReadMiss": 0, + "targetReadLatency": { + "numSeconds": 5, + "totalWeightInKb": 8488, + "numOccured": 18 + }, + "userDataCapacityInKb": 53217280, + "activeMovingInBckRebuildJobs": 0, + "movingCapacityInKb": 0, + "activeEnterProtectedMaintenanceModeCapacityInKb": 0, + "backgroundScanCompareErrorCount": 0, + "pendingMovingInFwdRebuildJobs": 0, + "rfcacheReadsReceived": 0, + "spSdsIds": [ + "ebb7772100020000", + "ebb7772200000000", + "ebb6772300010000" + ], + "pendingEnterProtectedMaintenanceModeCapacityInKb": 0, + "vtreeAddresSpaceInKb": 184549376, + "snapCapacityInUseOccupiedInKb": 0, + "activeFwdRebuildCapacityInKb": 0, + "rfcacheReadsSkippedStuckIo": 0, + "activeMovingOutNormRebuildJobs": 0, + "rfcacheWritePending": 0, + "numOfThinBaseVolumes": 12, + "degradedFailedVacInKb": 0, + "userDataTrimBwc": { + "numSeconds": 0, + "totalWeightInKb": 0, + "numOccured": 0 + }, + "numOfIncomingVtreeMigrations": 0 +} \ No newline at end of file diff --git a/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/login_response.json b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/login_response.json new file mode 100644 index 0000000000..597f7c1e4b --- /dev/null +++ b/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/login_response.json @@ -0,0 +1,10 @@ +{ + "scope": "openid profile email", + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI5QTNCYXpXRGRvdEdQcTM3TkQyVHNDSmhiVXYwOXprb2hMWG9tNE94bXVRIn0.eyJleHAiOjE2ODExMTcwNDIsImlhdCI6MTY4MTExNjc0MiwianRpIjoiMGZiMjk4MmUtMjJmZC00MDhjLWI4MmMtYTMwNTEyNDk4NGQ4IiwiaXNzIjoiaHR0cHM6Ly9wZmxleDRlbnY4LnBpZS5sYWIuZW1jLmNvbS9hdXRoL3JlYWxtcy9wb3dlcmZsZXgiLCJhdWQiOlsiUG93ZXJmbGV4U2VydmljZXMiLCJhY2NvdW50Il0sInN1YiI6IjEzMzRhMDAxLWU2MmItNDdhYy1iZGNlLWIyNmVlZThiMjAyZiIsInR5cCI6IkJlYXJlciIsImF6cCI6InBvd2VyZmxleFJlc3QiLCJzZXNzaW9uX3N0YXRlIjoiMzU0MDhiNTQtNWE0ZS00ODNlLTkwYTUtZDA2M2ZjMDFkY2JlIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyIqIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJTdXBlclVzZXIiXX0sInJlc291cmNlX2FjY2VzcyI6eyJQb3dlcmZsZXhTZXJ2aWNlcyI6eyJyb2xlcyI6WyJzdGFuZGFyZCIsIlJlYWRPbmx5IiwiQWRtaW5pc3RyYXRvciIsIm9wZXJhdG9yIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwic2lkIjoiMzU0MDhiNTQtNWE0ZS00ODNlLTkwYTUtZDA2M2ZjMDFkY2JlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInBvd2VyZmxleCI6eyJwZXJtaXNzaW9ucyI6eyJTdXBlclVzZXIiOlsiR0xCOkdMQiJdfX0sIm5hbWUiOiJhZG1pbiBhZG1pbiIsInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIiwiZ2l2ZW5fbmFtZSI6ImFkbWluIiwiZmFtaWx5X25hbWUiOiJhZG1pbiIsImVtYWlsIjoiYWRtaW5AZXhhbXBsZS5jb20ifQ.D78oxRxnf6hE238Wd9rVlm7L7ZpA_qqsHH_igqyA_ELtX-I3k0VMvOAKdpTOci5qEcMQYTgwQQ09ADUApw12wOxhgU_WCbSGdq07Emqfnb9Yw2vD1m6_sNNMrHOfgWXlpjZq6tS7ew7MGlnymzZXuUMRdPoI4QYZ8XDyIaqprHmJ3P1W4am9PAOWcciRMgwJo9t0LhJl2yP8fQKVgRXxnTAUVja1TYk_U8huKv9oqQR3dYLVJrGuBv8-YOvnS_RXNhUcZQUf0AGJzEG9Vjfk8MpuhuvAqjbiTQYei5rxosfxje3eVCEifEezxkZzdr_BFs1XQ-Df_Ll6m_psoxL7bA", + "expires_in": 300, + "refresh_expires_in": 1800, + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjZmU2NGYxNi05ZGRmLTQyYmUtYmVkMi04ZjMyZWNjM2RkYzAifQ.eyJleHAiOjE2ODExMTg1NDIsImlhdCI6MTY4MTExNjc0MiwianRpIjoiNzMxNGViZDgtNWU4Yy00N2MxLTg5OGMtYjkyZTFhYTg0ZGZlIiwiaXNzIjoiaHR0cHM6Ly9wZmxleDRlbnY4LnBpZS5sYWIuZW1jLmNvbS9hdXRoL3JlYWxtcy9wb3dlcmZsZXgiLCJhdWQiOiJodHRwczovL3BmbGV4NGVudjgucGllLmxhYi5lbWMuY29tL2F1dGgvcmVhbG1zL3Bvd2VyZmxleCIsInN1YiI6IjEzMzRhMDAxLWU2MmItNDdhYy1iZGNlLWIyNmVlZThiMjAyZiIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJwb3dlcmZsZXhSZXN0Iiwic2Vzc2lvbl9zdGF0ZSI6IjM1NDA4YjU0LTVhNGUtNDgzZS05MGE1LWQwNjNmYzAxZGNiZSIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJzaWQiOiIzNTQwOGI1NC01YTRlLTQ4M2UtOTBhNS1kMDYzZmMwMWRjYmUifQ.12EA6mujHEmsC49adECuqWrqhsfCnQHv5aGo_hipSsw", + "token_type": "Bearer", + "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI5QTNCYXpXRGRvdEdQcTM3TkQyVHNDSmhiVXYwOXprb2hMWG9tNE94bXVRIn0.eyJleHAiOjE2ODExMTcwNDIsImlhdCI6MTY4MTExNjc0MiwiYXV0aF90aW1lIjowLCJqdGkiOiI5YTg0ZDM4OC1iNTc5LTQ2ZGEtYjBkNC1mZjdlYzQ1MzA0MmMiLCJpc3MiOiJodHRwczovL3BmbGV4NGVudjgucGllLmxhYi5lbWMuY29tL2F1dGgvcmVhbG1zL3Bvd2VyZmxleCIsImF1ZCI6WyJwb3dlcmZsZXhSZXN0IiwiUG93ZXJmbGV4U2VydmljZXMiXSwic3ViIjoiMTMzNGEwMDEtZTYyYi00N2FjLWJkY2UtYjI2ZWVlOGIyMDJmIiwidHlwIjoiSUQiLCJhenAiOiJwb3dlcmZsZXhSZXN0Iiwic2Vzc2lvbl9zdGF0ZSI6IjM1NDA4YjU0LTVhNGUtNDgzZS05MGE1LWQwNjNmYzAxZGNiZSIsImF0X2hhc2giOiJielhNLVVrN0dHNl9lQlpVNTVXUVd3IiwiYWNyIjoiMSIsInNpZCI6IjM1NDA4YjU0LTVhNGUtNDgzZS05MGE1LWQwNjNmYzAxZGNiZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJwb3dlcmZsZXgiOnsicGVybWlzc2lvbnMiOnsiU3VwZXJVc2VyIjpbIkdMQjpHTEIiXX19LCJuYW1lIjoiYWRtaW4gYWRtaW4iLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsImdpdmVuX25hbWUiOiJhZG1pbiIsImZhbWlseV9uYW1lIjoiYWRtaW4iLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIn0.MVOfN10vq7VD75HMV4N2SYiGpVtnGRpXGFu3WLFPBrQjZrwFkKFb6gmtijw0Onz3xBcg7Eq7asd8lKcBaQ03LY_ru0DXpoStAlCd8z1Vfs2J5boYwn41QHrzwLn0VJK4w6zyHWbRXpK33gTNKjyX0L_JM_o2ZaCJZX8Hxvhb96-LAanbOBtwl1KR-umBHWh6FQOt43YRXAwQSo4Qz425taTmrb2U-LUu1hVZz8GjUmi2dakor6tRgT1ysxM7-9lsNXrFpgZk0XynKpxPg3yDxCdSEkIyoCGB8RH617kN4P1sGicWIk_swDZekwR23LNUiG9tjedaHTriuNAkQZ5a3w", + "session_state": "35408b54-5a4e-483e-90a5-d063fc01dcbe" +} \ No newline at end of file diff --git a/manila/tests/share/drivers/dell_emc/plugins/powerflex/test_connection.py b/manila/tests/share/drivers/dell_emc/plugins/powerflex/test_connection.py new file mode 100644 index 0000000000..244aaeeed7 --- /dev/null +++ b/manila/tests/share/drivers/dell_emc/plugins/powerflex/test_connection.py @@ -0,0 +1,558 @@ +# Copyright (c) 2023 EMC Corporation. +# 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. + +from unittest import mock + +import ddt +from oslo_log import log +from oslo_utils import units + +from manila.common import constants as const +from manila import exception +from manila.share.drivers.dell_emc.plugins.powerflex import connection +from manila import test + +LOG = log.getLogger(__name__) + + +@ddt.ddt +class PowerFlexTest(test.TestCase): + """Integration test for the PowerFlex Manila driver.""" + + POWERFLEX_ADDR = "192.168.0.110" + SHARE_NAME = "Manila-UT-filesystem" + STORAGE_POOL_ID = "28515fee00000000" + FILESYSTEM_ID = "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3" + NFS_EXPORT_ID = "6433a2b2-6d60-f737-9f3b-2a50fb1ccff3" + NFS_EXPORT_NAME = "Manila-UT-filesystem" + SNAPSHOT_NAME = "Manila-UT-filesystem-snap" + SNAPSHOT_PATH = "Manila-UT-filesystem" + SNAPSHOT_ID = "75758d63-2946-4c07-9118-9a6c6027d5e7" + NAS_SERVER_IP = "192.168.11.23" + + class MockConfig(object): + def safe_get(self, value): + if value == "dell_nas_backend_host": + return "192.168.0.110" + elif value == "dell_nas_backend_port": + return "443" + elif value == "dell_nas_login": + return "admin" + elif value == "dell_nas_password": + return "pwd" + elif value == "powerflex_storage_pool": + return "Env8-SP-SW_SSD-1" + elif value == "powerflex_protection_domain": + return "Env8-PD-1" + elif value == "dell_nas_server": + return "env8nasserver" + else: + return None + + @mock.patch( + "manila.share.drivers.dell_emc.plugins.powerflex.object_manager." + "StorageObjectManager", + autospec=True, + ) + def setUp(self, mock_powerflex_manager): + super(PowerFlexTest, self).setUp() + + self._mock_powerflex_manager = mock_powerflex_manager.return_value + self.storage_connection = connection.PowerFlexStorageConnection(LOG) + + self.mock_context = mock.Mock("Context") + self.mock_emc_driver = mock.Mock("EmcDriver") + + self._mock_config = self.MockConfig() + self.mock_emc_driver.attach_mock(self._mock_config, "configuration") + self.storage_connection.connect( + self.mock_emc_driver, self.mock_context + ) + + @mock.patch( + "manila.share.drivers.dell_emc.plugins.powerflex.object_manager." + "StorageObjectManager", + autospec=True, + ) + def test_connect(self, mock_powerflex_manager): + storage_connection = connection.PowerFlexStorageConnection(LOG) + + # execute method under test + storage_connection.connect(self.mock_emc_driver, self.mock_context) + + # verify connect sets driver params appropriately + mock_config = self.MockConfig() + server_addr = mock_config.safe_get("dell_nas_backend_host") + self.assertEqual(server_addr, storage_connection.rest_ip) + expected_port = int(mock_config.safe_get("dell_nas_backend_port")) + self.assertEqual(expected_port, storage_connection.rest_port) + self.assertEqual( + "https://{0}:{1}".format(server_addr, expected_port), + storage_connection.host_url, + ) + expected_username = mock_config.safe_get("dell_nas_login") + self.assertEqual(expected_username, storage_connection.rest_username) + expected_password = mock_config.safe_get("dell_nas_password") + self.assertEqual(expected_password, storage_connection.rest_password) + expected_erify_certificates = mock_config.safe_get( + "dell_ssl_cert_verify" + ) + self.assertEqual( + expected_erify_certificates, storage_connection.verify_certificate + ) + + def test_create_share_nfs(self): + self._mock_powerflex_manager.get_storage_pool_id.return_value = ( + self.STORAGE_POOL_ID + ) + self._mock_powerflex_manager.create_filesystem.return_value = ( + self.FILESYSTEM_ID + ) + self._mock_powerflex_manager.create_nfs_export.return_value = ( + self.NFS_EXPORT_ID + ) + self._mock_powerflex_manager.get_nfs_export_name.return_value = ( + self.NFS_EXPORT_NAME + ) + self._mock_powerflex_manager.get_nas_server_interfaces.return_value = ( + [self.NAS_SERVER_IP] + ) + + self.assertFalse( + self._mock_powerflex_manager.get_storage_pool_id.called + ) + self.assertFalse(self._mock_powerflex_manager.create_filesystem.called) + self.assertFalse(self._mock_powerflex_manager.create_nfs_export.called) + self.assertFalse( + self._mock_powerflex_manager.get_nfs_export_name.called + ) + + # create the share + share = {"name": self.SHARE_NAME, "share_proto": "NFS", "size": 8} + locations = self.storage_connection.create_share( + self.mock_context, share, None + ) + + # verify location and API call made + expected_locations = [{"path": "%s:/%s" % ( + self.NAS_SERVER_IP, + self.SHARE_NAME, + )}] + self.assertEqual(expected_locations, locations) + self._mock_powerflex_manager.get_storage_pool_id.assert_called_with( + self._mock_config.safe_get("powerflex_protection_domain"), + self._mock_config.safe_get("powerflex_storage_pool"), + ) + self._mock_powerflex_manager.create_filesystem.assert_called_with( + self.STORAGE_POOL_ID, + self._mock_config.safe_get("dell_nas_server"), + self.SHARE_NAME, + 8 * units.Gi, + ) + self._mock_powerflex_manager.create_nfs_export.assert_called_with( + self.FILESYSTEM_ID, self.SHARE_NAME + ) + self._mock_powerflex_manager.get_nfs_export_name.assert_called_with( + self.NFS_EXPORT_ID + ) + + def test_create_share_nfs_filesystem_id_not_found(self): + share = {"name": self.SHARE_NAME, "share_proto": "NFS", "size": 8} + self._mock_powerflex_manager.create_filesystem.return_value = None + + self.assertRaises( + exception.ShareBackendException, + self.storage_connection.create_share, + self.mock_context, + share, + share_server=None, + ) + + def test_create_share_nfs_backend_failure(self): + share = {"name": self.SHARE_NAME, "share_proto": "NFS", "size": 8} + self._mock_powerflex_manager.create_nfs_export.return_value = False + + self.assertRaises( + exception.ShareBackendException, + self.storage_connection.create_share, + self.mock_context, + share, + share_server=None, + ) + + def test_create_snapshot(self): + self._mock_powerflex_manager.get_fsid_from_export_name.return_value = ( + self.FILESYSTEM_ID + ) + self._mock_powerflex_manager.create_snapshot.return_value = True + + snapshot = { + "name": self.SNAPSHOT_NAME, + "share_name": self.SNAPSHOT_PATH, + "id": self.SNAPSHOT_ID, + } + self.storage_connection.create_snapshot( + self.mock_context, snapshot, None + ) + + # verify the create snapshot API call is executed + self._mock_powerflex_manager.get_fsid_from_export_name. \ + assert_called_with( + self.SNAPSHOT_PATH + ) + self._mock_powerflex_manager.create_snapshot.assert_called_with( + self.SNAPSHOT_NAME, self.FILESYSTEM_ID + ) + + def test_create_snapshot_failure(self): + self._mock_powerflex_manager.get_fsid_from_export_name.return_value = ( + self.FILESYSTEM_ID + ) + self._mock_powerflex_manager.create_snapshot.return_value = False + + snapshot = { + "name": self.SNAPSHOT_NAME, + "share_name": self.SNAPSHOT_PATH, + "id": self.SNAPSHOT_ID, + } + self.storage_connection.create_snapshot( + self.mock_context, snapshot, None + ) + + def test_delete_share_nfs(self): + share = {"name": self.SHARE_NAME, "share_proto": "NFS"} + + self._mock_powerflex_manager.get_filesystem_id.return_value = ( + self.FILESYSTEM_ID + ) + + self.assertFalse(self._mock_powerflex_manager.get_filesystem_id.called) + self.assertFalse(self._mock_powerflex_manager.delete_filesystem.called) + + # delete the share + self.storage_connection.delete_share(self.mock_context, share, None) + + # verify share delete + self._mock_powerflex_manager.get_filesystem_id.assert_called_with( + self.SHARE_NAME + ) + self._mock_powerflex_manager.delete_filesystem.assert_called_with( + self.FILESYSTEM_ID + ) + + def test_delete_nfs_share_backend_failure(self): + share = {"name": self.SHARE_NAME, "share_proto": "NFS"} + + self._mock_powerflex_manager.delete_filesystem.return_value = False + self.assertRaises( + exception.ShareBackendException, + self.storage_connection.delete_share, + self.mock_context, + share, + None, + ) + + def test_delete_nfs_share_share_does_not_exist(self): + self._mock_powerflex_manager.get_filesystem_id.return_value = None + share = {"name": self.SHARE_NAME, "share_proto": "NFS"} + + # verify the calling delete on a non-existent share returns and does + # not throw exception + self.storage_connection.delete_share(self.mock_context, share, None) + self.assertTrue(self._mock_powerflex_manager.get_filesystem_id.called) + self.assertFalse(self._mock_powerflex_manager.delete_filesystem.called) + + def test_delete_snapshot(self): + self._mock_powerflex_manager.get_fsid_from_snapshot_name. \ + return_value = ( + self.FILESYSTEM_ID + ) + + self.assertFalse( + self._mock_powerflex_manager.get_fsid_from_snapshot_name.called + ) + self.assertFalse(self._mock_powerflex_manager.delete_filesystem.called) + + # delete the created snapshot + snapshot = { + "name": self.SNAPSHOT_NAME, + "share_name": self.SNAPSHOT_PATH, + "id": self.SNAPSHOT_ID, + } + self.storage_connection.delete_snapshot( + self.mock_context, snapshot, None + ) + + # verify the API call was made to delete the snapshot + self._mock_powerflex_manager.get_fsid_from_snapshot_name. \ + assert_called_with( + self.SNAPSHOT_NAME + ) + self._mock_powerflex_manager.delete_filesystem.assert_called_with( + self.FILESYSTEM_ID + ) + + def test_delete_snapshot_backend_failure(self): + self._mock_powerflex_manager.get_fsid_from_snapshot_name. \ + return_value = ( + self.FILESYSTEM_ID + ) + self._mock_powerflex_manager.delete_filesystem.return_value = False + + self.assertFalse( + self._mock_powerflex_manager.get_fsid_from_snapshot_name.called + ) + self.assertFalse(self._mock_powerflex_manager.delete_filesystem.called) + + snapshot = { + "name": self.SNAPSHOT_NAME, + "share_name": self.SNAPSHOT_PATH, + "id": self.SNAPSHOT_ID, + } + # verify the API call was made to delete the snapshot + self.assertRaises( + exception.ShareBackendException, + self.storage_connection.delete_snapshot, + self.mock_context, + snapshot, + None, + ) + self._mock_powerflex_manager.get_fsid_from_snapshot_name. \ + assert_called_with( + self.SNAPSHOT_NAME + ) + self._mock_powerflex_manager.delete_filesystem.assert_called_with( + self.FILESYSTEM_ID + ) + + def test_extend_share(self): + new_share_size = 20 + share = { + "name": self.SHARE_NAME, + "share_proto": "NFS", + "size": new_share_size, + } + self._mock_powerflex_manager.get_filesystem_id.return_value = ( + self.FILESYSTEM_ID + ) + self.assertFalse(self._mock_powerflex_manager.get_filesystem_id.called) + + self.storage_connection.extend_share(share, new_share_size) + + self._mock_powerflex_manager.get_filesystem_id.assert_called_with( + self.SHARE_NAME + ) + expected_quota_size = new_share_size * units.Gi + self._mock_powerflex_manager.extend_export.assert_called_once_with( + self.FILESYSTEM_ID, expected_quota_size + ) + + def test_update_access_add_nfs(self): + share = {"name": self.SHARE_NAME, "share_proto": "NFS"} + + self._mock_powerflex_manager.get_nfs_export_id.return_value = ( + self.NFS_EXPORT_ID + ) + self._mock_powerflex_manager.set_export_access.return_value = True + + self.assertFalse(self._mock_powerflex_manager.get_nfs_export_id.called) + self.assertFalse(self._mock_powerflex_manager.set_export_access.called) + + nfs_rw_ip = "192.168.0.10" + nfs_ro_ip = "192.168.0.11" + nfs_access_rw = { + "access_type": "ip", + "access_to": nfs_rw_ip, + "access_level": const.ACCESS_LEVEL_RW, + "access_id": "09960614-8574-4e03-89cf-7cf267b0bd08", + } + nfs_access_ro = { + "access_type": "ip", + "access_to": nfs_ro_ip, + "access_level": const.ACCESS_LEVEL_RO, + "access_id": "09960614-8574-4e03-89cf-7cf267b0bd08", + } + access_rules = [nfs_access_rw, nfs_access_ro] + + self.storage_connection.update_access( + self.mock_context, + share, + access_rules, + add_rules=None, + delete_rules=None, + share_server=None, + ) + + self._mock_powerflex_manager.get_nfs_export_id.assert_called_once_with( + self.SHARE_NAME + ) + self._mock_powerflex_manager.set_export_access.assert_called_once_with( + self.NFS_EXPORT_ID, {nfs_rw_ip}, {nfs_ro_ip} + ) + + def test_update_access_add_nfs_invalid_acess_type(self): + share = { + "name": self.SHARE_NAME, + "share_proto": "NFS", + "display_name": "foo_display_name", + } + + nfs_rw_ip = "192.168.0.10" + nfs_ro_ip = "192.168.0.11" + nfs_access_rw = { + "access_type": "invalid_type", + "access_to": nfs_rw_ip, + "access_level": const.ACCESS_LEVEL_RW, + "access_id": "09960614-8574-4e03-89cf-7cf267b0bd08", + } + nfs_access_ro = { + "access_type": "invalid_type", + "access_to": nfs_ro_ip, + "access_level": const.ACCESS_LEVEL_RO, + "access_id": "09960614-8574-4e03-89cf-7cf267b0bd09", + } + access_rules = [nfs_access_rw, nfs_access_ro] + + self._mock_powerflex_manager.get_nfs_export_id.return_value = ( + self.NFS_EXPORT_ID + ) + + access_updates = self.storage_connection.update_access( + self.mock_context, + share, + access_rules, + add_rules=None, + delete_rules=None, + share_server=None, + ) + + self._mock_powerflex_manager.set_export_access.assert_called_once_with( + self.NFS_EXPORT_ID, set(), set() + ) + + self.assertIsNotNone(access_updates) + + def test_update_access_add_nfs_backend_failure(self): + share = { + "name": self.SHARE_NAME, + "share_proto": "NFS", + "display_name": "foo_display_name", + } + + self._mock_powerflex_manager.get_nfs_export_id.return_value = ( + self.NFS_EXPORT_ID + ) + self._mock_powerflex_manager.set_export_access.return_value = False + + self.assertFalse(self._mock_powerflex_manager.get_nfs_export_id.called) + self.assertFalse(self._mock_powerflex_manager.set_export_access.called) + + nfs_rw_ip = "192.168.0.10" + nfs_ro_ip = "192.168.0.11" + nfs_access_rw = { + "access_type": "ip", + "access_to": nfs_rw_ip, + "access_level": const.ACCESS_LEVEL_RW, + "access_id": "09960614-8574-4e03-89cf-7cf267b0bd08", + } + nfs_access_ro = { + "access_type": "ip", + "access_to": nfs_ro_ip, + "access_level": const.ACCESS_LEVEL_RO, + "access_id": "09960614-8574-4e03-89cf-7cf267b0bd08", + } + access_rules = [nfs_access_rw, nfs_access_ro] + + self.assertRaises( + exception.ShareBackendException, + self.storage_connection.update_access, + self.mock_context, + share, + access_rules, + add_rules=None, + delete_rules=None, + share_server=None, + ) + + def test_update_share_stats(self): + data = dict( + share_backend_name='powerflex', + vendor_name='Dell EMC', + storage_protocol='NFS_CIFS', + snapshot_support=True, + create_share_from_snapshot_support=True) + stats = dict( + maxCapacityInKb=4826330112, + capacityInUseInKb=53217280, + netUnusedCapacityInKb=1566080512, + primaryVacInKb=184549376) + + self._mock_powerflex_manager.get_storage_pool_id.return_value = ( + self.STORAGE_POOL_ID + ) + self._mock_powerflex_manager.get_storage_pool_statistic. \ + return_value = stats + self.storage_connection.update_share_stats(data) + self.assertEqual(data['storage_protocol'], 'NFS') + self.assertEqual(data['create_share_from_snapshot_support'], False) + self.assertEqual(data['driver_version'], connection.VERSION) + self.assertIsNotNone(data['pools']) + + def test_get_default_filter_function(self): + filter = self.storage_connection.get_default_filter_function() + self.assertEqual(filter, "share.size >= 3") + + def test_create_share_from_snapshot(self): + self.assertRaises( + NotImplementedError, + self.storage_connection.create_share_from_snapshot, + self.mock_context, + share=None, + snapshot=None, + ) + + def test_allow_access(self): + self.assertRaises( + NotImplementedError, + self.storage_connection.allow_access, + self.mock_context, + share=None, + access=None, + share_server=None, + ) + + def test_deny_access(self): + self.assertRaises( + NotImplementedError, + self.storage_connection.deny_access, + self.mock_context, + share=None, + access=None, + share_server=None, + ) + + def test_setup_server(self): + self.assertRaises( + NotImplementedError, + self.storage_connection.setup_server, + network_info=None, + ) + + def test_teardown_server(self): + self.assertRaises( + NotImplementedError, + self.storage_connection.teardown_server, + server_details=None, + ) diff --git a/manila/tests/share/drivers/dell_emc/plugins/powerflex/test_object_manager.py b/manila/tests/share/drivers/dell_emc/plugins/powerflex/test_object_manager.py new file mode 100644 index 0000000000..e1f5fe6712 --- /dev/null +++ b/manila/tests/share/drivers/dell_emc/plugins/powerflex/test_object_manager.py @@ -0,0 +1,458 @@ +# Copyright (c) 2023 Dell Inc. or its subsidiaries. +# 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. + +from http import client as http_client +import json +import pathlib + +import ddt +import requests_mock + +from manila import exception +from manila.share.drivers.dell_emc.plugins.powerflex import ( + object_manager as manager +) +from manila import test + + +@ddt.ddt +class StorageObjectManagerTestCase(test.TestCase): + def setUp(self): + super(StorageObjectManagerTestCase, self).setUp() + + self._mock_url = "https://192.168.0.110:443" + self.manager = manager.StorageObjectManager( + self._mock_url, username="admin", password="pwd", export_path=None + ) + self.mockup_file_base = ( + str(pathlib.Path.cwd()) + + "/manila/tests/share/drivers/dell_emc/plugins/powerflex/mockup/" + ) + + @ddt.data(False, True) + def test__get_headers(self, got_token): + self.manager.got_token = got_token + self.manager.rest_token = "token_str" + self.assertEqual( + self.manager._get_headers().get("Authorization") is not None, + got_token, + ) + + def _getJsonFile(self, filename): + f = open(self.mockup_file_base + filename) + data = json.load(f) + f.close() + return data + + @requests_mock.mock() + def test_get_nas_server_id(self, m): + nas_server = "env8nasserver" + self.assertEqual(0, len(m.request_history)) + self._add_get_nas_server_id_response( + m, nas_server, self._getJsonFile("get_nas_server_id_response.json") + ) + id = self.manager.get_nas_server_id(nas_server) + self.assertEqual(id, "64132f37-d33e-9d4a-89ba-d625520a4779") + + def _add_get_nas_server_id_response(self, m, nas_server, json_str): + url = "{0}/rest/v1/nas-servers?select=id&name=eq.{1}".format( + self._mock_url, nas_server + ) + m.get(url, status_code=200, json=json_str) + + @requests_mock.mock() + def test_create_filesystem(self, m): + nas_server = "env8nasserver" + self.assertEqual(0, len(m.request_history)) + self._add_get_nas_server_id_response( + m, nas_server, self._getJsonFile("get_nas_server_id_response.json") + ) + storage_pool_id = "8515fee00000000" + self._add_create_filesystem_response( + m, self._getJsonFile("create_filesystem_response.json") + ) + id = self.manager.create_filesystem( + storage_pool_id, + nas_server, + name="Manila-filesystem", + size=3221225472, + ) + self.assertEqual(id, "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3") + + def _add_create_filesystem_response(self, m, json_str): + url = "{0}/rest/v1/file-systems".format(self._mock_url) + m.post(url, status_code=201, json=json_str) + + @requests_mock.mock() + def test_create_nfs_export(self, m): + filesystem_id = "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3" + name = "Manila-UT-filesystem" + self.assertEqual(0, len(m.request_history)) + self._add_create_nfs_export_response( + m, self._getJsonFile("create_nfs_export_response.json") + ) + id = self.manager.create_nfs_export(filesystem_id, name) + self.assertEqual(id, "6433a2b2-6d60-f737-9f3b-2a50fb1ccff3") + + def _add_create_nfs_export_response(self, m, json_str): + url = "{0}/rest/v1/nfs-exports".format(self._mock_url) + m.post(url, status_code=201, json=json_str) + + @requests_mock.mock() + def test_delete_filesystem(self, m): + filesystem_id = "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3" + self.assertEqual(0, len(m.request_history)) + self._add_delete_filesystem_response(m, filesystem_id) + result = self.manager.delete_filesystem(filesystem_id) + self.assertEqual(result, True) + + def _add_delete_filesystem_response(self, m, filesystem_id): + url = "{0}/rest/v1/file-systems/{1}".format( + self._mock_url, filesystem_id + ) + m.delete(url, status_code=204) + + @requests_mock.mock() + def test_create_snapshot(self, m): + name = "Manila-UT-filesystem-snap" + filesystem_id = "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3" + self.assertEqual(0, len(m.request_history)) + self._add_create_snapshot_response( + m, + filesystem_id, + self._getJsonFile("create_nfs_snapshot_response.json"), + ) + result = self.manager.create_snapshot(name, filesystem_id) + self.assertEqual(result, True) + + def _add_create_snapshot_response(self, m, filesystem_id, json_str): + url = "{0}/rest/v1/file-systems/{1}/snapshot".format( + self._mock_url, filesystem_id + ) + m.post(url, status_code=201, json=json_str) + + @requests_mock.mock() + def test_get_nfs_export_name(self, m): + export_id = "6433a2b2-6d60-f737-9f3b-2a50fb1ccff3" + self.assertEqual(0, len(m.request_history)) + self._add_get_nfs_export_name_response( + m, + export_id, + self._getJsonFile("get_nfs_export_name_response.json"), + ) + name = self.manager.get_nfs_export_name(export_id) + self.assertEqual(name, "Manila-UT-filesystem") + + def _add_get_nfs_export_name_response(self, m, export_id, json_str): + url = "{0}/rest/v1/nfs-exports/{1}?select=*".format( + self._mock_url, export_id + ) + m.get(url, status_code=200, json=json_str) + + @requests_mock.mock() + def test_get_filesystem_id(self, m): + name = "Manila-UT-filesystem" + self.assertEqual(0, len(m.request_history)) + self._add_get_filesystem_id_response( + m, name, self._getJsonFile("get_fileystem_id_response.json") + ) + id = self.manager.get_filesystem_id(name) + self.assertEqual(id, "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3") + + def _add_get_filesystem_id_response(self, m, name, json_str): + url = "{0}/rest/v1/file-systems?select=id&name=eq.{1}".format( + self._mock_url, name + ) + m.get(url, status_code=200, json=json_str) + + @requests_mock.mock() + def test_get_nfs_export_id(self, m): + name = "Manila-UT-filesystem" + self.assertEqual(0, len(m.request_history)) + self._add_get_nfs_export_id_response( + m, name, self._getJsonFile("get_nfs_export_id_response.json") + ) + id = self.manager.get_nfs_export_id(name) + self.assertEqual(id, "6433a2b2-6d60-f737-9f3b-2a50fb1ccff3") + + def _add_get_nfs_export_id_response(self, m, name, json_str): + url = "{0}/rest/v1/nfs-exports?select=id&name=eq.{1}".format( + self._mock_url, name + ) + m.get(url, status_code=200, json=json_str) + + @requests_mock.mock() + def test_get_storage_pool_id(self, m): + protection_domain_name = "Env8-PD-1" + storage_pool_name = "Env8-SP-SW_SSD-1" + self.assertEqual(0, len(m.request_history)) + self._add_get_storage_pool_id_response( + m, self._getJsonFile("get_storage_pool_id_response.json") + ) + id = self.manager.get_storage_pool_id( + protection_domain_name, storage_pool_name + ) + self.assertEqual(id, "28515fee00000000") + + def _add_get_storage_pool_id_response(self, m, json_str): + url = "{0}/api/types/StoragePool/instances/action/queryIdByKey".format( + self._mock_url + ) + m.post(url, status_code=200, json=json_str) + + @requests_mock.mock() + def test_set_export_access(self, m): + export_id = "6433a2b2-6d60-f737-9f3b-2a50fb1ccff3" + rw_hosts = "192.168.1.110" + ro_hosts = "192.168.1.111" + self.assertEqual(0, len(m.request_history)) + self._add_set_export_access_response(m, export_id) + result = self.manager.set_export_access(export_id, rw_hosts, ro_hosts) + self.assertEqual(result, True) + + def _add_set_export_access_response(self, m, export_id): + url = "{0}/rest/v1/nfs-exports/{1}".format(self._mock_url, export_id) + m.patch(url, status_code=204) + + @requests_mock.mock() + def test_extend_export(self, m): + filesystem_id = "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3" + new_size = 6441225472 + self.assertEqual(0, len(m.request_history)) + self._add_extend_export_response(m, filesystem_id) + result = self.manager.extend_export(filesystem_id, new_size) + self.assertEqual(result, True) + + def _add_extend_export_response(self, m, filesystem_id): + url = "{0}/rest/v1/file-systems/{1}".format( + self._mock_url, filesystem_id + ) + m.patch(url, status_code=204) + + @requests_mock.mock() + def test_get_fsid_from_export_name(self, m): + name = "Manila-UT-filesystem" + self.assertEqual(0, len(m.request_history)) + self._add_get_fsid_from_export_name_response( + m, + name, + self._getJsonFile("get_fsid_from_export_name_response.json"), + ) + id = self.manager.get_fsid_from_export_name(name) + self.assertEqual(id, "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3") + + def _add_get_fsid_from_export_name_response(self, m, name, json_str): + url = ( + "{0}/rest/v1/nfs-exports?select=file_system_id&name=eq.{1}".format( + self._mock_url, name + ) + ) + m.get(url, status_code=200, json=json_str) + + @requests_mock.mock() + def test_get_fsid_from_snapshot_name(self, m): + snapshot_name = "Manila-UT-filesystem-snap" + self.assertEqual(0, len(m.request_history)) + self._add_get_fsid_from_snapshot_name_response( + m, + snapshot_name, + self._getJsonFile("get_fsid_from_snapshot_name_response.json"), + ) + id = self.manager.get_fsid_from_snapshot_name(snapshot_name) + self.assertEqual(id, "6433b635-6c1f-878e-6467-2a50fb1ccff3") + + def _add_get_fsid_from_snapshot_name_response( + self, m, snapshot_name, json_str + ): + url = "{0}/rest/v1/file-systems?select=id&name=eq.{1}".format( + self._mock_url, snapshot_name + ) + m.get(url, status_code=200, json=json_str) + + @requests_mock.mock() + def test_check_response_with_login_get(self, m): + nas_server = "env8nasserver" + self.assertEqual(0, len(m.request_history)) + self._add_get_nas_server_id_response_list(m, nas_server) + self._add_login_success_response(m) + id = self.manager.get_nas_server_id(nas_server) + self.assertEqual(id, "64132f37-d33e-9d4a-89ba-d625520a4779") + + def _add_get_nas_server_id_response_list(self, m, nas_server): + url = "{0}/rest/v1/nas-servers?select=id&name=eq.{1}".format( + self._mock_url, nas_server + ) + m.get( + url, + [ + {"status_code": http_client.UNAUTHORIZED}, + { + "status_code": 200, + "json": self._getJsonFile( + "get_nas_server_id_response.json" + ), + }, + ], + ) + + def _add_login_success_response(self, m): + url = "{0}/rest/auth/login".format(self._mock_url) + m.post( + url, status_code=200, json=self._getJsonFile("login_response.json") + ) + + @requests_mock.mock() + def test_check_response_with_login_post(self, m): + filesystem_id = "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3" + name = "Manila-UT-filesystem" + self.assertEqual(0, len(m.request_history)) + self._add_create_nfs_export_response_list(m) + self._add_login_success_response(m) + id = self.manager.create_nfs_export(filesystem_id, name) + self.assertEqual(id, "6433a2b2-6d60-f737-9f3b-2a50fb1ccff3") + + def _add_create_nfs_export_response_list(self, m): + url = "{0}/rest/v1/nfs-exports".format(self._mock_url) + m.post( + url, + [ + {"status_code": http_client.UNAUTHORIZED}, + { + "status_code": 201, + "json": self._getJsonFile( + "create_nfs_export_response.json" + ), + }, + ], + ) + + @requests_mock.mock() + def test_check_response_with_login_delete(self, m): + filesystem_id = "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3" + self.assertEqual(0, len(m.request_history)) + self._add_delete_filesystem_response_list(m, filesystem_id) + self._add_login_success_response(m) + result = self.manager.delete_filesystem(filesystem_id) + self.assertEqual(result, True) + + def _add_delete_filesystem_response_list(self, m, filesystem_id): + url = "{0}/rest/v1/file-systems/{1}".format( + self._mock_url, filesystem_id + ) + m.delete( + url, + [{"status_code": http_client.UNAUTHORIZED}, {"status_code": 204}], + ) + + @requests_mock.mock() + def test_check_response_with_login_patch(self, m): + filesystem_id = "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3" + new_size = 6441225472 + self.assertEqual(0, len(m.request_history)) + self._add_extend_export_response_list(m, filesystem_id) + self._add_login_success_response(m) + result = self.manager.extend_export(filesystem_id, new_size) + self.assertEqual(result, True) + + def _add_extend_export_response_list(self, m, filesystem_id): + url = "{0}/rest/v1/file-systems/{1}".format( + self._mock_url, filesystem_id + ) + m.patch( + url, + [{"status_code": http_client.UNAUTHORIZED}, {"status_code": 204}], + ) + + @requests_mock.mock() + def test_check_response_with_invalid_credential(self, m): + nas_server = "env8nasserver" + self.assertEqual(0, len(m.request_history)) + self._add_get_nas_server_id_unauthorized_response(m, nas_server) + self._add_login_fail_response(m) + self.assertRaises( + exception.NotAuthorized, self.manager.get_nas_server_id, nas_server + ) + + def _add_get_nas_server_id_unauthorized_response(self, m, nas_server): + url = "{0}/rest/v1/nas-servers?select=id&name=eq.{1}".format( + self._mock_url, nas_server + ) + m.get(url, status_code=http_client.UNAUTHORIZED) + + def _add_login_fail_response(self, m): + url = "{0}/rest/auth/login".format(self._mock_url) + m.post(url, status_code=http_client.UNAUTHORIZED) + + @requests_mock.mock() + def test_execute_powerflex_post_request_with_no_param(self, m): + url = self._mock_url + "/fake_url" + self.assertEqual(0, len(m.request_history)) + m.post(url, status_code=201) + res, response = self.manager.execute_powerflex_post_request(url) + self.assertEqual(res.status_code, 201) + + @requests_mock.mock() + def test_execute_powerflex_patch_request_with_no_param(self, m): + url = self._mock_url + "/fake_url" + self.assertEqual(0, len(m.request_history)) + m.patch(url, status_code=204) + res = self.manager.execute_powerflex_patch_request(url) + self.assertEqual(res.status_code, 204) + + @requests_mock.mock() + def test_get_storage_pool_spare_percentage(self, m): + storage_pool_id = "28515fee00000000" + self.assertEqual(0, len(m.request_history)) + self._add_get_storage_pool_spare_percentage( + m, + storage_pool_id, + self._getJsonFile("get_storage_pool_spare_percentage.json"), + ) + spare = self.manager.get_storage_pool_spare_percentage(storage_pool_id) + self.assertEqual(spare, 34) + + def _add_get_storage_pool_spare_percentage(self, m, storage_pool_id, + json_str): + url = ( + "{0}/api/instances/StoragePool::{1}".format( + self._mock_url, storage_pool_id + ) + ) + m.get(url, status_code=200, json=json_str) + + @requests_mock.mock() + def test_get_storage_pool_statistic(self, m): + storage_pool_id = "28515fee00000000" + self.assertEqual(0, len(m.request_history)) + self._add_get_storage_pool_statistic( + m, + storage_pool_id, + self._getJsonFile("get_storage_pool_statistic.json"), + ) + statistic = self.manager.get_storage_pool_statistic(storage_pool_id) + self.assertEqual(statistic['maxCapacityInKb'], 4826330112) + self.assertEqual(statistic['capacityInUseInKb'], 53217280) + self.assertEqual(statistic['netUnusedCapacityInKb'], 1566080512) + self.assertEqual(statistic['primaryVacInKb'], 184549376) + + def _add_get_storage_pool_statistic(self, m, storage_pool_id, + json_str): + url = ( + ("{0}/api/instances/StoragePool::{1}/relationships/" + + "Statistics").format( + self._mock_url, storage_pool_id + ) + ) + m.get(url, status_code=200, json=json_str) diff --git a/releasenotes/notes/bp-dell-powerflex-manila-driver-2c496483242e555a.yaml b/releasenotes/notes/bp-dell-powerflex-manila-driver-2c496483242e555a.yaml new file mode 100644 index 0000000000..d39e47cd4f --- /dev/null +++ b/releasenotes/notes/bp-dell-powerflex-manila-driver-2c496483242e555a.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added a new Manila driver to support Dell PowerFlex storage backend. + It supports the minimum set of Manila features. diff --git a/setup.cfg b/setup.cfg index 52e802c115..d5d00f5409 100644 --- a/setup.cfg +++ b/setup.cfg @@ -84,6 +84,7 @@ manila.share.drivers.dell_emc.plugins = isilon = manila.share.drivers.dell_emc.plugins.isilon.isilon:IsilonStorageConnection powermax = manila.share.drivers.dell_emc.plugins.powermax.connection:PowerMaxStorageConnection powerstore = manila.share.drivers.dell_emc.plugins.powerstore.connection:PowerStoreStorageConnection + powerflex = manila.share.drivers.dell_emc.plugins.powerflex.connection:PowerFlexStorageConnection manila.tests.scheduler.fakes = FakeWeigher1 = manila.tests.scheduler.fakes:FakeWeigher1 FakeWeigher2 = manila.tests.scheduler.fakes:FakeWeigher2