diff --git a/glance/api/v1/images.py b/glance/api/v1/images.py index 4d63ce06fc..ff582d9ffe 100644 --- a/glance/api/v1/images.py +++ b/glance/api/v1/images.py @@ -130,8 +130,8 @@ class Controller(wsgi.Controller): """ filters = {} - for param in SUPPORTED_FILTERS: - if param in req.str_params: + for param in req.str_params: + if param in SUPPORTED_FILTERS or param.startswith('property-'): filters[param] = req.str_params.get(param) return filters diff --git a/glance/registry/db/api.py b/glance/registry/db/api.py index e4f99905fe..14aa6fbe1c 100644 --- a/glance/registry/db/api.py +++ b/glance/registry/db/api.py @@ -172,9 +172,28 @@ def image_get_filtered(context, filters): del filters['size_max'] for (k, v) in filters.items(): - query = query.filter(getattr(models.Image, k) == v) + if not k.startswith('property-'): + query = query.filter(getattr(models.Image, k) == v) - return query.all() + images = query.all() + + #TODO(bcwaldon): use an actual sqlalchemy query to accomplish this + def prop_filter(key, value): + def func(image): + for prop in image.properties: + if prop.deleted == False and \ + prop.name == key and \ + prop.value == value: + return True + return False + return func + + for (k, v) in filters.items(): + if k.startswith('property-'): + _k = k[9:] + images = filter(prop_filter(_k, v), images) + + return images def _drop_protected_attrs(model_class, values): diff --git a/glance/registry/server.py b/glance/registry/server.py index 1dd9e63cb8..6312343aa6 100644 --- a/glance/registry/server.py +++ b/glance/registry/server.py @@ -102,8 +102,8 @@ class Controller(wsgi.Controller): """ filters = {} - for param in SUPPORTED_FILTERS: - if param in req.str_params: + for param in req.str_params: + if param in SUPPORTED_FILTERS or param.startswith('property-'): filters[param] = req.str_params.get(param) return filters diff --git a/tests/functional/test_curl_api.py b/tests/functional/test_curl_api.py index 26b122e43d..0b263e19e3 100644 --- a/tests/functional/test_curl_api.py +++ b/tests/functional/test_curl_api.py @@ -816,6 +816,7 @@ class TestCurlApi(functional.FunctionalTest): "-H 'X-Image-Meta-Disk-Format: vdi' " "-H 'X-Image-Meta-Size: 19' " "-H 'X-Image-Meta-Is-Public: True' " + "-H 'X-Image-Meta-Property-pants: are on' " "http://0.0.0.0:%d/v1/images") % api_port exitcode, out, err = execute(cmd) @@ -834,6 +835,7 @@ class TestCurlApi(functional.FunctionalTest): "-H 'X-Image-Meta-Disk-Format: vhd' " "-H 'X-Image-Meta-Size: 20' " "-H 'X-Image-Meta-Is-Public: True' " + "-H 'X-Image-Meta-Property-pants: are on' " "http://0.0.0.0:%d/v1/images") % api_port exitcode, out, err = execute(cmd) @@ -851,6 +853,7 @@ class TestCurlApi(functional.FunctionalTest): "-H 'X-Image-Meta-Disk-Format: ami' " "-H 'X-Image-Meta-Size: 21' " "-H 'X-Image-Meta-Is-Public: True' " + "-H 'X-Image-Meta-Property-pants: are off' " "http://0.0.0.0:%d/v1/images") % api_port exitcode, out, err = execute(cmd) @@ -964,3 +967,17 @@ class TestCurlApi(functional.FunctionalTest): self.assertEqual(len(images["images"]), 2) for image in images["images"]: self.assertTrue(image["size"] >= 20) + + # 9. GET /images with property filter + # Verify correct images returned with property + cmd = ("curl http://0.0.0.0:%d/v1/images/detail?" + "property-pants=are%%20on" % api_port) + + exitcode, out, err = execute(cmd) + + self.assertEqual(0, exitcode) + images = json.loads(out.strip()) + + self.assertEqual(len(images["images"]), 2) + for image in images["images"]: + self.assertEqual(image["properties"]["pants"], "are on") diff --git a/tests/stubs.py b/tests/stubs.py index c837e5c8ea..487b716eb3 100644 --- a/tests/stubs.py +++ b/tests/stubs.py @@ -401,8 +401,20 @@ def stub_out_registry_db_image_api(stubs): size_max = int(filters.pop('size_max')) images = [f for f in images if int(f['size']) <= size_max] + def _prop_filter(key, value): + def _func(image): + for prop in image['properties']: + if prop['name'] == key: + return prop['value'] == value + return False + return _func + for k, v in filters.items(): - images = [f for f in images if f[k] == v] + if k.startswith('property-'): + _k = k[9:] + images = filter(_prop_filter(_k, v), images) + else: + images = [f for f in images if f[k] == v] return images diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index 7b0c56becf..1c046247f0 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -162,11 +162,6 @@ class TestRegistryAPI(unittest.TestCase): public images that have a specific name """ - fixture = {'id': 2, - 'name': 'fake image #2', - 'size': 19, - 'checksum': None} - extra_fixture = {'id': 3, 'status': 'active', 'is_public': True, @@ -205,11 +200,6 @@ class TestRegistryAPI(unittest.TestCase): public images that have a specific status """ - fixture = {'id': 2, - 'name': 'fake image #2', - 'size': 19, - 'checksum': None} - extra_fixture = {'id': 3, 'status': 'saving', 'is_public': True, @@ -248,11 +238,6 @@ class TestRegistryAPI(unittest.TestCase): public images that have a specific container_format """ - fixture = {'id': 2, - 'name': 'fake image #2', - 'size': 19, - 'checksum': None} - extra_fixture = {'id': 3, 'status': 'active', 'is_public': True, @@ -291,11 +276,6 @@ class TestRegistryAPI(unittest.TestCase): public images that have a specific disk_format """ - fixture = {'id': 2, - 'name': 'fake image #2', - 'size': 19, - 'checksum': None} - extra_fixture = {'id': 3, 'status': 'active', 'is_public': True, @@ -334,11 +314,6 @@ class TestRegistryAPI(unittest.TestCase): public images that have a size greater than or equal to size_min """ - fixture = {'id': 2, - 'name': 'fake image #2', - 'size': 19, - 'checksum': None} - extra_fixture = {'id': 3, 'status': 'active', 'is_public': True, @@ -377,11 +352,6 @@ class TestRegistryAPI(unittest.TestCase): public images that have a size less than or equal to size_max """ - fixture = {'id': 2, - 'name': 'fake image #2', - 'size': 19, - 'checksum': None} - extra_fixture = {'id': 3, 'status': 'active', 'is_public': True, @@ -421,11 +391,6 @@ class TestRegistryAPI(unittest.TestCase): and greater than or equal to size_min """ - fixture = {'id': 2, - 'name': 'fake image #2', - 'size': 19, - 'checksum': None} - extra_fixture = {'id': 3, 'status': 'active', 'is_public': True, @@ -470,6 +435,46 @@ class TestRegistryAPI(unittest.TestCase): for image in images: self.assertTrue(image['size'] <= 19 and image['size'] >= 18) + def test_get_details_filter_property(self): + """Tests that the /images/detail registry API returns list of + public images that have a specific custom property + + """ + extra_fixture = {'id': 3, + 'status': 'active', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'fake image #3', + 'size': 19, + 'checksum': None, + 'properties': {'prop_123': 'v a'}} + + glance.registry.db.api.image_create(None, extra_fixture) + + extra_fixture = {'id': 4, + 'status': 'active', + 'is_public': True, + 'disk_format': 'ami', + 'container_format': 'ami', + 'name': 'fake image #4', + 'size': 19, + 'checksum': None, + 'properties': {'prop_123': 'v b'}} + + glance.registry.db.api.image_create(None, extra_fixture) + + req = webob.Request.blank('/images/detail?property-prop_123=v%20a') + res = req.get_response(self.api) + res_dict = json.loads(res.body) + self.assertEquals(res.status_int, 200) + + images = res_dict['images'] + self.assertEquals(len(images), 1) + + for image in images: + self.assertEqual('v a', image['properties']['prop_123']) + def test_create_image(self): """Tests that the /images POST registry API creates the image""" fixture = {'name': 'fake public image', diff --git a/tests/unit/test_clients.py b/tests/unit/test_clients.py index 5653f09aca..983b0517d6 100644 --- a/tests/unit/test_clients.py +++ b/tests/unit/test_clients.py @@ -222,6 +222,26 @@ class TestRegistryClient(unittest.TestCase): for image in images: self.assertTrue(image['size'] >= 20) + def test_get_image_details_by_property(self): + """Tests that a detailed call can be filtered by a property""" + extra_fixture = {'id': 3, + 'status': 'saving', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'ovf', + 'name': 'new name! #123', + 'size': 19, + 'checksum': None, + 'properties': {'p a': 'v a'}} + + glance.registry.db.api.image_create(None, extra_fixture) + + images = self.client.get_images_detailed({'property-p a': 'v a'}) + self.assertEquals(len(images), 1) + + for image in images: + self.assertEquals('v a', image['properties']['p a']) + def test_get_image(self): """Tests that the detailed info about an image returned""" fixture = {'id': 1,