Merge "Resource Locks: Support for share deletion lock"
This commit is contained in:
commit
aafc221027
@ -55,6 +55,7 @@ shared file system storage resources.
|
||||
.. include:: share-group-types.inc
|
||||
.. include:: share-group-snapshots.inc
|
||||
.. include:: share-transfers.inc
|
||||
.. include:: resource-locks.inc
|
||||
|
||||
======================================
|
||||
Shared File Systems API (EXPERIMENTAL)
|
||||
|
@ -1,5 +1,16 @@
|
||||
# variables in header
|
||||
#{}
|
||||
service_token_locks:
|
||||
description: |
|
||||
An auth-token specified via the header ``X-Service-Token``. With the
|
||||
OpenStack Identity (Keystone) context, this token can be obtained by
|
||||
a user that has the ``service`` role. The presence of this header is
|
||||
used by resource lock API methods to set or match the lock user's context.
|
||||
A resource lock created by a service user cannot be manipulated by
|
||||
non-service users.
|
||||
in: header
|
||||
required: false
|
||||
type: string
|
||||
|
||||
# variables in path
|
||||
access_id_path:
|
||||
@ -83,6 +94,12 @@ quota_class_name:
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
resource_lock_id_path:
|
||||
description: |
|
||||
The UUID of the resource lock.
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
security_service_id_path:
|
||||
description: |
|
||||
The UUID of the security service.
|
||||
@ -433,8 +450,7 @@ nova_net_id_query:
|
||||
max_version: 2.26
|
||||
offset:
|
||||
description: |
|
||||
The offset to define start point of share or share group
|
||||
listing.
|
||||
The offset to define start point of resource listing.
|
||||
in: query
|
||||
required: false
|
||||
type: integer
|
||||
@ -463,6 +479,69 @@ resource_id:
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
resource_lock_all_projects_query:
|
||||
description: |
|
||||
Set this parameter to True to get resource locks across all project
|
||||
namespaces.
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
resource_lock_id_query:
|
||||
description: |
|
||||
The ID of the resource lock to filter resource locks by.
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
resource_lock_lock_context_query:
|
||||
description: |
|
||||
The lock creator's context to filter locks by.
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
resource_lock_lock_reason_inexact_query:
|
||||
description: |
|
||||
The lock reason pattern that can be used to filter resource locks.
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
resource_lock_lock_reason_query:
|
||||
description: |
|
||||
The lock reason that can be used to filter resource locks.
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
resource_lock_project_id_query:
|
||||
description: |
|
||||
The ID of a project to filter resource locks by.
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
resource_lock_resource_action_query:
|
||||
description: |
|
||||
The ``action`` prevented by the filtered resource locks.
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
resource_lock_resource_id_query:
|
||||
description: |
|
||||
The ID of the resource that the locks pertain to to filter resource
|
||||
locks by.
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
resource_lock_resource_type_query:
|
||||
description: |
|
||||
The type of the resource that the locks pertain to to filter resource
|
||||
locks by.
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
resource_lock_user_id_query:
|
||||
description: |
|
||||
The ID of a user to filter resource locks by.
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
resource_type:
|
||||
description: |
|
||||
The type of the resource for which the message was created.
|
||||
@ -618,6 +697,15 @@ sort_key_messages:
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
sort_key_resource_locks:
|
||||
description: |
|
||||
The key to sort a list of resource locks. A valid value
|
||||
is ``id``, ``resource_id``, ``resource_type``, ``resource_action``,
|
||||
``user_id``, ``project_id``, ``created_at``, ``updated_act``,
|
||||
``lock_context``.
|
||||
in: query
|
||||
required: false
|
||||
type: string
|
||||
sort_key_transfer:
|
||||
description: |
|
||||
The key to sort a list of transfers. A valid value
|
||||
@ -654,13 +742,22 @@ user_id_query:
|
||||
with_count_query:
|
||||
description: |
|
||||
Whether to show ``count`` in share list API response or not, default is ``False``.
|
||||
This query parameter is useful with pagination.
|
||||
in: query
|
||||
required: false
|
||||
type: boolean
|
||||
min_version: 2.42
|
||||
with_count_query_without_min_version:
|
||||
description: |
|
||||
Whether to show ``count`` in API response or not, default is ``False``.
|
||||
This query parameter is useful with pagination.
|
||||
in: query
|
||||
required: false
|
||||
type: boolean
|
||||
with_count_snapshot_query:
|
||||
description: |
|
||||
Whether to show ``count`` in share snapshot list API response or not, default is ``False``.
|
||||
This query parameter is useful with pagination.
|
||||
in: query
|
||||
required: false
|
||||
type: boolean
|
||||
@ -1023,6 +1120,14 @@ count:
|
||||
required: false
|
||||
type: integer
|
||||
min_version: 2.42
|
||||
count_without_min_version:
|
||||
description: |
|
||||
The total count of requested resource before pagination is applied. This
|
||||
parameter is only present in the API response if "with_count=True" is
|
||||
supplied in the query.
|
||||
in: body
|
||||
required: false
|
||||
type: integer
|
||||
create_share_from_snapshot_support:
|
||||
description: |
|
||||
Boolean extra spec used for filtering of back ends by
|
||||
@ -2415,6 +2520,109 @@ resource_id_body:
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
resource_lock_id:
|
||||
description: |
|
||||
The UUID identifying the specific resource lock.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
resource_lock_lock_context:
|
||||
description: |
|
||||
The lock creator's context. Resource locks can be created by users with
|
||||
different roles. If a user with ``admin`` role creates the lock, the value
|
||||
of this field is ``admin``. If a user with ``service`` role creates the
|
||||
lock, the value of this field is ``service``. For all other contexts, the
|
||||
value of this field is ``user``. This field also determines the user's
|
||||
role that is required to unlock or manipulate a lock by virtue of the
|
||||
service's default RBAC.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
resource_lock_lock_reason:
|
||||
description: |
|
||||
A blob of text representing the reason for the specific resource lock.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
resource_lock_lock_reason_optional:
|
||||
description: |
|
||||
A blob of text representing the reason for the specific resource lock.
|
||||
in: body
|
||||
required: false
|
||||
type: string
|
||||
resource_lock_object:
|
||||
description: |
|
||||
A resource lock object when making resource lock requests. All other
|
||||
parameters are included in this object.
|
||||
in: body
|
||||
required: true
|
||||
type: object
|
||||
resource_lock_project_id:
|
||||
description: |
|
||||
The ID of the project that the resource lock was created for.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
resource_lock_resource_action:
|
||||
description: |
|
||||
The action pertaining to a resource that the resource lock prevents. For
|
||||
example, if a resource lock prevents deletion of a share, the value of
|
||||
``resource_action`` is ``delete``. Resource locks are not supported for
|
||||
all API actions. Currently support only exists for ``delete``, and for
|
||||
specific resources.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
resource_lock_resource_action_create_optional:
|
||||
description: |
|
||||
The action pertaining to a resource that the resource lock prevents. For
|
||||
example, if a resource lock prevents deletion of a share, the value of
|
||||
``resource_action`` is ``delete``. Resource locks are not supported for
|
||||
all API actions. Currently support only exists for ``delete``, and for
|
||||
specific resources. If not provided, the value of this parameter
|
||||
defaults to ``delete``.
|
||||
in: body
|
||||
required: false
|
||||
type: string
|
||||
resource_lock_resource_action_optional:
|
||||
description: |
|
||||
The action pertaining to a resource that the resource lock prevents. For
|
||||
example, if a resource lock prevents deletion of a share, the value of
|
||||
``resource_action`` is ``delete``. Resource locks are not supported for
|
||||
all API actions. Currently support only exists for ``delete``, and for
|
||||
specific resources.
|
||||
in: body
|
||||
required: false
|
||||
type: string
|
||||
resource_lock_resource_id:
|
||||
description: |
|
||||
The UUID of the resource that the lock pertains to. For example, this
|
||||
can be the ID of the share that is locked from deletion.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
resource_lock_resource_type:
|
||||
description: |
|
||||
The type of resource that the ID in ``resource_id`` denotes. For
|
||||
example, ``share`` is the resource type that is specified when the
|
||||
resource lock pertains to a share being locked from deletion. Resource
|
||||
locks are not supported for all resources. Currently support only
|
||||
exists for ``share``.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
resource_lock_user_id:
|
||||
description: |
|
||||
The ID of the user the resource lock was created for.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
resource_locks_object:
|
||||
description: |
|
||||
A resource locks object containing a collection or list of resource locks.
|
||||
in: body
|
||||
required: true
|
||||
type: object
|
||||
resource_type_body:
|
||||
description: |
|
||||
The type of the resource for which the message was created.
|
||||
|
325
api-ref/source/resource-locks.inc
Normal file
325
api-ref/source/resource-locks.inc
Normal file
@ -0,0 +1,325 @@
|
||||
.. -*- rst -*-
|
||||
|
||||
Resource Locks (since API v2.81)
|
||||
================================
|
||||
|
||||
Create, list, update and delete locks on user actions on resources.
|
||||
|
||||
|
||||
Create a resource lock
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. rest_method:: POST /v2/resource-locks
|
||||
|
||||
.. versionadded:: 2.81
|
||||
|
||||
Lock a specific action on a given resource.
|
||||
|
||||
Not all resources are supported, and not actions on supported resources can
|
||||
be prevented with this mechanism. A lock can only be removed or manipulated
|
||||
by the user that created it, or by a more privileged user. The cloud
|
||||
administrator can use a ``policy.yaml`` file to tweak permissions on who
|
||||
can manipulate and delete locks created by other users.
|
||||
|
||||
Response codes
|
||||
--------------
|
||||
|
||||
.. rest_status_code:: success status.yaml
|
||||
|
||||
- 200
|
||||
|
||||
.. rest_status_code:: error status.yaml
|
||||
|
||||
- 400
|
||||
- 401
|
||||
- 403
|
||||
- 404
|
||||
- 409
|
||||
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- service_token: service_token_locks
|
||||
- resource_lock: resource_lock_object
|
||||
- resource_id: resource_lock_resource_id
|
||||
- resource_type: resource_lock_resource_type
|
||||
- resource_action: resource_lock_resource_action_create_optional
|
||||
- lock_reason: resource_lock_lock_reason_optional
|
||||
|
||||
Request Example
|
||||
---------------
|
||||
|
||||
.. literalinclude:: ./samples/resource-lock-create-request.json
|
||||
:language: javascript
|
||||
|
||||
|
||||
Response Parameters
|
||||
-------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- resource_lock: resource_lock_object
|
||||
- id: resource_lock_id
|
||||
- user_id: resource_lock_user_id
|
||||
- project_id: resource_lock_project_id
|
||||
- lock_context: resource_lock_lock_context
|
||||
- resource_type: resource_lock_resource_type
|
||||
- resource_id: resource_lock_resource_id
|
||||
- resource_action: resource_lock_resource_action
|
||||
- lock_reason: resource_lock_lock_reason
|
||||
- created_at: created_at
|
||||
- updated_at: updated_at
|
||||
- links: links
|
||||
|
||||
Response Example
|
||||
----------------
|
||||
|
||||
.. literalinclude:: ./samples/resource-lock-create-response.json
|
||||
:language: javascript
|
||||
|
||||
|
||||
List resource locks
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. rest_method:: GET /v2/resource-locks
|
||||
|
||||
.. versionadded:: 2.81
|
||||
|
||||
Retrieve resource locks with filters
|
||||
|
||||
Response codes
|
||||
--------------
|
||||
|
||||
.. rest_status_code:: success status.yaml
|
||||
|
||||
- 200
|
||||
|
||||
.. rest_status_code:: error status.yaml
|
||||
|
||||
- 401
|
||||
- 403
|
||||
- 404
|
||||
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- service_token: service_token_locks
|
||||
- id: resource_lock_id_query
|
||||
- resource_id: resource_lock_resource_id_query
|
||||
- resource_action: resource_lock_resource_action_query
|
||||
- resource_type: resource_lock_resource_type_query
|
||||
- user_id: resource_lock_user_id_query
|
||||
- project_id: resource_lock_project_id_query
|
||||
- all_projects: resource_lock_all_projects_query
|
||||
- lock_context: resource_lock_lock_context_query
|
||||
- created_since: created_since_query
|
||||
- created_before: created_before_query
|
||||
- lock_reason: resource_lock_lock_reason_query
|
||||
- lock_reason~: resource_lock_lock_reason_inexact_query
|
||||
- sort_key: sort_key_resource_locks
|
||||
- sort_dir: sort_dir
|
||||
- offset: offset
|
||||
- with_count: with_count_query_without_min_version
|
||||
|
||||
|
||||
Response Parameters
|
||||
-------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- resource_locks: resource_locks_object
|
||||
- id: resource_lock_id
|
||||
- user_id: resource_lock_user_id
|
||||
- project_id: resource_lock_project_id
|
||||
- lock_context: resource_lock_lock_context
|
||||
- resource_type: resource_lock_resource_type
|
||||
- resource_id: resource_lock_resource_id
|
||||
- resource_action: resource_lock_resource_action
|
||||
- lock_reason: resource_lock_lock_reason
|
||||
- created_at: created_at
|
||||
- updated_at: updated_at
|
||||
- links: links
|
||||
- count: count_without_min_version
|
||||
|
||||
Response Example
|
||||
----------------
|
||||
|
||||
.. literalinclude:: ./samples/resource-lock-get-all-response.json
|
||||
:language: javascript
|
||||
|
||||
|
||||
Get a resource lock
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. rest_method:: GET /v2/resource-locks/{resource-lock-id}
|
||||
|
||||
.. versionadded:: 2.81
|
||||
|
||||
Retrieve a specific resource lock
|
||||
|
||||
By default, resource locks can be viewed by all users within a project that
|
||||
owns the locks. The cloud administrator can use a ``policy.yaml`` file to tweak
|
||||
this behavior.
|
||||
|
||||
Response codes
|
||||
--------------
|
||||
|
||||
.. rest_status_code:: success status.yaml
|
||||
|
||||
- 200
|
||||
|
||||
.. rest_status_code:: error status.yaml
|
||||
|
||||
- 401
|
||||
- 403
|
||||
- 404
|
||||
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- service_token: service_token_locks
|
||||
- resource_id: resource_lock_id_path
|
||||
|
||||
|
||||
Response Parameters
|
||||
-------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- resource_lock: resource_lock_object
|
||||
- id: resource_lock_id
|
||||
- user_id: resource_lock_user_id
|
||||
- project_id: resource_lock_project_id
|
||||
- lock_context: resource_lock_lock_context
|
||||
- resource_type: resource_lock_resource_type
|
||||
- resource_id: resource_lock_resource_id
|
||||
- resource_action: resource_lock_resource_action
|
||||
- lock_reason: resource_lock_lock_reason
|
||||
- created_at: created_at
|
||||
- updated_at: updated_at
|
||||
- links: links
|
||||
|
||||
Response Example
|
||||
----------------
|
||||
|
||||
.. literalinclude:: ./samples/resource-lock-get-response.json
|
||||
:language: javascript
|
||||
|
||||
|
||||
Update a resource lock
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. rest_method:: PUT /v2/resource-locks/{resource-lock-id}
|
||||
|
||||
.. versionadded:: 2.81
|
||||
|
||||
Update a specific resource lock
|
||||
|
||||
By default, resource locks can be updated by the user that created the lock
|
||||
unless the ``lock_context`` is set to ``admin`` or ``service``. A user with
|
||||
``service`` role is required to manipulate locks that have a ``lock_context``
|
||||
set to ``service``. Users with ``admin`` role can manipulate all locks.
|
||||
Administrators can use ``policy.yaml`` to tweak this behavior.
|
||||
|
||||
Response codes
|
||||
--------------
|
||||
|
||||
.. rest_status_code:: success status.yaml
|
||||
|
||||
- 200
|
||||
|
||||
.. rest_status_code:: error status.yaml
|
||||
|
||||
- 401
|
||||
- 403
|
||||
- 404
|
||||
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- service_token: service_token_locks
|
||||
- resource_id: resource_lock_id_path
|
||||
- resource_lock: resource_lock_object
|
||||
- resource_action: resource_lock_resource_action_optional
|
||||
- lock_reason: resource_lock_lock_reason_optional
|
||||
|
||||
Request Example
|
||||
----------------
|
||||
|
||||
.. literalinclude:: ./samples/resource-lock-update-request.json
|
||||
:language: javascript
|
||||
|
||||
Response Parameters
|
||||
-------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- resource_lock: resource_lock_object
|
||||
- id: resource_lock_id
|
||||
- user_id: resource_lock_user_id
|
||||
- project_id: resource_lock_project_id
|
||||
- lock_context: resource_lock_lock_context
|
||||
- resource_type: resource_lock_resource_type
|
||||
- resource_id: resource_lock_resource_id
|
||||
- resource_action: resource_lock_resource_action
|
||||
- lock_reason: resource_lock_lock_reason
|
||||
- created_at: created_at
|
||||
- updated_at: updated_at
|
||||
- links: links
|
||||
|
||||
Response Example
|
||||
----------------
|
||||
|
||||
.. literalinclude:: ./samples/resource-lock-update-response.json
|
||||
:language: javascript
|
||||
|
||||
|
||||
Delete a resource lock
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. rest_method:: DELETE /v2/resource-locks/{resource-lock-id}
|
||||
|
||||
.. versionadded:: 2.81
|
||||
|
||||
Delete a specific resource lock
|
||||
|
||||
By default, resource locks can be deleted by the user that created the lock
|
||||
unless the ``lock_context`` is set to ``admin`` or ``service``. A user with
|
||||
``service`` role is required to delete locks that have a ``lock_context``
|
||||
set to ``service``. Users with ``admin`` role can delete any lock.
|
||||
Administrators can use ``policy.yaml`` to tweak this behavior.
|
||||
|
||||
This request provides no response body.
|
||||
|
||||
Response codes
|
||||
--------------
|
||||
|
||||
.. rest_status_code:: success status.yaml
|
||||
|
||||
- 204
|
||||
|
||||
.. rest_status_code:: error status.yaml
|
||||
|
||||
- 401
|
||||
- 403
|
||||
- 404
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- service_token: service_token_locks
|
||||
- resource_id: resource_lock_id_path
|
8
api-ref/source/samples/resource-lock-create-request.json
Normal file
8
api-ref/source/samples/resource-lock-create-request.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"resource_lock": {
|
||||
"resource_id": "5a313549-d346-44b6-9650-738ce08a9fee",
|
||||
"resource_type": "share",
|
||||
"resource_action": "delete",
|
||||
"lock_reason": "Locked for deletion until year end audit."
|
||||
}
|
||||
}
|
24
api-ref/source/samples/resource-lock-create-response.json
Normal file
24
api-ref/source/samples/resource-lock-create-response.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"resource_lock": {
|
||||
"id": "713dc92d-bf5e-4b04-875b-2b2d284d8f94",
|
||||
"user_id": "89de351d3b5744b9853ec4829aa0e714",
|
||||
"project_id": "db2e72fef7864bbbbf210f22da7f1158",
|
||||
"lock_context": "user",
|
||||
"resource_type": "share",
|
||||
"resource_id": "5a313549-d346-44b6-9650-738ce08a9fee",
|
||||
"resource_action": "delete",
|
||||
"lock_reason": "Locked for deletion until year end audit.",
|
||||
"created_at": "2023-07-17T22:11:48.144302",
|
||||
"updated_at": null,
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://203.0.113.30/share/v2/resource_locks/713dc92d-bf5e-4b04-875b-2b2d284d8f94"
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://203.0.113.30/share/resource_locks/713dc92d-bf5e-4b04-875b-2b2d284d8f94"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
48
api-ref/source/samples/resource-lock-get-all-response.json
Normal file
48
api-ref/source/samples/resource-lock-get-all-response.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"resource_locks": [
|
||||
{
|
||||
"id": "118750ee-b62b-4cae-9a94-7da29a4f831f",
|
||||
"user_id": "89de351d3b5744b9853ec4829aa0e714",
|
||||
"project_id": "db2e72fef7864bbbbf210f22da7f1158",
|
||||
"lock_context": "user",
|
||||
"resource_type": "share",
|
||||
"resource_id": "4c0b4d35-4ea8-4811-a1e2-a065c64225a8",
|
||||
"resource_action": "delete",
|
||||
"lock_reason": null,
|
||||
"created_at": "2023-07-17T22:53:18.894553",
|
||||
"updated_at": null,
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://203.0.113.30/share/v2/resource_locks/118750ee-b62b-4cae-9a94-7da29a4f831f"
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://203.0.113.30/share/resource_locks/118750ee-b62b-4cae-9a94-7da29a4f831f"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "713dc92d-bf5e-4b04-875b-2b2d284d8f94",
|
||||
"user_id": "89de351d3b5744b9853ec4829aa0e714",
|
||||
"project_id": "db2e72fef7864bbbbf210f22da7f1158",
|
||||
"lock_context": "user",
|
||||
"resource_type": "share",
|
||||
"resource_id": "5a313549-d346-44b6-9650-738ce08a9fee",
|
||||
"resource_action": "delete",
|
||||
"lock_reason": "Locked for deletion until year end audit.",
|
||||
"created_at": "2023-07-17T22:11:48.144302",
|
||||
"updated_at": null,
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://203.0.113.30/share/v2/resource_locks/713dc92d-bf5e-4b04-875b-2b2d284d8f94"
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://203.0.113.30/share/resource_locks/713dc92d-bf5e-4b04-875b-2b2d284d8f94"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
24
api-ref/source/samples/resource-lock-get-response.json
Normal file
24
api-ref/source/samples/resource-lock-get-response.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"resource_lock": {
|
||||
"id": "713dc92d-bf5e-4b04-875b-2b2d284d8f94",
|
||||
"user_id": "89de351d3b5744b9853ec4829aa0e714",
|
||||
"project_id": "db2e72fef7864bbbbf210f22da7f1158",
|
||||
"lock_context": "user",
|
||||
"resource_type": "share",
|
||||
"resource_id": "5a313549-d346-44b6-9650-738ce08a9fee",
|
||||
"resource_action": "delete",
|
||||
"lock_reason": "Locked for deletion until year end audit.",
|
||||
"created_at": "2023-07-17T22:11:48.144302",
|
||||
"updated_at": null,
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://203.0.113.30/share/v2/resource_locks/713dc92d-bf5e-4b04-875b-2b2d284d8f94"
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://203.0.113.30/share/resource_locks/713dc92d-bf5e-4b04-875b-2b2d284d8f94"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
5
api-ref/source/samples/resource-lock-update-request.json
Normal file
5
api-ref/source/samples/resource-lock-update-request.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"resource_lock": {
|
||||
"lock_reason": "This is a protected share"
|
||||
}
|
||||
}
|
24
api-ref/source/samples/resource-lock-update-response.json
Normal file
24
api-ref/source/samples/resource-lock-update-response.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"resource_lock": {
|
||||
"id": "118750ee-b62b-4cae-9a94-7da29a4f831f",
|
||||
"user_id": "89de351d3b5744b9853ec4829aa0e714",
|
||||
"project_id": "db2e72fef7864bbbbf210f22da7f1158",
|
||||
"lock_context": "user",
|
||||
"resource_type": "share",
|
||||
"resource_id": "4c0b4d35-4ea8-4811-a1e2-a065c64225a8",
|
||||
"resource_action": "delete",
|
||||
"lock_reason": "This is a protected share",
|
||||
"created_at": "2023-07-17T22:53:18.894553",
|
||||
"updated_at": "2023-07-17T23:18:44.284565",
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://203.0.113.30/share/v2/resource_locks/118750ee-b62b-4cae-9a94-7da29a4f831f"
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://203.0.113.30/share/resource_locks/118750ee-b62b-4cae-9a94-7da29a4f831f"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -1031,4 +1031,76 @@ Share Transfer
|
||||
| expires_at | 2023-05-25T14:42:11.176049 |
|
||||
+------------------------+--------------------------------------+
|
||||
|
||||
Resource locks
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
* Prevent a share from being deleted by creating a ``resource lock``:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack share lock create myshare share
|
||||
+-----------------+--------------------------------------+
|
||||
| Field | Value |
|
||||
+-----------------+--------------------------------------+
|
||||
| created_at | 2023-07-18T05:11:56.626667 |
|
||||
| id | dc7ec691-a505-47d0-b2ec-8eb7fb9270e4 |
|
||||
| lock_context | user |
|
||||
| lock_reason | None |
|
||||
| project_id | db2e72fef7864bbbbf210f22da7f1158 |
|
||||
| resource_action | delete |
|
||||
| resource_id | 4c0b4d35-4ea8-4811-a1e2-a065c64225a8 |
|
||||
| resource_type | share |
|
||||
| updated_at | None |
|
||||
| user_id | 89de351d3b5744b9853ec4829aa0e714 |
|
||||
+-----------------+--------------------------------------+
|
||||
|
||||
.. note::
|
||||
|
||||
A ``delete`` (deletion) lock on a share would prevent deletion and other
|
||||
actions on a share that are similar to deletion. Similar actions include
|
||||
moving a share to the recycle bin for deferred deletion (``soft
|
||||
deletion``) or removing a share from the Shared File Systems service
|
||||
(``unmanage``).
|
||||
|
||||
|
||||
|
||||
* Get details of a resource lock:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack share lock list --resource myshare --resource-type share
|
||||
+--------------------------------------+--------------------------------------+---------------+-----------------+
|
||||
| ID | Resource Id | Resource Type | Resource Action |
|
||||
+--------------------------------------+--------------------------------------+---------------+-----------------+
|
||||
| dc7ec691-a505-47d0-b2ec-8eb7fb9270e4 | 4c0b4d35-4ea8-4811-a1e2-a065c64225a8 | share | delete |
|
||||
+--------------------------------------+--------------------------------------+---------------+-----------------+
|
||||
|
||||
$ openstack share lock show dc7ec691-a505-47d0-b2ec-8eb7fb9270e4
|
||||
+-----------------+--------------------------------------+
|
||||
| Field | Value |
|
||||
+-----------------+--------------------------------------+
|
||||
| ID | dc7ec691-a505-47d0-b2ec-8eb7fb9270e4 |
|
||||
| Resource Id | 4c0b4d35-4ea8-4811-a1e2-a065c64225a8 |
|
||||
| Resource Type | share |
|
||||
| Resource Action | delete |
|
||||
| Lock Context | user |
|
||||
| User Id | 89de351d3b5744b9853ec4829aa0e714 |
|
||||
| Project Id | db2e72fef7864bbbbf210f22da7f1158 |
|
||||
| Created At | 2023-07-18T05:11:56.626667 |
|
||||
| Updated At | None |
|
||||
| Lock Reason | None |
|
||||
+-----------------+--------------------------------------+
|
||||
|
||||
* Resource lock in action:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack share delete myshare
|
||||
Failed to delete share with name or ID 'myshare': Resource lock/s [dc7ec691-a505-47d0-b2ec-8eb7fb9270e4] prevent delete action. (HTTP 403) (Request-ID: req-331a8e31-e02a-40b2-accf-0f6dae1b6178)
|
||||
1 of 1 shares failed to delete.
|
||||
|
||||
* Delete a resource lock:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack share lock delete dc7ec691-a505-47d0-b2ec-8eb7fb9270e4
|
||||
|
@ -198,14 +198,14 @@ REST_API_VERSION_HISTORY = """
|
||||
* 2.79 - Added ``with_count`` in share snapshot list API to get total
|
||||
count info.
|
||||
* 2.80 - Added share backup APIs.
|
||||
|
||||
* 2.81 - Added API methods, endpoint /resource-locks.
|
||||
"""
|
||||
|
||||
# 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.80"
|
||||
_MAX_API_VERSION = "2.81"
|
||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||
|
||||
|
||||
|
@ -434,3 +434,8 @@ ____
|
||||
2.80
|
||||
----
|
||||
Added share backup APIs.
|
||||
|
||||
2.81
|
||||
----
|
||||
Introduce resource locks as a way users can restrict certain actions on
|
||||
resources. Only share deletion can be prevented at this version.
|
||||
|
187
manila/api/v2/resource_locks.py
Normal file
187
manila/api/v2/resource_locks.py
Normal file
@ -0,0 +1,187 @@
|
||||
# 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.
|
||||
|
||||
"""The resource_locks API controller module.
|
||||
|
||||
This module handles the following requests:
|
||||
GET /resource-locks
|
||||
GET /resource-locks/{lock_id}
|
||||
POST /resource-locks
|
||||
PUT /resource-locks/{lock_id}
|
||||
DELETE /resource-locks/{lock_id}
|
||||
"""
|
||||
|
||||
from http import client as http_client
|
||||
|
||||
from oslo_utils import timeutils
|
||||
from oslo_utils import uuidutils
|
||||
import webob
|
||||
from webob import exc
|
||||
|
||||
from manila.api import common
|
||||
from manila.api.openstack import wsgi
|
||||
from manila.api.views import resource_locks as resource_locks_view
|
||||
from manila.common import constants
|
||||
from manila import exception
|
||||
from manila.i18n import _
|
||||
from manila.lock import api as resource_locks
|
||||
from manila import utils
|
||||
|
||||
RESOURCE_LOCKS_MIN_API_VERSION = '2.81'
|
||||
|
||||
|
||||
class ResourceLocksController(wsgi.Controller):
|
||||
"""The Resource Locks API controller for the OpenStack API."""
|
||||
|
||||
_view_builder_class = resource_locks_view.ViewBuilder
|
||||
resource_name = 'resource_lock'
|
||||
|
||||
def _check_body(self, body, for_update=False):
|
||||
if 'resource_lock' not in body:
|
||||
raise exc.HTTPBadRequest(
|
||||
explanation="Malformed request body.")
|
||||
lock_data = body['resource_lock']
|
||||
resource_id = lock_data.get('resource_id') or ''
|
||||
resource_type = lock_data.get('resource_type') or ''
|
||||
resource_action = (lock_data.get('resource_action') or
|
||||
constants.RESOURCE_ACTION_DELETE)
|
||||
lock_reason = lock_data.get('lock_reason') or ''
|
||||
|
||||
if len(lock_reason) > 1023:
|
||||
msg = _("'lock_reason' can contain a maximum of 1023 characters.")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
if resource_action not in constants.RESOURCE_LOCK_RESOURCE_ACTIONS:
|
||||
msg = _("'resource_action' can only be one of %(actions)s" %
|
||||
{'actions': constants.RESOURCE_LOCK_RESOURCE_ACTIONS})
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
if for_update:
|
||||
if set(lock_data.keys()) - {'resource_action', 'lock_reason'}:
|
||||
msg = _("Only 'resource_action' and 'lock_reason' "
|
||||
"can be updated.")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
else:
|
||||
if not uuidutils.is_uuid_like(resource_id):
|
||||
msg = _("Resource ID is required and must be in uuid format.")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
if resource_type not in constants.RESOURCE_LOCK_RESOURCE_TYPES:
|
||||
msg = _("'resource_type' is required and must be one "
|
||||
"of %s" % constants.RESOURCE_LOCK_RESOURCE_TYPES)
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
def __init__(self):
|
||||
self.resource_locks_api = resource_locks.API()
|
||||
super(ResourceLocksController, self).__init__()
|
||||
|
||||
@wsgi.Controller.api_version(RESOURCE_LOCKS_MIN_API_VERSION)
|
||||
@wsgi.Controller.authorize('get_all')
|
||||
def index(self, req):
|
||||
"""Returns a list of locks, transformed through view builder."""
|
||||
context = req.environ['manila.context']
|
||||
filters = req.params.copy()
|
||||
|
||||
params = common.get_pagination_params(req)
|
||||
limit, offset = [params.pop('limit', None), params.pop('offset', None)]
|
||||
sort_key, sort_dir = common.get_sort_params(filters)
|
||||
for key in ('limit', 'offset'):
|
||||
filters.pop(key, None)
|
||||
|
||||
show_count = utils.get_bool_from_api_params(
|
||||
'with_count', {'with_count': filters.pop('with_count', False)})
|
||||
|
||||
for time_comparison_filter in ['created_since', 'created_before']:
|
||||
if time_comparison_filter in filters:
|
||||
time_str = filters.get(time_comparison_filter)
|
||||
try:
|
||||
parsed_time = timeutils.parse_isotime(time_str)
|
||||
filters[time_comparison_filter] = parsed_time
|
||||
except ValueError:
|
||||
msg = _('Invalid value specified for the query '
|
||||
'key: %s') % time_comparison_filter
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
locks, count = self.resource_locks_api.get_all(context,
|
||||
search_opts=filters,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir,
|
||||
show_count=show_count)
|
||||
|
||||
return self._view_builder.index(req,
|
||||
locks,
|
||||
count=count)
|
||||
|
||||
@wsgi.Controller.api_version(RESOURCE_LOCKS_MIN_API_VERSION)
|
||||
@wsgi.Controller.authorize('get')
|
||||
def show(self, req, id):
|
||||
"""Return an existing resource lock by ID."""
|
||||
context = req.environ['manila.context']
|
||||
try:
|
||||
resource_lock = self.resource_locks_api.get(context, id)
|
||||
except exception.ResourceLockNotFound as error:
|
||||
raise exc.HTTPNotFound(explanation=error.msg)
|
||||
return self._view_builder.detail(req, resource_lock)
|
||||
|
||||
@wsgi.Controller.api_version(RESOURCE_LOCKS_MIN_API_VERSION)
|
||||
@wsgi.Controller.authorize
|
||||
@wsgi.action("delete")
|
||||
def delete(self, req, id):
|
||||
"""Delete an existing resource lock."""
|
||||
context = req.environ['manila.context']
|
||||
try:
|
||||
self.resource_locks_api.delete(context, id)
|
||||
except exception.ResourceLockNotFound as error:
|
||||
raise exc.HTTPNotFound(explanation=error.msg)
|
||||
return webob.Response(status_int=http_client.NO_CONTENT)
|
||||
|
||||
@wsgi.Controller.api_version(RESOURCE_LOCKS_MIN_API_VERSION)
|
||||
@wsgi.Controller.authorize
|
||||
def create(self, req, body):
|
||||
"""Create a resource lock."""
|
||||
context = req.environ['manila.context']
|
||||
self._check_body(body)
|
||||
lock_data = body['resource_lock']
|
||||
try:
|
||||
resource_lock = self.resource_locks_api.create(
|
||||
context,
|
||||
resource_id=lock_data['resource_id'],
|
||||
resource_type=lock_data['resource_type'],
|
||||
resource_action=(lock_data.get('resource_action') or
|
||||
constants.RESOURCE_ACTION_DELETE),
|
||||
lock_reason=lock_data.get('lock_reason')
|
||||
)
|
||||
except exception.NotFound:
|
||||
raise exc.HTTPBadRequest(
|
||||
explanation="No such resource found.")
|
||||
except exception.InvalidInput as error:
|
||||
raise exc.HTTPConflict(explanation=error.msg)
|
||||
return self._view_builder.detail(req, resource_lock)
|
||||
|
||||
@wsgi.Controller.api_version(RESOURCE_LOCKS_MIN_API_VERSION)
|
||||
@wsgi.Controller.authorize
|
||||
def update(self, req, id, body):
|
||||
"""Update an existing resource lock."""
|
||||
context = req.environ['manila.context']
|
||||
self._check_body(body, for_update=True)
|
||||
lock_data = body['resource_lock']
|
||||
|
||||
resource_lock = self.resource_locks_api.update(
|
||||
context,
|
||||
id,
|
||||
lock_data,
|
||||
)
|
||||
return self._view_builder.detail(req, resource_lock)
|
||||
|
||||
|
||||
def create_resource():
|
||||
return wsgi.Resource(ResourceLocksController())
|
@ -32,6 +32,7 @@ from manila.api.v2 import availability_zones
|
||||
from manila.api.v2 import messages
|
||||
from manila.api.v2 import quota_class_sets
|
||||
from manila.api.v2 import quota_sets
|
||||
from manila.api.v2 import resource_locks
|
||||
from manila.api.v2 import services
|
||||
from manila.api.v2 import share_access_metadata
|
||||
from manila.api.v2 import share_accesses
|
||||
@ -651,3 +652,7 @@ class APIRouter(manila.api.openstack.APIRouter):
|
||||
controller=self.resources['share-backups'],
|
||||
collection={'detail': 'GET'},
|
||||
member={'action': 'POST'})
|
||||
|
||||
self.resources["resource_locks"] = resource_locks.create_resource()
|
||||
mapper.resource("resource-lock", "resource-locks",
|
||||
controller=self.resources["resource_locks"])
|
||||
|
72
manila/api/views/resource_locks.py
Normal file
72
manila/api/views/resource_locks.py
Normal file
@ -0,0 +1,72 @@
|
||||
# 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.
|
||||
|
||||
|
||||
from manila.api import common
|
||||
|
||||
|
||||
class ViewBuilder(common.ViewBuilder):
|
||||
"""Model a resource lock API response as a python dictionary."""
|
||||
|
||||
_collection_name = "resource_locks"
|
||||
|
||||
def index(self, request, resource_locks, count=None):
|
||||
"""Show a list of resource locks."""
|
||||
return self._list_view(self.detail,
|
||||
request,
|
||||
resource_locks,
|
||||
count=count)
|
||||
|
||||
def detail(self, request, resource_lock):
|
||||
"""Detailed view of a single resource lock."""
|
||||
lock_ref = {
|
||||
'id': resource_lock.get('id'),
|
||||
'user_id': resource_lock.get('user_id'),
|
||||
'project_id': resource_lock.get('project_id'),
|
||||
'lock_context': resource_lock.get('lock_context'),
|
||||
'resource_type': resource_lock.get('resource_type'),
|
||||
'resource_id': resource_lock.get('resource_id'),
|
||||
'resource_action': resource_lock.get('resource_action'),
|
||||
'lock_reason': resource_lock.get('lock_reason'),
|
||||
'created_at': resource_lock.get('created_at'),
|
||||
'updated_at': resource_lock.get('updated_at'),
|
||||
'links': self._get_links(request, resource_lock['id']),
|
||||
}
|
||||
return {'resource_lock': lock_ref}
|
||||
|
||||
def _list_view(self, func, request, resource_locks,
|
||||
coll_name=_collection_name, count=None):
|
||||
"""Provide a view for a list of resource_locks.
|
||||
|
||||
:param func: Function used to format the lock data
|
||||
:param request: API request
|
||||
:param resource_locks: List of locks in dictionary format
|
||||
:param coll_name: Name of collection, used to generate the next link
|
||||
for a pagination query
|
||||
:returns: lock data in dictionary format
|
||||
"""
|
||||
locks_list = [
|
||||
func(request, lock)['resource_lock']
|
||||
for lock in resource_locks
|
||||
]
|
||||
locks_links = self._get_collection_links(request,
|
||||
resource_locks,
|
||||
coll_name)
|
||||
locks_dict = dict({"resource_locks": locks_list})
|
||||
|
||||
if count:
|
||||
locks_dict['count'] = count
|
||||
|
||||
if locks_links:
|
||||
locks_dict['resource_locks_links'] = locks_links
|
||||
|
||||
return locks_dict
|
@ -254,6 +254,16 @@ REPLICATION_TYPE_DR = 'dr'
|
||||
|
||||
POLICY_EXTEND_BEYOND_MAX_SHARE_SIZE = 'extend_beyond_max_share_size_spec'
|
||||
|
||||
RESOURCE_ACTION_DELETE = 'delete' # delete, soft-delete, unmanage
|
||||
|
||||
RESOURCE_LOCK_RESOURCE_TYPES = (
|
||||
SHARE_RESOURCE_TYPE,
|
||||
)
|
||||
|
||||
RESOURCE_LOCK_RESOURCE_ACTIONS = (
|
||||
RESOURCE_ACTION_DELETE,
|
||||
)
|
||||
|
||||
|
||||
class ExtraSpecs(object):
|
||||
|
||||
|
@ -58,6 +58,8 @@ class RequestContext(context.RequestContext):
|
||||
self.is_admin = policy.check_is_admin(self)
|
||||
elif self.is_admin and 'admin' not in self.roles:
|
||||
self.roles.append('admin')
|
||||
# a "service" user's token will contain "service_roles"
|
||||
self.is_service = kwargs.get('service_roles') or False
|
||||
self.read_deleted = read_deleted
|
||||
self.remote_address = remote_address
|
||||
if not timestamp:
|
||||
|
@ -1822,3 +1822,30 @@ def share_backups_get_all(context, filters=None, limit=None, offset=None,
|
||||
def share_backup_delete(context, backup_id):
|
||||
"""Deletes backup with the specified ID."""
|
||||
return IMPL.share_backup_delete(context, backup_id)
|
||||
|
||||
#####################
|
||||
|
||||
|
||||
def resource_lock_create(context, values):
|
||||
"""Create a resource lock."""
|
||||
return IMPL.resource_lock_create(context, values)
|
||||
|
||||
|
||||
def resource_lock_update(context, lock_id, values):
|
||||
"""Update a resource lock."""
|
||||
return IMPL.resource_lock_update(context, lock_id, values)
|
||||
|
||||
|
||||
def resource_lock_delete(context, lock_id):
|
||||
"""Delete a resource lock."""
|
||||
return IMPL.resource_lock_delete(context, lock_id)
|
||||
|
||||
|
||||
def resource_lock_get(context, lock_id):
|
||||
"""Retrieve a resource lock."""
|
||||
return IMPL.resource_lock_get(context, lock_id)
|
||||
|
||||
|
||||
def resource_lock_get_all(context, **kwargs):
|
||||
"""Retrieve all resource locks."""
|
||||
return IMPL.resource_lock_get_all(context, **kwargs)
|
||||
|
@ -0,0 +1,65 @@
|
||||
# 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_resource_locks
|
||||
|
||||
Revision ID: cb20f743ca7b
|
||||
Revises: 9afbe2df4945
|
||||
Create Date: 2023-06-23 16:34:36.277477
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'cb20f743ca7b'
|
||||
down_revision = '9afbe2df4945'
|
||||
|
||||
from alembic import op
|
||||
from oslo_log import log
|
||||
import sqlalchemy as sa
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
def upgrade():
|
||||
context = op.get_context()
|
||||
mysql_dl = context.bind.dialect.name == 'mysql'
|
||||
datetime_type = (sa.dialects.mysql.DATETIME(fsp=6)
|
||||
if mysql_dl else sa.DateTime)
|
||||
try:
|
||||
op.create_table(
|
||||
'resource_locks',
|
||||
sa.Column('id', sa.String(36), primary_key=True, nullable=False),
|
||||
sa.Column('user_id', sa.String(255), nullable=False),
|
||||
sa.Column('project_id', sa.String(255), nullable=False),
|
||||
sa.Column('resource_action', sa.String(255), default='delete'),
|
||||
sa.Column('resource_type', sa.String(255), nullable=False),
|
||||
sa.Column('resource_id', sa.String(36), nullable=False),
|
||||
sa.Column('lock_context', sa.String(16), nullable=False),
|
||||
sa.Column('lock_reason', sa.String(1023), nullable=True),
|
||||
sa.Column('created_at', datetime_type),
|
||||
sa.Column('updated_at', datetime_type),
|
||||
sa.Column('deleted_at', datetime_type),
|
||||
sa.Column('deleted', sa.String(36), default='False'),
|
||||
mysql_engine='InnoDB',
|
||||
mysql_charset='utf8',
|
||||
)
|
||||
except Exception:
|
||||
LOG.error("Table resource_locks not created!")
|
||||
raise
|
||||
|
||||
|
||||
def downgrade():
|
||||
try:
|
||||
op.drop_table('resource_locks')
|
||||
except Exception:
|
||||
LOG.error("resource_locks table not dropped")
|
||||
raise
|
@ -7181,3 +7181,108 @@ def share_backup_update(context, backup_id, values):
|
||||
def share_backup_delete(context, backup_id):
|
||||
backup_ref = share_backup_get(context, backup_id)
|
||||
backup_ref.soft_delete(session=context.session, update_status=True)
|
||||
|
||||
###############################
|
||||
|
||||
|
||||
@require_context
|
||||
def _resource_lock_get(context, lock_id):
|
||||
query = model_query(context,
|
||||
models.ResourceLock,
|
||||
read_deleted="no",
|
||||
project_only="yes")
|
||||
result = query.filter_by(id=lock_id).first()
|
||||
if not result:
|
||||
raise exception.ResourceLockNotFound(lock_id=lock_id)
|
||||
return result
|
||||
|
||||
|
||||
@require_context
|
||||
@context_manager.writer
|
||||
def resource_lock_create(context, kwargs):
|
||||
"""Create a resource lock."""
|
||||
values = copy.deepcopy(kwargs)
|
||||
lock_ref = models.ResourceLock()
|
||||
if not values.get('id'):
|
||||
values['id'] = uuidutils.generate_uuid()
|
||||
lock_ref.update(values)
|
||||
|
||||
context.session.add(lock_ref)
|
||||
|
||||
return _resource_lock_get(context, lock_ref['id'])
|
||||
|
||||
|
||||
@require_context
|
||||
@context_manager.writer
|
||||
def resource_lock_update(context, lock_id, kwargs):
|
||||
"""Update a resource lock."""
|
||||
lock_ref = _resource_lock_get(context, lock_id)
|
||||
lock_ref.update(kwargs)
|
||||
lock_ref.save(session=context.session)
|
||||
return lock_ref
|
||||
|
||||
|
||||
@require_context
|
||||
@context_manager.writer
|
||||
def resource_lock_delete(context, lock_id):
|
||||
"""Delete a resource lock."""
|
||||
lock_ref = _resource_lock_get(context, lock_id)
|
||||
lock_ref.soft_delete(session=context.session)
|
||||
|
||||
|
||||
@require_context
|
||||
@context_manager.reader
|
||||
def resource_lock_get(context, lock_id):
|
||||
"""Retrieve a resource lock."""
|
||||
return _resource_lock_get(context, lock_id)
|
||||
|
||||
|
||||
@require_context
|
||||
@context_manager.reader
|
||||
def resource_lock_get_all(context, filters=None, limit=None, offset=None,
|
||||
sort_key='created_at', sort_dir='desc',
|
||||
show_count=False):
|
||||
"""Retrieve all resource locks.
|
||||
|
||||
If no sort parameters are specified then the returned locks are
|
||||
sorted by the 'created_at' key in descending order.
|
||||
|
||||
:param context: context to query under
|
||||
:param limit: maximum number of items to return
|
||||
:param offset: the number of items to skip from the marker or from the
|
||||
first element.
|
||||
:param sort_key: attributes by which results should be sorted.
|
||||
:param sort_dir: directions in which results should be sorted.
|
||||
:param filters: dictionary of filters; values that are in lists, tuples,
|
||||
or sets cause an 'IN' operation, while exact matching
|
||||
is used for other values, see exact_filter function for
|
||||
more information
|
||||
:returns: list of matching resource locks
|
||||
"""
|
||||
locks = models.ResourceLock
|
||||
|
||||
# add policy check to allow: all_projects, project_id filters
|
||||
filters = filters or {}
|
||||
|
||||
query = model_query(context, locks, read_deleted="no")
|
||||
|
||||
project_id = filters.get('project_id')
|
||||
all_projects = filters.get('all_projects') or filters.get('all_tenants')
|
||||
if project_id is None and not all_projects:
|
||||
filters['project_id'] = context.project_id
|
||||
|
||||
legal_filter_keys = ('id', 'user_id', 'resource_id', 'resource_type',
|
||||
'lock_context', 'resource_action', 'created_since',
|
||||
'created_before', 'lock_reason', 'lock_reason~',
|
||||
'project_id')
|
||||
|
||||
query = exact_filter(query, locks, filters, legal_filter_keys)
|
||||
|
||||
count = query.count() if show_count else None
|
||||
|
||||
query = utils.paginate_query(query, locks, limit,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir,
|
||||
offset=offset)
|
||||
|
||||
return query.all(), count
|
||||
|
@ -1455,6 +1455,31 @@ class Message(BASE, ManilaBase):
|
||||
deleted = Column(String(36), default='False')
|
||||
|
||||
|
||||
class ResourceLock(BASE, ManilaBase):
|
||||
"""Represents a resource lock.
|
||||
|
||||
Resource locks are held by users (or on behalf of users) and prevent
|
||||
actions to be performed on resources while the lock is present.
|
||||
"""
|
||||
__tablename__ = 'resource_locks'
|
||||
id = Column(String(36), primary_key=True, nullable=False)
|
||||
user_id = Column(String(255), nullable=False)
|
||||
project_id = Column(String(255), nullable=False)
|
||||
# If the lock is held on behalf of the user, but created by 'service' or
|
||||
# 'admin' users, as opposed to the user themselves ('project')
|
||||
lock_context = Column(String(10), nullable=False)
|
||||
# The uuid of the resource being locked.
|
||||
resource_id = Column(String(36), nullable=False)
|
||||
# The resource type, a constant dict will hold possible values
|
||||
resource_type = Column(Enum(*constants.RESOURCE_LOCK_RESOURCE_TYPES),
|
||||
default=constants.SHARE_RESOURCE_TYPE)
|
||||
# Action that lock prevents, a constant dict will hold possible values
|
||||
resource_action = Column(Enum(*constants.RESOURCE_LOCK_RESOURCE_ACTIONS),
|
||||
default=constants.RESOURCE_ACTION_DELETE)
|
||||
lock_reason = Column(String(1023), nullable=True)
|
||||
deleted = Column(String(36), default='False')
|
||||
|
||||
|
||||
class BackendInfo(BASE, ManilaBase):
|
||||
"""Represent Backend Info."""
|
||||
__tablename__ = "backend_info"
|
||||
|
@ -209,6 +209,10 @@ class MessageNotFound(NotFound):
|
||||
message = _("Message %(message_id)s could not be found.")
|
||||
|
||||
|
||||
class ResourceLockNotFound(NotFound):
|
||||
message = _("Resource lock %(lock_id)s could not be found.")
|
||||
|
||||
|
||||
class Found(ManilaException):
|
||||
message = _("Resource was found.")
|
||||
code = 302
|
||||
|
0
manila/lock/__init__.py
Normal file
0
manila/lock/__init__.py
Normal file
172
manila/lock/api.py
Normal file
172
manila/lock/api.py
Normal file
@ -0,0 +1,172 @@
|
||||
# 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.
|
||||
"""
|
||||
Handles all requests related to resource locks.
|
||||
"""
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from manila.common import constants
|
||||
from manila.db import base
|
||||
from manila import exception
|
||||
from manila import policy
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class API(base.Base):
|
||||
"""API for handling resource locks."""
|
||||
|
||||
resource_get = {
|
||||
"share": "share_get",
|
||||
}
|
||||
|
||||
def _get_lock_context(self, context):
|
||||
if context.is_service:
|
||||
lock_context = 'service'
|
||||
elif context.is_admin:
|
||||
lock_context = 'admin'
|
||||
else:
|
||||
lock_context = 'user'
|
||||
return {
|
||||
'lock_context': lock_context,
|
||||
'user_id': context.user_id,
|
||||
'project_id': context.project_id,
|
||||
}
|
||||
|
||||
def _check_allow_lock_manipulation(self, context, resource_lock):
|
||||
"""Lock owners may not manipulate a lock if lock_context disallows
|
||||
|
||||
The logic enforced by this method is that user created locks can be
|
||||
manipulated by all roles, service created locks can be manipulated
|
||||
by service and admin roles, while admin created locks can only be
|
||||
manipulated by admin role:
|
||||
|
||||
+------------+------------+--------------+---------+
|
||||
| Requester | Lock Owner | Lock Context | Allowed |
|
||||
+------------+------------+--------------+---------+
|
||||
| user | user | user | yes |
|
||||
| user | user | service | no |
|
||||
| user | admin | admin | no |
|
||||
| admin | user | user | yes |
|
||||
| admin | user | service | yes |
|
||||
| admin | admin | admin | yes |
|
||||
| service | user | user | yes |
|
||||
| service | user | service | yes |
|
||||
| service | admin | admin | no |
|
||||
+------------+------------+--------------+---------+
|
||||
"""
|
||||
locked_by = resource_lock['lock_context']
|
||||
update_requested_by = self._get_lock_context(context)['lock_context']
|
||||
if ((locked_by == 'admin' and update_requested_by != 'admin')
|
||||
or (locked_by == 'service' and update_requested_by == 'user')):
|
||||
raise exception.NotAuthorized("Resource lock cannot be "
|
||||
"manipulated by user. Please "
|
||||
"contact the administrator.")
|
||||
|
||||
def get(self, context, lock_id):
|
||||
"""Return resource lock with the specified id."""
|
||||
return self.db.resource_lock_get(context, lock_id)
|
||||
|
||||
def get_all(self, context, search_opts=None, limit=None,
|
||||
offset=None, sort_key="created_at", sort_dir="desc",
|
||||
show_count=False):
|
||||
"""Return resource locks for the given context."""
|
||||
LOG.debug("Searching for locks by: %s", search_opts)
|
||||
|
||||
search_opts = search_opts or {}
|
||||
if 'all_projects' in search_opts:
|
||||
allow_all_projects = policy.check_policy(
|
||||
context,
|
||||
'resource_lock',
|
||||
'get_all_projects',
|
||||
do_raise=False
|
||||
)
|
||||
LOG.warning("User %s not allowed to query locks across "
|
||||
"all projects.", context.user_id)
|
||||
if not allow_all_projects:
|
||||
search_opts.pop('all_projects')
|
||||
search_opts.pop('project_id', None)
|
||||
|
||||
locks, count = self.db.resource_lock_get_all(
|
||||
context,
|
||||
filters=search_opts,
|
||||
limit=limit, offset=offset,
|
||||
sort_key=sort_key,
|
||||
sort_dir=sort_dir,
|
||||
show_count=show_count,
|
||||
)
|
||||
|
||||
return locks, count
|
||||
|
||||
def create(self, context, resource_id=None, resource_type=None,
|
||||
resource_action=None, lock_reason=None):
|
||||
"""Create a resource lock with the specified information."""
|
||||
get_res_method = getattr(self.db, self.resource_get[resource_type])
|
||||
resource = get_res_method(context, resource_id)
|
||||
policy.check_policy(context, 'resource_lock', 'create', resource)
|
||||
self._check_resource_state_for_locking(resource_action, resource)
|
||||
lock_context_data = self._get_lock_context(context)
|
||||
resource_lock = lock_context_data.copy()
|
||||
resource_lock.update({
|
||||
'resource_id': resource_id,
|
||||
'resource_action': resource_action,
|
||||
'lock_reason': lock_reason,
|
||||
})
|
||||
return self.db.resource_lock_create(context, resource_lock)
|
||||
|
||||
def _check_resource_state_for_locking(self, resource_action, resource):
|
||||
"""Check if resource is in a "disallowed" state for locking.
|
||||
|
||||
For example, deletion lock on a "deleting" resource would be futile.
|
||||
"""
|
||||
resource_state = resource.get('status', resource.get('state', ''))
|
||||
disallowed_statuses = ()
|
||||
if resource_action == 'delete':
|
||||
disallowed_statuses = (
|
||||
constants.STATUS_DELETING,
|
||||
constants.STATUS_ERROR_DELETING,
|
||||
constants.STATUS_UNMANAGING,
|
||||
constants.STATUS_MANAGE_ERROR_UNMANAGING,
|
||||
constants.STATUS_UNMANAGE_ERROR,
|
||||
constants.STATUS_UNMANAGED, # not possible, future proofing
|
||||
constants.STATUS_DELETED, # not possible, future proofing
|
||||
)
|
||||
if resource_state in disallowed_statuses:
|
||||
msg = "Resource status not suitable for locking"
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
resource_is_soft_deleted = resource.get('is_soft_deleted', False)
|
||||
if resource_is_soft_deleted:
|
||||
msg = "Resource cannot be locked since it has been soft deleted."
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
def update(self, context, lock_id, updates):
|
||||
"""Update a resource lock with the specified information."""
|
||||
resource_lock = self.db.resource_lock_get(context, lock_id)
|
||||
policy.check_policy(context, 'resource_lock', 'update', resource_lock)
|
||||
self._check_allow_lock_manipulation(context, resource_lock)
|
||||
if 'resource_action' in updates:
|
||||
get_res_method = getattr(
|
||||
self.db,
|
||||
self.resource_get[resource_lock['resource_type']],
|
||||
)
|
||||
resource = get_res_method(context, resource_lock['resource_id'])
|
||||
self._check_resource_state_for_locking(
|
||||
updates['resource_action'], resource)
|
||||
return self.db.resource_lock_update(context, lock_id, updates)
|
||||
|
||||
def delete(self, context, lock_id):
|
||||
"""Delete resource lock with the specified id."""
|
||||
resource_lock = self.db.resource_lock_get(context, lock_id)
|
||||
policy.check_policy(context, 'resource_lock', 'delete', resource_lock)
|
||||
self._check_allow_lock_manipulation(context, resource_lock)
|
||||
self.db.resource_lock_delete(context, lock_id)
|
@ -21,6 +21,7 @@ from manila.policies import base
|
||||
from manila.policies import message
|
||||
from manila.policies import quota_class_set
|
||||
from manila.policies import quota_set
|
||||
from manila.policies import resource_lock
|
||||
from manila.policies import scheduler_stats
|
||||
from manila.policies import security_service
|
||||
from manila.policies import service
|
||||
@ -66,6 +67,7 @@ def list_rules():
|
||||
service.list_rules(),
|
||||
quota_set.list_rules(),
|
||||
quota_class_set.list_rules(),
|
||||
resource_lock.list_rules(),
|
||||
share_group_types_spec.list_rules(),
|
||||
share_group_type.list_rules(),
|
||||
share_group_snapshot.list_rules(),
|
||||
|
@ -25,6 +25,12 @@ from oslo_policy import policy
|
||||
# snapshots).
|
||||
ADMIN = 'rule:context_is_admin'
|
||||
|
||||
# This check string is reserved for actions performed by a "service" or the
|
||||
# "admin" super user. Service users act on behalf of other users and can
|
||||
# perform privileged service-specific actions.
|
||||
ADMIN_OR_SERVICE = 'rule:admin_or_service_api'
|
||||
|
||||
|
||||
# This check string is the primary use case for typical end-users, who are
|
||||
# working with resources that belong within a project (e.g., managing shares or
|
||||
# share replicas). These users don't require all the authorization that
|
||||
@ -37,13 +43,25 @@ PROJECT_MEMBER = 'rule:project-member'
|
||||
# needs access for auditing or even support.
|
||||
PROJECT_READER = 'rule:project-reader'
|
||||
|
||||
# This check string should used to protect user specific resources such as
|
||||
# resource locks, or access rule restrictions. Users are expendable
|
||||
# resources, so ensure that other resources can also perform actions to
|
||||
# avoid orphan resources when users are decommissioned.
|
||||
OWNER_USER = 'rule:owner-user'
|
||||
|
||||
ADMIN_OR_PROJECT_MEMBER = f'({ADMIN}) or ({PROJECT_MEMBER})'
|
||||
ADMIN_OR_PROJECT_READER = f'({ADMIN}) or ({PROJECT_READER})'
|
||||
ADMIN_OR_SERVICE_OR_PROJECT_READER = (f'({ADMIN_OR_SERVICE}) or '
|
||||
f'({PROJECT_READER})')
|
||||
ADMIN_OR_SERVICE_OR_PROJECT_MEMBER = (f'({ADMIN_OR_SERVICE}) or '
|
||||
f'({PROJECT_MEMBER})')
|
||||
ADMIN_OR_SERVICE_OR_OWNER_USER = f'({OWNER_USER} or {ADMIN_OR_SERVICE})'
|
||||
|
||||
# Old, "unscoped", deprecated check strings to be removed. Do not use these
|
||||
# in default RBAC any longer. These can be removed after "enforce_scope"
|
||||
# defaults to True in oslo.policy
|
||||
RULE_ADMIN_OR_OWNER = 'rule:admin_or_owner'
|
||||
RULE_ADMIN_OR_OWNER_USER = 'rule:admin_or_owner_user'
|
||||
RULE_ADMIN_API = 'rule:admin_api'
|
||||
RULE_DEFAULT = 'rule:default'
|
||||
|
||||
@ -77,6 +95,18 @@ rules = [
|
||||
'project_id:%(project_id)s',
|
||||
description='Project scoped Reader',
|
||||
scope_types=['project']),
|
||||
policy.RuleDefault(
|
||||
name='owner-user',
|
||||
check_str='user_id:%(user_id)s and '
|
||||
'project_id:%(project_id)s',
|
||||
description='Project scoped user that owns a user specific resource',
|
||||
scope_types=['project']),
|
||||
policy.RuleDefault(
|
||||
"admin_or_service_api",
|
||||
"role:admin or role:service",
|
||||
description="A service user or an administrator user.",
|
||||
scope_types=['project'],
|
||||
),
|
||||
|
||||
# ***Special personas for Manila*** #
|
||||
policy.RuleDefault(
|
||||
@ -99,6 +129,11 @@ rules = [
|
||||
name='admin_or_owner',
|
||||
check_str='is_admin:True or project_id:%(project_id)s',
|
||||
description='Administrator or Member of the project'),
|
||||
policy.RuleDefault(
|
||||
name='admin_or_owner_user',
|
||||
check_str='is_admin:True or '
|
||||
'project_id:%(project_id)s and user_id:%(user_id)s',
|
||||
description='Administrator or owner user of a resource'),
|
||||
policy.RuleDefault(
|
||||
name='default',
|
||||
check_str=RULE_ADMIN_OR_OWNER,
|
||||
|
154
manila/policies/resource_lock.py
Normal file
154
manila/policies/resource_lock.py
Normal file
@ -0,0 +1,154 @@
|
||||
# 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.
|
||||
|
||||
from oslo_policy import policy
|
||||
|
||||
from manila.policies import base
|
||||
|
||||
|
||||
BASE_POLICY_NAME = 'resource_lock:%s'
|
||||
|
||||
DEPRECATED_REASON = """
|
||||
The resource lock API now supports scope and default roles.
|
||||
"""
|
||||
|
||||
deprecated_lock_get = policy.DeprecatedRule(
|
||||
name=BASE_POLICY_NAME % 'get',
|
||||
check_str=base.RULE_DEFAULT,
|
||||
deprecated_reason=DEPRECATED_REASON,
|
||||
deprecated_since='2023.2/Bobcat',
|
||||
)
|
||||
deprecated_lock_get_all = policy.DeprecatedRule(
|
||||
name=BASE_POLICY_NAME % 'get_all',
|
||||
check_str=base.RULE_DEFAULT,
|
||||
deprecated_reason=DEPRECATED_REASON,
|
||||
deprecated_since='2023.2/Bobcat',
|
||||
)
|
||||
deprecated_lock_get_all_projects = policy.DeprecatedRule(
|
||||
name=BASE_POLICY_NAME % 'get_all_projects',
|
||||
check_str=base.RULE_ADMIN_API,
|
||||
deprecated_reason=DEPRECATED_REASON,
|
||||
deprecated_since='2023.2/Bobcat',
|
||||
)
|
||||
deprecated_lock_create = policy.DeprecatedRule(
|
||||
name=BASE_POLICY_NAME % 'create',
|
||||
check_str=base.RULE_DEFAULT,
|
||||
deprecated_reason=DEPRECATED_REASON,
|
||||
deprecated_since='2023.2/Bobcat'
|
||||
)
|
||||
deprecated_lock_update = policy.DeprecatedRule(
|
||||
name=BASE_POLICY_NAME % 'update',
|
||||
check_str=base.RULE_ADMIN_OR_OWNER_USER,
|
||||
deprecated_reason=DEPRECATED_REASON,
|
||||
deprecated_since='2023.2/Bobcat',
|
||||
)
|
||||
deprecated_lock_delete = policy.DeprecatedRule(
|
||||
name=BASE_POLICY_NAME % 'delete',
|
||||
check_str=base.RULE_ADMIN_OR_OWNER_USER,
|
||||
deprecated_reason=DEPRECATED_REASON,
|
||||
deprecated_since='2023.2/Bobcat',
|
||||
)
|
||||
|
||||
|
||||
lock_policies = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name=BASE_POLICY_NAME % 'get',
|
||||
check_str=base.ADMIN_OR_SERVICE_OR_PROJECT_READER,
|
||||
scope_types=['project'],
|
||||
description="Get details of a given resource lock.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/resource-locks/{lock_id}'
|
||||
}
|
||||
],
|
||||
deprecated_rule=deprecated_lock_get,
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=BASE_POLICY_NAME % 'get_all',
|
||||
check_str=base.ADMIN_OR_SERVICE_OR_PROJECT_READER,
|
||||
scope_types=['project'],
|
||||
description="Get all resource locks.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/resource-locks'
|
||||
},
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/resource-locks?{query}'
|
||||
}
|
||||
],
|
||||
deprecated_rule=deprecated_lock_get_all,
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=BASE_POLICY_NAME % 'get_all_projects',
|
||||
check_str=base.ADMIN_OR_SERVICE,
|
||||
scope_types=['project'],
|
||||
description="Get resource locks from all project namespaces.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/resource-locks?all_projects=1'
|
||||
},
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/resource-locks?all_projects=1&'
|
||||
'project_id={project_id}'
|
||||
}
|
||||
],
|
||||
deprecated_rule=deprecated_lock_get_all_projects,
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=BASE_POLICY_NAME % 'create',
|
||||
check_str=base.ADMIN_OR_SERVICE_OR_PROJECT_MEMBER,
|
||||
scope_types=['project'],
|
||||
description="Create a resource lock.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'POST',
|
||||
'path': '/resource-locks'
|
||||
}
|
||||
],
|
||||
deprecated_rule=deprecated_lock_create,
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=BASE_POLICY_NAME % 'update',
|
||||
check_str=base.ADMIN_OR_SERVICE_OR_OWNER_USER,
|
||||
scope_types=['project'],
|
||||
description="Update a resource lock.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'PUT',
|
||||
'path': '/resource-locks/{lock_id}'
|
||||
}
|
||||
],
|
||||
deprecated_rule=deprecated_lock_update,
|
||||
),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=BASE_POLICY_NAME % 'delete',
|
||||
check_str=base.ADMIN_OR_SERVICE_OR_OWNER_USER,
|
||||
scope_types=['project'],
|
||||
description="Delete a resource lock.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'DELETE',
|
||||
'path': '/resource-locks/{lock_id}'
|
||||
}
|
||||
],
|
||||
deprecated_rule=deprecated_lock_delete,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return lock_policies
|
@ -19,6 +19,7 @@
|
||||
"""
|
||||
Handles all requests relating to shares.
|
||||
"""
|
||||
import functools
|
||||
import json
|
||||
|
||||
from oslo_config import cfg
|
||||
@ -127,6 +128,55 @@ class API(base.Base):
|
||||
self.access_helper = access.ShareInstanceAccess(self.db, None)
|
||||
coordination.LOCK_COORDINATOR.start()
|
||||
|
||||
def prevent_locked_action_on_share(arg):
|
||||
"""Decorator for preventing a locked method from executing on a share.
|
||||
|
||||
Add this decorator to any API method which takes a RequestContext
|
||||
object as a first parameter and a share object as the second
|
||||
parameter.
|
||||
|
||||
Can be used in any of the following forms
|
||||
@prevent_locked_action_on_share
|
||||
@prevent_locked_action_on_share('my_action_name')
|
||||
|
||||
:param arg: Can either be the function being decorated or a str
|
||||
containing the 'action' that we need to check resource locks for.
|
||||
If no action name is provided, the function name is assumed to be
|
||||
the action name.
|
||||
"""
|
||||
action_name = None
|
||||
|
||||
def check_for_locks(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(self, context, share, *args, **kwargs):
|
||||
action = action_name or f.__name__
|
||||
resource_locks, __ = (
|
||||
self.db.resource_lock_get_all(
|
||||
context.elevated(),
|
||||
filters={'resource_id': share['id'],
|
||||
'resource_action': action,
|
||||
'all_projects': True},
|
||||
)
|
||||
)
|
||||
if resource_locks:
|
||||
msg_payload = {
|
||||
'locks': ', '.join(
|
||||
[lock['id'] for lock in resource_locks]
|
||||
),
|
||||
'action': action,
|
||||
}
|
||||
msg = (f"Resource lock/s [{msg_payload['locks']}] "
|
||||
f"prevent {action} action.")
|
||||
raise exception.InvalidShare(msg)
|
||||
return f(self, context, share, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
if callable(arg):
|
||||
return check_for_locks(arg)
|
||||
else:
|
||||
action_name = arg
|
||||
return check_for_locks
|
||||
|
||||
def _get_all_availability_zones_with_subnets(self, context,
|
||||
share_network_id):
|
||||
compatible_azs_name = []
|
||||
@ -1044,6 +1094,7 @@ class API(base.Base):
|
||||
}
|
||||
return request_spec
|
||||
|
||||
@prevent_locked_action_on_share('delete')
|
||||
def unmanage(self, context, share):
|
||||
policy.check_policy(context, 'share', 'unmanage')
|
||||
|
||||
@ -1239,6 +1290,7 @@ class API(base.Base):
|
||||
context, share, snapshot, active_replica['host'], reservations)
|
||||
|
||||
@policy.wrap_check_policy('share')
|
||||
@prevent_locked_action_on_share('delete')
|
||||
def soft_delete(self, context, share):
|
||||
"""Soft delete share."""
|
||||
share_id = share['id']
|
||||
@ -1291,6 +1343,7 @@ class API(base.Base):
|
||||
self.db.share_restore(context, share_id)
|
||||
|
||||
@policy.wrap_check_policy('share')
|
||||
@prevent_locked_action_on_share
|
||||
def delete(self, context, share, force=False):
|
||||
"""Delete share."""
|
||||
share = self.db.share_get(context, share['id'])
|
||||
|
@ -45,3 +45,23 @@ def stub_message(id, **kwargs):
|
||||
|
||||
def stub_message_get(self, context, message_id):
|
||||
return stub_message(message_id)
|
||||
|
||||
|
||||
def stub_lock(id, **kwargs):
|
||||
lock = {
|
||||
'id': id,
|
||||
'project_id': 'f63f7a159f404cfc8604b7065c609691',
|
||||
'user_id': 'e78f4294e3534e00ae176bd989d6a682',
|
||||
'resource_id': 'c474badd-f06e-4ff9-ae26-daa00e19867b',
|
||||
'resource_action': 'delete',
|
||||
'resource_type': 'share',
|
||||
'lock_context': 'user',
|
||||
'lock_reason': 'for the tests',
|
||||
'updated_at': datetime.datetime(2023, 8, 10, 20, 4, 39,
|
||||
tzinfo=iso8601.UTC),
|
||||
'created_at': datetime.datetime(2023, 1, 10, 15, 3, 1,
|
||||
tzinfo=iso8601.UTC),
|
||||
}
|
||||
|
||||
lock.update(kwargs)
|
||||
return lock
|
||||
|
373
manila/tests/api/v2/test_resource_locks.py
Normal file
373
manila/tests/api/v2/test_resource_locks.py
Normal file
@ -0,0 +1,373 @@
|
||||
# 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.
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import ddt
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import uuidutils
|
||||
import webob
|
||||
|
||||
from manila.api.v2 import resource_locks
|
||||
from manila import context
|
||||
from manila import exception
|
||||
from manila import policy
|
||||
from manila import test
|
||||
from manila.tests.api import fakes
|
||||
from manila.tests.api.v2 import stubs
|
||||
from manila.tests import utils as test_utils
|
||||
from manila import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class ResourceLockApiTest(test.TestCase):
|
||||
def setUp(self):
|
||||
super(ResourceLockApiTest, self).setUp()
|
||||
self.controller = resource_locks.ResourceLocksController()
|
||||
self.maxDiff = None
|
||||
self.ctxt = context.RequestContext('demo', 'fake', False)
|
||||
self.req = fakes.HTTPRequest.blank(
|
||||
'/resource-locks',
|
||||
version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION
|
||||
)
|
||||
self.mock_object(
|
||||
policy, 'check_policy', mock.Mock(return_value=True)
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
test_utils.annotated('no_body_content', {}),
|
||||
test_utils.annotated('invalid_body', {'share': 'somedata'}),
|
||||
test_utils.annotated(
|
||||
'invalid_action', {
|
||||
'resource_lock': {
|
||||
'resource_action': 'invalid_action',
|
||||
},
|
||||
},
|
||||
),
|
||||
test_utils.annotated(
|
||||
'invalid_reason', {
|
||||
'resource_lock': {
|
||||
'lock_reason': 'xyzzyspoon!' * 94,
|
||||
},
|
||||
},
|
||||
),
|
||||
test_utils.annotated(
|
||||
'disallowed_attributes', {
|
||||
'resource_lock': {
|
||||
'lock_reason': 'the reason is you',
|
||||
'resource_action': 'delete',
|
||||
'resource_id': uuidutils.generate_uuid(),
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
def test__check_body_for_update_invalid(self, body):
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller._check_body,
|
||||
body,
|
||||
for_update=True)
|
||||
|
||||
@ddt.data(
|
||||
test_utils.annotated('no_body_content', {}),
|
||||
test_utils.annotated('invalid_body', {'share': 'somedata'}),
|
||||
test_utils.annotated(
|
||||
'invalid_action', {
|
||||
'resource_lock': {
|
||||
'resource_action': 'invalid_action',
|
||||
},
|
||||
},
|
||||
),
|
||||
test_utils.annotated(
|
||||
'invalid_reason', {
|
||||
'resource_lock': {
|
||||
'lock_reason': 'xyzzyspoon!' * 94,
|
||||
},
|
||||
},
|
||||
),
|
||||
test_utils.annotated(
|
||||
'invalid_resource_id', {
|
||||
'resource_lock': {
|
||||
'resource_id': 'invalid-id',
|
||||
'resource_action': 'delete',
|
||||
},
|
||||
},
|
||||
),
|
||||
test_utils.annotated(
|
||||
'invalid_resource_type', {
|
||||
'resource_lock': {
|
||||
'resource_id': uuidutils.generate_uuid(),
|
||||
'resource_type': 'invalid-resource-type',
|
||||
},
|
||||
},
|
||||
),
|
||||
test_utils.annotated(
|
||||
'empty_resource_type', {
|
||||
'resource_lock': {
|
||||
'resource_id': uuidutils.generate_uuid(),
|
||||
'resource_type': '',
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
def test__check_body_for_create_invalid(self, body):
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller._check_body,
|
||||
body)
|
||||
|
||||
@ddt.data(
|
||||
test_utils.annotated(
|
||||
'action_and_lock_reason', {
|
||||
'resource_lock': {
|
||||
'resource_action': 'delete',
|
||||
'lock_reason': 'the reason is you',
|
||||
},
|
||||
},
|
||||
),
|
||||
test_utils.annotated(
|
||||
'lock_reason', {
|
||||
'resource_lock': {
|
||||
'lock_reason': 'tienes razon',
|
||||
},
|
||||
},
|
||||
),
|
||||
test_utils.annotated(
|
||||
'resource_action', {
|
||||
'resource_lock': {
|
||||
'resource_action': 'delete',
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
def test__check_body_for_update(self, body):
|
||||
result = self.controller._check_body(body, for_update=True)
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test__check_body_for_create(self):
|
||||
body = {
|
||||
'resource_lock': {
|
||||
'resource_id': uuidutils.generate_uuid(),
|
||||
'resource_type': 'share',
|
||||
},
|
||||
}
|
||||
|
||||
result = self.controller._check_body(body)
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
@ddt.data({'created_since': None, 'created_before': None},
|
||||
{'created_since': '2222-22-22', 'created_before': 'a_year_ago'},
|
||||
{'created_since': 'epoch'},
|
||||
{'created_before': 'december'})
|
||||
def test_index_invalid_time_filters(self, filters):
|
||||
url = '/resource-locks?'
|
||||
for key, value in filters.items():
|
||||
url += f'{key}={value}&'
|
||||
url.rstrip('&')
|
||||
req = fakes.HTTPRequest.blank(
|
||||
url, version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION)
|
||||
req.environ['manila.context'] = self.ctxt
|
||||
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller.index,
|
||||
req)
|
||||
|
||||
@ddt.data({'limit': 'a', 'offset': 'test'},
|
||||
{'limit': -1},
|
||||
{'with_count': 'oh-noes', 'limit': 0})
|
||||
def test_index_invalid_pagination(self, filters):
|
||||
url = '/resource-locks?'
|
||||
for key, value in filters.items():
|
||||
url += f'{key}={value}&'
|
||||
url.rstrip('&')
|
||||
|
||||
req = fakes.HTTPRequest.blank(
|
||||
url, version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION)
|
||||
req.environ['manila.context'] = self.ctxt
|
||||
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller.index,
|
||||
req)
|
||||
|
||||
def test_index(self):
|
||||
url = ('/resource-locks?sort_dir=asc&sort_key=resource_id&limit=3'
|
||||
'&offset=1&project_id=f63f7a159f404cfc8604b7065c609691'
|
||||
'&with_count=1')
|
||||
req = fakes.HTTPRequest.blank(
|
||||
url, version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION)
|
||||
locks = [
|
||||
stubs.stub_lock('68e2e33d-0f0c-49b7-aee3-f0696ab90360'),
|
||||
stubs.stub_lock('93748a9f-6dfe-4baf-ad4c-b9c82d6063ef'),
|
||||
stubs.stub_lock('44f8dd68-2eeb-41df-b5d1-9e7654212527'),
|
||||
]
|
||||
self.mock_object(self.controller.resource_locks_api,
|
||||
'get_all',
|
||||
mock.Mock(return_value=(locks, 3)))
|
||||
|
||||
actual_locks = self.controller.index(req)
|
||||
|
||||
expected_filters = {
|
||||
'project_id': 'f63f7a159f404cfc8604b7065c609691',
|
||||
}
|
||||
self.controller.resource_locks_api.get_all.assert_called_once_with(
|
||||
utils.IsAMatcher(context.RequestContext),
|
||||
search_opts=mock.ANY,
|
||||
limit=3,
|
||||
offset=1,
|
||||
sort_key='resource_id',
|
||||
sort_dir='asc',
|
||||
show_count=True,
|
||||
)
|
||||
# webob uses a "MultiDict" for request params
|
||||
actual_filters = {}
|
||||
call_args = self.controller.resource_locks_api.get_all.call_args[1]
|
||||
search_opts = call_args['search_opts']
|
||||
for key, value in search_opts.dict_of_lists().items():
|
||||
actual_filters[key] = value[0]
|
||||
|
||||
self.assertEqual(expected_filters, actual_filters)
|
||||
self.assertEqual(3, len(actual_locks['resource_locks']))
|
||||
for lock in actual_locks['resource_locks']:
|
||||
for key in locks[0].keys():
|
||||
self.assertIn(key, lock)
|
||||
self.assertIn('links', lock)
|
||||
self.assertIn('resource_locks_links', actual_locks)
|
||||
self.assertEqual(3, actual_locks['count'])
|
||||
|
||||
def test_show_not_found(self):
|
||||
url = '/resource-locks/fake-lock-id'
|
||||
req = fakes.HTTPRequest.blank(
|
||||
url, version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION)
|
||||
self.mock_object(
|
||||
self.controller.resource_locks_api, 'get',
|
||||
mock.Mock(side_effect=exception.ResourceLockNotFound(lock_id='1')))
|
||||
self.assertRaises(webob.exc.HTTPNotFound,
|
||||
self.controller.show,
|
||||
req,
|
||||
'fake-lock-id')
|
||||
|
||||
def test_show(self):
|
||||
url = '/resource-locks/c6aef27b-f583-48c7-aac1-bd8fb570ce16'
|
||||
req = fakes.HTTPRequest.blank(
|
||||
url, version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION)
|
||||
expected_lock = stubs.stub_lock(
|
||||
'c6aef27b-f583-48c7-aac1-bd8fb570ce16'
|
||||
)
|
||||
self.mock_object(
|
||||
self.controller.resource_locks_api,
|
||||
'get',
|
||||
mock.Mock(return_value=expected_lock)
|
||||
)
|
||||
|
||||
actual_lock = self.controller.show(
|
||||
req, 'c6aef27b-f583-48c7-aac1-bd8fb570ce16')
|
||||
self.assertSubDictMatch(expected_lock, actual_lock['resource_lock'])
|
||||
self.assertIn('links', actual_lock['resource_lock'])
|
||||
|
||||
def test_delete_not_found(self):
|
||||
url = '/resource-locks/fake-lock-id'
|
||||
req = fakes.HTTPRequest.blank(
|
||||
url, version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION)
|
||||
self.mock_object(
|
||||
self.controller.resource_locks_api,
|
||||
'delete',
|
||||
mock.Mock(side_effect=exception.ResourceLockNotFound(lock_id='1')),
|
||||
)
|
||||
self.assertRaises(webob.exc.HTTPNotFound,
|
||||
self.controller.delete,
|
||||
req,
|
||||
'fake-lock-id')
|
||||
|
||||
def test_delete(self):
|
||||
url = '/resource-locks/c6aef27b-f583-48c7-aac1-bd8fb570ce16'
|
||||
req = fakes.HTTPRequest.blank(
|
||||
url, version=resource_locks.RESOURCE_LOCKS_MIN_API_VERSION)
|
||||
self.mock_object(self.controller.resource_locks_api, 'delete')
|
||||
|
||||
result = self.controller.delete(req,
|
||||
'c6aef27b-f583-48c7-aac1-bd8fb570ce16')
|
||||
self.assertEqual(204, result.status_int)
|
||||
|
||||
def test_create_no_such_resource(self):
|
||||
self.mock_object(self.controller, '_check_body')
|
||||
body = {
|
||||
'resource_lock': {
|
||||
'resource_id': '27e14086-16e1-445b-ad32-b2ebb07225a8',
|
||||
'resource_type': 'share',
|
||||
},
|
||||
}
|
||||
self.mock_object(self.controller.resource_locks_api,
|
||||
'create',
|
||||
mock.Mock(side_effect=exception.NotFound))
|
||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||
self.controller.create,
|
||||
self.req,
|
||||
body)
|
||||
|
||||
def test_create(self):
|
||||
self.mock_object(self.controller, '_check_body')
|
||||
expected_lock = stubs.stub_lock(
|
||||
'04512dae-18c2-45b5-bbab-50b775ba6f1d',
|
||||
lock_reason=None,
|
||||
)
|
||||
body = {
|
||||
'resource_lock': {
|
||||
'resource_id': expected_lock['resource_id'],
|
||||
'resource_type': expected_lock['resource_type'],
|
||||
},
|
||||
}
|
||||
self.mock_object(self.controller.resource_locks_api,
|
||||
'create',
|
||||
mock.Mock(return_value=expected_lock))
|
||||
|
||||
actual_lock = self.controller.create(self.req, body)['resource_lock']
|
||||
|
||||
self.controller.resource_locks_api.create.assert_called_once_with(
|
||||
utils.IsAMatcher(context.RequestContext),
|
||||
resource_id=expected_lock['resource_id'],
|
||||
resource_type=expected_lock['resource_type'],
|
||||
resource_action='delete',
|
||||
lock_reason=None,
|
||||
)
|
||||
self.assertSubDictMatch(expected_lock, actual_lock)
|
||||
self.assertIn('links', actual_lock)
|
||||
|
||||
def test_update(self):
|
||||
self.mock_object(self.controller, '_check_body')
|
||||
expected_lock = stubs.stub_lock(
|
||||
'04512dae-18c2-45b5-bbab-50b775ba6f1d',
|
||||
lock_reason=None,
|
||||
)
|
||||
self.mock_object(self.controller.resource_locks_api,
|
||||
'update',
|
||||
mock.Mock(return_value=expected_lock))
|
||||
|
||||
body = {
|
||||
'resource_lock': {
|
||||
'lock_reason': None
|
||||
},
|
||||
}
|
||||
|
||||
actual_lock = self.controller.update(
|
||||
self.req,
|
||||
'04512dae-18c2-45b5-bbab-50b775ba6f1d',
|
||||
body
|
||||
)['resource_lock']
|
||||
|
||||
self.controller.resource_locks_api.update.assert_called_once_with(
|
||||
utils.IsAMatcher(context.RequestContext),
|
||||
'04512dae-18c2-45b5-bbab-50b775ba6f1d',
|
||||
{'lock_reason': None}
|
||||
)
|
||||
self.assertSubDictMatch(expected_lock, actual_lock)
|
@ -3341,3 +3341,34 @@ class AddServiceState(BaseMigrationChecks):
|
||||
s_table = utils.load_table('services', engine)
|
||||
for s in engine.execute(s_table.select()):
|
||||
self.test_case.assertFalse(hasattr(s, 'state'))
|
||||
|
||||
|
||||
@map_to_migration('cb20f743ca7b')
|
||||
class AddResourceLocks(BaseMigrationChecks):
|
||||
|
||||
def setup_upgrade_data(self, engine):
|
||||
pass
|
||||
|
||||
def check_upgrade(self, engine, data):
|
||||
lock_data = {
|
||||
'id': uuidutils.generate_uuid(),
|
||||
'project_id': uuidutils.generate_uuid(dashed=False),
|
||||
'user_id': uuidutils.generate_uuid(dashed=False),
|
||||
'resource_id': uuidutils.generate_uuid(),
|
||||
'created_at': datetime.datetime(2023, 7, 18, 12, 6, 30),
|
||||
'updated_at': None,
|
||||
'deleted_at': None,
|
||||
'deleted': 'False',
|
||||
'resource_type': 'share',
|
||||
'resource_action': 'delete',
|
||||
'lock_reason': 'xyzzy' * 200,
|
||||
'lock_context': 'user',
|
||||
}
|
||||
|
||||
locks_table = utils.load_table('resource_locks', engine)
|
||||
engine.execute(locks_table.insert(lock_data))
|
||||
|
||||
def check_downgrade(self, engine):
|
||||
self.test_case.assertRaises(sa_exc.NoSuchTableError,
|
||||
utils.load_table,
|
||||
'resource_locks', engine)
|
||||
|
@ -5438,3 +5438,177 @@ class ShareBackupDatabaseAPITestCase(BaseDatabaseAPITestCase):
|
||||
self.ctxt,
|
||||
'fake id',
|
||||
{})
|
||||
|
||||
|
||||
class ResourceLocksTestCase(test.TestCase):
|
||||
"""Test case for resource locks."""
|
||||
|
||||
def setUp(self):
|
||||
super(ResourceLocksTestCase, self).setUp()
|
||||
self.user_id = uuidutils.generate_uuid(dashed=False)
|
||||
self.project_id = uuidutils.generate_uuid(dashed=False)
|
||||
self.ctxt = context.RequestContext(user_id=self.user_id,
|
||||
project_id=self.project_id)
|
||||
|
||||
def test_resource_lock_create(self):
|
||||
lock_data = {
|
||||
'resource_id': uuidutils.generate_uuid(),
|
||||
'resource_type': 'share',
|
||||
'resource_action': 'delete',
|
||||
'lock_context': 'user',
|
||||
'user_id': self.user_id,
|
||||
'project_id': self.project_id,
|
||||
'lock_reason': 'xyzzyspoon!',
|
||||
}
|
||||
lock = db_api.resource_lock_create(self.ctxt, lock_data)
|
||||
|
||||
self.assertTrue(uuidutils.is_uuid_like(lock['id']))
|
||||
self.assertEqual(lock_data['user_id'], lock['user_id'])
|
||||
self.assertEqual(lock_data['project_id'], lock['project_id'])
|
||||
self.assertIsNone(lock['updated_at'])
|
||||
self.assertEqual('False', lock['deleted'])
|
||||
|
||||
def test_resource_lock_update_invalid(self):
|
||||
self.assertRaises(exception.ResourceLockNotFound,
|
||||
db_api.resource_lock_update,
|
||||
self.ctxt,
|
||||
'invalid-lock-id',
|
||||
{'lock_reason': 'yadayada'})
|
||||
|
||||
def test_resource_lock_update(self):
|
||||
lock = db_utils.create_lock(project_id=self.project_id)
|
||||
updated_lock = db_api.resource_lock_update(
|
||||
self.ctxt,
|
||||
lock['id'],
|
||||
{'lock_reason': 'new reason'},
|
||||
)
|
||||
|
||||
self.assertEqual(lock['id'], updated_lock['id'])
|
||||
self.assertEqual('new reason', updated_lock['lock_reason'])
|
||||
self.assertEqual(lock['user_id'], updated_lock['user_id'])
|
||||
self.assertEqual(lock['project_id'], updated_lock['project_id'])
|
||||
|
||||
lock_get = db_api.resource_lock_get(self.ctxt, lock['id'])
|
||||
|
||||
self.assertEqual(lock['id'], lock_get['id'])
|
||||
self.assertEqual('new reason', lock_get['lock_reason'])
|
||||
self.assertEqual(lock['user_id'], lock_get['user_id'])
|
||||
self.assertEqual(lock['project_id'], lock_get['project_id'])
|
||||
|
||||
def test_resource_lock_delete_invalid(self):
|
||||
self.assertRaises(exception.ResourceLockNotFound,
|
||||
db_api.resource_lock_delete,
|
||||
self.ctxt,
|
||||
'invalid-lock-id')
|
||||
|
||||
def test_resource_lock_delete(self):
|
||||
lock = db_utils.create_lock(project_id=self.project_id)
|
||||
lock_get = db_api.resource_lock_get(self.ctxt, lock['id'])
|
||||
|
||||
return_value = db_api.resource_lock_delete(self.ctxt, lock['id'])
|
||||
|
||||
self.assertIsNone(return_value)
|
||||
self.assertRaises(exception.ResourceLockNotFound,
|
||||
db_api._resource_lock_get,
|
||||
self.ctxt,
|
||||
lock_get['id'])
|
||||
|
||||
def test_resource_lock_get_invalid(self):
|
||||
self.assertRaises(exception.ResourceLockNotFound,
|
||||
db_api.resource_lock_get,
|
||||
self.ctxt,
|
||||
'invalid-lock-id')
|
||||
|
||||
def test_resource_lock_get(self):
|
||||
lock = db_utils.create_lock(project_id=self.project_id)
|
||||
|
||||
lock_get = db_api.resource_lock_get(self.ctxt, lock['id'])
|
||||
|
||||
self.assertEqual(lock['id'], lock_get['id'])
|
||||
self.assertEqual('for the tests', lock_get['lock_reason'])
|
||||
self.assertEqual(lock['user_id'], lock_get['user_id'])
|
||||
self.assertEqual(lock['project_id'], lock_get['project_id'])
|
||||
|
||||
def test_resource_lock_get_all_basic_filters(self):
|
||||
user_id_2 = uuidutils.generate_uuid(dashed=False)
|
||||
project_id_2 = uuidutils.generate_uuid(dashed=False)
|
||||
|
||||
lk_1 = db_utils.create_lock(lock_reason='austin',
|
||||
user_id=self.user_id,
|
||||
project_id=self.project_id)
|
||||
lk_2 = db_utils.create_lock(lock_reason='bexar',
|
||||
user_id=self.user_id,
|
||||
project_id=self.project_id)
|
||||
lk_3 = db_utils.create_lock(lock_reason='cactus',
|
||||
user_id=self.user_id,
|
||||
project_id=self.project_id)
|
||||
lk_4 = db_utils.create_lock(lock_reason='diablo',
|
||||
user_id=user_id_2,
|
||||
project_id=project_id_2)
|
||||
lk_5 = db_utils.create_lock(lock_reason='essex')
|
||||
|
||||
project_locks_limited_offset, count = db_api.resource_lock_get_all(
|
||||
self.ctxt, limit=2, offset=1, show_count=True)
|
||||
self.assertEqual(2, len(project_locks_limited_offset))
|
||||
self.assertEqual(3, count)
|
||||
order_expected = [lk_2['id'], lk_1['id']]
|
||||
self.assertEqual(order_expected,
|
||||
[lock['id'] for lock in project_locks_limited_offset])
|
||||
|
||||
all_project_locks, count = db_api.resource_lock_get_all(
|
||||
self.ctxt, filters={'all_projects': True}, sort_dir='asc')
|
||||
self.assertEqual(5, len(all_project_locks))
|
||||
order_expected = [
|
||||
lk_1['id'], lk_2['id'], lk_3['id'], lk_4['id'], lk_5['id']
|
||||
]
|
||||
self.assertEqual(order_expected,
|
||||
[lock['id'] for lock in all_project_locks])
|
||||
self.assertTrue(lk_5['project_id']
|
||||
not in [self.project_id, project_id_2])
|
||||
self.assertIsNone(count)
|
||||
|
||||
filtered_locks, count = db_api.resource_lock_get_all(
|
||||
self.ctxt, filters={'lock_reason~': 'xar'})
|
||||
self.assertEqual(1, len(filtered_locks))
|
||||
self.assertIsNone(count)
|
||||
self.assertEqual(lk_2['id'], filtered_locks[0]['id'])
|
||||
|
||||
def test_resource_locks_get_all_time_filters(self):
|
||||
now = timeutils.utcnow()
|
||||
lock_1 = db_utils.create_lock(
|
||||
lock_reason='folsom',
|
||||
project_id=self.project_id,
|
||||
created_at=now - datetime.timedelta(seconds=1),
|
||||
)
|
||||
lock_2 = db_utils.create_lock(
|
||||
lock_reason='grizly',
|
||||
project_id=self.project_id,
|
||||
created_at=now + datetime.timedelta(seconds=1),
|
||||
)
|
||||
lock_3 = db_utils.create_lock(
|
||||
lock_reason='havana',
|
||||
project_id=self.project_id,
|
||||
created_at=now + datetime.timedelta(seconds=2),
|
||||
)
|
||||
|
||||
filters1 = {'created_before': now}
|
||||
filters2 = {'created_since': now}
|
||||
|
||||
result1, count1 = db_api.resource_lock_get_all(
|
||||
self.ctxt, filters=filters1)
|
||||
result2, count2 = db_api.resource_lock_get_all(
|
||||
self.ctxt, filters=filters2)
|
||||
|
||||
self.assertEqual(1, len(result1))
|
||||
self.assertEqual(lock_1['id'], result1[0]['id'])
|
||||
self.assertEqual(2, len(result2))
|
||||
self.assertEqual([lock_3['id'], lock_2['id']],
|
||||
[lock['id'] for lock in result2])
|
||||
self.assertIsNone(count1)
|
||||
self.assertIsNone(count2)
|
||||
|
||||
filters1.update(filters2)
|
||||
result3, count3 = db_api.resource_lock_get_all(
|
||||
self.ctxt, filters=filters1, show_count=True)
|
||||
self.assertEqual(0, len(result3))
|
||||
self.assertEqual(0, count3)
|
||||
|
@ -15,6 +15,8 @@
|
||||
|
||||
import copy
|
||||
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from manila.common import constants
|
||||
from manila import context
|
||||
from manila import db
|
||||
@ -326,3 +328,16 @@ def create_backup(share_id, **kwargs):
|
||||
backup.update(kwargs)
|
||||
return db.share_backup_create(
|
||||
context.get_admin_context(), share_id, backup)
|
||||
|
||||
|
||||
def create_lock(**kwargs):
|
||||
lock = {
|
||||
'resource_id': uuidutils.generate_uuid(),
|
||||
'user_id': uuidutils.generate_uuid(dashed=False),
|
||||
'project_id': uuidutils.generate_uuid(dashed=False),
|
||||
'lock_context': 'user',
|
||||
'lock_reason': 'for the tests',
|
||||
'resource_type': 'share',
|
||||
'resource_action': 'delete',
|
||||
}
|
||||
return _create_db_row(db.resource_lock_create, lock, kwargs)
|
||||
|
0
manila/tests/lock/__init__.py
Normal file
0
manila/tests/lock/__init__.py
Normal file
388
manila/tests/lock/test_api.py
Normal file
388
manila/tests/lock/test_api.py
Normal file
@ -0,0 +1,388 @@
|
||||
# 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.
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import ddt
|
||||
from oslo_config import cfg
|
||||
|
||||
from manila.common import constants
|
||||
from manila import context
|
||||
from manila import exception
|
||||
from manila.lock import api as lock_api
|
||||
from manila import policy
|
||||
from manila import test
|
||||
from manila.tests import utils as test_utils
|
||||
from manila import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class ResourceLockApiTest(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ResourceLockApiTest, self).setUp()
|
||||
self.lock_api = lock_api.API()
|
||||
self.mock_object(self.lock_api, 'db')
|
||||
self.ctxt = context.RequestContext('fakeuser',
|
||||
'fakeproject',
|
||||
is_admin=False)
|
||||
self.mock_object(policy, 'check_policy')
|
||||
|
||||
@ddt.data(
|
||||
test_utils.annotated(
|
||||
'admin_context',
|
||||
(context.RequestContext('fake', 'fake', is_admin=True), 'admin'),
|
||||
),
|
||||
test_utils.annotated(
|
||||
'admin_also_service_context',
|
||||
(context.RequestContext('fake', 'fake', service_roles=['service'],
|
||||
is_admin=True), 'service'),
|
||||
),
|
||||
test_utils.annotated(
|
||||
'service_context',
|
||||
(context.RequestContext('fake', 'fake', service_roles=['service'],
|
||||
is_admin=False), 'service'),
|
||||
),
|
||||
test_utils.annotated(
|
||||
'user_context',
|
||||
(context.RequestContext('fake', 'fake', is_admin=False), 'user')
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test__get_lock_context(self, ctxt, expected_lock_context):
|
||||
result = self.lock_api._get_lock_context(ctxt)
|
||||
|
||||
self.assertEqual(expected_lock_context, result['lock_context'])
|
||||
self.assertEqual(('fake', 'fake'),
|
||||
(result['user_id'], result['project_id']))
|
||||
|
||||
@ddt.data(
|
||||
test_utils.annotated(
|
||||
'user_manipulating_admin_lock',
|
||||
(context.RequestContext('fake', 'fake', is_admin=False), 'admin'),
|
||||
),
|
||||
test_utils.annotated(
|
||||
'user_manipulating_service_lock',
|
||||
(context.RequestContext('fake', 'fake', is_admin=False),
|
||||
'service'),
|
||||
),
|
||||
test_utils.annotated(
|
||||
'service_manipulating_admin_lock',
|
||||
(context.RequestContext('fake', 'fake', is_admin=False,
|
||||
service_roles=['service']), 'admin'),
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test__check_allow_lock_manipulation_not_allowed(self, ctxt, lock_ctxt):
|
||||
self.assertRaises(exception.NotAuthorized,
|
||||
self.lock_api._check_allow_lock_manipulation,
|
||||
ctxt, {'lock_context': lock_ctxt})
|
||||
|
||||
@ddt.data(
|
||||
test_utils.annotated(
|
||||
'user_manipulating_user_lock',
|
||||
(context.RequestContext('fake', 'fake', is_admin=False), 'user'),
|
||||
),
|
||||
test_utils.annotated(
|
||||
'service_manipulating_service_lock',
|
||||
(context.RequestContext(
|
||||
'fake', 'fake', is_admin=False, service_roles=['service']),
|
||||
'service'),
|
||||
),
|
||||
test_utils.annotated(
|
||||
'service_manipulating_user_lock',
|
||||
(context.RequestContext(
|
||||
'fake', 'fake', is_admin=False, service_roles=['service']),
|
||||
'user'),
|
||||
),
|
||||
test_utils.annotated(
|
||||
'admin_manipulating_service_lock',
|
||||
(context.RequestContext('fake', 'fake', is_admin=True), 'service'),
|
||||
),
|
||||
test_utils.annotated(
|
||||
'admin_manipulating_user_lock',
|
||||
(context.RequestContext('fake', 'fake', is_admin=True), 'user'),
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test__check_allow_lock_manipulation_allowed(self, ctxt, lock_ctxt):
|
||||
|
||||
result = self.lock_api._check_allow_lock_manipulation(
|
||||
ctxt,
|
||||
{'lock_context': lock_ctxt}
|
||||
)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_get_all_all_projects_ignored(self):
|
||||
self.mock_object(policy, 'check_policy', mock.Mock(return_value=False))
|
||||
self.mock_object(self.lock_api.db, 'resource_lock_get_all',
|
||||
mock.Mock(return_value=('list of locks', None)))
|
||||
|
||||
locks, count = self.lock_api.get_all(
|
||||
self.ctxt,
|
||||
search_opts={
|
||||
'all_projects': True,
|
||||
'project_id': '5dca5323e33b49fca4a5b261c72e612c',
|
||||
})
|
||||
self.lock_api.db.resource_lock_get_all.assert_called_once_with(
|
||||
utils.IsAMatcher(context.RequestContext),
|
||||
filters={},
|
||||
limit=None,
|
||||
offset=None,
|
||||
sort_key='created_at',
|
||||
sort_dir='desc',
|
||||
show_count=False,
|
||||
)
|
||||
self.assertEqual(('list of locks', None), (locks, count))
|
||||
|
||||
def test_get_all_with_filters(self):
|
||||
self.mock_object(self.lock_api.db, 'resource_lock_get_all',
|
||||
mock.Mock(return_value=('list of locks', 4)))
|
||||
search_opts = {
|
||||
'all_projects': True,
|
||||
'project_id': '5dca5323e33b49fca4a5b261c72e612c',
|
||||
'resource_type': 'snapshot',
|
||||
}
|
||||
locks = self.lock_api.get_all(
|
||||
self.ctxt,
|
||||
limit=3,
|
||||
offset=3,
|
||||
search_opts=search_opts,
|
||||
show_count=True
|
||||
)
|
||||
self.lock_api.db.resource_lock_get_all.assert_called_once_with(
|
||||
utils.IsAMatcher(context.RequestContext),
|
||||
filters=search_opts,
|
||||
limit=3,
|
||||
offset=3,
|
||||
sort_key='created_at',
|
||||
sort_dir='desc',
|
||||
show_count=True,
|
||||
)
|
||||
self.assertEqual('list of locks', locks[0])
|
||||
self.assertEqual(4, locks[1])
|
||||
|
||||
def test_create_lock_resource_not_owned_by_user(self):
|
||||
self.mock_object(
|
||||
policy,
|
||||
'check_policy',
|
||||
mock.Mock(side_effect=exception.PolicyNotAuthorized(
|
||||
action="resource_lock:create")),
|
||||
)
|
||||
|
||||
self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.lock_api.create,
|
||||
self.ctxt,
|
||||
resource_id='19529cea-0471-4972-adaa-fee8694b7538',
|
||||
resource_type='share',
|
||||
resource_action='delete')
|
||||
self.lock_api.db.share_get.assert_called_once_with(
|
||||
utils.IsAMatcher(context.RequestContext),
|
||||
'19529cea-0471-4972-adaa-fee8694b7538',
|
||||
)
|
||||
self.lock_api.db.resource_lock_create.assert_not_called()
|
||||
|
||||
@ddt.data(constants.STATUS_DELETING,
|
||||
constants.STATUS_ERROR_DELETING,
|
||||
constants.STATUS_UNMANAGING,
|
||||
constants.STATUS_MANAGE_ERROR_UNMANAGING,
|
||||
constants.STATUS_UNMANAGE_ERROR,
|
||||
constants.STATUS_UNMANAGED,
|
||||
constants.STATUS_DELETED)
|
||||
def test_create_lock_invalid_resource_status(self, status):
|
||||
self.mock_object(self.lock_api.db, 'resource_lock_create',
|
||||
mock.Mock(return_value='created_obj'))
|
||||
self.mock_object(self.lock_api.db, 'share_get',
|
||||
mock.Mock(return_value={'status': status}))
|
||||
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.lock_api.create,
|
||||
self.ctxt,
|
||||
resource_id='7dab6090-1dfd-4829-bbaf-602fcd1c8248',
|
||||
resource_action='delete',
|
||||
resource_type='share')
|
||||
|
||||
self.lock_api.db.resource_lock_create.assert_not_called()
|
||||
|
||||
def test_create_lock_invalid_resource_soft_deleted(self):
|
||||
self.mock_object(self.lock_api.db, 'resource_lock_create',
|
||||
mock.Mock(return_value='created_obj'))
|
||||
self.mock_object(self.lock_api.db, 'share_get',
|
||||
mock.Mock(return_value={'is_soft_deleted': True}))
|
||||
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.lock_api.create,
|
||||
self.ctxt,
|
||||
resource_id='0bbf0b62-cb29-4218-920b-3f62faa99ff8',
|
||||
resource_action='delete',
|
||||
resource_type='share')
|
||||
|
||||
self.lock_api.db.resource_lock_create.assert_not_called()
|
||||
|
||||
def test_create_lock(self):
|
||||
self.mock_object(self.lock_api.db, 'resource_lock_create',
|
||||
mock.Mock(return_value='created_obj'))
|
||||
mock_share = {
|
||||
'id': 'cacac01c-853d-47f3-afcb-da4484bd09a5',
|
||||
'status': constants.STATUS_AVAILABLE,
|
||||
'is_soft_deleted': False,
|
||||
}
|
||||
self.mock_object(self.lock_api.db, 'share_get',
|
||||
mock.Mock(return_value=mock_share))
|
||||
|
||||
result = self.lock_api.create(
|
||||
self.ctxt,
|
||||
resource_id='cacac01c-853d-47f3-afcb-da4484bd09a5',
|
||||
resource_action='delete',
|
||||
resource_type='share',
|
||||
)
|
||||
|
||||
self.assertEqual('created_obj', result)
|
||||
db_create_arg = self.lock_api.db.resource_lock_create.call_args[0][1]
|
||||
expected_create_arg = {
|
||||
'resource_id': 'cacac01c-853d-47f3-afcb-da4484bd09a5',
|
||||
'resource_action': 'delete',
|
||||
'user_id': 'fakeuser',
|
||||
'project_id': 'fakeproject',
|
||||
'lock_context': 'user',
|
||||
'lock_reason': None,
|
||||
|
||||
}
|
||||
self.assertEqual(expected_create_arg, db_create_arg)
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_update_lock_resource_not_allowed_with_policy_failure(
|
||||
self, policy_fails):
|
||||
self.mock_object(self.lock_api.db, 'resource_lock_get', mock.Mock(
|
||||
return_value={'id': 'd767d3cd-1187-404a-a91f-8b172e0e768e'}))
|
||||
if policy_fails:
|
||||
self.mock_object(
|
||||
policy,
|
||||
'check_policy',
|
||||
mock.Mock(
|
||||
side_effect=exception.PolicyNotAuthorized(
|
||||
action='resource_lock:update'),
|
||||
),
|
||||
)
|
||||
self.mock_object(
|
||||
self.lock_api,
|
||||
'_check_allow_lock_manipulation',
|
||||
mock.Mock(
|
||||
side_effect=exception.NotAuthorized
|
||||
),
|
||||
)
|
||||
|
||||
self.assertRaises(exception.NotAuthorized,
|
||||
self.lock_api.update,
|
||||
self.ctxt,
|
||||
'd767d3cd-1187-404a-a91f-8b172e0e768e',
|
||||
{'foo': 'bar'})
|
||||
|
||||
@ddt.data(constants.STATUS_DELETING,
|
||||
constants.STATUS_ERROR_DELETING,
|
||||
constants.STATUS_UNMANAGING,
|
||||
constants.STATUS_MANAGE_ERROR_UNMANAGING,
|
||||
constants.STATUS_UNMANAGE_ERROR,
|
||||
constants.STATUS_UNMANAGED,
|
||||
constants.STATUS_DELETED)
|
||||
def test_update_invalid_resource_status(self, status):
|
||||
lock = {
|
||||
'id': 'd767d3cd-1187-404a-a91f-8b172e0e768e',
|
||||
'resource_id': '266cf54f-f9cf-4d6c-94f3-7b67f00e0465',
|
||||
'resource_action': 'something',
|
||||
'resource_type': 'share',
|
||||
}
|
||||
self.mock_object(self.lock_api.db, 'resource_lock_get',
|
||||
mock.Mock(return_value=lock))
|
||||
self.mock_object(self.lock_api, '_check_allow_lock_manipulation')
|
||||
self.mock_object(self.lock_api.db,
|
||||
'share_get',
|
||||
mock.Mock(return_value={'status': status}))
|
||||
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.lock_api.update,
|
||||
self.ctxt,
|
||||
'd767d3cd-1187-404a-a91f-8b172e0e768e',
|
||||
{'resource_action': 'delete'})
|
||||
|
||||
self.lock_api.db.resource_lock_update.assert_not_called()
|
||||
|
||||
def test_update(self):
|
||||
self.mock_object(self.lock_api.db, 'resource_lock_get', mock.Mock(
|
||||
return_value={'id': 'd767d3cd-1187-404a-a91f-8b172e0e768e'}))
|
||||
self.mock_object(self.lock_api, '_check_allow_lock_manipulation')
|
||||
self.mock_object(self.lock_api.db, 'resource_lock_update',
|
||||
mock.Mock(return_value='updated_obj'))
|
||||
|
||||
result = self.lock_api.update(
|
||||
self.ctxt,
|
||||
'd767d3cd-1187-404a-a91f-8b172e0e768e',
|
||||
{'foo': 'bar'},
|
||||
)
|
||||
|
||||
self.assertEqual('updated_obj', result)
|
||||
self.lock_api.db.resource_lock_update.assert_called_once_with(
|
||||
utils.IsAMatcher(context.RequestContext),
|
||||
'd767d3cd-1187-404a-a91f-8b172e0e768e',
|
||||
{'foo': 'bar'},
|
||||
)
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_delete_not_allowed_with_policy_failure(self, policy_fails):
|
||||
self.mock_object(self.lock_api.db, 'resource_lock_get', mock.Mock(
|
||||
return_value={'id': 'd767d3cd-1187-404a-a91f-8b172e0e768e'}))
|
||||
if policy_fails:
|
||||
self.mock_object(
|
||||
policy,
|
||||
'check_policy',
|
||||
mock.Mock(
|
||||
side_effect=exception.PolicyNotAuthorized(
|
||||
action='resource_lock:delete'),
|
||||
),
|
||||
)
|
||||
self.mock_object(
|
||||
self.lock_api,
|
||||
'_check_allow_lock_manipulation',
|
||||
mock.Mock(
|
||||
side_effect=exception.NotAuthorized
|
||||
),
|
||||
)
|
||||
|
||||
self.assertRaises(exception.NotAuthorized,
|
||||
self.lock_api.delete,
|
||||
self.ctxt,
|
||||
'd767d3cd-1187-404a-a91f-8b172e0e768e')
|
||||
|
||||
policy.check_policy.assert_called_once_with(
|
||||
utils.IsAMatcher(context.RequestContext),
|
||||
'resource_lock',
|
||||
'delete',
|
||||
{'id': 'd767d3cd-1187-404a-a91f-8b172e0e768e'},
|
||||
)
|
||||
self.assertEqual(not policy_fails,
|
||||
self.lock_api._check_allow_lock_manipulation.called)
|
||||
self.lock_api.db.resource_lock_delete.assert_not_called()
|
||||
|
||||
def test_delete(self):
|
||||
self.mock_object(self.lock_api.db, 'resource_lock_get', mock.Mock(
|
||||
return_value={'id': 'd767d3cd-1187-404a-a91f-8b172e0e768e'}))
|
||||
self.mock_object(self.lock_api, '_check_allow_lock_manipulation')
|
||||
|
||||
result = self.lock_api.delete(self.ctxt,
|
||||
'd767d3cd-1187-404a-a91f-8b172e0e768e')
|
||||
self.assertIsNone(result)
|
||||
self.lock_api.db.resource_lock_delete.assert_called_once_with(
|
||||
utils.IsAMatcher(context.RequestContext),
|
||||
'd767d3cd-1187-404a-a91f-8b172e0e768e'
|
||||
)
|
@ -113,6 +113,8 @@ class ShareAPITestCase(test.TestCase):
|
||||
self.scheduler_rpcapi = mock.Mock()
|
||||
self.share_rpcapi = mock.Mock()
|
||||
self.api = share.API()
|
||||
self.mock_object(self.api.db, 'resource_lock_get_all',
|
||||
mock.Mock(return_value=([], None)))
|
||||
self.mock_object(self.api, 'scheduler_rpcapi', self.scheduler_rpcapi)
|
||||
self.mock_object(self.api, 'share_rpcapi', self.share_rpcapi)
|
||||
self.mock_object(quota.QUOTAS, 'reserve',
|
||||
@ -1516,6 +1518,31 @@ class ShareAPITestCase(test.TestCase):
|
||||
self.assertRaises(exception.ShareBusyException, self.api.unmanage,
|
||||
self.context, share)
|
||||
|
||||
def test_unmanage_locked_share(self):
|
||||
self.mock_object(
|
||||
self.api.db,
|
||||
'resource_lock_get_all',
|
||||
mock.Mock(return_value=([{'id': 'l1'}, {'id': 'l2'}], None))
|
||||
)
|
||||
share = db_utils.create_share(
|
||||
id='fakeid',
|
||||
host='fake',
|
||||
size='1',
|
||||
status=constants.STATUS_AVAILABLE,
|
||||
user_id=self.context.user_id,
|
||||
project_id=self.context.project_id,
|
||||
task_state=None)
|
||||
self.mock_object(db_api, 'share_update', mock.Mock())
|
||||
|
||||
self.assertRaises(exception.InvalidShare,
|
||||
self.api.unmanage,
|
||||
self.context,
|
||||
share)
|
||||
|
||||
# lock check decorator executed first, nothing else is invoked
|
||||
self.share_rpcapi.unmanage_share.assert_not_called()
|
||||
db_api.share_update.assert_not_called()
|
||||
|
||||
@mock.patch.object(quota.QUOTAS, 'reserve',
|
||||
mock.Mock(return_value='reservation'))
|
||||
@mock.patch.object(quota.QUOTAS, 'commit', mock.Mock())
|
||||
@ -2601,6 +2628,23 @@ class ShareAPITestCase(test.TestCase):
|
||||
|
||||
self.api.delete(self.context, share)
|
||||
|
||||
def test_delete_locked_share(self):
|
||||
self.mock_object(
|
||||
self.api.db,
|
||||
'resource_lock_get_all',
|
||||
mock.Mock(return_value=([{'id': 'l1'}, {'id': 'l2'}], None))
|
||||
)
|
||||
share = self._setup_delete_mocks('available')
|
||||
|
||||
self.assertRaises(exception.InvalidShare,
|
||||
self.api.delete,
|
||||
self.context,
|
||||
share)
|
||||
|
||||
# lock check decorator executed first, nothing else is invoked
|
||||
self.api.delete_instance.assert_not_called()
|
||||
db_api.share_snapshot_get_all_for_share.assert_not_called()
|
||||
|
||||
@ddt.data({'status': constants.STATUS_AVAILABLE, 'force': False},
|
||||
{'status': constants.STATUS_ERROR, 'force': True})
|
||||
@ddt.unpack
|
||||
@ -6630,6 +6674,24 @@ class ShareAPITestCase(test.TestCase):
|
||||
self.assertRaises(exception.InvalidShare,
|
||||
self.api.soft_delete, self.context, share)
|
||||
|
||||
def test_soft_delete_locked_share(self):
|
||||
self.mock_object(
|
||||
self.api.db,
|
||||
'resource_lock_get_all',
|
||||
mock.Mock(return_value=([{'id': 'l1'}, {'id': 'l2'}], None))
|
||||
)
|
||||
share = self._setup_delete_mocks('available')
|
||||
self.mock_object(db_api, 'share_soft_delete')
|
||||
|
||||
self.assertRaises(exception.InvalidShare,
|
||||
self.api.soft_delete,
|
||||
self.context,
|
||||
share)
|
||||
|
||||
# lock check decorator executed first, nothing else is invoked
|
||||
db_api.share_soft_delete.assert_not_called()
|
||||
db_api.share_snapshot_get_all_for_share.assert_not_called()
|
||||
|
||||
def test_soft_delete_share(self):
|
||||
share = fakes.fake_share(id='fake_id',
|
||||
status=constants.STATUS_AVAILABLE,
|
||||
|
@ -35,6 +35,13 @@ class ContextTestCase(test.TestCase):
|
||||
roles=['admin', 'weasel'])
|
||||
self.assertTrue(ctxt.is_admin)
|
||||
|
||||
def test_request_context_sets_is_service(self):
|
||||
ctxt = context.RequestContext('111',
|
||||
'222',
|
||||
roles=['service', 'admin'],
|
||||
service_roles=['service'])
|
||||
self.assertTrue(ctxt.is_service)
|
||||
|
||||
def test_request_context_sets_is_admin_upcase(self):
|
||||
ctxt = context.RequestContext('111',
|
||||
'222',
|
||||
|
@ -564,6 +564,13 @@ class ManilaExceptionResponseCode404(test.TestCase):
|
||||
self.assertEqual(404, e.code)
|
||||
self.assertIn(share_id, e.msg)
|
||||
|
||||
def test_resource_lock_not_found(self):
|
||||
# verify response code for exception.ResourceLockNotFound
|
||||
lock_id = "fake_lock_id"
|
||||
e = exception.ResourceLockNotFound(lock_id=lock_id)
|
||||
self.assertEqual(404, e.code)
|
||||
self.assertIn(lock_id, e.msg)
|
||||
|
||||
|
||||
class ManilaExceptionResponseCode413(test.TestCase):
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Added new API endpoints and methods to create, retrieve, update and
|
||||
delete resource locks. Resource locks can be used to restrict certain
|
||||
actions from occurring on the resource. Currently users can prevent
|
||||
deletion of a share (including soft-deletion, transfer and unmanage
|
||||
operations) by creating a resource lock against the share. In future
|
||||
releases, more resource actions may be supported by this feature.
|
Loading…
Reference in New Issue
Block a user