Merge "Resource Locks: Support for share deletion lock"

This commit is contained in:
Zuul 2023-08-23 23:27:37 +00:00 committed by Gerrit Code Review
commit aafc221027
39 changed files with 2752 additions and 4 deletions

View File

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

View File

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

View 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

View 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."
}
}

View 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"
}
]
}
}

View 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"
}
]
}
]
}

View 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"
}
]
}
}

View File

@ -0,0 +1,5 @@
{
"resource_lock": {
"lock_reason": "This is a protected share"
}
}

View 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"
}
]
}
}

View File

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

View File

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

View File

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

View 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())

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

172
manila/lock/api.py Normal file
View 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)

View File

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

View File

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

View 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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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