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:
parent
a01cdc7719
commit
09555c80de
@ -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
|
||||
|
@ -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 | \- | \- |
|
||||
|
@ -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
|
||||
|
@ -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.
|
@ -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."""
|
||||
|
412
manila/share/drivers/dell_emc/plugins/powerstore/client.py
Normal file
412
manila/share/drivers/dell_emc/plugins/powerstore/client.py
Normal 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
|
510
manila/share/drivers/dell_emc/plugins/powerstore/connection.py
Normal file
510
manila/share/drivers/dell_emc/plugins/powerstore/connection.py
Normal 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'
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"id": "64560f05-e677-ec2a-7fcf-1a9efb93188b"
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"id": "6454e9a9-a698-e9bc-ca61-1a9efb93188b"
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"id": "6454ec18-7b8d-1532-1b8a-1a9efb93188b"
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"id": "64927ae9-3403-6930-a784-f227b9987c54"
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"id": "6454ea29-09c3-030e-cfc3-1a9efb93188b"
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
[
|
||||
{
|
||||
"id": "0"
|
||||
}
|
||||
]
|
@ -0,0 +1,5 @@
|
||||
[
|
||||
{
|
||||
"id": "6454e9a9-a698-e9bc-ca61-1a9efb93188b"
|
||||
}
|
||||
]
|
@ -0,0 +1,5 @@
|
||||
[
|
||||
{
|
||||
"file_system_id": "6454e9a9-a698-e9bc-ca61-1a9efb93188b"
|
||||
}
|
||||
]
|
@ -0,0 +1,5 @@
|
||||
[
|
||||
{
|
||||
"file_system_id": "6454e9a9-a698-e9bc-ca61-1a9efb93188b"
|
||||
}
|
||||
]
|
@ -0,0 +1,5 @@
|
||||
[
|
||||
{
|
||||
"id": "6423d56e-eaf3-7424-be0b-1a9efb93188b"
|
||||
}
|
||||
]
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"smb_servers": [
|
||||
{
|
||||
"is_standalone": true,
|
||||
"domain": null,
|
||||
"netbios_name": "OPENSTACK"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -0,0 +1,5 @@
|
||||
[
|
||||
{
|
||||
"id": "6454ec18-7b8d-1532-1b8a-1a9efb93188b"
|
||||
}
|
||||
]
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "powerstore-nfs-share"
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
[
|
||||
{
|
||||
"id": "64927ae9-3403-6930-a784-f227b9987c54"
|
||||
}
|
||||
]
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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
@ -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()
|
||||
|
@ -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.
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user