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
This commit is contained in:
Yian Zong 2023-04-20 07:23:43 +00:00
parent 16af92c694
commit ac8a9a2380
26 changed files with 2539 additions and 1 deletions

View File

@ -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

View File

@ -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 | \- | \- |

View File

@ -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

View File

@ -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 = <Management IP of the PowerFlex system>
dell_nas_backend_port = <Port number used for secured connection>
dell_nas_server = <Name of the NAS server within the PowerFlex system>
dell_nas_login = <user with administrator privilege>
dell_nas_password = <password>
powerflex_storage_pool = <Name of the storage pool>
powerflex_protection_domain = <Name of the protection domain>
share_backend_name = powerflex
dell_ssl_cert_verify = <True|False>
dell_ssl_certificate_path = <Path to SSL certificates>
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

View File

@ -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."""

View File

@ -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'

View File

@ -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]

View File

@ -0,0 +1,3 @@
{
"id": "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3"
}

View File

@ -0,0 +1,3 @@
{
"id": "6433a2b2-6d60-f737-9f3b-2a50fb1ccff3"
}

View File

@ -0,0 +1,3 @@
{
"id": "6433b635-6c1f-878e-6467-2a50fb1ccff3"
}

View File

@ -0,0 +1,5 @@
[
{
"id": "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3"
}
]

View File

@ -0,0 +1,5 @@
[
{
"file_system_id": "6432b79e-1cc3-0414-3ffd-2a50fb1ccff3"
}
]

View File

@ -0,0 +1,5 @@
[
{
"id": "6433b635-6c1f-878e-6467-2a50fb1ccff3"
}
]

View File

@ -0,0 +1,5 @@
[
{
"id": "64132f37-d33e-9d4a-89ba-d625520a4779"
}
]

View File

@ -0,0 +1,5 @@
[
{
"id": "6433a2b2-6d60-f737-9f3b-2a50fb1ccff3"
}
]

View File

@ -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
}

View File

@ -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"
}
]
}

View File

@ -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
}

View File

@ -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"
}

View File

@ -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,
)

View File

@ -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)

View File

@ -0,0 +1,5 @@
---
features:
- |
Added a new Manila driver to support Dell PowerFlex storage backend.
It supports the minimum set of Manila features.

View File

@ -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