diff --git a/api-ref/source/samples/share-actions-revert-to-snapshot-request.json b/api-ref/source/samples/share-actions-revert-to-snapshot-request.json new file mode 100644 index 0000000000..6859ed6a10 --- /dev/null +++ b/api-ref/source/samples/share-actions-revert-to-snapshot-request.json @@ -0,0 +1,5 @@ +{ + "revert": { + "snapshot_id": "6020af24-a305-4155-9a29-55e20efcb0e8" + } +} diff --git a/api-ref/source/share-actions.inc b/api-ref/source/share-actions.inc index 926763f5ad..3aa996a58e 100644 --- a/api-ref/source/share-actions.inc +++ b/api-ref/source/share-actions.inc @@ -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 diff --git a/api-ref/source/shares.inc b/api-ref/source/shares.inc index d0aa77ab62..442fd62e40 100644 --- a/api-ref/source/shares.inc +++ b/api-ref/source/shares.inc @@ -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 diff --git a/api-ref/source/snapshots.inc b/api-ref/source/snapshots.inc index 8e058f80d7..b4ee4c958d 100644 --- a/api-ref/source/snapshots.inc +++ b/api-ref/source/snapshots.inc @@ -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. diff --git a/contrib/ci/post_test_hook.sh b/contrib/ci/post_test_hook.sh index 7f66b250fc..1611fa2e48 100755 --- a/contrib/ci/post_test_hook.sh +++ b/contrib/ci/post_test_hook.sh @@ -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"} diff --git a/contrib/ci/pre_test_hook.sh b/contrib/ci/pre_test_hook.sh index 1f9afe2abf..bb1a8112c6 100755 --- a/contrib/ci/pre_test_hook.sh +++ b/contrib/ci/pre_test_hook.sh @@ -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 diff --git a/doc/source/devref/capabilities_and_extra_specs.rst b/doc/source/devref/capabilities_and_extra_specs.rst index 1729313fd8..3547ae8e90 100644 --- a/doc/source/devref/capabilities_and_extra_specs.rst +++ b/doc/source/devref/capabilities_and_extra_specs.rst @@ -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, diff --git a/doc/source/devref/share_back_ends_feature_support_mapping.rst b/doc/source/devref/share_back_ends_feature_support_mapping.rst index 7077f742d4..2963d49d39 100644 --- a/doc/source/devref/share_back_ends_feature_support_mapping.rst +++ b/doc/source/devref/share_back_ends_feature_support_mapping.rst @@ -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:: diff --git a/etc/manila/policy.json b/etc/manila/policy.json index d8188f6653..4736dd70f9 100644 --- a/etc/manila/policy.json +++ b/etc/manila/policy.json @@ -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", diff --git a/etc/manila/rootwrap.d/share.filters b/etc/manila/rootwrap.d/share.filters index 6c5229a6fe..a67b3600d3 100644 --- a/etc/manila/rootwrap.d/share.filters +++ b/etc/manila/rootwrap.d/share.filters @@ -171,5 +171,12 @@ brctl: CommandFilter, brctl, root # manila/share/drivers/container/container.py: e2fsck 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 diff --git a/manila/api/openstack/api_version_request.py b/manila/api/openstack/api_version_request.py index 99b2323f16..a3b2d95279 100644 --- a/manila/api/openstack/api_version_request.py +++ b/manila/api/openstack/api_version_request.py @@ -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 diff --git a/manila/api/openstack/rest_api_version_history.rst b/manila/api/openstack/rest_api_version_history.rst index 0030f494ce..ab5b8d1df3 100644 --- a/manila/api/openstack/rest_api_version_history.rst +++ b/manila/api/openstack/rest_api_version_history.rst @@ -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. diff --git a/manila/api/v1/shares.py b/manila/api/v1/shares.py index 02ac1dab52..c1ef1cf23e 100644 --- a/manila/api/v1/shares.py +++ b/manila/api/v1/shares.py @@ -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) diff --git a/manila/api/v2/shares.py b/manila/api/v2/shares.py index 9cf0f9bec7..3b78b5ecdc 100644 --- a/manila/api/v2/shares.py +++ b/manila/api/v2/shares.py @@ -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()) diff --git a/manila/api/views/shares.py b/manila/api/views/shares.py index f70d623932..f592eeebaf 100644 --- a/manila/api/views/shares.py +++ b/manila/api/views/shares.py @@ -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] diff --git a/manila/common/constants.py b/manila/common/constants.py index 06d42bb219..b8c7fc8632 100644 --- a/manila/common/constants.py +++ b/manila/common/constants.py @@ -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') diff --git a/manila/db/api.py b/manila/db/api.py index 9b7fad9dba..05cab8c5b9 100644 --- a/manila/db/api.py +++ b/manila/db/api.py @@ -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. diff --git a/manila/db/migrations/alembic/versions/87ce15c59bbe_add_revert_to_snapshot_support.py b/manila/db/migrations/alembic/versions/87ce15c59bbe_add_revert_to_snapshot_support.py new file mode 100644 index 0000000000..d4fd675aae --- /dev/null +++ b/manila/db/migrations/alembic/versions/87ce15c59bbe_add_revert_to_snapshot_support.py @@ -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') diff --git a/manila/db/sqlalchemy/api.py b/manila/db/sqlalchemy/api.py index 87169d2d7a..ec69e91708 100644 --- a/manila/db/sqlalchemy/api.py +++ b/manila/db/sqlalchemy/api.py @@ -2194,6 +2194,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): diff --git a/manila/db/sqlalchemy/models.py b/manila/db/sqlalchemy/models.py index 6265425ac9..caae19335e 100644 --- a/manila/db/sqlalchemy/models.py +++ b/manila/db/sqlalchemy/models.py @@ -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) diff --git a/manila/scheduler/host_manager.py b/manila/scheduler/host_manager.py index df6ca3b338..0cc2dbfac0 100644 --- a/manila/scheduler/host_manager.py +++ b/manila/scheduler/host_manager.py @@ -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'] diff --git a/manila/scheduler/utils.py b/manila/scheduler/utils.py index 7569b2ee2b..674d581c26 100644 --- a/manila/scheduler/utils.py +++ b/manila/scheduler/utils.py @@ -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, diff --git a/manila/share/api.py b/manila/share/api.py index 29b8fb5c50..04527cc4c8 100644 --- a/manila/share/api.py +++ b/manila/share/api.py @@ -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.""" diff --git a/manila/share/driver.py b/manila/share/driver.py index 5ef9f260a1..4d1d4ff971 100644 --- a/manila/share/driver.py +++ b/manila/share/driver.py @@ -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. diff --git a/manila/share/drivers/lvm.py b/manila/share/drivers/lvm.py index 80570d9947..b8e7d8798b 100644 --- a/manila/share/drivers/lvm.py +++ b/manila/share/drivers/lvm.py @@ -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) diff --git a/manila/share/manager.py b/manila/share/manager.py index a57261694f..84f6fa503f 100644 --- a/manila/share/manager.py +++ b/manila/share/manager.py @@ -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 diff --git a/manila/share/rpcapi.py b/manila/share/rpcapi.py index 7bacce784c..bcddadc2f8 100644 --- a/manila/share/rpcapi.py +++ b/manila/share/rpcapi.py @@ -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') diff --git a/manila/share/share_types.py b/manila/share/share_types.py index 10c7be01c9..3489c65ad5 100644 --- a/manila/share/share_types.py +++ b/manila/share/share_types.py @@ -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 diff --git a/manila/tests/api/contrib/stubs.py b/manila/tests/api/contrib/stubs.py index ed339c3232..9a71dfbbc3 100644 --- a/manila/tests/api/contrib/stubs.py +++ b/manila/tests/api/contrib/stubs.py @@ -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, } diff --git a/manila/tests/api/v2/test_share_types.py b/manila/tests/api/v2/test_share_types.py index a683d00d24..9ea3e61c36 100644 --- a/manila/tests/api/v2/test_share_types.py +++ b/manila/tests/api/v2/test_share_types.py @@ -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() diff --git a/manila/tests/api/v2/test_shares.py b/manila/tests/api/v2/test_shares.py index 89fca6c3ba..5b302fe485 100644 --- a/manila/tests/api/v2/test_shares.py +++ b/manila/tests/api/v2/test_shares.py @@ -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') diff --git a/manila/tests/api/views/test_shares.py b/manila/tests/api/views/test_shares.py index d55b1a1090..4cee082b74 100644 --- a/manila/tests/api/views/test_shares.py +++ b/manila/tests/api/views/test_shares.py @@ -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']) diff --git a/manila/tests/db/migrations/alembic/migrations_data_checks.py b/manila/tests/db/migrations/alembic/migrations_data_checks.py index 9055587490..de94669bb3 100644 --- a/manila/tests/db/migrations/alembic/migrations_data_checks.py +++ b/manila/tests/db/migrations/alembic/migrations_data_checks.py @@ -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' diff --git a/manila/tests/db/sqlalchemy/test_api.py b/manila/tests/db/sqlalchemy/test_api.py index aae1343405..e0b55308cb 100644 --- a/manila/tests/db/sqlalchemy/test_api.py +++ b/manila/tests/db/sqlalchemy/test_api.py @@ -17,6 +17,8 @@ """Testing of SQLAlchemy backend.""" +import copy + import ddt import mock @@ -899,6 +901,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) diff --git a/manila/tests/db/sqlalchemy/test_models.py b/manila/tests/db/sqlalchemy/test_models.py index f0ef0848de..6f75faa2f5 100644 --- a/manila/tests/db/sqlalchemy/test_models.py +++ b/manila/tests/db/sqlalchemy/test_models.py @@ -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): diff --git a/manila/tests/scheduler/fakes.py b/manila/tests/scheduler/fakes.py index 0d704f9816..2385bb674c 100644 --- a/manila/tests/scheduler/fakes.py +++ b/manila/tests/scheduler/fakes.py @@ -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', diff --git a/manila/tests/scheduler/test_host_manager.py b/manila/tests/scheduler/test_host_manager.py index a926a11e89..9d6d759063 100644 --- a/manila/tests/scheduler/test_host_manager.py +++ b/manila/tests/scheduler/test_host_manager.py @@ -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, diff --git a/manila/tests/share/drivers/dell_emc/test_driver.py b/manila/tests/share/drivers/dell_emc/test_driver.py index c4d30931e3..fd1c479868 100644 --- a/manila/tests/share/drivers/dell_emc/test_driver.py +++ b/manila/tests/share/drivers/dell_emc/test_driver.py @@ -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 diff --git a/manila/tests/share/drivers/dummy.py b/manila/tests/share/drivers/dummy.py index a8fa569799..441f1274fb 100644 --- a/manila/tests/share/drivers/dummy.py +++ b/manila/tests/share/drivers/dummy.py @@ -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): diff --git a/manila/tests/share/drivers/glusterfs/test_glusterfs_native.py b/manila/tests/share/drivers/glusterfs/test_glusterfs_native.py index 005f76b01c..bcf5f4c8b0 100644 --- a/manila/tests/share/drivers/glusterfs/test_glusterfs_native.py +++ b/manila/tests/share/drivers/glusterfs/test_glusterfs_native.py @@ -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, diff --git a/manila/tests/share/drivers/hpe/test_hpe_3par_driver.py b/manila/tests/share/drivers/hpe/test_hpe_3par_driver.py index 28c5dd7c0f..5669d72b9a 100644 --- a/manila/tests/share/drivers/hpe/test_hpe_3par_driver.py +++ b/manila/tests/share/drivers/hpe/test_hpe_3par_driver.py @@ -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, diff --git a/manila/tests/share/drivers/huawei/test_huawei_nas.py b/manila/tests/share/drivers/huawei/test_huawei_nas.py index 44f2354495..8a11429dd8 100644 --- a/manila/tests/share/drivers/huawei/test_huawei_nas.py +++ b/manila/tests/share/drivers/huawei/test_huawei_nas.py @@ -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, diff --git a/manila/tests/share/drivers/test_lvm.py b/manila/tests/share/drivers/test_lvm.py index e0b492d2f5..69d93496fc 100644 --- a/manila/tests/share/drivers/test_lvm.py +++ b/manila/tests/share/drivers/test_lvm.py @@ -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()) diff --git a/manila/tests/share/drivers/zfsonlinux/test_driver.py b/manila/tests/share/drivers/zfsonlinux/test_driver.py index 8bff2b2b5b..9ddbc24efa 100644 --- a/manila/tests/share/drivers/zfsonlinux/test_driver.py +++ b/manila/tests/share/drivers/zfsonlinux/test_driver.py @@ -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', diff --git a/manila/tests/share/test_api.py b/manila/tests/share/test_api.py index f9a0f594f8..8dce301b41 100644 --- a/manila/tests/share/test_api.py +++ b/manila/tests/share/test_api.py @@ -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, }, } diff --git a/manila/tests/share/test_driver.py b/manila/tests/share/test_driver.py index 769be5b409..75fc3099d7 100644 --- a/manila/tests/share/test_driver.py +++ b/manila/tests/share/test_driver.py @@ -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) diff --git a/manila/tests/share/test_manager.py b/manila/tests/share/test_manager.py index 54c4c04d97..782decd671 100644 --- a/manila/tests/share/test_manager.py +++ b/manila/tests/share/test_manager.py @@ -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() diff --git a/manila/tests/share/test_rpcapi.py b/manila/tests/share/test_rpcapi.py index c63c21cc73..5322840747 100644 --- a/manila/tests/share/test_rpcapi.py +++ b/manila/tests/share/test_rpcapi.py @@ -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', diff --git a/manila/tests/share/test_share_types.py b/manila/tests/share/test_share_types.py index e2a8391ddb..5592fdd124 100644 --- a/manila/tests/share/test_share_types.py +++ b/manila/tests/share/test_share_types.py @@ -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,), diff --git a/manila_tempest_tests/common/constants.py b/manila_tempest_tests/common/constants.py index abef181497..a478487040 100644 --- a/manila_tempest_tests/common/constants.py +++ b/manila_tempest_tests/common/constants.py @@ -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' diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py index aebe1254d6..11631143d7 100644 --- a/manila_tempest_tests/config.py +++ b/manila_tempest_tests/config.py @@ -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. " diff --git a/manila_tempest_tests/plugin.py b/manila_tempest_tests/plugin.py index dfec0b182e..7e1fa120d7 100644 --- a/manila_tempest_tests/plugin.py +++ b/manila_tempest_tests/plugin.py @@ -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), diff --git a/manila_tempest_tests/services/share/v2/json/shares_client.py b/manila_tempest_tests/services/share/v2/json/shares_client.py index cbc26b5063..0d88963c71 100644 --- a/manila_tempest_tests/services/share/v2/json/shares_client.py +++ b/manila_tempest_tests/services/share/v2/json/shares_client.py @@ -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): diff --git a/manila_tempest_tests/tests/api/admin/test_share_types_extra_specs_negative.py b/manila_tempest_tests/tests/api/admin/test_share_types_extra_specs_negative.py index 85cbc5df95..63fdf233d1 100644 --- a/manila_tempest_tests/tests/api/admin/test_share_types_extra_specs_negative.py +++ b/manila_tempest_tests/tests/api/admin/test_share_types_extra_specs_negative.py @@ -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; ' diff --git a/manila_tempest_tests/tests/api/base.py b/manila_tempest_tests/tests/api/base.py index 4459f59268..398108d3eb 100644 --- a/manila_tempest_tests/tests/api/base.py +++ b/manila_tempest_tests/tests/api/base.py @@ -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 diff --git a/manila_tempest_tests/tests/api/test_revert_to_snapshot.py b/manila_tempest_tests/tests/api/test_revert_to_snapshot.py new file mode 100644 index 0000000000..686b185b19 --- /dev/null +++ b/manila_tempest_tests/tests/api/test_revert_to_snapshot.py @@ -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) diff --git a/manila_tempest_tests/tests/api/test_revert_to_snapshot_negative.py b/manila_tempest_tests/tests/api/test_revert_to_snapshot_negative.py new file mode 100644 index 0000000000..ee403af26d --- /dev/null +++ b/manila_tempest_tests/tests/api/test_revert_to_snapshot_negative.py @@ -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) diff --git a/manila_tempest_tests/tests/api/test_shares_actions.py b/manila_tempest_tests/tests/api/test_shares_actions.py index d03df33db3..0f904fc91c 100644 --- a/manila_tempest_tests/tests/api/test_shares_actions.py +++ b/manila_tempest_tests/tests/api/test_shares_actions.py @@ -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} diff --git a/releasenotes/notes/share-revert-to-snapshot-3d028fa00620651e.yaml b/releasenotes/notes/share-revert-to-snapshot-3d028fa00620651e.yaml new file mode 100644 index 0000000000..aeda96721b --- /dev/null +++ b/releasenotes/notes/share-revert-to-snapshot-3d028fa00620651e.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added revert-to-snapshot feature for regular and replicated shares. + - Added revert-to-snapshot support to the LVM driver.