Merge "Separate APIs for share & replica export locations"
This commit is contained in:
commit
c88c2fd02f
@ -38,6 +38,7 @@ Shared File Systems API (EXPERIMENTAL)
|
||||
.. include:: experimental.inc
|
||||
.. include:: share-migration.inc
|
||||
.. include:: share-replicas.inc
|
||||
.. include:: share-replica-export-locations.inc
|
||||
.. include:: share-groups.inc
|
||||
.. include:: share-group-types.inc
|
||||
.. include:: share-group-snapshots.inc
|
||||
|
@ -1178,6 +1178,12 @@ export_location:
|
||||
required: false
|
||||
type: string
|
||||
max_version: 2.8
|
||||
export_location_availability_zone:
|
||||
description: |
|
||||
The name of the availability zone that the export location belongs to.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
export_location_created_at:
|
||||
description: |
|
||||
The date and time stamp when the share export location was
|
||||
@ -1208,7 +1214,9 @@ export_location_is_admin_only:
|
||||
Defines purpose of an export location. If set to
|
||||
``true``, then it is expected to be used for service needs and by
|
||||
administrators only. If it is set to ``false``, then this export
|
||||
location can be used by end users.
|
||||
location can be used by end users. This parameter is only available to
|
||||
users with an "administrator" role, and cannot be controlled via policy
|
||||
.json.
|
||||
in: body
|
||||
required: true
|
||||
type: boolean
|
||||
@ -1227,10 +1235,19 @@ export_location_preferred:
|
||||
required: true
|
||||
type: boolean
|
||||
min_version: 2.14
|
||||
export_location_preferred_replicas:
|
||||
description: |
|
||||
Drivers may use this field to identify which export locations
|
||||
are most efficient and should be used preferentially by clients.
|
||||
By default it is set to ``false`` value.
|
||||
in: body
|
||||
required: true
|
||||
type: boolean
|
||||
export_location_share_instance_id:
|
||||
description: |
|
||||
The UUID of the share instance that this
|
||||
export location belongs to.
|
||||
export location belongs to. This parameter is only available to users
|
||||
with an "administrator" role, and cannot be controlled via policy.json.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
|
@ -0,0 +1,22 @@
|
||||
{
|
||||
"export_locations": [
|
||||
{
|
||||
"path": "10.254.0.3:/shares/share-e1c2d35e-fe67-4028-ad7a-45f668732b1d",
|
||||
"share_instance_id": "e1c2d35e-fe67-4028-ad7a-45f668732b1d",
|
||||
"is_admin_only": false,
|
||||
"id": "b6bd76ce-12a2-42a9-a30a-8a43b503867d",
|
||||
"preferred": false,
|
||||
"replica_state": "in_sync",
|
||||
"availability_zone": "paris"
|
||||
},
|
||||
{
|
||||
"path": "10.0.0.3:/shares/share-e1c2d35e-fe67-4028-ad7a-45f668732b1d",
|
||||
"share_instance_id": "e1c2d35e-fe67-4028-ad7a-45f668732b1d",
|
||||
"is_admin_only": true,
|
||||
"id": "6921e862-88bc-49a5-a2df-efeed9acd583",
|
||||
"preferred": false,
|
||||
"replica_state": "in_sync",
|
||||
"availability_zone": "paris"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"export_location": {
|
||||
"created_at": "2016-03-24T14:20:47.000000",
|
||||
"updated_at": "2016-03-24T14:20:47.000000",
|
||||
"preferred": false,
|
||||
"is_admin_only": true,
|
||||
"share_instance_id": "e1c2d35e-fe67-4028-ad7a-45f668732b1d",
|
||||
"path": "10.0.0.3:/shares/share-e1c2d35e-fe67-4028-ad7a-45f668732b1d",
|
||||
"id": "6921e862-88bc-49a5-a2df-efeed9acd583",
|
||||
"replica_state": "in_sync",
|
||||
"availability_zone": "paris"
|
||||
}
|
||||
}
|
@ -6,8 +6,10 @@ Share export locations (since API v2.9)
|
||||
|
||||
Set of APIs used for viewing export locations of shares.
|
||||
|
||||
By default, these APIs are admin-only. Use the ``policy.json`` file
|
||||
to grant permissions for these actions to other roles.
|
||||
These APIs allow retrieval of export locations belonging to non-active share
|
||||
replicas until API version 2.46. In and beyond API version 2.47, export
|
||||
locations of non-active share replicas can only be retrieved using the
|
||||
:ref:`Share Replica Export Locations APIs <share_replica_export_locations>`.
|
||||
|
||||
|
||||
List export locations
|
||||
|
106
api-ref/source/share-replica-export-locations.inc
Normal file
106
api-ref/source/share-replica-export-locations.inc
Normal file
@ -0,0 +1,106 @@
|
||||
.. -*- rst -*-
|
||||
|
||||
.. _share_replica_export_locations:
|
||||
|
||||
================================================
|
||||
Share replica export locations (since API v2.47)
|
||||
================================================
|
||||
|
||||
Set of APIs used to view export locations of share replicas.
|
||||
|
||||
List export locations
|
||||
=====================
|
||||
|
||||
.. rest_method:: GET /v2/{tenant_id}/share-replicas/{share_replica_id}/export-locations
|
||||
|
||||
Response codes
|
||||
--------------
|
||||
|
||||
.. rest_status_code:: success status.yaml
|
||||
|
||||
- 200
|
||||
|
||||
.. rest_status_code:: error status.yaml
|
||||
|
||||
- 400
|
||||
- 401
|
||||
- 403
|
||||
- 404
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- tenant_id: tenant_id_path
|
||||
- share_replica_id: share_replica_id_path
|
||||
|
||||
Response parameters
|
||||
-------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- id: export_location_id
|
||||
- share_instance_id: export_location_share_instance_id
|
||||
- path: export_location_path
|
||||
- is_admin_only: export_location_is_admin_only
|
||||
- preferred: export_location_preferred_replicas
|
||||
- availability_zone: export_location_availability_zone
|
||||
- replica_state: share_replica_replica_state
|
||||
|
||||
Response example
|
||||
----------------
|
||||
|
||||
.. literalinclude:: samples/share-replica-export-location-list-response.json
|
||||
:language: javascript
|
||||
|
||||
|
||||
Show single export location
|
||||
===========================
|
||||
|
||||
.. rest_method:: GET /v2/{tenant_id}/share-replicas/{share_replica_id}/export-locations/{export-location-id}
|
||||
|
||||
|
||||
Response codes
|
||||
--------------
|
||||
|
||||
.. rest_status_code:: success status.yaml
|
||||
|
||||
- 200
|
||||
|
||||
.. rest_status_code:: error status.yaml
|
||||
|
||||
- 400
|
||||
- 401
|
||||
- 403
|
||||
- 404
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- tenant_id: tenant_id_path
|
||||
- share_replica_id: share_replica_id_path
|
||||
- export_location_id: export_location_id_path
|
||||
|
||||
Response parameters
|
||||
-------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- id: export_location_id
|
||||
- share_instance_id: export_location_share_instance_id
|
||||
- path: export_location_path
|
||||
- is_admin_only: export_location_is_admin_only
|
||||
- preferred: export_location_preferred_replicas
|
||||
- availability_zone: export_location_availability_zone
|
||||
- replica_state: share_replica_replica_state
|
||||
- created_at: export_location_created_at
|
||||
- updated_at: export_location_updated_at
|
||||
|
||||
Response example
|
||||
----------------
|
||||
|
||||
.. literalinclude:: samples/share-replica-export-location-show-response.json
|
||||
:language: javascript
|
@ -121,13 +121,21 @@ REST_API_VERSION_HISTORY = """
|
||||
access rules will not work with API version >=2.45.
|
||||
* 2.46 - Added 'is_default' field to 'share_type' and 'share_group_type'
|
||||
objects.
|
||||
* 2.47 - Export locations for non-active share replicas are no longer
|
||||
retrievable through the export locations APIs:
|
||||
GET /v2/{tenant_id}/shares/{share_id}/export_locations and
|
||||
GET /v2/{tenant_id}/shares/{share_id}/export_locations/{
|
||||
export_location_id}. A new API is introduced at this
|
||||
version: GET /v2/{tenant_id}/share-replicas/{
|
||||
replica_id}/export-locations to allow retrieving individual
|
||||
replica export locations if available.
|
||||
"""
|
||||
|
||||
# The minimum and maximum versions of the API supported
|
||||
# The default api version request is defined to be the
|
||||
# minimum version of the API supported.
|
||||
_MIN_API_VERSION = "2.0"
|
||||
_MAX_API_VERSION = "2.46"
|
||||
_MAX_API_VERSION = "2.47"
|
||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||
|
||||
|
||||
|
@ -253,3 +253,14 @@ user documentation.
|
||||
-----------------------
|
||||
Added 'is_default' field to 'share_type' and 'share_group_type'
|
||||
objects.
|
||||
|
||||
2.47
|
||||
----
|
||||
|
||||
Export locations for non-active share replicas are no longer retrievable
|
||||
through the export locations APIs: ``GET
|
||||
/v2/{tenant_id}/shares/{share_id}/export_locations`` and ``GET
|
||||
/v2/{tenant_id}/shares/{share_id}/export_locations/{export_location_id}``.
|
||||
A new API is introduced at this version: ``GET
|
||||
/v2/{tenant_id}/share-replicas/{replica_id}/export-locations`` to allow
|
||||
retrieving export locations of share replicas if available.
|
||||
|
@ -45,6 +45,7 @@ from manila.api.v2 import share_groups
|
||||
from manila.api.v2 import share_instance_export_locations
|
||||
from manila.api.v2 import share_instances
|
||||
from manila.api.v2 import share_networks
|
||||
from manila.api.v2 import share_replica_export_locations
|
||||
from manila.api.v2 import share_replicas
|
||||
from manila.api.v2 import share_snapshot_export_locations
|
||||
from manila.api.v2 import share_snapshot_instance_export_locations
|
||||
@ -413,6 +414,22 @@ class APIRouter(manila.api.openstack.APIRouter):
|
||||
controller=self.resources['share-replicas'],
|
||||
collection={'detail': 'GET'},
|
||||
member={'action': 'POST'})
|
||||
self.resources["share-replica-export-locations"] = (
|
||||
share_replica_export_locations.create_resource())
|
||||
mapper.connect("share-replicas",
|
||||
("/{project_id}/share-replicas/{share_replica_id}/"
|
||||
"export-locations"),
|
||||
controller=self.resources[
|
||||
"share-replica-export-locations"],
|
||||
action="index",
|
||||
conditions={"method": ["GET"]})
|
||||
mapper.connect("share-replicas",
|
||||
("/{project_id}/share-replicas/{share_replica_id}/"
|
||||
"export-locations/{export_location_uuid}"),
|
||||
controller=self.resources[
|
||||
"share-replica-export-locations"],
|
||||
action="show",
|
||||
conditions={"method": ["GET"]})
|
||||
|
||||
self.resources['messages'] = messages.create_resource()
|
||||
mapper.resource("message", "messages",
|
||||
|
@ -37,27 +37,28 @@ class ShareExportLocationController(wsgi.Controller):
|
||||
msg = _("Share '%s' not found.") % share_id
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
@wsgi.Controller.api_version('2.9')
|
||||
@wsgi.Controller.authorize
|
||||
def index(self, req, share_id):
|
||||
"""Return a list of export locations for share."""
|
||||
|
||||
@wsgi.Controller.authorize('index')
|
||||
def _index(self, req, share_id, ignore_secondary_replicas=False):
|
||||
context = req.environ['manila.context']
|
||||
self._verify_share(context, share_id)
|
||||
kwargs = {
|
||||
'include_admin_only': context.is_admin,
|
||||
'ignore_migration_destination': True,
|
||||
'ignore_secondary_replicas': ignore_secondary_replicas,
|
||||
}
|
||||
export_locations = db_api.share_export_locations_get_by_share_id(
|
||||
context, share_id, include_admin_only=context.is_admin,
|
||||
ignore_migration_destination=True)
|
||||
context, share_id, **kwargs)
|
||||
return self._view_builder.summary_list(req, export_locations)
|
||||
|
||||
@wsgi.Controller.api_version('2.9')
|
||||
@wsgi.Controller.authorize
|
||||
def show(self, req, share_id, export_location_uuid):
|
||||
"""Return data about the requested export location."""
|
||||
@wsgi.Controller.authorize('show')
|
||||
def _show(self, req, share_id, export_location_uuid,
|
||||
ignore_secondary_replicas=False):
|
||||
context = req.environ['manila.context']
|
||||
self._verify_share(context, share_id)
|
||||
try:
|
||||
export_location = db_api.share_export_location_get_by_uuid(
|
||||
context, export_location_uuid)
|
||||
context, export_location_uuid,
|
||||
ignore_secondary_replicas=ignore_secondary_replicas)
|
||||
except exception.ExportLocationNotFound:
|
||||
msg = _("Export location '%s' not found.") % export_location_uuid
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
@ -67,6 +68,28 @@ class ShareExportLocationController(wsgi.Controller):
|
||||
|
||||
return self._view_builder.detail(req, export_location)
|
||||
|
||||
@wsgi.Controller.api_version('2.9', '2.46')
|
||||
def index(self, req, share_id):
|
||||
"""Return a list of export locations for share."""
|
||||
return self._index(req, share_id)
|
||||
|
||||
@wsgi.Controller.api_version('2.47') # noqa: F811
|
||||
def index(self, req, share_id):
|
||||
"""Return a list of export locations for share."""
|
||||
return self._index(req, share_id,
|
||||
ignore_secondary_replicas=True)
|
||||
|
||||
@wsgi.Controller.api_version('2.9', '2.46')
|
||||
def show(self, req, share_id, export_location_uuid):
|
||||
"""Return data about the requested export location."""
|
||||
return self._show(req, share_id, export_location_uuid)
|
||||
|
||||
@wsgi.Controller.api_version('2.47') # noqa: F811
|
||||
def show(self, req, share_id, export_location_uuid):
|
||||
"""Return data about the requested export location."""
|
||||
return self._show(req, share_id, export_location_uuid,
|
||||
ignore_secondary_replicas=True)
|
||||
|
||||
|
||||
def create_resource():
|
||||
return wsgi.Resource(ShareExportLocationController())
|
||||
|
70
manila/api/v2/share_replica_export_locations.py
Normal file
70
manila/api/v2/share_replica_export_locations.py
Normal file
@ -0,0 +1,70 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import six
|
||||
from webob import exc
|
||||
|
||||
from manila.api.openstack import wsgi
|
||||
from manila.api.views import export_locations as export_locations_views
|
||||
from manila.db import api as db_api
|
||||
from manila import exception
|
||||
from manila.i18n import _
|
||||
|
||||
|
||||
class ShareReplicaExportLocationController(wsgi.Controller):
|
||||
"""The Share Instance Export Locations API controller."""
|
||||
|
||||
def __init__(self):
|
||||
self._view_builder_class = export_locations_views.ViewBuilder
|
||||
self.resource_name = 'share_replica_export_location'
|
||||
super(ShareReplicaExportLocationController, self).__init__()
|
||||
|
||||
def _verify_share_replica(self, context, share_replica_id):
|
||||
try:
|
||||
db_api.share_replica_get(context, share_replica_id)
|
||||
except exception.NotFound:
|
||||
msg = _("Share replica '%s' not found.") % share_replica_id
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
@wsgi.Controller.api_version('2.47', experimental=True)
|
||||
@wsgi.Controller.authorize
|
||||
def index(self, req, share_replica_id):
|
||||
"""Return a list of export locations for the share instance."""
|
||||
context = req.environ['manila.context']
|
||||
self._verify_share_replica(context, share_replica_id)
|
||||
export_locations = (
|
||||
db_api.share_export_locations_get_by_share_instance_id(
|
||||
context, share_replica_id,
|
||||
include_admin_only=context.is_admin)
|
||||
)
|
||||
return self._view_builder.summary_list(req, export_locations,
|
||||
replica=True)
|
||||
|
||||
@wsgi.Controller.api_version('2.47', experimental=True)
|
||||
@wsgi.Controller.authorize
|
||||
def show(self, req, share_replica_id, export_location_uuid):
|
||||
"""Return data about the requested export location."""
|
||||
context = req.environ['manila.context']
|
||||
self._verify_share_replica(context, share_replica_id)
|
||||
try:
|
||||
export_location = db_api.share_export_location_get_by_uuid(
|
||||
context, export_location_uuid)
|
||||
return self._view_builder.detail(req, export_location,
|
||||
replica=True)
|
||||
except exception.ExportLocationNotFound as e:
|
||||
raise exc.HTTPNotFound(explanation=six.text_type(e))
|
||||
|
||||
|
||||
def create_resource():
|
||||
return wsgi.Resource(ShareReplicaExportLocationController())
|
@ -28,7 +28,7 @@ class ViewBuilder(common.ViewBuilder):
|
||||
]
|
||||
|
||||
def _get_export_location_view(self, request, export_location,
|
||||
detail=False):
|
||||
detail=False, replica=False):
|
||||
|
||||
context = request.environ['manila.context']
|
||||
|
||||
@ -38,43 +38,49 @@ class ViewBuilder(common.ViewBuilder):
|
||||
}
|
||||
self.update_versioned_resource_dict(request, view, export_location)
|
||||
if context.is_admin:
|
||||
view['share_instance_id'] = export_location[
|
||||
'share_instance_id']
|
||||
view['share_instance_id'] = export_location['share_instance_id']
|
||||
view['is_admin_only'] = export_location['is_admin_only']
|
||||
|
||||
if detail:
|
||||
view['created_at'] = export_location['created_at']
|
||||
view['updated_at'] = export_location['updated_at']
|
||||
|
||||
if replica:
|
||||
share_instance = export_location['share_instance']
|
||||
view['replica_state'] = share_instance['replica_state']
|
||||
view['availability_zone'] = share_instance['availability_zone']
|
||||
|
||||
return {'export_location': view}
|
||||
|
||||
def summary(self, request, export_location):
|
||||
def summary(self, request, export_location, replica=False):
|
||||
"""Summary view of a single export location."""
|
||||
return self._get_export_location_view(request, export_location,
|
||||
detail=False)
|
||||
return self._get_export_location_view(
|
||||
request, export_location, detail=False, replica=replica)
|
||||
|
||||
def detail(self, request, export_location):
|
||||
def detail(self, request, export_location, replica=False):
|
||||
"""Detailed view of a single export location."""
|
||||
return self._get_export_location_view(request, export_location,
|
||||
detail=True)
|
||||
return self._get_export_location_view(
|
||||
request, export_location, detail=True, replica=replica)
|
||||
|
||||
def _list_export_locations(self, request, export_locations, detail=False):
|
||||
def _list_export_locations(self, req, export_locations,
|
||||
detail=False, replica=False):
|
||||
"""View of export locations list."""
|
||||
view_method = self.detail if detail else self.summary
|
||||
return {self._collection_name: [
|
||||
view_method(request, export_location)['export_location']
|
||||
for export_location in export_locations
|
||||
]}
|
||||
return {
|
||||
self._collection_name: [
|
||||
view_method(req, elocation, replica=replica)['export_location']
|
||||
for elocation in export_locations
|
||||
]}
|
||||
|
||||
def detail_list(self, request, export_locations):
|
||||
"""Detailed View of export locations list."""
|
||||
return self._list_export_locations(request, export_locations,
|
||||
detail=True)
|
||||
|
||||
def summary_list(self, request, export_locations):
|
||||
def summary_list(self, request, export_locations, replica=False):
|
||||
"""Summary View of export locations list."""
|
||||
return self._list_export_locations(request, export_locations,
|
||||
detail=False)
|
||||
detail=False, replica=replica)
|
||||
|
||||
@common.ViewBuilder.versioned_method('2.14')
|
||||
def add_preferred_path_attribute(self, context, view_dict,
|
||||
|
@ -728,10 +728,12 @@ def share_metadata_update(context, share, metadata, delete):
|
||||
|
||||
###################
|
||||
|
||||
def share_export_location_get_by_uuid(context, export_location_uuid):
|
||||
def share_export_location_get_by_uuid(context, export_location_uuid,
|
||||
ignore_secondary_replicas=False):
|
||||
"""Get specific export location of a share."""
|
||||
return IMPL.share_export_location_get_by_uuid(
|
||||
context, export_location_uuid)
|
||||
context, export_location_uuid,
|
||||
ignore_secondary_replicas=ignore_secondary_replicas)
|
||||
|
||||
|
||||
def share_export_locations_get(context, share_id):
|
||||
@ -741,18 +743,21 @@ def share_export_locations_get(context, share_id):
|
||||
|
||||
def share_export_locations_get_by_share_id(context, share_id,
|
||||
include_admin_only=True,
|
||||
ignore_migration_destination=False):
|
||||
ignore_migration_destination=False,
|
||||
ignore_secondary_replicas=False):
|
||||
"""Get all export locations of a share by its ID."""
|
||||
return IMPL.share_export_locations_get_by_share_id(
|
||||
context, share_id, include_admin_only=include_admin_only,
|
||||
ignore_migration_destination=ignore_migration_destination)
|
||||
ignore_migration_destination=ignore_migration_destination,
|
||||
ignore_secondary_replicas=ignore_secondary_replicas)
|
||||
|
||||
|
||||
def share_export_locations_get_by_share_instance_id(context,
|
||||
share_instance_id):
|
||||
share_instance_id,
|
||||
include_admin_only=True):
|
||||
"""Get all export locations of a share instance by its ID."""
|
||||
return IMPL.share_export_locations_get_by_share_instance_id(
|
||||
context, share_instance_id)
|
||||
context, share_instance_id, include_admin_only=include_admin_only)
|
||||
|
||||
|
||||
def share_export_locations_update(context, share_instance_id, export_locations,
|
||||
|
@ -3005,7 +3005,8 @@ def _share_metadata_get_item(context, share_id, key, session=None):
|
||||
############################
|
||||
|
||||
def _share_export_locations_get(context, share_instance_ids,
|
||||
include_admin_only=True, session=None):
|
||||
include_admin_only=True,
|
||||
ignore_secondary_replicas=False, session=None):
|
||||
session = session or get_session()
|
||||
|
||||
if not isinstance(share_instance_ids, (set, list, tuple)):
|
||||
@ -3027,6 +3028,13 @@ def _share_export_locations_get(context, share_instance_ids,
|
||||
|
||||
if not include_admin_only:
|
||||
query = query.filter_by(is_admin_only=False)
|
||||
|
||||
if ignore_secondary_replicas:
|
||||
replica_state_attr = models.ShareInstance.replica_state
|
||||
query = query.join("share_instance").filter(
|
||||
or_(replica_state_attr == None, # noqa
|
||||
replica_state_attr == constants.REPLICA_STATE_ACTIVE))
|
||||
|
||||
return query.all()
|
||||
|
||||
|
||||
@ -3034,7 +3042,8 @@ def _share_export_locations_get(context, share_instance_ids,
|
||||
@require_share_exists
|
||||
def share_export_locations_get_by_share_id(context, share_id,
|
||||
include_admin_only=True,
|
||||
ignore_migration_destination=False):
|
||||
ignore_migration_destination=False,
|
||||
ignore_secondary_replicas=False):
|
||||
share = share_get(context, share_id)
|
||||
if ignore_migration_destination:
|
||||
ids = [instance.id for instance in share.instances
|
||||
@ -3042,16 +3051,18 @@ def share_export_locations_get_by_share_id(context, share_id,
|
||||
else:
|
||||
ids = [instance.id for instance in share.instances]
|
||||
rows = _share_export_locations_get(
|
||||
context, ids, include_admin_only=include_admin_only)
|
||||
context, ids, include_admin_only=include_admin_only,
|
||||
ignore_secondary_replicas=ignore_secondary_replicas)
|
||||
return rows
|
||||
|
||||
|
||||
@require_context
|
||||
@require_share_instance_exists
|
||||
def share_export_locations_get_by_share_instance_id(context,
|
||||
share_instance_id):
|
||||
share_instance_id,
|
||||
include_admin_only=True):
|
||||
rows = _share_export_locations_get(
|
||||
context, [share_instance_id], include_admin_only=True)
|
||||
context, [share_instance_id], include_admin_only=include_admin_only)
|
||||
return rows
|
||||
|
||||
|
||||
@ -3070,6 +3081,7 @@ def share_export_locations_get(context, share_id):
|
||||
|
||||
@require_context
|
||||
def share_export_location_get_by_uuid(context, export_location_uuid,
|
||||
ignore_secondary_replicas=False,
|
||||
session=None):
|
||||
session = session or get_session()
|
||||
|
||||
@ -3084,6 +3096,12 @@ def share_export_location_get_by_uuid(context, export_location_uuid,
|
||||
joinedload("_el_metadata_bare"),
|
||||
)
|
||||
|
||||
if ignore_secondary_replicas:
|
||||
replica_state_attr = models.ShareInstance.replica_state
|
||||
query = query.join("share_instance").filter(
|
||||
or_(replica_state_attr == None, # noqa
|
||||
replica_state_attr == constants.REPLICA_STATE_ACTIVE))
|
||||
|
||||
result = query.first()
|
||||
if not result:
|
||||
raise exception.ExportLocationNotFound(uuid=export_location_uuid)
|
||||
|
@ -400,7 +400,8 @@ class ShareInstance(BASE, ManilaBase):
|
||||
|
||||
export_locations = orm.relationship(
|
||||
"ShareInstanceExportLocations",
|
||||
lazy='immediate',
|
||||
lazy='joined',
|
||||
backref=orm.backref('share_instance', lazy='joined'),
|
||||
primaryjoin=(
|
||||
'and_('
|
||||
'ShareInstance.id == '
|
||||
@ -434,6 +435,10 @@ class ShareInstanceExportLocations(BASE, ManilaBase):
|
||||
el_metadata[meta['key']] = meta['value']
|
||||
return el_metadata
|
||||
|
||||
@property
|
||||
def replica_state(self):
|
||||
return self.share_instance['replica_state']
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
uuid = Column(String(36), nullable=False, unique=True)
|
||||
share_instance_id = Column(
|
||||
|
@ -35,6 +35,7 @@ from manila.policies import share_instance
|
||||
from manila.policies import share_instance_export_location
|
||||
from manila.policies import share_network
|
||||
from manila.policies import share_replica
|
||||
from manila.policies import share_replica_export_location
|
||||
from manila.policies import share_server
|
||||
from manila.policies import share_snapshot
|
||||
from manila.policies import share_snapshot_export_location
|
||||
@ -67,6 +68,7 @@ def list_rules():
|
||||
share_group_snapshot.list_rules(),
|
||||
share_group.list_rules(),
|
||||
share_replica.list_rules(),
|
||||
share_replica_export_location.list_rules(),
|
||||
share_network.list_rules(),
|
||||
security_service.list_rules(),
|
||||
share_export_location.list_rules(),
|
||||
|
48
manila/policies/share_replica_export_location.py
Normal file
48
manila/policies/share_replica_export_location.py
Normal file
@ -0,0 +1,48 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_policy import policy
|
||||
|
||||
from manila.policies import base
|
||||
|
||||
|
||||
BASE_POLICY_NAME = 'share_replica_export_location:%s'
|
||||
|
||||
|
||||
share_replica_export_location_policies = [
|
||||
policy.DocumentedRuleDefault(
|
||||
name=BASE_POLICY_NAME % 'index',
|
||||
check_str=base.RULE_DEFAULT,
|
||||
description="Get all export locations of a given share replica.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/share-replicas/{share_replica_id}/export-locations',
|
||||
}
|
||||
]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=BASE_POLICY_NAME % 'show',
|
||||
check_str=base.RULE_DEFAULT,
|
||||
description="Get details about the requested share replica export "
|
||||
"location.",
|
||||
operations=[
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': ('/share-replicas/{share_replica_id}/export-locations/'
|
||||
'{export_location_id}'),
|
||||
}
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
def list_rules():
|
||||
return share_replica_export_location_policies
|
@ -17,7 +17,9 @@ import ddt
|
||||
import mock
|
||||
from webob import exc
|
||||
|
||||
from manila.api.openstack import api_version_request as api_version
|
||||
from manila.api.v2 import share_export_locations as export_locations
|
||||
from manila.common import constants
|
||||
from manila import context
|
||||
from manila import db
|
||||
from manila import exception
|
||||
@ -169,6 +171,63 @@ class ShareExportLocationsAPITest(test.TestCase):
|
||||
for k, v in el.items():
|
||||
self.assertEqual(v, el[k])
|
||||
|
||||
@ddt.data(*set(('2.46', '2.47', api_version._MAX_API_VERSION)))
|
||||
def test_list_export_locations_replicated_share(self, version):
|
||||
"""Test the export locations API changes between 2.46 and 2.47
|
||||
|
||||
For API version <= 2.46, non-active replica export locations are
|
||||
included in the API response. They are not included in and beyond
|
||||
version 2.47.
|
||||
"""
|
||||
# Setup data
|
||||
share = db_utils.create_share(
|
||||
replication_type=constants.REPLICATION_TYPE_READABLE,
|
||||
replica_state=constants.REPLICA_STATE_ACTIVE)
|
||||
active_replica_id = share.instance.id
|
||||
exports = [
|
||||
{'path': 'myshare.mydomain/active-replica-exp1',
|
||||
'is_admin_only': False},
|
||||
{'path': 'myshare.mydomain/active-replica-exp2',
|
||||
'is_admin_only': False},
|
||||
]
|
||||
db.share_export_locations_update(
|
||||
self.ctxt['user'], active_replica_id, exports)
|
||||
|
||||
# Replicas
|
||||
share_replica2 = db_utils.create_share_replica(
|
||||
share_id=share.id, replica_state=constants.REPLICA_STATE_IN_SYNC)
|
||||
share_replica3 = db_utils.create_share_replica(
|
||||
share_id=share.id,
|
||||
replica_state=constants.REPLICA_STATE_OUT_OF_SYNC)
|
||||
replica2_exports = [
|
||||
{'path': 'myshare.mydomain/insync-replica-exp',
|
||||
'is_admin_only': False}
|
||||
]
|
||||
replica3_exports = [
|
||||
{'path': 'myshare.mydomain/outofsync-replica-exp',
|
||||
'is_admin_only': False}
|
||||
]
|
||||
db.share_export_locations_update(
|
||||
self.ctxt['user'], share_replica2.id, replica2_exports)
|
||||
db.share_export_locations_update(
|
||||
self.ctxt['user'], share_replica3.id, replica3_exports)
|
||||
|
||||
req = self._get_request(version=version)
|
||||
index_result = self.controller.index(req, share['id'])
|
||||
|
||||
actual_paths = [el['path'] for el in index_result['export_locations']]
|
||||
if self.is_microversion_ge(version, '2.47'):
|
||||
self.assertEqual(2, len(index_result['export_locations']))
|
||||
self.assertNotIn(
|
||||
'myshare.mydomain/insync-replica-exp', actual_paths)
|
||||
self.assertNotIn(
|
||||
'myshare.mydomain/outofsync-replica-exp', actual_paths)
|
||||
else:
|
||||
self.assertEqual(4, len(index_result['export_locations']))
|
||||
self.assertIn('myshare.mydomain/insync-replica-exp', actual_paths)
|
||||
self.assertIn(
|
||||
'myshare.mydomain/outofsync-replica-exp', actual_paths)
|
||||
|
||||
@ddt.data('1.0', '2.0', '2.8')
|
||||
def test_list_with_unsupported_version(self, version):
|
||||
self.assertRaises(
|
||||
|
199
manila/tests/api/v2/test_share_replica_export_locations.py
Normal file
199
manila/tests/api/v2/test_share_replica_export_locations.py
Normal file
@ -0,0 +1,199 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
from webob import exc
|
||||
|
||||
from manila.api.v2 import share_replica_export_locations as export_locations
|
||||
from manila.common import constants
|
||||
from manila import context
|
||||
from manila import db
|
||||
from manila import exception
|
||||
from manila import policy
|
||||
from manila import test
|
||||
from manila.tests.api import fakes
|
||||
from manila.tests import db_utils
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class ShareReplicaExportLocationsAPITest(test.TestCase):
|
||||
|
||||
def _get_request(self, version="2.47", use_admin_context=False):
|
||||
req = fakes.HTTPRequest.blank(
|
||||
'/v2/share-replicas/%s/export-locations' % self.active_replica_id,
|
||||
version=version, use_admin_context=use_admin_context,
|
||||
experimental=True)
|
||||
return req
|
||||
|
||||
def setUp(self):
|
||||
super(ShareReplicaExportLocationsAPITest, self).setUp()
|
||||
self.controller = (
|
||||
export_locations.ShareReplicaExportLocationController())
|
||||
self.resource_name = 'share_replica_export_location'
|
||||
self.ctxt = context.RequestContext('fake', 'fake')
|
||||
self.mock_policy_check = self.mock_object(
|
||||
policy, 'check_policy', mock.Mock(return_value=True))
|
||||
self.share = db_utils.create_share(
|
||||
replication_type=constants.REPLICATION_TYPE_READABLE,
|
||||
replica_state=constants.REPLICA_STATE_ACTIVE)
|
||||
self.active_replica_id = self.share.instance.id
|
||||
self.req = self._get_request()
|
||||
exports = [
|
||||
{'path': 'myshare.mydomain/active-replica-exp1',
|
||||
'is_admin_only': False},
|
||||
{'path': 'myshare.mydomain/active-replica-exp2',
|
||||
'is_admin_only': False},
|
||||
]
|
||||
db.share_export_locations_update(
|
||||
self.ctxt, self.active_replica_id, exports)
|
||||
|
||||
# Replicas
|
||||
self.share_replica2 = db_utils.create_share_replica(
|
||||
share_id=self.share.id,
|
||||
replica_state=constants.REPLICA_STATE_IN_SYNC)
|
||||
self.share_replica3 = db_utils.create_share_replica(
|
||||
share_id=self.share.id,
|
||||
replica_state=constants.REPLICA_STATE_OUT_OF_SYNC)
|
||||
replica2_exports = [
|
||||
{'path': 'myshare.mydomain/insync-replica-exp',
|
||||
'is_admin_only': False},
|
||||
{'path': 'myshare.mydomain/insync-replica-exp2',
|
||||
'is_admin_only': False}
|
||||
]
|
||||
replica3_exports = [
|
||||
{'path': 'myshare.mydomain/outofsync-replica-exp',
|
||||
'is_admin_only': False},
|
||||
{'path': 'myshare.mydomain/outofsync-replica-exp2',
|
||||
'is_admin_only': False}
|
||||
]
|
||||
db.share_export_locations_update(
|
||||
self.ctxt, self.share_replica2.id, replica2_exports)
|
||||
db.share_export_locations_update(
|
||||
self.ctxt, self.share_replica3.id, replica3_exports)
|
||||
|
||||
@ddt.data('user', 'admin')
|
||||
def test_list_and_show(self, role):
|
||||
summary_keys = [
|
||||
'id', 'path', 'replica_state', 'availability_zone', 'preferred'
|
||||
]
|
||||
admin_summary_keys = summary_keys + [
|
||||
'share_instance_id', 'is_admin_only'
|
||||
]
|
||||
detail_keys = summary_keys + ['created_at', 'updated_at']
|
||||
admin_detail_keys = admin_summary_keys + ['created_at', 'updated_at']
|
||||
|
||||
self._test_list_and_show(role, summary_keys, detail_keys,
|
||||
admin_summary_keys, admin_detail_keys)
|
||||
|
||||
def _test_list_and_show(self, role, summary_keys, detail_keys,
|
||||
admin_summary_keys, admin_detail_keys):
|
||||
|
||||
req = self._get_request(use_admin_context=(role == 'admin'))
|
||||
for replica_id in (self.active_replica_id, self.share_replica2.id,
|
||||
self.share_replica3.id):
|
||||
index_result = self.controller.index(req, replica_id)
|
||||
|
||||
self.assertIn('export_locations', index_result)
|
||||
self.assertEqual(1, len(index_result))
|
||||
self.assertEqual(2, len(index_result['export_locations']))
|
||||
|
||||
for index_el in index_result['export_locations']:
|
||||
self.assertIn('id', index_el)
|
||||
show_result = self.controller.show(
|
||||
req, replica_id, index_el['id'])
|
||||
self.assertIn('export_location', show_result)
|
||||
self.assertEqual(1, len(show_result))
|
||||
|
||||
show_el = show_result['export_location']
|
||||
|
||||
# Check summary keys in index result & detail keys in show
|
||||
if role == 'admin':
|
||||
self.assertEqual(len(admin_summary_keys), len(index_el))
|
||||
for key in admin_summary_keys:
|
||||
self.assertIn(key, index_el)
|
||||
self.assertEqual(len(admin_detail_keys), len(show_el))
|
||||
for key in admin_detail_keys:
|
||||
self.assertIn(key, show_el)
|
||||
else:
|
||||
self.assertEqual(len(summary_keys), len(index_el))
|
||||
for key in summary_keys:
|
||||
self.assertIn(key, index_el)
|
||||
self.assertEqual(len(detail_keys), len(show_el))
|
||||
for key in detail_keys:
|
||||
self.assertIn(key, show_el)
|
||||
|
||||
# Ensure keys common to index & show have matching values
|
||||
for key in summary_keys:
|
||||
self.assertEqual(index_el[key], show_el[key])
|
||||
|
||||
def test_list_and_show_with_non_replicas(self):
|
||||
non_replicated_share = db_utils.create_share()
|
||||
instance_id = non_replicated_share.instance.id
|
||||
exports = [
|
||||
{'path': 'myshare.mydomain/non-replicated-share',
|
||||
'is_admin_only': False},
|
||||
{'path': 'myshare.mydomain/non-replicated-share-2',
|
||||
'is_admin_only': False},
|
||||
]
|
||||
db.share_export_locations_update(self.ctxt, instance_id, exports)
|
||||
updated_exports = db.share_export_locations_get_by_share_id(
|
||||
self.ctxt, non_replicated_share.id)
|
||||
|
||||
self.assertRaises(exc.HTTPNotFound, self.controller.index, self.req,
|
||||
instance_id)
|
||||
|
||||
for export in updated_exports:
|
||||
self.assertRaises(exc.HTTPNotFound, self.controller.show, self.req,
|
||||
instance_id, export['id'])
|
||||
|
||||
def test_list_export_locations_share_replica_not_found(self):
|
||||
self.assertRaises(
|
||||
exc.HTTPNotFound,
|
||||
self.controller.index,
|
||||
self.req, 'non-existent-share-replica-id')
|
||||
|
||||
def test_show_export_location_share_replica_not_found(self):
|
||||
index_result = self.controller.index(self.req, self.active_replica_id)
|
||||
el_id = index_result['export_locations'][0]['id']
|
||||
|
||||
self.assertRaises(
|
||||
exc.HTTPNotFound,
|
||||
self.controller.show,
|
||||
self.req, 'non-existent-share-replica-id', el_id)
|
||||
|
||||
self.assertRaises(
|
||||
exc.HTTPNotFound,
|
||||
self.controller.show,
|
||||
self.req, self.active_replica_id,
|
||||
'non-existent-export-location-id')
|
||||
|
||||
@ddt.data('1.0', '2.0', '2.46')
|
||||
def test_list_with_unsupported_version(self, version):
|
||||
self.assertRaises(
|
||||
exception.VersionNotFoundForAPIMethod,
|
||||
self.controller.index,
|
||||
self._get_request(version),
|
||||
self.active_replica_id)
|
||||
|
||||
@ddt.data('1.0', '2.0', '2.46')
|
||||
def test_show_with_unsupported_version(self, version):
|
||||
index_result = self.controller.index(self.req, self.active_replica_id)
|
||||
|
||||
self.assertRaises(
|
||||
exception.VersionNotFoundForAPIMethod,
|
||||
self.controller.show,
|
||||
self._get_request(version),
|
||||
self.active_replica_id,
|
||||
index_result['export_locations'][0]['id'])
|
@ -0,0 +1,22 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
New experimental APIs were introduced version ``2.47`` to retrieve
|
||||
export locations of share replicas. With API versions ``2.46`` and
|
||||
prior, export locations of non-active or secondary share replicas are
|
||||
included in the share export locations APIs, albeit these APIs do not
|
||||
provide all the necessary distinguishing information (availability zone,
|
||||
replica state and replica ID). See the `API reference
|
||||
<https://developer.openstack.org/api-ref/shared-file-system/>`_ for more
|
||||
information regarding these API changes.
|
||||
deprecations:
|
||||
- |
|
||||
In API version ``2.47``, export locations APIs: ``GET
|
||||
/v2/{tenant_id}/shares/{share_id}/export_locations`` and ``GET
|
||||
/v2/{tenant_id}/shares/{share_id}/export_locations/{export_location_id
|
||||
}`` no longer provide export locations of non-active or secondary share
|
||||
replicas where available. Use the newly introduced share replica export
|
||||
locations APIs to gather this information: ``GET
|
||||
/v2/{tenant_id}/share-replicas/{share_replica_id}/export-locations`` and
|
||||
``GET /v2/{tenant_id}/share-replicas/{share_replica_id}/export
|
||||
-locations/{export_location_id}``.
|
Loading…
Reference in New Issue
Block a user