Adds a new Manila driver for Dell PowerStore storage backend

Adds a new Manila driver to support Dell PowerStore storage backend.
It will support NFS share operations and snapshot operations.

Implements: blueprint dell-powerstore-manila-driver
Change-Id: If0d0a7820a1ae2392e5e0e4a4b65c4e631f4c3d4
This commit is contained in:
Yian Zong 2023-06-14 08:01:10 +00:00
parent a01cdc7719
commit 09555c80de
31 changed files with 9893 additions and 5 deletions

View File

@ -85,6 +85,7 @@ each back end.
emc_isilon_driver
emc_vnx_driver
../configuration/shared-file-systems/drivers/dell-emc-unity-driver
../configuration/shared-file-systems/drivers/dell-emc-powerstore-driver
generic_driver
glusterfs_driver
glusterfs_native_driver

View File

@ -51,6 +51,8 @@ Mapping of share drivers and share features support
+----------------------------------------+-----------------------+-----------------------+--------------------------+--------------------------+------------------------+-----------------------------------+--------------------------+--------------------+--------------------+
| EMC Isilon | K | \- | M | \- | K | K | \- | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------------------+--------------------------+------------------------+-----------------------------------+--------------------------+--------------------+--------------------+
| Dell EMC PowerStore | B | \- | B | B | B | B | \- | B | \- |
+----------------------------------------+-----------------------+-----------------------+--------------------------+--------------------------+------------------------+-----------------------------------+--------------------------+--------------------+--------------------+
| GlusterFS | J | \- | directory layout (T) | directory layout (T) | volume layout (L) | volume layout (L) | \- | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------------------+--------------------------+------------------------+-----------------------------------+--------------------------+--------------------+--------------------+
| GlusterFS-Native | J | \- | \- | \- | K | L | \- | \- | \- |
@ -124,6 +126,8 @@ Mapping of share drivers and share access rules support
+----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+
| EMC Isilon | NFS,CIFS (K) | \- | CIFS (M) | \- | \- | NFS (M) | \- | CIFS (M) | \- | \- |
+----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+
| Dell EMC PowerStore | NFS (B) | \- | CIFS (B) | \- | \- | NFS (B) | \- | CIFS (B) | \- | \- |
+----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+
| GlusterFS | NFS (J) | \- | \- | \- | \- | \- | \- | \- | \- | \- |
+----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+
| GlusterFS-Native | \- | \- | \- | J | \- | \- | \- | \- | \- | \- |
@ -195,6 +199,8 @@ Mapping of share drivers and security services support
+----------------------------------------+------------------+-----------------+------------------+
| EMC Isilon | \- | \- | \- |
+----------------------------------------+------------------+-----------------+------------------+
| Dell EMC PowerStore | B | \- | \- |
+----------------------------------------+------------------+-----------------+------------------+
| GlusterFS | \- | \- | \- |
+----------------------------------------+------------------+-----------------+------------------+
| GlusterFS-Native | \- | \- | \- |
@ -268,6 +274,8 @@ More information: :ref:`capabilities_and_extra_specs`
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+-------------------------+
| EMC Isilon | \- | K | \- | \- | \- | L | \- | K | \- | \- | P | \- | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+-------------------------+
| Dell EMC PowerStore | \- | B | \- | \- | B | \- | \- | B | B | \- | B | \- | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+-------------------------+
| GlusterFS | \- | J | \- | \- | \- | L | \- | volume layout (L) | \- | \- | P | \- | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+-------------------------+
| GlusterFS-Native | \- | J | \- | \- | \- | L | \- | L | \- | \- | P | \- | \- |

View File

@ -15,6 +15,7 @@ Share drivers
drivers/dell-emc-powermax-driver.rst
drivers/dell-emc-unity-driver.rst
drivers/dell-emc-vnx-driver.rst
drivers/dell-emc-powerstore-driver.rst
drivers/glusterfs-driver.rst
drivers/glusterfs-native-driver.rst
drivers/hdfs-native-driver.rst

View File

@ -0,0 +1,186 @@
===========================
Dell EMC PowerStore Plugin
===========================
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 PowerStore plug-in manages the PowerStore to provide shared file systems.
The Dell EMC driver framework with the PowerStore plug-in is referred to as the
PowerStore driver in this document.
This driver performs the operations on PowerStore through RESTful APIs. Each backend
manages one PowerStore storage system. Configure multiple Shared File Systems service
backends to manage multiple PowerStore systems.
Requirements
------------
- PowerStore version 3.0 or higher.
- PowerStore File is enabled.
Supported shared filesystems and operations
-------------------------------------------
The driver supports NFS shares and CIFS shares.
The following operations are supported.
- Create a share.
- Delete a share.
- Allow share access.
- Deny share access.
- Extend a share.
- Shrink a share.
- Create a snapshot.
- Delete a snapshot.
- Create a share from a snapshot.
- Revert a share to a snapshot.
Driver configuration
--------------------
Edit the configuration file ``/etc/manila/manila.conf``.
* Add a section for the PowerStore 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 = powerstore
dell_nas_backend_host = <Management IP of the PowerStore system>
dell_nas_server = <Name of the NAS server in the PowerStore system>
dell_ad_domain = <Domain name of the active directory joined by the NAS server>
dell_nas_login = <User with administrator privilege>
dell_nas_password = <Password>
share_backend_name = <Backend name>
dell_ssl_cert_verify = True/False
dell_ssl_cert_path = <Path to cert>
Where:
+---------------------------------+----------------------------------------------------+
| **Parameter** | **Description** |
+=================================+====================================================+
| ``share_driver`` | Full path of the EMCShareDriver used to enable |
| | the plugin. |
+---------------------------------+----------------------------------------------------+
| ``emc_share_backend`` | The plugin name. Set it to `powerstore` to |
| | enable the PowerStore driver. |
+---------------------------------+----------------------------------------------------+
| ``dell_nas_backend_host`` | The management IP of the PowerStore system. |
+---------------------------------+----------------------------------------------------+
| ``dell_nas_server`` | The name of the NAS server in the |
| | PowerStore system. |
+---------------------------------+----------------------------------------------------+
| ``dell_ad_domain`` | The name of the Active Directory Domain. |
| | Only applicable when the SMB server joins |
| | to the Active Directory Domain. |
+---------------------------------+----------------------------------------------------+
| ``dell_nas_login`` | The login to use to connect to the PowerStore |
| | system. It must have administrator privileges. |
+---------------------------------+----------------------------------------------------+
| ``dell_nas_password`` | The password associated with the login. |
+---------------------------------+----------------------------------------------------+
| ``share_backend_name`` | The share backend name for a given driver |
| | implementation. |
+---------------------------------+----------------------------------------------------+
| ``dell_ssl_cert_verify`` | The https client validates the SSL certificate of |
| | the PowerStore endpoint. Optional. |
| | Value: True or False. |
| | Default: False. |
+---------------------------------+----------------------------------------------------+
| ``dell_ssl_cert_path`` | The path to PowerStore SSL certificate on |
| | Manila host. Optional. |
+---------------------------------+----------------------------------------------------+
Restart of ``manila-share`` service is needed for the configuration
changes to take effect.
Pre-configurations for share support (DHSS=False)
--------------------------------------------------
To create a file share in this mode, you need to:
#. Create NAS server with network interface in PowerStore system.
#. Set 'dell_nas_server' in ``/etc/manila/manila.conf``:
.. code-block:: ini
dell_nas_server = <name of NAS server in PowerStore system>
#. Create the share type with driver_handles_share_servers = False extra
specification:
.. code-block:: console
$ openstack share type create ${share_type_name} False
#. Map this share type to the share backend name
.. code-block:: console
$ openstack share type set ${share_type_name} \
--extra-specs share_backend_name=${share_backend_name}
#. Create NFS share.
.. code-block:: console
$ openstack share create NFS ${size} --name ${share_name} --share-type ${share_type_name}
Pre-configurations for snapshot support
---------------------------------------
The driver can:
- create/delete a snapshot
- create a share from a snapshot
- revert a share to a snapshot
The following extra specifications need to be configured with share type.
- snapshot_support = True
- create_share_from_snapshot_support = True
- revert_to_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 ${share_type_name} False \
--snapshot-support=True \
--create-share-from-snapshot-support=True \
--revert-to-snapshot-support=True
Or you can update already existing share type with command:
.. code-block:: console
$ openstack share type set ${share_type_name} \
--extra-specs snapshot_support=True \
create_share_from_snapshot_support=True \
revert_to_snapshot_support=True
Known restrictions
------------------
The PowerStore driver has the following restrictions.
- Minimum share size is 3GiB.
- Only IP access type is supported for NFS shares.
- Only user access type is supported for CIFS shares.
- Only DHSS=False is supported.
- Modification of CIFS share access is supported in PowerStore 3.5 and above.

View File

@ -41,7 +41,8 @@ EMC_NAS_OPTS = [
help='Use secure connection to server.'),
cfg.StrOpt('emc_share_backend',
ignore_case=True,
choices=['isilon', 'vnx', 'unity', 'vmax', 'powermax'],
choices=['isilon', 'vnx', 'unity', 'vmax', 'powermax',
'powerstore'],
help='Share backend.'),
cfg.StrOpt('emc_nas_root_dir',
help='The root directory where shares will be located.'),
@ -258,8 +259,8 @@ class EMCShareDriver(driver.ShareDriver):
def update_access(self, context, share, access_rules, add_rules,
delete_rules, share_server=None):
"""Update access to the share."""
self.plugin.update_access(context, share, access_rules, add_rules,
delete_rules, share_server)
return self.plugin.update_access(context, share, access_rules,
add_rules, delete_rules, share_server)
def check_for_setup_error(self):
"""Check for setup error."""

View File

@ -0,0 +1,412 @@
# 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.
"""REST client for Dell EMC PowerStore Manila Driver."""
import functools
import json
from oslo_log import log as logging
from oslo_utils import strutils
import requests
LOG = logging.getLogger(__name__)
class PowerStoreClient(object):
def __init__(self,
rest_ip,
rest_username,
rest_password,
verify_certificate=False,
certificate_path=None):
self.rest_ip = rest_ip
self.rest_username = rest_username
self.rest_password = rest_password
self.verify_certificate = verify_certificate
self.certificate_path = certificate_path
self.base_url = "https://%s/api/rest" % self.rest_ip
self.ok_codes = [
requests.codes.ok,
requests.codes.created,
requests.codes.accepted,
requests.codes.no_content,
requests.codes.partial_content
]
@property
def _verify_cert(self):
verify_cert = self.verify_certificate
if self.verify_certificate and self.certificate_path:
verify_cert = self.certificate_path
return verify_cert
def _send_request(self,
method,
url,
payload=None,
params=None,
log_response_data=True):
if not params:
params = {}
request_params = {
"auth": (self.rest_username, self.rest_password),
"verify": self._verify_cert,
"params": params
}
if payload and method != "GET":
request_params["data"] = json.dumps(payload)
request_url = self.base_url + url
r = requests.request(method, request_url, **request_params)
log_level = logging.DEBUG
if r.status_code not in self.ok_codes:
log_level = logging.ERROR
LOG.log(log_level,
"REST Request: %s %s with body %s",
r.request.method,
r.request.url,
strutils.mask_password(r.request.body))
if log_response_data or log_level == logging.ERROR:
msg = "REST Response: %s with data %s" % (r.status_code, r.text)
else:
msg = "REST Response: %s" % r.status_code
LOG.log(log_level, msg)
try:
response = r.json()
except ValueError:
response = None
return r, response
_send_get_request = functools.partialmethod(_send_request, "GET")
_send_post_request = functools.partialmethod(_send_request, "POST")
_send_patch_request = functools.partialmethod(_send_request, "PATCH")
_send_delete_request = functools.partialmethod(_send_request, "DELETE")
def get_nas_server_id(self, nas_server_name):
"""Retrieves the NAS server ID.
:param nas_server_name: NAS server name
:return: ID of the NAS server if success
"""
url = '/nas_server?name=eq.' + nas_server_name
res, response = self._send_get_request(url)
if res.status_code == requests.codes.ok:
return response[0]['id']
def get_nas_server_interfaces(self, nas_server_id):
"""Retrieves the NAS server ID.
:param nas_server_id: NAS server ID
:return: File interfaces of the NAS server if success
"""
url = '/nas_server/' + nas_server_id + \
'?select=current_preferred_IPv4_interface_id,' \
'current_preferred_IPv6_interface_id,' \
'file_interfaces(id,ip_address)'
res, response = self._send_get_request(url)
if res.status_code == requests.codes.ok:
preferred_IP = [response['current_preferred_IPv4_interface_id'],
response['current_preferred_IPv6_interface_id']]
file_interfaces = []
for i in response['file_interfaces']:
file_interfaces.append({
'ip': i['ip_address'],
'preferred': i['id'] in preferred_IP
})
return file_interfaces
def create_filesystem(self, nas_server_id, name, size):
"""Creates a filesystem.
:param nas_server_id: ID of the nas_server
:param name: name of the filesystem
:param size: size in Byte
:return: ID of the filesystem if created successfully
"""
payload = {
"name": name,
"size_total": size,
"nas_server_id": nas_server_id
}
url = '/file_system'
res, response = self._send_post_request(url, payload)
if res.status_code == requests.codes.created:
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
"""
payload = {
"file_system_id": filesystem_id,
"path": "/" + str(name),
"name": name
}
url = '/nfs_export'
res, response = self._send_post_request(url, payload)
if res.status_code == requests.codes.created:
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 = '/file_system/' + filesystem_id
res, _ = self._send_delete_request(url)
return res.status_code == requests.codes.no_content
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 = '/nfs_export/' + export_id + '?select=name'
res, response = self._send_get_request(url)
if res.status_code == requests.codes.ok:
return response["name"]
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 = '/nfs_export?select=id&name=eq.' + name
res, response = self._send_get_request(url)
if res.status_code == requests.codes.ok:
return response[0]['id']
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 = '/file_system?name=eq.' + name
res, response = self._send_get_request(url)
if res.status_code == requests.codes.ok:
return response[0]['id']
def set_export_access(self, export_id, rw_hosts, ro_hosts):
"""Sets the access hosts 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
"""
payload = {
"read_only_hosts": list(ro_hosts),
"read_write_root_hosts": list(rw_hosts)
}
url = '/nfs_export/' + export_id
res, _ = self._send_patch_request(url, payload)
return res.status_code == requests.codes.no_content
def resize_filesystem(self, filesystem_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
"""
payload = {
"size_total": new_size
}
url = '/file_system/' + filesystem_id
res, response = self._send_patch_request(url, payload)
if res.status_code == requests.codes.unprocessable and \
response['messages'][0]['code'] == '0xE08010080449':
return False, response['messages'][0]['message_l10n']
return res.status_code == requests.codes.no_content, None
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 = '/nfs_export?select=file_system_id&name=eq.' + name
res, response = self._send_get_request(url)
if res.status_code == requests.codes.ok:
return response[0]['file_system_id']
def create_snapshot(self, filesystem_id, name):
"""Creates a snapshot of a filesystem.
:param filesystem_id: ID of the filesystem
:param name: name of the snapshot
:return: ID of the snapshot if created successfully
"""
payload = {
"name": name
}
url = '/file_system/' + filesystem_id + '/snapshot'
res, response = self._send_post_request(url, payload)
if res.status_code == requests.codes.created:
return response["id"]
def restore_snapshot(self, snapshot_id):
"""Restore a snapshot of a filesystem.
:param snapshot_id: ID of the snapshot
:return: True if operation succeeded
"""
url = '/file_system/' + snapshot_id + '/restore'
res, _ = self._send_post_request(url)
return res.status_code == requests.codes.no_content
def clone_snapshot(self, snapshot_id, name):
"""Clone a snapshot of a filesystem.
:param snapshot_id: ID of the snapshot
:param name: name the snapshot
:return: ID of the clone if created successfully
"""
payload = {
"name": name
}
url = '/file_system/' + snapshot_id + '/clone'
res, response = self._send_post_request(url, payload)
if res.status_code == requests.codes.created:
return response["id"]
def get_cluster_id(self):
"""Get cluster id.
:return: ID of the cluster
"""
url = '/cluster'
res, response = self._send_get_request(url)
if res.status_code == requests.codes.ok:
return response[0]["id"]
def retreive_cluster_capacity_metrics(self, cluster_id):
"""Retreive cluster capacity metrics.
:param cluster_id: ID of the cluster
:return: total and used capacity in Byte
"""
payload = {
"entity": "space_metrics_by_cluster",
"entity_id": cluster_id
}
url = '/metrics/generate?order=timestamp'
# disable logging of the response
res, response = self._send_post_request(url, payload,
log_response_data=False)
if res.status_code == requests.codes.ok:
# latest cluster capacity metrics
latestMetrics = response[len(response) - 1]
LOG.debug(f"Latest cluster capacity: {latestMetrics}")
return (latestMetrics["physical_total"],
latestMetrics["physical_used"])
return None, None
def create_smb_share(self, filesystem_id, name):
"""Creates a SMB share.
:param filesystem_id: ID of the filesystem on which
the export will be created
:param name: name of the SMB share
:return: ID of the share if created successfully
"""
payload = {
"file_system_id": filesystem_id,
"path": "/" + str(name),
"name": name
}
url = '/smb_share'
res, response = self._send_post_request(url, payload)
if res.status_code == requests.codes.created:
return response["id"]
def get_fsid_from_share_name(self, name):
"""Retieves the Filesystem ID used by a SMB share.
:param name: name of the SMB share
:return: ID of the Filesystem which owns the share
"""
url = '/smb_share?select=file_system_id&name=eq.' + name
res, response = self._send_get_request(url)
if res.status_code == requests.codes.ok:
return response[0]['file_system_id']
def get_smb_share_id(self, name):
"""Retrieves SMB share ID.
:param name: name of the SMB share
:return: id of the SMB share if success
"""
url = '/smb_share?select=id&name=eq.' + name
res, response = self._send_get_request(url)
if res.status_code == requests.codes.ok:
return response[0]['id']
def get_nas_server_smb_netbios(self, nas_server_name):
"""Retrieves the domain name or netbios name.
:param nas_server_name: NAS server name
:return: Netbios name of SMB server if success
"""
url = '/nas_server?select=smb_servers(is_standalone,netbios_name)' \
'&name=eq.' + nas_server_name
res, response = self._send_get_request(url)
if res.status_code == requests.codes.ok:
smb_server = response[0]['smb_servers'][0]
if smb_server["is_standalone"]:
return smb_server["netbios_name"]
def set_acl(self, smb_share_id, cifs_rw_users, cifs_ro_users):
"""Set ACL for a SMB share.
:param smb_share_id: ID of the SMB share
:param name: name of the SMB share
:return: ID of the share if created successfully
"""
aces = list()
for rw_user in cifs_rw_users:
ace = {
"trustee_type": "User",
"trustee_name": rw_user,
"access_level": "Change",
"access_type": "Allow"
}
aces.append(ace)
for ro_user in cifs_ro_users:
ace = {
"trustee_type": "User",
"trustee_name": ro_user,
"access_level": "Read",
"access_type": "Allow"
}
aces.append(ace)
payload = {
"aces": aces
}
url = '/smb_share/' + smb_share_id + '/set_acl'
res, _ = self._send_post_request(url, payload)
return res.status_code == requests.codes.no_content

View File

@ -0,0 +1,510 @@
# 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.
"""
PowerStore 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.powerstore import client
"""Version history:
1.0 - Initial version
"""
VERSION = "1.0"
CONF = cfg.CONF
LOG = log.getLogger(__name__)
POWERSTORE_OPTS = [
cfg.StrOpt('dell_nas_backend_host',
help='Dell NAS backend hostname or IP address.'),
cfg.StrOpt('dell_nas_server',
help='Root directory or NAS server which owns the shares.'),
cfg.StrOpt('dell_ad_domain',
help='Domain name of the active directory '
'joined by the NAS server.'),
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.'),
cfg.BoolOpt('dell_ssl_cert_verify',
default=False,
help='If set to False the https client will not validate the '
'SSL certificate of the backend endpoint.'),
cfg.StrOpt('dell_ssl_cert_path',
help='Can be used to specify a non default path to a '
'CA_BUNDLE file or directory with certificates of trusted '
'CAs, which will be used to validate the backend.')
]
class PowerStoreStorageConnection(driver.StorageConnection):
"""Implements PowerStore specific functionality for Dell Manila driver."""
def __init__(self, *args, **kwargs):
"""Do initialization"""
LOG.debug('Invoking base constructor for Manila'
' Dell PowerStore Driver.')
super(PowerStoreStorageConnection,
self).__init__(*args, **kwargs)
LOG.debug('Setting up attributes for Manila'
' Dell PowerStore Driver.')
if 'configuration' in kwargs:
kwargs['configuration'].append_config_values(POWERSTORE_OPTS)
self.client = None
self.verify_certificate = None
self.certificate_path = None
self.ipv6_implemented = True
self.revert_to_snap_support = True
self.shrink_share_support = True
# props from super class
self.driver_handles_share_servers = False
# props for share status update
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 PowerStore"""
LOG.debug('Reading configuration parameters for Manila'
' Dell PowerStore Driver.')
config = dell_share_driver.configuration
get_config_value = config.safe_get
self.rest_ip = get_config_value("dell_nas_backend_host")
self.rest_username = get_config_value("dell_nas_login")
self.rest_password = get_config_value("dell_nas_password")
# validate IP, username and password
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)
self.nas_server = get_config_value("dell_nas_server")
self.ad_domain = get_config_value("dell_ad_domain")
self.verify_certificate = (get_config_value("dell_ssl_cert_verify") or
False)
if self.verify_certificate:
self.certificate_path = get_config_value(
"dell_ssl_cert_path")
LOG.debug('Initializing Dell PowerStore REST Client.')
LOG.info("REST server IP: %(ip)s, username: %(user)s. "
"Verify server's certificate: %(verify_cert)s.",
{
"ip": self.rest_ip,
"user": self.rest_username,
"verify_cert": self.verify_certificate,
})
self.client = client.PowerStoreClient(self.rest_ip,
self.rest_username,
self.rest_password,
self.verify_certificate,
self.certificate_path)
# 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.')
locations = self._create_share(share)
return locations
def _create_share(self, share):
"""Creates a NFS or SMB share.
In PowerStore, an export (share) belongs to a filesystem.
This function creates a filesystem and an export.
"""
share_name = share['name']
size_in_bytes = share['size'] * units.Gi
# create a filesystem
nas_server_id = self.client.get_nas_server_id(self.nas_server)
LOG.debug(f"Creating filesystem {share_name}")
filesystem_id = self.client.create_filesystem(nas_server_id,
share_name,
size_in_bytes)
if not filesystem_id:
message = {
_('The filesystem "%(export)s" was not created.') %
{'export': share_name}}
LOG.error(message)
raise exception.ShareBackendException(msg=message)
# create a share
locations = self._create_share_NFS_CIFS(nas_server_id, filesystem_id,
share_name,
share['share_proto'].upper())
return locations
def _create_share_NFS_CIFS(self, nas_server_id, filesystem_id, share_name,
protocal):
LOG.debug(f"Get file interfaces of {nas_server_id}")
file_interfaces = self.client.get_nas_server_interfaces(
nas_server_id)
LOG.debug(f"Creating {protocal} export {share_name}")
if protocal == 'NFS':
export_id = self.client.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)
locations = self._get_nfs_location(file_interfaces, share_name)
elif protocal == 'CIFS':
export_id = self.client.create_smb_share(filesystem_id,
share_name)
if not export_id:
message = (
_('The requested SMB share "%(export)s"'
' was not created.') %
{'export': share_name})
LOG.error(message)
raise exception.ShareBackendException(msg=message)
locations = self._get_cifs_location(file_interfaces,
share_name)
return locations
def _get_nfs_location(self, file_interfaces, share_name):
export_locations = []
for interface in file_interfaces:
export_locations.append(
{'path': f"{interface['ip']}:/{share_name}",
'metadata': {
'preferred': interface['preferred']
}
})
return export_locations
def _get_cifs_location(self, file_interfaces, share_name):
export_locations = []
for interface in file_interfaces:
export_locations.append(
{'path': f"\\\\{interface['ip']}\\{share_name}",
'metadata': {
'preferred': interface['preferred']
}
})
return export_locations
def delete_share(self, context, share, share_server):
"""Is called to delete a share."""
LOG.debug(f'Deleting {share["share_proto"]} share.')
self._delete_share(share)
def _delete_share(self, share):
"""Deletes a filesystem and its associated export."""
LOG.debug(f"Retrieving filesystem ID for filesystem {share['name']}")
filesystem_id = self.client.get_filesystem_id(share['name'])
if not filesystem_id:
LOG.warning(f'Filesystem with share name {share["name"]} \
is not found.')
else:
LOG.debug(f"Deleting filesystem ID {filesystem_id}")
share_deleted = self.client.delete_filesystem(filesystem_id)
if not share_deleted:
message = (
_('Failed to delete share "%(export)s".') %
{'export': share['name']})
LOG.error(message)
raise exception.ShareBackendException(msg=message)
def extend_share(self, share, new_size, share_server):
"""Is called to extend a share."""
LOG.debug(f"Extending {share['name']} to {new_size}GiB")
self._resize_filesystem(share, new_size)
def shrink_share(self, share, new_size, share_server):
"""Is called to shrink a share."""
LOG.debug(f"Shrinking {share['name']} to {new_size}GiB")
self._resize_filesystem(share, new_size)
def _resize_filesystem(self, share, new_size):
"""Is called to resize a filesystem"""
# Converts the size from GiB to Bytes
new_size_in_bytes = new_size * units.Gi
filesystem_id = self.client.get_filesystem_id(share['name'])
is_success, detail = self.client.resize_filesystem(filesystem_id,
new_size_in_bytes)
if not is_success:
message = (_('Failed to resize share "%(export)s".') %
{'export': share['name']})
LOG.error(message)
if detail:
raise exception.ShareShrinkingPossibleDataLoss(
share_id=share['id'])
raise exception.ShareBackendException(msg=message)
def allow_access(self, context, share, access, share_server):
"""Allow access to the share."""
raise NotImplementedError()
def deny_access(self, context, share, access, share_server):
"""Deny access to the share."""
raise NotImplementedError()
def update_access(self, context, share, access_rules, add_rules,
delete_rules, share_server=None):
"""Is called to update share access."""
protocal = share['share_proto'].upper()
LOG.debug(f'Updating access to {protocal} share.')
if protocal == 'NFS':
return self._update_nfs_access(share, access_rules)
elif protocal == 'CIFS':
return self._update_cifs_access(share, access_rules)
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.client.get_nfs_export_id(share['name'])
share_updated = self.client.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_cifs_access(self, share, access_rules):
"""Updates access rules for CIFS share type."""
cifs_rw_users = set()
cifs_ro_users = set()
access_updates = {}
for rule in access_rules:
if rule['access_type'].lower() != 'user':
message = (_("Only user access type currently supported for "
"CIFS. 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:
prefix = self.ad_domain or \
self.client.get_nas_server_smb_netbios(self.nas_server)
if not prefix:
message = (
_('Failed to get daomain/netbios name of '
'"%(nas_server)s".'
) % {'nas_server': self.nas_server})
LOG.error(message)
access_updates.update({rule['access_id']:
{'state': 'error'}})
continue
prefix = prefix + '\\'
if rule['access_level'] == const.ACCESS_LEVEL_RW:
cifs_rw_users.add(prefix + rule['access_to'])
elif rule['access_level'] == const.ACCESS_LEVEL_RO:
cifs_ro_users.add(prefix + rule['access_to'])
access_updates.update({rule['access_id']: {'state': 'active'}})
share_id = self.client.get_smb_share_id(share['name'])
share_updated = self.client.set_acl(share_id,
cifs_rw_users,
cifs_ro_users)
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_CIFS'
stats_dict['reserved_percentage'] = self.reserved_percentage
stats_dict['reserved_snapshot_percentage'] = \
self.reserved_snapshot_percentage
stats_dict['reserved_share_extend_percentage'] = \
self.reserved_share_extend_percentage
stats_dict['max_over_subscription_ratio'] = \
self.max_over_subscription_ratio
cluster_id = self.client.get_cluster_id()
total, used = self.client.retreive_cluster_capacity_metrics(cluster_id)
if(total and used):
free = total - used
stats_dict['total_capacity_gb'] = total // units.Gi
stats_dict['free_capacity_gb'] = free // units.Gi
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.client.get_filesystem_id(export_name)
if not filesystem_id:
message = (
_('Failed to get filesystem id for export "%(export)s".') %
{'export': export_name})
LOG.error(message)
raise exception.ShareBackendException(msg=message)
snapshot_name = snapshot['name']
LOG.debug(
f'Creating snapshot {snapshot_name} for filesystem {filesystem_id}'
)
snapshot_id = self.client.create_snapshot(filesystem_id,
snapshot_name)
if not snapshot_id:
message = (
_('Failed to create snapshot "%(snapshot)s".') %
{'snapshot': snapshot_name})
LOG.error(message)
raise exception.ShareBackendException(msg=message)
else:
LOG.info("Snapshot %(snapshot)s successfully created.",
{'snapshot': snapshot_name})
def delete_snapshot(self, context, snapshot, share_server):
"""Is called to delete snapshot."""
snapshot_name = snapshot['name']
LOG.debug(f'Retrieving filesystem ID for snapshot {snapshot_name}')
filesystem_id = self.client.get_filesystem_id(snapshot_name)
LOG.debug(f'Deleting filesystem ID {filesystem_id}')
snapshot_deleted = self.client.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 %(snapshot)s successfully deleted.",
{'snapshot': snapshot_name})
def revert_to_snapshot(self, context, snapshot, share_access_rules,
snapshot_access_rules, share_server=None):
"""Reverts a share (in place) to the specified snapshot."""
snapshot_name = snapshot['name']
snapshot_id = self.client.get_filesystem_id(snapshot_name)
snapshot_restored = self.client.restore_snapshot(snapshot_id)
if not snapshot_restored:
message = (
_('Failed to restore snapshot "%(snapshot)s".') %
{'snapshot': snapshot_name})
LOG.error(message)
raise exception.ShareBackendException(msg=message)
else:
LOG.info("Snapshot %(snapshot)s successfully restored.",
{'snapshot': snapshot_name})
def create_share_from_snapshot(self, context, share, snapshot,
share_server=None, parent_share=None):
"""Create a share from a snapshot - clone a snapshot."""
LOG.debug(f'Creating {share["share_proto"]} share.')
locations = self._create_share_from_snapshot(share, snapshot)
if share['size'] != snapshot['size']:
LOG.debug(f"Resizing {share['name']} to {share['size']}GiB")
self._resize_filesystem(share, share['size'])
return locations
def _create_share_from_snapshot(self, share, snapshot):
LOG.debug(f"Retrieving snapshot id of snapshot {snapshot['name']}")
snapshot_id = self.client.get_filesystem_id(snapshot['name'])
share_name = share['name']
LOG.debug(
f"Cloning filesystem {share_name} from snapshot {snapshot_id}"
)
filesystem_id = self.client.clone_snapshot(snapshot_id,
share_name)
if not filesystem_id:
message = {
_('The filesystem "%(export)s" was not created.') %
{'export': share_name}}
LOG.error(message)
raise exception.ShareBackendException(msg=message)
# create a share
nas_server_id = self.client.get_nas_server_id(self.nas_server)
locations = self._create_share_NFS_CIFS(nas_server_id, filesystem_id,
share_name,
share['share_proto'].upper())
return locations
def ensure_share(self, context, share, share_server):
"""Invoked to ensure that share is exported."""
def setup_server(self, network_info, metadata=None):
"""Set up and configures share server with given network parameters."""
def teardown_server(self, server_details, security_services=None):
"""Teardown share server."""
def check_for_setup_error(self):
"""Is called to check for setup error."""
def get_default_filter_function(self):
return 'share.size >= 3'

View File

@ -0,0 +1,3 @@
{
"id": "64560f05-e677-ec2a-7fcf-1a9efb93188b"
}

View File

@ -0,0 +1,3 @@
{
"id": "6454e9a9-a698-e9bc-ca61-1a9efb93188b"
}

View File

@ -0,0 +1,3 @@
{
"id": "6454ec18-7b8d-1532-1b8a-1a9efb93188b"
}

View File

@ -0,0 +1,3 @@
{
"id": "64927ae9-3403-6930-a784-f227b9987c54"
}

View File

@ -0,0 +1,3 @@
{
"id": "6454ea29-09c3-030e-cfc3-1a9efb93188b"
}

View File

@ -0,0 +1,5 @@
[
{
"id": "0"
}
]

View File

@ -0,0 +1,5 @@
[
{
"id": "6454e9a9-a698-e9bc-ca61-1a9efb93188b"
}
]

View File

@ -0,0 +1,5 @@
[
{
"file_system_id": "6454e9a9-a698-e9bc-ca61-1a9efb93188b"
}
]

View File

@ -0,0 +1,5 @@
[
{
"file_system_id": "6454e9a9-a698-e9bc-ca61-1a9efb93188b"
}
]

View File

@ -0,0 +1,5 @@
[
{
"id": "6423d56e-eaf3-7424-be0b-1a9efb93188b"
}
]

View File

@ -0,0 +1,10 @@
{
"current_preferred_IPv4_interface_id": "6423d586-4070-f752-c4da-1a9efb93188b",
"current_preferred_IPv6_interface_id": null,
"file_interfaces": [
{
"id": "6423d586-4070-f752-c4da-1a9efb93188b",
"ip_address": "192.168.11.23"
}
]
}

View File

@ -0,0 +1,11 @@
[
{
"smb_servers": [
{
"is_standalone": true,
"domain": null,
"netbios_name": "OPENSTACK"
}
]
}
]

View File

@ -0,0 +1,5 @@
[
{
"id": "6454ec18-7b8d-1532-1b8a-1a9efb93188b"
}
]

View File

@ -0,0 +1,3 @@
{
"name": "powerstore-nfs-share"
}

View File

@ -0,0 +1,5 @@
[
{
"id": "64927ae9-3403-6930-a784-f227b9987c54"
}
]

View File

@ -0,0 +1,12 @@
{
"messages": [
{
"code": "0xE08010080449",
"severity": "Error",
"message_l10n": "The new size for the file system is below the file system's current size used (5222 MB).",
"arguments": [
"5222"
]
}
]
}

View File

@ -0,0 +1,674 @@
# 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.
import json
import pathlib
import requests_mock
from manila.share.drivers.dell_emc.plugins.powerstore import client
from manila import test
class TestClient(test.TestCase):
REST_IP = "192.168.0.110"
NAS_SERVER_NAME = "powerstore-nasserver"
NAS_SERVER_ID = "6423d56e-eaf3-7424-be0b-1a9efb93188b"
NAS_SERVER_IP = "192.168.11.23"
NFS_EXPORT_NAME = "powerstore-nfs-share"
NFS_EXPORT_SIZE = 3221225472
NFS_EXPORT_NEW_SIZE = 6221225472
FILESYSTEM_ID = "6454e9a9-a698-e9bc-ca61-1a9efb93188b"
NFS_EXPORT_ID = "6454ec18-7b8d-1532-1b8a-1a9efb93188b"
RW_HOSTS = "192.168.1.10"
RO_HOSTS = "192.168.1.11"
SMB_SHARE_NAME = "powerstore-smb-share"
SMB_SHARE_ID = "64927ae9-3403-6930-a784-f227b9987c54"
RW_USERS = "user1"
RO_USERS = "user2"
SNAPSHOT_NAME = "powerstore-nfs-share-snap"
SNAPSHOT_ID = "6454ea29-09c3-030e-cfc3-1a9efb93188b"
CLONE_ID = "64560f05-e677-ec2a-7fcf-1a9efb93188b"
CLONE_NAME = "powerstore-nfs-share-snap-clone"
CLUSTER_ID = "0"
CLIENT_OPTIONS = {
"rest_ip": REST_IP,
"rest_username": "admin",
"rest_password": "pwd",
"verify_certificate": False,
"certificate_path": None
}
def setUp(self):
super(TestClient, self).setUp()
self._mock_url = "https://%s/api/rest" % self.REST_IP
self.client = client.PowerStoreClient(**self.CLIENT_OPTIONS)
self.mockup_file_base = (
str(pathlib.Path.cwd())
+ "/manila/tests/share/drivers/dell_emc/plugins/powerstore/mockup/"
)
def _getJsonFile(self, filename):
f = open(self.mockup_file_base + filename)
data = json.load(f)
f.close()
return data
def test__verify_cert(self):
verify_cert = self.client.verify_certificate
certificate_path = self.client.certificate_path
self.client.verify_certificate = True
self.client.certificate_path = "fake_certificate_path"
self.assertEqual(self.client._verify_cert,
self.client.certificate_path)
self.client.verify_certificate = verify_cert
self.client.certificate_path = certificate_path
@requests_mock.mock()
def test__send_request(self, m):
url = "{0}/fake_res".format(self._mock_url)
m.get(url, status_code=200)
self.client._send_get_request("/fake_res", None, None, False)
@requests_mock.mock()
def test_get_nas_server_id(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_nas_server_id_response(
m, self.NAS_SERVER_NAME,
self._getJsonFile("get_nas_server_id_response.json")
)
id = self.client.get_nas_server_id(self.NAS_SERVER_NAME)
self.assertEqual(id, self.NAS_SERVER_ID)
def _add_get_nas_server_id_response(self, m, nas_server, json_str):
url = "{0}/nas_server?name=eq.{1}".format(
self._mock_url, nas_server
)
m.get(url, status_code=200, json=json_str)
@requests_mock.mock()
def test_get_nas_server_id_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_nas_server_id_response_failure(
m, self.NAS_SERVER_NAME
)
id = self.client.get_nas_server_id(self.NAS_SERVER_NAME)
self.assertIsNone(id)
def _add_get_nas_server_id_response_failure(self, m, nas_server):
url = "{0}/nas_server?name=eq.{1}".format(
self._mock_url, nas_server
)
m.get(url, status_code=400)
@requests_mock.mock()
def test_get_nas_server_interfaces(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_nas_server_interfaces_response(
m, self.NAS_SERVER_ID,
self._getJsonFile("get_nas_server_interfaces_response.json")
)
interfaces = self.client.get_nas_server_interfaces(self.NAS_SERVER_ID)
self.assertEqual(interfaces[0]['ip'], self.NAS_SERVER_IP)
self.assertEqual(interfaces[0]['preferred'], True)
def _add_get_nas_server_interfaces_response(self, m, nas_server_id,
json_str):
url = "{0}/nas_server/{1}?select=" \
"current_preferred_IPv4_interface_id," \
"current_preferred_IPv6_interface_id," \
"file_interfaces(id,ip_address)".format(
self._mock_url, nas_server_id
)
m.get(url, status_code=200, json=json_str)
@requests_mock.mock()
def test_get_nas_server_interfaces_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_nas_server_interfaces_response_failure(
m, self.NAS_SERVER_ID
)
interfaces = self.client.get_nas_server_interfaces(self.NAS_SERVER_ID)
self.assertIsNone(interfaces)
def _add_get_nas_server_interfaces_response_failure(self, m,
nas_server_id):
url = "{0}/nas_server/{1}?select=" \
"current_preferred_IPv4_interface_id," \
"current_preferred_IPv6_interface_id," \
"file_interfaces(id,ip_address)".format(
self._mock_url, nas_server_id
)
m.get(url, status_code=400)
@requests_mock.mock()
def test_create_filesystem(self, m):
self.assertEqual(0, len(m.request_history))
self._add_create_filesystem_response(
m, self._getJsonFile("create_filesystem_response.json")
)
id = self.client.create_filesystem(
self.NAS_SERVER_ID,
self.NFS_EXPORT_NAME,
self.NFS_EXPORT_SIZE
)
self.assertEqual(id, self.FILESYSTEM_ID)
def _add_create_filesystem_response(self, m, json_str):
url = "{0}/file_system".format(self._mock_url)
m.post(url, status_code=201, json=json_str)
@requests_mock.mock()
def test_create_filesystem_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_create_filesystem_response_failure(m)
id = self.client.create_filesystem(
self.NAS_SERVER_ID,
self.NFS_EXPORT_NAME,
self.NFS_EXPORT_SIZE
)
self.assertIsNone(id)
def _add_create_filesystem_response_failure(self, m):
url = "{0}/file_system".format(self._mock_url)
m.post(url, status_code=400)
@requests_mock.mock()
def test_create_nfs_export(self, m):
self.assertEqual(0, len(m.request_history))
self._add_create_nfs_export_response(
m, self._getJsonFile("create_nfs_export_response.json")
)
id = self.client.create_nfs_export(self.FILESYSTEM_ID,
self.NFS_EXPORT_NAME)
self.assertEqual(id, self.NFS_EXPORT_ID)
def _add_create_nfs_export_response(self, m, json_str):
url = "{0}/nfs_export".format(self._mock_url)
m.post(url, status_code=201, json=json_str)
@requests_mock.mock()
def test_create_nfs_export_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_create_nfs_export_response_failure(m)
id = self.client.create_nfs_export(self.FILESYSTEM_ID,
self.NFS_EXPORT_NAME)
self.assertIsNone(id)
def _add_create_nfs_export_response_failure(self, m):
url = "{0}/nfs_export".format(self._mock_url)
m.post(url, status_code=400)
@requests_mock.mock()
def test_delete_filesystem(self, m):
self.assertEqual(0, len(m.request_history))
self._add_delete_filesystem_response(m, self.FILESYSTEM_ID)
result = self.client.delete_filesystem(self.FILESYSTEM_ID)
self.assertEqual(result, True)
def _add_delete_filesystem_response(self, m, filesystem_id):
url = "{0}/file_system/{1}".format(
self._mock_url, filesystem_id
)
m.delete(url, status_code=204)
@requests_mock.mock()
def test_get_nfs_export_name(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_nfs_export_name_response(
m,
self.NFS_EXPORT_ID,
self._getJsonFile("get_nfs_export_name_response.json"),
)
name = self.client.get_nfs_export_name(self.NFS_EXPORT_ID)
self.assertEqual(name, self.NFS_EXPORT_NAME)
def _add_get_nfs_export_name_response(self, m, export_id, json_str):
url = "{0}/nfs_export/{1}?select=name".format(
self._mock_url, export_id
)
m.get(url, status_code=200, json=json_str)
@requests_mock.mock()
def test_get_nfs_export_name_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_nfs_export_name_response_failure(m, self.NFS_EXPORT_ID)
name = self.client.get_nfs_export_name(self.NFS_EXPORT_ID)
self.assertIsNone(name)
def _add_get_nfs_export_name_response_failure(self,
m, export_id):
url = "{0}/nfs_export/{1}?select=name".format(
self._mock_url, export_id
)
m.get(url, status_code=400)
@requests_mock.mock()
def test_get_nfs_export_id(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_nfs_export_id_response(
m, self.NFS_EXPORT_NAME,
self._getJsonFile("get_nfs_export_id_response.json")
)
id = self.client.get_nfs_export_id(self.NFS_EXPORT_NAME)
self.assertEqual(id, self.NFS_EXPORT_ID)
def _add_get_nfs_export_id_response(self, m, name, json_str):
url = "{0}/nfs_export?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_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_nfs_export_id_response_failure(m, self.NFS_EXPORT_NAME)
id = self.client.get_nfs_export_id(self.NFS_EXPORT_NAME)
self.assertIsNone(id)
def _add_get_nfs_export_id_response_failure(self, m, name):
url = "{0}/nfs_export?select=id&name=eq.{1}".format(
self._mock_url, name
)
m.get(url, status_code=400)
@requests_mock.mock()
def test_get_filesystem_id(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_filesystem_id_response(
m, self.NFS_EXPORT_NAME,
self._getJsonFile("get_fileystem_id_response.json")
)
id = self.client.get_filesystem_id(self.NFS_EXPORT_NAME)
self.assertEqual(id, self.FILESYSTEM_ID)
def _add_get_filesystem_id_response(self, m, name, json_str):
url = "{0}/file_system?name=eq.{1}".format(
self._mock_url, name
)
m.get(url, status_code=200, json=json_str)
@requests_mock.mock()
def test_get_filesystem_id_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_filesystem_id_response_failure(m, self.NFS_EXPORT_NAME)
id = self.client.get_filesystem_id(self.NFS_EXPORT_NAME)
self.assertIsNone(id)
def _add_get_filesystem_id_response_failure(self, m, name):
url = "{0}/file_system?name=eq.{1}".format(
self._mock_url, name
)
m.get(url, status_code=400)
@requests_mock.mock()
def test_set_export_access(self, m):
self.assertEqual(0, len(m.request_history))
self._add_set_export_access_response(m, self.NFS_EXPORT_ID)
result = self.client.set_export_access(self.NFS_EXPORT_ID,
self.RW_HOSTS,
self.RO_HOSTS)
self.assertEqual(result, True)
def _add_set_export_access_response(self, m, export_id):
url = "{0}/nfs_export/{1}".format(self._mock_url, export_id)
m.patch(url, status_code=204)
@requests_mock.mock()
def test_resize_filesystem(self, m):
self.assertEqual(0, len(m.request_history))
self._add_resize_filesystem_response(m, self.FILESYSTEM_ID)
result, detail = self.client.resize_filesystem(
self.FILESYSTEM_ID, self.NFS_EXPORT_NEW_SIZE)
self.assertTrue(result)
self.assertIsNone(detail)
def _add_resize_filesystem_response(self, m, filesystem_id):
url = "{0}/file_system/{1}".format(
self._mock_url, filesystem_id
)
m.patch(url, status_code=204)
@requests_mock.mock()
def test_resize_filesystem_shrink_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_resize_filesystem_shrink_failure_response(
m, self.FILESYSTEM_ID,
self._getJsonFile(
"resize_filesystem_shrink_failure_response.json"))
result, detail = self.client.resize_filesystem(
self.FILESYSTEM_ID, self.NFS_EXPORT_NEW_SIZE)
self.assertFalse(result)
self.assertIsNotNone(detail)
def _add_resize_filesystem_shrink_failure_response(
self, m, filesystem_id, json_str):
url = "{0}/file_system/{1}".format(
self._mock_url, filesystem_id
)
m.patch(url, status_code=422, json=json_str)
@requests_mock.mock()
def test_get_fsid_from_export_name(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_fsid_from_export_name_response(
m, self.NFS_EXPORT_NAME,
self._getJsonFile("get_fsid_from_export_name_response.json")
)
id = self.client.get_fsid_from_export_name(self.NFS_EXPORT_NAME)
self.assertEqual(id, self.FILESYSTEM_ID)
def _add_get_fsid_from_export_name_response(self, m, name, json_str):
url = "{0}/nfs_export?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_export_name_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_fsid_from_export_name_response_failure(
m, self.NFS_EXPORT_NAME
)
id = self.client.get_fsid_from_export_name(self.NFS_EXPORT_NAME)
self.assertIsNone(id)
def _add_get_fsid_from_export_name_response_failure(self, m, name):
url = "{0}/nfs_export?select=file_system_id&name=eq.{1}".format(
self._mock_url, name
)
m.get(url, status_code=400)
@requests_mock.mock()
def test_create_snapshot(self, m):
self.assertEqual(0, len(m.request_history))
self._add_create_snapshot_response(
m, self.FILESYSTEM_ID,
self._getJsonFile("create_snapshot_response.json")
)
id = self.client.create_snapshot(self.FILESYSTEM_ID,
self.SNAPSHOT_NAME)
self.assertEqual(id, self.SNAPSHOT_ID)
def _add_create_snapshot_response(self, m, filesystem_id, json_str):
url = "{0}/file_system/{1}/snapshot".format(self._mock_url,
filesystem_id)
m.post(url, status_code=201, json=json_str)
@requests_mock.mock()
def test_create_snapshot_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_create_snapshot_response_failure(m, self.FILESYSTEM_ID)
id = self.client.create_snapshot(self.FILESYSTEM_ID,
self.SNAPSHOT_NAME)
self.assertIsNone(id)
def _add_create_snapshot_response_failure(self, m, filesystem_id):
url = "{0}/file_system/{1}/snapshot".format(self._mock_url,
filesystem_id)
m.post(url, status_code=400)
@requests_mock.mock()
def test_restore_snapshot(self, m):
self.assertEqual(0, len(m.request_history))
self._add_restore_snapshot_response(
m, self.SNAPSHOT_ID
)
result = self.client.restore_snapshot(self.SNAPSHOT_ID)
self.assertEqual(result, True)
def _add_restore_snapshot_response(self, m, snapshot_id):
url = "{0}/file_system/{1}/restore".format(self._mock_url,
snapshot_id)
m.post(url, status_code=204)
@requests_mock.mock()
def test_restore_snapshot_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_restore_snapshot_response_failure(
m, self.SNAPSHOT_ID
)
result = self.client.restore_snapshot(self.SNAPSHOT_ID)
self.assertEqual(result, False)
def _add_restore_snapshot_response_failure(self, m, snapshot_id):
url = "{0}/file_system/{1}/restore".format(self._mock_url,
snapshot_id)
m.post(url, status_code=400)
@requests_mock.mock()
def test_clone_snapshot(self, m):
self.assertEqual(0, len(m.request_history))
self._add_clone_snapshot_response(
m, self.SNAPSHOT_ID,
self._getJsonFile("clone_snapshot_response.json")
)
id = self.client.clone_snapshot(self.SNAPSHOT_ID,
self.CLONE_NAME)
self.assertEqual(id, self.CLONE_ID)
def _add_clone_snapshot_response(self, m, snapshot_id, json_str):
url = "{0}/file_system/{1}/clone".format(self._mock_url,
snapshot_id)
m.post(url, status_code=201, json=json_str)
@requests_mock.mock()
def test_clone_snapshot_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_clone_snapshot_response_failure(
m, self.SNAPSHOT_ID
)
id = self.client.clone_snapshot(self.SNAPSHOT_ID,
self.CLONE_NAME)
self.assertIsNone(id)
def _add_clone_snapshot_response_failure(self, m, snapshot_id):
url = "{0}/file_system/{1}/clone".format(self._mock_url,
snapshot_id)
m.post(url, status_code=400)
@requests_mock.mock()
def test_get_cluster_id(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_cluster_id_response(
m,
self._getJsonFile("get_cluster_id_response.json")
)
id = self.client.get_cluster_id()
self.assertEqual(id, self.CLUSTER_ID)
def _add_get_cluster_id_response(self, m, json_str):
url = "{0}/cluster".format(self._mock_url)
m.get(url, status_code=200, json=json_str)
@requests_mock.mock()
def test_get_cluster_id_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_cluster_id_response_failure(m)
id = self.client.get_cluster_id()
self.assertIsNone(id)
def _add_get_cluster_id_response_failure(self, m):
url = "{0}/cluster".format(self._mock_url)
m.get(url, status_code=400)
@requests_mock.mock()
def test_retreive_cluster_capacity_metrics(self, m):
self.assertEqual(0, len(m.request_history))
self._add_retreive_cluster_capacity_metrics_response(
m,
self._getJsonFile(
"retreive_cluster_capacity_metrics_response.json")
)
total, used = self.client.retreive_cluster_capacity_metrics(
self.CLUSTER_ID)
self.assertEqual(total, 47345047046144)
self.assertEqual(used, 366003363027)
def _add_retreive_cluster_capacity_metrics_response(self, m, json_str):
url = "{0}/metrics/generate?order=timestamp".format(self._mock_url)
m.post(url, status_code=200, json=json_str)
@requests_mock.mock()
def test_retreive_cluster_capacity_metrics_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_retreive_cluster_capacity_metrics_response_failure(m)
total, used = self.client.retreive_cluster_capacity_metrics(
self.CLUSTER_ID)
self.assertIsNone(total)
self.assertIsNone(used)
def _add_retreive_cluster_capacity_metrics_response_failure(self, m):
url = "{0}/metrics/generate?order=timestamp".format(self._mock_url)
m.post(url, status_code=400)
@requests_mock.mock()
def test_create_smb_share(self, m):
self.assertEqual(0, len(m.request_history))
self._add_create_smb_share_response(
m, self._getJsonFile("create_smb_share_response.json")
)
id = self.client.create_smb_share(self.FILESYSTEM_ID,
self.SMB_SHARE_NAME)
self.assertEqual(id, self.SMB_SHARE_ID)
def _add_create_smb_share_response(self, m, json_str):
url = "{0}/smb_share".format(self._mock_url)
m.post(url, status_code=201, json=json_str)
@requests_mock.mock()
def test_create_smb_share_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_create_smb_share_response_failure(m)
id = self.client.create_smb_share(self.FILESYSTEM_ID,
self.SMB_SHARE_NAME)
self.assertIsNone(id)
def _add_create_smb_share_response_failure(self, m):
url = "{0}/smb_share".format(self._mock_url)
m.post(url, status_code=400)
@requests_mock.mock()
def test_get_fsid_from_share_name(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_fsid_from_share_name_response(
m, self.NFS_EXPORT_NAME,
self._getJsonFile("get_fsid_from_share_name_response.json")
)
id = self.client.get_fsid_from_share_name(self.NFS_EXPORT_NAME)
self.assertEqual(id, self.FILESYSTEM_ID)
def _add_get_fsid_from_share_name_response(self, m, name, json_str):
url = "{0}/smb_share?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_share_name_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_fsid_from_share_name_response_failure(
m, self.SMB_SHARE_NAME
)
id = self.client.get_fsid_from_share_name(self.SMB_SHARE_NAME)
self.assertIsNone(id)
def _add_get_fsid_from_share_name_response_failure(self, m, name):
url = "{0}/smb_share?select=file_system_id&name=eq.{1}".format(
self._mock_url, name
)
m.get(url, status_code=400)
@requests_mock.mock()
def test_get_smb_share_id(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_smb_share_id_response(
m, self.SMB_SHARE_NAME,
self._getJsonFile("get_smb_share_id_response.json")
)
id = self.client.get_smb_share_id(self.SMB_SHARE_NAME)
self.assertEqual(id, self.SMB_SHARE_ID)
def _add_get_smb_share_id_response(self, m, name, json_str):
url = "{0}/smb_share?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_smb_share_id_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_smb_share_id_response_failure(
m, self.SMB_SHARE_NAME
)
id = self.client.get_smb_share_id(self.SMB_SHARE_NAME)
self.assertIsNone(id)
def _add_get_smb_share_id_response_failure(self, m, name):
url = "{0}/smb_share?select=id&name=eq.{1}".format(
self._mock_url, name
)
m.get(url, status_code=400)
@requests_mock.mock()
def test_get_nas_server_smb_netbios(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_nas_server_smb_netbios_response(
m, self.SMB_SHARE_NAME,
self._getJsonFile("get_nas_server_smb_netbios_response.json")
)
id = self.client.get_nas_server_smb_netbios(self.SMB_SHARE_NAME)
self.assertEqual(id, "OPENSTACK")
def _add_get_nas_server_smb_netbios_response(self, m, name, json_str):
url = "{0}/nas_server?select=smb_servers" \
"(is_standalone,netbios_name)&name=eq.{1}".format(
self._mock_url, name
)
m.get(url, status_code=200, json=json_str)
@requests_mock.mock()
def test_get_nas_server_smb_netbios_failure(self, m):
self.assertEqual(0, len(m.request_history))
self._add_get_nas_server_smb_netbios_response_failure(
m, self.SMB_SHARE_NAME
)
id = self.client.get_nas_server_smb_netbios(self.SMB_SHARE_NAME)
self.assertIsNone(id)
def _add_get_nas_server_smb_netbios_response_failure(self, m, name):
url = "{0}/nas_server?select=smb_servers" \
"(is_standalone,netbios_name)&name=eq.{1}".format(
self._mock_url, name
)
m.get(url, status_code=400)
@requests_mock.mock()
def test_set_acl(self, m):
self.assertEqual(0, len(m.request_history))
self._add_set_acl_response(m, self.SMB_SHARE_ID)
result = self.client.set_acl(self.SMB_SHARE_ID,
self.RW_USERS,
self.RO_USERS)
self.assertEqual(result, True)
def _add_set_acl_response(self, m, share_id):
url = "{0}/smb_share/{1}/set_acl".format(self._mock_url, share_id)
m.post(url, status_code=204)

File diff suppressed because it is too large Load Diff

View File

@ -65,14 +65,12 @@ class FakeConnection(base.StorageConnection):
def connect(self, emc_share_driver, context):
"""Any initialization the share driver does while starting."""
raise NotImplementedError()
def update_share_stats(self, stats_dict):
"""Add key/values to stats_dict."""
def get_network_allocations_number(self):
"""Returns number of network allocations for creating VIFs."""
return 0
def setup_server(self, network_info, metadata=None):
"""Set up and configures share server with given network parameters."""
@ -81,7 +79,23 @@ class FakeConnection(base.StorageConnection):
"""Teardown share server."""
class FakeConnection_vmax(FakeConnection):
def __init__(self, *args, **kwargs):
self.dhss_mandatory_security_service_association = {}
self.revert_to_snap_support = False
self.shrink_share_support = False
self.manage_existing_support = False
self.manage_existing_with_server_support = False
self.manage_existing_snapshot_support = False
self.manage_snapshot_with_server_support = False
self.manage_server_support = False
self.get_share_server_network_info_support = False
pass
FAKE_BACKEND = 'fake_backend'
FAKE_BACKEND_VMAX = 'vmax'
FAKE_BACKEND_POWERMAX = 'powermax'
class FakeEMCExtensionManager(object):
@ -92,6 +106,11 @@ class FakeEMCExtensionManager(object):
plugin=FakeConnection,
entry_point=None,
obj=None))
self.extensions.append(
extension.Extension(name=FAKE_BACKEND_POWERMAX,
plugin=FakeConnection_vmax,
entry_point=None,
obj=None))
class EMCShareFrameworkTestCase(test.TestCase):
@ -107,6 +126,15 @@ class EMCShareFrameworkTestCase(test.TestCase):
self.driver = emcdriver.EMCShareDriver(
configuration=self.configuration)
self.configuration_vmax = conf.Configuration(None)
self.configuration_vmax.append_config_values = \
mock.Mock(return_value=0)
self.configuration_vmax.share_backend_name = FAKE_BACKEND_VMAX
self.mock_object(self.configuration_vmax, 'safe_get',
self._fake_safe_get_vmax)
self.driver_vmax = emcdriver.EMCShareDriver(
configuration=self.configuration_vmax)
def test_driver_setup(self):
FakeConnection.connect = mock.Mock()
self.driver.do_setup(None)
@ -157,6 +185,13 @@ class EMCShareFrameworkTestCase(test.TestCase):
return True
return None
def _fake_safe_get_vmax(self, value):
if value in ['emc_share_backend', 'share_backend_name']:
return FAKE_BACKEND_VMAX
elif value == 'driver_handles_share_servers':
return True
return None
def test_support_manage(self):
share = mock.Mock()
driver_options = mock.Mock()
@ -179,6 +214,33 @@ class EMCShareFrameworkTestCase(test.TestCase):
share_server)
self.driver.manage_server(context, share_server, identifier,
driver_options)
self.driver.get_share_server_network_info_support = True
self.driver.get_share_server_network_info(context, share_server,
identifier, driver_options)
self.driver.create_share(context, share, share_server)
self.driver.create_share_from_snapshot(context, share, snapshot,
share_server)
self.driver.extend_share(share, 20, share_server)
self.driver.shrink_share_support = True
self.driver.shrink_share(share, 20, share_server)
self.driver.create_snapshot(context, snapshot, share_server)
self.driver.delete_share(context, share, share_server)
self.driver.delete_snapshot(context, snapshot, share_server)
self.driver.ensure_share(context, share, share_server)
access = mock.Mock()
self.driver.allow_access(context, share, access, share_server)
self.driver.deny_access(context, share, access, share_server)
self.driver.update_access(context, share, None, None, share_server)
self.driver.check_for_setup_error()
self.driver.get_network_allocations_number()
self.driver._teardown_server(None)
self.driver.revert_to_snap_support = True
share_access_rules = mock.Mock()
snapshot_access_rules = mock.Mock()
self.driver.revert_to_snapshot(context, snapshot, share_access_rules,
snapshot_access_rules, share_server)
self.driver.ipv6_implemented = False
self.driver.get_configured_ip_versions()
def test_not_support_manage(self):
share = mock.Mock()
@ -200,6 +262,20 @@ class EMCShareFrameworkTestCase(test.TestCase):
result = self.driver.manage_server(None, share_server, identifier,
driver_options)
self.assertIsInstance(result, NotImplementedError)
result = self.driver.get_share_server_network_info(None,
share_server,
identifier,
driver_options)
self.assertIsInstance(result, NotImplementedError)
self.assertRaises(NotImplementedError, self.driver.shrink_share, share,
20, share_server)
share_access_rules = mock.Mock()
snapshot_access_rules = mock.Mock()
self.assertRaises(NotImplementedError, self.driver.revert_to_snapshot,
None, snapshot, share_access_rules,
snapshot_access_rules, share_server)
def test_unmanage_manage(self):
share = mock.Mock()

View File

@ -0,0 +1,5 @@
---
features:
- |
Added a new Manila driver to support Dell PowerStore storage backend.
It supports NFS and CIFS shares operations, and snapshot operations.

View File

@ -83,6 +83,7 @@ manila.share.drivers.dell_emc.plugins =
unity = manila.share.drivers.dell_emc.plugins.unity.connection:UnityStorageConnection
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
manila.tests.scheduler.fakes =
FakeWeigher1 = manila.tests.scheduler.fakes:FakeWeigher1
FakeWeigher2 = manila.tests.scheduler.fakes:FakeWeigher2