Implement share revert to snapshot

This commit adds the ability for Manila to revert a
share to the latest available snapshot.

The feature is implemented in the LVM driver, for
testing purposes.

APIImpact
DocImpact
Co-Authored-By: Ben Swartzlander <ben@swartzlander.org>
Co-Authored-By: Andrew Kerr <andrew.kerr@netapp.com>
Implements: blueprint manila-share-revert-to-snapshot
Change-Id: Id497e13070e0003db2db951526a52de6c2182cca
This commit is contained in:
Clinton Knight 2016-06-08 13:46:51 -07:00
parent 8bdf0d476d
commit d4a379d083
59 changed files with 2310 additions and 154 deletions

View File

@ -0,0 +1,5 @@
{
"revert": {
"snapshot_id": "6020af24-a305-4155-9a29-55e20efcb0e8"
}
}

View File

@ -326,3 +326,31 @@ Request example
.. literalinclude:: samples/share-actions-unmanage-request.json
:language: javascript
Revert share to snapshot
========================
.. rest_method:: POST /v2/{tenant_id}/shares/{share_id}/action
Reverts a share to the specified snapshot, which must be the most recent one
known to manila. This API is available in versions later than or equal to 2.27.
Normal response codes: 202
Error response codes: badRequest(400), unauthorized(401), forbidden(403),
itemNotFound(404), conflict(409)
Request
-------
.. rest_parameters:: parameters.yaml
- snapshot_id: snapshot_id
- share_id: share_id
- tenant_id: tenant_id_path
Request example
---------------
.. literalinclude:: samples/share-actions-revert-to-snapshot-request.json
:language: javascript

View File

@ -64,6 +64,10 @@ A share has one of these status values:
+----------------------------------------+--------------------------------------------------------+
| ``shrinking_possible_data_loss_error`` | Shrink share failed due to possible data loss. |
+----------------------------------------+--------------------------------------------------------+
| ``reverting`` | Share is being reverted to a snapshot. |
+----------------------------------------+--------------------------------------------------------+
| ``reverting_error`` | Share revert to snapshot failed. |
+----------------------------------------+--------------------------------------------------------+
List shares

View File

@ -8,7 +8,8 @@ Use the shared file service to make snapshots of shares. A share
snapshot is a point-in-time, read-only copy of the data that is
contained in a share. You can create, manage, update, and delete
share snapshots. After you create or manage a share snapshot, you
can create a share from it.
can create a share from it. You can also revert a share to its most
recent snapshot.
You can update a share snapshot to rename it, change its
description, or update its state to one of these supported states:
@ -31,6 +32,8 @@ description, or update its state to one of these supported states:
- ``unmanage_error``
- ``restoring``
As administrator, you can also reset the state of a snapshot and
force-delete a share snapshot in any state. Use the ``policy.json``
file to grant permissions for these actions to other roles.

View File

@ -69,6 +69,7 @@ PASSWORD_FOR_SAMBA_USER=${PASSWORD_FOR_SAMBA_USER:-$USERNAME_FOR_USER_RULES}
RUN_MANILA_QUOTA_TESTS=${RUN_MANILA_QUOTA_TESTS:-True}
RUN_MANILA_SHRINK_TESTS=${RUN_MANILA_SHRINK_TESTS:-True}
RUN_MANILA_SNAPSHOT_TESTS=${RUN_MANILA_SNAPSHOT_TESTS:-True}
RUN_MANILA_REVERT_TO_SNAPSHOT_TESTS=${RUN_MANILA_REVERT_TO_SNAPSHOT_TESTS:-False}
RUN_MANILA_CG_TESTS=${RUN_MANILA_CG_TESTS:-True}
RUN_MANILA_MANAGE_TESTS=${RUN_MANILA_MANAGE_TESTS:-True}
RUN_MANILA_MANAGE_SNAPSHOT_TESTS=${RUN_MANILA_MANAGE_SNAPSHOT_TESTS:-False}
@ -164,6 +165,7 @@ if [[ "$DRIVER" == "lvm" ]]; then
RUN_MANILA_MANAGE_TESTS=False
RUN_MANILA_HOST_ASSISTED_MIGRATION_TESTS=True
RUN_MANILA_SHRINK_TESTS=False
RUN_MANILA_REVERT_TO_SNAPSHOT_TESTS=True
iniset $TEMPEST_CONFIG share enable_ip_rules_for_protocols 'nfs'
iniset $TEMPEST_CONFIG share enable_user_rules_for_protocols 'cifs'
iniset $TEMPEST_CONFIG share image_with_share_tools 'manila-service-image-master'
@ -207,6 +209,7 @@ elif [[ "$DRIVER" == "dummy" ]]; then
RUN_MANILA_CG_TESTS=True
RUN_MANILA_MANAGE_TESTS=False
RUN_MANILA_DRIVER_ASSISTED_MIGRATION_TESTS=True
RUN_MANILA_REVERT_TO_SNAPSHOT_TESTS=True
iniset $TEMPEST_CONFIG share enable_ip_rules_for_protocols 'nfs'
iniset $TEMPEST_CONFIG share enable_user_rules_for_protocols 'cifs'
iniset $TEMPEST_CONFIG share enable_cert_rules_for_protocols ''
@ -243,6 +246,9 @@ iniset $TEMPEST_CONFIG share run_shrink_tests $RUN_MANILA_SHRINK_TESTS
# Enable snapshot tests
iniset $TEMPEST_CONFIG share run_snapshot_tests $RUN_MANILA_SNAPSHOT_TESTS
# Enable revert to snapshot tests
iniset $TEMPEST_CONFIG share run_revert_to_snapshot_tests $RUN_MANILA_REVERT_TO_SNAPSHOT_TESTS
# Enable consistency group tests
iniset $TEMPEST_CONFIG share run_consistency_group_tests $RUN_MANILA_CG_TESTS
@ -262,6 +268,10 @@ iniset $TEMPEST_CONFIG share run_driver_assisted_migration_tests $RUN_MANILA_DRI
# Create share from snapshot support
iniset $TEMPEST_CONFIG share capability_create_share_from_snapshot_support $CAPABILITY_CREATE_SHARE_FROM_SNAPSHOT_SUPPORT
# Revert share to snapshot support
CAPABILITY_REVERT_TO_SNAPSHOT_SUPPORT=${CAPABILITY_REVERT_TO_SNAPSHOT_SUPPORT:-$RUN_MANILA_REVERT_TO_SNAPSHOT_TESTS}
iniset $TEMPEST_CONFIG share capability_revert_to_snapshot_support $CAPABILITY_REVERT_TO_SNAPSHOT_SUPPORT
iniset $TEMPEST_CONFIG validation ip_version_for_ssh 4
iniset $TEMPEST_CONFIG validation network_for_ssh ${PRIVATE_NETWORK_NAME:-"private"}

View File

@ -99,6 +99,7 @@ elif [[ "$DRIVER" == "windows" ]]; then
save_configuration "SHARE_DRIVER" "manila.share.drivers.windows.windows_smb_driver.WindowsSMBDriver"
elif [[ "$DRIVER" == "dummy" ]]; then
driver_path="manila.tests.share.drivers.dummy.DummyDriver"
DEFAULT_EXTRA_SPECS="'snapshot_support=True create_share_from_snapshot_support=True revert_to_snapshot_support=True'"
save_configuration "MANILA_SERVICE_IMAGE_ENABLED" "False"
save_configuration "SHARE_DRIVER" "$driver_path"
save_configuration "SUPPRESS_ERRORS_IN_CLEANUP" "False"
@ -148,6 +149,7 @@ elif [[ "$DRIVER" == "dummy" ]]; then
elif [[ "$DRIVER" == "lvm" ]]; then
MANILA_SERVICE_IMAGE_ENABLED=True
DEFAULT_EXTRA_SPECS="'snapshot_support=True create_share_from_snapshot_support=True revert_to_snapshot_support=True'"
save_configuration "SHARE_DRIVER" "manila.share.drivers.lvm.LVMShareDriver"
save_configuration "SHARE_BACKING_FILE_SIZE" "32000M"
elif [[ "$DRIVER" == "zfsonlinux" ]]; then

View File

@ -171,6 +171,12 @@ can be used verbatim as extra_specs in share types used to create shares.
type in pools without regard for whether creating shares from snapshots is
supported, and those shares will not support creating shares from snapshots.
* `revert_to_snapshot_support` - indicates that a driver is capable of
reverting a share in place to its most recent snapshot. When administrators
do not set this capability as an extra-spec in a share type, the scheduler
can place new shares of that type in pools without regard for whether
reverting shares to snapshots is supported, and those shares will not support
reverting shares to snapshots.
Reporting Capabilities
----------------------
@ -210,6 +216,7 @@ example vendor prefix:
'compression': True, #
'snapshot_support': True, #
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': True,
'qos': True, # this backend supports QoS
'thin_provisioning': True, #
'max_over_subscription_ratio': 10, # (mandatory for thin)
@ -238,6 +245,7 @@ example vendor prefix:
# allow creating
# shares from
# snapshots
'revert_to_snapshot_support': True,
'reserved_percentage': 0,
'dedupe': False,
'compression': False,

View File

@ -30,57 +30,58 @@ Column value "-" means that this feature is not currently supported.
Mapping of share drivers and share features support
---------------------------------------------------
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Driver name | create delete share | manage unmanage share | extend share | shrink share | create delete snapshot | create share from snapshot | manage unmanage snapshot |
+========================================+=======================+=======================+==============+==============+========================+============================+==========================+
| ZFSonLinux | M | N | M | M | M | M | N |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Container | N | \- | N | \- | \- | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Generic (Cinder as back-end) | J | K | L | L | J | J | M |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| NetApp Clustered Data ONTAP | J | L | L | L | J | J | N |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| EMC VNX | J | \- | \- | \- | J | J | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| EMC Unity | N | \- | N | \- | N | N | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| EMC Isilon | K | \- | M | \- | K | K | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Red Hat GlusterFS | J | \- | \- | \- | volume layout (L) | volume layout (L) | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Red Hat GlusterFS-Native | J | \- | \- | \- | K | L | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| HDFS | K | \- | M | \- | K | K | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Hitachi HNAS | L | L | L | M | L | L | O |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Hitachi HSP | N | N | N | N | \- | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| HPE 3PAR | K | \- | \- | \- | K | K | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Huawei | K | L | L | L | K | M | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| IBM GPFS | K | O | L | \- | K | K | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| LVM | M | \- | M | \- | M | M | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Quobyte | K | \- | M | M | \- | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Windows SMB | L | L | L | L | L | L | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Oracle ZFSSA | K | N | M | M | K | K | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| CephFS Native | M | \- | M | M | M | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Tegile | M | \- | M | M | M | M | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| NexentaStor4 | N | \- | N | \- | N | N | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| NexentaStor5 | N | \- | N | N | N | N | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| MapRFS | O | O | O | O | O | O | O |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| Driver name | create delete share | manage unmanage share | extend share | shrink share | create delete snapshot | create share from snapshot | manage unmanage snapshot | revert to snapshot |
+========================================+=======================+=======================+==============+==============+========================+============================+==========================+====================+
| ZFSonLinux | M | N | M | M | M | M | N | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| Container | N | \- | N | \- | \- | \- | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| Generic (Cinder as back-end) | J | K | L | L | J | J | M | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| NetApp Clustered Data ONTAP | J | L | L | L | J | J | N | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| EMC VNX | J | \- | \- | \- | J | J | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| EMC Unity | N | \- | N | \- | N | N | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| EMC Isilon | K | \- | M | \- | K | K | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| Red Hat GlusterFS | J | \- | \- | \- | volume layout (L) | volume layout (L) | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| Red Hat GlusterFS-Native | J | \- | \- | \- | K | L | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| HDFS | K | \- | M | \- | K | K | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| Hitachi HNAS | L | L | L | M | L | L | O | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| Hitachi HSP | N | N | N | N | \- | \- | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| HPE 3PAR | K | \- | \- | \- | K | K | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| Huawei | K | L | L | L | K | M | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| IBM GPFS | K | O | L | \- | K | K | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| LVM | M | \- | M | \- | M | M | \- | O |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| Quobyte | K | \- | M | M | \- | \- | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| Windows SMB | L | L | L | L | L | L | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| Oracle ZFSSA | K | N | M | M | K | K | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| CephFS Native | M | \- | M | M | M | \- | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| Tegile | M | \- | M | M | M | M | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| NexentaStor4 | N | \- | N | \- | N | N | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| NexentaStor5 | N | \- | N | N | N | N | \- | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
| MapRFS | O | O | O | O | O | O | O | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
Mapping of share drivers and share access rules support
-------------------------------------------------------
@ -200,57 +201,57 @@ Mapping of share drivers and common capabilities
More information: :ref:`capabilities_and_extra_specs`
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| Driver name | DHSS=True | DHSS=False | dedupe | compression | thin_provisioning | thick_provisioning | qos | create share from snapshot |
+========================================+===========+============+========+=============+===================+====================+=====+============================+
| ZFSonLinux | \- | M | M | M | M | \- | \- | M |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| Container | N | \- | \- | \- | \- | N | \- | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| Generic (Cinder as back-end) | J | K | \- | \- | \- | L | \- | J |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| NetApp Clustered Data ONTAP | J | K | M | M | M | L | \- | J |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| EMC VNX | J | \- | \- | \- | \- | L | \- | J |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| EMC Unity | N | \- | \- | \- | N | \- | \- | N |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| EMC Isilon | \- | K | \- | \- | \- | L | \- | K |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| Red Hat GlusterFS | \- | J | \- | \- | \- | L | \- | volume layout (L) |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| Red Hat GlusterFS-Native | \- | J | \- | \- | \- | L | \- | L |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| HDFS | \- | K | \- | \- | \- | L | \- | K |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| Hitachi HNAS | \- | L | N | \- | L | \- | \- | L |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| Hitachi HSP | \- | N | \- | \- | N | \- | \- | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| HPE 3PAR | L | K | L | \- | L | L | \- | K |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| Huawei | M | K | L | L | L | L | M | M |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| LVM | \- | M | \- | \- | \- | M | \- | K |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| Quobyte | \- | K | \- | \- | \- | L | \- | M |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| Windows SMB | L | L | \- | \- | \- | L | \- | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| IBM GPFS | \- | K | \- | \- | \- | L | \- | L |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| Oracle ZFSSA | \- | K | \- | \- | \- | L | \- | K |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| CephFS Native | \- | M | \- | \- | \- | M | \- | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| Tegile | \- | M | M | M | M | \- | \- | M |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| NexentaStor4 | \- | N | N | N | N | N | \- | N |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| NexentaStor5 | \- | N | N | N | N | N | \- | N |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
| MapRFS | \- | N | \- | \- | \- | N | \- | O |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| Driver name | DHSS=True | DHSS=False | dedupe | compression | thin_provisioning | thick_provisioning | qos | create share from snapshot | revert to snapshot |
+========================================+===========+============+========+=============+===================+====================+=====+============================+====================+
| ZFSonLinux | \- | M | M | M | M | \- | \- | M | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| Container | N | \- | \- | \- | \- | N | \- | \- | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| Generic (Cinder as back-end) | J | K | \- | \- | \- | L | \- | J | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| NetApp Clustered Data ONTAP | J | K | M | M | M | L | \- | J | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| EMC VNX | J | \- | \- | \- | \- | L | \- | J | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| EMC Unity | N | \- | \- | \- | N | \- | \- | N | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| EMC Isilon | \- | K | \- | \- | \- | L | \- | K | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| Red Hat GlusterFS | \- | J | \- | \- | \- | L | \- | volume layout (L) | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| Red Hat GlusterFS-Native | \- | J | \- | \- | \- | L | \- | L | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| HDFS | \- | K | \- | \- | \- | L | \- | K | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| Hitachi HNAS | \- | L | N | \- | L | \- | \- | L | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| Hitachi HSP | \- | N | \- | \- | N | \- | \- | \- | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| HPE 3PAR | L | K | L | \- | L | L | \- | K | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| Huawei | M | K | L | L | L | L | M | M | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| LVM | \- | M | \- | \- | \- | M | \- | K | O |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| Quobyte | \- | K | \- | \- | \- | L | \- | M | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| Windows SMB | L | L | \- | \- | \- | L | \- | \- | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| IBM GPFS | \- | K | \- | \- | \- | L | \- | L | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| Oracle ZFSSA | \- | K | \- | \- | \- | L | \- | K | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| CephFS Native | \- | M | \- | \- | \- | M | \- | \- | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| Tegile | \- | M | M | M | M | \- | \- | M | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| NexentaStor4 | \- | N | N | N | N | N | \- | N | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| NexentaStor5 | \- | N | N | N | N | N | \- | N | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
| MapRFS | \- | N | \- | \- | \- | N | \- | O | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
.. note::

View File

@ -41,6 +41,7 @@
"share:unmanage": "rule:admin_api",
"share:force_delete": "rule:admin_api",
"share:reset_status": "rule:admin_api",
"share:revert_to_snapshot": "rule:default",
"share_export_location:index": "rule:default",
"share_export_location:show": "rule:default",

View File

@ -171,5 +171,12 @@ brctl: CommandFilter, brctl, root
# manila/share/drivers/container/container.py: e2fsck <whatever>
e2fsck: CommandFilter, e2fsck, root
# manila/share/drivers/lvm.py: lvconvert --merge %s
lvconvert: CommandFilter, lvconvert, root
# manila/share/drivers/lvm.py: lvchange -an %s
# manila/share/drivers/lvm.py: lvchange -ay %s
lvchange: CommandFilter, lvchange, root
# manila/data/utils.py: 'sha256sum', '%s'
sha256sum: CommandFilter, sha256sum, root

View File

@ -84,13 +84,15 @@ REST_API_VERSION_HISTORY = """
spec. Also made the 'snapshot_support' extra spec optional.
* 2.25 - Added quota-show detail API.
* 2.26 - Removed 'nova_net_id' parameter from share_network API.
* 2.27 - Added share revert to snapshot API.
"""
# The minimum and maximum versions of the API supported
# The default api version request is defined to be the
# minimum version of the API supported.
_MIN_API_VERSION = "2.0"
_MAX_API_VERSION = "2.26"
_MAX_API_VERSION = "2.27"
DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -156,3 +156,10 @@ user documentation.
----
Removed nova-net plugin support and removed 'nova_net_id' parameter from
share_network API.
2.27
----
Added share revert to snapshot. This API reverts a share to the specified
snapshot. The share is reverted in place, and the snapshot must be the most
recent one known to manila. The feature is controlled by a new standard
optional extra spec, revert_to_snapshot_support.

View File

@ -267,8 +267,10 @@ class ShareMixin(object):
# Verify that share can be created from a snapshot
if (check_create_share_from_snapshot_support and
not parent_share['create_share_from_snapshot_support']):
msg = _("Share cannot be created from snapshot '%s', because "
"share back end does not support it.") % snapshot_id
msg = (_("A new share may not be created from snapshot '%s', "
"because the snapshot's parent share does not have "
"that capability.")
% snapshot_id)
LOG.error(msg)
raise exc.HTTPBadRequest(explanation=msg)

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log
import six
import webob
from webob import exc
@ -25,12 +26,15 @@ from manila.api.v1 import shares
from manila.api.views import share_accesses as share_access_views
from manila.api.views import share_migration as share_migration_views
from manila.api.views import shares as share_views
from manila.common import constants
from manila import db
from manila import exception
from manila.i18n import _
from manila.i18n import _, _LI
from manila import share
from manila import utils
LOG = log.getLogger(__name__)
class ShareController(shares.ShareMixin,
share_manage.ShareManageMixin,
@ -47,6 +51,118 @@ class ShareController(shares.ShareMixin,
self._access_view_builder = share_access_views.ViewBuilder()
self._migration_view_builder = share_migration_views.ViewBuilder()
@wsgi.Controller.authorize('revert_to_snapshot')
def _revert(self, req, id, body=None):
"""Revert a share to a snapshot."""
context = req.environ['manila.context']
revert_data = self._validate_revert_parameters(context, body)
try:
share_id = id
snapshot_id = revert_data['snapshot_id']
share = self.share_api.get(context, share_id)
snapshot = self.share_api.get_snapshot(context, snapshot_id)
# Ensure share supports reverting to a snapshot
if not share['revert_to_snapshot_support']:
msg_args = {'share_id': share_id, 'snap_id': snapshot_id}
msg = _('Share %(share_id)s may not be reverted to snapshot '
'%(snap_id)s, because the share does not have that '
'capability.')
raise exc.HTTPBadRequest(explanation=msg % msg_args)
# Ensure requested share & snapshot match.
if share['id'] != snapshot['share_id']:
msg_args = {'share_id': share_id, 'snap_id': snapshot_id}
msg = _('Snapshot %(snap_id)s is not associated with share '
'%(share_id)s.')
raise exc.HTTPBadRequest(explanation=msg % msg_args)
# Ensure share status is 'available'.
if share['status'] != constants.STATUS_AVAILABLE:
msg_args = {
'share_id': share_id,
'state': share['status'],
'available': constants.STATUS_AVAILABLE,
}
msg = _("Share %(share_id)s is in '%(state)s' state, but it "
"must be in '%(available)s' state to be reverted to a "
"snapshot.")
raise exc.HTTPConflict(explanation=msg % msg_args)
# Ensure snapshot status is 'available'.
if snapshot['status'] != constants.STATUS_AVAILABLE:
msg_args = {
'snap_id': snapshot_id,
'state': snapshot['status'],
'available': constants.STATUS_AVAILABLE,
}
msg = _("Snapshot %(snap_id)s is in '%(state)s' state, but it "
"must be in '%(available)s' state to be restored.")
raise exc.HTTPConflict(explanation=msg % msg_args)
# Ensure a long-running task isn't active on the share
if share.is_busy:
msg_args = {'share_id': share_id}
msg = _("Share %(share_id)s may not be reverted while it has "
"an active task.")
raise exc.HTTPConflict(explanation=msg % msg_args)
# Ensure the snapshot is the most recent one.
latest_snapshot = self.share_api.get_latest_snapshot_for_share(
context, share_id)
if not latest_snapshot:
msg_args = {'share_id': share_id}
msg = _("Could not determine the latest snapshot for share "
"%(share_id)s.")
raise exc.HTTPBadRequest(explanation=msg % msg_args)
if latest_snapshot['id'] != snapshot_id:
msg_args = {
'share_id': share_id,
'snap_id': snapshot_id,
'latest_snap_id': latest_snapshot['id'],
}
msg = _("Snapshot %(snap_id)s may not be restored because "
"it is not the most recent snapshot of share "
"%(share_id)s. Currently the latest snapshot is "
"%(latest_snap_id)s.")
raise exc.HTTPConflict(explanation=msg % msg_args)
msg_args = {'share_id': share_id, 'snap_id': snapshot_id}
msg = _LI('Reverting share %(share_id)s to snapshot %(snap_id)s.')
LOG.info(msg, msg_args)
self.share_api.revert_to_snapshot(context, snapshot)
except exception.ShareNotFound as e:
raise exc.HTTPNotFound(explanation=six.text_type(e))
except exception.ShareSnapshotNotFound as e:
raise exc.HTTPBadRequest(explanation=six.text_type(e))
except exception.ShareSizeExceedsAvailableQuota as e:
raise exc.HTTPForbidden(explanation=six.text_type(e))
except exception.ReplicationException as e:
raise exc.HTTPBadRequest(explanation=six.text_type(e))
return webob.Response(status_int=202)
def _validate_revert_parameters(self, context, body):
if not (body and self.is_valid_body(body, 'revert')):
msg = _("Revert entity not found in request body.")
raise exc.HTTPBadRequest(explanation=msg)
required_parameters = ('snapshot_id',)
data = body['revert']
for parameter in required_parameters:
if parameter not in data:
msg = _("Required parameter %s not found.") % parameter
raise exc.HTTPBadRequest(explanation=msg)
if not data.get(parameter):
msg = _("Required parameter %s is empty.") % parameter
raise exc.HTTPBadRequest(explanation=msg)
return data
@wsgi.Controller.api_version("2.0", "2.3")
def create(self, req, body):
# Remove consistency group attributes
@ -276,6 +392,11 @@ class ShareController(shares.ShareMixin,
def unmanage(self, req, id, body=None):
return self._unmanage(req, id, body)
@wsgi.Controller.api_version('2.27')
@wsgi.action('revert')
def revert(self, req, id, body=None):
return self._revert(req, id, body)
def create_resource():
return wsgi.Resource(ShareController())

View File

@ -30,6 +30,7 @@ class ViewBuilder(common.ViewBuilder):
"add_replication_fields",
"add_user_id",
"add_create_share_from_snapshot_support_field",
"add_revert_to_snapshot_support_field",
]
def summary_list(self, request, shares):
@ -148,6 +149,11 @@ class ViewBuilder(common.ViewBuilder):
share_dict['create_share_from_snapshot_support'] = share.get(
'create_share_from_snapshot_support')
@common.ViewBuilder.versioned_method("2.27")
def add_revert_to_snapshot_support_field(self, context, share_dict, share):
share_dict['revert_to_snapshot_support'] = share.get(
'revert_to_snapshot_support')
def _list_view(self, func, request, shares):
"""Provide a view for a list of shares."""
shares_list = [func(request, share)['share'] for share in shares]

View File

@ -40,6 +40,9 @@ STATUS_SHRINKING_POSSIBLE_DATA_LOSS_ERROR = (
'shrinking_possible_data_loss_error'
)
STATUS_REPLICATION_CHANGE = 'replication_change'
STATUS_RESTORING = 'restoring'
STATUS_REVERTING = 'reverting'
STATUS_REVERTING_ERROR = 'reverting_error'
TASK_STATE_MIGRATION_STARTING = 'migration_starting'
TASK_STATE_MIGRATION_IN_PROGRESS = 'migration_in_progress'
@ -81,6 +84,7 @@ TRANSITIONAL_STATUSES = (
STATUS_MANAGING, STATUS_UNMANAGING,
STATUS_EXTENDING, STATUS_SHRINKING,
STATUS_MIGRATING, STATUS_MIGRATING_TO,
STATUS_RESTORING, STATUS_REVERTING,
)
UPDATING_RULES_STATUSES = (
@ -161,6 +165,7 @@ class ExtraSpecs(object):
SNAPSHOT_SUPPORT = "snapshot_support"
REPLICATION_TYPE_SPEC = "replication_type"
CREATE_SHARE_FROM_SNAPSHOT_SUPPORT = "create_share_from_snapshot_support"
REVERT_TO_SNAPSHOT_SUPPORT = "revert_to_snapshot_support"
# Extra specs containers
REQUIRED = (
@ -170,6 +175,7 @@ class ExtraSpecs(object):
OPTIONAL = (
SNAPSHOT_SUPPORT,
CREATE_SHARE_FROM_SNAPSHOT_SUPPORT,
REVERT_TO_SNAPSHOT_SUPPORT,
REPLICATION_TYPE_SPEC,
)
@ -182,6 +188,7 @@ class ExtraSpecs(object):
DRIVER_HANDLES_SHARE_SERVERS,
SNAPSHOT_SUPPORT,
CREATE_SHARE_FROM_SNAPSHOT_SUPPORT,
REVERT_TO_SNAPSHOT_SUPPORT,
)
# NOTE(cknight): Some extra specs are optional, but a nominal (typically
@ -190,6 +197,7 @@ class ExtraSpecs(object):
INFERRED_OPTIONAL_MAP = {
SNAPSHOT_SUPPORT: False,
CREATE_SHARE_FROM_SNAPSHOT_SUPPORT: False,
REVERT_TO_SNAPSHOT_SUPPORT: False,
}
REPLICATION_TYPES = ('writable', 'readable', 'dr')

View File

@ -530,6 +530,11 @@ def share_snapshot_get_all_for_share(context, share_id, filters=None,
)
def share_snapshot_get_latest_for_share(context, share_id):
"""Get the most recent snapshot for a share."""
return IMPL.share_snapshot_get_latest_for_share(context, share_id)
def share_snapshot_update(context, snapshot_id, values):
"""Set the given properties on an snapshot and update it.

View File

@ -0,0 +1,66 @@
# Copyright (c) 2016 Clinton Knight. 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.
"""add_revert_to_snapshot_support
Revision ID: 87ce15c59bbe
Revises: 3e7d62517afa
Create Date: 2016-08-18 00:12:34.587018
"""
# revision identifiers, used by Alembic.
revision = '87ce15c59bbe'
down_revision = '95e3cf760840'
from alembic import op
import sqlalchemy as sa
def upgrade():
"""Performs DB upgrade to add revert_to_snapshot_support.
Add attribute 'revert_to_snapshot_support' to Share model.
"""
session = sa.orm.Session(bind=op.get_bind().connect())
# Add create_share_from_snapshot_support attribute to shares table
op.add_column(
'shares',
sa.Column('revert_to_snapshot_support', sa.Boolean, default=False))
# Set revert_to_snapshot_support on each share
shares_table = sa.Table(
'shares',
sa.MetaData(),
sa.Column('id', sa.String(length=36)),
sa.Column('deleted', sa.String(length=36)),
sa.Column('revert_to_snapshot_support', sa.Boolean),
)
update = shares_table.update().where(
shares_table.c.deleted == 'False').values(
revert_to_snapshot_support=False)
session.execute(update)
session.commit()
session.close_all()
def downgrade():
"""Performs DB downgrade removing revert_to_snapshot_support.
Remove attribute 'revert_to_snapshot_support' from Share model.
"""
op.drop_column('shares', 'revert_to_snapshot_support')

View File

@ -2192,6 +2192,14 @@ def share_snapshot_get_all_for_share(context, share_id, filters=None,
)
@require_context
def share_snapshot_get_latest_for_share(context, share_id):
snapshots = _share_snapshot_get_all_with_filters(
context, share_id=share_id, sort_key='created_at', sort_dir='desc')
return snapshots[0] if snapshots else None
@require_context
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
def share_snapshot_update(context, snapshot_id, values):

View File

@ -264,7 +264,8 @@ class Share(BASE, ManilaBase):
# preferred.
result = None
if len(self.instances) > 0:
order = (constants.STATUS_REPLICATION_CHANGE,
order = (constants.STATUS_REVERTING,
constants.STATUS_REPLICATION_CHANGE,
constants.STATUS_MIGRATING, constants.STATUS_AVAILABLE,
constants.STATUS_ERROR)
other_statuses = (
@ -303,6 +304,7 @@ class Share(BASE, ManilaBase):
snapshot_id = Column(String(36))
snapshot_support = Column(Boolean, default=True)
create_share_from_snapshot_support = Column(Boolean, default=True)
revert_to_snapshot_support = Column(Boolean, default=False)
replication_type = Column(String(255), nullable=True)
share_proto = Column(String(255))
is_public = Column(Boolean, default=False)

View File

@ -131,6 +131,7 @@ class HostState(object):
self.driver_handles_share_servers = False
self.snapshot_support = True
self.create_share_from_snapshot_support = True
self.revert_to_snapshot_support = False
self.consistency_group_support = False
self.dedupe = False
self.compression = False
@ -299,6 +300,10 @@ class HostState(object):
pool_cap['create_share_from_snapshot_support'] = (
self.create_share_from_snapshot_support)
if 'revert_to_snapshot_support' not in pool_cap:
pool_cap['revert_to_snapshot_support'] = (
self.revert_to_snapshot_support)
if not pool_cap.get('consistency_group_support'):
pool_cap['consistency_group_support'] = \
self.consistency_group_support
@ -325,6 +330,8 @@ class HostState(object):
self.snapshot_support = capability.get('snapshot_support')
self.create_share_from_snapshot_support = capability.get(
'create_share_from_snapshot_support')
self.revert_to_snapshot_support = capability.get(
'revert_to_snapshot_support', False)
self.consistency_group_support = capability.get(
'consistency_group_support', False)
self.updated = capability['timestamp']

View File

@ -46,6 +46,7 @@ def generate_stats(host_state, properties):
'snapshot_support': host_state.snapshot_support,
'create_share_from_snapshot_support':
host_state.create_share_from_snapshot_support,
'revert_to_snapshot_support': host_state.revert_to_snapshot_support,
'replication_domain': host_state.replication_domain,
'replication_type': host_state.replication_type,
'provisioned_capacity_gb': host_state.provisioned_capacity_gb,

View File

@ -266,40 +266,49 @@ class API(base.Base):
"""
inferred_map = constants.ExtraSpecs.INFERRED_OPTIONAL_MAP
snapshot_support_default = inferred_map.get(
constants.ExtraSpecs.SNAPSHOT_SUPPORT)
create_share_from_snapshot_support_default = inferred_map.get(
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT)
snapshot_support_key = constants.ExtraSpecs.SNAPSHOT_SUPPORT
create_share_from_snapshot_key = (
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT)
revert_to_snapshot_key = (
constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT)
try:
if share_type:
snapshot_support = share_types.parse_boolean_extra_spec(
constants.ExtraSpecs.SNAPSHOT_SUPPORT,
snapshot_support_default = inferred_map.get(snapshot_support_key)
create_share_from_snapshot_support_default = inferred_map.get(
create_share_from_snapshot_key)
revert_to_snapshot_support_default = inferred_map.get(
revert_to_snapshot_key)
if share_type:
snapshot_support = share_types.parse_boolean_extra_spec(
snapshot_support_key,
share_type.get('extra_specs', {}).get(
snapshot_support_key, snapshot_support_default))
create_share_from_snapshot_support = (
share_types.parse_boolean_extra_spec(
create_share_from_snapshot_key,
share_type.get('extra_specs', {}).get(
constants.ExtraSpecs.SNAPSHOT_SUPPORT,
snapshot_support_default))
create_share_from_snapshot_support = (
share_types.parse_boolean_extra_spec(
create_share_from_snapshot_key, share_type.get(
'extra_specs', {}).get(
create_share_from_snapshot_key,
create_share_from_snapshot_support_default)))
replication_type = share_type.get('extra_specs', {}).get(
'replication_type')
else:
snapshot_support = snapshot_support_default
create_share_from_snapshot_support = (
create_share_from_snapshot_support_default)
replication_type = None
except Exception as e:
raise exception.InvalidParameterValue(six.text_type(e))
create_share_from_snapshot_key,
create_share_from_snapshot_support_default)))
revert_to_snapshot_support = (
share_types.parse_boolean_extra_spec(
revert_to_snapshot_key,
share_type.get('extra_specs', {}).get(
revert_to_snapshot_key,
revert_to_snapshot_support_default)))
replication_type = share_type.get('extra_specs', {}).get(
'replication_type')
else:
snapshot_support = snapshot_support_default
create_share_from_snapshot_support = (
create_share_from_snapshot_support_default)
revert_to_snapshot_support = revert_to_snapshot_support_default
replication_type = None
return {
'snapshot_support': snapshot_support,
'create_share_from_snapshot_support':
create_share_from_snapshot_support,
'revert_to_snapshot_support': revert_to_snapshot_support,
'replication_type': replication_type,
}
@ -382,6 +391,7 @@ class API(base.Base):
'snapshot_support': share['snapshot_support'],
'create_share_from_snapshot_support':
share['create_share_from_snapshot_support'],
'revert_to_snapshot_support': share['revert_to_snapshot_support'],
'share_proto': share['share_proto'],
'share_type_id': share_type_id,
'is_public': share['is_public'],
@ -614,6 +624,12 @@ class API(base.Base):
share_type.get('extra_specs', {}).get(
'create_share_from_snapshot_support')
),
'revert_to_snapshot_support': kwargs.get(
'revert_to_snapshot_support',
share_type.get('extra_specs', {}).get(
'revert_to_snapshot_support')
),
'share_proto': kwargs.get('share_proto', share.get('share_proto')),
'share_type_id': share_type['id'],
'is_public': kwargs.get('is_public', share.get('is_public')),
@ -713,6 +729,120 @@ class API(base.Base):
self.share_rpcapi.unmanage_snapshot(context, snapshot_ref, host)
def revert_to_snapshot(self, context, snapshot):
"""Revert a share to a snapshot."""
share = self.db.share_get(context, snapshot['share_id'])
reservations = self._handle_revert_to_snapshot_quotas(
context, share, snapshot)
try:
if share.get('has_replicas'):
self._revert_to_replicated_snapshot(
context, share, snapshot, reservations)
else:
self._revert_to_snapshot(
context, share, snapshot, reservations)
except Exception:
with excutils.save_and_reraise_exception():
if reservations:
QUOTAS.rollback(context, reservations)
def _handle_revert_to_snapshot_quotas(self, context, share, snapshot):
"""Reserve extra quota if a revert will result in a larger share."""
# Note(cknight): This value may be positive or negative.
size_increase = snapshot['size'] - share['size']
if not size_increase:
return None
try:
return QUOTAS.reserve(context,
project_id=share['project_id'],
gigabytes=size_increase,
user_id=share['user_id'])
except exception.OverQuota as exc:
usages = exc.kwargs['usages']
quotas = exc.kwargs['quotas']
consumed_gb = (usages['gigabytes']['reserved'] +
usages['gigabytes']['in_use'])
msg = _("Quota exceeded for %(s_pid)s. Reverting share "
"%(s_sid)s to snapshot %(s_ssid)s will increase the "
"share's size by %(s_size)sG, "
"(%(d_consumed)dG of %(d_quota)dG already consumed).")
msg_args = {
's_pid': context.project_id,
's_sid': share['id'],
's_ssid': snapshot['id'],
's_size': size_increase,
'd_consumed': consumed_gb,
'd_quota': quotas['gigabytes'],
}
message = msg % msg_args
LOG.error(message)
raise exception.ShareSizeExceedsAvailableQuota(message=message)
def _revert_to_snapshot(self, context, share, snapshot, reservations):
"""Revert a non-replicated share to a snapshot."""
# Set status of share to 'reverting'
self.db.share_update(
context, snapshot['share_id'],
{'status': constants.STATUS_REVERTING})
# Set status of snapshot to 'restoring'
self.db.share_snapshot_update(
context, snapshot['id'],
{'status': constants.STATUS_RESTORING})
# Send revert API to share host
self.share_rpcapi.revert_to_snapshot(
context, share, snapshot, share['instance']['host'], reservations)
def _revert_to_replicated_snapshot(self, context, share, snapshot,
reservations):
"""Revert a replicated share to a snapshot."""
# Get active replica
active_replica = self.db.share_replicas_get_available_active_replica(
context, share['id'])
if not active_replica:
msg = _('Share %s has no active replica in available state.')
raise exception.ReplicationException(reason=msg % share['id'])
# Get snapshot instance on active replica
snapshot_instance_filters = {
'share_instance_ids': active_replica['id'],
'snapshot_ids': snapshot['id'],
}
snapshot_instances = (
self.db.share_snapshot_instance_get_all_with_filters(
context, snapshot_instance_filters))
active_snapshot_instance = (
snapshot_instances[0] if snapshot_instances else None)
if not active_snapshot_instance:
msg = _('Share %(share)s has no snapshot %(snap)s associated with '
'its active replica.')
msg_args = {'share': share['id'], 'snap': snapshot['id']}
raise exception.ReplicationException(reason=msg % msg_args)
# Set active replica to 'reverting'
self.db.share_replica_update(
context, active_replica['id'],
{'status': constants.STATUS_REVERTING})
# Set snapshot instance on active replica to 'restoring'
self.db.share_snapshot_instance_update(
context, active_snapshot_instance['id'],
{'status': constants.STATUS_RESTORING})
# Send revert API to active replica host
self.share_rpcapi.revert_to_snapshot(
context, share, snapshot, active_replica['host'], reservations)
@policy.wrap_check_policy('share')
def delete(self, context, share, force=False):
"""Delete share."""
@ -1379,6 +1509,10 @@ class API(base.Base):
snapshots = results
return snapshots
def get_latest_snapshot_for_share(self, context, share_id):
"""Get the newest snapshot of a share."""
return self.db.share_snapshot_get_latest_for_share(context, share_id)
def allow_access(self, ctx, share, access_type, access_to,
access_level=None):
"""Allow access to share."""

View File

@ -855,6 +855,24 @@ class ShareDriver(object):
the failure.
"""
def revert_to_snapshot(self, context, snapshot, share_server=None):
"""Reverts a share (in place) to the specified snapshot.
Does not delete the share snapshot. The share and snapshot must both
be 'available' for the restore to be attempted. The snapshot must be
the most recent one taken by Manila; the API layer performs this check
so the driver doesn't have to.
The share must be reverted in place to the contents of the snapshot.
Application admins should quiesce or otherwise prepare the application
for the shared file system contents to change suddenly.
:param context: Current context
:param snapshot: The snapshot to be restored
:param share_server: Optional -- Share server model or None
"""
raise NotImplementedError()
def extend_share(self, share, new_size, share_server=None):
"""Extends size of existing share.
@ -957,6 +975,7 @@ class ShareDriver(object):
snapshot_support=self.snapshots_are_supported,
create_share_from_snapshot_support=(
self.creating_shares_from_snapshots_is_supported),
revert_to_snapshot_support=False,
replication_domain=self.replication_domain,
filter_function=self.get_filter_function(),
goodness_function=self.get_goodness_function(),
@ -1788,6 +1807,38 @@ class ShareDriver(object):
"""
raise NotImplementedError()
def revert_to_replicated_snapshot(self, context, active_replica,
replica_list, active_replica_snapshot,
replica_snapshots, share_server=None):
"""Reverts a replicated share (in place) to the specified snapshot.
.. note::
This call is made on the 'active' replica's host, since drivers may
not be able to revert snapshots on individual replicas.
Does not delete the share snapshot. The share and snapshot must both
be 'available' for the restore to be attempted. The snapshot must be
the most recent one taken by Manila; the API layer performs this check
so the driver doesn't have to.
The share must be reverted in place to the contents of the snapshot.
Application admins should quiesce or otherwise prepare the application
for the shared file system contents to change suddenly.
:param context: Current context
:param active_replica: The current active replica
:param replica_list: List of all replicas for a particular share
The 'active' replica will have its 'replica_state' attr set to
'active' and its 'status' set to 'reverting'.
:param active_replica_snapshot: snapshot to be restored
:param replica_snapshots: List of dictionaries of snapshot instances.
These snapshot instances track the snapshot across the replicas.
The snapshot of the active replica to be restored with have its
status attribute set to 'restoring'.
:param share_server: Optional -- Share server model
"""
raise NotImplementedError()
def delete_replicated_snapshot(self, context, replica_list,
replica_snapshots, share_server=None):
"""Delete a snapshot by deleting its instances across the replicas.

View File

@ -108,7 +108,7 @@ class LVMMixin(driver.ExecuteMixin):
raise
LOG.warning(_LW("Volume not found: %s") % exc.stderr)
def create_snapshot(self, context, snapshot, share_server=None):
def _create_snapshot(self, context, snapshot):
"""Creates a snapshot."""
orig_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group,
snapshot['share_name'])
@ -121,6 +121,9 @@ class LVMMixin(driver.ExecuteMixin):
'tune2fs', '-U', 'random', snapshot_device_name, run_as_root=True,
)
def create_snapshot(self, context, snapshot, share_server=None):
self._create_snapshot(context, snapshot)
def delete_snapshot(self, context, snapshot, share_server=None):
"""Deletes a snapshot."""
self._deallocate_container(snapshot['name'])
@ -188,6 +191,7 @@ class LVMShareDriver(LVMMixin, driver.ShareDriver):
'consistency_group_support': None,
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': True,
'driver_name': 'LVMShareDriver',
'pools': self.get_share_server_pools()
}
@ -356,3 +360,25 @@ class LVMShareDriver(LVMMixin, driver.ShareDriver):
device_name = self._get_local_path(share)
self._extend_container(share, device_name, new_size)
self._execute('resize2fs', device_name, run_as_root=True)
def revert_to_snapshot(self, context, snapshot, share_server=None):
# First we merge the snapshot LV and the share LV
# This won't actually do anything until the LV is reactivated
snap_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group,
snapshot['name'])
self._execute('lvconvert', '--merge', snap_lv_name, run_as_root=True)
# Unmount the share so we can deactivate it
share = snapshot['share']
self._unmount_device(share)
# Deactivate the share LV
share_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group,
share['name'])
self._execute('lvchange', '-an', share_lv_name, run_as_root=True)
# Reactivate the share LV. This will trigger the merge and delete the
# snapshot.
self._execute('lvchange', '-ay', share_lv_name, run_as_root=True)
# Now recreate the snapshot that was destroyed by the merge
self._create_snapshot(context, snapshot)
# Finally we can mount the share again
device_name = self._get_local_path(share)
self._mount_device(share, device_name)

View File

@ -188,7 +188,7 @@ def add_hooks(f):
class ShareManager(manager.SchedulerDependentManager):
"""Manages NAS storages."""
RPC_API_VERSION = '1.12'
RPC_API_VERSION = '1.13'
def __init__(self, share_driver=None, service_name=None, *args, **kwargs):
"""Load the driver from args, or from flags."""
@ -2140,6 +2140,76 @@ class ShareManager(manager.SchedulerDependentManager):
self.db.share_snapshot_instance_delete(
context, snapshot_instance['id'])
@add_hooks
@utils.require_driver_initialized
def revert_to_snapshot(self, context, snapshot_id, reservations,
share_id=None):
context = context.elevated()
snapshot = self.db.share_snapshot_get(context, snapshot_id)
share = snapshot['share']
share_id = share['id']
if share.get('has_replicas'):
self._revert_to_replicated_snapshot(
context, share, snapshot, reservations, share_id=share_id)
else:
self._revert_to_snapshot(context, share, snapshot, reservations)
def _revert_to_snapshot(self, context, share, snapshot, reservations):
share_server = self._get_share_server(context, share)
share_id = share['id']
snapshot_id = snapshot['id']
project_id = share['project_id']
user_id = share['user_id']
snapshot_instance = self.db.share_snapshot_instance_get(
context, snapshot.instance['id'], with_share_data=True)
# Make primitive to pass the information to the driver
snapshot_instance_dict = self._get_snapshot_instance_dict(
context, snapshot_instance, snapshot=snapshot)
try:
self.driver.revert_to_snapshot(context,
snapshot_instance_dict,
share_server=share_server)
except Exception:
with excutils.save_and_reraise_exception():
msg = _LE('Share %(share)s could not be reverted '
'to snapshot %(snap)s.')
msg_args = {'share': share_id, 'snap': snapshot_id}
LOG.exception(msg, msg_args)
if reservations:
QUOTAS.rollback(
context, reservations, project_id=project_id,
user_id=user_id)
self.db.share_update(
context, share_id,
{'status': constants.STATUS_REVERTING_ERROR})
self.db.share_snapshot_update(
context, snapshot_id,
{'status': constants.STATUS_AVAILABLE})
if reservations:
QUOTAS.commit(
context, reservations, project_id=project_id, user_id=user_id)
self.db.share_update(
context, share_id,
{'status': constants.STATUS_AVAILABLE, 'size': snapshot['size']})
self.db.share_snapshot_update(
context, snapshot_id, {'status': constants.STATUS_AVAILABLE})
msg = _LI('Share %(share)s reverted to snapshot %(snap)s '
'successfully.')
msg_args = {'share': share_id, 'snap': snapshot_id}
LOG.info(msg, msg_args)
@add_hooks
@utils.require_driver_initialized
def delete_share_instance(self, context, share_instance_id, force=False):
@ -2359,6 +2429,90 @@ class ShareManager(manager.SchedulerDependentManager):
self.db.share_snapshot_instance_update(
context, instance['id'], instance)
def _find_active_replica_on_host(self, replica_list):
"""Find the active replica matching this manager's host."""
for replica in replica_list:
if (replica['replica_state'] == constants.REPLICA_STATE_ACTIVE and
share_utils.extract_host(replica['host']) == self.host):
return replica
@locked_share_replica_operation
def _revert_to_replicated_snapshot(self, context, share, snapshot,
reservations, share_id=None):
share_server = self._get_share_server(context, share)
snapshot_id = snapshot['id']
project_id = share['project_id']
user_id = share['user_id']
# Get replicas, including an active replica
replica_list = self.db.share_replicas_get_all_by_share(
context, share_id, with_share_data=True, with_share_server=True)
active_replica = self._find_active_replica_on_host(replica_list)
# Get snapshot instances, including one on an active replica
replica_snapshots = (
self.db.share_snapshot_instance_get_all_with_filters(
context, {'snapshot_ids': snapshot_id},
with_share_data=True))
snapshot_instance_filters = {
'share_instance_ids': active_replica['id'],
'snapshot_ids': snapshot_id,
}
active_replica_snapshot = (
self.db.share_snapshot_instance_get_all_with_filters(
context, snapshot_instance_filters))[0]
# Make primitives to pass the information to the driver
replica_list = [self._get_share_replica_dict(context, replica)
for replica in replica_list]
active_replica = self._get_share_replica_dict(context, active_replica)
replica_snapshots = [self._get_snapshot_instance_dict(context, s)
for s in replica_snapshots]
active_replica_snapshot = self._get_snapshot_instance_dict(
context, active_replica_snapshot, snapshot=snapshot)
try:
self.driver.revert_to_replicated_snapshot(
context, active_replica, replica_list, active_replica_snapshot,
replica_snapshots, share_server=share_server)
except Exception:
with excutils.save_and_reraise_exception():
msg = _LE('Share %(share)s could not be reverted '
'to snapshot %(snap)s.')
msg_args = {'share': share_id, 'snap': snapshot_id}
LOG.exception(msg, msg_args)
if reservations:
QUOTAS.rollback(
context, reservations, project_id=project_id,
user_id=user_id)
self.db.share_replica_update(
context, active_replica['id'],
{'status': constants.STATUS_REVERTING_ERROR})
self.db.share_snapshot_instance_update(
context, active_replica_snapshot['id'],
{'status': constants.STATUS_AVAILABLE})
if reservations:
QUOTAS.commit(
context, reservations, project_id=project_id, user_id=user_id)
self.db.share_update(context, share_id, {'size': snapshot['size']})
self.db.share_replica_update(
context, active_replica['id'],
{'status': constants.STATUS_AVAILABLE})
self.db.share_snapshot_instance_update(
context, active_replica_snapshot['id'],
{'status': constants.STATUS_AVAILABLE})
msg = _LI('Share %(share)s reverted to snapshot %(snap)s '
'successfully.')
msg_args = {'share': share_id, 'snap': snapshot_id}
LOG.info(msg, msg_args)
@add_hooks
@utils.require_driver_initialized
@locked_share_replica_operation
@ -3236,7 +3390,8 @@ class ShareManager(manager.SchedulerDependentManager):
return share_replica_ref
def _get_snapshot_instance_dict(self, context, snapshot_instance):
def _get_snapshot_instance_dict(self, context, snapshot_instance,
snapshot=None):
# TODO(gouthamr): remove method when the db layer returns primitives
snapshot_instance_ref = {
'name': snapshot_instance.get('name'),
@ -3255,4 +3410,9 @@ class ShareManager(manager.SchedulerDependentManager):
'provider_location': snapshot_instance.get('provider_location'),
}
if snapshot:
snapshot_instance_ref.update({
'size': snapshot.get('size'),
})
return snapshot_instance_ref

View File

@ -64,6 +64,7 @@ class ShareAPI(object):
update migration_cancel(), migration_complete() and
migration_get_progress method signature, rename
migration_get_info() to connection_get_info()
1.13 - Introduce share revert to snapshot: revert_to_snapshot()
"""
BASE_RPC_API_VERSION = '1.0'
@ -72,7 +73,7 @@ class ShareAPI(object):
super(ShareAPI, self).__init__()
target = messaging.Target(topic=CONF.share_topic,
version=self.BASE_RPC_API_VERSION)
self.client = rpc.get_client(target, version_cap='1.12')
self.client = rpc.get_client(target, version_cap='1.13')
def create_share_instance(self, context, share_instance, host,
request_spec, filter_properties,
@ -116,6 +117,15 @@ class ShareAPI(object):
'unmanage_snapshot',
snapshot_id=snapshot['id'])
def revert_to_snapshot(self, context, share, snapshot, host, reservations):
host = utils.extract_host(host)
call_context = self.client.prepare(server=host, version='1.13')
call_context.cast(context,
'revert_to_snapshot',
share_id=share['id'],
snapshot_id=snapshot['id'],
reservations=reservations)
def delete_share_instance(self, context, share_instance, force=False):
host = utils.extract_host(share_instance['host'])
call_context = self.client.prepare(server=host, version='1.4')

View File

@ -266,6 +266,8 @@ def is_valid_optional_extra_spec(key, value):
return parse_boolean_extra_spec(key, value) is not None
elif key == constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT:
return parse_boolean_extra_spec(key, value) is not None
elif key == constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT:
return parse_boolean_extra_spec(key, value) is not None
elif key == constants.ExtraSpecs.REPLICATION_TYPE_SPEC:
return value in constants.ExtraSpecs.REPLICATION_TYPES

View File

@ -42,6 +42,7 @@ def stub_share(id, **kwargs):
'is_public': False,
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'replication_type': None,
'has_replicas': False,
}

View File

@ -233,6 +233,8 @@ class ShareTypesAPITest(test.TestCase):
('2.23', 'share_type_access', False),
('2.24', 'share_type_access', True),
('2.24', 'share_type_access', False),
('2.27', 'share_type_access', True),
('2.27', 'share_type_access', False),
)
@ddt.unpack
def test_view_builder_show(self, version, prefix, admin):
@ -284,6 +286,8 @@ class ShareTypesAPITest(test.TestCase):
('2.23', 'share_type_access', False),
('2.24', 'share_type_access', True),
('2.24', 'share_type_access', False),
('2.27', 'share_type_access', True),
('2.27', 'share_type_access', False),
)
@ddt.unpack
def test_view_builder_list(self, version, prefix, admin):
@ -292,6 +296,7 @@ class ShareTypesAPITest(test.TestCase):
extra_specs = {
constants.ExtraSpecs.SNAPSHOT_SUPPORT: True,
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT: False,
constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT: True,
}
now = timeutils.utcnow().isoformat()

View File

@ -22,6 +22,7 @@ from oslo_config import cfg
from oslo_serialization import jsonutils
import six
import webob
import webob.exc
from manila.api import common
from manila.api.openstack import api_version_request as api_version
@ -38,6 +39,7 @@ from manila import test
from manila.tests.api.contrib import stubs
from manila.tests.api import fakes
from manila.tests import db_utils
from manila.tests import fake_share
from manila import utils
CONF = cfg.CONF
@ -61,12 +63,14 @@ class ShareAPITest(test.TestCase):
stubs.stub_snapshot_get)
self.maxDiff = None
self.share = {
"id": "1",
"size": 100,
"display_name": "Share Test Name",
"display_description": "Share Test Desc",
"share_proto": "fakeproto",
"availability_zone": "zone1:host1",
"is_public": False,
"task_state": None,
}
self.create_mock = mock.Mock(
return_value=stubs.stub_share(
@ -83,6 +87,12 @@ class ShareAPITest(test.TestCase):
'id': 'fake_volume_type_id',
'name': 'fake_volume_type_name',
}
self.snapshot = {
'id': '2',
'share_id': '1',
'status': constants.STATUS_AVAILABLE,
}
CONF.set_default("default_share_type", None)
def _get_expected_share_detailed_response(self, values=None, admin=False):
@ -133,6 +143,256 @@ class ShareAPITest(test.TestCase):
share['share_server_id'] = 'fake_share_server_id'
return {'share': share}
def test__revert(self):
share = copy.deepcopy(self.share)
share['status'] = constants.STATUS_AVAILABLE
share['revert_to_snapshot_support'] = True
share = fake_share.fake_share(**share)
snapshot = copy.deepcopy(self.snapshot)
snapshot['status'] = constants.STATUS_AVAILABLE
body = {'revert': {'snapshot_id': '2'}}
req = fakes.HTTPRequest.blank(
'/shares/1/action', use_admin_context=False, version='2.27')
mock_validate_revert_parameters = self.mock_object(
self.controller, '_validate_revert_parameters',
mock.Mock(return_value=body['revert']))
mock_get = self.mock_object(
share_api.API, 'get', mock.Mock(return_value=share))
mock_get_snapshot = self.mock_object(
share_api.API, 'get_snapshot', mock.Mock(return_value=snapshot))
mock_get_latest_snapshot_for_share = self.mock_object(
share_api.API, 'get_latest_snapshot_for_share',
mock.Mock(return_value=snapshot))
mock_revert_to_snapshot = self.mock_object(
share_api.API, 'revert_to_snapshot')
response = self.controller._revert(req, '1', body=body)
self.assertEqual(202, response.status_int)
mock_validate_revert_parameters.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), body)
mock_get.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), '1')
mock_get_snapshot.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), '2')
mock_get_latest_snapshot_for_share.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), '1')
mock_revert_to_snapshot.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot)
def test__revert_not_supported(self):
share = copy.deepcopy(self.share)
share['revert_to_snapshot_support'] = False
share = fake_share.fake_share(**share)
snapshot = copy.deepcopy(self.snapshot)
snapshot['status'] = constants.STATUS_AVAILABLE
snapshot['share_id'] = 'wrong_id'
body = {'revert': {'snapshot_id': '2'}}
req = fakes.HTTPRequest.blank(
'/shares/1/action', use_admin_context=False, version='2.27')
self.mock_object(
self.controller, '_validate_revert_parameters',
mock.Mock(return_value=body['revert']))
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
self.mock_object(
share_api.API, 'get_snapshot', mock.Mock(return_value=snapshot))
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller._revert,
req,
'1',
body=body)
def test__revert_id_mismatch(self):
share = copy.deepcopy(self.share)
share['status'] = constants.STATUS_AVAILABLE
share['revert_to_snapshot_support'] = True
share = fake_share.fake_share(**share)
snapshot = copy.deepcopy(self.snapshot)
snapshot['status'] = constants.STATUS_AVAILABLE
snapshot['share_id'] = 'wrong_id'
body = {'revert': {'snapshot_id': '2'}}
req = fakes.HTTPRequest.blank(
'/shares/1/action', use_admin_context=False, version='2.27')
self.mock_object(
self.controller, '_validate_revert_parameters',
mock.Mock(return_value=body['revert']))
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
self.mock_object(
share_api.API, 'get_snapshot', mock.Mock(return_value=snapshot))
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller._revert,
req,
'1',
body=body)
@ddt.data(
{
'share_status': constants.STATUS_ERROR,
'share_is_busy': False,
'snapshot_status': constants.STATUS_AVAILABLE,
}, {
'share_status': constants.STATUS_AVAILABLE,
'share_is_busy': True,
'snapshot_status': constants.STATUS_AVAILABLE,
}, {
'share_status': constants.STATUS_AVAILABLE,
'share_is_busy': False,
'snapshot_status': constants.STATUS_ERROR,
})
@ddt.unpack
def test__revert_invalid_status(self, share_status, share_is_busy,
snapshot_status):
share = copy.deepcopy(self.share)
share['status'] = share_status
share['is_busy'] = share_is_busy
share['revert_to_snapshot_support'] = True
share = fake_share.fake_share(**share)
snapshot = copy.deepcopy(self.snapshot)
snapshot['status'] = snapshot_status
body = {'revert': {'snapshot_id': '2'}}
req = fakes.HTTPRequest.blank(
'/shares/1/action', use_admin_context=False, version='2.27')
self.mock_object(
self.controller, '_validate_revert_parameters',
mock.Mock(return_value=body['revert']))
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
self.mock_object(
share_api.API, 'get_snapshot', mock.Mock(return_value=snapshot))
self.assertRaises(webob.exc.HTTPConflict,
self.controller._revert,
req,
'1',
body=body)
def test__revert_snapshot_latest_not_found(self):
share = copy.deepcopy(self.share)
share['status'] = constants.STATUS_AVAILABLE
share['revert_to_snapshot_support'] = True
share = fake_share.fake_share(**share)
snapshot = copy.deepcopy(self.snapshot)
snapshot['status'] = constants.STATUS_AVAILABLE
body = {'revert': {'snapshot_id': '2'}}
req = fakes.HTTPRequest.blank(
'/shares/1/action', use_admin_context=False, version='2.27')
self.mock_object(
self.controller, '_validate_revert_parameters',
mock.Mock(return_value=body['revert']))
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
self.mock_object(
share_api.API, 'get_snapshot', mock.Mock(return_value=snapshot))
self.mock_object(
share_api.API, 'get_latest_snapshot_for_share',
mock.Mock(return_value=None))
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller._revert,
req,
'1',
body=body)
def test__revert_snapshot_not_latest(self):
share = copy.deepcopy(self.share)
share['status'] = constants.STATUS_AVAILABLE
share['revert_to_snapshot_support'] = True
share = fake_share.fake_share(**share)
snapshot = copy.deepcopy(self.snapshot)
snapshot['status'] = constants.STATUS_AVAILABLE
latest_snapshot = copy.deepcopy(self.snapshot)
latest_snapshot['status'] = constants.STATUS_AVAILABLE
latest_snapshot['id'] = '3'
body = {'revert': {'snapshot_id': '2'}}
req = fakes.HTTPRequest.blank(
'/shares/1/action', use_admin_context=False, version='2.27')
self.mock_object(
self.controller, '_validate_revert_parameters',
mock.Mock(return_value=body['revert']))
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
self.mock_object(
share_api.API, 'get_snapshot', mock.Mock(return_value=snapshot))
self.mock_object(
share_api.API, 'get_latest_snapshot_for_share',
mock.Mock(return_value=latest_snapshot))
self.assertRaises(webob.exc.HTTPConflict,
self.controller._revert,
req,
'1',
body=body)
@ddt.data(
{
'caught': exception.ShareNotFound,
'exc_args': {
'share_id': '1',
},
'thrown': webob.exc.HTTPNotFound,
}, {
'caught': exception.ShareSnapshotNotFound,
'exc_args': {
'snapshot_id': '2',
},
'thrown': webob.exc.HTTPBadRequest,
}, {
'caught': exception.ShareSizeExceedsAvailableQuota,
'exc_args': {},
'thrown': webob.exc.HTTPForbidden,
}, {
'caught': exception.ReplicationException,
'exc_args': {
'reason': 'catastrophic failure',
},
'thrown': webob.exc.HTTPBadRequest,
})
@ddt.unpack
def test__revert_exception(self, caught, exc_args, thrown):
body = {'revert': {'snapshot_id': '2'}}
req = fakes.HTTPRequest.blank(
'/shares/1/action', use_admin_context=False, version='2.27')
self.mock_object(
self.controller, '_validate_revert_parameters',
mock.Mock(return_value=body['revert']))
self.mock_object(
share_api.API, 'get', mock.Mock(side_effect=caught(**exc_args)))
self.assertRaises(thrown,
self.controller._revert,
req,
'1',
body=body)
def test_validate_revert_parameters(self):
body = {'revert': {'snapshot_id': 'fake_snapshot_id'}}
result = self.controller._validate_revert_parameters(
'fake_context', body)
self.assertEqual(body['revert'], result)
@ddt.data(
None,
{},
{'manage': {'snapshot_id': 'fake_snapshot_id'}},
{'revert': {'share_id': 'fake_snapshot_id'}},
{'revert': {'snapshot_id': ''}},
)
def test_validate_revert_parameters_invalid(self, body):
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller._validate_revert_parameters,
'fake_context',
body)
@ddt.data("2.0", "2.1")
def test_share_create_original(self, microversion):
self.mock_object(share_api.API, 'create', self.create_mock)
@ -2071,3 +2331,28 @@ class ShareManageTest(test.TestCase):
self.controller.manage,
req,
share_id)
def test_revert(self):
mock_revert = self.mock_object(
self.controller, '_revert',
mock.Mock(return_value='fake_response'))
req = fakes.HTTPRequest.blank(
'/shares/fake_id/action', use_admin_context=False, version='2.27')
result = self.controller.revert(req, 'fake_id', 'fake_body')
self.assertEqual('fake_response', result)
mock_revert.assert_called_once_with(
req, 'fake_id', 'fake_body')
def test_revert_unsupported(self):
req = fakes.HTTPRequest.blank(
'/shares/fake_id/action', use_admin_context=False, version='2.24')
self.assertRaises(exception.VersionNotFoundForAPIMethod,
self.controller.revert,
req,
'fake_id',
'fake_body')

View File

@ -45,13 +45,14 @@ class ViewBuilderTestCase(test.TestCase):
'user_id': 'fake_userid',
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': True,
}
return stubs.stub_share('fake_id', **fake_share)
def test__collection_name(self):
self.assertEqual('shares', self.builder._collection_name)
@ddt.data('2.6', '2.9', '2.10', '2.11', '2.16', '2.24')
@ddt.data('2.6', '2.9', '2.10', '2.11', '2.16', '2.24', '2.27')
def test_detail(self, microversion):
req = fakes.HTTPRequest.blank('/shares', version=microversion)
@ -77,5 +78,7 @@ class ViewBuilderTestCase(test.TestCase):
expected['user_id'] = 'fake_userid'
if self.is_microversion_ge(microversion, '2.24'):
expected['create_share_from_snapshot_support'] = True
if self.is_microversion_ge(microversion, '2.27'):
expected['revert_to_snapshot_support'] = True
self.assertSubDictMatch(expected, result['share'])

View File

@ -1371,6 +1371,7 @@ class CreateFromSnapshotExtraSpecAndShareColumn(BaseMigrationChecks):
# Pre-existing Shares must be present
shares_in_db = engine.execute(shares_table.select()).fetchall()
share_ids_in_db = [s['id'] for s in shares_in_db]
self.test_case.assertTrue(len(share_ids_in_db) > 1)
for share_id in share_ids:
self.test_case.assertIn(share_id, share_ids_in_db)
@ -1420,6 +1421,7 @@ class CreateFromSnapshotExtraSpecAndShareColumn(BaseMigrationChecks):
# Pre-existing Shares must be present
shares_in_db = engine.execute(shares_table.select()).fetchall()
share_ids_in_db = [s['id'] for s in shares_in_db]
self.test_case.assertTrue(len(share_ids_in_db) > 1)
for share_id in share_ids:
self.test_case.assertIn(share_id, share_ids_in_db)
@ -1449,6 +1451,102 @@ class CreateFromSnapshotExtraSpecAndShareColumn(BaseMigrationChecks):
self.test_case.assertEqual(0, len(new_extra_spec))
@map_to_migration('87ce15c59bbe')
class RevertToSnapshotShareColumn(BaseMigrationChecks):
expected_attr = constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT
def _get_fake_data(self):
extra_specs = []
shares = []
share_instances = []
share_types = [
{
'id': uuidutils.generate_uuid(),
'deleted': 'False',
'name': 'revert-1',
'is_public': False,
},
{
'id': uuidutils.generate_uuid(),
'deleted': 'False',
'name': 'revert-2',
'is_public': True,
},
]
snapshot_support = ('0', '1')
dhss = ('True', 'False')
for idx, share_type in enumerate(share_types):
extra_specs.append({
'share_type_id': share_type['id'],
'spec_key': 'snapshot_support',
'spec_value': snapshot_support[idx],
'deleted': 0,
})
extra_specs.append({
'share_type_id': share_type['id'],
'spec_key': 'driver_handles_share_servers',
'spec_value': dhss[idx],
'deleted': 0,
})
share = fake_share(snapshot_support=snapshot_support[idx])
shares.append(share)
share_instances.append(
fake_instance(share_id=share['id'],
share_type_id=share_type['id'])
)
return share_types, extra_specs, shares, share_instances
def setup_upgrade_data(self, engine):
(self.share_types, self.extra_specs, self.shares,
self.share_instances) = self._get_fake_data()
share_types_table = utils.load_table('share_types', engine)
engine.execute(share_types_table.insert(self.share_types))
extra_specs_table = utils.load_table('share_type_extra_specs',
engine)
engine.execute(extra_specs_table.insert(self.extra_specs))
shares_table = utils.load_table('shares', engine)
engine.execute(shares_table.insert(self.shares))
share_instances_table = utils.load_table('share_instances', engine)
engine.execute(share_instances_table.insert(self.share_instances))
def check_upgrade(self, engine, data):
share_ids = [s['id'] for s in self.shares]
shares_table = utils.load_table('shares', engine)
# Pre-existing Shares must be present
shares_in_db = engine.execute(shares_table.select().where(
shares_table.c.deleted == 'False')).fetchall()
share_ids_in_db = [s['id'] for s in shares_in_db]
self.test_case.assertTrue(len(share_ids_in_db) > 1)
for share_id in share_ids:
self.test_case.assertIn(share_id, share_ids_in_db)
# New shares attr must be present and set to False
for share in shares_in_db:
self.test_case.assertTrue(hasattr(share, self.expected_attr))
self.test_case.assertEqual(False, share[self.expected_attr])
def check_downgrade(self, engine):
share_ids = [s['id'] for s in self.shares]
shares_table = utils.load_table('shares', engine)
# Pre-existing Shares must be present
shares_in_db = engine.execute(shares_table.select()).fetchall()
share_ids_in_db = [s['id'] for s in shares_in_db]
self.test_case.assertTrue(len(share_ids_in_db) > 1)
for share_id in share_ids:
self.test_case.assertIn(share_id, share_ids_in_db)
# Shares should have no attr to revert share to snapshot
for share in shares_in_db:
self.test_case.assertFalse(hasattr(share, self.expected_attr))
@map_to_migration('95e3cf760840')
class RemoveNovaNetIdColumnFromShareNetworks(BaseMigrationChecks):
table_name = 'share_networks'

View File

@ -17,6 +17,8 @@
"""Testing of SQLAlchemy backend."""
import copy
import ddt
import mock
@ -894,6 +896,35 @@ class ShareSnapshotDatabaseAPITestCase(test.TestCase):
self.assertEqual(1, len(actual_result.instances))
self.assertSubDictMatch(values, actual_result.to_dict())
def test_share_snapshot_get_latest_for_share(self):
share = db_utils.create_share(size=1)
values = {
'share_id': share['id'],
'size': share['size'],
'user_id': share['user_id'],
'project_id': share['project_id'],
'status': constants.STATUS_CREATING,
'progress': '0%',
'share_size': share['size'],
'display_description': 'fake',
'share_proto': share['share_proto'],
}
values1 = copy.deepcopy(values)
values1['display_name'] = 'snap1'
db_api.share_snapshot_create(self.ctxt, values1)
values2 = copy.deepcopy(values)
values2['display_name'] = 'snap2'
db_api.share_snapshot_create(self.ctxt, values2)
values3 = copy.deepcopy(values)
values3['display_name'] = 'snap3'
db_api.share_snapshot_create(self.ctxt, values3)
result = db_api.share_snapshot_get_latest_for_share(self.ctxt,
share['id'])
self.assertSubDictMatch(values3, result.to_dict())
def test_get_instance(self):
snapshot = db_utils.create_snapshot(with_share=True)

View File

@ -76,6 +76,28 @@ class ShareTestCase(test.TestCase):
self.assertEqual(constants.STATUS_CREATING, share.instance['status'])
@ddt.data(constants.STATUS_REPLICATION_CHANGE, constants.STATUS_AVAILABLE,
constants.STATUS_ERROR, constants.STATUS_CREATING)
def test_share_instance_reverting(self, status):
instance_list = [
db_utils.create_share_instance(
status=constants.STATUS_REVERTING,
share_id='fake_id'),
db_utils.create_share_instance(
status=status, share_id='fake_id'),
db_utils.create_share_instance(
status=constants.STATUS_ERROR_DELETING, share_id='fake_id'),
]
share1 = db_utils.create_share(instances=instance_list)
share2 = db_utils.create_share(instances=list(reversed(instance_list)))
self.assertEqual(
constants.STATUS_REVERTING, share1.instance['status'])
self.assertEqual(
constants.STATUS_REVERTING, share2.instance['status'])
@ddt.data(constants.STATUS_AVAILABLE, constants.STATUS_ERROR,
constants.STATUS_CREATING)
def test_share_instance_replication_change(self, status):

View File

@ -40,6 +40,7 @@ SERVICE_STATES_NO_POOLS = {
thin_provisioning=False,
snapshot_support=False,
create_share_from_snapshot_support=False,
revert_to_snapshot_support=True,
driver_handles_share_servers=False),
'host2@back1': dict(share_backend_name='BBB',
total_capacity_gb=256, free_capacity_gb=100,
@ -49,6 +50,7 @@ SERVICE_STATES_NO_POOLS = {
thin_provisioning=True,
snapshot_support=True,
create_share_from_snapshot_support=True,
revert_to_snapshot_support=False,
driver_handles_share_servers=False),
'host2@back2': dict(share_backend_name='CCC',
total_capacity_gb=10000, free_capacity_gb=700,
@ -58,6 +60,7 @@ SERVICE_STATES_NO_POOLS = {
thin_provisioning=True,
snapshot_support=True,
create_share_from_snapshot_support=True,
revert_to_snapshot_support=False,
driver_handles_share_servers=False),
}
@ -83,6 +86,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
driver_handles_share_servers=False,
snapshot_support=True,
create_share_from_snapshot_support=True,
revert_to_snapshot_support=True,
replication_type=None,
pools=[dict(pool_name='pool1',
total_capacity_gb=51,
@ -96,6 +100,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
driver_handles_share_servers=False,
snapshot_support=True,
create_share_from_snapshot_support=True,
revert_to_snapshot_support=False,
replication_type=None,
pools=[dict(pool_name='pool2',
total_capacity_gb=52,
@ -109,6 +114,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
driver_handles_share_servers=False,
snapshot_support=True,
create_share_from_snapshot_support=True,
revert_to_snapshot_support=False,
replication_type=None,
pools=[dict(pool_name='pool3',
total_capacity_gb=53,
@ -123,6 +129,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
driver_handles_share_servers=False,
snapshot_support=True,
create_share_from_snapshot_support=True,
revert_to_snapshot_support=False,
replication_type=None,
pools=[dict(pool_name='pool4a',
total_capacity_gb=541,
@ -145,6 +152,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
driver_handles_share_servers=False,
snapshot_support=True,
create_share_from_snapshot_support=True,
revert_to_snapshot_support=False,
replication_type=None,
pools=[dict(pool_name='pool5a',
total_capacity_gb=551,
@ -165,6 +173,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
driver_handles_share_servers=False,
snapshot_support=True,
create_share_from_snapshot_support=True,
revert_to_snapshot_support=False,
replication_type=None,
pools=[dict(pool_name='pool6a',
total_capacity_gb='unknown',

View File

@ -211,6 +211,7 @@ class HostManagerTestCase(test.TestCase):
'driver_handles_share_servers': False,
'snapshot_support': False,
'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': True,
'consistency_group_support': False,
'dedupe': False,
'compression': False,
@ -237,6 +238,7 @@ class HostManagerTestCase(test.TestCase):
'driver_handles_share_servers': False,
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'consistency_group_support': False,
'dedupe': False,
'compression': False,
@ -263,6 +265,7 @@ class HostManagerTestCase(test.TestCase):
'driver_handles_share_servers': False,
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'consistency_group_support': False,
'dedupe': False,
'compression': False,
@ -311,6 +314,7 @@ class HostManagerTestCase(test.TestCase):
'driver_handles_share_servers': False,
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': True,
'consistency_group_support': False,
'dedupe': False,
'compression': False,
@ -338,6 +342,7 @@ class HostManagerTestCase(test.TestCase):
'driver_handles_share_servers': False,
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'consistency_group_support': False,
'dedupe': False,
'compression': False,
@ -365,6 +370,7 @@ class HostManagerTestCase(test.TestCase):
'driver_handles_share_servers': False,
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'consistency_group_support': 'pool',
'dedupe': False,
'compression': False,
@ -392,6 +398,7 @@ class HostManagerTestCase(test.TestCase):
'driver_handles_share_servers': False,
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'consistency_group_support': 'host',
'dedupe': False,
'compression': False,
@ -419,6 +426,7 @@ class HostManagerTestCase(test.TestCase):
'driver_handles_share_servers': False,
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'consistency_group_support': 'host',
'dedupe': False,
'compression': False,
@ -469,6 +477,7 @@ class HostManagerTestCase(test.TestCase):
'driver_handles_share_servers': False,
'snapshot_support': False,
'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': True,
'share_backend_name': 'AAA',
'free_capacity_gb': 200,
'driver_version': None,
@ -495,6 +504,7 @@ class HostManagerTestCase(test.TestCase):
'driver_handles_share_servers': False,
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'share_backend_name': 'BBB',
'free_capacity_gb': 100,
'driver_version': None,
@ -549,6 +559,7 @@ class HostManagerTestCase(test.TestCase):
'driver_handles_share_servers': False,
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'share_backend_name': 'BBB',
'free_capacity_gb': 42,
'driver_version': None,

View File

@ -125,6 +125,7 @@ class EMCShareFrameworkTestCase(test.TestCase):
data['pools'] = None
data['snapshot_support'] = True
data['create_share_from_snapshot_support'] = True
data['revert_to_snapshot_support'] = False
data['replication_domain'] = None
data['filter_function'] = None
data['goodness_function'] = None

View File

@ -278,6 +278,10 @@ class DummyDriver(driver.ShareDriver):
def unmanage_snapshot(self, snapshot):
"""Removes the specified snapshot from Manila management."""
@slow_me_down
def revert_to_snapshot(self, context, snapshot, share_server=None):
"""Reverts a share (in place) to the specified snapshot."""
@slow_me_down
def extend_share(self, share, new_size, share_server=None):
"""Extends size of existing share."""
@ -338,6 +342,7 @@ class DummyDriver(driver.ShareDriver):
"consistency_group_support": "pool",
"snapshot_support": True,
"create_share_from_snapshot_support": True,
"revert_to_snapshot_support": True,
"driver_name": "Dummy",
"pools": self._get_pools_info(),
}
@ -443,6 +448,12 @@ class DummyDriver(driver.ShareDriver):
{"id": r["id"], "status": constants.STATUS_AVAILABLE})
return return_replica_snapshots
@slow_me_down
def revert_to_replicated_snapshot(self, context, active_replica,
replica_list, active_replica_snapshot,
replica_snapshots, share_server=None):
"""Reverts a replicated share (in place) to the specified snapshot."""
@slow_me_down
def delete_replicated_snapshot(self, context, replica_list,
replica_snapshots, share_server=None):

View File

@ -257,6 +257,7 @@ class GlusterfsNativeShareDriverTestCase(test.TestCase):
'pools': None,
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'replication_domain': None,
'filter_function': None,
'goodness_function': None,

View File

@ -734,6 +734,7 @@ class HPE3ParDriverTestCase(test.TestCase):
'share_backend_name': 'HPE_3PAR',
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'storage_protocol': 'NFS_CIFS',
'thin_provisioning': True,
'total_capacity_gb': 0,
@ -809,6 +810,7 @@ class HPE3ParDriverTestCase(test.TestCase):
'provisioned_capacity_gb': expected_capacity}],
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'replication_domain': None,
'filter_function': None,
'goodness_function': None,
@ -846,6 +848,7 @@ class HPE3ParDriverTestCase(test.TestCase):
'vendor_name': 'HPE',
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'replication_domain': None,
'filter_function': None,
'goodness_function': None,

View File

@ -2424,6 +2424,7 @@ class HuaweiShareDriverTestCase(test.TestCase):
"qos": True,
"snapshot_support": snapshot_support,
"create_share_from_snapshot_support": snapshot_support,
"revert_to_snapshot_support": False,
"replication_domain": None,
"filter_function": None,
"goodness_function": None,

View File

@ -54,7 +54,11 @@ def fake_snapshot(**kwargs):
'name': 'fakesnapshotname',
'share_proto': 'NFS',
'export_location': '127.0.0.1:/mnt/nfs/volume-00002',
'share': {'size': 1},
'share': {
'id': 'fakeid',
'name': 'fakename',
'size': 1
},
}
snapshot.update(kwargs)
return db_fakes.FakeModel(snapshot)
@ -520,3 +524,26 @@ class LVMShareDriverTestCase(test.TestCase):
self.assertTrue(self._driver._stats['snapshot_support'])
self.assertEqual('LVMShareDriver', self._driver._stats['driver_name'])
self.assertEqual('test-pool', self._driver._stats['pools'])
def test_revert_to_snapshot(self):
self._driver.revert_to_snapshot(self._context, self.snapshot,
self.share_server)
snap_lv = "%s/fakesnapshotname" % (CONF.lvm_share_volume_group)
share_lv = "%s/fakename" % (CONF.lvm_share_volume_group)
mount_path = self._get_mount_path(self.snapshot['share'])
expected_exec = [
("lvconvert --merge %s" % snap_lv),
("umount %s" % mount_path),
("rmdir %s" % mount_path),
("lvchange -an %s" % share_lv),
("lvchange -ay %s" % share_lv),
("lvcreate -L 1G --name fakesnapshotname --snapshot %s" %
share_lv),
('tune2fs -U random /dev/mapper/%s-fakesnapshotname' %
CONF.lvm_share_volume_group),
("mkdir -p %s" % mount_path),
("mount /dev/mapper/%s-fakename %s" %
(CONF.lvm_share_volume_group, mount_path)),
("chmod 777 %s" % mount_path),
]
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())

View File

@ -355,6 +355,7 @@ class ZFSonLinuxShareDriverTestCase(test.TestCase):
'share_backend_name': self.driver.backend_name,
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'storage_protocol': 'NFS',
'total_capacity_gb': 'unknown',
'vendor_name': 'Open Source',

View File

@ -749,6 +749,7 @@ class ShareAPITestCase(test.TestCase):
'extra_specs': {
'snapshot_support': True,
'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': False,
'replication_type': 'dr',
}
}
@ -765,6 +766,7 @@ class ShareAPITestCase(test.TestCase):
expected = {
'snapshot_support': False,
'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': False,
'replication_type': None,
}
self.assertEqual(expected, result)
@ -773,7 +775,7 @@ class ShareAPITestCase(test.TestCase):
{'extra_specs': {'create_share_from_snapshot_support': 'fake'}})
def test_get_share_attributes_from_share_type_invalid(self, share_type):
self.assertRaises(exception.InvalidParameterValue,
self.assertRaises(exception.InvalidExtraSpec,
self.api._get_share_attributes_from_share_type,
share_type)
@ -798,6 +800,7 @@ class ShareAPITestCase(test.TestCase):
'snapshot_support': False,
'replication_type': replication_type,
'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': False,
},
}
@ -824,6 +827,8 @@ class ShareAPITestCase(test.TestCase):
'snapshot_support': fake_type['extra_specs']['snapshot_support'],
'create_share_from_snapshot_support':
fake_type['extra_specs']['create_share_from_snapshot_support'],
'revert_to_snapshot_support':
fake_type['extra_specs']['revert_to_snapshot_support'],
'replication_type': replication_type,
})
@ -883,6 +888,9 @@ class ShareAPITestCase(test.TestCase):
'create_share_from_snapshot_support',
share_type['extra_specs']
['create_share_from_snapshot_support']),
'revert_to_snapshot_support': kwargs.get(
'revert_to_snapshot_support',
share_type['extra_specs']['revert_to_snapshot_support']),
'share_proto': kwargs.get('share_proto', share.get('share_proto')),
'share_type_id': share_type['id'],
'is_public': kwargs.get('is_public', share.get('is_public')),
@ -983,6 +991,48 @@ class ShareAPITestCase(test.TestCase):
db_api.share_snapshot_create.assert_called_once_with(
self.context, options)
def test_create_snapshot_space_quota_exceeded(self):
share = fakes.fake_share(
id=uuidutils.generate_uuid(), size=1, project_id='fake_project',
user_id='fake_user', has_replicas=False, status='available')
usages = {'gigabytes': {'reserved': 10, 'in_use': 0}}
quotas = {'snapshot_gigabytes': 10}
side_effect = exception.OverQuota(
overs='snapshot_gigabytes', usages=usages, quotas=quotas)
self.mock_object(
quota.QUOTAS, 'reserve', mock.Mock(side_effect=side_effect))
mock_snap_create = self.mock_object(db_api, 'share_snapshot_create')
self.assertRaises(exception.SnapshotSizeExceedsAvailableQuota,
self.api.create_snapshot,
self.context,
share,
'fake_name',
'fake_description')
mock_snap_create.assert_not_called()
def test_create_snapshot_count_quota_exceeded(self):
share = fakes.fake_share(
id=uuidutils.generate_uuid(), size=1, project_id='fake_project',
user_id='fake_user', has_replicas=False, status='available')
usages = {'snapshots': {'reserved': 10, 'in_use': 0}}
quotas = {'snapshots': 10}
side_effect = exception.OverQuota(
overs='snapshots', usages=usages, quotas=quotas)
self.mock_object(
quota.QUOTAS, 'reserve', mock.Mock(side_effect=side_effect))
mock_snap_create = self.mock_object(db_api, 'share_snapshot_create')
self.assertRaises(exception.SnapshotLimitExceeded,
self.api.create_snapshot,
self.context,
share,
'fake_name',
'fake_description')
mock_snap_create.assert_not_called()
def test_manage_snapshot_share_not_found(self):
snapshot = fakes.fake_snapshot(share_id='fake_share',
as_primitive=True)
@ -1098,6 +1148,218 @@ class ShareAPITestCase(test.TestCase):
mock_rpc_call.assert_called_once_with(
self.context, snapshot, fake_host)
@ddt.data(True, False)
def test_revert_to_snapshot(self, has_replicas):
share = fakes.fake_share(id=uuidutils.generate_uuid(),
has_replicas=has_replicas)
self.mock_object(db_api, 'share_get', mock.Mock(return_value=share))
mock_handle_revert_to_snapshot_quotas = self.mock_object(
self.api, '_handle_revert_to_snapshot_quotas',
mock.Mock(return_value='fake_reservations'))
mock_revert_to_replicated_snapshot = self.mock_object(
self.api, '_revert_to_replicated_snapshot')
mock_revert_to_snapshot = self.mock_object(
self.api, '_revert_to_snapshot')
snapshot = fakes.fake_snapshot(share_id=share['id'])
self.api.revert_to_snapshot(self.context, snapshot)
mock_handle_revert_to_snapshot_quotas.assert_called_once_with(
self.context, share, snapshot)
if not has_replicas:
self.assertFalse(mock_revert_to_replicated_snapshot.called)
mock_revert_to_snapshot.assert_called_once_with(
self.context, share, snapshot, 'fake_reservations')
else:
mock_revert_to_replicated_snapshot.assert_called_once_with(
self.context, share, snapshot, 'fake_reservations')
self.assertFalse(mock_revert_to_snapshot.called)
@ddt.data(None, 'fake_reservations')
def test_revert_to_snapshot_exception(self, reservations):
share = fakes.fake_share(id=uuidutils.generate_uuid(),
has_replicas=False)
self.mock_object(db_api, 'share_get', mock.Mock(return_value=share))
self.mock_object(
self.api, '_handle_revert_to_snapshot_quotas',
mock.Mock(return_value=reservations))
side_effect = exception.ReplicationException(reason='error')
self.mock_object(
self.api, '_revert_to_snapshot',
mock.Mock(side_effect=side_effect))
mock_quotas_rollback = self.mock_object(quota.QUOTAS, 'rollback')
snapshot = fakes.fake_snapshot(share_id=share['id'])
self.assertRaises(exception.ReplicationException,
self.api.revert_to_snapshot,
self.context,
snapshot)
if reservations is not None:
mock_quotas_rollback.assert_called_once_with(
self.context, reservations)
else:
self.assertFalse(mock_quotas_rollback.called)
def test_handle_revert_to_snapshot_quotas(self):
share = fakes.fake_share(
id=uuidutils.generate_uuid(), size=1, project_id='fake_project',
user_id='fake_user', has_replicas=False)
snapshot = fakes.fake_snapshot(
id=uuidutils.generate_uuid(), share_id=share['id'], size=1)
mock_quotas_reserve = self.mock_object(quota.QUOTAS, 'reserve')
result = self.api._handle_revert_to_snapshot_quotas(
self.context, share, snapshot)
self.assertIsNone(result)
self.assertFalse(mock_quotas_reserve.called)
def test_handle_revert_to_snapshot_quotas_different_size(self):
share = fakes.fake_share(
id=uuidutils.generate_uuid(), size=1, project_id='fake_project',
user_id='fake_user', has_replicas=False)
snapshot = fakes.fake_snapshot(
id=uuidutils.generate_uuid(), share_id=share['id'], size=2)
mock_quotas_reserve = self.mock_object(
quota.QUOTAS, 'reserve',
mock.Mock(return_value='fake_reservations'))
result = self.api._handle_revert_to_snapshot_quotas(
self.context, share, snapshot)
self.assertEqual('fake_reservations', result)
mock_quotas_reserve.assert_called_once_with(
self.context, project_id='fake_project', gigabytes=1,
user_id='fake_user')
def test_handle_revert_to_snapshot_quotas_quota_exceeded(self):
share = fakes.fake_share(
id=uuidutils.generate_uuid(), size=1, project_id='fake_project',
user_id='fake_user', has_replicas=False)
snapshot = fakes.fake_snapshot(
id=uuidutils.generate_uuid(), share_id=share['id'], size=2)
usages = {'gigabytes': {'reserved': 10, 'in_use': 0}}
quotas = {'gigabytes': 10}
side_effect = exception.OverQuota(
overs='fake', usages=usages, quotas=quotas)
self.mock_object(
quota.QUOTAS, 'reserve', mock.Mock(side_effect=side_effect))
self.assertRaises(exception.ShareSizeExceedsAvailableQuota,
self.api._handle_revert_to_snapshot_quotas,
self.context,
share,
snapshot)
def test__revert_to_snapshot(self):
share = fakes.fake_share(
id=uuidutils.generate_uuid(), size=1, project_id='fake_project',
user_id='fake_user', has_replicas=False)
snapshot = fakes.fake_snapshot(
id=uuidutils.generate_uuid(), share_id=share['id'], size=2)
mock_share_update = self.mock_object(db_api, 'share_update')
mock_share_snapshot_update = self.mock_object(
db_api, 'share_snapshot_update')
mock_revert_rpc_call = self.mock_object(
self.share_rpcapi, 'revert_to_snapshot')
self.api._revert_to_snapshot(
self.context, share, snapshot, 'fake_reservations')
mock_share_update.assert_called_once_with(
self.context, share['id'], {'status': constants.STATUS_REVERTING})
mock_share_snapshot_update.assert_called_once_with(
self.context, snapshot['id'],
{'status': constants.STATUS_RESTORING})
mock_revert_rpc_call.assert_called_once_with(
self.context, share, snapshot, share['instance']['host'],
'fake_reservations')
def test_revert_to_replicated_snapshot(self):
share = fakes.fake_share(
has_replicas=True, status=constants.STATUS_AVAILABLE)
snapshot = fakes.fake_snapshot(share_instance_id='id1')
snapshot_instance = fakes.fake_snapshot_instance(
base_snapshot=snapshot, id='sid1')
replicas = [
fakes.fake_replica(
id='rid1', replica_state=constants.REPLICA_STATE_ACTIVE),
fakes.fake_replica(
id='rid2', replica_state=constants.REPLICA_STATE_IN_SYNC),
]
self.mock_object(
db_api, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=replicas[0]))
self.mock_object(
db_api, 'share_snapshot_instance_get_all_with_filters',
mock.Mock(return_value=[snapshot_instance]))
mock_share_replica_update = self.mock_object(
db_api, 'share_replica_update')
mock_share_snapshot_instance_update = self.mock_object(
db_api, 'share_snapshot_instance_update')
mock_revert_rpc_call = self.mock_object(
self.share_rpcapi, 'revert_to_snapshot')
self.api._revert_to_replicated_snapshot(
self.context, share, snapshot, 'fake_reservations')
mock_share_replica_update.assert_called_once_with(
self.context, 'rid1', {'status': constants.STATUS_REVERTING})
mock_share_snapshot_instance_update.assert_called_once_with(
self.context, 'sid1', {'status': constants.STATUS_RESTORING})
mock_revert_rpc_call.assert_called_once_with(
self.context, share, snapshot, replicas[0]['host'],
'fake_reservations')
def test_revert_to_replicated_snapshot_no_active_replica(self):
share = fakes.fake_share(
has_replicas=True, status=constants.STATUS_AVAILABLE)
snapshot = fakes.fake_snapshot(share_instance_id='id1')
self.mock_object(
db_api, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=None))
self.assertRaises(exception.ReplicationException,
self.api._revert_to_replicated_snapshot,
self.context,
share,
snapshot,
'fake_reservations')
def test_revert_to_replicated_snapshot_no_snapshot_instance(self):
share = fakes.fake_share(
has_replicas=True, status=constants.STATUS_AVAILABLE)
snapshot = fakes.fake_snapshot(share_instance_id='id1')
replicas = [
fakes.fake_replica(
id='rid1', replica_state=constants.REPLICA_STATE_ACTIVE),
fakes.fake_replica(
id='rid2', replica_state=constants.REPLICA_STATE_IN_SYNC),
]
self.mock_object(
db_api, 'share_replicas_get_available_active_replica',
mock.Mock(return_value=replicas[0]))
self.mock_object(
db_api, 'share_snapshot_instance_get_all_with_filters',
mock.Mock(return_value=[None]))
self.assertRaises(exception.ReplicationException,
self.api._revert_to_replicated_snapshot,
self.context,
share,
snapshot,
'fake_reservations')
def test_create_snapshot_for_replicated_share(self):
share = fakes.fake_share(
has_replicas=True, status=constants.STATUS_AVAILABLE)
@ -2051,6 +2313,7 @@ class ShareAPITestCase(test.TestCase):
'extra_specs': {
'snapshot_support': False,
'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': False,
'driver_handles_share_servers': dhss,
},
}
@ -2061,6 +2324,7 @@ class ShareAPITestCase(test.TestCase):
'extra_specs': {
'snapshot_support': False,
'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': False,
'driver_handles_share_servers': dhss,
},
}

View File

@ -230,6 +230,14 @@ class ShareDriverTestCase(test.TestCase):
self._assert_is_callable(share_driver, method)
@ddt.data('revert_to_snapshot',
'revert_to_replicated_snapshot')
def test_drivers_methods_needed_by_share_revert_to_snapshot_functionality(
self, method):
share_driver = self._instantiate_share_driver(None, False)
self._assert_is_callable(share_driver, method)
@ddt.data(True, False)
def test_get_share_server_pools(self, value):
driver.CONF.set_default('driver_handles_share_servers', value)

View File

@ -1367,7 +1367,8 @@ class ShareManagerTestCase(test.TestCase):
self.assertIsNone(retval)
self.assertTrue(replica_update_call.called)
def _get_snapshot_instance_dict(self, snapshot_instance, share):
def _get_snapshot_instance_dict(self, snapshot_instance, share,
snapshot=None):
expected_snapshot_instance_dict = {
'status': constants.STATUS_CREATING,
'share_id': share['id'],
@ -1384,6 +1385,10 @@ class ShareManagerTestCase(test.TestCase):
'deleted_at': snapshot_instance['deleted_at'],
'provider_location': snapshot_instance['provider_location'],
}
if snapshot:
expected_snapshot_instance_dict.update({
'size': snapshot['size'],
})
return expected_snapshot_instance_dict
def test_create_snapshot_driver_exception(self):
@ -4837,6 +4842,162 @@ class ShareManagerTestCase(test.TestCase):
if quota_error:
self.assertTrue(mock_log_warning.called)
@ddt.data(True, False)
def test_revert_to_snapshot(self, has_replicas):
reservations = 'fake_reservations'
share_id = 'fake_share_id'
snapshot_id = 'fake_snapshot_id'
share = fakes.fake_share(
id=share_id, instance={'id': 'fake_instance_id'},
project_id='fake_project', user_id='fake_user', size=2,
has_replicas=has_replicas)
snapshot_instance = fakes.fake_snapshot_instance(
share_id=share_id, share=share, name='fake_snapshot')
snapshot = fakes.fake_snapshot(
id=snapshot_id, share_id=share_id, share=share,
instance=snapshot_instance, project_id='fake_project',
user_id='fake_user', size=1)
mock_share_snapshot_get = self.mock_object(
self.share_manager.db, 'share_snapshot_get',
mock.Mock(return_value=snapshot))
mock_revert_to_snapshot = self.mock_object(
self.share_manager, '_revert_to_snapshot')
mock_revert_to_replicated_snapshot = self.mock_object(
self.share_manager, '_revert_to_replicated_snapshot')
self.share_manager.revert_to_snapshot(
self.context, snapshot_id, reservations, share_id=share_id)
mock_share_snapshot_get.assert_called_once_with(mock.ANY, snapshot_id)
if not has_replicas:
mock_revert_to_snapshot.assert_called_once_with(
mock.ANY, share, snapshot, reservations)
self.assertFalse(mock_revert_to_replicated_snapshot.called)
else:
self.assertFalse(mock_revert_to_snapshot.called)
mock_revert_to_replicated_snapshot.assert_called_once_with(
mock.ANY, share, snapshot, reservations, share_id=share_id)
@ddt.data(None, 'fake_reservations')
def test__revert_to_snapshot(self, reservations):
mock_quotas_rollback = self.mock_object(quota.QUOTAS, 'rollback')
mock_quotas_commit = self.mock_object(quota.QUOTAS, 'commit')
self.mock_object(
self.share_manager, '_get_share_server',
mock.Mock(return_value=None))
mock_driver = self.mock_object(self.share_manager, 'driver')
share_id = 'fake_share_id'
share = fakes.fake_share(
id=share_id, instance={'id': 'fake_instance_id'},
project_id='fake_project', user_id='fake_user', size=2)
snapshot_instance = fakes.fake_snapshot_instance(
share_id=share_id, share=share, name='fake_snapshot')
snapshot = fakes.fake_snapshot(
id='fake_snapshot_id', share_id=share_id, share=share,
instance=snapshot_instance, project_id='fake_project',
user_id='fake_user', size=1)
self.mock_object(
self.share_manager.db, 'share_snapshot_get',
mock.Mock(return_value=snapshot))
self.mock_object(
self.share_manager.db, 'share_snapshot_instance_get',
mock.Mock(return_value=snapshot_instance))
mock_share_update = self.mock_object(
self.share_manager.db, 'share_update')
mock_share_snapshot_update = self.mock_object(
self.share_manager.db, 'share_snapshot_update')
self.share_manager._revert_to_snapshot(
self.context, share, snapshot, reservations)
mock_driver.revert_to_snapshot.assert_called_once_with(
mock.ANY,
self._get_snapshot_instance_dict(
snapshot_instance, share, snapshot=snapshot),
share_server=None)
self.assertFalse(mock_quotas_rollback.called)
if reservations:
mock_quotas_commit.assert_called_once_with(
mock.ANY, reservations, project_id='fake_project',
user_id='fake_user')
else:
self.assertFalse(mock_quotas_commit.called)
mock_share_update.assert_called_once_with(
mock.ANY, share_id,
{'status': constants.STATUS_AVAILABLE, 'size': snapshot['size']})
mock_share_snapshot_update.assert_called_once_with(
mock.ANY, 'fake_snapshot_id',
{'status': constants.STATUS_AVAILABLE})
@ddt.data(None, 'fake_reservations')
def test__revert_to_snapshot_driver_exception(self, reservations):
mock_quotas_rollback = self.mock_object(quota.QUOTAS, 'rollback')
mock_quotas_commit = self.mock_object(quota.QUOTAS, 'commit')
self.mock_object(
self.share_manager, '_get_share_server',
mock.Mock(return_value=None))
mock_driver = self.mock_object(self.share_manager, 'driver')
mock_driver.revert_to_snapshot.side_effect = exception.ManilaException
share_id = 'fake_share_id'
share = fakes.fake_share(
id=share_id, instance={'id': 'fake_instance_id'},
project_id='fake_project', user_id='fake_user', size=2)
snapshot_instance = fakes.fake_snapshot_instance(
share_id=share_id, share=share, name='fake_snapshot')
snapshot = fakes.fake_snapshot(
id='fake_snapshot_id', share_id=share_id, share=share,
instance=snapshot_instance, project_id='fake_project',
user_id='fake_user', size=1)
self.mock_object(
self.share_manager.db, 'share_snapshot_get',
mock.Mock(return_value=snapshot))
self.mock_object(
self.share_manager.db, 'share_snapshot_instance_get',
mock.Mock(return_value=snapshot_instance))
mock_share_update = self.mock_object(
self.share_manager.db, 'share_update')
mock_share_snapshot_update = self.mock_object(
self.share_manager.db, 'share_snapshot_update')
self.assertRaises(exception.ManilaException,
self.share_manager._revert_to_snapshot,
self.context,
share,
snapshot,
reservations)
mock_driver.revert_to_snapshot.assert_called_once_with(
mock.ANY,
self._get_snapshot_instance_dict(
snapshot_instance, share, snapshot=snapshot),
share_server=None)
self.assertFalse(mock_quotas_commit.called)
if reservations:
mock_quotas_rollback.assert_called_once_with(
mock.ANY, reservations, project_id='fake_project',
user_id='fake_user')
else:
self.assertFalse(mock_quotas_rollback.called)
mock_share_update.assert_called_once_with(
mock.ANY, share_id,
{'status': constants.STATUS_REVERTING_ERROR})
mock_share_snapshot_update.assert_called_once_with(
mock.ANY, 'fake_snapshot_id',
{'status': constants.STATUS_AVAILABLE})
def _setup_crud_replicated_snapshot_data(self):
snapshot = fakes.fake_snapshot(create_instance=True)
snapshot_instance = fakes.fake_snapshot_instance(
@ -4928,6 +5089,135 @@ class ShareManagerTestCase(test.TestCase):
mock_db_update_call.assert_called_once_with(
self.context, snapshot['instance']['id'], snapshot_dict)
@ddt.data(None, 'fake_reservations')
def test_revert_to_replicated_snapshot(self, reservations):
share_id = 'id1'
mock_quotas_rollback = self.mock_object(quota.QUOTAS, 'rollback')
mock_quotas_commit = self.mock_object(quota.QUOTAS, 'commit')
share = fakes.fake_share(
id=share_id, project_id='fake_project', user_id='fake_user')
snapshot = fakes.fake_snapshot(
create_instance=True, share=share, size=1)
snapshot_instance = fakes.fake_snapshot_instance(
base_snapshot=snapshot)
snapshot_instances = [snapshot['instance'], snapshot_instance]
active_replica = fake_replica(
id='rid1', share_id=share_id, host=self.share_manager.host,
replica_state=constants.REPLICA_STATE_ACTIVE, as_primitive=False)
replica = fake_replica(
id='rid2', share_id=share_id, host='secondary',
replica_state=constants.REPLICA_STATE_IN_SYNC, as_primitive=False)
replicas = [active_replica, replica]
self.mock_object(
db, 'share_snapshot_get', mock.Mock(return_value=snapshot))
self.mock_object(
self.share_manager, '_get_share_server',
mock.Mock(return_value=None))
self.mock_object(
db, 'share_replicas_get_all_by_share',
mock.Mock(return_value=replicas))
self.mock_object(
db, 'share_snapshot_instance_get_all_with_filters',
mock.Mock(side_effect=[snapshot_instances,
[snapshot_instances[0]]]))
mock_driver = self.mock_object(self.share_manager, 'driver')
mock_share_update = self.mock_object(
self.share_manager.db, 'share_update')
mock_share_replica_update = self.mock_object(
self.share_manager.db, 'share_replica_update')
mock_share_snapshot_instance_update = self.mock_object(
self.share_manager.db, 'share_snapshot_instance_update')
self.share_manager._revert_to_replicated_snapshot(
self.context, share, snapshot, reservations, share_id=share_id)
self.assertTrue(mock_driver.revert_to_replicated_snapshot.called)
self.assertFalse(mock_quotas_rollback.called)
if reservations:
mock_quotas_commit.assert_called_once_with(
mock.ANY, reservations, project_id='fake_project',
user_id='fake_user')
else:
self.assertFalse(mock_quotas_commit.called)
mock_share_update.assert_called_once_with(
mock.ANY, share_id, {'size': snapshot['size']})
mock_share_replica_update.assert_called_once_with(
mock.ANY, active_replica['id'],
{'status': constants.STATUS_AVAILABLE})
mock_share_snapshot_instance_update.assert_called_once_with(
mock.ANY, snapshot['instance']['id'],
{'status': constants.STATUS_AVAILABLE})
@ddt.data(None, 'fake_reservations')
def test_revert_to_replicated_snapshot_driver_exception(
self, reservations):
mock_quotas_rollback = self.mock_object(quota.QUOTAS, 'rollback')
mock_quotas_commit = self.mock_object(quota.QUOTAS, 'commit')
share_id = 'id1'
share = fakes.fake_share(
id=share_id, project_id='fake_project', user_id='fake_user')
snapshot = fakes.fake_snapshot(
create_instance=True, share=share, size=1)
snapshot_instance = fakes.fake_snapshot_instance(
base_snapshot=snapshot)
snapshot_instances = [snapshot['instance'], snapshot_instance]
active_replica = fake_replica(
id='rid1', share_id=share_id, host=self.share_manager.host,
replica_state=constants.REPLICA_STATE_ACTIVE, as_primitive=False)
replica = fake_replica(
id='rid2', share_id=share_id, host='secondary',
replica_state=constants.REPLICA_STATE_IN_SYNC, as_primitive=False)
replicas = [active_replica, replica]
self.mock_object(
db, 'share_snapshot_get', mock.Mock(return_value=snapshot))
self.mock_object(
self.share_manager, '_get_share_server',
mock.Mock(return_value=None))
self.mock_object(
db, 'share_replicas_get_all_by_share',
mock.Mock(return_value=replicas))
self.mock_object(
db, 'share_snapshot_instance_get_all_with_filters',
mock.Mock(side_effect=[snapshot_instances,
[snapshot_instances[0]]]))
mock_driver = self.mock_object(self.share_manager, 'driver')
mock_driver.revert_to_replicated_snapshot.side_effect = (
exception.ManilaException)
mock_share_update = self.mock_object(
self.share_manager.db, 'share_update')
mock_share_replica_update = self.mock_object(
self.share_manager.db, 'share_replica_update')
mock_share_snapshot_instance_update = self.mock_object(
self.share_manager.db, 'share_snapshot_instance_update')
self.assertRaises(exception.ManilaException,
self.share_manager._revert_to_replicated_snapshot,
self.context,
share,
snapshot,
reservations,
share_id=share_id)
self.assertTrue(mock_driver.revert_to_replicated_snapshot.called)
self.assertFalse(mock_quotas_commit.called)
if reservations:
mock_quotas_rollback.assert_called_once_with(
mock.ANY, reservations, project_id='fake_project',
user_id='fake_user')
else:
self.assertFalse(mock_quotas_rollback.called)
self.assertFalse(mock_share_update.called)
mock_share_replica_update.assert_called_once_with(
mock.ANY, active_replica['id'],
{'status': constants.STATUS_REVERTING_ERROR})
mock_share_snapshot_instance_update.assert_called_once_with(
mock.ANY, snapshot['instance']['id'],
{'status': constants.STATUS_AVAILABLE})
def delete_replicated_snapshot_driver_exception(self):
snapshot, snapshot_instances, replicas = (
self._setup_crud_replicated_snapshot_data()

View File

@ -325,6 +325,15 @@ class ShareRpcAPITestCase(test.TestCase):
snapshot=self.fake_snapshot,
host='fake_host')
def test_revert_to_snapshot(self):
self._test_share_api('revert_to_snapshot',
rpc_method='cast',
version='1.13',
share=self.fake_share,
snapshot=self.fake_snapshot,
host='fake_host',
reservations={'fake': 'fake'})
def test_create_replicated_snapshot(self):
self._test_share_api('create_replicated_snapshot',
rpc_method='cast',

View File

@ -78,6 +78,7 @@ class ShareTypesTestCase(test.TestCase):
fake_optional_extra_specs = {
constants.ExtraSpecs.SNAPSHOT_SUPPORT: 'true',
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT: 'false',
constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT: 'false',
}
fake_type_w_valid_extra = {
@ -237,7 +238,8 @@ class ShareTypesTestCase(test.TestCase):
@ddt.data(*(
list(itertools.product(
(constants.ExtraSpecs.SNAPSHOT_SUPPORT,
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT),
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT,
constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT),
strutils.TRUE_STRINGS + strutils.FALSE_STRINGS))) +
list(itertools.product(
(constants.ExtraSpecs.REPLICATION_TYPE_SPEC,),

View File

@ -10,11 +10,13 @@
# License for the specific language governing permissions and limitations
# under the License.
# Shares
STATUS_ERROR = 'error'
STATUS_AVAILABLE = 'available'
STATUS_ERROR_DELETING = 'error_deleting'
TEMPEST_MANILA_PREFIX = 'tempest-manila'
# Replication
REPLICATION_STYLE_READABLE = 'readable'
REPLICATION_STYLE_WRITABLE = 'writable'
REPLICATION_STYLE_DR = 'dr'
@ -31,6 +33,7 @@ REPLICATION_STATE_ACTIVE = 'active'
REPLICATION_STATE_IN_SYNC = 'in_sync'
REPLICATION_STATE_OUT_OF_SYNC = 'out_of_sync'
# Access Rules
RULE_STATE_ACTIVE = 'active'
RULE_STATE_OUT_OF_SYNC = 'out_of_sync'
RULE_STATE_ERROR = 'error'
@ -50,3 +53,10 @@ TASK_STATE_DATA_COPYING_COMPLETING = 'data_copying_completing'
TASK_STATE_DATA_COPYING_COMPLETED = 'data_copying_completed'
TASK_STATE_DATA_COPYING_CANCELLED = 'data_copying_cancelled'
TASK_STATE_DATA_COPYING_ERROR = 'data_copying_error'
# Revert to snapshot
REVERT_TO_SNAPSHOT_MICROVERSION = '2.27'
REVERT_TO_SNAPSHOT_SUPPORT = 'revert_to_snapshot_support'
STATUS_RESTORING = 'restoring'
STATUS_REVERTING = 'reverting'
STATUS_REVERTING_ERROR = 'reverting_error'

View File

@ -30,7 +30,7 @@ ShareGroup = [
help="The minimum api microversion is configured to be the "
"value of the minimum microversion supported by Manila."),
cfg.StrOpt("max_api_microversion",
default="2.26",
default="2.27",
help="The maximum api microversion is configured to be the "
"value of the latest microversion supported by Manila."),
cfg.StrOpt("region",
@ -103,7 +103,11 @@ ShareGroup = [
"Defaults to the value of run_snapshot_tests. Set it to "
"False if the driver being tested does not support "
"creating shares from snapshots."),
cfg.BoolOpt("capability_revert_to_snapshot_support",
help="Defines extra spec that satisfies specific back end "
"capability called 'revert_to_snapshot_support' "
"and will be used for setting up custom share type. "
"Defaults to the value of run_revert_to_snapshot_tests."),
cfg.StrOpt("share_network_id",
default="",
help="Some backend drivers requires share network "
@ -161,6 +165,11 @@ ShareGroup = [
help="Defines whether to run tests that use share snapshots "
"or not. Disable this feature if used driver doesn't "
"support it."),
cfg.BoolOpt("run_revert_to_snapshot_tests",
default=False,
help="Defines whether to run tests that revert shares "
"to snapshots or not. Enable this feature if used "
"driver supports it."),
cfg.BoolOpt("run_consistency_group_tests",
default=True,
help="Defines whether to run consistency group tests or not. "

View File

@ -50,6 +50,12 @@ class ManilaTempestPlugin(plugins.TempestPlugin):
conf.share.run_snapshot_tests,
group="share",
)
if conf.share.capability_revert_to_snapshot_support is None:
conf.set_default(
"capability_revert_to_snapshot_support",
conf.share.run_revert_to_snapshot_tests,
group="share",
)
def get_opt_lists(self):
return [(config_share.share_group.name, config_share.ShareGroup),

View File

@ -551,6 +551,67 @@ class SharesV2Client(shares_client.SharesClient):
self.expected_success(202, resp.status)
return body
###############
def revert_to_snapshot(self, share_id, snapshot_id,
version=LATEST_MICROVERSION):
url = 'shares/%s/action' % share_id
body = json.dumps({'revert': {'snapshot_id': snapshot_id}})
resp, body = self.post(url, body, version=version)
self.expected_success(202, resp.status)
return self._parse_resp(body)
###############
def create_share_type_extra_specs(self, share_type_id, extra_specs,
version=LATEST_MICROVERSION):
url = "types/%s/extra_specs" % share_type_id
post_body = json.dumps({'extra_specs': extra_specs})
resp, body = self.post(url, post_body, version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def get_share_type_extra_spec(self, share_type_id, extra_spec_name,
version=LATEST_MICROVERSION):
uri = "types/%s/extra_specs/%s" % (share_type_id, extra_spec_name)
resp, body = self.get(uri, version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def get_share_type_extra_specs(self, share_type_id, params=None,
version=LATEST_MICROVERSION):
uri = "types/%s/extra_specs" % share_type_id
if params is not None:
uri += '?%s' % urlparse.urlencode(params)
resp, body = self.get(uri, version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def update_share_type_extra_spec(self, share_type_id, spec_name,
spec_value, version=LATEST_MICROVERSION):
uri = "types/%s/extra_specs/%s" % (share_type_id, spec_name)
extra_spec = {spec_name: spec_value}
post_body = json.dumps(extra_spec)
resp, body = self.put(uri, post_body, version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def update_share_type_extra_specs(self, share_type_id, extra_specs,
version=LATEST_MICROVERSION):
uri = "types/%s/extra_specs" % share_type_id
extra_specs = {"extra_specs": extra_specs}
post_body = json.dumps(extra_specs)
resp, body = self.post(uri, post_body, version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def delete_share_type_extra_spec(self, share_type_id, extra_spec_name,
version=LATEST_MICROVERSION):
uri = "types/%s/extra_specs/%s" % (share_type_id, extra_spec_name)
resp, body = self.delete(uri, version=version)
self.expected_success(202, resp.status)
return body
###############
def get_snapshot_instance(self, instance_id, version=LATEST_MICROVERSION):
@ -726,13 +787,6 @@ class SharesV2Client(shares_client.SharesClient):
self.expected_success(200, resp.status)
return self._parse_resp(body)
def delete_share_type_extra_spec(self, share_type_id, extra_spec_name,
version=LATEST_MICROVERSION):
uri = "types/%s/extra_specs/%s" % (share_type_id, extra_spec_name)
resp, body = self.delete(uri, version=version)
self.expected_success(202, resp.status)
return body
def list_access_to_share_type(self, share_type_id,
version=LATEST_MICROVERSION,
action_name=None):

View File

@ -14,11 +14,16 @@
# under the License.
import ddt
from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib import exceptions as lib_exc
from testtools import testcase as tc
from manila_tempest_tests.common import constants
from manila_tempest_tests.tests.api import base
from manila_tempest_tests import utils
CONF = config.CONF
@ddt.ddt
@ -69,8 +74,12 @@ class ExtraSpecsAdminNegativeTest(base.BaseSharesMixedTest):
share_type = self.shares_v2_client.get_share_type(
st['share_type']['id'])
# Verify a non-admin can only read the required extra-specs
expected_keys = ['driver_handles_share_servers', 'snapshot_support',
'create_share_from_snapshot_support']
expected_keys = ['driver_handles_share_servers', 'snapshot_support']
if utils.is_microversion_ge(CONF.share.max_api_microversion, '2.24'):
expected_keys.append('create_share_from_snapshot_support')
if utils.is_microversion_ge(CONF.share.max_api_microversion,
constants.REVERT_TO_SNAPSHOT_MICROVERSION):
expected_keys.append('revert_to_snapshot_support')
actual_keys = share_type['share_type']['extra_specs'].keys()
self.assertEqual(sorted(expected_keys), sorted(actual_keys),
'Incorrect extra specs visible to non-admin user; '

View File

@ -740,6 +740,8 @@ class BaseSharesTest(test.BaseTestCase):
CONF.share.capability_snapshot_support)
create_from_snapshot_support = six.text_type(
CONF.share.capability_create_share_from_snapshot_support)
revert_to_snapshot_support = six.text_type(
CONF.share.capability_revert_to_snapshot_support)
extra_specs_dict = {
"driver_handles_share_servers": dhss,
@ -748,6 +750,7 @@ class BaseSharesTest(test.BaseTestCase):
optional = {
"snapshot_support": snapshot_support,
"create_share_from_snapshot_support": create_from_snapshot_support,
"revert_to_snapshot_support": revert_to_snapshot_support,
}
# NOTE(gouthamr): In micro-versions < 2.24, snapshot_support is a
# required extra-spec

View File

@ -0,0 +1,109 @@
# Copyright 2016 Andrew Kerr
# 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 ddt
from tempest import config
from tempest.lib.common.utils import data_utils
from testtools import testcase as tc
from manila_tempest_tests.common import constants
from manila_tempest_tests.tests.api import base
CONF = config.CONF
@base.skip_if_microversion_not_supported(
constants.REVERT_TO_SNAPSHOT_MICROVERSION)
@ddt.ddt
class RevertToSnapshotTest(base.BaseSharesMixedTest):
@classmethod
def skip_checks(cls):
super(RevertToSnapshotTest, cls).skip_checks()
if not CONF.share.run_revert_to_snapshot_tests:
msg = "Revert to snapshot tests are disabled."
raise cls.skipException(msg)
if not CONF.share.capability_revert_to_snapshot_support:
msg = "Revert to snapshot support is disabled."
raise cls.skipException(msg)
if not CONF.share.capability_snapshot_support:
msg = "Snapshot support is disabled."
raise cls.skipException(msg)
if not CONF.share.run_snapshot_tests:
msg = "Snapshot tests are disabled."
raise cls.skipException(msg)
@classmethod
def resource_setup(cls):
super(RevertToSnapshotTest, cls).resource_setup()
cls.admin_client = cls.admin_shares_v2_client
pools = cls.admin_client.list_pools(detail=True)['pools']
revert_support = [
pool['capabilities'][constants.REVERT_TO_SNAPSHOT_SUPPORT]
for pool in pools]
if not any(revert_support):
msg = "Revert to snapshot not supported."
raise cls.skipException(msg)
cls.share_type_name = data_utils.rand_name("share-type")
extra_specs = {constants.REVERT_TO_SNAPSHOT_SUPPORT: True}
cls.revert_enabled_extra_specs = cls.add_extra_specs_to_dict(
extra_specs=extra_specs)
cls.share_type = cls.create_share_type(
cls.share_type_name,
extra_specs=cls.revert_enabled_extra_specs,
client=cls.admin_client)
cls.st_id = cls.share_type['share_type']['id']
cls.share = cls.create_share(share_type_id=cls.st_id)
@tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND)
@ddt.data(
*{constants.REVERT_TO_SNAPSHOT_MICROVERSION,
CONF.share.max_api_microversion}
)
def test_revert_to_latest_snapshot(self, version):
snapshot = self.create_snapshot_wait_for_active(self.share['id'],
cleanup_in_class=False)
self.shares_v2_client.revert_to_snapshot(
self.share['id'],
snapshot['id'],
version=version)
self.shares_v2_client.wait_for_share_status(self.share['id'],
constants.STATUS_AVAILABLE)
@tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND)
@ddt.data(
*{constants.REVERT_TO_SNAPSHOT_MICROVERSION,
CONF.share.max_api_microversion}
)
def test_revert_to_previous_snapshot(self, version):
snapshot1 = self.create_snapshot_wait_for_active(
self.share['id'], cleanup_in_class=False)
snapshot2 = self.create_snapshot_wait_for_active(
self.share['id'], cleanup_in_class=False)
self.shares_v2_client.delete_snapshot(snapshot2['id'])
self.shares_v2_client.wait_for_resource_deletion(
snapshot_id=snapshot2['id'])
self.shares_v2_client.revert_to_snapshot(self.share['id'],
snapshot1['id'],
version=version)
self.shares_v2_client.wait_for_share_status(self.share['id'],
constants.STATUS_AVAILABLE)

View File

@ -0,0 +1,162 @@
# Copyright 2016 Andrew Kerr
# 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 ddt
from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib import exceptions
from testtools import testcase as tc
from manila_tempest_tests.common import constants
from manila_tempest_tests.tests.api import base
CONF = config.CONF
@base.skip_if_microversion_not_supported(
constants.REVERT_TO_SNAPSHOT_MICROVERSION)
@ddt.ddt
class RevertToSnapshotNegativeTest(base.BaseSharesMixedTest):
@classmethod
def skip_checks(cls):
super(RevertToSnapshotNegativeTest, cls).skip_checks()
if not CONF.share.run_revert_to_snapshot_tests:
msg = "Revert to snapshot tests are disabled."
raise cls.skipException(msg)
if not CONF.share.capability_revert_to_snapshot_support:
msg = "Revert to snapshot support is disabled."
raise cls.skipException(msg)
if not CONF.share.capability_snapshot_support:
msg = "Snapshot support is disabled."
raise cls.skipException(msg)
if not CONF.share.run_snapshot_tests:
msg = "Snapshot tests are disabled."
raise cls.skipException(msg)
@classmethod
def resource_setup(cls):
super(RevertToSnapshotNegativeTest, cls).resource_setup()
cls.admin_client = cls.admin_shares_v2_client
pools = cls.admin_client.list_pools(detail=True)['pools']
revert_support = [
pool['capabilities'][constants.REVERT_TO_SNAPSHOT_SUPPORT]
for pool in pools]
if not any(revert_support):
msg = "Revert to snapshot not supported."
raise cls.skipException(msg)
cls.share_type_name = data_utils.rand_name("share-type")
extra_specs = {constants.REVERT_TO_SNAPSHOT_SUPPORT: True}
cls.revert_enabled_extra_specs = cls.add_extra_specs_to_dict(
extra_specs=extra_specs)
cls.share_type = cls.create_share_type(
cls.share_type_name,
extra_specs=cls.revert_enabled_extra_specs,
client=cls.admin_client)
cls.st_id = cls.share_type['share_type']['id']
cls.share = cls.create_share(share_type_id=cls.st_id)
cls.share2 = cls.create_share(share_type_id=cls.st_id)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
@ddt.data(
*{constants.REVERT_TO_SNAPSHOT_MICROVERSION,
CONF.share.max_api_microversion}
)
def test_revert_to_second_latest_snapshot(self, version):
snapshot1 = self.create_snapshot_wait_for_active(
self.share['id'], cleanup_in_class=False)
self.create_snapshot_wait_for_active(self.share['id'],
cleanup_in_class=False)
self.assertRaises(exceptions.Conflict,
self.shares_v2_client.revert_to_snapshot,
self.share['id'],
snapshot1['id'],
version=version)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
@ddt.data(
*{constants.REVERT_TO_SNAPSHOT_MICROVERSION,
CONF.share.max_api_microversion}
)
def test_revert_to_error_snapshot(self, version):
snapshot = self.create_snapshot_wait_for_active(self.share['id'],
cleanup_in_class=False)
self.admin_client.reset_state(snapshot['id'],
status=constants.STATUS_ERROR,
s_type='snapshots')
self.assertRaises(exceptions.Conflict,
self.shares_v2_client.revert_to_snapshot,
self.share['id'],
snapshot['id'],
version=version)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
@ddt.data(
*{constants.REVERT_TO_SNAPSHOT_MICROVERSION,
CONF.share.max_api_microversion}
)
def test_revert_error_share_to_snapshot(self, version):
snapshot = self.create_snapshot_wait_for_active(self.share['id'],
cleanup_in_class=False)
self.admin_client.reset_state(self.share['id'],
status=constants.STATUS_ERROR,
s_type='shares')
self.addCleanup(self.admin_client.reset_state,
self.share['id'],
status=constants.STATUS_AVAILABLE,
s_type='shares')
self.assertRaises(exceptions.Conflict,
self.shares_v2_client.revert_to_snapshot,
self.share['id'],
snapshot['id'],
version=version)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
@ddt.data(
*{constants.REVERT_TO_SNAPSHOT_MICROVERSION,
CONF.share.max_api_microversion}
)
def test_revert_to_missing_snapshot(self, version):
self.assertRaises(exceptions.BadRequest,
self.shares_v2_client.revert_to_snapshot,
self.share['id'],
self.share['id'],
version=version)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
@ddt.data(
*{constants.REVERT_TO_SNAPSHOT_MICROVERSION,
CONF.share.max_api_microversion}
)
def test_revert_to_invalid_snapshot(self, version):
snapshot = self.create_snapshot_wait_for_active(
self.share['id'], cleanup_in_class=False)
self.assertRaises(exceptions.BadRequest,
self.shares_v2_client.revert_to_snapshot,
self.share2['id'],
snapshot['id'],
version=version)

View File

@ -20,6 +20,7 @@ from tempest.lib.common.utils import data_utils
import testtools
from testtools import testcase as tc
from manila_tempest_tests.common import constants
from manila_tempest_tests.tests.api import base
from manila_tempest_tests import utils
@ -106,6 +107,9 @@ class SharesActionsTest(base.BaseSharesTest):
expected_keys.append("user_id")
if utils.is_microversion_ge(version, '2.24'):
expected_keys.append("create_share_from_snapshot_support")
if utils.is_microversion_ge(version,
constants.REVERT_TO_SNAPSHOT_MICROVERSION):
expected_keys.append("revert_to_snapshot_support")
actual_keys = list(share.keys())
[self.assertIn(key, actual_keys) for key in expected_keys]
@ -167,6 +171,12 @@ class SharesActionsTest(base.BaseSharesTest):
def test_get_share_with_create_share_from_snapshot_support(self):
self._get_share('2.24')
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
@utils.skip_if_microversion_not_supported(
constants.REVERT_TO_SNAPSHOT_MICROVERSION)
def test_get_share_with_revert_to_snapshot_support(self):
self._get_share(constants.REVERT_TO_SNAPSHOT_MICROVERSION)
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
def test_list_shares(self):
@ -213,6 +223,9 @@ class SharesActionsTest(base.BaseSharesTest):
keys.append("user_id")
if utils.is_microversion_ge(version, '2.24'):
keys.append("create_share_from_snapshot_support")
if utils.is_microversion_ge(version,
constants.REVERT_TO_SNAPSHOT_MICROVERSION):
keys.append("revert_to_snapshot_support")
[self.assertIn(key, sh.keys()) for sh in shares for key in keys]
# our shares in list and have no duplicates
@ -264,6 +277,13 @@ class SharesActionsTest(base.BaseSharesTest):
self):
self._list_shares_with_detail('2.24')
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
@utils.skip_if_microversion_not_supported(
constants.REVERT_TO_SNAPSHOT_MICROVERSION)
def test_list_shares_with_detail_with_revert_to_snapshot_support(self):
self._list_shares_with_detail(
constants.REVERT_TO_SNAPSHOT_MICROVERSION)
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
def test_list_shares_with_detail_filter_by_metadata(self):
filters = {'metadata': self.metadata}

View File

@ -0,0 +1,4 @@
---
features:
- Added revert-to-snapshot feature for regular and replicated shares.
- Added revert-to-snapshot support to the LVM driver.