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:
parent
8bdf0d476d
commit
d4a379d083
@ -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
|
||||
: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. |
|
||||
+----------------------------------------+--------------------------------------------------------+
|
||||
| ``reverting`` | Share is being reverted to a snapshot. |
|
||||
+----------------------------------------+--------------------------------------------------------+
|
||||
| ``reverting_error`` | Share revert to snapshot failed. |
|
||||
+----------------------------------------+--------------------------------------------------------+
|
||||
|
||||
|
||||
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
|
||||
contained in a share. You can create, manage, update, and delete
|
||||
share snapshots. After you create or manage a share snapshot, you
|
||||
can create a share from it.
|
||||
can create a share from it. You can also revert a share to its most
|
||||
recent snapshot.
|
||||
|
||||
You can update a share snapshot to rename it, change its
|
||||
description, or update its state to one of these supported states:
|
||||
@ -31,6 +32,8 @@ description, or update its state to one of these supported states:
|
||||
|
||||
- ``unmanage_error``
|
||||
|
||||
- ``restoring``
|
||||
|
||||
As administrator, you can also reset the state of a snapshot and
|
||||
force-delete a share snapshot in any state. Use the ``policy.json``
|
||||
file to grant permissions for these actions to other roles.
|
||||
|
@ -69,6 +69,7 @@ PASSWORD_FOR_SAMBA_USER=${PASSWORD_FOR_SAMBA_USER:-$USERNAME_FOR_USER_RULES}
|
||||
RUN_MANILA_QUOTA_TESTS=${RUN_MANILA_QUOTA_TESTS:-True}
|
||||
RUN_MANILA_SHRINK_TESTS=${RUN_MANILA_SHRINK_TESTS:-True}
|
||||
RUN_MANILA_SNAPSHOT_TESTS=${RUN_MANILA_SNAPSHOT_TESTS:-True}
|
||||
RUN_MANILA_REVERT_TO_SNAPSHOT_TESTS=${RUN_MANILA_REVERT_TO_SNAPSHOT_TESTS:-False}
|
||||
RUN_MANILA_CG_TESTS=${RUN_MANILA_CG_TESTS:-True}
|
||||
RUN_MANILA_MANAGE_TESTS=${RUN_MANILA_MANAGE_TESTS:-True}
|
||||
RUN_MANILA_MANAGE_SNAPSHOT_TESTS=${RUN_MANILA_MANAGE_SNAPSHOT_TESTS:-False}
|
||||
@ -164,6 +165,7 @@ if [[ "$DRIVER" == "lvm" ]]; then
|
||||
RUN_MANILA_MANAGE_TESTS=False
|
||||
RUN_MANILA_HOST_ASSISTED_MIGRATION_TESTS=True
|
||||
RUN_MANILA_SHRINK_TESTS=False
|
||||
RUN_MANILA_REVERT_TO_SNAPSHOT_TESTS=True
|
||||
iniset $TEMPEST_CONFIG share enable_ip_rules_for_protocols 'nfs'
|
||||
iniset $TEMPEST_CONFIG share enable_user_rules_for_protocols 'cifs'
|
||||
iniset $TEMPEST_CONFIG share image_with_share_tools 'manila-service-image-master'
|
||||
@ -207,6 +209,7 @@ elif [[ "$DRIVER" == "dummy" ]]; then
|
||||
RUN_MANILA_CG_TESTS=True
|
||||
RUN_MANILA_MANAGE_TESTS=False
|
||||
RUN_MANILA_DRIVER_ASSISTED_MIGRATION_TESTS=True
|
||||
RUN_MANILA_REVERT_TO_SNAPSHOT_TESTS=True
|
||||
iniset $TEMPEST_CONFIG share enable_ip_rules_for_protocols 'nfs'
|
||||
iniset $TEMPEST_CONFIG share enable_user_rules_for_protocols 'cifs'
|
||||
iniset $TEMPEST_CONFIG share enable_cert_rules_for_protocols ''
|
||||
@ -243,6 +246,9 @@ iniset $TEMPEST_CONFIG share run_shrink_tests $RUN_MANILA_SHRINK_TESTS
|
||||
# Enable snapshot tests
|
||||
iniset $TEMPEST_CONFIG share run_snapshot_tests $RUN_MANILA_SNAPSHOT_TESTS
|
||||
|
||||
# Enable revert to snapshot tests
|
||||
iniset $TEMPEST_CONFIG share run_revert_to_snapshot_tests $RUN_MANILA_REVERT_TO_SNAPSHOT_TESTS
|
||||
|
||||
# Enable consistency group tests
|
||||
iniset $TEMPEST_CONFIG share run_consistency_group_tests $RUN_MANILA_CG_TESTS
|
||||
|
||||
@ -262,6 +268,10 @@ iniset $TEMPEST_CONFIG share run_driver_assisted_migration_tests $RUN_MANILA_DRI
|
||||
# Create share from snapshot support
|
||||
iniset $TEMPEST_CONFIG share capability_create_share_from_snapshot_support $CAPABILITY_CREATE_SHARE_FROM_SNAPSHOT_SUPPORT
|
||||
|
||||
# Revert share to snapshot support
|
||||
CAPABILITY_REVERT_TO_SNAPSHOT_SUPPORT=${CAPABILITY_REVERT_TO_SNAPSHOT_SUPPORT:-$RUN_MANILA_REVERT_TO_SNAPSHOT_TESTS}
|
||||
iniset $TEMPEST_CONFIG share capability_revert_to_snapshot_support $CAPABILITY_REVERT_TO_SNAPSHOT_SUPPORT
|
||||
|
||||
iniset $TEMPEST_CONFIG validation ip_version_for_ssh 4
|
||||
iniset $TEMPEST_CONFIG validation network_for_ssh ${PRIVATE_NETWORK_NAME:-"private"}
|
||||
|
||||
|
@ -99,6 +99,7 @@ elif [[ "$DRIVER" == "windows" ]]; then
|
||||
save_configuration "SHARE_DRIVER" "manila.share.drivers.windows.windows_smb_driver.WindowsSMBDriver"
|
||||
elif [[ "$DRIVER" == "dummy" ]]; then
|
||||
driver_path="manila.tests.share.drivers.dummy.DummyDriver"
|
||||
DEFAULT_EXTRA_SPECS="'snapshot_support=True create_share_from_snapshot_support=True revert_to_snapshot_support=True'"
|
||||
save_configuration "MANILA_SERVICE_IMAGE_ENABLED" "False"
|
||||
save_configuration "SHARE_DRIVER" "$driver_path"
|
||||
save_configuration "SUPPRESS_ERRORS_IN_CLEANUP" "False"
|
||||
@ -148,6 +149,7 @@ elif [[ "$DRIVER" == "dummy" ]]; then
|
||||
|
||||
elif [[ "$DRIVER" == "lvm" ]]; then
|
||||
MANILA_SERVICE_IMAGE_ENABLED=True
|
||||
DEFAULT_EXTRA_SPECS="'snapshot_support=True create_share_from_snapshot_support=True revert_to_snapshot_support=True'"
|
||||
save_configuration "SHARE_DRIVER" "manila.share.drivers.lvm.LVMShareDriver"
|
||||
save_configuration "SHARE_BACKING_FILE_SIZE" "32000M"
|
||||
elif [[ "$DRIVER" == "zfsonlinux" ]]; then
|
||||
|
@ -171,6 +171,12 @@ can be used verbatim as extra_specs in share types used to create shares.
|
||||
type in pools without regard for whether creating shares from snapshots is
|
||||
supported, and those shares will not support creating shares from snapshots.
|
||||
|
||||
* `revert_to_snapshot_support` - indicates that a driver is capable of
|
||||
reverting a share in place to its most recent snapshot. When administrators
|
||||
do not set this capability as an extra-spec in a share type, the scheduler
|
||||
can place new shares of that type in pools without regard for whether
|
||||
reverting shares to snapshots is supported, and those shares will not support
|
||||
reverting shares to snapshots.
|
||||
|
||||
Reporting Capabilities
|
||||
----------------------
|
||||
@ -210,6 +216,7 @@ example vendor prefix:
|
||||
'compression': True, #
|
||||
'snapshot_support': True, #
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': True,
|
||||
'qos': True, # this backend supports QoS
|
||||
'thin_provisioning': True, #
|
||||
'max_over_subscription_ratio': 10, # (mandatory for thin)
|
||||
@ -238,6 +245,7 @@ example vendor prefix:
|
||||
# allow creating
|
||||
# shares from
|
||||
# snapshots
|
||||
'revert_to_snapshot_support': True,
|
||||
'reserved_percentage': 0,
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
|
@ -30,57 +30,58 @@ Column value "-" means that this feature is not currently supported.
|
||||
Mapping of share drivers and share features support
|
||||
---------------------------------------------------
|
||||
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| Driver name | create delete share | manage unmanage share | extend share | shrink share | create delete snapshot | create share from snapshot | manage unmanage snapshot |
|
||||
+========================================+=======================+=======================+==============+==============+========================+============================+==========================+
|
||||
| ZFSonLinux | M | N | M | M | M | M | N |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| Container | N | \- | N | \- | \- | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| Generic (Cinder as back-end) | J | K | L | L | J | J | M |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| NetApp Clustered Data ONTAP | J | L | L | L | J | J | N |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| EMC VNX | J | \- | \- | \- | J | J | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| EMC Unity | N | \- | N | \- | N | N | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| EMC Isilon | K | \- | M | \- | K | K | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| Red Hat GlusterFS | J | \- | \- | \- | volume layout (L) | volume layout (L) | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| Red Hat GlusterFS-Native | J | \- | \- | \- | K | L | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| HDFS | K | \- | M | \- | K | K | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| Hitachi HNAS | L | L | L | M | L | L | O |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| Hitachi HSP | N | N | N | N | \- | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| HPE 3PAR | K | \- | \- | \- | K | K | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| Huawei | K | L | L | L | K | M | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| IBM GPFS | K | O | L | \- | K | K | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| LVM | M | \- | M | \- | M | M | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| Quobyte | K | \- | M | M | \- | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| Windows SMB | L | L | L | L | L | L | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| Oracle ZFSSA | K | N | M | M | K | K | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| CephFS Native | M | \- | M | M | M | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| Tegile | M | \- | M | M | M | M | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| NexentaStor4 | N | \- | N | \- | N | N | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| NexentaStor5 | N | \- | N | N | N | N | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
| MapRFS | O | O | O | O | O | O | O |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| Driver name | create delete share | manage unmanage share | extend share | shrink share | create delete snapshot | create share from snapshot | manage unmanage snapshot | revert to snapshot |
|
||||
+========================================+=======================+=======================+==============+==============+========================+============================+==========================+====================+
|
||||
| ZFSonLinux | M | N | M | M | M | M | N | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| Container | N | \- | N | \- | \- | \- | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| Generic (Cinder as back-end) | J | K | L | L | J | J | M | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| NetApp Clustered Data ONTAP | J | L | L | L | J | J | N | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| EMC VNX | J | \- | \- | \- | J | J | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| EMC Unity | N | \- | N | \- | N | N | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| EMC Isilon | K | \- | M | \- | K | K | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| Red Hat GlusterFS | J | \- | \- | \- | volume layout (L) | volume layout (L) | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| Red Hat GlusterFS-Native | J | \- | \- | \- | K | L | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| HDFS | K | \- | M | \- | K | K | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| Hitachi HNAS | L | L | L | M | L | L | O | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| Hitachi HSP | N | N | N | N | \- | \- | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| HPE 3PAR | K | \- | \- | \- | K | K | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| Huawei | K | L | L | L | K | M | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| IBM GPFS | K | O | L | \- | K | K | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| LVM | M | \- | M | \- | M | M | \- | O |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| Quobyte | K | \- | M | M | \- | \- | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| Windows SMB | L | L | L | L | L | L | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| Oracle ZFSSA | K | N | M | M | K | K | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| CephFS Native | M | \- | M | M | M | \- | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| Tegile | M | \- | M | M | M | M | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| NexentaStor4 | N | \- | N | \- | N | N | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| NexentaStor5 | N | \- | N | N | N | N | \- | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
| MapRFS | O | O | O | O | O | O | O | \- |
|
||||
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+
|
||||
|
||||
|
||||
Mapping of share drivers and share access rules support
|
||||
-------------------------------------------------------
|
||||
@ -200,57 +201,57 @@ Mapping of share drivers and common capabilities
|
||||
|
||||
More information: :ref:`capabilities_and_extra_specs`
|
||||
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| Driver name | DHSS=True | DHSS=False | dedupe | compression | thin_provisioning | thick_provisioning | qos | create share from snapshot |
|
||||
+========================================+===========+============+========+=============+===================+====================+=====+============================+
|
||||
| ZFSonLinux | \- | M | M | M | M | \- | \- | M |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| Container | N | \- | \- | \- | \- | N | \- | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| Generic (Cinder as back-end) | J | K | \- | \- | \- | L | \- | J |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| NetApp Clustered Data ONTAP | J | K | M | M | M | L | \- | J |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| EMC VNX | J | \- | \- | \- | \- | L | \- | J |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| EMC Unity | N | \- | \- | \- | N | \- | \- | N |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| EMC Isilon | \- | K | \- | \- | \- | L | \- | K |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| Red Hat GlusterFS | \- | J | \- | \- | \- | L | \- | volume layout (L) |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| Red Hat GlusterFS-Native | \- | J | \- | \- | \- | L | \- | L |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| HDFS | \- | K | \- | \- | \- | L | \- | K |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| Hitachi HNAS | \- | L | N | \- | L | \- | \- | L |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| Hitachi HSP | \- | N | \- | \- | N | \- | \- | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| HPE 3PAR | L | K | L | \- | L | L | \- | K |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| Huawei | M | K | L | L | L | L | M | M |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| LVM | \- | M | \- | \- | \- | M | \- | K |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| Quobyte | \- | K | \- | \- | \- | L | \- | M |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| Windows SMB | L | L | \- | \- | \- | L | \- | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| IBM GPFS | \- | K | \- | \- | \- | L | \- | L |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| Oracle ZFSSA | \- | K | \- | \- | \- | L | \- | K |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| CephFS Native | \- | M | \- | \- | \- | M | \- | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| Tegile | \- | M | M | M | M | \- | \- | M |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| NexentaStor4 | \- | N | N | N | N | N | \- | N |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| NexentaStor5 | \- | N | N | N | N | N | \- | N |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
| MapRFS | \- | N | \- | \- | \- | N | \- | O |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| Driver name | DHSS=True | DHSS=False | dedupe | compression | thin_provisioning | thick_provisioning | qos | create share from snapshot | revert to snapshot |
|
||||
+========================================+===========+============+========+=============+===================+====================+=====+============================+====================+
|
||||
| ZFSonLinux | \- | M | M | M | M | \- | \- | M | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| Container | N | \- | \- | \- | \- | N | \- | \- | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| Generic (Cinder as back-end) | J | K | \- | \- | \- | L | \- | J | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| NetApp Clustered Data ONTAP | J | K | M | M | M | L | \- | J | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| EMC VNX | J | \- | \- | \- | \- | L | \- | J | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| EMC Unity | N | \- | \- | \- | N | \- | \- | N | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| EMC Isilon | \- | K | \- | \- | \- | L | \- | K | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| Red Hat GlusterFS | \- | J | \- | \- | \- | L | \- | volume layout (L) | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| Red Hat GlusterFS-Native | \- | J | \- | \- | \- | L | \- | L | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| HDFS | \- | K | \- | \- | \- | L | \- | K | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| Hitachi HNAS | \- | L | N | \- | L | \- | \- | L | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| Hitachi HSP | \- | N | \- | \- | N | \- | \- | \- | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| HPE 3PAR | L | K | L | \- | L | L | \- | K | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| Huawei | M | K | L | L | L | L | M | M | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| LVM | \- | M | \- | \- | \- | M | \- | K | O |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| Quobyte | \- | K | \- | \- | \- | L | \- | M | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| Windows SMB | L | L | \- | \- | \- | L | \- | \- | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| IBM GPFS | \- | K | \- | \- | \- | L | \- | L | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| Oracle ZFSSA | \- | K | \- | \- | \- | L | \- | K | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| CephFS Native | \- | M | \- | \- | \- | M | \- | \- | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| Tegile | \- | M | M | M | M | \- | \- | M | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| NexentaStor4 | \- | N | N | N | N | N | \- | N | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| NexentaStor5 | \- | N | N | N | N | N | \- | N | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
| MapRFS | \- | N | \- | \- | \- | N | \- | O | \- |
|
||||
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -41,6 +41,7 @@
|
||||
"share:unmanage": "rule:admin_api",
|
||||
"share:force_delete": "rule:admin_api",
|
||||
"share:reset_status": "rule:admin_api",
|
||||
"share:revert_to_snapshot": "rule:default",
|
||||
"share_export_location:index": "rule:default",
|
||||
"share_export_location:show": "rule:default",
|
||||
|
||||
|
@ -171,5 +171,12 @@ brctl: CommandFilter, brctl, root
|
||||
# manila/share/drivers/container/container.py: e2fsck <whatever>
|
||||
e2fsck: CommandFilter, e2fsck, root
|
||||
|
||||
# manila/share/drivers/lvm.py: lvconvert --merge %s
|
||||
lvconvert: CommandFilter, lvconvert, root
|
||||
|
||||
# manila/share/drivers/lvm.py: lvchange -an %s
|
||||
# manila/share/drivers/lvm.py: lvchange -ay %s
|
||||
lvchange: CommandFilter, lvchange, root
|
||||
|
||||
# manila/data/utils.py: 'sha256sum', '%s'
|
||||
sha256sum: CommandFilter, sha256sum, root
|
||||
|
@ -84,13 +84,15 @@ REST_API_VERSION_HISTORY = """
|
||||
spec. Also made the 'snapshot_support' extra spec optional.
|
||||
* 2.25 - Added quota-show detail API.
|
||||
* 2.26 - Removed 'nova_net_id' parameter from share_network API.
|
||||
|
||||
* 2.27 - Added share revert to snapshot API.
|
||||
"""
|
||||
|
||||
# The minimum and maximum versions of the API supported
|
||||
# The default api version request is defined to be the
|
||||
# minimum version of the API supported.
|
||||
_MIN_API_VERSION = "2.0"
|
||||
_MAX_API_VERSION = "2.26"
|
||||
_MAX_API_VERSION = "2.27"
|
||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||
|
||||
|
||||
|
@ -156,3 +156,10 @@ user documentation.
|
||||
----
|
||||
Removed nova-net plugin support and removed 'nova_net_id' parameter from
|
||||
share_network API.
|
||||
|
||||
2.27
|
||||
----
|
||||
Added share revert to snapshot. This API reverts a share to the specified
|
||||
snapshot. The share is reverted in place, and the snapshot must be the most
|
||||
recent one known to manila. The feature is controlled by a new standard
|
||||
optional extra spec, revert_to_snapshot_support.
|
||||
|
@ -267,8 +267,10 @@ class ShareMixin(object):
|
||||
# Verify that share can be created from a snapshot
|
||||
if (check_create_share_from_snapshot_support and
|
||||
not parent_share['create_share_from_snapshot_support']):
|
||||
msg = _("Share cannot be created from snapshot '%s', because "
|
||||
"share back end does not support it.") % snapshot_id
|
||||
msg = (_("A new share may not be created from snapshot '%s', "
|
||||
"because the snapshot's parent share does not have "
|
||||
"that capability.")
|
||||
% snapshot_id)
|
||||
LOG.error(msg)
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_log import log
|
||||
import six
|
||||
import webob
|
||||
from webob import exc
|
||||
@ -25,12 +26,15 @@ from manila.api.v1 import shares
|
||||
from manila.api.views import share_accesses as share_access_views
|
||||
from manila.api.views import share_migration as share_migration_views
|
||||
from manila.api.views import shares as share_views
|
||||
from manila.common import constants
|
||||
from manila import db
|
||||
from manila import exception
|
||||
from manila.i18n import _
|
||||
from manila.i18n import _, _LI
|
||||
from manila import share
|
||||
from manila import utils
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class ShareController(shares.ShareMixin,
|
||||
share_manage.ShareManageMixin,
|
||||
@ -47,6 +51,118 @@ class ShareController(shares.ShareMixin,
|
||||
self._access_view_builder = share_access_views.ViewBuilder()
|
||||
self._migration_view_builder = share_migration_views.ViewBuilder()
|
||||
|
||||
@wsgi.Controller.authorize('revert_to_snapshot')
|
||||
def _revert(self, req, id, body=None):
|
||||
"""Revert a share to a snapshot."""
|
||||
context = req.environ['manila.context']
|
||||
revert_data = self._validate_revert_parameters(context, body)
|
||||
|
||||
try:
|
||||
share_id = id
|
||||
snapshot_id = revert_data['snapshot_id']
|
||||
|
||||
share = self.share_api.get(context, share_id)
|
||||
snapshot = self.share_api.get_snapshot(context, snapshot_id)
|
||||
|
||||
# Ensure share supports reverting to a snapshot
|
||||
if not share['revert_to_snapshot_support']:
|
||||
msg_args = {'share_id': share_id, 'snap_id': snapshot_id}
|
||||
msg = _('Share %(share_id)s may not be reverted to snapshot '
|
||||
'%(snap_id)s, because the share does not have that '
|
||||
'capability.')
|
||||
raise exc.HTTPBadRequest(explanation=msg % msg_args)
|
||||
|
||||
# Ensure requested share & snapshot match.
|
||||
if share['id'] != snapshot['share_id']:
|
||||
msg_args = {'share_id': share_id, 'snap_id': snapshot_id}
|
||||
msg = _('Snapshot %(snap_id)s is not associated with share '
|
||||
'%(share_id)s.')
|
||||
raise exc.HTTPBadRequest(explanation=msg % msg_args)
|
||||
|
||||
# Ensure share status is 'available'.
|
||||
if share['status'] != constants.STATUS_AVAILABLE:
|
||||
msg_args = {
|
||||
'share_id': share_id,
|
||||
'state': share['status'],
|
||||
'available': constants.STATUS_AVAILABLE,
|
||||
}
|
||||
msg = _("Share %(share_id)s is in '%(state)s' state, but it "
|
||||
"must be in '%(available)s' state to be reverted to a "
|
||||
"snapshot.")
|
||||
raise exc.HTTPConflict(explanation=msg % msg_args)
|
||||
|
||||
# Ensure snapshot status is 'available'.
|
||||
if snapshot['status'] != constants.STATUS_AVAILABLE:
|
||||
msg_args = {
|
||||
'snap_id': snapshot_id,
|
||||
'state': snapshot['status'],
|
||||
'available': constants.STATUS_AVAILABLE,
|
||||
}
|
||||
msg = _("Snapshot %(snap_id)s is in '%(state)s' state, but it "
|
||||
"must be in '%(available)s' state to be restored.")
|
||||
raise exc.HTTPConflict(explanation=msg % msg_args)
|
||||
|
||||
# Ensure a long-running task isn't active on the share
|
||||
if share.is_busy:
|
||||
msg_args = {'share_id': share_id}
|
||||
msg = _("Share %(share_id)s may not be reverted while it has "
|
||||
"an active task.")
|
||||
raise exc.HTTPConflict(explanation=msg % msg_args)
|
||||
|
||||
# Ensure the snapshot is the most recent one.
|
||||
latest_snapshot = self.share_api.get_latest_snapshot_for_share(
|
||||
context, share_id)
|
||||
if not latest_snapshot:
|
||||
msg_args = {'share_id': share_id}
|
||||
msg = _("Could not determine the latest snapshot for share "
|
||||
"%(share_id)s.")
|
||||
raise exc.HTTPBadRequest(explanation=msg % msg_args)
|
||||
if latest_snapshot['id'] != snapshot_id:
|
||||
msg_args = {
|
||||
'share_id': share_id,
|
||||
'snap_id': snapshot_id,
|
||||
'latest_snap_id': latest_snapshot['id'],
|
||||
}
|
||||
msg = _("Snapshot %(snap_id)s may not be restored because "
|
||||
"it is not the most recent snapshot of share "
|
||||
"%(share_id)s. Currently the latest snapshot is "
|
||||
"%(latest_snap_id)s.")
|
||||
raise exc.HTTPConflict(explanation=msg % msg_args)
|
||||
|
||||
msg_args = {'share_id': share_id, 'snap_id': snapshot_id}
|
||||
msg = _LI('Reverting share %(share_id)s to snapshot %(snap_id)s.')
|
||||
LOG.info(msg, msg_args)
|
||||
|
||||
self.share_api.revert_to_snapshot(context, snapshot)
|
||||
except exception.ShareNotFound as e:
|
||||
raise exc.HTTPNotFound(explanation=six.text_type(e))
|
||||
except exception.ShareSnapshotNotFound as e:
|
||||
raise exc.HTTPBadRequest(explanation=six.text_type(e))
|
||||
except exception.ShareSizeExceedsAvailableQuota as e:
|
||||
raise exc.HTTPForbidden(explanation=six.text_type(e))
|
||||
except exception.ReplicationException as e:
|
||||
raise exc.HTTPBadRequest(explanation=six.text_type(e))
|
||||
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
def _validate_revert_parameters(self, context, body):
|
||||
if not (body and self.is_valid_body(body, 'revert')):
|
||||
msg = _("Revert entity not found in request body.")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
required_parameters = ('snapshot_id',)
|
||||
data = body['revert']
|
||||
|
||||
for parameter in required_parameters:
|
||||
if parameter not in data:
|
||||
msg = _("Required parameter %s not found.") % parameter
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
if not data.get(parameter):
|
||||
msg = _("Required parameter %s is empty.") % parameter
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
return data
|
||||
|
||||
@wsgi.Controller.api_version("2.0", "2.3")
|
||||
def create(self, req, body):
|
||||
# Remove consistency group attributes
|
||||
@ -276,6 +392,11 @@ class ShareController(shares.ShareMixin,
|
||||
def unmanage(self, req, id, body=None):
|
||||
return self._unmanage(req, id, body)
|
||||
|
||||
@wsgi.Controller.api_version('2.27')
|
||||
@wsgi.action('revert')
|
||||
def revert(self, req, id, body=None):
|
||||
return self._revert(req, id, body)
|
||||
|
||||
|
||||
def create_resource():
|
||||
return wsgi.Resource(ShareController())
|
||||
|
@ -30,6 +30,7 @@ class ViewBuilder(common.ViewBuilder):
|
||||
"add_replication_fields",
|
||||
"add_user_id",
|
||||
"add_create_share_from_snapshot_support_field",
|
||||
"add_revert_to_snapshot_support_field",
|
||||
]
|
||||
|
||||
def summary_list(self, request, shares):
|
||||
@ -148,6 +149,11 @@ class ViewBuilder(common.ViewBuilder):
|
||||
share_dict['create_share_from_snapshot_support'] = share.get(
|
||||
'create_share_from_snapshot_support')
|
||||
|
||||
@common.ViewBuilder.versioned_method("2.27")
|
||||
def add_revert_to_snapshot_support_field(self, context, share_dict, share):
|
||||
share_dict['revert_to_snapshot_support'] = share.get(
|
||||
'revert_to_snapshot_support')
|
||||
|
||||
def _list_view(self, func, request, shares):
|
||||
"""Provide a view for a list of shares."""
|
||||
shares_list = [func(request, share)['share'] for share in shares]
|
||||
|
@ -40,6 +40,9 @@ STATUS_SHRINKING_POSSIBLE_DATA_LOSS_ERROR = (
|
||||
'shrinking_possible_data_loss_error'
|
||||
)
|
||||
STATUS_REPLICATION_CHANGE = 'replication_change'
|
||||
STATUS_RESTORING = 'restoring'
|
||||
STATUS_REVERTING = 'reverting'
|
||||
STATUS_REVERTING_ERROR = 'reverting_error'
|
||||
|
||||
TASK_STATE_MIGRATION_STARTING = 'migration_starting'
|
||||
TASK_STATE_MIGRATION_IN_PROGRESS = 'migration_in_progress'
|
||||
@ -81,6 +84,7 @@ TRANSITIONAL_STATUSES = (
|
||||
STATUS_MANAGING, STATUS_UNMANAGING,
|
||||
STATUS_EXTENDING, STATUS_SHRINKING,
|
||||
STATUS_MIGRATING, STATUS_MIGRATING_TO,
|
||||
STATUS_RESTORING, STATUS_REVERTING,
|
||||
)
|
||||
|
||||
UPDATING_RULES_STATUSES = (
|
||||
@ -161,6 +165,7 @@ class ExtraSpecs(object):
|
||||
SNAPSHOT_SUPPORT = "snapshot_support"
|
||||
REPLICATION_TYPE_SPEC = "replication_type"
|
||||
CREATE_SHARE_FROM_SNAPSHOT_SUPPORT = "create_share_from_snapshot_support"
|
||||
REVERT_TO_SNAPSHOT_SUPPORT = "revert_to_snapshot_support"
|
||||
|
||||
# Extra specs containers
|
||||
REQUIRED = (
|
||||
@ -170,6 +175,7 @@ class ExtraSpecs(object):
|
||||
OPTIONAL = (
|
||||
SNAPSHOT_SUPPORT,
|
||||
CREATE_SHARE_FROM_SNAPSHOT_SUPPORT,
|
||||
REVERT_TO_SNAPSHOT_SUPPORT,
|
||||
REPLICATION_TYPE_SPEC,
|
||||
)
|
||||
|
||||
@ -182,6 +188,7 @@ class ExtraSpecs(object):
|
||||
DRIVER_HANDLES_SHARE_SERVERS,
|
||||
SNAPSHOT_SUPPORT,
|
||||
CREATE_SHARE_FROM_SNAPSHOT_SUPPORT,
|
||||
REVERT_TO_SNAPSHOT_SUPPORT,
|
||||
)
|
||||
|
||||
# NOTE(cknight): Some extra specs are optional, but a nominal (typically
|
||||
@ -190,6 +197,7 @@ class ExtraSpecs(object):
|
||||
INFERRED_OPTIONAL_MAP = {
|
||||
SNAPSHOT_SUPPORT: False,
|
||||
CREATE_SHARE_FROM_SNAPSHOT_SUPPORT: False,
|
||||
REVERT_TO_SNAPSHOT_SUPPORT: False,
|
||||
}
|
||||
|
||||
REPLICATION_TYPES = ('writable', 'readable', 'dr')
|
||||
|
@ -530,6 +530,11 @@ def share_snapshot_get_all_for_share(context, share_id, filters=None,
|
||||
)
|
||||
|
||||
|
||||
def share_snapshot_get_latest_for_share(context, share_id):
|
||||
"""Get the most recent snapshot for a share."""
|
||||
return IMPL.share_snapshot_get_latest_for_share(context, share_id)
|
||||
|
||||
|
||||
def share_snapshot_update(context, snapshot_id, values):
|
||||
"""Set the given properties on an snapshot and update it.
|
||||
|
||||
|
@ -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
|
||||
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
|
||||
def share_snapshot_update(context, snapshot_id, values):
|
||||
|
@ -264,7 +264,8 @@ class Share(BASE, ManilaBase):
|
||||
# preferred.
|
||||
result = None
|
||||
if len(self.instances) > 0:
|
||||
order = (constants.STATUS_REPLICATION_CHANGE,
|
||||
order = (constants.STATUS_REVERTING,
|
||||
constants.STATUS_REPLICATION_CHANGE,
|
||||
constants.STATUS_MIGRATING, constants.STATUS_AVAILABLE,
|
||||
constants.STATUS_ERROR)
|
||||
other_statuses = (
|
||||
@ -303,6 +304,7 @@ class Share(BASE, ManilaBase):
|
||||
snapshot_id = Column(String(36))
|
||||
snapshot_support = Column(Boolean, default=True)
|
||||
create_share_from_snapshot_support = Column(Boolean, default=True)
|
||||
revert_to_snapshot_support = Column(Boolean, default=False)
|
||||
replication_type = Column(String(255), nullable=True)
|
||||
share_proto = Column(String(255))
|
||||
is_public = Column(Boolean, default=False)
|
||||
|
@ -131,6 +131,7 @@ class HostState(object):
|
||||
self.driver_handles_share_servers = False
|
||||
self.snapshot_support = True
|
||||
self.create_share_from_snapshot_support = True
|
||||
self.revert_to_snapshot_support = False
|
||||
self.consistency_group_support = False
|
||||
self.dedupe = False
|
||||
self.compression = False
|
||||
@ -299,6 +300,10 @@ class HostState(object):
|
||||
pool_cap['create_share_from_snapshot_support'] = (
|
||||
self.create_share_from_snapshot_support)
|
||||
|
||||
if 'revert_to_snapshot_support' not in pool_cap:
|
||||
pool_cap['revert_to_snapshot_support'] = (
|
||||
self.revert_to_snapshot_support)
|
||||
|
||||
if not pool_cap.get('consistency_group_support'):
|
||||
pool_cap['consistency_group_support'] = \
|
||||
self.consistency_group_support
|
||||
@ -325,6 +330,8 @@ class HostState(object):
|
||||
self.snapshot_support = capability.get('snapshot_support')
|
||||
self.create_share_from_snapshot_support = capability.get(
|
||||
'create_share_from_snapshot_support')
|
||||
self.revert_to_snapshot_support = capability.get(
|
||||
'revert_to_snapshot_support', False)
|
||||
self.consistency_group_support = capability.get(
|
||||
'consistency_group_support', False)
|
||||
self.updated = capability['timestamp']
|
||||
|
@ -46,6 +46,7 @@ def generate_stats(host_state, properties):
|
||||
'snapshot_support': host_state.snapshot_support,
|
||||
'create_share_from_snapshot_support':
|
||||
host_state.create_share_from_snapshot_support,
|
||||
'revert_to_snapshot_support': host_state.revert_to_snapshot_support,
|
||||
'replication_domain': host_state.replication_domain,
|
||||
'replication_type': host_state.replication_type,
|
||||
'provisioned_capacity_gb': host_state.provisioned_capacity_gb,
|
||||
|
@ -266,40 +266,49 @@ class API(base.Base):
|
||||
"""
|
||||
|
||||
inferred_map = constants.ExtraSpecs.INFERRED_OPTIONAL_MAP
|
||||
snapshot_support_default = inferred_map.get(
|
||||
constants.ExtraSpecs.SNAPSHOT_SUPPORT)
|
||||
create_share_from_snapshot_support_default = inferred_map.get(
|
||||
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT)
|
||||
snapshot_support_key = constants.ExtraSpecs.SNAPSHOT_SUPPORT
|
||||
create_share_from_snapshot_key = (
|
||||
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT)
|
||||
revert_to_snapshot_key = (
|
||||
constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT)
|
||||
|
||||
try:
|
||||
if share_type:
|
||||
snapshot_support = share_types.parse_boolean_extra_spec(
|
||||
constants.ExtraSpecs.SNAPSHOT_SUPPORT,
|
||||
snapshot_support_default = inferred_map.get(snapshot_support_key)
|
||||
create_share_from_snapshot_support_default = inferred_map.get(
|
||||
create_share_from_snapshot_key)
|
||||
revert_to_snapshot_support_default = inferred_map.get(
|
||||
revert_to_snapshot_key)
|
||||
|
||||
if share_type:
|
||||
snapshot_support = share_types.parse_boolean_extra_spec(
|
||||
snapshot_support_key,
|
||||
share_type.get('extra_specs', {}).get(
|
||||
snapshot_support_key, snapshot_support_default))
|
||||
create_share_from_snapshot_support = (
|
||||
share_types.parse_boolean_extra_spec(
|
||||
create_share_from_snapshot_key,
|
||||
share_type.get('extra_specs', {}).get(
|
||||
constants.ExtraSpecs.SNAPSHOT_SUPPORT,
|
||||
snapshot_support_default))
|
||||
create_share_from_snapshot_support = (
|
||||
share_types.parse_boolean_extra_spec(
|
||||
create_share_from_snapshot_key, share_type.get(
|
||||
'extra_specs', {}).get(
|
||||
create_share_from_snapshot_key,
|
||||
create_share_from_snapshot_support_default)))
|
||||
replication_type = share_type.get('extra_specs', {}).get(
|
||||
'replication_type')
|
||||
else:
|
||||
snapshot_support = snapshot_support_default
|
||||
create_share_from_snapshot_support = (
|
||||
create_share_from_snapshot_support_default)
|
||||
replication_type = None
|
||||
except Exception as e:
|
||||
raise exception.InvalidParameterValue(six.text_type(e))
|
||||
create_share_from_snapshot_key,
|
||||
create_share_from_snapshot_support_default)))
|
||||
revert_to_snapshot_support = (
|
||||
share_types.parse_boolean_extra_spec(
|
||||
revert_to_snapshot_key,
|
||||
share_type.get('extra_specs', {}).get(
|
||||
revert_to_snapshot_key,
|
||||
revert_to_snapshot_support_default)))
|
||||
replication_type = share_type.get('extra_specs', {}).get(
|
||||
'replication_type')
|
||||
else:
|
||||
snapshot_support = snapshot_support_default
|
||||
create_share_from_snapshot_support = (
|
||||
create_share_from_snapshot_support_default)
|
||||
revert_to_snapshot_support = revert_to_snapshot_support_default
|
||||
replication_type = None
|
||||
|
||||
return {
|
||||
'snapshot_support': snapshot_support,
|
||||
'create_share_from_snapshot_support':
|
||||
create_share_from_snapshot_support,
|
||||
'revert_to_snapshot_support': revert_to_snapshot_support,
|
||||
'replication_type': replication_type,
|
||||
}
|
||||
|
||||
@ -382,6 +391,7 @@ class API(base.Base):
|
||||
'snapshot_support': share['snapshot_support'],
|
||||
'create_share_from_snapshot_support':
|
||||
share['create_share_from_snapshot_support'],
|
||||
'revert_to_snapshot_support': share['revert_to_snapshot_support'],
|
||||
'share_proto': share['share_proto'],
|
||||
'share_type_id': share_type_id,
|
||||
'is_public': share['is_public'],
|
||||
@ -614,6 +624,12 @@ class API(base.Base):
|
||||
share_type.get('extra_specs', {}).get(
|
||||
'create_share_from_snapshot_support')
|
||||
),
|
||||
'revert_to_snapshot_support': kwargs.get(
|
||||
'revert_to_snapshot_support',
|
||||
share_type.get('extra_specs', {}).get(
|
||||
'revert_to_snapshot_support')
|
||||
),
|
||||
|
||||
'share_proto': kwargs.get('share_proto', share.get('share_proto')),
|
||||
'share_type_id': share_type['id'],
|
||||
'is_public': kwargs.get('is_public', share.get('is_public')),
|
||||
@ -713,6 +729,120 @@ class API(base.Base):
|
||||
|
||||
self.share_rpcapi.unmanage_snapshot(context, snapshot_ref, host)
|
||||
|
||||
def revert_to_snapshot(self, context, snapshot):
|
||||
"""Revert a share to a snapshot."""
|
||||
|
||||
share = self.db.share_get(context, snapshot['share_id'])
|
||||
reservations = self._handle_revert_to_snapshot_quotas(
|
||||
context, share, snapshot)
|
||||
|
||||
try:
|
||||
if share.get('has_replicas'):
|
||||
self._revert_to_replicated_snapshot(
|
||||
context, share, snapshot, reservations)
|
||||
else:
|
||||
self._revert_to_snapshot(
|
||||
context, share, snapshot, reservations)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
if reservations:
|
||||
QUOTAS.rollback(context, reservations)
|
||||
|
||||
def _handle_revert_to_snapshot_quotas(self, context, share, snapshot):
|
||||
"""Reserve extra quota if a revert will result in a larger share."""
|
||||
|
||||
# Note(cknight): This value may be positive or negative.
|
||||
size_increase = snapshot['size'] - share['size']
|
||||
if not size_increase:
|
||||
return None
|
||||
|
||||
try:
|
||||
return QUOTAS.reserve(context,
|
||||
project_id=share['project_id'],
|
||||
gigabytes=size_increase,
|
||||
user_id=share['user_id'])
|
||||
except exception.OverQuota as exc:
|
||||
usages = exc.kwargs['usages']
|
||||
quotas = exc.kwargs['quotas']
|
||||
consumed_gb = (usages['gigabytes']['reserved'] +
|
||||
usages['gigabytes']['in_use'])
|
||||
|
||||
msg = _("Quota exceeded for %(s_pid)s. Reverting share "
|
||||
"%(s_sid)s to snapshot %(s_ssid)s will increase the "
|
||||
"share's size by %(s_size)sG, "
|
||||
"(%(d_consumed)dG of %(d_quota)dG already consumed).")
|
||||
msg_args = {
|
||||
's_pid': context.project_id,
|
||||
's_sid': share['id'],
|
||||
's_ssid': snapshot['id'],
|
||||
's_size': size_increase,
|
||||
'd_consumed': consumed_gb,
|
||||
'd_quota': quotas['gigabytes'],
|
||||
}
|
||||
message = msg % msg_args
|
||||
LOG.error(message)
|
||||
raise exception.ShareSizeExceedsAvailableQuota(message=message)
|
||||
|
||||
def _revert_to_snapshot(self, context, share, snapshot, reservations):
|
||||
"""Revert a non-replicated share to a snapshot."""
|
||||
|
||||
# Set status of share to 'reverting'
|
||||
self.db.share_update(
|
||||
context, snapshot['share_id'],
|
||||
{'status': constants.STATUS_REVERTING})
|
||||
|
||||
# Set status of snapshot to 'restoring'
|
||||
self.db.share_snapshot_update(
|
||||
context, snapshot['id'],
|
||||
{'status': constants.STATUS_RESTORING})
|
||||
|
||||
# Send revert API to share host
|
||||
self.share_rpcapi.revert_to_snapshot(
|
||||
context, share, snapshot, share['instance']['host'], reservations)
|
||||
|
||||
def _revert_to_replicated_snapshot(self, context, share, snapshot,
|
||||
reservations):
|
||||
"""Revert a replicated share to a snapshot."""
|
||||
|
||||
# Get active replica
|
||||
active_replica = self.db.share_replicas_get_available_active_replica(
|
||||
context, share['id'])
|
||||
|
||||
if not active_replica:
|
||||
msg = _('Share %s has no active replica in available state.')
|
||||
raise exception.ReplicationException(reason=msg % share['id'])
|
||||
|
||||
# Get snapshot instance on active replica
|
||||
snapshot_instance_filters = {
|
||||
'share_instance_ids': active_replica['id'],
|
||||
'snapshot_ids': snapshot['id'],
|
||||
}
|
||||
snapshot_instances = (
|
||||
self.db.share_snapshot_instance_get_all_with_filters(
|
||||
context, snapshot_instance_filters))
|
||||
active_snapshot_instance = (
|
||||
snapshot_instances[0] if snapshot_instances else None)
|
||||
|
||||
if not active_snapshot_instance:
|
||||
msg = _('Share %(share)s has no snapshot %(snap)s associated with '
|
||||
'its active replica.')
|
||||
msg_args = {'share': share['id'], 'snap': snapshot['id']}
|
||||
raise exception.ReplicationException(reason=msg % msg_args)
|
||||
|
||||
# Set active replica to 'reverting'
|
||||
self.db.share_replica_update(
|
||||
context, active_replica['id'],
|
||||
{'status': constants.STATUS_REVERTING})
|
||||
|
||||
# Set snapshot instance on active replica to 'restoring'
|
||||
self.db.share_snapshot_instance_update(
|
||||
context, active_snapshot_instance['id'],
|
||||
{'status': constants.STATUS_RESTORING})
|
||||
|
||||
# Send revert API to active replica host
|
||||
self.share_rpcapi.revert_to_snapshot(
|
||||
context, share, snapshot, active_replica['host'], reservations)
|
||||
|
||||
@policy.wrap_check_policy('share')
|
||||
def delete(self, context, share, force=False):
|
||||
"""Delete share."""
|
||||
@ -1379,6 +1509,10 @@ class API(base.Base):
|
||||
snapshots = results
|
||||
return snapshots
|
||||
|
||||
def get_latest_snapshot_for_share(self, context, share_id):
|
||||
"""Get the newest snapshot of a share."""
|
||||
return self.db.share_snapshot_get_latest_for_share(context, share_id)
|
||||
|
||||
def allow_access(self, ctx, share, access_type, access_to,
|
||||
access_level=None):
|
||||
"""Allow access to share."""
|
||||
|
@ -855,6 +855,24 @@ class ShareDriver(object):
|
||||
the failure.
|
||||
"""
|
||||
|
||||
def revert_to_snapshot(self, context, snapshot, share_server=None):
|
||||
"""Reverts a share (in place) to the specified snapshot.
|
||||
|
||||
Does not delete the share snapshot. The share and snapshot must both
|
||||
be 'available' for the restore to be attempted. The snapshot must be
|
||||
the most recent one taken by Manila; the API layer performs this check
|
||||
so the driver doesn't have to.
|
||||
|
||||
The share must be reverted in place to the contents of the snapshot.
|
||||
Application admins should quiesce or otherwise prepare the application
|
||||
for the shared file system contents to change suddenly.
|
||||
|
||||
:param context: Current context
|
||||
:param snapshot: The snapshot to be restored
|
||||
:param share_server: Optional -- Share server model or None
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def extend_share(self, share, new_size, share_server=None):
|
||||
"""Extends size of existing share.
|
||||
|
||||
@ -957,6 +975,7 @@ class ShareDriver(object):
|
||||
snapshot_support=self.snapshots_are_supported,
|
||||
create_share_from_snapshot_support=(
|
||||
self.creating_shares_from_snapshots_is_supported),
|
||||
revert_to_snapshot_support=False,
|
||||
replication_domain=self.replication_domain,
|
||||
filter_function=self.get_filter_function(),
|
||||
goodness_function=self.get_goodness_function(),
|
||||
@ -1788,6 +1807,38 @@ class ShareDriver(object):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def revert_to_replicated_snapshot(self, context, active_replica,
|
||||
replica_list, active_replica_snapshot,
|
||||
replica_snapshots, share_server=None):
|
||||
"""Reverts a replicated share (in place) to the specified snapshot.
|
||||
|
||||
.. note::
|
||||
This call is made on the 'active' replica's host, since drivers may
|
||||
not be able to revert snapshots on individual replicas.
|
||||
|
||||
Does not delete the share snapshot. The share and snapshot must both
|
||||
be 'available' for the restore to be attempted. The snapshot must be
|
||||
the most recent one taken by Manila; the API layer performs this check
|
||||
so the driver doesn't have to.
|
||||
|
||||
The share must be reverted in place to the contents of the snapshot.
|
||||
Application admins should quiesce or otherwise prepare the application
|
||||
for the shared file system contents to change suddenly.
|
||||
|
||||
:param context: Current context
|
||||
:param active_replica: The current active replica
|
||||
:param replica_list: List of all replicas for a particular share
|
||||
The 'active' replica will have its 'replica_state' attr set to
|
||||
'active' and its 'status' set to 'reverting'.
|
||||
:param active_replica_snapshot: snapshot to be restored
|
||||
:param replica_snapshots: List of dictionaries of snapshot instances.
|
||||
These snapshot instances track the snapshot across the replicas.
|
||||
The snapshot of the active replica to be restored with have its
|
||||
status attribute set to 'restoring'.
|
||||
:param share_server: Optional -- Share server model
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def delete_replicated_snapshot(self, context, replica_list,
|
||||
replica_snapshots, share_server=None):
|
||||
"""Delete a snapshot by deleting its instances across the replicas.
|
||||
|
@ -108,7 +108,7 @@ class LVMMixin(driver.ExecuteMixin):
|
||||
raise
|
||||
LOG.warning(_LW("Volume not found: %s") % exc.stderr)
|
||||
|
||||
def create_snapshot(self, context, snapshot, share_server=None):
|
||||
def _create_snapshot(self, context, snapshot):
|
||||
"""Creates a snapshot."""
|
||||
orig_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group,
|
||||
snapshot['share_name'])
|
||||
@ -121,6 +121,9 @@ class LVMMixin(driver.ExecuteMixin):
|
||||
'tune2fs', '-U', 'random', snapshot_device_name, run_as_root=True,
|
||||
)
|
||||
|
||||
def create_snapshot(self, context, snapshot, share_server=None):
|
||||
self._create_snapshot(context, snapshot)
|
||||
|
||||
def delete_snapshot(self, context, snapshot, share_server=None):
|
||||
"""Deletes a snapshot."""
|
||||
self._deallocate_container(snapshot['name'])
|
||||
@ -188,6 +191,7 @@ class LVMShareDriver(LVMMixin, driver.ShareDriver):
|
||||
'consistency_group_support': None,
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': True,
|
||||
'driver_name': 'LVMShareDriver',
|
||||
'pools': self.get_share_server_pools()
|
||||
}
|
||||
@ -356,3 +360,25 @@ class LVMShareDriver(LVMMixin, driver.ShareDriver):
|
||||
device_name = self._get_local_path(share)
|
||||
self._extend_container(share, device_name, new_size)
|
||||
self._execute('resize2fs', device_name, run_as_root=True)
|
||||
|
||||
def revert_to_snapshot(self, context, snapshot, share_server=None):
|
||||
# First we merge the snapshot LV and the share LV
|
||||
# This won't actually do anything until the LV is reactivated
|
||||
snap_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group,
|
||||
snapshot['name'])
|
||||
self._execute('lvconvert', '--merge', snap_lv_name, run_as_root=True)
|
||||
# Unmount the share so we can deactivate it
|
||||
share = snapshot['share']
|
||||
self._unmount_device(share)
|
||||
# Deactivate the share LV
|
||||
share_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group,
|
||||
share['name'])
|
||||
self._execute('lvchange', '-an', share_lv_name, run_as_root=True)
|
||||
# Reactivate the share LV. This will trigger the merge and delete the
|
||||
# snapshot.
|
||||
self._execute('lvchange', '-ay', share_lv_name, run_as_root=True)
|
||||
# Now recreate the snapshot that was destroyed by the merge
|
||||
self._create_snapshot(context, snapshot)
|
||||
# Finally we can mount the share again
|
||||
device_name = self._get_local_path(share)
|
||||
self._mount_device(share, device_name)
|
||||
|
@ -188,7 +188,7 @@ def add_hooks(f):
|
||||
class ShareManager(manager.SchedulerDependentManager):
|
||||
"""Manages NAS storages."""
|
||||
|
||||
RPC_API_VERSION = '1.12'
|
||||
RPC_API_VERSION = '1.13'
|
||||
|
||||
def __init__(self, share_driver=None, service_name=None, *args, **kwargs):
|
||||
"""Load the driver from args, or from flags."""
|
||||
@ -2140,6 +2140,76 @@ class ShareManager(manager.SchedulerDependentManager):
|
||||
self.db.share_snapshot_instance_delete(
|
||||
context, snapshot_instance['id'])
|
||||
|
||||
@add_hooks
|
||||
@utils.require_driver_initialized
|
||||
def revert_to_snapshot(self, context, snapshot_id, reservations,
|
||||
share_id=None):
|
||||
|
||||
context = context.elevated()
|
||||
snapshot = self.db.share_snapshot_get(context, snapshot_id)
|
||||
share = snapshot['share']
|
||||
share_id = share['id']
|
||||
|
||||
if share.get('has_replicas'):
|
||||
self._revert_to_replicated_snapshot(
|
||||
context, share, snapshot, reservations, share_id=share_id)
|
||||
else:
|
||||
self._revert_to_snapshot(context, share, snapshot, reservations)
|
||||
|
||||
def _revert_to_snapshot(self, context, share, snapshot, reservations):
|
||||
|
||||
share_server = self._get_share_server(context, share)
|
||||
share_id = share['id']
|
||||
snapshot_id = snapshot['id']
|
||||
project_id = share['project_id']
|
||||
user_id = share['user_id']
|
||||
|
||||
snapshot_instance = self.db.share_snapshot_instance_get(
|
||||
context, snapshot.instance['id'], with_share_data=True)
|
||||
|
||||
# Make primitive to pass the information to the driver
|
||||
snapshot_instance_dict = self._get_snapshot_instance_dict(
|
||||
context, snapshot_instance, snapshot=snapshot)
|
||||
|
||||
try:
|
||||
self.driver.revert_to_snapshot(context,
|
||||
snapshot_instance_dict,
|
||||
share_server=share_server)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
|
||||
msg = _LE('Share %(share)s could not be reverted '
|
||||
'to snapshot %(snap)s.')
|
||||
msg_args = {'share': share_id, 'snap': snapshot_id}
|
||||
LOG.exception(msg, msg_args)
|
||||
|
||||
if reservations:
|
||||
QUOTAS.rollback(
|
||||
context, reservations, project_id=project_id,
|
||||
user_id=user_id)
|
||||
|
||||
self.db.share_update(
|
||||
context, share_id,
|
||||
{'status': constants.STATUS_REVERTING_ERROR})
|
||||
self.db.share_snapshot_update(
|
||||
context, snapshot_id,
|
||||
{'status': constants.STATUS_AVAILABLE})
|
||||
|
||||
if reservations:
|
||||
QUOTAS.commit(
|
||||
context, reservations, project_id=project_id, user_id=user_id)
|
||||
|
||||
self.db.share_update(
|
||||
context, share_id,
|
||||
{'status': constants.STATUS_AVAILABLE, 'size': snapshot['size']})
|
||||
self.db.share_snapshot_update(
|
||||
context, snapshot_id, {'status': constants.STATUS_AVAILABLE})
|
||||
|
||||
msg = _LI('Share %(share)s reverted to snapshot %(snap)s '
|
||||
'successfully.')
|
||||
msg_args = {'share': share_id, 'snap': snapshot_id}
|
||||
LOG.info(msg, msg_args)
|
||||
|
||||
@add_hooks
|
||||
@utils.require_driver_initialized
|
||||
def delete_share_instance(self, context, share_instance_id, force=False):
|
||||
@ -2359,6 +2429,90 @@ class ShareManager(manager.SchedulerDependentManager):
|
||||
self.db.share_snapshot_instance_update(
|
||||
context, instance['id'], instance)
|
||||
|
||||
def _find_active_replica_on_host(self, replica_list):
|
||||
"""Find the active replica matching this manager's host."""
|
||||
for replica in replica_list:
|
||||
if (replica['replica_state'] == constants.REPLICA_STATE_ACTIVE and
|
||||
share_utils.extract_host(replica['host']) == self.host):
|
||||
return replica
|
||||
|
||||
@locked_share_replica_operation
|
||||
def _revert_to_replicated_snapshot(self, context, share, snapshot,
|
||||
reservations, share_id=None):
|
||||
|
||||
share_server = self._get_share_server(context, share)
|
||||
snapshot_id = snapshot['id']
|
||||
project_id = share['project_id']
|
||||
user_id = share['user_id']
|
||||
|
||||
# Get replicas, including an active replica
|
||||
replica_list = self.db.share_replicas_get_all_by_share(
|
||||
context, share_id, with_share_data=True, with_share_server=True)
|
||||
active_replica = self._find_active_replica_on_host(replica_list)
|
||||
|
||||
# Get snapshot instances, including one on an active replica
|
||||
replica_snapshots = (
|
||||
self.db.share_snapshot_instance_get_all_with_filters(
|
||||
context, {'snapshot_ids': snapshot_id},
|
||||
with_share_data=True))
|
||||
snapshot_instance_filters = {
|
||||
'share_instance_ids': active_replica['id'],
|
||||
'snapshot_ids': snapshot_id,
|
||||
}
|
||||
active_replica_snapshot = (
|
||||
self.db.share_snapshot_instance_get_all_with_filters(
|
||||
context, snapshot_instance_filters))[0]
|
||||
|
||||
# Make primitives to pass the information to the driver
|
||||
replica_list = [self._get_share_replica_dict(context, replica)
|
||||
for replica in replica_list]
|
||||
active_replica = self._get_share_replica_dict(context, active_replica)
|
||||
replica_snapshots = [self._get_snapshot_instance_dict(context, s)
|
||||
for s in replica_snapshots]
|
||||
active_replica_snapshot = self._get_snapshot_instance_dict(
|
||||
context, active_replica_snapshot, snapshot=snapshot)
|
||||
|
||||
try:
|
||||
self.driver.revert_to_replicated_snapshot(
|
||||
context, active_replica, replica_list, active_replica_snapshot,
|
||||
replica_snapshots, share_server=share_server)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
|
||||
msg = _LE('Share %(share)s could not be reverted '
|
||||
'to snapshot %(snap)s.')
|
||||
msg_args = {'share': share_id, 'snap': snapshot_id}
|
||||
LOG.exception(msg, msg_args)
|
||||
|
||||
if reservations:
|
||||
QUOTAS.rollback(
|
||||
context, reservations, project_id=project_id,
|
||||
user_id=user_id)
|
||||
|
||||
self.db.share_replica_update(
|
||||
context, active_replica['id'],
|
||||
{'status': constants.STATUS_REVERTING_ERROR})
|
||||
self.db.share_snapshot_instance_update(
|
||||
context, active_replica_snapshot['id'],
|
||||
{'status': constants.STATUS_AVAILABLE})
|
||||
|
||||
if reservations:
|
||||
QUOTAS.commit(
|
||||
context, reservations, project_id=project_id, user_id=user_id)
|
||||
|
||||
self.db.share_update(context, share_id, {'size': snapshot['size']})
|
||||
self.db.share_replica_update(
|
||||
context, active_replica['id'],
|
||||
{'status': constants.STATUS_AVAILABLE})
|
||||
self.db.share_snapshot_instance_update(
|
||||
context, active_replica_snapshot['id'],
|
||||
{'status': constants.STATUS_AVAILABLE})
|
||||
|
||||
msg = _LI('Share %(share)s reverted to snapshot %(snap)s '
|
||||
'successfully.')
|
||||
msg_args = {'share': share_id, 'snap': snapshot_id}
|
||||
LOG.info(msg, msg_args)
|
||||
|
||||
@add_hooks
|
||||
@utils.require_driver_initialized
|
||||
@locked_share_replica_operation
|
||||
@ -3236,7 +3390,8 @@ class ShareManager(manager.SchedulerDependentManager):
|
||||
|
||||
return share_replica_ref
|
||||
|
||||
def _get_snapshot_instance_dict(self, context, snapshot_instance):
|
||||
def _get_snapshot_instance_dict(self, context, snapshot_instance,
|
||||
snapshot=None):
|
||||
# TODO(gouthamr): remove method when the db layer returns primitives
|
||||
snapshot_instance_ref = {
|
||||
'name': snapshot_instance.get('name'),
|
||||
@ -3255,4 +3410,9 @@ class ShareManager(manager.SchedulerDependentManager):
|
||||
'provider_location': snapshot_instance.get('provider_location'),
|
||||
}
|
||||
|
||||
if snapshot:
|
||||
snapshot_instance_ref.update({
|
||||
'size': snapshot.get('size'),
|
||||
})
|
||||
|
||||
return snapshot_instance_ref
|
||||
|
@ -64,6 +64,7 @@ class ShareAPI(object):
|
||||
update migration_cancel(), migration_complete() and
|
||||
migration_get_progress method signature, rename
|
||||
migration_get_info() to connection_get_info()
|
||||
1.13 - Introduce share revert to snapshot: revert_to_snapshot()
|
||||
"""
|
||||
|
||||
BASE_RPC_API_VERSION = '1.0'
|
||||
@ -72,7 +73,7 @@ class ShareAPI(object):
|
||||
super(ShareAPI, self).__init__()
|
||||
target = messaging.Target(topic=CONF.share_topic,
|
||||
version=self.BASE_RPC_API_VERSION)
|
||||
self.client = rpc.get_client(target, version_cap='1.12')
|
||||
self.client = rpc.get_client(target, version_cap='1.13')
|
||||
|
||||
def create_share_instance(self, context, share_instance, host,
|
||||
request_spec, filter_properties,
|
||||
@ -116,6 +117,15 @@ class ShareAPI(object):
|
||||
'unmanage_snapshot',
|
||||
snapshot_id=snapshot['id'])
|
||||
|
||||
def revert_to_snapshot(self, context, share, snapshot, host, reservations):
|
||||
host = utils.extract_host(host)
|
||||
call_context = self.client.prepare(server=host, version='1.13')
|
||||
call_context.cast(context,
|
||||
'revert_to_snapshot',
|
||||
share_id=share['id'],
|
||||
snapshot_id=snapshot['id'],
|
||||
reservations=reservations)
|
||||
|
||||
def delete_share_instance(self, context, share_instance, force=False):
|
||||
host = utils.extract_host(share_instance['host'])
|
||||
call_context = self.client.prepare(server=host, version='1.4')
|
||||
|
@ -266,6 +266,8 @@ def is_valid_optional_extra_spec(key, value):
|
||||
return parse_boolean_extra_spec(key, value) is not None
|
||||
elif key == constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT:
|
||||
return parse_boolean_extra_spec(key, value) is not None
|
||||
elif key == constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT:
|
||||
return parse_boolean_extra_spec(key, value) is not None
|
||||
elif key == constants.ExtraSpecs.REPLICATION_TYPE_SPEC:
|
||||
return value in constants.ExtraSpecs.REPLICATION_TYPES
|
||||
|
||||
|
@ -42,6 +42,7 @@ def stub_share(id, **kwargs):
|
||||
'is_public': False,
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': False,
|
||||
'replication_type': None,
|
||||
'has_replicas': False,
|
||||
}
|
||||
|
@ -233,6 +233,8 @@ class ShareTypesAPITest(test.TestCase):
|
||||
('2.23', 'share_type_access', False),
|
||||
('2.24', 'share_type_access', True),
|
||||
('2.24', 'share_type_access', False),
|
||||
('2.27', 'share_type_access', True),
|
||||
('2.27', 'share_type_access', False),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_view_builder_show(self, version, prefix, admin):
|
||||
@ -284,6 +286,8 @@ class ShareTypesAPITest(test.TestCase):
|
||||
('2.23', 'share_type_access', False),
|
||||
('2.24', 'share_type_access', True),
|
||||
('2.24', 'share_type_access', False),
|
||||
('2.27', 'share_type_access', True),
|
||||
('2.27', 'share_type_access', False),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_view_builder_list(self, version, prefix, admin):
|
||||
@ -292,6 +296,7 @@ class ShareTypesAPITest(test.TestCase):
|
||||
extra_specs = {
|
||||
constants.ExtraSpecs.SNAPSHOT_SUPPORT: True,
|
||||
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT: False,
|
||||
constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT: True,
|
||||
}
|
||||
|
||||
now = timeutils.utcnow().isoformat()
|
||||
|
@ -22,6 +22,7 @@ from oslo_config import cfg
|
||||
from oslo_serialization import jsonutils
|
||||
import six
|
||||
import webob
|
||||
import webob.exc
|
||||
|
||||
from manila.api import common
|
||||
from manila.api.openstack import api_version_request as api_version
|
||||
@ -38,6 +39,7 @@ from manila import test
|
||||
from manila.tests.api.contrib import stubs
|
||||
from manila.tests.api import fakes
|
||||
from manila.tests import db_utils
|
||||
from manila.tests import fake_share
|
||||
from manila import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
@ -61,12 +63,14 @@ class ShareAPITest(test.TestCase):
|
||||
stubs.stub_snapshot_get)
|
||||
self.maxDiff = None
|
||||
self.share = {
|
||||
"id": "1",
|
||||
"size": 100,
|
||||
"display_name": "Share Test Name",
|
||||
"display_description": "Share Test Desc",
|
||||
"share_proto": "fakeproto",
|
||||
"availability_zone": "zone1:host1",
|
||||
"is_public": False,
|
||||
"task_state": None,
|
||||
}
|
||||
self.create_mock = mock.Mock(
|
||||
return_value=stubs.stub_share(
|
||||
@ -83,6 +87,12 @@ class ShareAPITest(test.TestCase):
|
||||
'id': 'fake_volume_type_id',
|
||||
'name': 'fake_volume_type_name',
|
||||
}
|
||||
self.snapshot = {
|
||||
'id': '2',
|
||||
'share_id': '1',
|
||||
'status': constants.STATUS_AVAILABLE,
|
||||
}
|
||||
|
||||
CONF.set_default("default_share_type", None)
|
||||
|
||||
def _get_expected_share_detailed_response(self, values=None, admin=False):
|
||||
@ -133,6 +143,256 @@ class ShareAPITest(test.TestCase):
|
||||
share['share_server_id'] = 'fake_share_server_id'
|
||||
return {'share': share}
|
||||
|
||||
def test__revert(self):
|
||||
|
||||
share = copy.deepcopy(self.share)
|
||||
share['status'] = constants.STATUS_AVAILABLE
|
||||
share['revert_to_snapshot_support'] = True
|
||||
share = fake_share.fake_share(**share)
|
||||
snapshot = copy.deepcopy(self.snapshot)
|
||||
snapshot['status'] = constants.STATUS_AVAILABLE
|
||||
body = {'revert': {'snapshot_id': '2'}}
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/shares/1/action', use_admin_context=False, version='2.27')
|
||||
mock_validate_revert_parameters = self.mock_object(
|
||||
self.controller, '_validate_revert_parameters',
|
||||
mock.Mock(return_value=body['revert']))
|
||||
mock_get = self.mock_object(
|
||||
share_api.API, 'get', mock.Mock(return_value=share))
|
||||
mock_get_snapshot = self.mock_object(
|
||||
share_api.API, 'get_snapshot', mock.Mock(return_value=snapshot))
|
||||
mock_get_latest_snapshot_for_share = self.mock_object(
|
||||
share_api.API, 'get_latest_snapshot_for_share',
|
||||
mock.Mock(return_value=snapshot))
|
||||
mock_revert_to_snapshot = self.mock_object(
|
||||
share_api.API, 'revert_to_snapshot')
|
||||
|
||||
response = self.controller._revert(req, '1', body=body)
|
||||
|
||||
self.assertEqual(202, response.status_int)
|
||||
mock_validate_revert_parameters.assert_called_once_with(
|
||||
utils.IsAMatcher(context.RequestContext), body)
|
||||
mock_get.assert_called_once_with(
|
||||
utils.IsAMatcher(context.RequestContext), '1')
|
||||
mock_get_snapshot.assert_called_once_with(
|
||||
utils.IsAMatcher(context.RequestContext), '2')
|
||||
mock_get_latest_snapshot_for_share.assert_called_once_with(
|
||||
utils.IsAMatcher(context.RequestContext), '1')
|
||||
mock_revert_to_snapshot.assert_called_once_with(
|
||||
utils.IsAMatcher(context.RequestContext), snapshot)
|
||||
|
||||
def test__revert_not_supported(self):
|
||||
|
||||
share = copy.deepcopy(self.share)
|
||||
share['revert_to_snapshot_support'] = False
|
||||
share = fake_share.fake_share(**share)
|
||||
snapshot = copy.deepcopy(self.snapshot)
|
||||
snapshot['status'] = constants.STATUS_AVAILABLE
|
||||
snapshot['share_id'] = 'wrong_id'
|
||||
body = {'revert': {'snapshot_id': '2'}}
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/shares/1/action', use_admin_context=False, version='2.27')
|
||||
self.mock_object(
|
||||
self.controller, '_validate_revert_parameters',
|
||||
mock.Mock(return_value=body['revert']))
|
||||
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
|
||||
self.mock_object(
|
||||
share_api.API, 'get_snapshot', mock.Mock(return_value=snapshot))
|
||||
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller._revert,
|
||||
req,
|
||||
'1',
|
||||
body=body)
|
||||
|
||||
def test__revert_id_mismatch(self):
|
||||
|
||||
share = copy.deepcopy(self.share)
|
||||
share['status'] = constants.STATUS_AVAILABLE
|
||||
share['revert_to_snapshot_support'] = True
|
||||
share = fake_share.fake_share(**share)
|
||||
snapshot = copy.deepcopy(self.snapshot)
|
||||
snapshot['status'] = constants.STATUS_AVAILABLE
|
||||
snapshot['share_id'] = 'wrong_id'
|
||||
body = {'revert': {'snapshot_id': '2'}}
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/shares/1/action', use_admin_context=False, version='2.27')
|
||||
self.mock_object(
|
||||
self.controller, '_validate_revert_parameters',
|
||||
mock.Mock(return_value=body['revert']))
|
||||
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
|
||||
self.mock_object(
|
||||
share_api.API, 'get_snapshot', mock.Mock(return_value=snapshot))
|
||||
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller._revert,
|
||||
req,
|
||||
'1',
|
||||
body=body)
|
||||
|
||||
@ddt.data(
|
||||
{
|
||||
'share_status': constants.STATUS_ERROR,
|
||||
'share_is_busy': False,
|
||||
'snapshot_status': constants.STATUS_AVAILABLE,
|
||||
}, {
|
||||
'share_status': constants.STATUS_AVAILABLE,
|
||||
'share_is_busy': True,
|
||||
'snapshot_status': constants.STATUS_AVAILABLE,
|
||||
}, {
|
||||
'share_status': constants.STATUS_AVAILABLE,
|
||||
'share_is_busy': False,
|
||||
'snapshot_status': constants.STATUS_ERROR,
|
||||
})
|
||||
@ddt.unpack
|
||||
def test__revert_invalid_status(self, share_status, share_is_busy,
|
||||
snapshot_status):
|
||||
|
||||
share = copy.deepcopy(self.share)
|
||||
share['status'] = share_status
|
||||
share['is_busy'] = share_is_busy
|
||||
share['revert_to_snapshot_support'] = True
|
||||
share = fake_share.fake_share(**share)
|
||||
snapshot = copy.deepcopy(self.snapshot)
|
||||
snapshot['status'] = snapshot_status
|
||||
body = {'revert': {'snapshot_id': '2'}}
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/shares/1/action', use_admin_context=False, version='2.27')
|
||||
self.mock_object(
|
||||
self.controller, '_validate_revert_parameters',
|
||||
mock.Mock(return_value=body['revert']))
|
||||
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
|
||||
self.mock_object(
|
||||
share_api.API, 'get_snapshot', mock.Mock(return_value=snapshot))
|
||||
|
||||
self.assertRaises(webob.exc.HTTPConflict,
|
||||
self.controller._revert,
|
||||
req,
|
||||
'1',
|
||||
body=body)
|
||||
|
||||
def test__revert_snapshot_latest_not_found(self):
|
||||
|
||||
share = copy.deepcopy(self.share)
|
||||
share['status'] = constants.STATUS_AVAILABLE
|
||||
share['revert_to_snapshot_support'] = True
|
||||
share = fake_share.fake_share(**share)
|
||||
snapshot = copy.deepcopy(self.snapshot)
|
||||
snapshot['status'] = constants.STATUS_AVAILABLE
|
||||
body = {'revert': {'snapshot_id': '2'}}
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/shares/1/action', use_admin_context=False, version='2.27')
|
||||
self.mock_object(
|
||||
self.controller, '_validate_revert_parameters',
|
||||
mock.Mock(return_value=body['revert']))
|
||||
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
|
||||
self.mock_object(
|
||||
share_api.API, 'get_snapshot', mock.Mock(return_value=snapshot))
|
||||
self.mock_object(
|
||||
share_api.API, 'get_latest_snapshot_for_share',
|
||||
mock.Mock(return_value=None))
|
||||
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller._revert,
|
||||
req,
|
||||
'1',
|
||||
body=body)
|
||||
|
||||
def test__revert_snapshot_not_latest(self):
|
||||
|
||||
share = copy.deepcopy(self.share)
|
||||
share['status'] = constants.STATUS_AVAILABLE
|
||||
share['revert_to_snapshot_support'] = True
|
||||
share = fake_share.fake_share(**share)
|
||||
snapshot = copy.deepcopy(self.snapshot)
|
||||
snapshot['status'] = constants.STATUS_AVAILABLE
|
||||
latest_snapshot = copy.deepcopy(self.snapshot)
|
||||
latest_snapshot['status'] = constants.STATUS_AVAILABLE
|
||||
latest_snapshot['id'] = '3'
|
||||
body = {'revert': {'snapshot_id': '2'}}
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/shares/1/action', use_admin_context=False, version='2.27')
|
||||
self.mock_object(
|
||||
self.controller, '_validate_revert_parameters',
|
||||
mock.Mock(return_value=body['revert']))
|
||||
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
|
||||
self.mock_object(
|
||||
share_api.API, 'get_snapshot', mock.Mock(return_value=snapshot))
|
||||
self.mock_object(
|
||||
share_api.API, 'get_latest_snapshot_for_share',
|
||||
mock.Mock(return_value=latest_snapshot))
|
||||
|
||||
self.assertRaises(webob.exc.HTTPConflict,
|
||||
self.controller._revert,
|
||||
req,
|
||||
'1',
|
||||
body=body)
|
||||
|
||||
@ddt.data(
|
||||
{
|
||||
'caught': exception.ShareNotFound,
|
||||
'exc_args': {
|
||||
'share_id': '1',
|
||||
},
|
||||
'thrown': webob.exc.HTTPNotFound,
|
||||
}, {
|
||||
'caught': exception.ShareSnapshotNotFound,
|
||||
'exc_args': {
|
||||
'snapshot_id': '2',
|
||||
},
|
||||
'thrown': webob.exc.HTTPBadRequest,
|
||||
}, {
|
||||
'caught': exception.ShareSizeExceedsAvailableQuota,
|
||||
'exc_args': {},
|
||||
'thrown': webob.exc.HTTPForbidden,
|
||||
}, {
|
||||
'caught': exception.ReplicationException,
|
||||
'exc_args': {
|
||||
'reason': 'catastrophic failure',
|
||||
},
|
||||
'thrown': webob.exc.HTTPBadRequest,
|
||||
})
|
||||
@ddt.unpack
|
||||
def test__revert_exception(self, caught, exc_args, thrown):
|
||||
|
||||
body = {'revert': {'snapshot_id': '2'}}
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/shares/1/action', use_admin_context=False, version='2.27')
|
||||
self.mock_object(
|
||||
self.controller, '_validate_revert_parameters',
|
||||
mock.Mock(return_value=body['revert']))
|
||||
self.mock_object(
|
||||
share_api.API, 'get', mock.Mock(side_effect=caught(**exc_args)))
|
||||
|
||||
self.assertRaises(thrown,
|
||||
self.controller._revert,
|
||||
req,
|
||||
'1',
|
||||
body=body)
|
||||
|
||||
def test_validate_revert_parameters(self):
|
||||
|
||||
body = {'revert': {'snapshot_id': 'fake_snapshot_id'}}
|
||||
|
||||
result = self.controller._validate_revert_parameters(
|
||||
'fake_context', body)
|
||||
|
||||
self.assertEqual(body['revert'], result)
|
||||
|
||||
@ddt.data(
|
||||
None,
|
||||
{},
|
||||
{'manage': {'snapshot_id': 'fake_snapshot_id'}},
|
||||
{'revert': {'share_id': 'fake_snapshot_id'}},
|
||||
{'revert': {'snapshot_id': ''}},
|
||||
)
|
||||
def test_validate_revert_parameters_invalid(self, body):
|
||||
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller._validate_revert_parameters,
|
||||
'fake_context',
|
||||
body)
|
||||
|
||||
@ddt.data("2.0", "2.1")
|
||||
def test_share_create_original(self, microversion):
|
||||
self.mock_object(share_api.API, 'create', self.create_mock)
|
||||
@ -2071,3 +2331,28 @@ class ShareManageTest(test.TestCase):
|
||||
self.controller.manage,
|
||||
req,
|
||||
share_id)
|
||||
|
||||
def test_revert(self):
|
||||
|
||||
mock_revert = self.mock_object(
|
||||
self.controller, '_revert',
|
||||
mock.Mock(return_value='fake_response'))
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/shares/fake_id/action', use_admin_context=False, version='2.27')
|
||||
|
||||
result = self.controller.revert(req, 'fake_id', 'fake_body')
|
||||
|
||||
self.assertEqual('fake_response', result)
|
||||
mock_revert.assert_called_once_with(
|
||||
req, 'fake_id', 'fake_body')
|
||||
|
||||
def test_revert_unsupported(self):
|
||||
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/shares/fake_id/action', use_admin_context=False, version='2.24')
|
||||
|
||||
self.assertRaises(exception.VersionNotFoundForAPIMethod,
|
||||
self.controller.revert,
|
||||
req,
|
||||
'fake_id',
|
||||
'fake_body')
|
||||
|
@ -45,13 +45,14 @@ class ViewBuilderTestCase(test.TestCase):
|
||||
'user_id': 'fake_userid',
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': True,
|
||||
}
|
||||
return stubs.stub_share('fake_id', **fake_share)
|
||||
|
||||
def test__collection_name(self):
|
||||
self.assertEqual('shares', self.builder._collection_name)
|
||||
|
||||
@ddt.data('2.6', '2.9', '2.10', '2.11', '2.16', '2.24')
|
||||
@ddt.data('2.6', '2.9', '2.10', '2.11', '2.16', '2.24', '2.27')
|
||||
def test_detail(self, microversion):
|
||||
req = fakes.HTTPRequest.blank('/shares', version=microversion)
|
||||
|
||||
@ -77,5 +78,7 @@ class ViewBuilderTestCase(test.TestCase):
|
||||
expected['user_id'] = 'fake_userid'
|
||||
if self.is_microversion_ge(microversion, '2.24'):
|
||||
expected['create_share_from_snapshot_support'] = True
|
||||
if self.is_microversion_ge(microversion, '2.27'):
|
||||
expected['revert_to_snapshot_support'] = True
|
||||
|
||||
self.assertSubDictMatch(expected, result['share'])
|
||||
|
@ -1371,6 +1371,7 @@ class CreateFromSnapshotExtraSpecAndShareColumn(BaseMigrationChecks):
|
||||
# Pre-existing Shares must be present
|
||||
shares_in_db = engine.execute(shares_table.select()).fetchall()
|
||||
share_ids_in_db = [s['id'] for s in shares_in_db]
|
||||
self.test_case.assertTrue(len(share_ids_in_db) > 1)
|
||||
for share_id in share_ids:
|
||||
self.test_case.assertIn(share_id, share_ids_in_db)
|
||||
|
||||
@ -1420,6 +1421,7 @@ class CreateFromSnapshotExtraSpecAndShareColumn(BaseMigrationChecks):
|
||||
# Pre-existing Shares must be present
|
||||
shares_in_db = engine.execute(shares_table.select()).fetchall()
|
||||
share_ids_in_db = [s['id'] for s in shares_in_db]
|
||||
self.test_case.assertTrue(len(share_ids_in_db) > 1)
|
||||
for share_id in share_ids:
|
||||
self.test_case.assertIn(share_id, share_ids_in_db)
|
||||
|
||||
@ -1449,6 +1451,102 @@ class CreateFromSnapshotExtraSpecAndShareColumn(BaseMigrationChecks):
|
||||
self.test_case.assertEqual(0, len(new_extra_spec))
|
||||
|
||||
|
||||
@map_to_migration('87ce15c59bbe')
|
||||
class RevertToSnapshotShareColumn(BaseMigrationChecks):
|
||||
|
||||
expected_attr = constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT
|
||||
|
||||
def _get_fake_data(self):
|
||||
extra_specs = []
|
||||
shares = []
|
||||
share_instances = []
|
||||
share_types = [
|
||||
{
|
||||
'id': uuidutils.generate_uuid(),
|
||||
'deleted': 'False',
|
||||
'name': 'revert-1',
|
||||
'is_public': False,
|
||||
},
|
||||
{
|
||||
'id': uuidutils.generate_uuid(),
|
||||
'deleted': 'False',
|
||||
'name': 'revert-2',
|
||||
'is_public': True,
|
||||
|
||||
},
|
||||
]
|
||||
snapshot_support = ('0', '1')
|
||||
dhss = ('True', 'False')
|
||||
for idx, share_type in enumerate(share_types):
|
||||
extra_specs.append({
|
||||
'share_type_id': share_type['id'],
|
||||
'spec_key': 'snapshot_support',
|
||||
'spec_value': snapshot_support[idx],
|
||||
'deleted': 0,
|
||||
})
|
||||
extra_specs.append({
|
||||
'share_type_id': share_type['id'],
|
||||
'spec_key': 'driver_handles_share_servers',
|
||||
'spec_value': dhss[idx],
|
||||
'deleted': 0,
|
||||
})
|
||||
share = fake_share(snapshot_support=snapshot_support[idx])
|
||||
shares.append(share)
|
||||
share_instances.append(
|
||||
fake_instance(share_id=share['id'],
|
||||
share_type_id=share_type['id'])
|
||||
)
|
||||
|
||||
return share_types, extra_specs, shares, share_instances
|
||||
|
||||
def setup_upgrade_data(self, engine):
|
||||
|
||||
(self.share_types, self.extra_specs, self.shares,
|
||||
self.share_instances) = self._get_fake_data()
|
||||
|
||||
share_types_table = utils.load_table('share_types', engine)
|
||||
engine.execute(share_types_table.insert(self.share_types))
|
||||
extra_specs_table = utils.load_table('share_type_extra_specs',
|
||||
engine)
|
||||
engine.execute(extra_specs_table.insert(self.extra_specs))
|
||||
shares_table = utils.load_table('shares', engine)
|
||||
engine.execute(shares_table.insert(self.shares))
|
||||
share_instances_table = utils.load_table('share_instances', engine)
|
||||
engine.execute(share_instances_table.insert(self.share_instances))
|
||||
|
||||
def check_upgrade(self, engine, data):
|
||||
share_ids = [s['id'] for s in self.shares]
|
||||
shares_table = utils.load_table('shares', engine)
|
||||
|
||||
# Pre-existing Shares must be present
|
||||
shares_in_db = engine.execute(shares_table.select().where(
|
||||
shares_table.c.deleted == 'False')).fetchall()
|
||||
share_ids_in_db = [s['id'] for s in shares_in_db]
|
||||
self.test_case.assertTrue(len(share_ids_in_db) > 1)
|
||||
for share_id in share_ids:
|
||||
self.test_case.assertIn(share_id, share_ids_in_db)
|
||||
|
||||
# New shares attr must be present and set to False
|
||||
for share in shares_in_db:
|
||||
self.test_case.assertTrue(hasattr(share, self.expected_attr))
|
||||
self.test_case.assertEqual(False, share[self.expected_attr])
|
||||
|
||||
def check_downgrade(self, engine):
|
||||
share_ids = [s['id'] for s in self.shares]
|
||||
shares_table = utils.load_table('shares', engine)
|
||||
|
||||
# Pre-existing Shares must be present
|
||||
shares_in_db = engine.execute(shares_table.select()).fetchall()
|
||||
share_ids_in_db = [s['id'] for s in shares_in_db]
|
||||
self.test_case.assertTrue(len(share_ids_in_db) > 1)
|
||||
for share_id in share_ids:
|
||||
self.test_case.assertIn(share_id, share_ids_in_db)
|
||||
|
||||
# Shares should have no attr to revert share to snapshot
|
||||
for share in shares_in_db:
|
||||
self.test_case.assertFalse(hasattr(share, self.expected_attr))
|
||||
|
||||
|
||||
@map_to_migration('95e3cf760840')
|
||||
class RemoveNovaNetIdColumnFromShareNetworks(BaseMigrationChecks):
|
||||
table_name = 'share_networks'
|
||||
|
@ -17,6 +17,8 @@
|
||||
|
||||
"""Testing of SQLAlchemy backend."""
|
||||
|
||||
import copy
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
|
||||
@ -894,6 +896,35 @@ class ShareSnapshotDatabaseAPITestCase(test.TestCase):
|
||||
self.assertEqual(1, len(actual_result.instances))
|
||||
self.assertSubDictMatch(values, actual_result.to_dict())
|
||||
|
||||
def test_share_snapshot_get_latest_for_share(self):
|
||||
|
||||
share = db_utils.create_share(size=1)
|
||||
values = {
|
||||
'share_id': share['id'],
|
||||
'size': share['size'],
|
||||
'user_id': share['user_id'],
|
||||
'project_id': share['project_id'],
|
||||
'status': constants.STATUS_CREATING,
|
||||
'progress': '0%',
|
||||
'share_size': share['size'],
|
||||
'display_description': 'fake',
|
||||
'share_proto': share['share_proto'],
|
||||
}
|
||||
values1 = copy.deepcopy(values)
|
||||
values1['display_name'] = 'snap1'
|
||||
db_api.share_snapshot_create(self.ctxt, values1)
|
||||
values2 = copy.deepcopy(values)
|
||||
values2['display_name'] = 'snap2'
|
||||
db_api.share_snapshot_create(self.ctxt, values2)
|
||||
values3 = copy.deepcopy(values)
|
||||
values3['display_name'] = 'snap3'
|
||||
db_api.share_snapshot_create(self.ctxt, values3)
|
||||
|
||||
result = db_api.share_snapshot_get_latest_for_share(self.ctxt,
|
||||
share['id'])
|
||||
|
||||
self.assertSubDictMatch(values3, result.to_dict())
|
||||
|
||||
def test_get_instance(self):
|
||||
snapshot = db_utils.create_snapshot(with_share=True)
|
||||
|
||||
|
@ -76,6 +76,28 @@ class ShareTestCase(test.TestCase):
|
||||
|
||||
self.assertEqual(constants.STATUS_CREATING, share.instance['status'])
|
||||
|
||||
@ddt.data(constants.STATUS_REPLICATION_CHANGE, constants.STATUS_AVAILABLE,
|
||||
constants.STATUS_ERROR, constants.STATUS_CREATING)
|
||||
def test_share_instance_reverting(self, status):
|
||||
|
||||
instance_list = [
|
||||
db_utils.create_share_instance(
|
||||
status=constants.STATUS_REVERTING,
|
||||
share_id='fake_id'),
|
||||
db_utils.create_share_instance(
|
||||
status=status, share_id='fake_id'),
|
||||
db_utils.create_share_instance(
|
||||
status=constants.STATUS_ERROR_DELETING, share_id='fake_id'),
|
||||
]
|
||||
|
||||
share1 = db_utils.create_share(instances=instance_list)
|
||||
share2 = db_utils.create_share(instances=list(reversed(instance_list)))
|
||||
|
||||
self.assertEqual(
|
||||
constants.STATUS_REVERTING, share1.instance['status'])
|
||||
self.assertEqual(
|
||||
constants.STATUS_REVERTING, share2.instance['status'])
|
||||
|
||||
@ddt.data(constants.STATUS_AVAILABLE, constants.STATUS_ERROR,
|
||||
constants.STATUS_CREATING)
|
||||
def test_share_instance_replication_change(self, status):
|
||||
|
@ -40,6 +40,7 @@ SERVICE_STATES_NO_POOLS = {
|
||||
thin_provisioning=False,
|
||||
snapshot_support=False,
|
||||
create_share_from_snapshot_support=False,
|
||||
revert_to_snapshot_support=True,
|
||||
driver_handles_share_servers=False),
|
||||
'host2@back1': dict(share_backend_name='BBB',
|
||||
total_capacity_gb=256, free_capacity_gb=100,
|
||||
@ -49,6 +50,7 @@ SERVICE_STATES_NO_POOLS = {
|
||||
thin_provisioning=True,
|
||||
snapshot_support=True,
|
||||
create_share_from_snapshot_support=True,
|
||||
revert_to_snapshot_support=False,
|
||||
driver_handles_share_servers=False),
|
||||
'host2@back2': dict(share_backend_name='CCC',
|
||||
total_capacity_gb=10000, free_capacity_gb=700,
|
||||
@ -58,6 +60,7 @@ SERVICE_STATES_NO_POOLS = {
|
||||
thin_provisioning=True,
|
||||
snapshot_support=True,
|
||||
create_share_from_snapshot_support=True,
|
||||
revert_to_snapshot_support=False,
|
||||
driver_handles_share_servers=False),
|
||||
}
|
||||
|
||||
@ -83,6 +86,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
|
||||
driver_handles_share_servers=False,
|
||||
snapshot_support=True,
|
||||
create_share_from_snapshot_support=True,
|
||||
revert_to_snapshot_support=True,
|
||||
replication_type=None,
|
||||
pools=[dict(pool_name='pool1',
|
||||
total_capacity_gb=51,
|
||||
@ -96,6 +100,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
|
||||
driver_handles_share_servers=False,
|
||||
snapshot_support=True,
|
||||
create_share_from_snapshot_support=True,
|
||||
revert_to_snapshot_support=False,
|
||||
replication_type=None,
|
||||
pools=[dict(pool_name='pool2',
|
||||
total_capacity_gb=52,
|
||||
@ -109,6 +114,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
|
||||
driver_handles_share_servers=False,
|
||||
snapshot_support=True,
|
||||
create_share_from_snapshot_support=True,
|
||||
revert_to_snapshot_support=False,
|
||||
replication_type=None,
|
||||
pools=[dict(pool_name='pool3',
|
||||
total_capacity_gb=53,
|
||||
@ -123,6 +129,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
|
||||
driver_handles_share_servers=False,
|
||||
snapshot_support=True,
|
||||
create_share_from_snapshot_support=True,
|
||||
revert_to_snapshot_support=False,
|
||||
replication_type=None,
|
||||
pools=[dict(pool_name='pool4a',
|
||||
total_capacity_gb=541,
|
||||
@ -145,6 +152,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
|
||||
driver_handles_share_servers=False,
|
||||
snapshot_support=True,
|
||||
create_share_from_snapshot_support=True,
|
||||
revert_to_snapshot_support=False,
|
||||
replication_type=None,
|
||||
pools=[dict(pool_name='pool5a',
|
||||
total_capacity_gb=551,
|
||||
@ -165,6 +173,7 @@ SHARE_SERVICE_STATES_WITH_POOLS = {
|
||||
driver_handles_share_servers=False,
|
||||
snapshot_support=True,
|
||||
create_share_from_snapshot_support=True,
|
||||
revert_to_snapshot_support=False,
|
||||
replication_type=None,
|
||||
pools=[dict(pool_name='pool6a',
|
||||
total_capacity_gb='unknown',
|
||||
|
@ -211,6 +211,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'driver_handles_share_servers': False,
|
||||
'snapshot_support': False,
|
||||
'create_share_from_snapshot_support': False,
|
||||
'revert_to_snapshot_support': True,
|
||||
'consistency_group_support': False,
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
@ -237,6 +238,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'driver_handles_share_servers': False,
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': False,
|
||||
'consistency_group_support': False,
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
@ -263,6 +265,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'driver_handles_share_servers': False,
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': False,
|
||||
'consistency_group_support': False,
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
@ -311,6 +314,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'driver_handles_share_servers': False,
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': True,
|
||||
'consistency_group_support': False,
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
@ -338,6 +342,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'driver_handles_share_servers': False,
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': False,
|
||||
'consistency_group_support': False,
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
@ -365,6 +370,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'driver_handles_share_servers': False,
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': False,
|
||||
'consistency_group_support': 'pool',
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
@ -392,6 +398,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'driver_handles_share_servers': False,
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': False,
|
||||
'consistency_group_support': 'host',
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
@ -419,6 +426,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'driver_handles_share_servers': False,
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': False,
|
||||
'consistency_group_support': 'host',
|
||||
'dedupe': False,
|
||||
'compression': False,
|
||||
@ -469,6 +477,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'driver_handles_share_servers': False,
|
||||
'snapshot_support': False,
|
||||
'create_share_from_snapshot_support': False,
|
||||
'revert_to_snapshot_support': True,
|
||||
'share_backend_name': 'AAA',
|
||||
'free_capacity_gb': 200,
|
||||
'driver_version': None,
|
||||
@ -495,6 +504,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'driver_handles_share_servers': False,
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': False,
|
||||
'share_backend_name': 'BBB',
|
||||
'free_capacity_gb': 100,
|
||||
'driver_version': None,
|
||||
@ -549,6 +559,7 @@ class HostManagerTestCase(test.TestCase):
|
||||
'driver_handles_share_servers': False,
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': False,
|
||||
'share_backend_name': 'BBB',
|
||||
'free_capacity_gb': 42,
|
||||
'driver_version': None,
|
||||
|
@ -125,6 +125,7 @@ class EMCShareFrameworkTestCase(test.TestCase):
|
||||
data['pools'] = None
|
||||
data['snapshot_support'] = True
|
||||
data['create_share_from_snapshot_support'] = True
|
||||
data['revert_to_snapshot_support'] = False
|
||||
data['replication_domain'] = None
|
||||
data['filter_function'] = None
|
||||
data['goodness_function'] = None
|
||||
|
@ -278,6 +278,10 @@ class DummyDriver(driver.ShareDriver):
|
||||
def unmanage_snapshot(self, snapshot):
|
||||
"""Removes the specified snapshot from Manila management."""
|
||||
|
||||
@slow_me_down
|
||||
def revert_to_snapshot(self, context, snapshot, share_server=None):
|
||||
"""Reverts a share (in place) to the specified snapshot."""
|
||||
|
||||
@slow_me_down
|
||||
def extend_share(self, share, new_size, share_server=None):
|
||||
"""Extends size of existing share."""
|
||||
@ -338,6 +342,7 @@ class DummyDriver(driver.ShareDriver):
|
||||
"consistency_group_support": "pool",
|
||||
"snapshot_support": True,
|
||||
"create_share_from_snapshot_support": True,
|
||||
"revert_to_snapshot_support": True,
|
||||
"driver_name": "Dummy",
|
||||
"pools": self._get_pools_info(),
|
||||
}
|
||||
@ -443,6 +448,12 @@ class DummyDriver(driver.ShareDriver):
|
||||
{"id": r["id"], "status": constants.STATUS_AVAILABLE})
|
||||
return return_replica_snapshots
|
||||
|
||||
@slow_me_down
|
||||
def revert_to_replicated_snapshot(self, context, active_replica,
|
||||
replica_list, active_replica_snapshot,
|
||||
replica_snapshots, share_server=None):
|
||||
"""Reverts a replicated share (in place) to the specified snapshot."""
|
||||
|
||||
@slow_me_down
|
||||
def delete_replicated_snapshot(self, context, replica_list,
|
||||
replica_snapshots, share_server=None):
|
||||
|
@ -257,6 +257,7 @@ class GlusterfsNativeShareDriverTestCase(test.TestCase):
|
||||
'pools': None,
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': False,
|
||||
'replication_domain': None,
|
||||
'filter_function': None,
|
||||
'goodness_function': None,
|
||||
|
@ -734,6 +734,7 @@ class HPE3ParDriverTestCase(test.TestCase):
|
||||
'share_backend_name': 'HPE_3PAR',
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': False,
|
||||
'storage_protocol': 'NFS_CIFS',
|
||||
'thin_provisioning': True,
|
||||
'total_capacity_gb': 0,
|
||||
@ -809,6 +810,7 @@ class HPE3ParDriverTestCase(test.TestCase):
|
||||
'provisioned_capacity_gb': expected_capacity}],
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': False,
|
||||
'replication_domain': None,
|
||||
'filter_function': None,
|
||||
'goodness_function': None,
|
||||
@ -846,6 +848,7 @@ class HPE3ParDriverTestCase(test.TestCase):
|
||||
'vendor_name': 'HPE',
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': False,
|
||||
'replication_domain': None,
|
||||
'filter_function': None,
|
||||
'goodness_function': None,
|
||||
|
@ -2424,6 +2424,7 @@ class HuaweiShareDriverTestCase(test.TestCase):
|
||||
"qos": True,
|
||||
"snapshot_support": snapshot_support,
|
||||
"create_share_from_snapshot_support": snapshot_support,
|
||||
"revert_to_snapshot_support": False,
|
||||
"replication_domain": None,
|
||||
"filter_function": None,
|
||||
"goodness_function": None,
|
||||
|
@ -54,7 +54,11 @@ def fake_snapshot(**kwargs):
|
||||
'name': 'fakesnapshotname',
|
||||
'share_proto': 'NFS',
|
||||
'export_location': '127.0.0.1:/mnt/nfs/volume-00002',
|
||||
'share': {'size': 1},
|
||||
'share': {
|
||||
'id': 'fakeid',
|
||||
'name': 'fakename',
|
||||
'size': 1
|
||||
},
|
||||
}
|
||||
snapshot.update(kwargs)
|
||||
return db_fakes.FakeModel(snapshot)
|
||||
@ -520,3 +524,26 @@ class LVMShareDriverTestCase(test.TestCase):
|
||||
self.assertTrue(self._driver._stats['snapshot_support'])
|
||||
self.assertEqual('LVMShareDriver', self._driver._stats['driver_name'])
|
||||
self.assertEqual('test-pool', self._driver._stats['pools'])
|
||||
|
||||
def test_revert_to_snapshot(self):
|
||||
self._driver.revert_to_snapshot(self._context, self.snapshot,
|
||||
self.share_server)
|
||||
snap_lv = "%s/fakesnapshotname" % (CONF.lvm_share_volume_group)
|
||||
share_lv = "%s/fakename" % (CONF.lvm_share_volume_group)
|
||||
mount_path = self._get_mount_path(self.snapshot['share'])
|
||||
expected_exec = [
|
||||
("lvconvert --merge %s" % snap_lv),
|
||||
("umount %s" % mount_path),
|
||||
("rmdir %s" % mount_path),
|
||||
("lvchange -an %s" % share_lv),
|
||||
("lvchange -ay %s" % share_lv),
|
||||
("lvcreate -L 1G --name fakesnapshotname --snapshot %s" %
|
||||
share_lv),
|
||||
('tune2fs -U random /dev/mapper/%s-fakesnapshotname' %
|
||||
CONF.lvm_share_volume_group),
|
||||
("mkdir -p %s" % mount_path),
|
||||
("mount /dev/mapper/%s-fakename %s" %
|
||||
(CONF.lvm_share_volume_group, mount_path)),
|
||||
("chmod 777 %s" % mount_path),
|
||||
]
|
||||
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
|
||||
|
@ -355,6 +355,7 @@ class ZFSonLinuxShareDriverTestCase(test.TestCase):
|
||||
'share_backend_name': self.driver.backend_name,
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': True,
|
||||
'revert_to_snapshot_support': False,
|
||||
'storage_protocol': 'NFS',
|
||||
'total_capacity_gb': 'unknown',
|
||||
'vendor_name': 'Open Source',
|
||||
|
@ -749,6 +749,7 @@ class ShareAPITestCase(test.TestCase):
|
||||
'extra_specs': {
|
||||
'snapshot_support': True,
|
||||
'create_share_from_snapshot_support': False,
|
||||
'revert_to_snapshot_support': False,
|
||||
'replication_type': 'dr',
|
||||
}
|
||||
}
|
||||
@ -765,6 +766,7 @@ class ShareAPITestCase(test.TestCase):
|
||||
expected = {
|
||||
'snapshot_support': False,
|
||||
'create_share_from_snapshot_support': False,
|
||||
'revert_to_snapshot_support': False,
|
||||
'replication_type': None,
|
||||
}
|
||||
self.assertEqual(expected, result)
|
||||
@ -773,7 +775,7 @@ class ShareAPITestCase(test.TestCase):
|
||||
{'extra_specs': {'create_share_from_snapshot_support': 'fake'}})
|
||||
def test_get_share_attributes_from_share_type_invalid(self, share_type):
|
||||
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.assertRaises(exception.InvalidExtraSpec,
|
||||
self.api._get_share_attributes_from_share_type,
|
||||
share_type)
|
||||
|
||||
@ -798,6 +800,7 @@ class ShareAPITestCase(test.TestCase):
|
||||
'snapshot_support': False,
|
||||
'replication_type': replication_type,
|
||||
'create_share_from_snapshot_support': False,
|
||||
'revert_to_snapshot_support': False,
|
||||
},
|
||||
}
|
||||
|
||||
@ -824,6 +827,8 @@ class ShareAPITestCase(test.TestCase):
|
||||
'snapshot_support': fake_type['extra_specs']['snapshot_support'],
|
||||
'create_share_from_snapshot_support':
|
||||
fake_type['extra_specs']['create_share_from_snapshot_support'],
|
||||
'revert_to_snapshot_support':
|
||||
fake_type['extra_specs']['revert_to_snapshot_support'],
|
||||
'replication_type': replication_type,
|
||||
})
|
||||
|
||||
@ -883,6 +888,9 @@ class ShareAPITestCase(test.TestCase):
|
||||
'create_share_from_snapshot_support',
|
||||
share_type['extra_specs']
|
||||
['create_share_from_snapshot_support']),
|
||||
'revert_to_snapshot_support': kwargs.get(
|
||||
'revert_to_snapshot_support',
|
||||
share_type['extra_specs']['revert_to_snapshot_support']),
|
||||
'share_proto': kwargs.get('share_proto', share.get('share_proto')),
|
||||
'share_type_id': share_type['id'],
|
||||
'is_public': kwargs.get('is_public', share.get('is_public')),
|
||||
@ -983,6 +991,48 @@ class ShareAPITestCase(test.TestCase):
|
||||
db_api.share_snapshot_create.assert_called_once_with(
|
||||
self.context, options)
|
||||
|
||||
def test_create_snapshot_space_quota_exceeded(self):
|
||||
|
||||
share = fakes.fake_share(
|
||||
id=uuidutils.generate_uuid(), size=1, project_id='fake_project',
|
||||
user_id='fake_user', has_replicas=False, status='available')
|
||||
usages = {'gigabytes': {'reserved': 10, 'in_use': 0}}
|
||||
quotas = {'snapshot_gigabytes': 10}
|
||||
side_effect = exception.OverQuota(
|
||||
overs='snapshot_gigabytes', usages=usages, quotas=quotas)
|
||||
self.mock_object(
|
||||
quota.QUOTAS, 'reserve', mock.Mock(side_effect=side_effect))
|
||||
mock_snap_create = self.mock_object(db_api, 'share_snapshot_create')
|
||||
|
||||
self.assertRaises(exception.SnapshotSizeExceedsAvailableQuota,
|
||||
self.api.create_snapshot,
|
||||
self.context,
|
||||
share,
|
||||
'fake_name',
|
||||
'fake_description')
|
||||
mock_snap_create.assert_not_called()
|
||||
|
||||
def test_create_snapshot_count_quota_exceeded(self):
|
||||
|
||||
share = fakes.fake_share(
|
||||
id=uuidutils.generate_uuid(), size=1, project_id='fake_project',
|
||||
user_id='fake_user', has_replicas=False, status='available')
|
||||
usages = {'snapshots': {'reserved': 10, 'in_use': 0}}
|
||||
quotas = {'snapshots': 10}
|
||||
side_effect = exception.OverQuota(
|
||||
overs='snapshots', usages=usages, quotas=quotas)
|
||||
self.mock_object(
|
||||
quota.QUOTAS, 'reserve', mock.Mock(side_effect=side_effect))
|
||||
mock_snap_create = self.mock_object(db_api, 'share_snapshot_create')
|
||||
|
||||
self.assertRaises(exception.SnapshotLimitExceeded,
|
||||
self.api.create_snapshot,
|
||||
self.context,
|
||||
share,
|
||||
'fake_name',
|
||||
'fake_description')
|
||||
mock_snap_create.assert_not_called()
|
||||
|
||||
def test_manage_snapshot_share_not_found(self):
|
||||
snapshot = fakes.fake_snapshot(share_id='fake_share',
|
||||
as_primitive=True)
|
||||
@ -1098,6 +1148,218 @@ class ShareAPITestCase(test.TestCase):
|
||||
mock_rpc_call.assert_called_once_with(
|
||||
self.context, snapshot, fake_host)
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_revert_to_snapshot(self, has_replicas):
|
||||
|
||||
share = fakes.fake_share(id=uuidutils.generate_uuid(),
|
||||
has_replicas=has_replicas)
|
||||
self.mock_object(db_api, 'share_get', mock.Mock(return_value=share))
|
||||
mock_handle_revert_to_snapshot_quotas = self.mock_object(
|
||||
self.api, '_handle_revert_to_snapshot_quotas',
|
||||
mock.Mock(return_value='fake_reservations'))
|
||||
mock_revert_to_replicated_snapshot = self.mock_object(
|
||||
self.api, '_revert_to_replicated_snapshot')
|
||||
mock_revert_to_snapshot = self.mock_object(
|
||||
self.api, '_revert_to_snapshot')
|
||||
snapshot = fakes.fake_snapshot(share_id=share['id'])
|
||||
|
||||
self.api.revert_to_snapshot(self.context, snapshot)
|
||||
|
||||
mock_handle_revert_to_snapshot_quotas.assert_called_once_with(
|
||||
self.context, share, snapshot)
|
||||
if not has_replicas:
|
||||
self.assertFalse(mock_revert_to_replicated_snapshot.called)
|
||||
mock_revert_to_snapshot.assert_called_once_with(
|
||||
self.context, share, snapshot, 'fake_reservations')
|
||||
else:
|
||||
mock_revert_to_replicated_snapshot.assert_called_once_with(
|
||||
self.context, share, snapshot, 'fake_reservations')
|
||||
self.assertFalse(mock_revert_to_snapshot.called)
|
||||
|
||||
@ddt.data(None, 'fake_reservations')
|
||||
def test_revert_to_snapshot_exception(self, reservations):
|
||||
|
||||
share = fakes.fake_share(id=uuidutils.generate_uuid(),
|
||||
has_replicas=False)
|
||||
self.mock_object(db_api, 'share_get', mock.Mock(return_value=share))
|
||||
self.mock_object(
|
||||
self.api, '_handle_revert_to_snapshot_quotas',
|
||||
mock.Mock(return_value=reservations))
|
||||
side_effect = exception.ReplicationException(reason='error')
|
||||
self.mock_object(
|
||||
self.api, '_revert_to_snapshot',
|
||||
mock.Mock(side_effect=side_effect))
|
||||
mock_quotas_rollback = self.mock_object(quota.QUOTAS, 'rollback')
|
||||
snapshot = fakes.fake_snapshot(share_id=share['id'])
|
||||
|
||||
self.assertRaises(exception.ReplicationException,
|
||||
self.api.revert_to_snapshot,
|
||||
self.context,
|
||||
snapshot)
|
||||
|
||||
if reservations is not None:
|
||||
mock_quotas_rollback.assert_called_once_with(
|
||||
self.context, reservations)
|
||||
else:
|
||||
self.assertFalse(mock_quotas_rollback.called)
|
||||
|
||||
def test_handle_revert_to_snapshot_quotas(self):
|
||||
|
||||
share = fakes.fake_share(
|
||||
id=uuidutils.generate_uuid(), size=1, project_id='fake_project',
|
||||
user_id='fake_user', has_replicas=False)
|
||||
snapshot = fakes.fake_snapshot(
|
||||
id=uuidutils.generate_uuid(), share_id=share['id'], size=1)
|
||||
mock_quotas_reserve = self.mock_object(quota.QUOTAS, 'reserve')
|
||||
|
||||
result = self.api._handle_revert_to_snapshot_quotas(
|
||||
self.context, share, snapshot)
|
||||
|
||||
self.assertIsNone(result)
|
||||
self.assertFalse(mock_quotas_reserve.called)
|
||||
|
||||
def test_handle_revert_to_snapshot_quotas_different_size(self):
|
||||
|
||||
share = fakes.fake_share(
|
||||
id=uuidutils.generate_uuid(), size=1, project_id='fake_project',
|
||||
user_id='fake_user', has_replicas=False)
|
||||
snapshot = fakes.fake_snapshot(
|
||||
id=uuidutils.generate_uuid(), share_id=share['id'], size=2)
|
||||
mock_quotas_reserve = self.mock_object(
|
||||
quota.QUOTAS, 'reserve',
|
||||
mock.Mock(return_value='fake_reservations'))
|
||||
|
||||
result = self.api._handle_revert_to_snapshot_quotas(
|
||||
self.context, share, snapshot)
|
||||
|
||||
self.assertEqual('fake_reservations', result)
|
||||
mock_quotas_reserve.assert_called_once_with(
|
||||
self.context, project_id='fake_project', gigabytes=1,
|
||||
user_id='fake_user')
|
||||
|
||||
def test_handle_revert_to_snapshot_quotas_quota_exceeded(self):
|
||||
|
||||
share = fakes.fake_share(
|
||||
id=uuidutils.generate_uuid(), size=1, project_id='fake_project',
|
||||
user_id='fake_user', has_replicas=False)
|
||||
snapshot = fakes.fake_snapshot(
|
||||
id=uuidutils.generate_uuid(), share_id=share['id'], size=2)
|
||||
usages = {'gigabytes': {'reserved': 10, 'in_use': 0}}
|
||||
quotas = {'gigabytes': 10}
|
||||
side_effect = exception.OverQuota(
|
||||
overs='fake', usages=usages, quotas=quotas)
|
||||
self.mock_object(
|
||||
quota.QUOTAS, 'reserve', mock.Mock(side_effect=side_effect))
|
||||
|
||||
self.assertRaises(exception.ShareSizeExceedsAvailableQuota,
|
||||
self.api._handle_revert_to_snapshot_quotas,
|
||||
self.context,
|
||||
share,
|
||||
snapshot)
|
||||
|
||||
def test__revert_to_snapshot(self):
|
||||
|
||||
share = fakes.fake_share(
|
||||
id=uuidutils.generate_uuid(), size=1, project_id='fake_project',
|
||||
user_id='fake_user', has_replicas=False)
|
||||
snapshot = fakes.fake_snapshot(
|
||||
id=uuidutils.generate_uuid(), share_id=share['id'], size=2)
|
||||
mock_share_update = self.mock_object(db_api, 'share_update')
|
||||
mock_share_snapshot_update = self.mock_object(
|
||||
db_api, 'share_snapshot_update')
|
||||
mock_revert_rpc_call = self.mock_object(
|
||||
self.share_rpcapi, 'revert_to_snapshot')
|
||||
|
||||
self.api._revert_to_snapshot(
|
||||
self.context, share, snapshot, 'fake_reservations')
|
||||
|
||||
mock_share_update.assert_called_once_with(
|
||||
self.context, share['id'], {'status': constants.STATUS_REVERTING})
|
||||
mock_share_snapshot_update.assert_called_once_with(
|
||||
self.context, snapshot['id'],
|
||||
{'status': constants.STATUS_RESTORING})
|
||||
mock_revert_rpc_call.assert_called_once_with(
|
||||
self.context, share, snapshot, share['instance']['host'],
|
||||
'fake_reservations')
|
||||
|
||||
def test_revert_to_replicated_snapshot(self):
|
||||
|
||||
share = fakes.fake_share(
|
||||
has_replicas=True, status=constants.STATUS_AVAILABLE)
|
||||
snapshot = fakes.fake_snapshot(share_instance_id='id1')
|
||||
snapshot_instance = fakes.fake_snapshot_instance(
|
||||
base_snapshot=snapshot, id='sid1')
|
||||
replicas = [
|
||||
fakes.fake_replica(
|
||||
id='rid1', replica_state=constants.REPLICA_STATE_ACTIVE),
|
||||
fakes.fake_replica(
|
||||
id='rid2', replica_state=constants.REPLICA_STATE_IN_SYNC),
|
||||
]
|
||||
self.mock_object(
|
||||
db_api, 'share_replicas_get_available_active_replica',
|
||||
mock.Mock(return_value=replicas[0]))
|
||||
self.mock_object(
|
||||
db_api, 'share_snapshot_instance_get_all_with_filters',
|
||||
mock.Mock(return_value=[snapshot_instance]))
|
||||
mock_share_replica_update = self.mock_object(
|
||||
db_api, 'share_replica_update')
|
||||
mock_share_snapshot_instance_update = self.mock_object(
|
||||
db_api, 'share_snapshot_instance_update')
|
||||
mock_revert_rpc_call = self.mock_object(
|
||||
self.share_rpcapi, 'revert_to_snapshot')
|
||||
|
||||
self.api._revert_to_replicated_snapshot(
|
||||
self.context, share, snapshot, 'fake_reservations')
|
||||
|
||||
mock_share_replica_update.assert_called_once_with(
|
||||
self.context, 'rid1', {'status': constants.STATUS_REVERTING})
|
||||
mock_share_snapshot_instance_update.assert_called_once_with(
|
||||
self.context, 'sid1', {'status': constants.STATUS_RESTORING})
|
||||
mock_revert_rpc_call.assert_called_once_with(
|
||||
self.context, share, snapshot, replicas[0]['host'],
|
||||
'fake_reservations')
|
||||
|
||||
def test_revert_to_replicated_snapshot_no_active_replica(self):
|
||||
|
||||
share = fakes.fake_share(
|
||||
has_replicas=True, status=constants.STATUS_AVAILABLE)
|
||||
snapshot = fakes.fake_snapshot(share_instance_id='id1')
|
||||
self.mock_object(
|
||||
db_api, 'share_replicas_get_available_active_replica',
|
||||
mock.Mock(return_value=None))
|
||||
|
||||
self.assertRaises(exception.ReplicationException,
|
||||
self.api._revert_to_replicated_snapshot,
|
||||
self.context,
|
||||
share,
|
||||
snapshot,
|
||||
'fake_reservations')
|
||||
|
||||
def test_revert_to_replicated_snapshot_no_snapshot_instance(self):
|
||||
|
||||
share = fakes.fake_share(
|
||||
has_replicas=True, status=constants.STATUS_AVAILABLE)
|
||||
snapshot = fakes.fake_snapshot(share_instance_id='id1')
|
||||
replicas = [
|
||||
fakes.fake_replica(
|
||||
id='rid1', replica_state=constants.REPLICA_STATE_ACTIVE),
|
||||
fakes.fake_replica(
|
||||
id='rid2', replica_state=constants.REPLICA_STATE_IN_SYNC),
|
||||
]
|
||||
self.mock_object(
|
||||
db_api, 'share_replicas_get_available_active_replica',
|
||||
mock.Mock(return_value=replicas[0]))
|
||||
self.mock_object(
|
||||
db_api, 'share_snapshot_instance_get_all_with_filters',
|
||||
mock.Mock(return_value=[None]))
|
||||
|
||||
self.assertRaises(exception.ReplicationException,
|
||||
self.api._revert_to_replicated_snapshot,
|
||||
self.context,
|
||||
share,
|
||||
snapshot,
|
||||
'fake_reservations')
|
||||
|
||||
def test_create_snapshot_for_replicated_share(self):
|
||||
share = fakes.fake_share(
|
||||
has_replicas=True, status=constants.STATUS_AVAILABLE)
|
||||
@ -2051,6 +2313,7 @@ class ShareAPITestCase(test.TestCase):
|
||||
'extra_specs': {
|
||||
'snapshot_support': False,
|
||||
'create_share_from_snapshot_support': False,
|
||||
'revert_to_snapshot_support': False,
|
||||
'driver_handles_share_servers': dhss,
|
||||
},
|
||||
}
|
||||
@ -2061,6 +2324,7 @@ class ShareAPITestCase(test.TestCase):
|
||||
'extra_specs': {
|
||||
'snapshot_support': False,
|
||||
'create_share_from_snapshot_support': False,
|
||||
'revert_to_snapshot_support': False,
|
||||
'driver_handles_share_servers': dhss,
|
||||
},
|
||||
}
|
||||
|
@ -230,6 +230,14 @@ class ShareDriverTestCase(test.TestCase):
|
||||
|
||||
self._assert_is_callable(share_driver, method)
|
||||
|
||||
@ddt.data('revert_to_snapshot',
|
||||
'revert_to_replicated_snapshot')
|
||||
def test_drivers_methods_needed_by_share_revert_to_snapshot_functionality(
|
||||
self, method):
|
||||
share_driver = self._instantiate_share_driver(None, False)
|
||||
|
||||
self._assert_is_callable(share_driver, method)
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_get_share_server_pools(self, value):
|
||||
driver.CONF.set_default('driver_handles_share_servers', value)
|
||||
|
@ -1367,7 +1367,8 @@ class ShareManagerTestCase(test.TestCase):
|
||||
self.assertIsNone(retval)
|
||||
self.assertTrue(replica_update_call.called)
|
||||
|
||||
def _get_snapshot_instance_dict(self, snapshot_instance, share):
|
||||
def _get_snapshot_instance_dict(self, snapshot_instance, share,
|
||||
snapshot=None):
|
||||
expected_snapshot_instance_dict = {
|
||||
'status': constants.STATUS_CREATING,
|
||||
'share_id': share['id'],
|
||||
@ -1384,6 +1385,10 @@ class ShareManagerTestCase(test.TestCase):
|
||||
'deleted_at': snapshot_instance['deleted_at'],
|
||||
'provider_location': snapshot_instance['provider_location'],
|
||||
}
|
||||
if snapshot:
|
||||
expected_snapshot_instance_dict.update({
|
||||
'size': snapshot['size'],
|
||||
})
|
||||
return expected_snapshot_instance_dict
|
||||
|
||||
def test_create_snapshot_driver_exception(self):
|
||||
@ -4837,6 +4842,162 @@ class ShareManagerTestCase(test.TestCase):
|
||||
if quota_error:
|
||||
self.assertTrue(mock_log_warning.called)
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_revert_to_snapshot(self, has_replicas):
|
||||
|
||||
reservations = 'fake_reservations'
|
||||
share_id = 'fake_share_id'
|
||||
snapshot_id = 'fake_snapshot_id'
|
||||
share = fakes.fake_share(
|
||||
id=share_id, instance={'id': 'fake_instance_id'},
|
||||
project_id='fake_project', user_id='fake_user', size=2,
|
||||
has_replicas=has_replicas)
|
||||
snapshot_instance = fakes.fake_snapshot_instance(
|
||||
share_id=share_id, share=share, name='fake_snapshot')
|
||||
snapshot = fakes.fake_snapshot(
|
||||
id=snapshot_id, share_id=share_id, share=share,
|
||||
instance=snapshot_instance, project_id='fake_project',
|
||||
user_id='fake_user', size=1)
|
||||
|
||||
mock_share_snapshot_get = self.mock_object(
|
||||
self.share_manager.db, 'share_snapshot_get',
|
||||
mock.Mock(return_value=snapshot))
|
||||
mock_revert_to_snapshot = self.mock_object(
|
||||
self.share_manager, '_revert_to_snapshot')
|
||||
mock_revert_to_replicated_snapshot = self.mock_object(
|
||||
self.share_manager, '_revert_to_replicated_snapshot')
|
||||
|
||||
self.share_manager.revert_to_snapshot(
|
||||
self.context, snapshot_id, reservations, share_id=share_id)
|
||||
|
||||
mock_share_snapshot_get.assert_called_once_with(mock.ANY, snapshot_id)
|
||||
|
||||
if not has_replicas:
|
||||
mock_revert_to_snapshot.assert_called_once_with(
|
||||
mock.ANY, share, snapshot, reservations)
|
||||
self.assertFalse(mock_revert_to_replicated_snapshot.called)
|
||||
else:
|
||||
self.assertFalse(mock_revert_to_snapshot.called)
|
||||
mock_revert_to_replicated_snapshot.assert_called_once_with(
|
||||
mock.ANY, share, snapshot, reservations, share_id=share_id)
|
||||
|
||||
@ddt.data(None, 'fake_reservations')
|
||||
def test__revert_to_snapshot(self, reservations):
|
||||
|
||||
mock_quotas_rollback = self.mock_object(quota.QUOTAS, 'rollback')
|
||||
mock_quotas_commit = self.mock_object(quota.QUOTAS, 'commit')
|
||||
self.mock_object(
|
||||
self.share_manager, '_get_share_server',
|
||||
mock.Mock(return_value=None))
|
||||
mock_driver = self.mock_object(self.share_manager, 'driver')
|
||||
|
||||
share_id = 'fake_share_id'
|
||||
share = fakes.fake_share(
|
||||
id=share_id, instance={'id': 'fake_instance_id'},
|
||||
project_id='fake_project', user_id='fake_user', size=2)
|
||||
snapshot_instance = fakes.fake_snapshot_instance(
|
||||
share_id=share_id, share=share, name='fake_snapshot')
|
||||
snapshot = fakes.fake_snapshot(
|
||||
id='fake_snapshot_id', share_id=share_id, share=share,
|
||||
instance=snapshot_instance, project_id='fake_project',
|
||||
user_id='fake_user', size=1)
|
||||
|
||||
self.mock_object(
|
||||
self.share_manager.db, 'share_snapshot_get',
|
||||
mock.Mock(return_value=snapshot))
|
||||
self.mock_object(
|
||||
self.share_manager.db, 'share_snapshot_instance_get',
|
||||
mock.Mock(return_value=snapshot_instance))
|
||||
mock_share_update = self.mock_object(
|
||||
self.share_manager.db, 'share_update')
|
||||
mock_share_snapshot_update = self.mock_object(
|
||||
self.share_manager.db, 'share_snapshot_update')
|
||||
|
||||
self.share_manager._revert_to_snapshot(
|
||||
self.context, share, snapshot, reservations)
|
||||
|
||||
mock_driver.revert_to_snapshot.assert_called_once_with(
|
||||
mock.ANY,
|
||||
self._get_snapshot_instance_dict(
|
||||
snapshot_instance, share, snapshot=snapshot),
|
||||
share_server=None)
|
||||
|
||||
self.assertFalse(mock_quotas_rollback.called)
|
||||
if reservations:
|
||||
mock_quotas_commit.assert_called_once_with(
|
||||
mock.ANY, reservations, project_id='fake_project',
|
||||
user_id='fake_user')
|
||||
else:
|
||||
self.assertFalse(mock_quotas_commit.called)
|
||||
|
||||
mock_share_update.assert_called_once_with(
|
||||
mock.ANY, share_id,
|
||||
{'status': constants.STATUS_AVAILABLE, 'size': snapshot['size']})
|
||||
mock_share_snapshot_update.assert_called_once_with(
|
||||
mock.ANY, 'fake_snapshot_id',
|
||||
{'status': constants.STATUS_AVAILABLE})
|
||||
|
||||
@ddt.data(None, 'fake_reservations')
|
||||
def test__revert_to_snapshot_driver_exception(self, reservations):
|
||||
|
||||
mock_quotas_rollback = self.mock_object(quota.QUOTAS, 'rollback')
|
||||
mock_quotas_commit = self.mock_object(quota.QUOTAS, 'commit')
|
||||
self.mock_object(
|
||||
self.share_manager, '_get_share_server',
|
||||
mock.Mock(return_value=None))
|
||||
mock_driver = self.mock_object(self.share_manager, 'driver')
|
||||
mock_driver.revert_to_snapshot.side_effect = exception.ManilaException
|
||||
|
||||
share_id = 'fake_share_id'
|
||||
share = fakes.fake_share(
|
||||
id=share_id, instance={'id': 'fake_instance_id'},
|
||||
project_id='fake_project', user_id='fake_user', size=2)
|
||||
snapshot_instance = fakes.fake_snapshot_instance(
|
||||
share_id=share_id, share=share, name='fake_snapshot')
|
||||
snapshot = fakes.fake_snapshot(
|
||||
id='fake_snapshot_id', share_id=share_id, share=share,
|
||||
instance=snapshot_instance, project_id='fake_project',
|
||||
user_id='fake_user', size=1)
|
||||
|
||||
self.mock_object(
|
||||
self.share_manager.db, 'share_snapshot_get',
|
||||
mock.Mock(return_value=snapshot))
|
||||
self.mock_object(
|
||||
self.share_manager.db, 'share_snapshot_instance_get',
|
||||
mock.Mock(return_value=snapshot_instance))
|
||||
mock_share_update = self.mock_object(
|
||||
self.share_manager.db, 'share_update')
|
||||
mock_share_snapshot_update = self.mock_object(
|
||||
self.share_manager.db, 'share_snapshot_update')
|
||||
|
||||
self.assertRaises(exception.ManilaException,
|
||||
self.share_manager._revert_to_snapshot,
|
||||
self.context,
|
||||
share,
|
||||
snapshot,
|
||||
reservations)
|
||||
|
||||
mock_driver.revert_to_snapshot.assert_called_once_with(
|
||||
mock.ANY,
|
||||
self._get_snapshot_instance_dict(
|
||||
snapshot_instance, share, snapshot=snapshot),
|
||||
share_server=None)
|
||||
|
||||
self.assertFalse(mock_quotas_commit.called)
|
||||
if reservations:
|
||||
mock_quotas_rollback.assert_called_once_with(
|
||||
mock.ANY, reservations, project_id='fake_project',
|
||||
user_id='fake_user')
|
||||
else:
|
||||
self.assertFalse(mock_quotas_rollback.called)
|
||||
|
||||
mock_share_update.assert_called_once_with(
|
||||
mock.ANY, share_id,
|
||||
{'status': constants.STATUS_REVERTING_ERROR})
|
||||
mock_share_snapshot_update.assert_called_once_with(
|
||||
mock.ANY, 'fake_snapshot_id',
|
||||
{'status': constants.STATUS_AVAILABLE})
|
||||
|
||||
def _setup_crud_replicated_snapshot_data(self):
|
||||
snapshot = fakes.fake_snapshot(create_instance=True)
|
||||
snapshot_instance = fakes.fake_snapshot_instance(
|
||||
@ -4928,6 +5089,135 @@ class ShareManagerTestCase(test.TestCase):
|
||||
mock_db_update_call.assert_called_once_with(
|
||||
self.context, snapshot['instance']['id'], snapshot_dict)
|
||||
|
||||
@ddt.data(None, 'fake_reservations')
|
||||
def test_revert_to_replicated_snapshot(self, reservations):
|
||||
|
||||
share_id = 'id1'
|
||||
mock_quotas_rollback = self.mock_object(quota.QUOTAS, 'rollback')
|
||||
mock_quotas_commit = self.mock_object(quota.QUOTAS, 'commit')
|
||||
share = fakes.fake_share(
|
||||
id=share_id, project_id='fake_project', user_id='fake_user')
|
||||
snapshot = fakes.fake_snapshot(
|
||||
create_instance=True, share=share, size=1)
|
||||
snapshot_instance = fakes.fake_snapshot_instance(
|
||||
base_snapshot=snapshot)
|
||||
snapshot_instances = [snapshot['instance'], snapshot_instance]
|
||||
active_replica = fake_replica(
|
||||
id='rid1', share_id=share_id, host=self.share_manager.host,
|
||||
replica_state=constants.REPLICA_STATE_ACTIVE, as_primitive=False)
|
||||
replica = fake_replica(
|
||||
id='rid2', share_id=share_id, host='secondary',
|
||||
replica_state=constants.REPLICA_STATE_IN_SYNC, as_primitive=False)
|
||||
replicas = [active_replica, replica]
|
||||
self.mock_object(
|
||||
db, 'share_snapshot_get', mock.Mock(return_value=snapshot))
|
||||
self.mock_object(
|
||||
self.share_manager, '_get_share_server',
|
||||
mock.Mock(return_value=None))
|
||||
self.mock_object(
|
||||
db, 'share_replicas_get_all_by_share',
|
||||
mock.Mock(return_value=replicas))
|
||||
self.mock_object(
|
||||
db, 'share_snapshot_instance_get_all_with_filters',
|
||||
mock.Mock(side_effect=[snapshot_instances,
|
||||
[snapshot_instances[0]]]))
|
||||
mock_driver = self.mock_object(self.share_manager, 'driver')
|
||||
mock_share_update = self.mock_object(
|
||||
self.share_manager.db, 'share_update')
|
||||
mock_share_replica_update = self.mock_object(
|
||||
self.share_manager.db, 'share_replica_update')
|
||||
mock_share_snapshot_instance_update = self.mock_object(
|
||||
self.share_manager.db, 'share_snapshot_instance_update')
|
||||
|
||||
self.share_manager._revert_to_replicated_snapshot(
|
||||
self.context, share, snapshot, reservations, share_id=share_id)
|
||||
|
||||
self.assertTrue(mock_driver.revert_to_replicated_snapshot.called)
|
||||
self.assertFalse(mock_quotas_rollback.called)
|
||||
if reservations:
|
||||
mock_quotas_commit.assert_called_once_with(
|
||||
mock.ANY, reservations, project_id='fake_project',
|
||||
user_id='fake_user')
|
||||
else:
|
||||
self.assertFalse(mock_quotas_commit.called)
|
||||
|
||||
mock_share_update.assert_called_once_with(
|
||||
mock.ANY, share_id, {'size': snapshot['size']})
|
||||
mock_share_replica_update.assert_called_once_with(
|
||||
mock.ANY, active_replica['id'],
|
||||
{'status': constants.STATUS_AVAILABLE})
|
||||
mock_share_snapshot_instance_update.assert_called_once_with(
|
||||
mock.ANY, snapshot['instance']['id'],
|
||||
{'status': constants.STATUS_AVAILABLE})
|
||||
|
||||
@ddt.data(None, 'fake_reservations')
|
||||
def test_revert_to_replicated_snapshot_driver_exception(
|
||||
self, reservations):
|
||||
|
||||
mock_quotas_rollback = self.mock_object(quota.QUOTAS, 'rollback')
|
||||
mock_quotas_commit = self.mock_object(quota.QUOTAS, 'commit')
|
||||
share_id = 'id1'
|
||||
share = fakes.fake_share(
|
||||
id=share_id, project_id='fake_project', user_id='fake_user')
|
||||
snapshot = fakes.fake_snapshot(
|
||||
create_instance=True, share=share, size=1)
|
||||
snapshot_instance = fakes.fake_snapshot_instance(
|
||||
base_snapshot=snapshot)
|
||||
snapshot_instances = [snapshot['instance'], snapshot_instance]
|
||||
active_replica = fake_replica(
|
||||
id='rid1', share_id=share_id, host=self.share_manager.host,
|
||||
replica_state=constants.REPLICA_STATE_ACTIVE, as_primitive=False)
|
||||
replica = fake_replica(
|
||||
id='rid2', share_id=share_id, host='secondary',
|
||||
replica_state=constants.REPLICA_STATE_IN_SYNC, as_primitive=False)
|
||||
replicas = [active_replica, replica]
|
||||
self.mock_object(
|
||||
db, 'share_snapshot_get', mock.Mock(return_value=snapshot))
|
||||
self.mock_object(
|
||||
self.share_manager, '_get_share_server',
|
||||
mock.Mock(return_value=None))
|
||||
self.mock_object(
|
||||
db, 'share_replicas_get_all_by_share',
|
||||
mock.Mock(return_value=replicas))
|
||||
self.mock_object(
|
||||
db, 'share_snapshot_instance_get_all_with_filters',
|
||||
mock.Mock(side_effect=[snapshot_instances,
|
||||
[snapshot_instances[0]]]))
|
||||
mock_driver = self.mock_object(self.share_manager, 'driver')
|
||||
mock_driver.revert_to_replicated_snapshot.side_effect = (
|
||||
exception.ManilaException)
|
||||
mock_share_update = self.mock_object(
|
||||
self.share_manager.db, 'share_update')
|
||||
mock_share_replica_update = self.mock_object(
|
||||
self.share_manager.db, 'share_replica_update')
|
||||
mock_share_snapshot_instance_update = self.mock_object(
|
||||
self.share_manager.db, 'share_snapshot_instance_update')
|
||||
|
||||
self.assertRaises(exception.ManilaException,
|
||||
self.share_manager._revert_to_replicated_snapshot,
|
||||
self.context,
|
||||
share,
|
||||
snapshot,
|
||||
reservations,
|
||||
share_id=share_id)
|
||||
|
||||
self.assertTrue(mock_driver.revert_to_replicated_snapshot.called)
|
||||
self.assertFalse(mock_quotas_commit.called)
|
||||
if reservations:
|
||||
mock_quotas_rollback.assert_called_once_with(
|
||||
mock.ANY, reservations, project_id='fake_project',
|
||||
user_id='fake_user')
|
||||
else:
|
||||
self.assertFalse(mock_quotas_rollback.called)
|
||||
|
||||
self.assertFalse(mock_share_update.called)
|
||||
mock_share_replica_update.assert_called_once_with(
|
||||
mock.ANY, active_replica['id'],
|
||||
{'status': constants.STATUS_REVERTING_ERROR})
|
||||
mock_share_snapshot_instance_update.assert_called_once_with(
|
||||
mock.ANY, snapshot['instance']['id'],
|
||||
{'status': constants.STATUS_AVAILABLE})
|
||||
|
||||
def delete_replicated_snapshot_driver_exception(self):
|
||||
snapshot, snapshot_instances, replicas = (
|
||||
self._setup_crud_replicated_snapshot_data()
|
||||
|
@ -325,6 +325,15 @@ class ShareRpcAPITestCase(test.TestCase):
|
||||
snapshot=self.fake_snapshot,
|
||||
host='fake_host')
|
||||
|
||||
def test_revert_to_snapshot(self):
|
||||
self._test_share_api('revert_to_snapshot',
|
||||
rpc_method='cast',
|
||||
version='1.13',
|
||||
share=self.fake_share,
|
||||
snapshot=self.fake_snapshot,
|
||||
host='fake_host',
|
||||
reservations={'fake': 'fake'})
|
||||
|
||||
def test_create_replicated_snapshot(self):
|
||||
self._test_share_api('create_replicated_snapshot',
|
||||
rpc_method='cast',
|
||||
|
@ -78,6 +78,7 @@ class ShareTypesTestCase(test.TestCase):
|
||||
fake_optional_extra_specs = {
|
||||
constants.ExtraSpecs.SNAPSHOT_SUPPORT: 'true',
|
||||
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT: 'false',
|
||||
constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT: 'false',
|
||||
}
|
||||
|
||||
fake_type_w_valid_extra = {
|
||||
@ -237,7 +238,8 @@ class ShareTypesTestCase(test.TestCase):
|
||||
@ddt.data(*(
|
||||
list(itertools.product(
|
||||
(constants.ExtraSpecs.SNAPSHOT_SUPPORT,
|
||||
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT),
|
||||
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT,
|
||||
constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT),
|
||||
strutils.TRUE_STRINGS + strutils.FALSE_STRINGS))) +
|
||||
list(itertools.product(
|
||||
(constants.ExtraSpecs.REPLICATION_TYPE_SPEC,),
|
||||
|
@ -10,11 +10,13 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# Shares
|
||||
STATUS_ERROR = 'error'
|
||||
STATUS_AVAILABLE = 'available'
|
||||
STATUS_ERROR_DELETING = 'error_deleting'
|
||||
|
||||
TEMPEST_MANILA_PREFIX = 'tempest-manila'
|
||||
|
||||
# Replication
|
||||
REPLICATION_STYLE_READABLE = 'readable'
|
||||
REPLICATION_STYLE_WRITABLE = 'writable'
|
||||
REPLICATION_STYLE_DR = 'dr'
|
||||
@ -31,6 +33,7 @@ REPLICATION_STATE_ACTIVE = 'active'
|
||||
REPLICATION_STATE_IN_SYNC = 'in_sync'
|
||||
REPLICATION_STATE_OUT_OF_SYNC = 'out_of_sync'
|
||||
|
||||
# Access Rules
|
||||
RULE_STATE_ACTIVE = 'active'
|
||||
RULE_STATE_OUT_OF_SYNC = 'out_of_sync'
|
||||
RULE_STATE_ERROR = 'error'
|
||||
@ -50,3 +53,10 @@ TASK_STATE_DATA_COPYING_COMPLETING = 'data_copying_completing'
|
||||
TASK_STATE_DATA_COPYING_COMPLETED = 'data_copying_completed'
|
||||
TASK_STATE_DATA_COPYING_CANCELLED = 'data_copying_cancelled'
|
||||
TASK_STATE_DATA_COPYING_ERROR = 'data_copying_error'
|
||||
|
||||
# Revert to snapshot
|
||||
REVERT_TO_SNAPSHOT_MICROVERSION = '2.27'
|
||||
REVERT_TO_SNAPSHOT_SUPPORT = 'revert_to_snapshot_support'
|
||||
STATUS_RESTORING = 'restoring'
|
||||
STATUS_REVERTING = 'reverting'
|
||||
STATUS_REVERTING_ERROR = 'reverting_error'
|
||||
|
@ -30,7 +30,7 @@ ShareGroup = [
|
||||
help="The minimum api microversion is configured to be the "
|
||||
"value of the minimum microversion supported by Manila."),
|
||||
cfg.StrOpt("max_api_microversion",
|
||||
default="2.26",
|
||||
default="2.27",
|
||||
help="The maximum api microversion is configured to be the "
|
||||
"value of the latest microversion supported by Manila."),
|
||||
cfg.StrOpt("region",
|
||||
@ -103,7 +103,11 @@ ShareGroup = [
|
||||
"Defaults to the value of run_snapshot_tests. Set it to "
|
||||
"False if the driver being tested does not support "
|
||||
"creating shares from snapshots."),
|
||||
|
||||
cfg.BoolOpt("capability_revert_to_snapshot_support",
|
||||
help="Defines extra spec that satisfies specific back end "
|
||||
"capability called 'revert_to_snapshot_support' "
|
||||
"and will be used for setting up custom share type. "
|
||||
"Defaults to the value of run_revert_to_snapshot_tests."),
|
||||
cfg.StrOpt("share_network_id",
|
||||
default="",
|
||||
help="Some backend drivers requires share network "
|
||||
@ -161,6 +165,11 @@ ShareGroup = [
|
||||
help="Defines whether to run tests that use share snapshots "
|
||||
"or not. Disable this feature if used driver doesn't "
|
||||
"support it."),
|
||||
cfg.BoolOpt("run_revert_to_snapshot_tests",
|
||||
default=False,
|
||||
help="Defines whether to run tests that revert shares "
|
||||
"to snapshots or not. Enable this feature if used "
|
||||
"driver supports it."),
|
||||
cfg.BoolOpt("run_consistency_group_tests",
|
||||
default=True,
|
||||
help="Defines whether to run consistency group tests or not. "
|
||||
|
@ -50,6 +50,12 @@ class ManilaTempestPlugin(plugins.TempestPlugin):
|
||||
conf.share.run_snapshot_tests,
|
||||
group="share",
|
||||
)
|
||||
if conf.share.capability_revert_to_snapshot_support is None:
|
||||
conf.set_default(
|
||||
"capability_revert_to_snapshot_support",
|
||||
conf.share.run_revert_to_snapshot_tests,
|
||||
group="share",
|
||||
)
|
||||
|
||||
def get_opt_lists(self):
|
||||
return [(config_share.share_group.name, config_share.ShareGroup),
|
||||
|
@ -551,6 +551,67 @@ class SharesV2Client(shares_client.SharesClient):
|
||||
self.expected_success(202, resp.status)
|
||||
return body
|
||||
|
||||
###############
|
||||
|
||||
def revert_to_snapshot(self, share_id, snapshot_id,
|
||||
version=LATEST_MICROVERSION):
|
||||
url = 'shares/%s/action' % share_id
|
||||
body = json.dumps({'revert': {'snapshot_id': snapshot_id}})
|
||||
resp, body = self.post(url, body, version=version)
|
||||
self.expected_success(202, resp.status)
|
||||
return self._parse_resp(body)
|
||||
|
||||
###############
|
||||
|
||||
def create_share_type_extra_specs(self, share_type_id, extra_specs,
|
||||
version=LATEST_MICROVERSION):
|
||||
url = "types/%s/extra_specs" % share_type_id
|
||||
post_body = json.dumps({'extra_specs': extra_specs})
|
||||
resp, body = self.post(url, post_body, version=version)
|
||||
self.expected_success(200, resp.status)
|
||||
return self._parse_resp(body)
|
||||
|
||||
def get_share_type_extra_spec(self, share_type_id, extra_spec_name,
|
||||
version=LATEST_MICROVERSION):
|
||||
uri = "types/%s/extra_specs/%s" % (share_type_id, extra_spec_name)
|
||||
resp, body = self.get(uri, version=version)
|
||||
self.expected_success(200, resp.status)
|
||||
return self._parse_resp(body)
|
||||
|
||||
def get_share_type_extra_specs(self, share_type_id, params=None,
|
||||
version=LATEST_MICROVERSION):
|
||||
uri = "types/%s/extra_specs" % share_type_id
|
||||
if params is not None:
|
||||
uri += '?%s' % urlparse.urlencode(params)
|
||||
resp, body = self.get(uri, version=version)
|
||||
self.expected_success(200, resp.status)
|
||||
return self._parse_resp(body)
|
||||
|
||||
def update_share_type_extra_spec(self, share_type_id, spec_name,
|
||||
spec_value, version=LATEST_MICROVERSION):
|
||||
uri = "types/%s/extra_specs/%s" % (share_type_id, spec_name)
|
||||
extra_spec = {spec_name: spec_value}
|
||||
post_body = json.dumps(extra_spec)
|
||||
resp, body = self.put(uri, post_body, version=version)
|
||||
self.expected_success(200, resp.status)
|
||||
return self._parse_resp(body)
|
||||
|
||||
def update_share_type_extra_specs(self, share_type_id, extra_specs,
|
||||
version=LATEST_MICROVERSION):
|
||||
uri = "types/%s/extra_specs" % share_type_id
|
||||
extra_specs = {"extra_specs": extra_specs}
|
||||
post_body = json.dumps(extra_specs)
|
||||
resp, body = self.post(uri, post_body, version=version)
|
||||
self.expected_success(200, resp.status)
|
||||
return self._parse_resp(body)
|
||||
|
||||
def delete_share_type_extra_spec(self, share_type_id, extra_spec_name,
|
||||
version=LATEST_MICROVERSION):
|
||||
uri = "types/%s/extra_specs/%s" % (share_type_id, extra_spec_name)
|
||||
resp, body = self.delete(uri, version=version)
|
||||
self.expected_success(202, resp.status)
|
||||
return body
|
||||
|
||||
###############
|
||||
|
||||
def get_snapshot_instance(self, instance_id, version=LATEST_MICROVERSION):
|
||||
@ -726,13 +787,6 @@ class SharesV2Client(shares_client.SharesClient):
|
||||
self.expected_success(200, resp.status)
|
||||
return self._parse_resp(body)
|
||||
|
||||
def delete_share_type_extra_spec(self, share_type_id, extra_spec_name,
|
||||
version=LATEST_MICROVERSION):
|
||||
uri = "types/%s/extra_specs/%s" % (share_type_id, extra_spec_name)
|
||||
resp, body = self.delete(uri, version=version)
|
||||
self.expected_success(202, resp.status)
|
||||
return body
|
||||
|
||||
def list_access_to_share_type(self, share_type_id,
|
||||
version=LATEST_MICROVERSION,
|
||||
action_name=None):
|
||||
|
@ -14,11 +14,16 @@
|
||||
# under the License.
|
||||
|
||||
import ddt
|
||||
from tempest import config
|
||||
from tempest.lib.common.utils import data_utils
|
||||
from tempest.lib import exceptions as lib_exc
|
||||
from testtools import testcase as tc
|
||||
|
||||
from manila_tempest_tests.common import constants
|
||||
from manila_tempest_tests.tests.api import base
|
||||
from manila_tempest_tests import utils
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@ -69,8 +74,12 @@ class ExtraSpecsAdminNegativeTest(base.BaseSharesMixedTest):
|
||||
share_type = self.shares_v2_client.get_share_type(
|
||||
st['share_type']['id'])
|
||||
# Verify a non-admin can only read the required extra-specs
|
||||
expected_keys = ['driver_handles_share_servers', 'snapshot_support',
|
||||
'create_share_from_snapshot_support']
|
||||
expected_keys = ['driver_handles_share_servers', 'snapshot_support']
|
||||
if utils.is_microversion_ge(CONF.share.max_api_microversion, '2.24'):
|
||||
expected_keys.append('create_share_from_snapshot_support')
|
||||
if utils.is_microversion_ge(CONF.share.max_api_microversion,
|
||||
constants.REVERT_TO_SNAPSHOT_MICROVERSION):
|
||||
expected_keys.append('revert_to_snapshot_support')
|
||||
actual_keys = share_type['share_type']['extra_specs'].keys()
|
||||
self.assertEqual(sorted(expected_keys), sorted(actual_keys),
|
||||
'Incorrect extra specs visible to non-admin user; '
|
||||
|
@ -740,6 +740,8 @@ class BaseSharesTest(test.BaseTestCase):
|
||||
CONF.share.capability_snapshot_support)
|
||||
create_from_snapshot_support = six.text_type(
|
||||
CONF.share.capability_create_share_from_snapshot_support)
|
||||
revert_to_snapshot_support = six.text_type(
|
||||
CONF.share.capability_revert_to_snapshot_support)
|
||||
|
||||
extra_specs_dict = {
|
||||
"driver_handles_share_servers": dhss,
|
||||
@ -748,6 +750,7 @@ class BaseSharesTest(test.BaseTestCase):
|
||||
optional = {
|
||||
"snapshot_support": snapshot_support,
|
||||
"create_share_from_snapshot_support": create_from_snapshot_support,
|
||||
"revert_to_snapshot_support": revert_to_snapshot_support,
|
||||
}
|
||||
# NOTE(gouthamr): In micro-versions < 2.24, snapshot_support is a
|
||||
# required extra-spec
|
||||
|
109
manila_tempest_tests/tests/api/test_revert_to_snapshot.py
Normal file
109
manila_tempest_tests/tests/api/test_revert_to_snapshot.py
Normal file
@ -0,0 +1,109 @@
|
||||
# Copyright 2016 Andrew Kerr
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import ddt
|
||||
|
||||
from tempest import config
|
||||
from tempest.lib.common.utils import data_utils
|
||||
from testtools import testcase as tc
|
||||
|
||||
from manila_tempest_tests.common import constants
|
||||
from manila_tempest_tests.tests.api import base
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
@base.skip_if_microversion_not_supported(
|
||||
constants.REVERT_TO_SNAPSHOT_MICROVERSION)
|
||||
@ddt.ddt
|
||||
class RevertToSnapshotTest(base.BaseSharesMixedTest):
|
||||
|
||||
@classmethod
|
||||
def skip_checks(cls):
|
||||
super(RevertToSnapshotTest, cls).skip_checks()
|
||||
if not CONF.share.run_revert_to_snapshot_tests:
|
||||
msg = "Revert to snapshot tests are disabled."
|
||||
raise cls.skipException(msg)
|
||||
if not CONF.share.capability_revert_to_snapshot_support:
|
||||
msg = "Revert to snapshot support is disabled."
|
||||
raise cls.skipException(msg)
|
||||
if not CONF.share.capability_snapshot_support:
|
||||
msg = "Snapshot support is disabled."
|
||||
raise cls.skipException(msg)
|
||||
if not CONF.share.run_snapshot_tests:
|
||||
msg = "Snapshot tests are disabled."
|
||||
raise cls.skipException(msg)
|
||||
|
||||
@classmethod
|
||||
def resource_setup(cls):
|
||||
super(RevertToSnapshotTest, cls).resource_setup()
|
||||
cls.admin_client = cls.admin_shares_v2_client
|
||||
pools = cls.admin_client.list_pools(detail=True)['pools']
|
||||
revert_support = [
|
||||
pool['capabilities'][constants.REVERT_TO_SNAPSHOT_SUPPORT]
|
||||
for pool in pools]
|
||||
if not any(revert_support):
|
||||
msg = "Revert to snapshot not supported."
|
||||
raise cls.skipException(msg)
|
||||
|
||||
cls.share_type_name = data_utils.rand_name("share-type")
|
||||
extra_specs = {constants.REVERT_TO_SNAPSHOT_SUPPORT: True}
|
||||
cls.revert_enabled_extra_specs = cls.add_extra_specs_to_dict(
|
||||
extra_specs=extra_specs)
|
||||
|
||||
cls.share_type = cls.create_share_type(
|
||||
cls.share_type_name,
|
||||
extra_specs=cls.revert_enabled_extra_specs,
|
||||
client=cls.admin_client)
|
||||
|
||||
cls.st_id = cls.share_type['share_type']['id']
|
||||
|
||||
cls.share = cls.create_share(share_type_id=cls.st_id)
|
||||
|
||||
@tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND)
|
||||
@ddt.data(
|
||||
*{constants.REVERT_TO_SNAPSHOT_MICROVERSION,
|
||||
CONF.share.max_api_microversion}
|
||||
)
|
||||
def test_revert_to_latest_snapshot(self, version):
|
||||
snapshot = self.create_snapshot_wait_for_active(self.share['id'],
|
||||
cleanup_in_class=False)
|
||||
self.shares_v2_client.revert_to_snapshot(
|
||||
self.share['id'],
|
||||
snapshot['id'],
|
||||
version=version)
|
||||
self.shares_v2_client.wait_for_share_status(self.share['id'],
|
||||
constants.STATUS_AVAILABLE)
|
||||
|
||||
@tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND)
|
||||
@ddt.data(
|
||||
*{constants.REVERT_TO_SNAPSHOT_MICROVERSION,
|
||||
CONF.share.max_api_microversion}
|
||||
)
|
||||
def test_revert_to_previous_snapshot(self, version):
|
||||
snapshot1 = self.create_snapshot_wait_for_active(
|
||||
self.share['id'], cleanup_in_class=False)
|
||||
snapshot2 = self.create_snapshot_wait_for_active(
|
||||
self.share['id'], cleanup_in_class=False)
|
||||
|
||||
self.shares_v2_client.delete_snapshot(snapshot2['id'])
|
||||
self.shares_v2_client.wait_for_resource_deletion(
|
||||
snapshot_id=snapshot2['id'])
|
||||
|
||||
self.shares_v2_client.revert_to_snapshot(self.share['id'],
|
||||
snapshot1['id'],
|
||||
version=version)
|
||||
self.shares_v2_client.wait_for_share_status(self.share['id'],
|
||||
constants.STATUS_AVAILABLE)
|
@ -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
|
||||
from testtools import testcase as tc
|
||||
|
||||
from manila_tempest_tests.common import constants
|
||||
from manila_tempest_tests.tests.api import base
|
||||
from manila_tempest_tests import utils
|
||||
|
||||
@ -106,6 +107,9 @@ class SharesActionsTest(base.BaseSharesTest):
|
||||
expected_keys.append("user_id")
|
||||
if utils.is_microversion_ge(version, '2.24'):
|
||||
expected_keys.append("create_share_from_snapshot_support")
|
||||
if utils.is_microversion_ge(version,
|
||||
constants.REVERT_TO_SNAPSHOT_MICROVERSION):
|
||||
expected_keys.append("revert_to_snapshot_support")
|
||||
actual_keys = list(share.keys())
|
||||
[self.assertIn(key, actual_keys) for key in expected_keys]
|
||||
|
||||
@ -167,6 +171,12 @@ class SharesActionsTest(base.BaseSharesTest):
|
||||
def test_get_share_with_create_share_from_snapshot_support(self):
|
||||
self._get_share('2.24')
|
||||
|
||||
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
|
||||
@utils.skip_if_microversion_not_supported(
|
||||
constants.REVERT_TO_SNAPSHOT_MICROVERSION)
|
||||
def test_get_share_with_revert_to_snapshot_support(self):
|
||||
self._get_share(constants.REVERT_TO_SNAPSHOT_MICROVERSION)
|
||||
|
||||
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
|
||||
def test_list_shares(self):
|
||||
|
||||
@ -213,6 +223,9 @@ class SharesActionsTest(base.BaseSharesTest):
|
||||
keys.append("user_id")
|
||||
if utils.is_microversion_ge(version, '2.24'):
|
||||
keys.append("create_share_from_snapshot_support")
|
||||
if utils.is_microversion_ge(version,
|
||||
constants.REVERT_TO_SNAPSHOT_MICROVERSION):
|
||||
keys.append("revert_to_snapshot_support")
|
||||
[self.assertIn(key, sh.keys()) for sh in shares for key in keys]
|
||||
|
||||
# our shares in list and have no duplicates
|
||||
@ -264,6 +277,13 @@ class SharesActionsTest(base.BaseSharesTest):
|
||||
self):
|
||||
self._list_shares_with_detail('2.24')
|
||||
|
||||
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
|
||||
@utils.skip_if_microversion_not_supported(
|
||||
constants.REVERT_TO_SNAPSHOT_MICROVERSION)
|
||||
def test_list_shares_with_detail_with_revert_to_snapshot_support(self):
|
||||
self._list_shares_with_detail(
|
||||
constants.REVERT_TO_SNAPSHOT_MICROVERSION)
|
||||
|
||||
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
|
||||
def test_list_shares_with_detail_filter_by_metadata(self):
|
||||
filters = {'metadata': self.metadata}
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- Added revert-to-snapshot feature for regular and replicated shares.
|
||||
- Added revert-to-snapshot support to the LVM driver.
|
Loading…
Reference in New Issue
Block a user