Improve share list API filtering

This change adds possibility to filter shares in a lot of possible ways.

All types of share listing were updated to support additional filtering by:
- share network
- snapshot
- volume type
- host
- limit and offset a la pagination
- project id (useful with '--all-tenants')
- metadata (whether specific metadata keys and values are set or not)
- extra-specs (whether any volume type exists and has specific extra-specs
    set up)

And sorting by:
- key of models.Share
- direction (asc, desc)

Implements blueprint improve-share-list-filtering

Change-Id: Ifc53ab27bc710562cc510f30a2c847eda385d5bc
This commit is contained in:
Your Name 2014-09-30 11:06:22 -04:00 committed by Valeriy Ponomaryov
parent c5e48b592b
commit 8f0d0dc6c9
10 changed files with 914 additions and 144 deletions

View File

@ -0,0 +1,373 @@
# Copyright 2014 Mirantis Inc.
# 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.
from tempest.api.share import base
from tempest.common.utils import data_utils
from tempest import config_share as config
from tempest import test
CONF = config.CONF
class SharesActionsAdminTest(base.BaseSharesAdminTest):
"""Covers share functionality, that doesn't related to share type."""
@classmethod
def setUpClass(cls):
super(SharesActionsAdminTest, cls).setUpClass()
# create volume type for share filtering purposes
cls.vt_name = data_utils.rand_name("tempest-vt-name")
cls.extra_specs = {'storage_protocol': CONF.share.storage_protocol}
__, cls.vt = cls.create_volume_type(
name=cls.vt_name,
cleanup_in_class=True,
extra_specs=cls.extra_specs,
)
# create share
cls.share_name = data_utils.rand_name("tempest-share-name")
cls.share_desc = data_utils.rand_name("tempest-share-description")
cls.metadata = {
'foo_key_share_1': 'foo_value_share_1',
'bar_key_share_1': 'foo_value_share_1',
}
cls.share_size = 1
__, cls.share = cls.create_share(
name=cls.share_name,
description=cls.share_desc,
size=cls.share_size,
metadata=cls.metadata,
volume_type_id=cls.vt['id'],
)
# create snapshot
cls.snap_name = data_utils.rand_name("tempest-snapshot-name")
cls.snap_desc = data_utils.rand_name("tempest-snapshot-description")
__, cls.snap = cls.create_snapshot_wait_for_active(cls.share["id"],
cls.snap_name,
cls.snap_desc)
# create second share from snapshot for purposes of sorting and
# snapshot filtering
cls.share_name2 = data_utils.rand_name("tempest-share-name")
cls.share_desc2 = data_utils.rand_name("tempest-share-description")
cls.metadata2 = {
'foo_key_share_2': 'foo_value_share_2',
'bar_key_share_2': 'foo_value_share_2',
}
__, cls.share2 = cls.create_share(
name=cls.share_name2,
description=cls.share_desc2,
size=cls.share_size,
metadata=cls.metadata2,
snapshot_id=cls.snap['id'],
)
@test.attr(type=["gate", ])
def test_get_share(self):
# get share
resp, share = self.shares_client.get_share(self.share['id'])
# verify response
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
# verify keys
expected_keys = ["status", "description", "links", "availability_zone",
"created_at", "export_location", "share_proto",
"name", "snapshot_id", "id", "size"]
actual_keys = share.keys()
[self.assertIn(key, actual_keys) for key in expected_keys]
# verify values
msg = "Expected name: '%s', actual name: '%s'" % (self.share_name,
share["name"])
self.assertEqual(self.share_name, str(share["name"]), msg)
msg = "Expected description: '%s', "\
"actual description: '%s'" % (self.share_desc,
share["description"])
self.assertEqual(self.share_desc, str(share["description"]), msg)
msg = "Expected size: '%s', actual size: '%s'" % (self.share_size,
share["size"])
self.assertEqual(self.share_size, int(share["size"]), msg)
@test.attr(type=["gate", ])
def test_list_shares(self):
# list shares
resp, shares = self.shares_client.list_shares()
# verify response
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
# verify keys
keys = ["name", "id", "links"]
[self.assertIn(key, sh.keys()) for sh in shares for key in keys]
# our share id in list and have no duplicates
for share_id in [self.share["id"], self.share2["id"]]:
gen = [sid["id"] for sid in shares if sid["id"] in share_id]
msg = "expected id lists %s times in share list" % (len(gen))
self.assertEqual(1, len(gen), msg)
@test.attr(type=["gate", ])
def test_list_shares_with_detail(self):
# list shares
resp, shares = self.shares_client.list_shares_with_detail()
# verify response
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
# verify keys
keys = [
"status", "description", "links", "availability_zone",
"created_at", "export_location", "share_proto", "host",
"name", "snapshot_id", "id", "size", "project_id",
]
[self.assertIn(key, sh.keys()) for sh in shares for key in keys]
# our shares in list and have no duplicates
for share_id in [self.share["id"], self.share2["id"]]:
gen = [sid["id"] for sid in shares if sid["id"] in share_id]
msg = "expected id lists %s times in share list" % (len(gen))
self.assertEqual(1, len(gen), msg)
@test.attr(type=["gate", ])
def test_list_shares_with_detail_filter_by_metadata(self):
filters = {'metadata': self.metadata}
# list shares
__, shares = self.shares_client.list_shares_with_detail(params=filters)
# verify response
self.assertTrue(len(shares) > 0)
for share in shares:
self.assertDictContainsSubset(
filters['metadata'], share['metadata'])
self.assertFalse(self.share2['id'] in [s['id'] for s in shares])
@test.attr(type=["gate", ])
def test_list_shares_with_detail_filter_by_extra_specs(self):
filters = {"extra_specs": self.extra_specs}
# list shares
__, shares = self.shares_client.list_shares_with_detail(params=filters)
# verify response
self.assertTrue(len(shares) > 0)
shares_ids = [s["id"] for s in shares]
self.assertTrue(self.share["id"] in shares_ids)
self.assertFalse(self.share2["id"] in shares_ids)
for share in shares:
__, vts = self.shares_client.list_volume_types()
# find its name or id, get id
vt_id = None
for vt in vts:
if share["volume_type"] in [vt["id"], vt["name"]]:
vt_id = vt["id"]
break
if vt_id is None:
raise ValueError(
"Share '%(s_id)s' listed with extra_specs filter has "
"nonexistent volume type '%(vt)s'." % {
"s_id": share["id"], "vt": share["volume_type"]}
)
__, extra_specs = self.shares_client.list_volume_types_extra_specs(
vt_id)
self.assertDictContainsSubset(filters["extra_specs"], extra_specs)
@test.attr(type=["gate", ])
def test_list_shares_with_detail_filter_by_volume_type_id(self):
filters = {'volume_type_id': self.vt['id']}
# list shares
__, shares = self.shares_client.list_shares_with_detail(params=filters)
# verify response
self.assertTrue(len(shares) > 0)
for share in shares:
__, vts = self.shares_client.list_volume_types()
# find its name or id, get id
vt_id = None
for vt in vts:
if share["volume_type"] in [vt["id"], vt["name"]]:
vt_id = vt["id"]
break
if vt_id is None:
raise ValueError(
"Share '%(s_id)s' listed with volume_type_id filter has "
"nonexistent volume type '%(vt)s'." % {
"s_id": share["id"], "vt": share["volume_type"]}
)
self.assertEqual(
filters['volume_type_id'], vt_id)
self.assertFalse(self.share2['id'] in [s['id'] for s in shares])
@test.attr(type=["gate", ])
def test_list_shares_with_detail_filter_by_host(self):
__, base_share = self.shares_client.get_share(self.share['id'])
filters = {'host': base_share['host']}
# list shares
__, shares = self.shares_client.list_shares_with_detail(params=filters)
# verify response
self.assertTrue(len(shares) > 0)
for share in shares:
self.assertEqual(filters['host'], share['host'])
@test.attr(type=["gate", ])
def test_list_shares_with_detail_filter_by_share_network_id(self):
__, base_share = self.shares_client.get_share(self.share['id'])
filters = {'share_network_id': base_share['share_network_id']}
# list shares
__, shares = self.shares_client.list_shares_with_detail(params=filters)
# verify response
self.assertTrue(len(shares) > 1)
for share in shares:
self.assertEqual(
filters['share_network_id'], share['share_network_id'])
@test.attr(type=["gate", ])
def test_list_shares_with_detail_filter_by_snapshot_id(self):
filters = {'snapshot_id': self.snap['id']}
# list shares
__, shares = self.shares_client.list_shares_with_detail(params=filters)
# verify response
self.assertTrue(len(shares) > 0)
for share in shares:
self.assertEqual(filters['snapshot_id'], share['snapshot_id'])
self.assertFalse(self.share['id'] in [s['id'] for s in shares])
@test.attr(type=["gate", ])
def test_list_shares_with_detail_with_asc_sorting(self):
filters = {'sort_key': 'created_at', 'sort_dir': 'asc'}
# list shares
__, shares = self.shares_client.list_shares_with_detail(params=filters)
# verify response
self.assertTrue(len(shares) > 0)
sorted_list = [share['created_at'] for share in shares]
self.assertEqual(sorted_list, sorted(sorted_list))
@test.attr(type=["gate", ])
def test_list_shares_with_detail_filter_by_existed_name(self):
# list shares by name, at least one share is expected
params = {"name": self.share_name}
resp, shares = self.shares_client.list_shares_with_detail(params)
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(shares[0]["name"], self.share_name)
@test.attr(type=["gate", ])
def test_list_shares_with_detail_filter_by_fake_name(self):
# list shares by fake name, no shares are expected
params = {"name": data_utils.rand_name("fake-nonexistent-name")}
resp, shares = self.shares_client.list_shares_with_detail(params)
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(0, len(shares))
@test.attr(type=["gate", ])
def test_list_shares_with_detail_filter_by_active_status(self):
# list shares by active status, at least one share is expected
params = {"status": "available"}
resp, shares = self.shares_client.list_shares_with_detail(params)
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertTrue(len(shares) > 0)
for share in shares:
self.assertEqual(share["status"], params["status"])
@test.attr(type=["gate", ])
def test_list_shares_with_detail_filter_by_fake_status(self):
# list shares by fake status, no shares are expected
params = {"status": 'fake'}
resp, shares = self.shares_client.list_shares_with_detail(params)
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(0, len(shares))
@test.attr(type=["gate", ])
def test_get_snapshot(self):
# get snapshot
resp, get = self.shares_client.get_snapshot(self.snap["id"])
# verify data
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
# verify keys
expected_keys = ["status", "links", "share_id", "name",
"export_location", "share_proto", "created_at",
"description", "id", "share_size"]
actual_keys = get.keys()
[self.assertIn(key, actual_keys) for key in expected_keys]
# verify data
msg = "Expected name: '%s', actual name: '%s'" % (self.snap_name,
get["name"])
self.assertEqual(self.snap_name, get["name"], msg)
msg = "Expected description: '%s', "\
"actual description: '%s'" % (self.snap_desc, get["description"])
self.assertEqual(self.snap_desc, get["description"], msg)
msg = "Expected share_id: '%s', "\
"actual share_id: '%s'" % (self.share["id"], get["share_id"])
self.assertEqual(self.share["id"], get["share_id"], msg)
@test.attr(type=["gate", ])
def test_list_snapshots(self):
# list share snapshots
resp, snaps = self.shares_client.list_snapshots()
# verify response
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
# verify keys
keys = ["id", "name", "links"]
[self.assertIn(key, sn.keys()) for sn in snaps for key in keys]
# our share id in list and have no duplicates
gen = [sid["id"] for sid in snaps if sid["id"] in self.snap["id"]]
msg = "expected id lists %s times in share list" % (len(gen))
self.assertEqual(1, len(gen), msg)
@test.attr(type=["gate", ])
def test_list_snapshots_with_detail(self):
# list share snapshots
resp, snaps = self.shares_client.list_snapshots_with_detail()
# verify response
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
# verify keys
keys = ["status", "links", "share_id", "name",
"export_location", "share_proto", "created_at",
"description", "id", "share_size"]
[self.assertIn(key, sn.keys()) for sn in snaps for key in keys]
# our share id in list and have no duplicates
gen = [sid["id"] for sid in snaps if sid["id"] in self.snap["id"]]
msg = "expected id lists %s times in share list" % (len(gen))
self.assertEqual(1, len(gen), msg)

View File

@ -1,4 +1,4 @@
# Copyright 2014 mirantis Inc.
# Copyright 2014 Mirantis Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -21,25 +21,49 @@ from tempest import test
CONF = config.CONF
class SharesTest(base.BaseSharesTest):
class SharesActionsTest(base.BaseSharesTest):
"""Covers share functionality, that doesn't related to share type."""
@classmethod
def setUpClass(cls):
super(SharesTest, cls).setUpClass()
super(SharesActionsTest, cls).setUpClass()
# create share
cls.share_name = data_utils.rand_name("tempest-share-name")
cls.share_desc = data_utils.rand_name("tempest-share-description")
cls.metadata = {
'foo_key_share_1': 'foo_value_share_1',
'bar_key_share_1': 'foo_value_share_1',
}
cls.share_size = 1
__, cls.share = cls.create_share(name=cls.share_name,
description=cls.share_desc,
size=cls.share_size)
__, cls.share = cls.create_share(
name=cls.share_name,
description=cls.share_desc,
size=cls.share_size,
metadata=cls.metadata,
)
# create snapshot
cls.snap_name = data_utils.rand_name("tempest-snapshot-name")
cls.snap_desc = data_utils.rand_name("tempest-snapshot-description")
__, cls.snap = cls.create_snapshot_wait_for_active(cls.share["id"],
cls.snap_name,
cls.snap_desc)
__, cls.snap = cls.create_snapshot_wait_for_active(
cls.share["id"], cls.snap_name, cls.snap_desc)
# create second share from snapshot for purposes of sorting and
# snapshot filtering
cls.share_name2 = data_utils.rand_name("tempest-share-name")
cls.share_desc2 = data_utils.rand_name("tempest-share-description")
cls.metadata2 = {
'foo_key_share_2': 'foo_value_share_2',
'bar_key_share_2': 'foo_value_share_2',
}
__, cls.share2 = cls.create_share(
name=cls.share_name2,
description=cls.share_desc2,
size=cls.share_size,
metadata=cls.metadata2,
snapshot_id=cls.snap['id'],
)
@test.attr(type=["gate", ])
def test_get_share(self):
@ -85,9 +109,10 @@ class SharesTest(base.BaseSharesTest):
[self.assertIn(key, sh.keys()) for sh in shares for key in keys]
# our share id in list and have no duplicates
gen = [sid["id"] for sid in shares if sid["id"] in self.share["id"]]
msg = "expected id lists %s times in share list" % (len(gen))
self.assertEqual(len(gen), 1, msg)
for share_id in [self.share["id"], self.share2["id"]]:
gen = [sid["id"] for sid in shares if sid["id"] in share_id]
msg = "expected id lists %s times in share list" % (len(gen))
self.assertEqual(1, len(gen), msg)
@test.attr(type=["gate", ])
def test_list_shares_with_detail(self):
@ -106,10 +131,77 @@ class SharesTest(base.BaseSharesTest):
]
[self.assertIn(key, sh.keys()) for sh in shares for key in keys]
# our share id in list and have no duplicates
gen = [sid["id"] for sid in shares if sid["id"] in self.share["id"]]
msg = "expected id lists %s times in share list" % (len(gen))
self.assertEqual(len(gen), 1, msg)
# our shares in list and have no duplicates
for share_id in [self.share["id"], self.share2["id"]]:
gen = [sid["id"] for sid in shares if sid["id"] in share_id]
msg = "expected id lists %s times in share list" % (len(gen))
self.assertEqual(1, len(gen), msg)
@test.attr(type=["gate", ])
def test_list_shares_with_detail_filter_by_metadata(self):
filters = {'metadata': self.metadata}
# list shares
__, shares = self.shares_client.list_shares_with_detail(params=filters)
# verify response
self.assertTrue(len(shares) > 0)
for share in shares:
self.assertDictContainsSubset(
filters['metadata'], share['metadata'])
self.assertFalse(self.share2['id'] in [s['id'] for s in shares])
@test.attr(type=["gate", ])
def test_list_shares_with_detail_filter_by_host(self):
__, base_share = self.shares_client.get_share(self.share['id'])
filters = {'host': base_share['host']}
# list shares
__, shares = self.shares_client.list_shares_with_detail(params=filters)
# verify response
self.assertTrue(len(shares) > 0)
for share in shares:
self.assertEqual(filters['host'], share['host'])
@test.attr(type=["gate", ])
def test_list_shares_with_detail_filter_by_share_network_id(self):
__, base_share = self.shares_client.get_share(self.share['id'])
filters = {'share_network_id': base_share['share_network_id']}
# list shares
__, shares = self.shares_client.list_shares_with_detail(params=filters)
# verify response
self.assertTrue(len(shares) > 1)
for share in shares:
self.assertEqual(
filters['share_network_id'], share['share_network_id'])
@test.attr(type=["gate", ])
def test_list_shares_with_detail_filter_by_snapshot_id(self):
filters = {'snapshot_id': self.snap['id']}
# list shares
__, shares = self.shares_client.list_shares_with_detail(params=filters)
# verify response
self.assertTrue(len(shares) > 0)
for share in shares:
self.assertEqual(filters['snapshot_id'], share['snapshot_id'])
self.assertFalse(self.share['id'] in [s['id'] for s in shares])
@test.attr(type=["gate", ])
def test_list_shares_with_detail_with_asc_sorting(self):
filters = {'sort_key': 'created_at', 'sort_dir': 'asc'}
# list shares
__, shares = self.shares_client.list_shares_with_detail(params=filters)
# verify response
self.assertTrue(len(shares) > 0)
sorted_list = [share['created_at'] for share in shares]
self.assertEqual(sorted_list, sorted(sorted_list))
@test.attr(type=["gate", ])
def test_list_shares_with_detail_filter_by_existed_name(self):

View File

@ -80,17 +80,16 @@ class SharesClient(rest_client.RestClient):
def delete_share(self, share_id):
return self.delete("shares/%s" % share_id)
def list_shares(self):
resp, body = self.get("shares")
def list_shares(self, detailed=False, params=None):
"""Get list of shares w/o filters."""
uri = 'shares/detail' if detailed else 'shares'
uri += '?%s' % urllib.urlencode(params) if params else ''
resp, body = self.get(uri)
return resp, self._parse_resp(body)
def list_shares_with_detail(self, params=None):
"""List the details of all shares."""
uri = 'shares/detail'
if params:
uri += '?%s' % urllib.urlencode(params)
resp, body = self.get(uri)
return resp, self._parse_resp(body)
"""Get detailed list of shares w/o filters."""
return self.list_shares(detailed=True, params=params)
def get_share(self, share_id):
resp, body = self.get("shares/%s" % share_id)

View File

@ -15,6 +15,8 @@
"""The shares api."""
import ast
import six
import webob
from webob import exc
@ -110,15 +112,33 @@ class ShareController(wsgi.Controller):
search_opts = {}
search_opts.update(req.GET)
# NOTE(rushiagr): v2 API allows name instead of display_name
# Remove keys that are not related to share attrs
search_opts.pop('limit', None)
search_opts.pop('offset', None)
sort_key = search_opts.pop('sort_key', 'created_at')
sort_dir = search_opts.pop('sort_dir', 'desc')
# Deserialize dicts
if 'metadata' in search_opts:
search_opts['metadata'] = ast.literal_eval(search_opts['metadata'])
if 'extra_specs' in search_opts:
search_opts['extra_specs'] = ast.literal_eval(
search_opts['extra_specs'])
# NOTE(vponomaryov): Manila stores in DB key 'display_name', but
# allows to use both keys 'name' and 'display_name'. It is leftover
# from Cinder v1 and v2 APIs.
if 'name' in search_opts:
search_opts['display_name'] = search_opts['name']
del search_opts['name']
search_opts['display_name'] = search_opts.pop('name')
if sort_key == 'name':
sort_key = 'display_name'
common.remove_invalid_options(
context, search_opts, self._get_share_search_options())
shares = self.share_api.get_all(context, search_opts=search_opts)
shares = self.share_api.get_all(
context, search_opts=search_opts, sort_key=sort_key,
sort_dir=sort_dir)
limited_list = common.limited(shares, req)
@ -132,7 +152,13 @@ class ShareController(wsgi.Controller):
"""Return share search options allowed by non-admin."""
# NOTE(vponomaryov): share_server_id depends on policy, allow search
# by it for non-admins in case policy changed.
return ('display_name', 'status', 'share_server_id', )
# Also allow search by extra_specs in case policy
# for it allows non-admin access.
return (
'display_name', 'status', 'share_server_id', 'volume_type_id',
'snapshot_id', 'host', 'share_network_id',
'metadata', 'extra_specs', 'sort_key', 'sort_dir',
)
@wsgi.serializers(xml=ShareTemplate)
def update(self, req, id, body):

View File

@ -294,24 +294,37 @@ def share_get(context, share_id):
return IMPL.share_get(context, share_id)
def share_get_all(context):
def share_get_all(context, filters=None, sort_key=None, sort_dir=None):
"""Get all shares."""
return IMPL.share_get_all(context)
return IMPL.share_get_all(
context, filters=filters, sort_key=sort_key, sort_dir=sort_dir,
)
def share_get_all_by_host(context, host):
def share_get_all_by_host(context, host, filters=None, sort_key=None,
sort_dir=None):
"""Returns all shares with given host."""
return IMPL.share_get_all_by_host(context, host)
return IMPL.share_get_all_by_host(
context, host, filters=filters, sort_key=sort_key, sort_dir=sort_dir,
)
def share_get_all_by_project(context, project_id):
def share_get_all_by_project(context, project_id, filters=None, sort_key=None,
sort_dir=None):
"""Returns all shares with given project ID."""
return IMPL.share_get_all_by_project(context, project_id)
return IMPL.share_get_all_by_project(
context, project_id, filters=filters, sort_key=sort_key,
sort_dir=sort_dir,
)
def share_get_all_by_share_server(context, share_server_id):
"""Returns all shares with given share server."""
return IMPL.share_get_all_by_share_server(context, share_server_id)
def share_get_all_by_share_server(context, share_server_id, filters=None,
sort_key=None, sort_dir=None):
"""Returns all shares with given share server ID."""
return IMPL.share_get_all_by_share_server(
context, share_server_id, filters=filters, sort_key=sort_key,
sort_dir=sort_dir,
)
def share_delete(context, share_id):

View File

@ -1,6 +1,7 @@
# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# Copyright (c) 2014 Mirantis, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -1164,28 +1165,109 @@ def share_get(context, share_id, session=None):
return result
@require_admin_context
def share_get_all(context):
return _share_get_query(context).all()
@require_context
def _share_get_all_with_filters(context, project_id=None, share_server_id=None,
host=None, filters=None,
sort_key=None, sort_dir=None):
"""Returns sorted list of shares that satisfies filters.
@require_admin_context
def share_get_all_by_host(context, host):
:param context: context to query under
:param project_id: project id that owns shares
:param share_server_id: share server that hosts shares
:param host: host name where shares [and share servers] are located
:param filters: dict of filters to specify share selection
:param sort_key: key of models.Share to be used for sorting
:param sort_dir: desired direction of sorting, can be 'asc' and 'desc'
:returns: list -- models.Share
:raises: exception.InvalidInput
"""
if not sort_key:
sort_key = 'created_at'
if not sort_dir:
sort_dir = 'desc'
query = _share_get_query(context)
return query.filter_by(host=host).all()
if project_id:
query = query.filter_by(project_id=project_id)
if share_server_id:
query = query.filter_by(share_server_id=share_server_id)
if host:
query = query.filter_by(host=host)
# Apply filters
if not filters:
filters = {}
if 'metadata' in filters:
for k, v in filters['metadata'].items():
query = query.filter(
or_(models.Share.share_metadata.any( # pylint: disable=E1101
key=k, value=v)))
if 'extra_specs' in filters:
query = query.join(
models.VolumeTypeExtraSpecs,
models.VolumeTypeExtraSpecs.volume_type_id ==
models.Share.volume_type_id)
for k, v in filters['extra_specs'].items():
query = query.filter(or_(models.VolumeTypeExtraSpecs.key == k,
models.VolumeTypeExtraSpecs.value == v))
# Apply sorting
try:
attr = getattr(models.Share, sort_key)
except AttributeError:
msg = _("Wrong sorting key provided - '%s'.") % sort_key
raise exception.InvalidInput(reason=msg)
if sort_dir.lower() == 'desc':
query = query.order_by(attr.desc())
elif sort_dir.lower() == 'asc':
query = query.order_by(attr.asc())
else:
msg = _("Wrong sorting data provided: sort key is '%(sort_key)s' "
"and sort direction is '%(sort_dir)s'.") % {
"sort_key": sort_key, "sort_dir": sort_dir}
raise exception.InvalidInput(reason=msg)
# Returns list of shares that satisfy filters.
query = query.all()
return query
@require_admin_context
def share_get_all(context, filters=None, sort_key=None, sort_dir=None):
query = _share_get_all_with_filters(
context, filters=filters, sort_key=sort_key, sort_dir=sort_dir)
return query
@require_admin_context
def share_get_all_by_host(context, host, filters=None,
sort_key=None, sort_dir=None):
query = _share_get_all_with_filters(
context, host=host, filters=filters,
sort_key=sort_key, sort_dir=sort_dir,
)
return query
@require_context
def share_get_all_by_project(context, project_id):
def share_get_all_by_project(context, project_id, filters=None,
sort_key=None, sort_dir=None):
"""Returns list of shares with given project ID."""
return _share_get_query(context).filter_by(project_id=project_id).all()
query = _share_get_all_with_filters(
context, project_id=project_id, filters=filters,
sort_key=sort_key, sort_dir=sort_dir,
)
return query
@require_context
def share_get_all_by_share_server(context, share_server_id):
def share_get_all_by_share_server(context, share_server_id, filters=None,
sort_key=None, sort_dir=None):
"""Returns list of shares with given share server."""
return _share_get_query(context).filter_by(
share_server_id=share_server_id).all()
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

View File

@ -21,6 +21,7 @@ Handles all requests relating to shares.
from oslo.config import cfg
import six
from manila.api import extensions
from manila.db import base
from manila import exception
from manila.openstack.common import excutils
@ -337,24 +338,60 @@ class API(base.Base):
policy.check_policy(context, 'share', 'get', rv)
return rv
def get_all(self, context, search_opts=None):
def get_all(self, context, search_opts=None, sort_key='created_at',
sort_dir='desc'):
policy.check_policy(context, 'share', 'get_all')
if search_opts is None:
search_opts = {}
LOG.debug("Searching for shares by: %s", six.text_type(search_opts))
# Prepare filters
filters = {}
if 'metadata' in search_opts:
filters['metadata'] = search_opts.pop('metadata')
if not isinstance(filters['metadata'], dict):
msg = _("Wrong metadata filter provided: "
"%s.") % six.text_type(filters['metadata'])
raise exception.InvalidInput(reason=msg)
if 'extra_specs' in search_opts:
# Verify policy for extra-specs access
extensions.extension_authorizer(
'share', 'types_extra_specs')(context)
filters['extra_specs'] = search_opts.pop('extra_specs')
if not isinstance(filters['extra_specs'], dict):
msg = _("Wrong extra specs filter provided: "
"%s.") % six.text_type(filters['extra_specs'])
raise exception.InvalidInput(reason=msg)
if not (isinstance(sort_key, six.string_types) and sort_key):
msg = _("Wrong sort_key filter provided: "
"'%s'.") % six.text_type(sort_key)
raise exception.InvalidInput(reason=msg)
if not (isinstance(sort_dir, six.string_types) and sort_dir):
msg = _("Wrong sort_dir filter provided: "
"'%s'.") % six.text_type(sort_dir)
raise exception.InvalidInput(reason=msg)
# Get filtered list of shares
if 'share_server_id' in search_opts:
# NOTE(vponomaryov): this is project_id independent
policy.check_policy(context, 'share', 'list_by_share_server_id')
shares = self.db.share_get_all_by_share_server(
context, search_opts.pop('share_server_id'))
context, search_opts.pop('share_server_id'), filters=filters,
sort_key=sort_key, sort_dir=sort_dir)
elif (context.is_admin and 'all_tenants' in search_opts):
shares = self.db.share_get_all(context)
shares = self.db.share_get_all(
context, filters=filters, sort_key=sort_key, sort_dir=sort_dir)
else:
shares = self.db.share_get_all_by_project(context,
context.project_id)
shares = self.db.share_get_all_by_project(
context, project_id=context.project_id, filters=filters,
sort_key=sort_key, sort_dir=sort_dir)
# NOTE(vponomaryov): we do not need 'all_tenants' opt anymore
search_opts.pop('all_tenants', None)
if search_opts:
LOG.debug("Searching for shares by: %s", str(search_opts))
results = []
for s in shares:
# values in search_opts can be only strings

View File

@ -86,7 +86,8 @@ def stub_snapshot_update(self, context, *args, **param):
return share
def stub_share_get_all_by_project(self, context, search_opts=None):
def stub_share_get_all_by_project(self, context, sort_key=None, sort_dir=None,
search_opts={}):
return [stub_share_get(self, context, '1')]

View File

@ -322,50 +322,67 @@ class ShareApiTest(test.TestCase):
req,
1)
def test_share_list_summary_with_search_opts_by_non_admin(self):
def _share_list_summary_with_search_opts(self, use_admin_context):
search_opts = {
'name': 'fake_name',
'status': 'available',
'share_server_id': 'fake_share_server_id',
'volume_type_id': 'fake_volume_type_id',
'snapshot_id': 'fake_snapshot_id',
'host': 'fake_host',
'share_network_id': 'fake_share_network_id',
'metadata': '%7B%27k1%27%3A+%27v1%27%7D', # serialized k1=v1
'extra_specs': '%7B%27k2%27%3A+%27v2%27%7D', # serialized k2=v2
'sort_key': 'fake_sort_key',
'sort_dir': 'fake_sort_dir',
'limit': '1',
'offset': '1',
}
# fake_key should be filtered for non-admin
fake_key = 'fake_value'
name = 'fake_name'
status = 'available'
share_server_id = 'fake_share_server_id'
req = fakes.HTTPRequest.blank(
'/shares?fake_key=%s&name=%s&share_server_id=%s&'
'status=%s' % (fake_key, name, share_server_id, status),
use_admin_context=False,
)
self.stubs.Set(share_api.API, 'get_all', mock.Mock(return_value=[]))
self.controller.index(req)
url = '/shares?fake_key=fake_value'
for k, v in search_opts.items():
url = url + '&' + k + '=' + v
req = fakes.HTTPRequest.blank(url, use_admin_context=use_admin_context)
shares = [
{'id': 'id1', 'display_name': 'n1'},
{'id': 'id2', 'display_name': 'n2'},
{'id': 'id3', 'display_name': 'n3'},
]
self.stubs.Set(share_api.API, 'get_all',
mock.Mock(return_value=shares))
result = self.controller.index(req)
search_opts_expected = {
'display_name': search_opts['name'],
'status': search_opts['status'],
'share_server_id': search_opts['share_server_id'],
'volume_type_id': search_opts['volume_type_id'],
'snapshot_id': search_opts['snapshot_id'],
'host': search_opts['host'],
'share_network_id': search_opts['share_network_id'],
'metadata': {'k1': 'v1'},
'extra_specs': {'k2': 'v2'},
}
if use_admin_context:
search_opts_expected.update({'fake_key': 'fake_value'})
share_api.API.get_all.assert_called_once_with(
req.environ['manila.context'],
search_opts={
'display_name': name,
'share_server_id': share_server_id,
'status': status,
},
sort_key=search_opts['sort_key'],
sort_dir=search_opts['sort_dir'],
search_opts=search_opts_expected,
)
self.assertEqual(1, len(result['shares']))
self.assertEqual(shares[1]['id'], result['shares'][0]['id'])
self.assertEqual(
shares[1]['display_name'], result['shares'][0]['name'])
def test_share_list_summary_with_search_opts_by_non_admin(self):
self._share_list_summary_with_search_opts(use_admin_context=False)
def test_share_list_summary_with_search_opts_by_admin(self):
# none of search_opts should be filtered for admin
fake_key = 'fake_value'
name = 'fake_name'
status = 'available'
share_server_id = 'fake_share_server_id'
req = fakes.HTTPRequest.blank(
'/shares?fake_key=%s&name=%s&share_server_id=%s&'
'status=%s' % (fake_key, name, share_server_id, status),
use_admin_context=True,
)
self.stubs.Set(share_api.API, 'get_all', mock.Mock(return_value=[]))
self.controller.index(req)
share_api.API.get_all.assert_called_once_with(
req.environ['manila.context'],
search_opts={
'display_name': name,
'fake_key': fake_key,
'share_server_id': share_server_id,
'status': status,
},
)
self._share_list_summary_with_search_opts(use_admin_context=True)
def test_share_list_summary(self):
self.stubs.Set(share_api.API, 'get_all',
@ -392,50 +409,89 @@ class ShareApiTest(test.TestCase):
}
self.assertEqual(res_dict, expected)
def test_share_list_detail_with_search_opts_by_non_admin(self):
def _share_list_detail_with_search_opts(self, use_admin_context):
search_opts = {
'name': 'fake_name',
'status': 'available',
'share_server_id': 'fake_share_server_id',
'volume_type_id': 'fake_volume_type_id',
'snapshot_id': 'fake_snapshot_id',
'host': 'fake_host',
'share_network_id': 'fake_share_network_id',
'metadata': '%7B%27k1%27%3A+%27v1%27%7D', # serialized k1=v1
'extra_specs': '%7B%27k2%27%3A+%27v2%27%7D', # serialized k2=v2
'sort_key': 'fake_sort_key',
'sort_dir': 'fake_sort_dir',
'limit': '1',
'offset': '1',
}
# fake_key should be filtered for non-admin
fake_key = 'fake_value'
name = 'fake_name'
status = 'available'
share_server_id = 'fake_share_server_id'
req = fakes.HTTPRequest.blank(
'/shares?fake_key=%s&name=%s&share_server_id=%s&'
'status=%s' % (fake_key, name, share_server_id, status),
use_admin_context=False,
)
self.stubs.Set(share_api.API, 'get_all', mock.Mock(return_value=[]))
self.controller.detail(req)
url = '/shares/detail?fake_key=fake_value'
for k, v in search_opts.items():
url = url + '&' + k + '=' + v
req = fakes.HTTPRequest.blank(url, use_admin_context=use_admin_context)
shares = [
{'id': 'id1', 'display_name': 'n1'},
{
'id': 'id2',
'display_name': 'n2',
'status': 'available',
'snapshot_id': 'fake_snapshot_id',
'volume_type_id': 'fake_volume_type_id',
'snapshot_id': 'fake_snapshot_id',
'host': 'fake_host',
'share_network_id': 'fake_share_network_id',
},
{'id': 'id3', 'display_name': 'n3'},
]
self.stubs.Set(share_api.API, 'get_all',
mock.Mock(return_value=shares))
result = self.controller.detail(req)
search_opts_expected = {
'display_name': search_opts['name'],
'status': search_opts['status'],
'share_server_id': search_opts['share_server_id'],
'volume_type_id': search_opts['volume_type_id'],
'snapshot_id': search_opts['snapshot_id'],
'host': search_opts['host'],
'share_network_id': search_opts['share_network_id'],
'metadata': {'k1': 'v1'},
'extra_specs': {'k2': 'v2'},
}
if use_admin_context:
search_opts_expected.update({'fake_key': 'fake_value'})
share_api.API.get_all.assert_called_once_with(
req.environ['manila.context'],
search_opts={
'display_name': name,
'share_server_id': share_server_id,
'status': status,
},
sort_key=search_opts['sort_key'],
sort_dir=search_opts['sort_dir'],
search_opts=search_opts_expected,
)
self.assertEqual(1, len(result['shares']))
self.assertEqual(shares[1]['id'], result['shares'][0]['id'])
self.assertEqual(
shares[1]['display_name'], result['shares'][0]['name'])
self.assertEqual(
shares[1]['snapshot_id'], result['shares'][0]['snapshot_id'])
self.assertEqual(
shares[1]['status'], result['shares'][0]['status'])
self.assertEqual(
shares[1]['volume_type_id'], result['shares'][0]['volume_type'])
self.assertEqual(
shares[1]['snapshot_id'], result['shares'][0]['snapshot_id'])
self.assertEqual(
shares[1]['host'], result['shares'][0]['host'])
self.assertEqual(
shares[1]['share_network_id'],
result['shares'][0]['share_network_id'])
def test_share_list_detail_with_search_opts_by_non_admin(self):
self._share_list_detail_with_search_opts(use_admin_context=False)
def test_share_list_detail_with_search_opts_by_admin(self):
# none of search_opts should be filtered for admin
fake_key = 'fake_value'
name = 'fake_name'
status = 'available'
share_server_id = 'fake_share_server_id'
req = fakes.HTTPRequest.blank(
'/shares?fake_key=%s&name=%s&share_server_id=%s&'
'status=%s' % (fake_key, name, share_server_id, status),
use_admin_context=True,
)
self.stubs.Set(share_api.API, 'get_all', mock.Mock(return_value=[]))
self.controller.detail(req)
share_api.API.get_all.assert_called_once_with(
req.environ['manila.context'],
search_opts={
'display_name': name,
'fake_key': fake_key,
'share_server_id': share_server_id,
'status': status,
},
)
self._share_list_detail_with_search_opts(use_admin_context=True)
def test_share_list_detail(self):
self.stubs.Set(share_api.API, 'get_all',

View File

@ -158,7 +158,9 @@ class ShareAPITestCase(test.TestCase):
share_api.policy.check_policy.assert_called_once_with(
ctx, 'share', 'get_all')
db_driver.share_get_all_by_project.assert_called_once_with(
ctx, 'fake_pid_1')
ctx, sort_dir='desc', sort_key='created_at',
project_id='fake_pid_1', filters={},
)
self.assertEqual(shares, _FAKE_LIST_OF_ALL_SHARES[0])
def test_get_all_admin_filter_by_all_tenants(self):
@ -168,7 +170,8 @@ class ShareAPITestCase(test.TestCase):
shares = self.api.get_all(ctx, {'all_tenants': 1})
share_api.policy.check_policy.assert_called_once_with(
ctx, 'share', 'get_all')
db_driver.share_get_all.assert_called_once_with(ctx)
db_driver.share_get_all.assert_called_once_with(
ctx, sort_dir='desc', sort_key='created_at', filters={})
self.assertEqual(shares, _FAKE_LIST_OF_ALL_SHARES)
def test_get_all_non_admin_filter_by_share_server(self):
@ -206,7 +209,9 @@ class ShareAPITestCase(test.TestCase):
mock.call(ctx, 'share', 'list_by_share_server_id'),
])
db_driver.share_get_all_by_share_server.assert_called_once_with(
ctx, 'fake_server_3')
ctx, 'fake_server_3', sort_dir='desc', sort_key='created_at',
filters={},
)
db_driver.share_get_all_by_project.assert_has_calls([])
db_driver.share_get_all.assert_has_calls([])
self.assertEqual(shares, _FAKE_LIST_OF_ALL_SHARES[2:])
@ -220,7 +225,9 @@ class ShareAPITestCase(test.TestCase):
mock.call(ctx, 'share', 'get_all'),
])
db_driver.share_get_all_by_project.assert_called_once_with(
ctx, 'fake_pid_2')
ctx, sort_dir='desc', sort_key='created_at',
project_id='fake_pid_2', filters={},
)
self.assertEqual(shares, _FAKE_LIST_OF_ALL_SHARES[1::2])
def test_get_all_admin_filter_by_name_and_all_tenants(self):
@ -231,7 +238,8 @@ class ShareAPITestCase(test.TestCase):
share_api.policy.check_policy.assert_has_calls([
mock.call(ctx, 'share', 'get_all'),
])
db_driver.share_get_all.assert_called_once_with(ctx)
db_driver.share_get_all.assert_called_once_with(
ctx, sort_dir='desc', sort_key='created_at', filters={})
self.assertEqual(shares, _FAKE_LIST_OF_ALL_SHARES[::2])
def test_get_all_admin_filter_by_status(self):
@ -243,7 +251,9 @@ class ShareAPITestCase(test.TestCase):
mock.call(ctx, 'share', 'get_all'),
])
db_driver.share_get_all_by_project.assert_called_once_with(
ctx, 'fake_pid_2')
ctx, sort_dir='desc', sort_key='created_at',
project_id='fake_pid_2', filters={}
)
self.assertEqual(shares, _FAKE_LIST_OF_ALL_SHARES[2::4])
def test_get_all_admin_filter_by_status_and_all_tenants(self):
@ -254,7 +264,8 @@ class ShareAPITestCase(test.TestCase):
share_api.policy.check_policy.assert_has_calls([
mock.call(ctx, 'share', 'get_all'),
])
db_driver.share_get_all.assert_called_once_with(ctx)
db_driver.share_get_all.assert_called_once_with(
ctx, sort_dir='desc', sort_key='created_at', filters={})
self.assertEqual(shares, _FAKE_LIST_OF_ALL_SHARES[1::2])
def test_get_all_non_admin_filter_by_all_tenants(self):
@ -267,7 +278,9 @@ class ShareAPITestCase(test.TestCase):
mock.call(ctx, 'share', 'get_all'),
])
db_driver.share_get_all_by_project.assert_called_once_with(
ctx, 'fake_pid_2')
ctx, sort_dir='desc', sort_key='created_at',
project_id='fake_pid_2', filters={},
)
self.assertEqual(shares, _FAKE_LIST_OF_ALL_SHARES[1:])
def test_get_all_non_admin_with_name_and_status_filters(self):
@ -279,7 +292,9 @@ class ShareAPITestCase(test.TestCase):
mock.call(ctx, 'share', 'get_all'),
])
db_driver.share_get_all_by_project.assert_called_once_with(
ctx, 'fake_pid_2')
ctx, sort_dir='desc', sort_key='created_at',
project_id='fake_pid_2', filters={},
)
# two items expected, one filtered
self.assertEqual(shares, _FAKE_LIST_OF_ALL_SHARES[1::2])
@ -292,10 +307,86 @@ class ShareAPITestCase(test.TestCase):
mock.call(ctx, 'share', 'get_all'),
])
db_driver.share_get_all_by_project.assert_has_calls([
mock.call(ctx, 'fake_pid_2'),
mock.call(ctx, 'fake_pid_2'),
mock.call(ctx, sort_dir='desc', sort_key='created_at',
project_id='fake_pid_2', filters={}),
mock.call(ctx, sort_dir='desc', sort_key='created_at',
project_id='fake_pid_2', filters={}),
])
def test_get_all_with_sorting_valid(self):
self.stubs.Set(db_driver, 'share_get_all_by_project',
mock.Mock(return_value=_FAKE_LIST_OF_ALL_SHARES[0]))
ctx = context.RequestContext('fake_uid', 'fake_pid_1', is_admin=False)
shares = self.api.get_all(ctx, sort_key='status', sort_dir='asc')
share_api.policy.check_policy.assert_called_once_with(
ctx, 'share', 'get_all')
db_driver.share_get_all_by_project.assert_called_once_with(
ctx, sort_dir='asc', sort_key='status',
project_id='fake_pid_1', filters={},
)
self.assertEqual(_FAKE_LIST_OF_ALL_SHARES[0], shares)
def test_get_all_sort_key_invalid(self):
self.stubs.Set(db_driver, 'share_get_all_by_project',
mock.Mock(return_value=_FAKE_LIST_OF_ALL_SHARES[0]))
ctx = context.RequestContext('fake_uid', 'fake_pid_1', is_admin=False)
self.assertRaises(
exception.InvalidInput,
self.api.get_all,
ctx,
sort_key=1,
)
share_api.policy.check_policy.assert_called_once_with(
ctx, 'share', 'get_all')
def test_get_all_sort_dir_invalid(self):
self.stubs.Set(db_driver, 'share_get_all_by_project',
mock.Mock(return_value=_FAKE_LIST_OF_ALL_SHARES[0]))
ctx = context.RequestContext('fake_uid', 'fake_pid_1', is_admin=False)
self.assertRaises(
exception.InvalidInput,
self.api.get_all,
ctx,
sort_dir=1,
)
share_api.policy.check_policy.assert_called_once_with(
ctx, 'share', 'get_all')
def _get_all_filter_metadata_or_extra_specs_valid(self, key):
self.stubs.Set(db_driver, 'share_get_all_by_project',
mock.Mock(return_value=_FAKE_LIST_OF_ALL_SHARES[0]))
ctx = context.RequestContext('fake_uid', 'fake_pid_1', is_admin=False)
search_opts = {key: {'foo1': 'bar1', 'foo2': 'bar2'}}
shares = self.api.get_all(ctx, search_opts=search_opts.copy())
share_api.policy.check_policy.assert_called_once_with(
ctx, 'share', 'get_all')
db_driver.share_get_all_by_project.assert_called_once_with(
ctx, sort_dir='desc', sort_key='created_at',
project_id='fake_pid_1', filters=search_opts)
self.assertEqual(_FAKE_LIST_OF_ALL_SHARES[0], shares)
def test_get_all_filter_by_metadata(self):
self._get_all_filter_metadata_or_extra_specs_valid(key='metadata')
def test_get_all_filter_by_extra_specs(self):
self._get_all_filter_metadata_or_extra_specs_valid(key='extra_specs')
def _get_all_filter_metadata_or_extra_specs_invalid(self, key):
self.stubs.Set(db_driver, 'share_get_all_by_project',
mock.Mock(return_value=_FAKE_LIST_OF_ALL_SHARES[0]))
ctx = context.RequestContext('fake_uid', 'fake_pid_1', is_admin=False)
search_opts = {key: "{'foo': 'bar'}"}
self.assertRaises(exception.InvalidInput, self.api.get_all, ctx,
search_opts=search_opts)
share_api.policy.check_policy.assert_called_once_with(
ctx, 'share', 'get_all')
def test_get_all_filter_by_invalid_metadata(self):
self._get_all_filter_metadata_or_extra_specs_invalid(key='metadata')
def test_get_all_filter_by_invalid_extra_specs(self):
self._get_all_filter_metadata_or_extra_specs_invalid(key='extra_specs')
def test_create(self):
date = datetime.datetime(1, 1, 1, 1, 1, 1)
self.mock_utcnow.return_value = date