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

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

@ -326,3 +326,31 @@ Request example
.. literalinclude:: samples/share-actions-unmanage-request.json .. literalinclude:: samples/share-actions-unmanage-request.json
:language: javascript :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

@ -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. | | ``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 List shares

@ -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 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 contained in a share. You can create, manage, update, and delete
share snapshots. After you create or manage a share snapshot, you 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 You can update a share snapshot to rename it, change its
description, or update its state to one of these supported states: 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`` - ``unmanage_error``
- ``restoring``
As administrator, you can also reset the state of a snapshot and As administrator, you can also reset the state of a snapshot and
force-delete a share snapshot in any state. Use the ``policy.json`` force-delete a share snapshot in any state. Use the ``policy.json``
file to grant permissions for these actions to other roles. file to grant permissions for these actions to other roles.

@ -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_QUOTA_TESTS=${RUN_MANILA_QUOTA_TESTS:-True}
RUN_MANILA_SHRINK_TESTS=${RUN_MANILA_SHRINK_TESTS:-True} RUN_MANILA_SHRINK_TESTS=${RUN_MANILA_SHRINK_TESTS:-True}
RUN_MANILA_SNAPSHOT_TESTS=${RUN_MANILA_SNAPSHOT_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_CG_TESTS=${RUN_MANILA_CG_TESTS:-True}
RUN_MANILA_MANAGE_TESTS=${RUN_MANILA_MANAGE_TESTS:-True} RUN_MANILA_MANAGE_TESTS=${RUN_MANILA_MANAGE_TESTS:-True}
RUN_MANILA_MANAGE_SNAPSHOT_TESTS=${RUN_MANILA_MANAGE_SNAPSHOT_TESTS:-False} 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_MANAGE_TESTS=False
RUN_MANILA_HOST_ASSISTED_MIGRATION_TESTS=True RUN_MANILA_HOST_ASSISTED_MIGRATION_TESTS=True
RUN_MANILA_SHRINK_TESTS=False 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_ip_rules_for_protocols 'nfs'
iniset $TEMPEST_CONFIG share enable_user_rules_for_protocols 'cifs' iniset $TEMPEST_CONFIG share enable_user_rules_for_protocols 'cifs'
iniset $TEMPEST_CONFIG share image_with_share_tools 'manila-service-image-master' 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_CG_TESTS=True
RUN_MANILA_MANAGE_TESTS=False RUN_MANILA_MANAGE_TESTS=False
RUN_MANILA_DRIVER_ASSISTED_MIGRATION_TESTS=True 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_ip_rules_for_protocols 'nfs'
iniset $TEMPEST_CONFIG share enable_user_rules_for_protocols 'cifs' iniset $TEMPEST_CONFIG share enable_user_rules_for_protocols 'cifs'
iniset $TEMPEST_CONFIG share enable_cert_rules_for_protocols '' 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 # Enable snapshot tests
iniset $TEMPEST_CONFIG share run_snapshot_tests $RUN_MANILA_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 # Enable consistency group tests
iniset $TEMPEST_CONFIG share run_consistency_group_tests $RUN_MANILA_CG_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 # Create share from snapshot support
iniset $TEMPEST_CONFIG share capability_create_share_from_snapshot_support $CAPABILITY_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 ip_version_for_ssh 4
iniset $TEMPEST_CONFIG validation network_for_ssh ${PRIVATE_NETWORK_NAME:-"private"} iniset $TEMPEST_CONFIG validation network_for_ssh ${PRIVATE_NETWORK_NAME:-"private"}

@ -99,6 +99,7 @@ elif [[ "$DRIVER" == "windows" ]]; then
save_configuration "SHARE_DRIVER" "manila.share.drivers.windows.windows_smb_driver.WindowsSMBDriver" save_configuration "SHARE_DRIVER" "manila.share.drivers.windows.windows_smb_driver.WindowsSMBDriver"
elif [[ "$DRIVER" == "dummy" ]]; then elif [[ "$DRIVER" == "dummy" ]]; then
driver_path="manila.tests.share.drivers.dummy.DummyDriver" 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 "MANILA_SERVICE_IMAGE_ENABLED" "False"
save_configuration "SHARE_DRIVER" "$driver_path" save_configuration "SHARE_DRIVER" "$driver_path"
save_configuration "SUPPRESS_ERRORS_IN_CLEANUP" "False" save_configuration "SUPPRESS_ERRORS_IN_CLEANUP" "False"
@ -148,6 +149,7 @@ elif [[ "$DRIVER" == "dummy" ]]; then
elif [[ "$DRIVER" == "lvm" ]]; then elif [[ "$DRIVER" == "lvm" ]]; then
MANILA_SERVICE_IMAGE_ENABLED=True 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_DRIVER" "manila.share.drivers.lvm.LVMShareDriver"
save_configuration "SHARE_BACKING_FILE_SIZE" "32000M" save_configuration "SHARE_BACKING_FILE_SIZE" "32000M"
elif [[ "$DRIVER" == "zfsonlinux" ]]; then elif [[ "$DRIVER" == "zfsonlinux" ]]; then

@ -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 type in pools without regard for whether creating shares from snapshots is
supported, and those shares will not support creating shares from snapshots. 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 Reporting Capabilities
---------------------- ----------------------
@ -210,6 +216,7 @@ example vendor prefix:
'compression': True, # 'compression': True, #
'snapshot_support': True, # 'snapshot_support': True, #
'create_share_from_snapshot_support': True, 'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': True,
'qos': True, # this backend supports QoS 'qos': True, # this backend supports QoS
'thin_provisioning': True, # 'thin_provisioning': True, #
'max_over_subscription_ratio': 10, # (mandatory for thin) 'max_over_subscription_ratio': 10, # (mandatory for thin)
@ -238,6 +245,7 @@ example vendor prefix:
# allow creating # allow creating
# shares from # shares from
# snapshots # snapshots
'revert_to_snapshot_support': True,
'reserved_percentage': 0, 'reserved_percentage': 0,
'dedupe': False, 'dedupe': False,
'compression': False, 'compression': False,

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

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

@ -171,5 +171,12 @@ brctl: CommandFilter, brctl, root
# manila/share/drivers/container/container.py: e2fsck <whatever> # manila/share/drivers/container/container.py: e2fsck <whatever>
e2fsck: CommandFilter, e2fsck, root 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' # manila/data/utils.py: 'sha256sum', '%s'
sha256sum: CommandFilter, sha256sum, root sha256sum: CommandFilter, sha256sum, root

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

@ -156,3 +156,10 @@ user documentation.
---- ----
Removed nova-net plugin support and removed 'nova_net_id' parameter from Removed nova-net plugin support and removed 'nova_net_id' parameter from
share_network API. 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.

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

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from oslo_log import log
import six import six
import webob import webob
from webob import exc 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_accesses as share_access_views
from manila.api.views import share_migration as share_migration_views from manila.api.views import share_migration as share_migration_views
from manila.api.views import shares as share_views from manila.api.views import shares as share_views
from manila.common import constants
from manila import db from manila import db
from manila import exception from manila import exception
from manila.i18n import _ from manila.i18n import _, _LI
from manila import share from manila import share
from manila import utils from manila import utils
LOG = log.getLogger(__name__)
class ShareController(shares.ShareMixin, class ShareController(shares.ShareMixin,
share_manage.ShareManageMixin, share_manage.ShareManageMixin,
@ -47,6 +51,118 @@ class ShareController(shares.ShareMixin,
self._access_view_builder = share_access_views.ViewBuilder() self._access_view_builder = share_access_views.ViewBuilder()
self._migration_view_builder = share_migration_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") @wsgi.Controller.api_version("2.0", "2.3")
def create(self, req, body): def create(self, req, body):
# Remove consistency group attributes # Remove consistency group attributes
@ -276,6 +392,11 @@ class ShareController(shares.ShareMixin,
def unmanage(self, req, id, body=None): def unmanage(self, req, id, body=None):
return self._unmanage(req, id, body) 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(): def create_resource():
return wsgi.Resource(ShareController()) return wsgi.Resource(ShareController())

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

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

@ -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): def share_snapshot_update(context, snapshot_id, values):
"""Set the given properties on an snapshot and update it. """Set the given properties on an snapshot and update it.

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

@ -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 @require_context
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True) @oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
def share_snapshot_update(context, snapshot_id, values): def share_snapshot_update(context, snapshot_id, values):

@ -264,7 +264,8 @@ class Share(BASE, ManilaBase):
# preferred. # preferred.
result = None result = None
if len(self.instances) > 0: 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_MIGRATING, constants.STATUS_AVAILABLE,
constants.STATUS_ERROR) constants.STATUS_ERROR)
other_statuses = ( other_statuses = (
@ -303,6 +304,7 @@ class Share(BASE, ManilaBase):
snapshot_id = Column(String(36)) snapshot_id = Column(String(36))
snapshot_support = Column(Boolean, default=True) snapshot_support = Column(Boolean, default=True)
create_share_from_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) replication_type = Column(String(255), nullable=True)
share_proto = Column(String(255)) share_proto = Column(String(255))
is_public = Column(Boolean, default=False) is_public = Column(Boolean, default=False)

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

@ -46,6 +46,7 @@ def generate_stats(host_state, properties):
'snapshot_support': host_state.snapshot_support, 'snapshot_support': host_state.snapshot_support,
'create_share_from_snapshot_support': 'create_share_from_snapshot_support':
host_state.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_domain': host_state.replication_domain,
'replication_type': host_state.replication_type, 'replication_type': host_state.replication_type,
'provisioned_capacity_gb': host_state.provisioned_capacity_gb, 'provisioned_capacity_gb': host_state.provisioned_capacity_gb,

@ -266,40 +266,49 @@ class API(base.Base):
""" """
inferred_map = constants.ExtraSpecs.INFERRED_OPTIONAL_MAP inferred_map = constants.ExtraSpecs.INFERRED_OPTIONAL_MAP
snapshot_support_default = inferred_map.get( snapshot_support_key = constants.ExtraSpecs.SNAPSHOT_SUPPORT
constants.ExtraSpecs.SNAPSHOT_SUPPORT)
create_share_from_snapshot_support_default = inferred_map.get(
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT)
create_share_from_snapshot_key = ( create_share_from_snapshot_key = (
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT) constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT)
revert_to_snapshot_key = (
constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT)
try: snapshot_support_default = inferred_map.get(snapshot_support_key)
if share_type: create_share_from_snapshot_support_default = inferred_map.get(
snapshot_support = share_types.parse_boolean_extra_spec( create_share_from_snapshot_key)
constants.ExtraSpecs.SNAPSHOT_SUPPORT, 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( share_type.get('extra_specs', {}).get(
constants.ExtraSpecs.SNAPSHOT_SUPPORT, create_share_from_snapshot_key,
snapshot_support_default)) create_share_from_snapshot_support_default)))
create_share_from_snapshot_support = ( revert_to_snapshot_support = (
share_types.parse_boolean_extra_spec( share_types.parse_boolean_extra_spec(
create_share_from_snapshot_key, share_type.get( revert_to_snapshot_key,
'extra_specs', {}).get( share_type.get('extra_specs', {}).get(
create_share_from_snapshot_key, revert_to_snapshot_key,
create_share_from_snapshot_support_default))) revert_to_snapshot_support_default)))
replication_type = share_type.get('extra_specs', {}).get( replication_type = share_type.get('extra_specs', {}).get(
'replication_type') 'replication_type')
else: else:
snapshot_support = snapshot_support_default snapshot_support = snapshot_support_default
create_share_from_snapshot_support = ( create_share_from_snapshot_support = (
create_share_from_snapshot_support_default) create_share_from_snapshot_support_default)
replication_type = None revert_to_snapshot_support = revert_to_snapshot_support_default
except Exception as e: replication_type = None
raise exception.InvalidParameterValue(six.text_type(e))
return { return {
'snapshot_support': snapshot_support, 'snapshot_support': snapshot_support,
'create_share_from_snapshot_support': 'create_share_from_snapshot_support':
create_share_from_snapshot_support, create_share_from_snapshot_support,
'revert_to_snapshot_support': revert_to_snapshot_support,
'replication_type': replication_type, 'replication_type': replication_type,
} }
@ -382,6 +391,7 @@ class API(base.Base):
'snapshot_support': share['snapshot_support'], 'snapshot_support': share['snapshot_support'],
'create_share_from_snapshot_support': 'create_share_from_snapshot_support':
share['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_proto': share['share_proto'],
'share_type_id': share_type_id, 'share_type_id': share_type_id,
'is_public': share['is_public'], 'is_public': share['is_public'],
@ -614,6 +624,12 @@ class API(base.Base):
share_type.get('extra_specs', {}).get( share_type.get('extra_specs', {}).get(
'create_share_from_snapshot_support') '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_proto': kwargs.get('share_proto', share.get('share_proto')),
'share_type_id': share_type['id'], 'share_type_id': share_type['id'],
'is_public': kwargs.get('is_public', share.get('is_public')), '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) 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') @policy.wrap_check_policy('share')
def delete(self, context, share, force=False): def delete(self, context, share, force=False):
"""Delete share.""" """Delete share."""
@ -1379,6 +1509,10 @@ class API(base.Base):
snapshots = results snapshots = results
return snapshots 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, def allow_access(self, ctx, share, access_type, access_to,
access_level=None): access_level=None):
"""Allow access to share.""" """Allow access to share."""

@ -855,6 +855,24 @@ class ShareDriver(object):
the failure. 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): def extend_share(self, share, new_size, share_server=None):
"""Extends size of existing share. """Extends size of existing share.
@ -957,6 +975,7 @@ class ShareDriver(object):
snapshot_support=self.snapshots_are_supported, snapshot_support=self.snapshots_are_supported,
create_share_from_snapshot_support=( create_share_from_snapshot_support=(
self.creating_shares_from_snapshots_is_supported), self.creating_shares_from_snapshots_is_supported),
revert_to_snapshot_support=False,
replication_domain=self.replication_domain, replication_domain=self.replication_domain,
filter_function=self.get_filter_function(), filter_function=self.get_filter_function(),
goodness_function=self.get_goodness_function(), goodness_function=self.get_goodness_function(),
@ -1788,6 +1807,38 @@ class ShareDriver(object):
""" """
raise NotImplementedError() 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, def delete_replicated_snapshot(self, context, replica_list,
replica_snapshots, share_server=None): replica_snapshots, share_server=None):
"""Delete a snapshot by deleting its instances across the replicas. """Delete a snapshot by deleting its instances across the replicas.

@ -108,7 +108,7 @@ class LVMMixin(driver.ExecuteMixin):
raise raise
LOG.warning(_LW("Volume not found: %s") % exc.stderr) 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.""" """Creates a snapshot."""
orig_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group, orig_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group,
snapshot['share_name']) snapshot['share_name'])
@ -121,6 +121,9 @@ class LVMMixin(driver.ExecuteMixin):
'tune2fs', '-U', 'random', snapshot_device_name, run_as_root=True, '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): def delete_snapshot(self, context, snapshot, share_server=None):
"""Deletes a snapshot.""" """Deletes a snapshot."""
self._deallocate_container(snapshot['name']) self._deallocate_container(snapshot['name'])
@ -188,6 +191,7 @@ class LVMShareDriver(LVMMixin, driver.ShareDriver):
'consistency_group_support': None, 'consistency_group_support': None,
'snapshot_support': True, 'snapshot_support': True,
'create_share_from_snapshot_support': True, 'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': True,
'driver_name': 'LVMShareDriver', 'driver_name': 'LVMShareDriver',
'pools': self.get_share_server_pools() 'pools': self.get_share_server_pools()
} }
@ -356,3 +360,25 @@ class LVMShareDriver(LVMMixin, driver.ShareDriver):
device_name = self._get_local_path(share) device_name = self._get_local_path(share)
self._extend_container(share, device_name, new_size) self._extend_container(share, device_name, new_size)
self._execute('resize2fs', device_name, run_as_root=True) 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)

@ -188,7 +188,7 @@ def add_hooks(f):
class ShareManager(manager.SchedulerDependentManager): class ShareManager(manager.SchedulerDependentManager):
"""Manages NAS storages.""" """Manages NAS storages."""
RPC_API_VERSION = '1.12' RPC_API_VERSION = '1.13'
def __init__(self, share_driver=None, service_name=None, *args, **kwargs): def __init__(self, share_driver=None, service_name=None, *args, **kwargs):
"""Load the driver from args, or from flags.""" """Load the driver from args, or from flags."""
@ -2140,6 +2140,76 @@ class ShareManager(manager.SchedulerDependentManager):
self.db.share_snapshot_instance_delete( self.db.share_snapshot_instance_delete(
context, snapshot_instance['id']) 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 @add_hooks
@utils.require_driver_initialized @utils.require_driver_initialized
def delete_share_instance(self, context, share_instance_id, force=False): 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( self.db.share_snapshot_instance_update(
context, instance['id'], instance) 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 @add_hooks
@utils.require_driver_initialized @utils.require_driver_initialized
@locked_share_replica_operation @locked_share_replica_operation
@ -3236,7 +3390,8 @@ class ShareManager(manager.SchedulerDependentManager):
return share_replica_ref 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 # TODO(gouthamr): remove method when the db layer returns primitives
snapshot_instance_ref = { snapshot_instance_ref = {
'name': snapshot_instance.get('name'), 'name': snapshot_instance.get('name'),
@ -3255,4 +3410,9 @@ class ShareManager(manager.SchedulerDependentManager):
'provider_location': snapshot_instance.get('provider_location'), 'provider_location': snapshot_instance.get('provider_location'),
} }
if snapshot:
snapshot_instance_ref.update({
'size': snapshot.get('size'),
})
return snapshot_instance_ref return snapshot_instance_ref

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

@ -266,6 +266,8 @@ def is_valid_optional_extra_spec(key, value):
return parse_boolean_extra_spec(key, value) is not None return parse_boolean_extra_spec(key, value) is not None
elif key == constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT: elif key == constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT:
return parse_boolean_extra_spec(key, value) is not None 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: elif key == constants.ExtraSpecs.REPLICATION_TYPE_SPEC:
return value in constants.ExtraSpecs.REPLICATION_TYPES return value in constants.ExtraSpecs.REPLICATION_TYPES

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

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

@ -22,6 +22,7 @@ from oslo_config import cfg
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
import six import six
import webob import webob
import webob.exc
from manila.api import common from manila.api import common
from manila.api.openstack import api_version_request as api_version 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.contrib import stubs
from manila.tests.api import fakes from manila.tests.api import fakes
from manila.tests import db_utils from manila.tests import db_utils
from manila.tests import fake_share
from manila import utils from manila import utils
CONF = cfg.CONF CONF = cfg.CONF
@ -61,12 +63,14 @@ class ShareAPITest(test.TestCase):
stubs.stub_snapshot_get) stubs.stub_snapshot_get)
self.maxDiff = None self.maxDiff = None
self.share = { self.share = {
"id": "1",
"size": 100, "size": 100,
"display_name": "Share Test Name", "display_name": "Share Test Name",
"display_description": "Share Test Desc", "display_description": "Share Test Desc",
"share_proto": "fakeproto", "share_proto": "fakeproto",
"availability_zone": "zone1:host1", "availability_zone": "zone1:host1",
"is_public": False, "is_public": False,
"task_state": None,
} }
self.create_mock = mock.Mock( self.create_mock = mock.Mock(
return_value=stubs.stub_share( return_value=stubs.stub_share(
@ -83,6 +87,12 @@ class ShareAPITest(test.TestCase):
'id': 'fake_volume_type_id', 'id': 'fake_volume_type_id',
'name': 'fake_volume_type_name', 'name': 'fake_volume_type_name',
} }
self.snapshot = {
'id': '2',
'share_id': '1',
'status': constants.STATUS_AVAILABLE,
}
CONF.set_default("default_share_type", None) CONF.set_default("default_share_type", None)
def _get_expected_share_detailed_response(self, values=None, admin=False): 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' share['share_server_id'] = 'fake_share_server_id'
return {'share': share} 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") @ddt.data("2.0", "2.1")
def test_share_create_original(self, microversion): def test_share_create_original(self, microversion):
self.mock_object(share_api.API, 'create', self.create_mock) self.mock_object(share_api.API, 'create', self.create_mock)
@ -2071,3 +2331,28 @@ class ShareManageTest(test.TestCase):
self.controller.manage, self.controller.manage,
req, req,
share_id) 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')

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

@ -1371,6 +1371,7 @@ class CreateFromSnapshotExtraSpecAndShareColumn(BaseMigrationChecks):
# Pre-existing Shares must be present # Pre-existing Shares must be present
shares_in_db = engine.execute(shares_table.select()).fetchall() shares_in_db = engine.execute(shares_table.select()).fetchall()
share_ids_in_db = [s['id'] for s in shares_in_db] 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: for share_id in share_ids:
self.test_case.assertIn(share_id, share_ids_in_db) self.test_case.assertIn(share_id, share_ids_in_db)
@ -1420,6 +1421,7 @@ class CreateFromSnapshotExtraSpecAndShareColumn(BaseMigrationChecks):
# Pre-existing Shares must be present # Pre-existing Shares must be present
shares_in_db = engine.execute(shares_table.select()).fetchall() shares_in_db = engine.execute(shares_table.select()).fetchall()
share_ids_in_db = [s['id'] for s in shares_in_db] 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: for share_id in share_ids:
self.test_case.assertIn(share_id, share_ids_in_db) 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)) 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') @map_to_migration('95e3cf760840')
class RemoveNovaNetIdColumnFromShareNetworks(BaseMigrationChecks): class RemoveNovaNetIdColumnFromShareNetworks(BaseMigrationChecks):
table_name = 'share_networks' table_name = 'share_networks'

@ -17,6 +17,8 @@
"""Testing of SQLAlchemy backend.""" """Testing of SQLAlchemy backend."""
import copy
import ddt import ddt
import mock import mock
@ -894,6 +896,35 @@ class ShareSnapshotDatabaseAPITestCase(test.TestCase):
self.assertEqual(1, len(actual_result.instances)) self.assertEqual(1, len(actual_result.instances))
self.assertSubDictMatch(values, actual_result.to_dict()) 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): def test_get_instance(self):
snapshot = db_utils.create_snapshot(with_share=True) snapshot = db_utils.create_snapshot(with_share=True)

@ -76,6 +76,28 @@ class ShareTestCase(test.TestCase):
self.assertEqual(constants.STATUS_CREATING, share.instance['status']) 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, @ddt.data(constants.STATUS_AVAILABLE, constants.STATUS_ERROR,
constants.STATUS_CREATING) constants.STATUS_CREATING)
def test_share_instance_replication_change(self, status): def test_share_instance_replication_change(self, status):

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

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

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

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

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

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

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

@ -54,7 +54,11 @@ def fake_snapshot(**kwargs):
'name': 'fakesnapshotname', 'name': 'fakesnapshotname',
'share_proto': 'NFS', 'share_proto': 'NFS',
'export_location': '127.0.0.1:/mnt/nfs/volume-00002', 'export_location': '127.0.0.1:/mnt/nfs/volume-00002',
'share': {'size': 1}, 'share': {
'id': 'fakeid',
'name': 'fakename',
'size': 1
},
} }
snapshot.update(kwargs) snapshot.update(kwargs)
return db_fakes.FakeModel(snapshot) return db_fakes.FakeModel(snapshot)
@ -520,3 +524,26 @@ class LVMShareDriverTestCase(test.TestCase):
self.assertTrue(self._driver._stats['snapshot_support']) self.assertTrue(self._driver._stats['snapshot_support'])
self.assertEqual('LVMShareDriver', self._driver._stats['driver_name']) self.assertEqual('LVMShareDriver', self._driver._stats['driver_name'])
self.assertEqual('test-pool', self._driver._stats['pools']) 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())

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

@ -749,6 +749,7 @@ class ShareAPITestCase(test.TestCase):
'extra_specs': { 'extra_specs': {
'snapshot_support': True, 'snapshot_support': True,
'create_share_from_snapshot_support': False, 'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': False,
'replication_type': 'dr', 'replication_type': 'dr',
} }
} }
@ -765,6 +766,7 @@ class ShareAPITestCase(test.TestCase):
expected = { expected = {
'snapshot_support': False, 'snapshot_support': False,
'create_share_from_snapshot_support': False, 'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': False,
'replication_type': None, 'replication_type': None,
} }
self.assertEqual(expected, result) self.assertEqual(expected, result)
@ -773,7 +775,7 @@ class ShareAPITestCase(test.TestCase):
{'extra_specs': {'create_share_from_snapshot_support': 'fake'}}) {'extra_specs': {'create_share_from_snapshot_support': 'fake'}})
def test_get_share_attributes_from_share_type_invalid(self, share_type): 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, self.api._get_share_attributes_from_share_type,
share_type) share_type)
@ -798,6 +800,7 @@ class ShareAPITestCase(test.TestCase):
'snapshot_support': False, 'snapshot_support': False,
'replication_type': replication_type, 'replication_type': replication_type,
'create_share_from_snapshot_support': False, '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'], 'snapshot_support': fake_type['extra_specs']['snapshot_support'],
'create_share_from_snapshot_support': 'create_share_from_snapshot_support':
fake_type['extra_specs']['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, 'replication_type': replication_type,
}) })
@ -883,6 +888,9 @@ class ShareAPITestCase(test.TestCase):
'create_share_from_snapshot_support', 'create_share_from_snapshot_support',
share_type['extra_specs'] share_type['extra_specs']
['create_share_from_snapshot_support']), ['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_proto': kwargs.get('share_proto', share.get('share_proto')),
'share_type_id': share_type['id'], 'share_type_id': share_type['id'],
'is_public': kwargs.get('is_public', share.get('is_public')), '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( db_api.share_snapshot_create.assert_called_once_with(
self.context, options) 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): def test_manage_snapshot_share_not_found(self):
snapshot = fakes.fake_snapshot(share_id='fake_share', snapshot = fakes.fake_snapshot(share_id='fake_share',
as_primitive=True) as_primitive=True)
@ -1098,6 +1148,218 @@ class ShareAPITestCase(test.TestCase):
mock_rpc_call.assert_called_once_with( mock_rpc_call.assert_called_once_with(
self.context, snapshot, fake_host) 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): def test_create_snapshot_for_replicated_share(self):
share = fakes.fake_share( share = fakes.fake_share(
has_replicas=True, status=constants.STATUS_AVAILABLE) has_replicas=True, status=constants.STATUS_AVAILABLE)
@ -2051,6 +2313,7 @@ class ShareAPITestCase(test.TestCase):
'extra_specs': { 'extra_specs': {
'snapshot_support': False, 'snapshot_support': False,
'create_share_from_snapshot_support': False, 'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': False,
'driver_handles_share_servers': dhss, 'driver_handles_share_servers': dhss,
}, },
} }
@ -2061,6 +2324,7 @@ class ShareAPITestCase(test.TestCase):
'extra_specs': { 'extra_specs': {
'snapshot_support': False, 'snapshot_support': False,
'create_share_from_snapshot_support': False, 'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': False,
'driver_handles_share_servers': dhss, 'driver_handles_share_servers': dhss,
}, },
} }

@ -230,6 +230,14 @@ class ShareDriverTestCase(test.TestCase):
self._assert_is_callable(share_driver, method) 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) @ddt.data(True, False)
def test_get_share_server_pools(self, value): def test_get_share_server_pools(self, value):
driver.CONF.set_default('driver_handles_share_servers', value) driver.CONF.set_default('driver_handles_share_servers', value)

@ -1367,7 +1367,8 @@ class ShareManagerTestCase(test.TestCase):
self.assertIsNone(retval) self.assertIsNone(retval)
self.assertTrue(replica_update_call.called) 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 = { expected_snapshot_instance_dict = {
'status': constants.STATUS_CREATING, 'status': constants.STATUS_CREATING,
'share_id': share['id'], 'share_id': share['id'],
@ -1384,6 +1385,10 @@ class ShareManagerTestCase(test.TestCase):
'deleted_at': snapshot_instance['deleted_at'], 'deleted_at': snapshot_instance['deleted_at'],
'provider_location': snapshot_instance['provider_location'], 'provider_location': snapshot_instance['provider_location'],
} }
if snapshot:
expected_snapshot_instance_dict.update({
'size': snapshot['size'],
})
return expected_snapshot_instance_dict return expected_snapshot_instance_dict
def test_create_snapshot_driver_exception(self): def test_create_snapshot_driver_exception(self):
@ -4837,6 +4842,162 @@ class ShareManagerTestCase(test.TestCase):
if quota_error: if quota_error:
self.assertTrue(mock_log_warning.called) 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): def _setup_crud_replicated_snapshot_data(self):
snapshot = fakes.fake_snapshot(create_instance=True) snapshot = fakes.fake_snapshot(create_instance=True)
snapshot_instance = fakes.fake_snapshot_instance( snapshot_instance = fakes.fake_snapshot_instance(
@ -4928,6 +5089,135 @@ class ShareManagerTestCase(test.TestCase):
mock_db_update_call.assert_called_once_with( mock_db_update_call.assert_called_once_with(
self.context, snapshot['instance']['id'], snapshot_dict) 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): def delete_replicated_snapshot_driver_exception(self):
snapshot, snapshot_instances, replicas = ( snapshot, snapshot_instances, replicas = (
self._setup_crud_replicated_snapshot_data() self._setup_crud_replicated_snapshot_data()

@ -325,6 +325,15 @@ class ShareRpcAPITestCase(test.TestCase):
snapshot=self.fake_snapshot, snapshot=self.fake_snapshot,
host='fake_host') 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): def test_create_replicated_snapshot(self):
self._test_share_api('create_replicated_snapshot', self._test_share_api('create_replicated_snapshot',
rpc_method='cast', rpc_method='cast',

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

@ -10,11 +10,13 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
# Shares
STATUS_ERROR = 'error' STATUS_ERROR = 'error'
STATUS_AVAILABLE = 'available' STATUS_AVAILABLE = 'available'
STATUS_ERROR_DELETING = 'error_deleting' STATUS_ERROR_DELETING = 'error_deleting'
TEMPEST_MANILA_PREFIX = 'tempest-manila' TEMPEST_MANILA_PREFIX = 'tempest-manila'
# Replication
REPLICATION_STYLE_READABLE = 'readable' REPLICATION_STYLE_READABLE = 'readable'
REPLICATION_STYLE_WRITABLE = 'writable' REPLICATION_STYLE_WRITABLE = 'writable'
REPLICATION_STYLE_DR = 'dr' REPLICATION_STYLE_DR = 'dr'
@ -31,6 +33,7 @@ REPLICATION_STATE_ACTIVE = 'active'
REPLICATION_STATE_IN_SYNC = 'in_sync' REPLICATION_STATE_IN_SYNC = 'in_sync'
REPLICATION_STATE_OUT_OF_SYNC = 'out_of_sync' REPLICATION_STATE_OUT_OF_SYNC = 'out_of_sync'
# Access Rules
RULE_STATE_ACTIVE = 'active' RULE_STATE_ACTIVE = 'active'
RULE_STATE_OUT_OF_SYNC = 'out_of_sync' RULE_STATE_OUT_OF_SYNC = 'out_of_sync'
RULE_STATE_ERROR = 'error' 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_COMPLETED = 'data_copying_completed'
TASK_STATE_DATA_COPYING_CANCELLED = 'data_copying_cancelled' TASK_STATE_DATA_COPYING_CANCELLED = 'data_copying_cancelled'
TASK_STATE_DATA_COPYING_ERROR = 'data_copying_error' 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'

@ -30,7 +30,7 @@ ShareGroup = [
help="The minimum api microversion is configured to be the " help="The minimum api microversion is configured to be the "
"value of the minimum microversion supported by Manila."), "value of the minimum microversion supported by Manila."),
cfg.StrOpt("max_api_microversion", cfg.StrOpt("max_api_microversion",
default="2.26", default="2.27",
help="The maximum api microversion is configured to be the " help="The maximum api microversion is configured to be the "
"value of the latest microversion supported by Manila."), "value of the latest microversion supported by Manila."),
cfg.StrOpt("region", cfg.StrOpt("region",
@ -103,7 +103,11 @@ ShareGroup = [
"Defaults to the value of run_snapshot_tests. Set it to " "Defaults to the value of run_snapshot_tests. Set it to "
"False if the driver being tested does not support " "False if the driver being tested does not support "
"creating shares from snapshots."), "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", cfg.StrOpt("share_network_id",
default="", default="",
help="Some backend drivers requires share network " help="Some backend drivers requires share network "
@ -161,6 +165,11 @@ ShareGroup = [
help="Defines whether to run tests that use share snapshots " help="Defines whether to run tests that use share snapshots "
"or not. Disable this feature if used driver doesn't " "or not. Disable this feature if used driver doesn't "
"support it."), "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", cfg.BoolOpt("run_consistency_group_tests",
default=True, default=True,
help="Defines whether to run consistency group tests or not. " help="Defines whether to run consistency group tests or not. "

@ -50,6 +50,12 @@ class ManilaTempestPlugin(plugins.TempestPlugin):
conf.share.run_snapshot_tests, conf.share.run_snapshot_tests,
group="share", 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): def get_opt_lists(self):
return [(config_share.share_group.name, config_share.ShareGroup), return [(config_share.share_group.name, config_share.ShareGroup),

@ -551,6 +551,67 @@ class SharesV2Client(shares_client.SharesClient):
self.expected_success(202, resp.status) self.expected_success(202, resp.status)
return body 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): 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) self.expected_success(200, resp.status)
return self._parse_resp(body) 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, def list_access_to_share_type(self, share_type_id,
version=LATEST_MICROVERSION, version=LATEST_MICROVERSION,
action_name=None): action_name=None):

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

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

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

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

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

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