Update micversion to API2.69, Manila share support Recycle Bin
Add support share Recycle Bin, the end user can soft delete share to Recycle Bin, and can restore the share within 7 days, otherwise the share will be deleted automatically. DocImpact APIImpact Partially-Implements: blueprint manila-share-support-recycle-bin Change-Id: Ic838eec5fea890be6513514053329b1d2d86b3ba
This commit is contained in:
parent
7c04fcb904
commit
d51eb05c05
@ -326,6 +326,15 @@ is_public_query:
|
|||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
|
is_soft_deleted_query:
|
||||||
|
description: |
|
||||||
|
A boolean query parameter that, when set to True, will return all shares
|
||||||
|
in recycle bin. Default is False, will return all shares not in recycle
|
||||||
|
bin.
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
min_version: 2.69
|
||||||
limit:
|
limit:
|
||||||
description: |
|
description: |
|
||||||
The maximum number of shares to return.
|
The maximum number of shares to return.
|
||||||
@ -1390,6 +1399,13 @@ is_public_shares_response:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: boolean
|
type: boolean
|
||||||
|
is_soft_deleted_response:
|
||||||
|
description: |
|
||||||
|
Whether the share has been soft deleted to recycle bin or not.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
min_version: 2.69
|
||||||
links:
|
links:
|
||||||
description: |
|
description: |
|
||||||
Pagination and bookmark links for the resource.
|
Pagination and bookmark links for the resource.
|
||||||
@ -2321,6 +2337,14 @@ revert_to_snapshot_support_share_capability:
|
|||||||
required: true
|
required: true
|
||||||
type: boolean
|
type: boolean
|
||||||
min_version: 2.27
|
min_version: 2.27
|
||||||
|
scheduled_to_be_deleted_at_response:
|
||||||
|
description: |
|
||||||
|
Estimated time at which the share in the recycle bin will be deleted
|
||||||
|
automatically.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
min_version: 2.69
|
||||||
scheduler_hints:
|
scheduler_hints:
|
||||||
description: |
|
description: |
|
||||||
One or more scheduler_hints key and value pairs as a dictionary of
|
One or more scheduler_hints key and value pairs as a dictionary of
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"restore": null
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"soft_delete": null
|
||||||
|
}
|
@ -500,3 +500,97 @@ Request example
|
|||||||
|
|
||||||
.. literalinclude:: samples/share-actions-revert-to-snapshot-request.json
|
.. literalinclude:: samples/share-actions-revert-to-snapshot-request.json
|
||||||
:language: javascript
|
:language: javascript
|
||||||
|
|
||||||
|
|
||||||
|
Soft delete share (since API v2.69)
|
||||||
|
===================================
|
||||||
|
|
||||||
|
.. rest_method:: POST /v2/shares/{share_id}/action
|
||||||
|
|
||||||
|
.. versionadded:: 2.69
|
||||||
|
|
||||||
|
Soft delete a share to recycle bin.
|
||||||
|
|
||||||
|
Preconditions
|
||||||
|
|
||||||
|
- Share status must be ``available``, ``error`` or ``inactive``
|
||||||
|
|
||||||
|
- Share can't have any snapshot.
|
||||||
|
|
||||||
|
- Share can't have a share group snapshot.
|
||||||
|
|
||||||
|
- Share can't have dependent replicas.
|
||||||
|
|
||||||
|
- You cannot soft delete share that already is in the Recycle Bin..
|
||||||
|
|
||||||
|
- You cannot soft delete a share that doesn't belong to your project.
|
||||||
|
|
||||||
|
- You cannot soft delete a share is busy with an active task.
|
||||||
|
|
||||||
|
Response codes
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. rest_status_code:: success status.yaml
|
||||||
|
|
||||||
|
- 202
|
||||||
|
|
||||||
|
.. rest_status_code:: error status.yaml
|
||||||
|
|
||||||
|
- 400
|
||||||
|
- 401
|
||||||
|
- 403
|
||||||
|
- 404
|
||||||
|
- 409
|
||||||
|
|
||||||
|
Request
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- project_id: project_id_path
|
||||||
|
- share_id: share_id
|
||||||
|
|
||||||
|
|
||||||
|
Request example
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. literalinclude:: samples/share-actions-soft-delete-request.json
|
||||||
|
:language: javascript
|
||||||
|
|
||||||
|
|
||||||
|
Restore share (since API v2.69)
|
||||||
|
===============================
|
||||||
|
|
||||||
|
.. rest_method:: POST /v2/shares/{share_id}/action
|
||||||
|
|
||||||
|
.. versionadded:: 2.69
|
||||||
|
|
||||||
|
Restore a share from recycle bin.
|
||||||
|
|
||||||
|
Response codes
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. rest_status_code:: success status.yaml
|
||||||
|
|
||||||
|
- 202
|
||||||
|
|
||||||
|
.. rest_status_code:: error status.yaml
|
||||||
|
|
||||||
|
- 401
|
||||||
|
- 403
|
||||||
|
- 404
|
||||||
|
|
||||||
|
Request
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- project_id: project_id_path
|
||||||
|
- share_id: share_id
|
||||||
|
|
||||||
|
|
||||||
|
Request example
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. literalinclude:: samples/share-actions-restore-request.json
|
||||||
|
:language: javascript
|
||||||
|
@ -130,6 +130,7 @@ Request
|
|||||||
- name~: name_inexact_query
|
- name~: name_inexact_query
|
||||||
- description~: description_inexact_query
|
- description~: description_inexact_query
|
||||||
- with_count: with_count_query
|
- with_count: with_count_query
|
||||||
|
- is_soft_deleted: is_soft_deleted_query
|
||||||
- limit: limit
|
- limit: limit
|
||||||
- offset: offset
|
- offset: offset
|
||||||
- sort_key: sort_key
|
- sort_key: sort_key
|
||||||
@ -198,6 +199,7 @@ Request
|
|||||||
- name~: name_inexact_query
|
- name~: name_inexact_query
|
||||||
- description~: description_inexact_query
|
- description~: description_inexact_query
|
||||||
- with_count: with_count_query
|
- with_count: with_count_query
|
||||||
|
- is_soft_deleted: is_soft_deleted_query
|
||||||
- limit: limit
|
- limit: limit
|
||||||
- offset: offset
|
- offset: offset
|
||||||
- sort_key: sort_key
|
- sort_key: sort_key
|
||||||
@ -242,6 +244,8 @@ Response parameters
|
|||||||
- volume_type: volume_type_shares_response
|
- volume_type: volume_type_shares_response
|
||||||
- export_location: export_location
|
- export_location: export_location
|
||||||
- export_locations: export_locations
|
- export_locations: export_locations
|
||||||
|
- is_soft_deleted: is_soft_deleted_response
|
||||||
|
- scheduled_to_be_deleted_at: scheduled_to_be_deleted_at_response
|
||||||
|
|
||||||
|
|
||||||
Response example
|
Response example
|
||||||
|
@ -170,19 +170,25 @@ REST_API_VERSION_HISTORY = """
|
|||||||
actions on the share network's endpoint:
|
actions on the share network's endpoint:
|
||||||
'update_security_service', 'update_security_service_check' and
|
'update_security_service', 'update_security_service_check' and
|
||||||
'add_security_service_check'.
|
'add_security_service_check'.
|
||||||
|
* 2.64 - Added 'force' field to extend share api, which can extend share
|
||||||
|
directly without validation through share scheduler.
|
||||||
* 2.65 - Added ability to set affinity scheduler hints via the share
|
* 2.65 - Added ability to set affinity scheduler hints via the share
|
||||||
create API.
|
create API.
|
||||||
* 2.66 - Added filter search by group spec for share group type list.
|
* 2.66 - Added filter search by group spec for share group type list.
|
||||||
* 2.67 - Added ability to set 'only_host' scheduler hint for the share
|
* 2.67 - Added ability to set 'only_host' scheduler hint for the share
|
||||||
create and share replica create API.
|
create and share replica create API.
|
||||||
* 2.68 - Added admin only capabilities to share metadata API
|
* 2.68 - Added admin only capabilities to share metadata API
|
||||||
|
* 2.69 - Added new share action to soft delete share to recycle bin or
|
||||||
|
restore share from recycle bin. Also, a new parameter called
|
||||||
|
`is_soft_deleted` was added so users can filter out
|
||||||
|
shares in the recycle bin while listing shares.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The minimum and maximum versions of the API supported
|
# The minimum and maximum versions of the API supported
|
||||||
# The default api version request is defined to be the
|
# The default api version request is defined to be the
|
||||||
# minimum version of the API supported.
|
# minimum version of the API supported.
|
||||||
_MIN_API_VERSION = "2.0"
|
_MIN_API_VERSION = "2.0"
|
||||||
_MAX_API_VERSION = "2.68"
|
_MAX_API_VERSION = "2.69"
|
||||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@ -376,4 +376,11 @@ ____
|
|||||||
|
|
||||||
2.68
|
2.68
|
||||||
----
|
----
|
||||||
Added admin only capabilities to share metadata API
|
Added admin only capabilities to share metadata API.
|
||||||
|
|
||||||
|
2.69
|
||||||
|
----
|
||||||
|
Manila support Recycle Bin. Soft delete share to Recycle Bin: ``POST
|
||||||
|
/v2/shares/{share_id}/action {"soft_delete": null}``. List shares in
|
||||||
|
Recycle Bin: `` GET /v2/shares?is_soft_deleted=true``. Restore share from
|
||||||
|
Recycle Bin: `` POST /v2/shares/{share_id}/action {'restore': null}``.
|
||||||
|
@ -190,6 +190,14 @@ class ShareSnapshotMixin(object):
|
|||||||
LOG.error(msg)
|
LOG.error(msg)
|
||||||
raise exc.HTTPUnprocessableEntity(explanation=msg)
|
raise exc.HTTPUnprocessableEntity(explanation=msg)
|
||||||
|
|
||||||
|
# we do not allow soft delete share with snapshot, and also
|
||||||
|
# do not allow create snapshot for shares in recycle bin,
|
||||||
|
# since it will lead to auto delete share failed.
|
||||||
|
if share['is_soft_deleted']:
|
||||||
|
msg = _("Snapshots cannot be created for share '%s' "
|
||||||
|
"since it has been soft deleted.") % share_id
|
||||||
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
|
|
||||||
LOG.info("Create snapshot from share %s",
|
LOG.info("Create snapshot from share %s",
|
||||||
share_id, context=context)
|
share_id, context=context)
|
||||||
|
|
||||||
|
@ -38,6 +38,10 @@ class ShareUnmanageMixin(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
share = self.share_api.get(context, id)
|
share = self.share_api.get(context, id)
|
||||||
|
if share.get('is_soft_deleted'):
|
||||||
|
msg = _("Share '%s cannot be unmanaged, "
|
||||||
|
"since it has been soft deleted.") % share['id']
|
||||||
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
if share.get('has_replicas'):
|
if share.get('has_replicas'):
|
||||||
msg = _("Share %s has replicas. It cannot be unmanaged "
|
msg = _("Share %s has replicas. It cannot be unmanaged "
|
||||||
"until all replicas are removed.") % share['id']
|
"until all replicas are removed.") % share['id']
|
||||||
|
@ -135,6 +135,11 @@ class ShareMixin(object):
|
|||||||
'with_count', search_opts)
|
'with_count', search_opts)
|
||||||
search_opts.pop('with_count')
|
search_opts.pop('with_count')
|
||||||
|
|
||||||
|
if 'is_soft_deleted' in search_opts:
|
||||||
|
is_soft_deleted = utils.get_bool_from_api_params(
|
||||||
|
'is_soft_deleted', search_opts)
|
||||||
|
search_opts['is_soft_deleted'] = is_soft_deleted
|
||||||
|
|
||||||
# Deserialize dicts
|
# Deserialize dicts
|
||||||
if 'metadata' in search_opts:
|
if 'metadata' in search_opts:
|
||||||
search_opts['metadata'] = ast.literal_eval(search_opts['metadata'])
|
search_opts['metadata'] = ast.literal_eval(search_opts['metadata'])
|
||||||
@ -192,7 +197,7 @@ class ShareMixin(object):
|
|||||||
'is_public', 'metadata', 'extra_specs', 'sort_key', 'sort_dir',
|
'is_public', 'metadata', 'extra_specs', 'sort_key', 'sort_dir',
|
||||||
'share_group_id', 'share_group_snapshot_id', 'export_location_id',
|
'share_group_id', 'share_group_snapshot_id', 'export_location_id',
|
||||||
'export_location_path', 'display_name~', 'display_description~',
|
'export_location_path', 'display_name~', 'display_description~',
|
||||||
'display_description', 'limit', 'offset')
|
'display_description', 'limit', 'offset', 'is_soft_deleted')
|
||||||
|
|
||||||
@wsgi.Controller.authorize
|
@wsgi.Controller.authorize
|
||||||
def update(self, req, id, body):
|
def update(self, req, id, body):
|
||||||
@ -218,6 +223,11 @@ class ShareMixin(object):
|
|||||||
except exception.NotFound:
|
except exception.NotFound:
|
||||||
raise exc.HTTPNotFound()
|
raise exc.HTTPNotFound()
|
||||||
|
|
||||||
|
if share.get('is_soft_deleted'):
|
||||||
|
msg = _("Share '%s cannot be updated, "
|
||||||
|
"since it has been soft deleted.") % share['id']
|
||||||
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
|
|
||||||
update_dict = common.validate_public_share_policy(
|
update_dict = common.validate_public_share_policy(
|
||||||
context, update_dict, api='update')
|
context, update_dict, api='update')
|
||||||
|
|
||||||
@ -443,6 +453,10 @@ class ShareMixin(object):
|
|||||||
access_data.pop('metadata', None)
|
access_data.pop('metadata', None)
|
||||||
share = self.share_api.get(context, id)
|
share = self.share_api.get(context, id)
|
||||||
|
|
||||||
|
if share.get('is_soft_deleted'):
|
||||||
|
msg = _("Cannot allow access for share '%s' "
|
||||||
|
"since it has been soft deleted.") % id
|
||||||
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
share_network_id = share.get('share_network_id')
|
share_network_id = share.get('share_network_id')
|
||||||
if share_network_id:
|
if share_network_id:
|
||||||
share_network = db.share_network_get(context, share_network_id)
|
share_network = db.share_network_get(context, share_network_id)
|
||||||
@ -490,6 +504,12 @@ class ShareMixin(object):
|
|||||||
'deny_access', body.get('os-deny_access'))['access_id']
|
'deny_access', body.get('os-deny_access'))['access_id']
|
||||||
|
|
||||||
share = self.share_api.get(context, id)
|
share = self.share_api.get(context, id)
|
||||||
|
|
||||||
|
if share.get('is_soft_deleted'):
|
||||||
|
msg = _("Cannot deny access for share '%s' "
|
||||||
|
"since it has been soft deleted.") % id
|
||||||
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
|
|
||||||
share_network_id = share.get('share_network_id', None)
|
share_network_id = share.get('share_network_id', None)
|
||||||
|
|
||||||
if share_network_id:
|
if share_network_id:
|
||||||
@ -521,6 +541,11 @@ class ShareMixin(object):
|
|||||||
share, size, force = self._get_valid_extend_parameters(
|
share, size, force = self._get_valid_extend_parameters(
|
||||||
context, id, body, 'os-extend')
|
context, id, body, 'os-extend')
|
||||||
|
|
||||||
|
if share.get('is_soft_deleted'):
|
||||||
|
msg = _("Cannot extend share '%s' "
|
||||||
|
"since it has been soft deleted.") % id
|
||||||
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.share_api.extend(context, share, size, force=force)
|
self.share_api.extend(context, share, size, force=force)
|
||||||
except (exception.InvalidInput, exception.InvalidShare) as e:
|
except (exception.InvalidInput, exception.InvalidShare) as e:
|
||||||
@ -536,6 +561,11 @@ class ShareMixin(object):
|
|||||||
share, size = self._get_valid_shrink_parameters(
|
share, size = self._get_valid_shrink_parameters(
|
||||||
context, id, body, 'os-shrink')
|
context, id, body, 'os-shrink')
|
||||||
|
|
||||||
|
if share.get('is_soft_deleted'):
|
||||||
|
msg = _("Cannot shrink share '%s' "
|
||||||
|
"since it has been soft deleted.") % id
|
||||||
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.share_api.shrink(context, share, size)
|
self.share_api.shrink(context, share, size)
|
||||||
except (exception.InvalidInput, exception.InvalidShare) as e:
|
except (exception.InvalidInput, exception.InvalidShare) as e:
|
||||||
|
@ -21,6 +21,7 @@ from manila.api.views import share_instance as instance_view
|
|||||||
from manila import db
|
from manila import db
|
||||||
from manila import exception
|
from manila import exception
|
||||||
from manila import share
|
from manila import share
|
||||||
|
from manila import utils
|
||||||
|
|
||||||
|
|
||||||
class ShareInstancesController(wsgi.Controller, wsgi.AdminActionsMixin):
|
class ShareInstancesController(wsgi.Controller, wsgi.AdminActionsMixin):
|
||||||
@ -72,7 +73,7 @@ class ShareInstancesController(wsgi.Controller, wsgi.AdminActionsMixin):
|
|||||||
instances = db.share_instances_get_all(context)
|
instances = db.share_instances_get_all(context)
|
||||||
return self._view_builder.detail_list(req, instances)
|
return self._view_builder.detail_list(req, instances)
|
||||||
|
|
||||||
@wsgi.Controller.api_version("2.35") # noqa
|
@wsgi.Controller.api_version("2.35", "2.68") # noqa
|
||||||
@wsgi.Controller.authorize
|
@wsgi.Controller.authorize
|
||||||
def index(self, req): # pylint: disable=function-redefined # noqa F811
|
def index(self, req): # pylint: disable=function-redefined # noqa F811
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
@ -84,6 +85,23 @@ class ShareInstancesController(wsgi.Controller, wsgi.AdminActionsMixin):
|
|||||||
instances = db.share_instances_get_all(context, filters)
|
instances = db.share_instances_get_all(context, filters)
|
||||||
return self._view_builder.detail_list(req, instances)
|
return self._view_builder.detail_list(req, instances)
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version("2.69") # noqa
|
||||||
|
@wsgi.Controller.authorize
|
||||||
|
def index(self, req): # pylint: disable=function-redefined # noqa F811
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
filters = {}
|
||||||
|
filters.update(req.GET)
|
||||||
|
common.remove_invalid_options(
|
||||||
|
context, filters, ('export_location_id', 'export_location_path',
|
||||||
|
'is_soft_deleted'))
|
||||||
|
if 'is_soft_deleted' in filters:
|
||||||
|
is_soft_deleted = utils.get_bool_from_api_params(
|
||||||
|
'is_soft_deleted', filters)
|
||||||
|
filters['is_soft_deleted'] = is_soft_deleted
|
||||||
|
|
||||||
|
instances = db.share_instances_get_all(context, filters)
|
||||||
|
return self._view_builder.detail_list(req, instances)
|
||||||
|
|
||||||
@wsgi.Controller.api_version("2.3")
|
@wsgi.Controller.api_version("2.3")
|
||||||
@wsgi.Controller.authorize
|
@wsgi.Controller.authorize
|
||||||
def show(self, req, id):
|
def show(self, req, id):
|
||||||
|
@ -169,6 +169,11 @@ class ShareReplicationController(wsgi.Controller, wsgi.AdminActionsMixin):
|
|||||||
msg = _("No share exists with ID %s.")
|
msg = _("No share exists with ID %s.")
|
||||||
raise exc.HTTPNotFound(explanation=msg % share_id)
|
raise exc.HTTPNotFound(explanation=msg % share_id)
|
||||||
|
|
||||||
|
if share_ref.get('is_soft_deleted'):
|
||||||
|
msg = _("Replica cannot be created for share '%s' "
|
||||||
|
"since it has been soft deleted.") % share_id
|
||||||
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
|
|
||||||
share_network_id = share_ref.get('share_network_id', None)
|
share_network_id = share_ref.get('share_network_id', None)
|
||||||
|
|
||||||
if share_network_id:
|
if share_network_id:
|
||||||
|
@ -116,18 +116,29 @@ class ShareSnapshotsController(share_snapshots.ShareSnapshotMixin,
|
|||||||
description = snapshot_data.get(
|
description = snapshot_data.get(
|
||||||
'display_description', snapshot_data.get('description'))
|
'display_description', snapshot_data.get('description'))
|
||||||
|
|
||||||
|
share_id = snapshot_data['share_id']
|
||||||
snapshot = {
|
snapshot = {
|
||||||
'share_id': snapshot_data['share_id'],
|
'share_id': share_id,
|
||||||
'provider_location': snapshot_data['provider_location'],
|
'provider_location': snapshot_data['provider_location'],
|
||||||
'display_name': name,
|
'display_name': name,
|
||||||
'display_description': description,
|
'display_description': description,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
share_ref = self.share_api.get(context, share_id)
|
||||||
|
except exception.NotFound:
|
||||||
|
raise exception.ShareNotFound(share_id=share_id)
|
||||||
|
if share_ref.get('is_soft_deleted'):
|
||||||
|
msg = _("Can not manage snapshot for share '%s' "
|
||||||
|
"since it has been soft deleted.") % share_id
|
||||||
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
|
|
||||||
driver_options = snapshot_data.get('driver_options', {})
|
driver_options = snapshot_data.get('driver_options', {})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
snapshot_ref = self.share_api.manage_snapshot(context, snapshot,
|
snapshot_ref = self.share_api.manage_snapshot(context, snapshot,
|
||||||
driver_options)
|
driver_options,
|
||||||
|
share=share_ref)
|
||||||
except (exception.ShareNotFound, exception.ShareSnapshotNotFound) as e:
|
except (exception.ShareNotFound, exception.ShareSnapshotNotFound) as e:
|
||||||
raise exc.HTTPNotFound(explanation=e.msg)
|
raise exc.HTTPNotFound(explanation=e.msg)
|
||||||
except (exception.InvalidShare,
|
except (exception.InvalidShare,
|
||||||
|
@ -66,6 +66,11 @@ class ShareController(shares.ShareMixin,
|
|||||||
share = self.share_api.get(context, share_id)
|
share = self.share_api.get(context, share_id)
|
||||||
snapshot = self.share_api.get_snapshot(context, snapshot_id)
|
snapshot = self.share_api.get_snapshot(context, snapshot_id)
|
||||||
|
|
||||||
|
if share.get('is_soft_deleted'):
|
||||||
|
msg = _("Share '%s cannot revert to snapshot, "
|
||||||
|
"since it has been soft deleted.") % share_id
|
||||||
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
|
|
||||||
# Ensure share supports reverting to a snapshot
|
# Ensure share supports reverting to a snapshot
|
||||||
if not share['revert_to_snapshot_support']:
|
if not share['revert_to_snapshot_support']:
|
||||||
msg_args = {'share_id': share_id, 'snap_id': snapshot_id}
|
msg_args = {'share_id': share_id, 'snap_id': snapshot_id}
|
||||||
@ -219,11 +224,29 @@ class ShareController(shares.ShareMixin,
|
|||||||
@wsgi.Controller.api_version('2.0', '2.6')
|
@wsgi.Controller.api_version('2.0', '2.6')
|
||||||
@wsgi.action('os-reset_status')
|
@wsgi.action('os-reset_status')
|
||||||
def share_reset_status_legacy(self, req, id, body):
|
def share_reset_status_legacy(self, req, id, body):
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
try:
|
||||||
|
share = self.share_api.get(context, id)
|
||||||
|
except exception.NotFound:
|
||||||
|
raise exception.ShareNotFound(share_id=id)
|
||||||
|
if share.get('is_soft_deleted'):
|
||||||
|
msg = _("status cannot be reset for share '%s' "
|
||||||
|
"since it has been soft deleted.") % id
|
||||||
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
return self._reset_status(req, id, body)
|
return self._reset_status(req, id, body)
|
||||||
|
|
||||||
@wsgi.Controller.api_version('2.7')
|
@wsgi.Controller.api_version('2.7')
|
||||||
@wsgi.action('reset_status')
|
@wsgi.action('reset_status')
|
||||||
def share_reset_status(self, req, id, body):
|
def share_reset_status(self, req, id, body):
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
try:
|
||||||
|
share = self.share_api.get(context, id)
|
||||||
|
except exception.NotFound:
|
||||||
|
raise exception.ShareNotFound(share_id=id)
|
||||||
|
if share.get('is_soft_deleted'):
|
||||||
|
msg = _("status cannot be reset for share '%s' "
|
||||||
|
"since it has been soft deleted.") % id
|
||||||
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
return self._reset_status(req, id, body)
|
return self._reset_status(req, id, body)
|
||||||
|
|
||||||
@wsgi.Controller.api_version('2.0', '2.6')
|
@wsgi.Controller.api_version('2.0', '2.6')
|
||||||
@ -236,6 +259,60 @@ class ShareController(shares.ShareMixin,
|
|||||||
def share_force_delete(self, req, id, body):
|
def share_force_delete(self, req, id, body):
|
||||||
return self._force_delete(req, id, body)
|
return self._force_delete(req, id, body)
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version('2.69')
|
||||||
|
@wsgi.action('soft_delete')
|
||||||
|
def share_soft_delete(self, req, id, body):
|
||||||
|
"""Soft delete a share."""
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
|
||||||
|
LOG.debug("Soft delete share with id: %s", id, context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
share = self.share_api.get(context, id)
|
||||||
|
self.share_api.soft_delete(context, share)
|
||||||
|
except exception.NotFound:
|
||||||
|
raise exc.HTTPNotFound()
|
||||||
|
except exception.InvalidShare as e:
|
||||||
|
raise exc.HTTPForbidden(explanation=e.msg)
|
||||||
|
except exception.ShareBusyException as e:
|
||||||
|
raise exc.HTTPForbidden(explanation=e.msg)
|
||||||
|
except exception.Conflict as e:
|
||||||
|
raise exc.HTTPConflict(explanation=e.msg)
|
||||||
|
|
||||||
|
return webob.Response(status_int=http_client.ACCEPTED)
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version('2.69')
|
||||||
|
@wsgi.action('restore')
|
||||||
|
def share_restore(self, req, id, body):
|
||||||
|
"""Restore a share from recycle bin."""
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
|
||||||
|
LOG.debug("Restore share with id: %s", id, context=context)
|
||||||
|
|
||||||
|
try:
|
||||||
|
share = self.share_api.get(context, id)
|
||||||
|
except exception.NotFound:
|
||||||
|
msg = _("No share exists with ID %s.")
|
||||||
|
raise exc.HTTPNotFound(explanation=msg % id)
|
||||||
|
|
||||||
|
# If the share not exist in Recycle Bin, the API will return
|
||||||
|
# success directly.
|
||||||
|
is_soft_deleted = share.get('is_soft_deleted')
|
||||||
|
if not is_soft_deleted:
|
||||||
|
return webob.Response(status_int=http_client.OK)
|
||||||
|
|
||||||
|
# If the share has reached the expired time, and is been deleting,
|
||||||
|
# it too late to restore the share.
|
||||||
|
if share['status'] in [constants.STATUS_DELETING,
|
||||||
|
constants.STATUS_ERROR_DELETING]:
|
||||||
|
msg = _("Share %s is being deleted or error deleted, "
|
||||||
|
"cannot be restore.")
|
||||||
|
raise exc.HTTPForbidden(explanation=msg % id)
|
||||||
|
|
||||||
|
self.share_api.restore(context, share)
|
||||||
|
|
||||||
|
return webob.Response(status_int=http_client.ACCEPTED)
|
||||||
|
|
||||||
@wsgi.Controller.api_version('2.29', experimental=True)
|
@wsgi.Controller.api_version('2.29', experimental=True)
|
||||||
@wsgi.action("migration_start")
|
@wsgi.action("migration_start")
|
||||||
@wsgi.Controller.authorize
|
@wsgi.Controller.authorize
|
||||||
@ -247,6 +324,12 @@ class ShareController(shares.ShareMixin,
|
|||||||
except exception.NotFound:
|
except exception.NotFound:
|
||||||
msg = _("Share %s not found.") % id
|
msg = _("Share %s not found.") % id
|
||||||
raise exc.HTTPNotFound(explanation=msg)
|
raise exc.HTTPNotFound(explanation=msg)
|
||||||
|
|
||||||
|
if share.get('is_soft_deleted'):
|
||||||
|
msg = _("Migration cannot start for share '%s' "
|
||||||
|
"since it has been soft deleted.") % id
|
||||||
|
raise exception.InvalidShare(reason=msg)
|
||||||
|
|
||||||
params = body.get('migration_start')
|
params = body.get('migration_start')
|
||||||
|
|
||||||
if not params:
|
if not params:
|
||||||
@ -355,6 +438,15 @@ class ShareController(shares.ShareMixin,
|
|||||||
@wsgi.action("reset_task_state")
|
@wsgi.action("reset_task_state")
|
||||||
@wsgi.Controller.authorize
|
@wsgi.Controller.authorize
|
||||||
def reset_task_state(self, req, id, body):
|
def reset_task_state(self, req, id, body):
|
||||||
|
context = req.environ['manila.context']
|
||||||
|
try:
|
||||||
|
share = self.share_api.get(context, id)
|
||||||
|
except exception.NotFound:
|
||||||
|
raise exception.ShareNotFound(share_id=id)
|
||||||
|
if share.get('is_soft_deleted'):
|
||||||
|
msg = _("task state cannot be reset for share '%s' "
|
||||||
|
"since it has been soft deleted.") % id
|
||||||
|
raise exc.HTTPForbidden(explanation=msg)
|
||||||
return self._reset_status(req, id, body, status_attr='task_state')
|
return self._reset_status(req, id, body, status_attr='task_state')
|
||||||
|
|
||||||
@wsgi.Controller.api_version('2.0', '2.6')
|
@wsgi.Controller.api_version('2.0', '2.6')
|
||||||
@ -482,6 +574,9 @@ class ShareController(shares.ShareMixin,
|
|||||||
if req.api_version_request < api_version.APIVersionRequest("2.42"):
|
if req.api_version_request < api_version.APIVersionRequest("2.42"):
|
||||||
req.GET.pop('with_count', None)
|
req.GET.pop('with_count', None)
|
||||||
|
|
||||||
|
if req.api_version_request < api_version.APIVersionRequest("2.69"):
|
||||||
|
req.GET.pop('is_soft_deleted', None)
|
||||||
|
|
||||||
return self._get_shares(req, is_detail=False)
|
return self._get_shares(req, is_detail=False)
|
||||||
|
|
||||||
@wsgi.Controller.api_version("2.0")
|
@wsgi.Controller.api_version("2.0")
|
||||||
@ -496,6 +591,9 @@ class ShareController(shares.ShareMixin,
|
|||||||
req.GET.pop('description~', None)
|
req.GET.pop('description~', None)
|
||||||
req.GET.pop('description', None)
|
req.GET.pop('description', None)
|
||||||
|
|
||||||
|
if req.api_version_request < api_version.APIVersionRequest("2.69"):
|
||||||
|
req.GET.pop('is_soft_deleted', None)
|
||||||
|
|
||||||
return self._get_shares(req, is_detail=True)
|
return self._get_shares(req, is_detail=True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ class ViewBuilder(common.ViewBuilder):
|
|||||||
"add_mount_snapshot_support_field",
|
"add_mount_snapshot_support_field",
|
||||||
"add_progress_field",
|
"add_progress_field",
|
||||||
"translate_creating_from_snapshot_status",
|
"translate_creating_from_snapshot_status",
|
||||||
|
"add_share_recycle_bin_field",
|
||||||
]
|
]
|
||||||
|
|
||||||
def summary_list(self, request, shares, count=None):
|
def summary_list(self, request, shares, count=None):
|
||||||
@ -197,3 +198,9 @@ class ViewBuilder(common.ViewBuilder):
|
|||||||
@common.ViewBuilder.versioned_method("2.54")
|
@common.ViewBuilder.versioned_method("2.54")
|
||||||
def add_progress_field(self, context, share_dict, share):
|
def add_progress_field(self, context, share_dict, share):
|
||||||
share_dict['progress'] = share.get('progress')
|
share_dict['progress'] = share.get('progress')
|
||||||
|
|
||||||
|
@common.ViewBuilder.versioned_method("2.69")
|
||||||
|
def add_share_recycle_bin_field(self, context, share_dict, share):
|
||||||
|
share_dict['is_soft_deleted'] = share.get('is_soft_deleted')
|
||||||
|
share_dict['scheduled_to_be_deleted_at'] = share.get(
|
||||||
|
'scheduled_to_be_deleted_at')
|
||||||
|
@ -127,6 +127,11 @@ global_opts = [
|
|||||||
help="Specify list of protocols to be allowed for share "
|
help="Specify list of protocols to be allowed for share "
|
||||||
"creation. Available values are '%s'" %
|
"creation. Available values are '%s'" %
|
||||||
list(constants.SUPPORTED_SHARE_PROTOCOLS)),
|
list(constants.SUPPORTED_SHARE_PROTOCOLS)),
|
||||||
|
cfg.IntOpt('soft_deleted_share_retention_time',
|
||||||
|
default=604800,
|
||||||
|
help='Maximum time (in seconds) to keep a share in the recycle '
|
||||||
|
'bin, it will be deleted automatically after this amount '
|
||||||
|
'of time has elapsed.'),
|
||||||
]
|
]
|
||||||
|
|
||||||
CONF.register_opts(global_opts)
|
CONF.register_opts(global_opts)
|
||||||
|
@ -451,6 +451,15 @@ def share_get_all_by_share_server(context, share_server_id, filters=None,
|
|||||||
sort_dir=sort_dir)
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def get_shares_in_recycle_bin_by_share_server(
|
||||||
|
context, share_server_id, filters=None,
|
||||||
|
sort_key=None, sort_dir=None):
|
||||||
|
"""Returns all shares in recycle bin with given share server ID."""
|
||||||
|
return IMPL.get_shares_in_recycle_bin_by_share_server(
|
||||||
|
context, share_server_id, filters=filters, sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
|
||||||
def share_get_all_by_share_server_with_count(
|
def share_get_all_by_share_server_with_count(
|
||||||
context, share_server_id, filters=None, sort_key=None, sort_dir=None):
|
context, share_server_id, filters=None, sort_key=None, sort_dir=None):
|
||||||
"""Returns all shares with given share server ID."""
|
"""Returns all shares with given share server ID."""
|
||||||
@ -459,11 +468,29 @@ def share_get_all_by_share_server_with_count(
|
|||||||
sort_dir=sort_dir)
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def get_shares_in_recycle_bin_by_network(
|
||||||
|
context, share_network_id, filters=None,
|
||||||
|
sort_key=None, sort_dir=None):
|
||||||
|
"""Returns all shares in recycle bin with given share network ID."""
|
||||||
|
return IMPL.get_shares_in_recycle_bin_by_network(
|
||||||
|
context, share_network_id, filters=filters, sort_key=sort_key,
|
||||||
|
sort_dir=sort_dir)
|
||||||
|
|
||||||
|
|
||||||
def share_delete(context, share_id):
|
def share_delete(context, share_id):
|
||||||
"""Delete share."""
|
"""Delete share."""
|
||||||
return IMPL.share_delete(context, share_id)
|
return IMPL.share_delete(context, share_id)
|
||||||
|
|
||||||
|
|
||||||
|
def share_soft_delete(context, share_id):
|
||||||
|
"""Soft delete share."""
|
||||||
|
return IMPL.share_soft_delete(context, share_id)
|
||||||
|
|
||||||
|
|
||||||
|
def share_restore(context, share_id):
|
||||||
|
"""Restore share."""
|
||||||
|
return IMPL.share_restore(context, share_id)
|
||||||
|
|
||||||
###################
|
###################
|
||||||
|
|
||||||
|
|
||||||
@ -1077,6 +1104,11 @@ def share_server_get_all_unused_deletable(context, host, updated_before):
|
|||||||
updated_before)
|
updated_before)
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_expired_shares(context):
|
||||||
|
"""Get all expired share DB records."""
|
||||||
|
return IMPL.get_all_expired_shares(context)
|
||||||
|
|
||||||
|
|
||||||
def share_server_backend_details_set(context, share_server_id, server_details):
|
def share_server_backend_details_set(context, share_server_id, server_details):
|
||||||
"""Create DB record with backend details."""
|
"""Create DB record with backend details."""
|
||||||
return IMPL.share_server_backend_details_set(context, share_server_id,
|
return IMPL.share_server_backend_details_set(context, share_server_id,
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
# 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 is_soft_deleted and scheduled_to_be_deleted_at to shares table
|
||||||
|
|
||||||
|
Revision ID: 1946cb97bb8d
|
||||||
|
Revises: fbdfabcba377
|
||||||
|
Create Date: 2021-07-14 14:41:58.615439
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1946cb97bb8d'
|
||||||
|
down_revision = 'fbdfabcba377'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
from oslo_log import log
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
try:
|
||||||
|
op.add_column('shares', sa.Column(
|
||||||
|
'is_soft_deleted', sa.Boolean,
|
||||||
|
nullable=False, server_default=sa.sql.false()))
|
||||||
|
op.add_column('shares', sa.Column(
|
||||||
|
'scheduled_to_be_deleted_at', sa.DateTime))
|
||||||
|
except Exception:
|
||||||
|
LOG.error("Columns shares.is_soft_deleted "
|
||||||
|
"and/or shares.scheduled_to_be_deleted_at not created!")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
try:
|
||||||
|
op.drop_column('shares', 'is_soft_deleted')
|
||||||
|
op.drop_column('shares', 'scheduled_to_be_deleted_at')
|
||||||
|
LOG.warning("All shares in recycle bin will automatically be "
|
||||||
|
"restored, need to be manually identified and deleted "
|
||||||
|
"again.")
|
||||||
|
except Exception:
|
||||||
|
LOG.error("Column shares.is_soft_deleted and/or "
|
||||||
|
"shares.scheduled_to_be_deleted_at not dropped!")
|
||||||
|
raise
|
@ -47,6 +47,7 @@ from sqlalchemy import MetaData
|
|||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
from sqlalchemy.orm import subqueryload
|
from sqlalchemy.orm import subqueryload
|
||||||
|
from sqlalchemy.sql.expression import false
|
||||||
from sqlalchemy.sql.expression import literal
|
from sqlalchemy.sql.expression import literal
|
||||||
from sqlalchemy.sql.expression import true
|
from sqlalchemy.sql.expression import true
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
@ -1652,6 +1653,16 @@ def share_instances_get_all(context, filters=None, session=None):
|
|||||||
models.ShareInstanceExportLocations.uuid ==
|
models.ShareInstanceExportLocations.uuid ==
|
||||||
export_location_id)
|
export_location_id)
|
||||||
|
|
||||||
|
query = query.join(
|
||||||
|
models.Share,
|
||||||
|
models.Share.id ==
|
||||||
|
models.ShareInstance.share_id)
|
||||||
|
is_soft_deleted = filters.get('is_soft_deleted')
|
||||||
|
if is_soft_deleted:
|
||||||
|
query = query.filter(models.Share.is_soft_deleted == true())
|
||||||
|
else:
|
||||||
|
query = query.filter(models.Share.is_soft_deleted == false())
|
||||||
|
|
||||||
instance_ids = filters.get('instance_ids')
|
instance_ids = filters.get('instance_ids')
|
||||||
if instance_ids:
|
if instance_ids:
|
||||||
query = query.filter(models.ShareInstance.id.in_(instance_ids))
|
query = query.filter(models.ShareInstance.id.in_(instance_ids))
|
||||||
@ -1987,7 +1998,7 @@ def _process_share_filters(query, filters, project_id=None, is_public=False):
|
|||||||
if filters is None:
|
if filters is None:
|
||||||
filters = {}
|
filters = {}
|
||||||
|
|
||||||
share_filter_keys = ['share_group_id', 'snapshot_id']
|
share_filter_keys = ['share_group_id', 'snapshot_id', 'is_soft_deleted']
|
||||||
instance_filter_keys = ['share_server_id', 'status', 'share_type_id',
|
instance_filter_keys = ['share_server_id', 'status', 'share_type_id',
|
||||||
'host', 'share_network_id']
|
'host', 'share_network_id']
|
||||||
share_filters = {}
|
share_filters = {}
|
||||||
@ -2196,6 +2207,11 @@ def _share_get_all_with_filters(context, project_id=None, share_server_id=None,
|
|||||||
if share_server_id:
|
if share_server_id:
|
||||||
filters['share_server_id'] = share_server_id
|
filters['share_server_id'] = share_server_id
|
||||||
|
|
||||||
|
# if not specified is_soft_deleted filter, default is False, to get
|
||||||
|
# shares not in recycle bin.
|
||||||
|
if 'is_soft_deleted' not in filters:
|
||||||
|
filters['is_soft_deleted'] = False
|
||||||
|
|
||||||
query = _process_share_filters(
|
query = _process_share_filters(
|
||||||
query, filters, project_id, is_public=is_public)
|
query, filters, project_id, is_public=is_public)
|
||||||
|
|
||||||
@ -2228,6 +2244,25 @@ def _share_get_all_with_filters(context, project_id=None, share_server_id=None,
|
|||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
@require_admin_context
|
||||||
|
def get_all_expired_shares(context):
|
||||||
|
query = (
|
||||||
|
_share_get_query(context).join(
|
||||||
|
models.ShareInstance,
|
||||||
|
models.ShareInstance.share_id == models.Share.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
filters = {"is_soft_deleted": True}
|
||||||
|
query = _process_share_filters(query, filters=filters)
|
||||||
|
scheduled_deleted_attr = getattr(models.Share,
|
||||||
|
'scheduled_to_be_deleted_at', None)
|
||||||
|
now_time = timeutils.utcnow()
|
||||||
|
query = query.filter(scheduled_deleted_attr.op('<=')(now_time))
|
||||||
|
result = query.all()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@require_admin_context
|
@require_admin_context
|
||||||
def share_get_all(context, filters=None, sort_key=None, sort_dir=None):
|
def share_get_all(context, filters=None, sort_key=None, sort_dir=None):
|
||||||
project_id = filters.pop('project_id', None) if filters else None
|
project_id = filters.pop('project_id', None) if filters else None
|
||||||
@ -2302,6 +2337,19 @@ def share_get_all_by_share_server(context, share_server_id, filters=None,
|
|||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def get_shares_in_recycle_bin_by_share_server(
|
||||||
|
context, share_server_id, filters=None, sort_key=None, sort_dir=None):
|
||||||
|
"""Returns list of shares in recycle bin with given share server."""
|
||||||
|
if filters is None:
|
||||||
|
filters = {}
|
||||||
|
filters["is_soft_deleted"] = True
|
||||||
|
query = _share_get_all_with_filters(
|
||||||
|
context, share_server_id=share_server_id, filters=filters,
|
||||||
|
sort_key=sort_key, sort_dir=sort_dir)
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
@require_context
|
@require_context
|
||||||
def share_get_all_by_share_server_with_count(
|
def share_get_all_by_share_server_with_count(
|
||||||
context, share_server_id, filters=None, sort_key=None, sort_dir=None):
|
context, share_server_id, filters=None, sort_key=None, sort_dir=None):
|
||||||
@ -2312,6 +2360,19 @@ def share_get_all_by_share_server_with_count(
|
|||||||
return count, query
|
return count, query
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def get_shares_in_recycle_bin_by_network(
|
||||||
|
context, share_network_id, filters=None, sort_key=None, sort_dir=None):
|
||||||
|
"""Returns list of shares in recycle bin with given share network."""
|
||||||
|
if filters is None:
|
||||||
|
filters = {}
|
||||||
|
filters["share_network_id"] = share_network_id
|
||||||
|
filters["is_soft_deleted"] = True
|
||||||
|
query = _share_get_all_with_filters(context, filters=filters,
|
||||||
|
sort_key=sort_key, sort_dir=sort_dir)
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
@require_context
|
@require_context
|
||||||
def share_delete(context, share_id):
|
def share_delete(context, share_id):
|
||||||
session = get_session()
|
session = get_session()
|
||||||
@ -2330,6 +2391,40 @@ def share_delete(context, share_id):
|
|||||||
filter_by(share_id=share_id).soft_delete())
|
filter_by(share_id=share_id).soft_delete())
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
|
||||||
|
def share_soft_delete(context, share_id):
|
||||||
|
session = get_session()
|
||||||
|
now_time = timeutils.utcnow()
|
||||||
|
time_delta = datetime.timedelta(
|
||||||
|
seconds=CONF.soft_deleted_share_retention_time)
|
||||||
|
scheduled_to_be_deleted_at = now_time + time_delta
|
||||||
|
update_values = {
|
||||||
|
'is_soft_deleted': True,
|
||||||
|
'scheduled_to_be_deleted_at': scheduled_to_be_deleted_at
|
||||||
|
}
|
||||||
|
|
||||||
|
with session.begin():
|
||||||
|
share_ref = share_get(context, share_id, session=session)
|
||||||
|
share_ref.update(update_values)
|
||||||
|
share_ref.save(session=session)
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
|
||||||
|
def share_restore(context, share_id):
|
||||||
|
session = get_session()
|
||||||
|
update_values = {
|
||||||
|
'is_soft_deleted': False,
|
||||||
|
'scheduled_to_be_deleted_at': None
|
||||||
|
}
|
||||||
|
|
||||||
|
with session.begin():
|
||||||
|
share_ref = share_get(context, share_id, session=session)
|
||||||
|
share_ref.update(update_values)
|
||||||
|
share_ref.save(session=session)
|
||||||
|
|
||||||
|
|
||||||
###################
|
###################
|
||||||
|
|
||||||
|
|
||||||
|
@ -315,6 +315,8 @@ class Share(BASE, ManilaBase):
|
|||||||
|
|
||||||
source_share_group_snapshot_member_id = Column(String(36), nullable=True)
|
source_share_group_snapshot_member_id = Column(String(36), nullable=True)
|
||||||
task_state = Column(String(255))
|
task_state = Column(String(255))
|
||||||
|
is_soft_deleted = Column(Boolean, default=False)
|
||||||
|
scheduled_to_be_deleted_at = Column(DateTime)
|
||||||
instances = orm.relationship(
|
instances = orm.relationship(
|
||||||
"ShareInstance",
|
"ShareInstance",
|
||||||
lazy='subquery',
|
lazy='subquery',
|
||||||
|
@ -316,6 +316,30 @@ shares_policies = [
|
|||||||
],
|
],
|
||||||
deprecated_rule=deprecated_share_delete
|
deprecated_rule=deprecated_share_delete
|
||||||
),
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=BASE_POLICY_NAME % 'soft_delete',
|
||||||
|
check_str=base.SYSTEM_ADMIN_OR_PROJECT_MEMBER,
|
||||||
|
scope_types=['system', 'project'],
|
||||||
|
description="Soft Delete a share.",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'POST',
|
||||||
|
'path': '/shares/{share_id}/action',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=BASE_POLICY_NAME % 'restore',
|
||||||
|
check_str=base.SYSTEM_ADMIN_OR_PROJECT_MEMBER,
|
||||||
|
scope_types=['system', 'project'],
|
||||||
|
description="Restore a share.",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'POST',
|
||||||
|
'path': '/shares/{share_id}/action',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
),
|
||||||
policy.DocumentedRuleDefault(
|
policy.DocumentedRuleDefault(
|
||||||
name=BASE_POLICY_NAME % 'force_delete',
|
name=BASE_POLICY_NAME % 'force_delete',
|
||||||
check_str=base.SYSTEM_ADMIN_OR_PROJECT_ADMIN,
|
check_str=base.SYSTEM_ADMIN_OR_PROJECT_ADMIN,
|
||||||
|
@ -988,11 +988,14 @@ class API(base.Base):
|
|||||||
# share server here, when manage/unmanage operations will be supported
|
# share server here, when manage/unmanage operations will be supported
|
||||||
# for driver_handles_share_servers=True mode
|
# for driver_handles_share_servers=True mode
|
||||||
|
|
||||||
def manage_snapshot(self, context, snapshot_data, driver_options):
|
def manage_snapshot(self, context, snapshot_data, driver_options,
|
||||||
try:
|
share=None):
|
||||||
share = self.db.share_get(context, snapshot_data['share_id'])
|
if not share:
|
||||||
except exception.NotFound:
|
try:
|
||||||
raise exception.ShareNotFound(share_id=snapshot_data['share_id'])
|
share = self.db.share_get(context, snapshot_data['share_id'])
|
||||||
|
except exception.NotFound:
|
||||||
|
raise exception.ShareNotFound(
|
||||||
|
share_id=snapshot_data['share_id'])
|
||||||
|
|
||||||
if share['has_replicas']:
|
if share['has_replicas']:
|
||||||
msg = (_("Share %s has replicas. Snapshots of this share cannot "
|
msg = (_("Share %s has replicas. Snapshots of this share cannot "
|
||||||
@ -1158,6 +1161,52 @@ class API(base.Base):
|
|||||||
self.share_rpcapi.revert_to_snapshot(
|
self.share_rpcapi.revert_to_snapshot(
|
||||||
context, share, snapshot, active_replica['host'], reservations)
|
context, share, snapshot, active_replica['host'], reservations)
|
||||||
|
|
||||||
|
@policy.wrap_check_policy('share')
|
||||||
|
def soft_delete(self, context, share):
|
||||||
|
"""Soft delete share."""
|
||||||
|
share_id = share['id']
|
||||||
|
|
||||||
|
if share['is_soft_deleted']:
|
||||||
|
msg = _("The share has been soft deleted already")
|
||||||
|
raise exception.InvalidShare(reason=msg)
|
||||||
|
|
||||||
|
statuses = (constants.STATUS_AVAILABLE, constants.STATUS_ERROR,
|
||||||
|
constants.STATUS_INACTIVE)
|
||||||
|
if share['status'] not in statuses:
|
||||||
|
msg = _("Share status must be one of %(statuses)s") % {
|
||||||
|
"statuses": statuses}
|
||||||
|
raise exception.InvalidShare(reason=msg)
|
||||||
|
|
||||||
|
# If the share has more than one replica,
|
||||||
|
# it can't be soft deleted until the additional replicas are removed.
|
||||||
|
if share.has_replicas:
|
||||||
|
msg = _("Share %s has replicas. Remove the replicas before "
|
||||||
|
"soft deleting the share.") % share_id
|
||||||
|
raise exception.Conflict(err=msg)
|
||||||
|
|
||||||
|
snapshots = self.db.share_snapshot_get_all_for_share(context, share_id)
|
||||||
|
if len(snapshots):
|
||||||
|
msg = _("Share still has %d dependent snapshots.") % len(snapshots)
|
||||||
|
raise exception.InvalidShare(reason=msg)
|
||||||
|
|
||||||
|
share_group_snapshot_members_count = (
|
||||||
|
self.db.count_share_group_snapshot_members_in_share(
|
||||||
|
context, share_id))
|
||||||
|
if share_group_snapshot_members_count:
|
||||||
|
msg = (
|
||||||
|
_("Share still has %d dependent share group snapshot "
|
||||||
|
"members.") % share_group_snapshot_members_count)
|
||||||
|
raise exception.InvalidShare(reason=msg)
|
||||||
|
|
||||||
|
self._check_is_share_busy(share)
|
||||||
|
self.db.share_soft_delete(context, share_id)
|
||||||
|
|
||||||
|
@policy.wrap_check_policy('share')
|
||||||
|
def restore(self, context, share):
|
||||||
|
"""Restore share."""
|
||||||
|
share_id = share['id']
|
||||||
|
self.db.share_restore(context, share_id)
|
||||||
|
|
||||||
@policy.wrap_check_policy('share')
|
@policy.wrap_check_policy('share')
|
||||||
def delete(self, context, share, force=False):
|
def delete(self, context, share, force=False):
|
||||||
"""Delete share."""
|
"""Delete share."""
|
||||||
@ -1859,7 +1908,7 @@ class API(base.Base):
|
|||||||
'display_description', 'display_description~', 'snapshot_id',
|
'display_description', 'display_description~', 'snapshot_id',
|
||||||
'status', 'share_type_id', 'project_id', 'export_location_id',
|
'status', 'share_type_id', 'project_id', 'export_location_id',
|
||||||
'export_location_path', 'limit', 'offset', 'host',
|
'export_location_path', 'limit', 'offset', 'host',
|
||||||
'share_network_id']
|
'share_network_id', 'is_soft_deleted']
|
||||||
|
|
||||||
for key in filter_keys:
|
for key in filter_keys:
|
||||||
if key in search_opts:
|
if key in search_opts:
|
||||||
@ -2516,11 +2565,20 @@ class API(base.Base):
|
|||||||
shares = self.db.share_get_all_by_share_server(
|
shares = self.db.share_get_all_by_share_server(
|
||||||
context, share_server['id'])
|
context, share_server['id'])
|
||||||
|
|
||||||
|
shares_in_recycle_bin = (
|
||||||
|
self.db.get_shares_in_recycle_bin_by_share_server(
|
||||||
|
context, share_server['id']))
|
||||||
|
|
||||||
if len(shares) == 0:
|
if len(shares) == 0:
|
||||||
msg = _("Share server %s does not have shares."
|
msg = _("Share server %s does not have shares."
|
||||||
% share_server['id'])
|
% share_server['id'])
|
||||||
raise exception.InvalidShareServer(reason=msg)
|
raise exception.InvalidShareServer(reason=msg)
|
||||||
|
|
||||||
|
if shares_in_recycle_bin:
|
||||||
|
msg = _("Share server %s has at least one share that has "
|
||||||
|
"been soft deleted." % share_server['id'])
|
||||||
|
raise exception.InvalidShareServer(reason=msg)
|
||||||
|
|
||||||
# We only handle "active" share servers for now
|
# We only handle "active" share servers for now
|
||||||
if share_server['status'] != constants.STATUS_ACTIVE:
|
if share_server['status'] != constants.STATUS_ACTIVE:
|
||||||
msg = _('Share server %(server_id)s status must be active, '
|
msg = _('Share server %(server_id)s status must be active, '
|
||||||
@ -2984,6 +3042,14 @@ class API(base.Base):
|
|||||||
# Make sure the host is in the list of available hosts
|
# Make sure the host is in the list of available hosts
|
||||||
utils.validate_service_host(admin_ctx, backend_host)
|
utils.validate_service_host(admin_ctx, backend_host)
|
||||||
|
|
||||||
|
shares_in_recycle_bin = (
|
||||||
|
self.db.get_shares_in_recycle_bin_by_network(
|
||||||
|
context, share_network['id']))
|
||||||
|
if shares_in_recycle_bin:
|
||||||
|
msg = _("Some shares with share network %(sn_id)s have "
|
||||||
|
"been soft deleted.") % {'sn_id': share_network['id']}
|
||||||
|
raise exception.InvalidShareNetwork(reason=msg)
|
||||||
|
|
||||||
shares = self.get_all(
|
shares = self.get_all(
|
||||||
context, search_opts={'share_network_id': share_network['id']})
|
context, search_opts={'share_network_id': share_network['id']})
|
||||||
shares_not_available = [
|
shares_not_available = [
|
||||||
|
@ -131,6 +131,11 @@ share_manager_opts = [
|
|||||||
default=False,
|
default=False,
|
||||||
help='Offload pending share ensure during '
|
help='Offload pending share ensure during '
|
||||||
'share service startup'),
|
'share service startup'),
|
||||||
|
cfg.IntOpt('check_for_expired_shares_in_recycle_bin_interval',
|
||||||
|
default=3600,
|
||||||
|
help='This value, specified in seconds, determines how often '
|
||||||
|
'the share manager will check for expired shares and '
|
||||||
|
'delete them from the Recycle bin.'),
|
||||||
]
|
]
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -3483,6 +3488,17 @@ class ShareManager(manager.SchedulerDependentManager):
|
|||||||
for server in servers:
|
for server in servers:
|
||||||
self.delete_share_server(ctxt, server)
|
self.delete_share_server(ctxt, server)
|
||||||
|
|
||||||
|
@periodic_task.periodic_task(
|
||||||
|
spacing=CONF.check_for_expired_shares_in_recycle_bin_interval)
|
||||||
|
@utils.require_driver_initialized
|
||||||
|
def delete_expired_share(self, ctxt):
|
||||||
|
LOG.debug("Check for expired share in recycle bin to delete.")
|
||||||
|
expired_shares = self.db.get_all_expired_shares(ctxt)
|
||||||
|
|
||||||
|
for share in expired_shares:
|
||||||
|
LOG.debug("share %s has expired, will be deleted", share['id'])
|
||||||
|
self.share_api.delete(ctxt, share, force=True)
|
||||||
|
|
||||||
@add_hooks
|
@add_hooks
|
||||||
@utils.require_driver_initialized
|
@utils.require_driver_initialized
|
||||||
def create_snapshot(self, context, share_id, snapshot_id):
|
def create_snapshot(self, context, share_id, snapshot_id):
|
||||||
|
@ -46,6 +46,7 @@ def stub_share(id, **kwargs):
|
|||||||
'mount_snapshot_support': False,
|
'mount_snapshot_support': False,
|
||||||
'replication_type': None,
|
'replication_type': None,
|
||||||
'has_replicas': False,
|
'has_replicas': False,
|
||||||
|
'is_soft_deleted': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
share_instance = {
|
share_instance = {
|
||||||
@ -149,6 +150,14 @@ def stub_share_delete(self, context, *args, **param):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def stub_share_soft_delete(self, context, *args, **param):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def stub_share_restore(self, context, *args, **param):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def stub_share_update(self, context, *args, **param):
|
def stub_share_update(self, context, *args, **param):
|
||||||
share = stub_share('1')
|
share = stub_share('1')
|
||||||
return share
|
return share
|
||||||
|
@ -112,6 +112,29 @@ class ShareSnapshotAPITest(test.TestCase):
|
|||||||
|
|
||||||
self.assertFalse(share_api.API.create_snapshot.called)
|
self.assertFalse(share_api.API.create_snapshot.called)
|
||||||
|
|
||||||
|
def test_snapshot_create_in_recycle_bin(self):
|
||||||
|
self.mock_object(share_api.API, 'create_snapshot')
|
||||||
|
self.mock_object(
|
||||||
|
share_api.API,
|
||||||
|
'get',
|
||||||
|
mock.Mock(return_value={'snapshot_support': True,
|
||||||
|
'is_soft_deleted': True}))
|
||||||
|
body = {
|
||||||
|
'snapshot': {
|
||||||
|
'share_id': 200,
|
||||||
|
'force': False,
|
||||||
|
'name': 'fake_share_name',
|
||||||
|
'description': 'fake_share_description',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req = fakes.HTTPRequest.blank('/fake/snapshots')
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
webob.exc.HTTPForbidden,
|
||||||
|
self.controller.create, req, body)
|
||||||
|
|
||||||
|
self.assertFalse(share_api.API.create_snapshot.called)
|
||||||
|
|
||||||
def test_snapshot_create_no_body(self):
|
def test_snapshot_create_no_body(self):
|
||||||
body = {}
|
body = {}
|
||||||
req = fakes.HTTPRequest.blank('/fake/snapshots')
|
req = fakes.HTTPRequest.blank('/fake/snapshots')
|
||||||
|
@ -121,6 +121,28 @@ class ShareUnmanageTest(test.TestCase):
|
|||||||
self.mock_policy_check.assert_called_once_with(
|
self.mock_policy_check.assert_called_once_with(
|
||||||
self.context, self.resource_name, 'unmanage')
|
self.context, self.resource_name, 'unmanage')
|
||||||
|
|
||||||
|
def test_unmanage_share_that_has_been_soft_deleted(self):
|
||||||
|
share = dict(status=constants.STATUS_AVAILABLE, id='foo_id',
|
||||||
|
instance={}, is_soft_deleted=True)
|
||||||
|
mock_api_unmanage = self.mock_object(self.controller.share_api,
|
||||||
|
'unmanage')
|
||||||
|
mock_db_snapshots_get = self.mock_object(
|
||||||
|
self.controller.share_api.db, 'share_snapshot_get_all_for_share')
|
||||||
|
self.mock_object(
|
||||||
|
self.controller.share_api, 'get',
|
||||||
|
mock.Mock(return_value=share))
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
webob.exc.HTTPForbidden,
|
||||||
|
self.controller.unmanage, self.request, share['id'])
|
||||||
|
|
||||||
|
self.assertFalse(mock_api_unmanage.called)
|
||||||
|
self.assertFalse(mock_db_snapshots_get.called)
|
||||||
|
self.controller.share_api.get.assert_called_once_with(
|
||||||
|
self.request.environ['manila.context'], share['id'])
|
||||||
|
self.mock_policy_check.assert_called_once_with(
|
||||||
|
self.context, self.resource_name, 'unmanage')
|
||||||
|
|
||||||
def test_unmanage_share_based_on_share_server(self):
|
def test_unmanage_share_based_on_share_server(self):
|
||||||
share = dict(instance=dict(share_server_id='foo_id'), id='bar_id')
|
share = dict(instance=dict(share_server_id='foo_id'), id='bar_id')
|
||||||
self.mock_object(
|
self.mock_object(
|
||||||
|
@ -67,19 +67,25 @@ class ShareInstancesAPITest(test.TestCase):
|
|||||||
self.assertEqual([i['id'] for i in expected],
|
self.assertEqual([i['id'] for i in expected],
|
||||||
[i['id'] for i in actual])
|
[i['id'] for i in actual])
|
||||||
|
|
||||||
@ddt.data("2.3", "2.34", "2.35")
|
@ddt.data("2.3", "2.34", "2.35", "2.69")
|
||||||
def test_index(self, version):
|
def test_index(self, version):
|
||||||
url = '/share_instances'
|
url = '/share_instances'
|
||||||
if (api_version_request.APIVersionRequest(version) >=
|
if (api_version_request.APIVersionRequest(version) >=
|
||||||
api_version_request.APIVersionRequest('2.35')):
|
api_version_request.APIVersionRequest('2.35')):
|
||||||
url += "?export_location_path=/admin/export/location"
|
url += "?export_location_path=/admin/export/location"
|
||||||
|
if (api_version_request.APIVersionRequest(version) >=
|
||||||
|
api_version_request.APIVersionRequest('2.69')):
|
||||||
|
url += "&is_soft_deleted=true"
|
||||||
req = self._get_request(url, version=version)
|
req = self._get_request(url, version=version)
|
||||||
req_context = req.environ['manila.context']
|
req_context = req.environ['manila.context']
|
||||||
|
last_instance = [db_utils.create_share(size=1,
|
||||||
|
is_soft_deleted=True).instance]
|
||||||
share_instances_count = 3
|
share_instances_count = 3
|
||||||
test_instances = [
|
other_instances = [
|
||||||
db_utils.create_share(size=s + 1).instance
|
db_utils.create_share(size=s + 1).instance
|
||||||
for s in range(0, share_instances_count)
|
for s in range(0, share_instances_count)
|
||||||
]
|
]
|
||||||
|
test_instances = other_instances + last_instance
|
||||||
|
|
||||||
db.share_export_locations_update(
|
db.share_export_locations_update(
|
||||||
self.admin_context, test_instances[0]['id'],
|
self.admin_context, test_instances[0]['id'],
|
||||||
@ -88,8 +94,13 @@ class ShareInstancesAPITest(test.TestCase):
|
|||||||
actual_result = self.controller.index(req)
|
actual_result = self.controller.index(req)
|
||||||
|
|
||||||
if (api_version_request.APIVersionRequest(version) >=
|
if (api_version_request.APIVersionRequest(version) >=
|
||||||
|
api_version_request.APIVersionRequest('2.69')):
|
||||||
|
test_instances = []
|
||||||
|
elif (api_version_request.APIVersionRequest(version) >=
|
||||||
api_version_request.APIVersionRequest('2.35')):
|
api_version_request.APIVersionRequest('2.35')):
|
||||||
test_instances = test_instances[:1]
|
test_instances = test_instances[:1]
|
||||||
|
else:
|
||||||
|
test_instances = other_instances
|
||||||
self._validate_ids_in_share_instances_list(
|
self._validate_ids_in_share_instances_list(
|
||||||
test_instances, actual_result['share_instances'])
|
test_instances, actual_result['share_instances'])
|
||||||
self.mock_policy_check.assert_called_once_with(
|
self.mock_policy_check.assert_called_once_with(
|
||||||
|
@ -375,6 +375,27 @@ class ShareReplicasApiTest(test.TestCase):
|
|||||||
self.mock_policy_check.assert_called_once_with(
|
self.mock_policy_check.assert_called_once_with(
|
||||||
self.member_context, self.resource_name, 'create')
|
self.member_context, self.resource_name, 'create')
|
||||||
|
|
||||||
|
def test_create_has_been_soft_deleted(self):
|
||||||
|
share_ref = fake_share.fake_share(is_soft_deleted=True)
|
||||||
|
body = {
|
||||||
|
'share_replica': {
|
||||||
|
'share_id': 'FAKE_SHAREID',
|
||||||
|
'availability_zone': 'FAKE_AZ'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock__view_builder_call = self.mock_object(
|
||||||
|
share_replicas.replication_view.ReplicationViewBuilder,
|
||||||
|
'detail_list')
|
||||||
|
self.mock_object(share_replicas.db, 'share_get',
|
||||||
|
mock.Mock(return_value=share_ref))
|
||||||
|
|
||||||
|
self.assertRaises(exc.HTTPForbidden,
|
||||||
|
self.controller.create,
|
||||||
|
self.replicas_req, body)
|
||||||
|
self.assertFalse(mock__view_builder_call.called)
|
||||||
|
self.mock_policy_check.assert_called_once_with(
|
||||||
|
self.member_context, self.resource_name, 'create')
|
||||||
|
|
||||||
@ddt.data(exception.AvailabilityZoneNotFound,
|
@ddt.data(exception.AvailabilityZoneNotFound,
|
||||||
exception.ReplicationException, exception.ShareBusyException)
|
exception.ReplicationException, exception.ShareBusyException)
|
||||||
def test_create_exception_path(self, exception_type):
|
def test_create_exception_path(self, exception_type):
|
||||||
|
@ -731,9 +731,14 @@ class ShareSnapshotAdminActionsAPITest(test.TestCase):
|
|||||||
data['snapshot']['share_id'] = 'fake'
|
data['snapshot']['share_id'] = 'fake'
|
||||||
data['snapshot']['provider_location'] = 'fake_volume_snapshot_id'
|
data['snapshot']['provider_location'] = 'fake_volume_snapshot_id'
|
||||||
data['snapshot']['driver_options'] = {}
|
data['snapshot']['driver_options'] = {}
|
||||||
|
return_share = fake_share.fake_share(is_soft_deleted=False,
|
||||||
|
id='fake')
|
||||||
return_snapshot = fake_share.fake_snapshot(
|
return_snapshot = fake_share.fake_snapshot(
|
||||||
create_instance=True, id='fake_snap',
|
create_instance=True, id='fake_snap',
|
||||||
provider_location='fake_volume_snapshot_id')
|
provider_location='fake_volume_snapshot_id')
|
||||||
|
self.mock_object(
|
||||||
|
share_api.API, 'get', mock.Mock(
|
||||||
|
return_value=return_share))
|
||||||
self.mock_object(
|
self.mock_object(
|
||||||
share_api.API, 'manage_snapshot', mock.Mock(
|
share_api.API, 'manage_snapshot', mock.Mock(
|
||||||
return_value=return_snapshot))
|
return_value=return_snapshot))
|
||||||
@ -752,7 +757,8 @@ class ShareSnapshotAdminActionsAPITest(test.TestCase):
|
|||||||
|
|
||||||
actual_snapshot = actual_result['snapshot']
|
actual_snapshot = actual_result['snapshot']
|
||||||
share_api.API.manage_snapshot.assert_called_once_with(
|
share_api.API.manage_snapshot.assert_called_once_with(
|
||||||
mock.ANY, share_snapshot, data['snapshot']['driver_options'])
|
mock.ANY, share_snapshot, data['snapshot']['driver_options'],
|
||||||
|
share=return_share)
|
||||||
self.assertEqual(return_snapshot['id'],
|
self.assertEqual(return_snapshot['id'],
|
||||||
actual_result['snapshot']['id'])
|
actual_result['snapshot']['id'])
|
||||||
self.assertEqual('fake_volume_snapshot_id',
|
self.assertEqual('fake_volume_snapshot_id',
|
||||||
@ -781,6 +787,11 @@ class ShareSnapshotAdminActionsAPITest(test.TestCase):
|
|||||||
body = get_fake_manage_body(
|
body = get_fake_manage_body(
|
||||||
share_id='fake', provider_location='fake_volume_snapshot_id',
|
share_id='fake', provider_location='fake_volume_snapshot_id',
|
||||||
driver_options={})
|
driver_options={})
|
||||||
|
return_share = fake_share.fake_share(is_soft_deleted=False,
|
||||||
|
id='fake')
|
||||||
|
self.mock_object(
|
||||||
|
share_api.API, 'get', mock.Mock(
|
||||||
|
return_value=return_share))
|
||||||
self.mock_object(
|
self.mock_object(
|
||||||
share_api.API, 'manage_snapshot', mock.Mock(
|
share_api.API, 'manage_snapshot', mock.Mock(
|
||||||
side_effect=exception_type))
|
side_effect=exception_type))
|
||||||
@ -798,6 +809,25 @@ class ShareSnapshotAdminActionsAPITest(test.TestCase):
|
|||||||
self.manage_request.environ['manila.context'],
|
self.manage_request.environ['manila.context'],
|
||||||
self.resource_name, 'manage_snapshot')
|
self.resource_name, 'manage_snapshot')
|
||||||
|
|
||||||
|
def test_manage_share_has_been_soft_deleted(self):
|
||||||
|
self.mock_policy_check = self.mock_object(
|
||||||
|
policy, 'check_policy', mock.Mock(return_value=True))
|
||||||
|
body = get_fake_manage_body(
|
||||||
|
share_id='fake', provider_location='fake_volume_snapshot_id',
|
||||||
|
driver_options={})
|
||||||
|
return_share = fake_share.fake_share(is_soft_deleted=True,
|
||||||
|
id='fake')
|
||||||
|
self.mock_object(
|
||||||
|
share_api.API, 'get', mock.Mock(
|
||||||
|
return_value=return_share))
|
||||||
|
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller.manage,
|
||||||
|
self.manage_request, body)
|
||||||
|
self.mock_policy_check.assert_called_once_with(
|
||||||
|
self.manage_request.environ['manila.context'],
|
||||||
|
self.resource_name, 'manage_snapshot')
|
||||||
|
|
||||||
@ddt.data('1.0', '2.6', '2.11')
|
@ddt.data('1.0', '2.6', '2.11')
|
||||||
def test_manage_version_not_found(self, version):
|
def test_manage_version_not_found(self, version):
|
||||||
body = get_fake_manage_body(
|
body = get_fake_manage_body(
|
||||||
|
@ -63,12 +63,25 @@ class ShareAPITest(test.TestCase):
|
|||||||
stubs.stub_share_get)
|
stubs.stub_share_get)
|
||||||
self.mock_object(share_api.API, 'update', stubs.stub_share_update)
|
self.mock_object(share_api.API, 'update', stubs.stub_share_update)
|
||||||
self.mock_object(share_api.API, 'delete', stubs.stub_share_delete)
|
self.mock_object(share_api.API, 'delete', stubs.stub_share_delete)
|
||||||
|
self.mock_object(share_api.API, 'soft_delete',
|
||||||
|
stubs.stub_share_soft_delete)
|
||||||
|
self.mock_object(share_api.API, 'restore', stubs.stub_share_restore)
|
||||||
self.mock_object(share_api.API, 'get_snapshot',
|
self.mock_object(share_api.API, 'get_snapshot',
|
||||||
stubs.stub_snapshot_get)
|
stubs.stub_snapshot_get)
|
||||||
self.mock_object(share_types, 'get_share_type',
|
self.mock_object(share_types, 'get_share_type',
|
||||||
stubs.stub_share_type_get)
|
stubs.stub_share_type_get)
|
||||||
self.maxDiff = None
|
self.maxDiff = None
|
||||||
self.share = {
|
self.share = {
|
||||||
|
"id": "1",
|
||||||
|
"size": 100,
|
||||||
|
"display_name": "Share Test Name",
|
||||||
|
"display_description": "Share Test Desc",
|
||||||
|
"share_proto": "fakeproto",
|
||||||
|
"availability_zone": "zone1:host1",
|
||||||
|
"is_public": False,
|
||||||
|
"task_state": None
|
||||||
|
}
|
||||||
|
self.share_in_recycle_bin = {
|
||||||
"id": "1",
|
"id": "1",
|
||||||
"size": 100,
|
"size": 100,
|
||||||
"display_name": "Share Test Name",
|
"display_name": "Share Test Name",
|
||||||
@ -77,6 +90,20 @@ class ShareAPITest(test.TestCase):
|
|||||||
"availability_zone": "zone1:host1",
|
"availability_zone": "zone1:host1",
|
||||||
"is_public": False,
|
"is_public": False,
|
||||||
"task_state": None,
|
"task_state": None,
|
||||||
|
"is_soft_deleted": True,
|
||||||
|
"status": "available"
|
||||||
|
}
|
||||||
|
self.share_in_recycle_bin_is_deleting = {
|
||||||
|
"id": "1",
|
||||||
|
"size": 100,
|
||||||
|
"display_name": "Share Test Name",
|
||||||
|
"display_description": "Share Test Desc",
|
||||||
|
"share_proto": "fakeproto",
|
||||||
|
"availability_zone": "zone1:host1",
|
||||||
|
"is_public": False,
|
||||||
|
"task_state": None,
|
||||||
|
"is_soft_deleted": True,
|
||||||
|
"status": "deleting"
|
||||||
}
|
}
|
||||||
self.create_mock = mock.Mock(
|
self.create_mock = mock.Mock(
|
||||||
return_value=stubs.stub_share(
|
return_value=stubs.stub_share(
|
||||||
@ -234,6 +261,23 @@ class ShareAPITest(test.TestCase):
|
|||||||
mock_revert_to_snapshot.assert_called_once_with(
|
mock_revert_to_snapshot.assert_called_once_with(
|
||||||
utils.IsAMatcher(context.RequestContext), share, snapshot)
|
utils.IsAMatcher(context.RequestContext), share, snapshot)
|
||||||
|
|
||||||
|
def test__revert_share_has_been_soft_deleted(self):
|
||||||
|
snapshot = copy.deepcopy(self.snapshot)
|
||||||
|
body = {'revert': {'snapshot_id': '2'}}
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/shares/1/action',
|
||||||
|
use_admin_context=False,
|
||||||
|
version='2.27')
|
||||||
|
self.mock_object(
|
||||||
|
self.controller, '_validate_revert_parameters',
|
||||||
|
mock.Mock(return_value=body['revert']))
|
||||||
|
self.mock_object(share_api.API, 'get',
|
||||||
|
mock.Mock(return_value=self.share_in_recycle_bin))
|
||||||
|
self.mock_object(
|
||||||
|
share_api.API, 'get_snapshot', mock.Mock(return_value=snapshot))
|
||||||
|
self.assertRaises(
|
||||||
|
webob.exc.HTTPForbidden, self.controller._revert,
|
||||||
|
req, 1, body)
|
||||||
|
|
||||||
def test__revert_not_supported(self):
|
def test__revert_not_supported(self):
|
||||||
|
|
||||||
share = copy.deepcopy(self.share)
|
share = copy.deepcopy(self.share)
|
||||||
@ -1100,6 +1144,24 @@ class ShareAPITest(test.TestCase):
|
|||||||
db.share_update.assert_called_once_with(utils.IsAMatcher(
|
db.share_update.assert_called_once_with(utils.IsAMatcher(
|
||||||
context.RequestContext), share['id'], update)
|
context.RequestContext), share['id'], update)
|
||||||
|
|
||||||
|
def test_reset_task_state_share_has_been_soft_deleted(self):
|
||||||
|
share = self.share_in_recycle_bin
|
||||||
|
req = fakes.HTTPRequest.blank(
|
||||||
|
'/v2/fake/shares/%s/action' % share['id'],
|
||||||
|
use_admin_context=True,
|
||||||
|
version='2.22')
|
||||||
|
req.method = 'POST'
|
||||||
|
req.headers['content-type'] = 'application/json'
|
||||||
|
req.api_version_request.experimental = True
|
||||||
|
update = {'task_state': constants.TASK_STATE_MIGRATION_ERROR}
|
||||||
|
body = {'reset_task_state': update}
|
||||||
|
self.mock_object(share_api.API, 'get',
|
||||||
|
mock.Mock(return_value=share))
|
||||||
|
|
||||||
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
self.controller.reset_task_state, req, share['id'],
|
||||||
|
body)
|
||||||
|
|
||||||
def test_migration_complete(self):
|
def test_migration_complete(self):
|
||||||
share = db_utils.create_share()
|
share = db_utils.create_share()
|
||||||
req = fakes.HTTPRequest.blank(
|
req = fakes.HTTPRequest.blank(
|
||||||
@ -1524,6 +1586,60 @@ class ShareAPITest(test.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(expected, res_dict['share']['access_rules_status'])
|
self.assertEqual(expected, res_dict['share']['access_rules_status'])
|
||||||
|
|
||||||
|
def test_share_soft_delete(self):
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/shares/1/action',
|
||||||
|
version='2.69')
|
||||||
|
body = {"soft_delete": None}
|
||||||
|
resp = self.controller.share_soft_delete(req, 1, body)
|
||||||
|
self.assertEqual(202, resp.status_int)
|
||||||
|
|
||||||
|
def test_share_soft_delete_has_been_soft_deleted_already(self):
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/shares/1/action',
|
||||||
|
version='2.69')
|
||||||
|
body = {"soft_delete": None}
|
||||||
|
self.mock_object(share_api.API, 'get',
|
||||||
|
mock.Mock(return_value=self.share_in_recycle_bin))
|
||||||
|
self.mock_object(share_api.API, 'soft_delete',
|
||||||
|
mock.Mock(
|
||||||
|
side_effect=exception.InvalidShare(reason='err')))
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
webob.exc.HTTPForbidden, self.controller.share_soft_delete,
|
||||||
|
req, 1, body)
|
||||||
|
|
||||||
|
def test_share_soft_delete_has_replicas(self):
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/shares/1/action',
|
||||||
|
version='2.69')
|
||||||
|
body = {"soft_delete": None}
|
||||||
|
self.mock_object(share_api.API, 'get',
|
||||||
|
mock.Mock(return_value=self.share))
|
||||||
|
self.mock_object(share_api.API, 'soft_delete',
|
||||||
|
mock.Mock(side_effect=exception.Conflict(err='err')))
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
webob.exc.HTTPConflict, self.controller.share_soft_delete,
|
||||||
|
req, 1, body)
|
||||||
|
|
||||||
|
def test_share_restore(self):
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/shares/1/action',
|
||||||
|
version='2.69')
|
||||||
|
body = {"restore": None}
|
||||||
|
self.mock_object(share_api.API, 'get',
|
||||||
|
mock.Mock(return_value=self.share_in_recycle_bin))
|
||||||
|
resp = self.controller.share_restore(req, 1, body)
|
||||||
|
self.assertEqual(202, resp.status_int)
|
||||||
|
|
||||||
|
def test_share_restore_with_deleting_status(self):
|
||||||
|
req = fakes.HTTPRequest.blank('/v2/fake/shares/1/action',
|
||||||
|
version='2.69')
|
||||||
|
body = {"restore": None}
|
||||||
|
self.mock_object(
|
||||||
|
share_api.API, 'get',
|
||||||
|
mock.Mock(return_value=self.share_in_recycle_bin_is_deleting))
|
||||||
|
self.assertRaises(
|
||||||
|
webob.exc.HTTPForbidden, self.controller.share_restore,
|
||||||
|
req, 1, body)
|
||||||
|
|
||||||
def test_share_delete(self):
|
def test_share_delete(self):
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/shares/1')
|
req = fakes.HTTPRequest.blank('/v2/fake/shares/1')
|
||||||
resp = self.controller.delete(req, 1)
|
resp = self.controller.delete(req, 1)
|
||||||
@ -1614,7 +1730,9 @@ class ShareAPITest(test.TestCase):
|
|||||||
{'use_admin_context': True, 'version': '2.36'},
|
{'use_admin_context': True, 'version': '2.36'},
|
||||||
{'use_admin_context': False, 'version': '2.36'},
|
{'use_admin_context': False, 'version': '2.36'},
|
||||||
{'use_admin_context': True, 'version': '2.42'},
|
{'use_admin_context': True, 'version': '2.42'},
|
||||||
{'use_admin_context': False, 'version': '2.42'})
|
{'use_admin_context': False, 'version': '2.42'},
|
||||||
|
{'use_admin_context': False, 'version': '2.69'},
|
||||||
|
{'use_admin_context': True, 'version': '2.69'})
|
||||||
@ddt.unpack
|
@ddt.unpack
|
||||||
def test_share_list_summary_with_search_opts(self, use_admin_context,
|
def test_share_list_summary_with_search_opts(self, use_admin_context,
|
||||||
version):
|
version):
|
||||||
@ -1640,6 +1758,9 @@ class ShareAPITest(test.TestCase):
|
|||||||
search_opts.update(
|
search_opts.update(
|
||||||
{'display_name~': 'fake',
|
{'display_name~': 'fake',
|
||||||
'display_description~': 'fake'})
|
'display_description~': 'fake'})
|
||||||
|
if (api_version.APIVersionRequest(version) >=
|
||||||
|
api_version.APIVersionRequest('2.69')):
|
||||||
|
search_opts.update({'is_soft_deleted': True})
|
||||||
method = 'get_all'
|
method = 'get_all'
|
||||||
shares = [
|
shares = [
|
||||||
{'id': 'id1', 'display_name': 'n1'},
|
{'id': 'id1', 'display_name': 'n1'},
|
||||||
@ -1658,7 +1779,7 @@ class ShareAPITest(test.TestCase):
|
|||||||
# fake_key should be filtered for non-admin
|
# fake_key should be filtered for non-admin
|
||||||
url = '/v2/fake/shares?fake_key=fake_value'
|
url = '/v2/fake/shares?fake_key=fake_value'
|
||||||
for k, v in search_opts.items():
|
for k, v in search_opts.items():
|
||||||
url = url + '&' + k + '=' + v
|
url = url + '&' + k + '=' + str(v)
|
||||||
req = fakes.HTTPRequest.blank(url, version=version,
|
req = fakes.HTTPRequest.blank(url, version=version,
|
||||||
use_admin_context=use_admin_context)
|
use_admin_context=use_admin_context)
|
||||||
|
|
||||||
@ -1691,6 +1812,10 @@ class ShareAPITest(test.TestCase):
|
|||||||
search_opts_expected.update(
|
search_opts_expected.update(
|
||||||
{'display_name~': search_opts['display_name~'],
|
{'display_name~': search_opts['display_name~'],
|
||||||
'display_description~': search_opts['display_description~']})
|
'display_description~': search_opts['display_description~']})
|
||||||
|
if (api_version.APIVersionRequest(version) >=
|
||||||
|
api_version.APIVersionRequest('2.69')):
|
||||||
|
search_opts_expected['is_soft_deleted'] = (
|
||||||
|
search_opts['is_soft_deleted'])
|
||||||
|
|
||||||
if use_admin_context:
|
if use_admin_context:
|
||||||
search_opts_expected.update({'fake_key': 'fake_value'})
|
search_opts_expected.update({'fake_key': 'fake_value'})
|
||||||
@ -1778,7 +1903,9 @@ class ShareAPITest(test.TestCase):
|
|||||||
{'use_admin_context': True, 'version': '2.35'},
|
{'use_admin_context': True, 'version': '2.35'},
|
||||||
{'use_admin_context': False, 'version': '2.35'},
|
{'use_admin_context': False, 'version': '2.35'},
|
||||||
{'use_admin_context': True, 'version': '2.42'},
|
{'use_admin_context': True, 'version': '2.42'},
|
||||||
{'use_admin_context': False, 'version': '2.42'})
|
{'use_admin_context': False, 'version': '2.42'},
|
||||||
|
{'use_admin_context': True, 'version': '2.69'},
|
||||||
|
{'use_admin_context': False, 'version': '2.69'})
|
||||||
@ddt.unpack
|
@ddt.unpack
|
||||||
def test_share_list_detail_with_search_opts(self, use_admin_context,
|
def test_share_list_detail_with_search_opts(self, use_admin_context,
|
||||||
version):
|
version):
|
||||||
@ -1812,6 +1939,8 @@ class ShareAPITest(test.TestCase):
|
|||||||
'share_type_id': 'fake_share_type_id',
|
'share_type_id': 'fake_share_type_id',
|
||||||
},
|
},
|
||||||
'has_replicas': False,
|
'has_replicas': False,
|
||||||
|
'is_soft_deleted': True,
|
||||||
|
'scheduled_to_be_deleted_at': 'fake_datatime',
|
||||||
},
|
},
|
||||||
{'id': 'id3', 'display_name': 'n3'},
|
{'id': 'id3', 'display_name': 'n3'},
|
||||||
]
|
]
|
||||||
@ -1823,12 +1952,15 @@ class ShareAPITest(test.TestCase):
|
|||||||
search_opts.update({'with_count': 'true'})
|
search_opts.update({'with_count': 'true'})
|
||||||
method = 'get_all_with_count'
|
method = 'get_all_with_count'
|
||||||
mock_action = {'side_effect': [(1, [shares[1]])]}
|
mock_action = {'side_effect': [(1, [shares[1]])]}
|
||||||
|
if (api_version.APIVersionRequest(version) >=
|
||||||
|
api_version.APIVersionRequest('2.69')):
|
||||||
|
search_opts.update({'is_soft_deleted': True})
|
||||||
if use_admin_context:
|
if use_admin_context:
|
||||||
search_opts['host'] = 'fake_host'
|
search_opts['host'] = 'fake_host'
|
||||||
# fake_key should be filtered for non-admin
|
# fake_key should be filtered for non-admin
|
||||||
url = '/v2/fake/shares/detail?fake_key=fake_value'
|
url = '/v2/fake/shares/detail?fake_key=fake_value'
|
||||||
for k, v in search_opts.items():
|
for k, v in search_opts.items():
|
||||||
url = url + '&' + k + '=' + v
|
url = url + '&' + k + '=' + str(v)
|
||||||
req = fakes.HTTPRequest.blank(url, version=version,
|
req = fakes.HTTPRequest.blank(url, version=version,
|
||||||
use_admin_context=use_admin_context)
|
use_admin_context=use_admin_context)
|
||||||
|
|
||||||
@ -1857,6 +1989,10 @@ class ShareAPITest(test.TestCase):
|
|||||||
search_opts['export_location_id'])
|
search_opts['export_location_id'])
|
||||||
search_opts_expected['export_location_path'] = (
|
search_opts_expected['export_location_path'] = (
|
||||||
search_opts['export_location_path'])
|
search_opts['export_location_path'])
|
||||||
|
if (api_version.APIVersionRequest(version) >=
|
||||||
|
api_version.APIVersionRequest('2.69')):
|
||||||
|
search_opts_expected['is_soft_deleted'] = (
|
||||||
|
search_opts['is_soft_deleted'])
|
||||||
|
|
||||||
if use_admin_context:
|
if use_admin_context:
|
||||||
search_opts_expected.update({'fake_key': 'fake_value'})
|
search_opts_expected.update({'fake_key': 'fake_value'})
|
||||||
@ -1889,6 +2025,11 @@ class ShareAPITest(test.TestCase):
|
|||||||
if (api_version.APIVersionRequest(version) >=
|
if (api_version.APIVersionRequest(version) >=
|
||||||
api_version.APIVersionRequest('2.42')):
|
api_version.APIVersionRequest('2.42')):
|
||||||
self.assertEqual(1, result['count'])
|
self.assertEqual(1, result['count'])
|
||||||
|
if (api_version.APIVersionRequest(version) >=
|
||||||
|
api_version.APIVersionRequest('2.69')):
|
||||||
|
self.assertEqual(
|
||||||
|
shares[1]['scheduled_to_be_deleted_at'],
|
||||||
|
result['shares'][0]['scheduled_to_be_deleted_at'])
|
||||||
|
|
||||||
def _list_detail_common_expected(self, admin=False):
|
def _list_detail_common_expected(self, admin=False):
|
||||||
share_dict = {
|
share_dict = {
|
||||||
@ -2530,6 +2671,7 @@ class ShareAdminActionsAPITest(test.TestCase):
|
|||||||
req.headers['X-Openstack-Manila-Api-Version'] = version
|
req.headers['X-Openstack-Manila-Api-Version'] = version
|
||||||
req.body = jsonutils.dumps(body).encode("utf-8")
|
req.body = jsonutils.dumps(body).encode("utf-8")
|
||||||
req.environ['manila.context'] = ctxt
|
req.environ['manila.context'] = ctxt
|
||||||
|
self.mock_object(share_api.API, 'get', mock.Mock(return_value=model))
|
||||||
|
|
||||||
resp = req.get_response(fakes.app())
|
resp = req.get_response(fakes.app())
|
||||||
|
|
||||||
@ -2565,7 +2707,7 @@ class ShareAdminActionsAPITest(test.TestCase):
|
|||||||
|
|
||||||
@ddt.data('2.6', '2.7')
|
@ddt.data('2.6', '2.7')
|
||||||
def test_share_reset_status_for_missing(self, version):
|
def test_share_reset_status_for_missing(self, version):
|
||||||
fake_share = {'id': 'missing-share-id'}
|
fake_share = {'id': 'missing-share-id', 'is_soft_deleted': False}
|
||||||
req = fakes.HTTPRequest.blank(
|
req = fakes.HTTPRequest.blank(
|
||||||
'/v2/fake/shares/%s/action' % fake_share['id'], version=version)
|
'/v2/fake/shares/%s/action' % fake_share['id'], version=version)
|
||||||
|
|
||||||
|
@ -55,13 +55,15 @@ class ViewBuilderTestCase(test.TestCase):
|
|||||||
'create_share_from_snapshot_support': True,
|
'create_share_from_snapshot_support': True,
|
||||||
'revert_to_snapshot_support': True,
|
'revert_to_snapshot_support': True,
|
||||||
'progress': '100%',
|
'progress': '100%',
|
||||||
|
'scheduled_to_be_deleted_at': 'fake_datetime',
|
||||||
}
|
}
|
||||||
return stubs.stub_share('fake_id', **fake_share)
|
return stubs.stub_share('fake_id', **fake_share)
|
||||||
|
|
||||||
def test__collection_name(self):
|
def test__collection_name(self):
|
||||||
self.assertEqual('shares', self.builder._collection_name)
|
self.assertEqual('shares', self.builder._collection_name)
|
||||||
|
|
||||||
@ddt.data('2.6', '2.9', '2.10', '2.11', '2.16', '2.24', '2.27', '2.54')
|
@ddt.data('2.6', '2.9', '2.10', '2.11', '2.16',
|
||||||
|
'2.24', '2.27', '2.54', '2.69')
|
||||||
def test_detail(self, microversion):
|
def test_detail(self, microversion):
|
||||||
req = fakes.HTTPRequest.blank('/shares', version=microversion)
|
req = fakes.HTTPRequest.blank('/shares', version=microversion)
|
||||||
|
|
||||||
@ -91,6 +93,8 @@ class ViewBuilderTestCase(test.TestCase):
|
|||||||
expected['revert_to_snapshot_support'] = True
|
expected['revert_to_snapshot_support'] = True
|
||||||
if self.is_microversion_ge(microversion, '2.54'):
|
if self.is_microversion_ge(microversion, '2.54'):
|
||||||
expected['progress'] = '100%'
|
expected['progress'] = '100%'
|
||||||
|
if self.is_microversion_ge(microversion, '2.69'):
|
||||||
|
expected['scheduled_to_be_deleted_at'] = 'fake_datetime'
|
||||||
|
|
||||||
self.assertSubDictMatch(expected, result['share'])
|
self.assertSubDictMatch(expected, result['share'])
|
||||||
|
|
||||||
|
@ -3044,3 +3044,43 @@ class AddUpdateSecurityServiceControlFields(BaseMigrationChecks):
|
|||||||
self.test_case.assertRaises(
|
self.test_case.assertRaises(
|
||||||
sa_exc.NoSuchTableError,
|
sa_exc.NoSuchTableError,
|
||||||
utils.load_table, 'async_operation_data', engine)
|
utils.load_table, 'async_operation_data', engine)
|
||||||
|
|
||||||
|
|
||||||
|
@map_to_migration('1946cb97bb8d')
|
||||||
|
class ShareIsSoftDeleted(BaseMigrationChecks):
|
||||||
|
|
||||||
|
def setup_upgrade_data(self, engine):
|
||||||
|
# Setup shares
|
||||||
|
share_fixture = [{'id': 'foo_share_id1'}, {'id': 'bar_share_id1'}]
|
||||||
|
share_table = utils.load_table('shares', engine)
|
||||||
|
for fixture in share_fixture:
|
||||||
|
engine.execute(share_table.insert(fixture))
|
||||||
|
|
||||||
|
# Setup share instances
|
||||||
|
si_fixture = [
|
||||||
|
{'id': 'foo_share_instance_id_oof1',
|
||||||
|
'share_id': share_fixture[0]['id'],
|
||||||
|
'cast_rules_to_readonly': False},
|
||||||
|
{'id': 'bar_share_instance_id_rab1',
|
||||||
|
'share_id': share_fixture[1]['id'],
|
||||||
|
'cast_rules_to_readonly': False},
|
||||||
|
]
|
||||||
|
si_table = utils.load_table('share_instances', engine)
|
||||||
|
for fixture in si_fixture:
|
||||||
|
engine.execute(si_table.insert(fixture))
|
||||||
|
|
||||||
|
def check_upgrade(self, engine, data):
|
||||||
|
s_table = utils.load_table('shares', engine)
|
||||||
|
for s in engine.execute(s_table.select()):
|
||||||
|
self.test_case.assertTrue(hasattr(s, 'is_soft_deleted'))
|
||||||
|
self.test_case.assertTrue(hasattr(s,
|
||||||
|
'scheduled_to_be_deleted_at'))
|
||||||
|
self.test_case.assertIn(s['is_soft_deleted'], (0, False))
|
||||||
|
self.test_case.assertIsNone(s['scheduled_to_be_deleted_at'])
|
||||||
|
|
||||||
|
def check_downgrade(self, engine):
|
||||||
|
s_table = utils.load_table('shares', engine)
|
||||||
|
for s in engine.execute(s_table.select()):
|
||||||
|
self.test_case.assertFalse(hasattr(s, 'is_soft_deleted'))
|
||||||
|
self.test_case.assertFalse(hasattr(s,
|
||||||
|
'scheduled_to_be_deleted_at'))
|
||||||
|
@ -377,6 +377,34 @@ class ShareDatabaseAPITestCase(test.TestCase):
|
|||||||
self.assertEqual(1, len(actual_result))
|
self.assertEqual(1, len(actual_result))
|
||||||
self.assertEqual(share['id'], actual_result[0].id)
|
self.assertEqual(share['id'], actual_result[0].id)
|
||||||
|
|
||||||
|
def test_share_in_recycle_bin_filter_all_by_share_server(self):
|
||||||
|
share_network = db_utils.create_share_network()
|
||||||
|
share_server = db_utils.create_share_server(
|
||||||
|
share_network_id=share_network['id'])
|
||||||
|
share = db_utils.create_share(share_server_id=share_server['id'],
|
||||||
|
share_network_id=share_network['id'],
|
||||||
|
is_soft_deleted=True)
|
||||||
|
|
||||||
|
actual_result = db_api.get_shares_in_recycle_bin_by_share_server(
|
||||||
|
self.ctxt, share_server['id'])
|
||||||
|
|
||||||
|
self.assertEqual(1, len(actual_result))
|
||||||
|
self.assertEqual(share['id'], actual_result[0].id)
|
||||||
|
|
||||||
|
def test_share_in_recycle_bin_filter_all_by_share_network(self):
|
||||||
|
share_network = db_utils.create_share_network()
|
||||||
|
share_server = db_utils.create_share_server(
|
||||||
|
share_network_id=share_network['id'])
|
||||||
|
share = db_utils.create_share(share_server_id=share_server['id'],
|
||||||
|
share_network_id=share_network['id'],
|
||||||
|
is_soft_deleted=True)
|
||||||
|
|
||||||
|
actual_result = db_api.get_shares_in_recycle_bin_by_network(
|
||||||
|
self.ctxt, share_network['id'])
|
||||||
|
|
||||||
|
self.assertEqual(1, len(actual_result))
|
||||||
|
self.assertEqual(share['id'], actual_result[0].id)
|
||||||
|
|
||||||
def test_share_filter_all_by_share_group(self):
|
def test_share_filter_all_by_share_group(self):
|
||||||
group = db_utils.create_share_group()
|
group = db_utils.create_share_group()
|
||||||
share = db_utils.create_share(share_group_id=group['id'])
|
share = db_utils.create_share(share_group_id=group['id'])
|
||||||
@ -506,6 +534,18 @@ class ShareDatabaseAPITestCase(test.TestCase):
|
|||||||
|
|
||||||
self.assertEqual('share-%s' % instance['id'], instance['name'])
|
self.assertEqual('share-%s' % instance['id'], instance['name'])
|
||||||
|
|
||||||
|
def test_share_instance_get_all_by_is_soft_deleted(self):
|
||||||
|
db_utils.create_share()
|
||||||
|
db_utils.create_share(is_soft_deleted=True)
|
||||||
|
|
||||||
|
instances = db_api.share_instances_get_all(
|
||||||
|
self.ctxt, filters={'is_soft_deleted': True})
|
||||||
|
|
||||||
|
self.assertEqual(1, len(instances))
|
||||||
|
instance = instances[0]
|
||||||
|
|
||||||
|
self.assertEqual('share-%s' % instance['id'], instance['name'])
|
||||||
|
|
||||||
def test_share_instance_get_all_by_ids(self):
|
def test_share_instance_get_all_by_ids(self):
|
||||||
fake_share = db_utils.create_share()
|
fake_share = db_utils.create_share()
|
||||||
expected_share_instance = db_utils.create_share_instance(
|
expected_share_instance = db_utils.create_share_instance(
|
||||||
@ -653,6 +693,25 @@ class ShareDatabaseAPITestCase(test.TestCase):
|
|||||||
self.assertEqual(shares[0]['id'], result[0]['id'])
|
self.assertEqual(shares[0]['id'], result[0]['id'])
|
||||||
self.assertEqual(1, len(result))
|
self.assertEqual(1, len(result))
|
||||||
|
|
||||||
|
def test_share_get_all_expired(self):
|
||||||
|
now_time = timeutils.utcnow()
|
||||||
|
time_delta = datetime.timedelta(seconds=3600)
|
||||||
|
time1 = now_time + time_delta
|
||||||
|
time2 = now_time - time_delta
|
||||||
|
share1 = db_utils.create_share(status=constants.STATUS_AVAILABLE,
|
||||||
|
is_soft_deleted=False,
|
||||||
|
scheduled_to_be_deleted_at=None)
|
||||||
|
share2 = db_utils.create_share(status=constants.STATUS_AVAILABLE,
|
||||||
|
is_soft_deleted=True,
|
||||||
|
scheduled_to_be_deleted_at=time1)
|
||||||
|
share3 = db_utils.create_share(status=constants.STATUS_AVAILABLE,
|
||||||
|
is_soft_deleted=True,
|
||||||
|
scheduled_to_be_deleted_at=time2)
|
||||||
|
shares = [share1, share2, share3]
|
||||||
|
result = db_api.get_all_expired_shares(self.ctxt)
|
||||||
|
self.assertEqual(1, len(result))
|
||||||
|
self.assertEqual(shares[2]['id'], result[0]['id'])
|
||||||
|
|
||||||
@ddt.data(
|
@ddt.data(
|
||||||
({'status': constants.STATUS_AVAILABLE}, 'status',
|
({'status': constants.STATUS_AVAILABLE}, 'status',
|
||||||
[constants.STATUS_AVAILABLE, constants.STATUS_ERROR]),
|
[constants.STATUS_AVAILABLE, constants.STATUS_ERROR]),
|
||||||
@ -669,7 +728,9 @@ class ShareDatabaseAPITestCase(test.TestCase):
|
|||||||
({'display_name': 'fake_share_name'}, 'display_name',
|
({'display_name': 'fake_share_name'}, 'display_name',
|
||||||
['fake_share_name', 'share_name']),
|
['fake_share_name', 'share_name']),
|
||||||
({'display_description': 'fake description'}, 'display_description',
|
({'display_description': 'fake description'}, 'display_description',
|
||||||
['fake description', 'description'])
|
['fake description', 'description']),
|
||||||
|
({'is_soft_deleted': True}, 'is_soft_deleted',
|
||||||
|
[True, False])
|
||||||
)
|
)
|
||||||
@ddt.unpack
|
@ddt.unpack
|
||||||
def test_share_get_all_with_filters(self, filters, key, share_values):
|
def test_share_get_all_with_filters(self, filters, key, share_values):
|
||||||
@ -1047,6 +1108,20 @@ class ShareDatabaseAPITestCase(test.TestCase):
|
|||||||
db_api.share_instance_access_get(
|
db_api.share_instance_access_get(
|
||||||
self.ctxt, rule_id, instance['id']))
|
self.ctxt, rule_id, instance['id']))
|
||||||
|
|
||||||
|
def test_share_soft_delete(self):
|
||||||
|
share = db_utils.create_share()
|
||||||
|
db_api.share_soft_delete(self.ctxt, share['id'])
|
||||||
|
share = db_api.share_get(self.ctxt, share['id'])
|
||||||
|
|
||||||
|
self.assertEqual(share['is_soft_deleted'], True)
|
||||||
|
|
||||||
|
def test_share_restore(self):
|
||||||
|
share = db_utils.create_share(is_soft_deleted=True)
|
||||||
|
db_api.share_restore(self.ctxt, share['id'])
|
||||||
|
share = db_api.share_get(self.ctxt, share['id'])
|
||||||
|
|
||||||
|
self.assertEqual(share['is_soft_deleted'], False)
|
||||||
|
|
||||||
|
|
||||||
@ddt.ddt
|
@ddt.ddt
|
||||||
class ShareGroupDatabaseAPITestCase(test.TestCase):
|
class ShareGroupDatabaseAPITestCase(test.TestCase):
|
||||||
@ -4293,6 +4368,10 @@ class ShareResourcesAPITestCase(test.TestCase):
|
|||||||
else:
|
else:
|
||||||
new_host = 'new-controller-X'
|
new_host = 'new-controller-X'
|
||||||
resources = [ # noqa
|
resources = [ # noqa
|
||||||
|
# share
|
||||||
|
db_utils.create_share_without_instance(
|
||||||
|
id=share_id,
|
||||||
|
status=constants.STATUS_AVAILABLE),
|
||||||
# share instances
|
# share instances
|
||||||
db_utils.create_share_instance(
|
db_utils.create_share_instance(
|
||||||
share_id=share_id,
|
share_id=share_id,
|
||||||
@ -4382,6 +4461,10 @@ class ShareResourcesAPITestCase(test.TestCase):
|
|||||||
+ expected_updates['groups']
|
+ expected_updates['groups']
|
||||||
+ expected_updates['servers'])
|
+ expected_updates['servers'])
|
||||||
resources = [ # noqa
|
resources = [ # noqa
|
||||||
|
# share
|
||||||
|
db_utils.create_share_without_instance(
|
||||||
|
id=share_id,
|
||||||
|
status=constants.STATUS_AVAILABLE),
|
||||||
# share instances
|
# share instances
|
||||||
db_utils.create_share_instance(
|
db_utils.create_share_instance(
|
||||||
share_id=share_id,
|
share_id=share_id,
|
||||||
|
@ -91,7 +91,8 @@ def create_share(**kwargs):
|
|||||||
'metadata': {'fake_key': 'fake_value'},
|
'metadata': {'fake_key': 'fake_value'},
|
||||||
'availability_zone': 'fake_availability_zone',
|
'availability_zone': 'fake_availability_zone',
|
||||||
'status': constants.STATUS_CREATING,
|
'status': constants.STATUS_CREATING,
|
||||||
'host': 'fake_host'
|
'host': 'fake_host',
|
||||||
|
'is_soft_deleted': False
|
||||||
}
|
}
|
||||||
return _create_db_row(db.share_create, share, kwargs)
|
return _create_db_row(db.share_create, share, kwargs)
|
||||||
|
|
||||||
@ -108,7 +109,8 @@ def create_share_without_instance(**kwargs):
|
|||||||
'metadata': {},
|
'metadata': {},
|
||||||
'availability_zone': 'fake_availability_zone',
|
'availability_zone': 'fake_availability_zone',
|
||||||
'status': constants.STATUS_CREATING,
|
'status': constants.STATUS_CREATING,
|
||||||
'host': 'fake_host'
|
'host': 'fake_host',
|
||||||
|
'is_soft_deleted': False
|
||||||
}
|
}
|
||||||
share.update(copy.deepcopy(kwargs))
|
share.update(copy.deepcopy(kwargs))
|
||||||
return db.share_create(context.get_admin_context(), share, False)
|
return db.share_create(context.get_admin_context(), share, False)
|
||||||
|
@ -4663,6 +4663,9 @@ class ShareAPITestCase(test.TestCase):
|
|||||||
mock_shares_get_all = self.mock_object(
|
mock_shares_get_all = self.mock_object(
|
||||||
db_api, 'share_get_all_by_share_server',
|
db_api, 'share_get_all_by_share_server',
|
||||||
mock.Mock(return_value=[fake_share]))
|
mock.Mock(return_value=[fake_share]))
|
||||||
|
mock_shares_in_recycle_bin_get_all = self.mock_object(
|
||||||
|
db_api, 'get_shares_in_recycle_bin_by_share_server',
|
||||||
|
mock.Mock(return_value=[]))
|
||||||
mock_get_type = self.mock_object(
|
mock_get_type = self.mock_object(
|
||||||
share_types, 'get_share_type', mock.Mock(return_value=share_type))
|
share_types, 'get_share_type', mock.Mock(return_value=share_type))
|
||||||
mock_validate_service = self.mock_object(
|
mock_validate_service = self.mock_object(
|
||||||
@ -4691,6 +4694,8 @@ class ShareAPITestCase(test.TestCase):
|
|||||||
mock_shares_get_all.assert_has_calls([
|
mock_shares_get_all.assert_has_calls([
|
||||||
mock.call(self.context, fake_share_server['id']),
|
mock.call(self.context, fake_share_server['id']),
|
||||||
mock.call(self.context, fake_share_server['id'])])
|
mock.call(self.context, fake_share_server['id'])])
|
||||||
|
mock_shares_in_recycle_bin_get_all.assert_has_calls([
|
||||||
|
mock.call(self.context, fake_share_server['id'])])
|
||||||
mock_get_type.assert_called_once_with(self.context, share_type['id'])
|
mock_get_type.assert_called_once_with(self.context, share_type['id'])
|
||||||
mock_validate_service.assert_called_once_with(self.context, fake_host)
|
mock_validate_service.assert_called_once_with(self.context, fake_host)
|
||||||
mock_service_get.assert_called_once_with(
|
mock_service_get.assert_called_once_with(
|
||||||
@ -6279,6 +6284,79 @@ class ShareAPITestCase(test.TestCase):
|
|||||||
new_sec_service_id,
|
new_sec_service_id,
|
||||||
current_security_service_id=curr_sec_service_id)
|
current_security_service_id=curr_sec_service_id)
|
||||||
|
|
||||||
|
def test_soft_delete_share_already_soft_deleted(self):
|
||||||
|
share = fakes.fake_share(id='fake_id',
|
||||||
|
status=constants.STATUS_AVAILABLE,
|
||||||
|
is_soft_deleted=True)
|
||||||
|
self.assertRaises(exception.InvalidShare,
|
||||||
|
self.api.soft_delete, self.context, share)
|
||||||
|
|
||||||
|
def test_soft_delete_invalid_status(self):
|
||||||
|
invalid_status = 'fake'
|
||||||
|
share = fakes.fake_share(id='fake_id',
|
||||||
|
status=invalid_status,
|
||||||
|
is_soft_deleted=False)
|
||||||
|
|
||||||
|
self.assertRaises(exception.InvalidShare,
|
||||||
|
self.api.soft_delete, self.context, share)
|
||||||
|
|
||||||
|
def test_soft_delete_share_with_replicas(self):
|
||||||
|
share = fakes.fake_share(id='fake_id',
|
||||||
|
has_replicas=True,
|
||||||
|
status=constants.STATUS_AVAILABLE,
|
||||||
|
is_soft_deleted=False)
|
||||||
|
|
||||||
|
self.assertRaises(exception.Conflict,
|
||||||
|
self.api.soft_delete, self.context, share)
|
||||||
|
|
||||||
|
def test_soft_delete_share_with_snapshot(self):
|
||||||
|
share = fakes.fake_share(id='fake_id',
|
||||||
|
status=constants.STATUS_AVAILABLE,
|
||||||
|
has_replicas=False,
|
||||||
|
is_soft_deleted=False)
|
||||||
|
snapshot = fakes.fake_snapshot(create_instance=True, as_primitive=True)
|
||||||
|
mock_db_snapshot_call = self.mock_object(
|
||||||
|
db_api, 'share_snapshot_get_all_for_share', mock.Mock(
|
||||||
|
return_value=[snapshot]))
|
||||||
|
|
||||||
|
self.assertRaises(exception.InvalidShare,
|
||||||
|
self.api.soft_delete, self.context, share)
|
||||||
|
|
||||||
|
mock_db_snapshot_call.assert_called_once_with(
|
||||||
|
self.context, share['id'])
|
||||||
|
|
||||||
|
@mock.patch.object(db_api, 'count_share_group_snapshot_members_in_share',
|
||||||
|
mock.Mock(return_value=2))
|
||||||
|
def test_soft_delete_share_with_group_snapshot_members(self):
|
||||||
|
share = fakes.fake_share(id='fake_id',
|
||||||
|
status=constants.STATUS_AVAILABLE,
|
||||||
|
has_replicas=False,
|
||||||
|
is_soft_deleted=False)
|
||||||
|
|
||||||
|
self.assertRaises(exception.InvalidShare,
|
||||||
|
self.api.soft_delete, self.context, share)
|
||||||
|
|
||||||
|
def test_soft_delete_share(self):
|
||||||
|
share = fakes.fake_share(id='fake_id',
|
||||||
|
status=constants.STATUS_AVAILABLE,
|
||||||
|
has_replicas=False,
|
||||||
|
is_soft_deleted=False)
|
||||||
|
self.mock_object(db_api, 'share_snapshot_get_all_for_share',
|
||||||
|
mock.Mock(return_value=[]))
|
||||||
|
self.mock_object(db_api, 'count_share_group_snapshot_members_in_share',
|
||||||
|
mock.Mock(return_value=0))
|
||||||
|
self.mock_object(db_api, 'share_soft_delete')
|
||||||
|
self.mock_object(self.api, '_check_is_share_busy')
|
||||||
|
self.api.soft_delete(self.context, share)
|
||||||
|
self.api._check_is_share_busy.assert_called_once_with(share)
|
||||||
|
|
||||||
|
def test_restore_share(self):
|
||||||
|
share = fakes.fake_share(id='fake_id',
|
||||||
|
status=constants.STATUS_AVAILABLE,
|
||||||
|
is_soft_deleted=True)
|
||||||
|
self.mock_object(db_api, 'share_restore')
|
||||||
|
self.api.restore(self.context, share)
|
||||||
|
|
||||||
|
|
||||||
class OtherTenantsShareActionsTestCase(test.TestCase):
|
class OtherTenantsShareActionsTestCase(test.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -208,6 +208,7 @@ class ShareManagerTestCase(test.TestCase):
|
|||||||
"unmanage_share",
|
"unmanage_share",
|
||||||
"delete_share_instance",
|
"delete_share_instance",
|
||||||
"delete_free_share_servers",
|
"delete_free_share_servers",
|
||||||
|
"delete_expired_share",
|
||||||
"create_snapshot",
|
"create_snapshot",
|
||||||
"delete_snapshot",
|
"delete_snapshot",
|
||||||
"update_access",
|
"update_access",
|
||||||
@ -3938,6 +3939,18 @@ class ShareManagerTestCase(test.TestCase):
|
|||||||
'server1')
|
'server1')
|
||||||
timeutils.utcnow.assert_called_once_with()
|
timeutils.utcnow.assert_called_once_with()
|
||||||
|
|
||||||
|
@mock.patch.object(db, 'get_all_expired_shares',
|
||||||
|
mock.Mock(return_value=[{"id": "share1"}, ]))
|
||||||
|
@mock.patch.object(api.API, 'delete',
|
||||||
|
mock.Mock())
|
||||||
|
def test_delete_expired_share(self):
|
||||||
|
self.share_manager.delete_expired_share(self.context)
|
||||||
|
db.get_all_expired_shares.assert_called_once_with(
|
||||||
|
self.context)
|
||||||
|
share1 = {"id": "share1"}
|
||||||
|
api.API.delete.assert_called_once_with(
|
||||||
|
self.context, share1, force=True)
|
||||||
|
|
||||||
@mock.patch('manila.tests.fake_notifier.FakeNotifier._notify')
|
@mock.patch('manila.tests.fake_notifier.FakeNotifier._notify')
|
||||||
def test_extend_share_invalid(self, mock_notify):
|
def test_extend_share_invalid(self, mock_notify):
|
||||||
share = db_utils.create_share()
|
share = db_utils.create_share()
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Manila now supports a "recycle bin" for shares. End users can soft-delete
|
||||||
|
their shares and have the ability to restore them for a specified interval.
|
||||||
|
This interval defaults to 7 days and is configurable via
|
||||||
|
"soft_deleted_share_retention_time". After this time has elapsed,
|
||||||
|
soft-deleted shares are automatically cleaned up.
|
||||||
|
upgrade:
|
||||||
|
- |
|
||||||
|
The share entity now contains two new fields: ``is_soft_deleted`` and
|
||||||
|
``scheduled_to_be_deleted_at``. The ``is_soft_deleted`` will be used to
|
||||||
|
identify shares in the recycle bin.. The ``scheduled_to_be_deleted_at``
|
||||||
|
field to show when the share will be deleted automatically. A new parameter
|
||||||
|
called ``is_soft_deleted`` was added to the share list API, and users will
|
||||||
|
be able to query shares and filter out the ones that are currently in the
|
||||||
|
recycle bin.
|
Loading…
Reference in New Issue
Block a user