Add ability to filter by volume_glance_metadata

This feature allows users to more conveniently query volume details by
filtering the volume list by certain image metadata.
For example, users can query a specific bootable volume quickly
filtering by image_name or other glance metadata.

APIImpact
1. User can use glance metadata to filter volume detail in cinder api.
   The query url is like this:
   "volumes/detail?glance_metadata={"image_name":"xxx"}"

2. Since microversion is implemented in M, this change will add a new
   version "3.4".

DocImpact
1.Operator would need to add glance_metadata to 'query_volume_filters'
option for new functionality to work.

Change-Id: I1d276d93ad5e799401b48d2234e61c28a3aaf790
Implements: blueprint support-volume-glance-metadata-query
This commit is contained in:
wanghao 2015-01-16 11:22:21 +08:00
parent 8091e9f737
commit fca31fc95e
8 changed files with 114 additions and 9 deletions

View File

@ -50,6 +50,7 @@ REST_API_VERSION_HISTORY = """
* 3.2 - Bootable filters in volume GET call no longer treats all values * 3.2 - Bootable filters in volume GET call no longer treats all values
passed to it as true. passed to it as true.
* 3.3 - Add user messages APIs. * 3.3 - Add user messages APIs.
* 3.4 - Adds glance_metadata filter to list/detail volumes in _get_volumes.
""" """
@ -58,7 +59,7 @@ REST_API_VERSION_HISTORY = """
# minimum version of the API supported. # minimum version of the API supported.
# Explicitly using /v1 or /v2 enpoints will still work # Explicitly using /v1 or /v2 enpoints will still work
_MIN_API_VERSION = "3.0" _MIN_API_VERSION = "3.0"
_MAX_API_VERSION = "3.3" _MAX_API_VERSION = "3.4"
_LEGACY_API_VERSION1 = "1.0" _LEGACY_API_VERSION1 = "1.0"
_LEGACY_API_VERSION2 = "2.0" _LEGACY_API_VERSION2 = "2.0"
@ -128,7 +129,7 @@ class APIVersionRequest(utils.ComparableMixin):
method.end_version, method.end_version,
method.experimental) method.experimental)
def matches(self, min_version, max_version, experimental=False): def matches(self, min_version, max_version=None, experimental=False):
"""Compares this version to the specified min/max range. """Compares this version to the specified min/max range.
Returns whether the version object represents a version Returns whether the version object represents a version

View File

@ -55,3 +55,8 @@ user documentation.
3.3 3.3
--- ---
Added /messages API. Added /messages API.
3.4
---
Added the filter parameters ``glance_metadata`` to
list/detail volumes requests.

View File

@ -97,6 +97,9 @@ class VolumeController(wsgi.Controller):
sort_keys, sort_dirs = common.get_sort_params(params) sort_keys, sort_dirs = common.get_sort_params(params)
filters = params filters = params
# NOTE(wanghao): Always removing glance_metadata since we support it
# only in API version >= 3.4.
filters.pop('glance_metadata', None)
utils.remove_invalid_filter_options(context, utils.remove_invalid_filter_options(context,
filters, filters,
self._get_volume_filter_options()) self._get_volume_filter_options())

View File

@ -22,20 +22,25 @@ from cinder import utils
class VolumeController(volumes_v2.VolumeController): class VolumeController(volumes_v2.VolumeController):
"""The Volumes API controller for the OpenStack API V3.""" """The Volumes API controller for the OpenStack API V3."""
def __init__(self, ext_mgr):
super(VolumeController, self).__init__(volumes_v2.VolumeController)
def _get_volumes(self, req, is_detail): def _get_volumes(self, req, is_detail):
"""Returns a list of volumes, transformed through view builder.""" """Returns a list of volumes, transformed through view builder."""
context = req.environ['cinder.context'] context = req.environ['cinder.context']
req_version = req.api_version_request
params = req.params.copy() params = req.params.copy()
marker, limit, offset = common.get_pagination_params(params) marker, limit, offset = common.get_pagination_params(params)
sort_keys, sort_dirs = common.get_sort_params(params) sort_keys, sort_dirs = common.get_sort_params(params)
filters = params filters = params
utils.remove_invalid_filter_options(context, if req_version.matches(None, "3.3"):
filters, filters.pop('glance_metadata', None)
self._get_volume_filter_options())
utils.remove_invalid_filter_options(context, filters,
self._get_volume_filter_options())
# NOTE(thingee): v2 API allows name instead of display_name # NOTE(thingee): v2 API allows name instead of display_name
if 'name' in sort_keys: if 'name' in sort_keys:
sort_keys[sort_keys.index('name')] = 'display_name' sort_keys[sort_keys.index('name')] = 'display_name'

View File

@ -1694,10 +1694,10 @@ def _process_volume_filters(query, filters):
# Apply exact match filters for everything else, ensure that the # Apply exact match filters for everything else, ensure that the
# filter value exists on the model # filter value exists on the model
for key in filters.keys(): for key in filters.keys():
# metadata is unique, must be a dict # metadata/glance_metadata is unique, must be a dict
if key == 'metadata': if key in ('metadata', 'glance_metadata'):
if not isinstance(filters[key], dict): if not isinstance(filters[key], dict):
LOG.debug("'metadata' filter value is not valid.") LOG.debug("'%s' filter value is not valid.", key)
return None return None
continue continue
try: try:
@ -1727,6 +1727,11 @@ def _process_volume_filters(query, filters):
for k, v in value.items(): for k, v in value.items():
query = query.filter(or_(col_attr.any(key=k, value=v), query = query.filter(or_(col_attr.any(key=k, value=v),
col_ad_attr.any(key=k, value=v))) col_ad_attr.any(key=k, value=v)))
elif key == 'glance_metadata':
# use models.Volume.volume_glance_metadata as column attribute key.
col_gl_attr = models.Volume.volume_glance_metadata
for k, v in value.items():
query = query.filter(col_gl_attr.any(key=k, value=v))
elif isinstance(value, (list, tuple, set, frozenset)): elif isinstance(value, (list, tuple, set, frozenset)):
# Looking for values in a list; apply to query directly # Looking for values in a list; apply to query directly
column_attr = getattr(models.Volume, key) column_attr = getattr(models.Volume, key)

View File

@ -1,3 +1,5 @@
# Copyright 2016 OpenStack Foundation
# All Rights Reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -13,11 +15,13 @@
import mock import mock
from oslo_config import cfg
from cinder.api import extensions from cinder.api import extensions
from cinder.api.openstack import api_version_request as api_version from cinder.api.openstack import api_version_request as api_version
from cinder.api.v3 import volumes from cinder.api.v3 import volumes
from cinder import context from cinder import context
from cinder import db
from cinder import test from cinder import test
from cinder.tests.unit.api import fakes from cinder.tests.unit.api import fakes
from cinder.tests.unit import fake_constants as fake from cinder.tests.unit import fake_constants as fake
@ -26,9 +30,10 @@ from cinder.volume.api import API as vol_get
version_header_name = 'OpenStack-API-Version' version_header_name = 'OpenStack-API-Version'
CONF = cfg.CONF
class VolumeApiTest(test.TestCase): class VolumeApiTest(test.TestCase):
def setUp(self): def setUp(self):
super(VolumeApiTest, self).setUp() super(VolumeApiTest, self).setUp()
self.ext_mgr = extensions.ExtensionManager() self.ext_mgr = extensions.ExtensionManager()
@ -70,3 +75,41 @@ class VolumeApiTest(test.TestCase):
filters = req.params.copy() filters = req.params.copy()
volume_get.assert_called_with(filters, True) volume_get.assert_called_with(filters, True)
def _create_volume_with_glance_metadata(self):
vol1 = db.volume_create(self.ctxt, {'display_name': 'test1',
'project_id':
self.ctxt.project_id})
db.volume_glance_metadata_create(self.ctxt, vol1.id, 'image_name',
'imageTestOne')
vol2 = db.volume_create(self.ctxt, {'display_name': 'test2',
'project_id':
self.ctxt.project_id})
db.volume_glance_metadata_create(self.ctxt, vol2.id, 'image_name',
'imageTestTwo')
db.volume_glance_metadata_create(self.ctxt, vol2.id, 'disk_format',
'qcow2')
return [vol1, vol2]
def test_volume_index_filter_by_glance_metadata(self):
vols = self._create_volume_with_glance_metadata()
req = fakes.HTTPRequest.blank("/v3/volumes?glance_metadata="
"{'image_name': 'imageTestOne'}")
req.headers["OpenStack-API-Version"] = "volume 3.4"
req.api_version_request = api_version.APIVersionRequest('3.4')
req.environ['cinder.context'] = self.ctxt
res_dict = self.controller.index(req)
volumes = res_dict['volumes']
self.assertEqual(1, len(volumes))
self.assertEqual(vols[0].id, volumes[0]['id'])
def test_volume_index_filter_by_glance_metadata_in_unsupport_version(self):
self._create_volume_with_glance_metadata()
req = fakes.HTTPRequest.blank("/v3/volumes?glance_metadata="
"{'image_name': 'imageTestOne'}")
req.headers["OpenStack-API-Version"] = "volume 3.0"
req.api_version_request = api_version.APIVersionRequest('3.0')
req.environ['cinder.context'] = self.ctxt
res_dict = self.controller.index(req)
volumes = res_dict['volumes']
self.assertEqual(2, len(volumes))

View File

@ -1180,6 +1180,44 @@ class DBAPIVolumeTestCase(BaseTest):
'deleted', 'deleted_at', 'deleted', 'deleted_at',
'updated_at']) 'updated_at'])
def _create_volume_with_image_metadata(self):
vol1 = db.volume_create(self.ctxt, {'display_name': 'test1'})
db.volume_glance_metadata_create(self.ctxt, vol1.id, 'image_name',
'imageTestOne')
db.volume_glance_metadata_create(self.ctxt, vol1.id, 'test_image_key',
'test_image_value')
vol2 = db.volume_create(self.ctxt, {'display_name': 'test2'})
db.volume_glance_metadata_create(self.ctxt, vol2.id, 'image_name',
'imageTestTwo')
db.volume_glance_metadata_create(self.ctxt, vol2.id, 'disk_format',
'qcow2')
return [vol1, vol2]
def test_volume_get_all_by_image_name_and_key(self):
vols = self._create_volume_with_image_metadata()
filters = {'glance_metadata': {'image_name': 'imageTestOne',
'test_image_key': 'test_image_value'}}
volumes = db.volume_get_all(self.ctxt, None, None, ['created_at'],
['desc'], filters=filters)
self._assertEqualListsOfObjects([vols[0]], volumes)
def test_volume_get_all_by_image_name_and_disk_format(self):
vols = self._create_volume_with_image_metadata()
filters = {'glance_metadata': {'image_name': 'imageTestTwo',
'disk_format': 'qcow2'}}
volumes = db.volume_get_all(self.ctxt, None, None, ['created_at'],
['desc'], filters=filters)
self._assertEqualListsOfObjects([vols[1]], volumes)
def test_volume_get_all_by_invalid_image_metadata(self):
# Test with invalid image metadata
self._create_volume_with_image_metadata()
filters = {'glance_metadata': {'invalid_key': 'invalid_value',
'test_image_key': 'test_image_value'}}
volumes = db.volume_get_all(self.ctxt, None, None, ['created_at'],
['desc'], filters=filters)
self._assertEqualListsOfObjects([], volumes)
class DBAPISnapshotTestCase(BaseTest): class DBAPISnapshotTestCase(BaseTest):

View File

@ -0,0 +1,5 @@
---
features:
- Added support for querying volumes filtered by glance metadata key/value
using 'glance_metadata' optional URL parameter.
For example, "volumes/detail?glance_metadata={"image_name":"xxx"}".