Sort locations based on store weight
Related to blueprint store-weight Change-Id: I2383a476cb7e79c7efecdf33203cff0b50ef3bbb
This commit is contained in:
parent
46c30f0b6d
commit
fd222f3128
@ -37,6 +37,9 @@ stores-detail:
|
|||||||
for more information.)
|
for more information.)
|
||||||
``read-only`` (optional)
|
``read-only`` (optional)
|
||||||
Included only when the store is read only.
|
Included only when the store is read only.
|
||||||
|
``weight`` (default 0)
|
||||||
|
Contains weight (positive integer) to sort image locations for
|
||||||
|
preference.
|
||||||
``properties``
|
``properties``
|
||||||
Contains store specific properties
|
Contains store specific properties
|
||||||
in: body
|
in: body
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"type": "rbd",
|
"type": "rbd",
|
||||||
"description": "More expensive store with data redundancy",
|
"description": "More expensive store with data redundancy",
|
||||||
"default": true,
|
"default": true,
|
||||||
|
"weight": 100,
|
||||||
"properties": {
|
"properties": {
|
||||||
"pool": "pool1",
|
"pool": "pool1",
|
||||||
"chunk_size": 65536,
|
"chunk_size": 65536,
|
||||||
@ -15,6 +16,7 @@
|
|||||||
"id":"cheap",
|
"id":"cheap",
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"description": "Less expensive store for seldom-used images",
|
"description": "Less expensive store for seldom-used images",
|
||||||
|
"weight": 200,
|
||||||
"properties": {
|
"properties": {
|
||||||
"datadir": "fdir",
|
"datadir": "fdir",
|
||||||
"chunk_size": 65536,
|
"chunk_size": 65536,
|
||||||
@ -25,6 +27,7 @@
|
|||||||
"id":"fast",
|
"id":"fast",
|
||||||
"type": "cinder",
|
"type": "cinder",
|
||||||
"description": "Reasonably-priced fast store",
|
"description": "Reasonably-priced fast store",
|
||||||
|
"weight": 300,
|
||||||
"properties": {
|
"properties": {
|
||||||
"volume_type": "volume1",
|
"volume_type": "volume1",
|
||||||
"use_multipath": false
|
"use_multipath": false
|
||||||
@ -34,6 +37,7 @@
|
|||||||
"id":"slow",
|
"id":"slow",
|
||||||
"type": "swift",
|
"type": "swift",
|
||||||
"description": "Entry-level store balancing price and speed",
|
"description": "Entry-level store balancing price and speed",
|
||||||
|
"weight": 400,
|
||||||
"properties": {
|
"properties": {
|
||||||
"container": "container1",
|
"container": "container1",
|
||||||
"large_object_size": 52428,
|
"large_object_size": 52428,
|
||||||
|
@ -149,6 +149,7 @@ class InfoController(object):
|
|||||||
store['id'])
|
store['id'])
|
||||||
store['properties'] = store_mapper.get(store_type)(
|
store['properties'] = store_mapper.get(store_type)(
|
||||||
store_detail)
|
store_detail)
|
||||||
|
store['weight'] = getattr(CONF, store['id']).weight
|
||||||
|
|
||||||
except exception.Forbidden as e:
|
except exception.Forbidden as e:
|
||||||
LOG.debug("User not permitted to view details")
|
LOG.debug("User not permitted to view details")
|
||||||
|
@ -36,7 +36,6 @@ from glance.api import common
|
|||||||
from glance.api import policy
|
from glance.api import policy
|
||||||
from glance.api.v2 import policy as api_policy
|
from glance.api.v2 import policy as api_policy
|
||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
from glance.common import location_strategy
|
|
||||||
from glance.common import store_utils
|
from glance.common import store_utils
|
||||||
from glance.common import timeutils
|
from glance.common import timeutils
|
||||||
from glance.common import utils
|
from glance.common import utils
|
||||||
@ -1610,7 +1609,7 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
|
|||||||
locations = _get_image_locations(image)
|
locations = _get_image_locations(image)
|
||||||
if locations:
|
if locations:
|
||||||
# Choose best location configured strategy
|
# Choose best location configured strategy
|
||||||
loc = location_strategy.choose_best_location(locations)
|
loc = utils.sort_image_locations(locations)[0]
|
||||||
image_view['direct_url'] = loc['url']
|
image_view['direct_url'] = loc['url']
|
||||||
else:
|
else:
|
||||||
LOG.debug("The 'locations' list of image %s is empty, "
|
LOG.debug("The 'locations' list of image %s is empty, "
|
||||||
|
@ -42,9 +42,10 @@ from oslo_utils import strutils
|
|||||||
from webob import exc
|
from webob import exc
|
||||||
|
|
||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
|
from glance.common import location_strategy
|
||||||
from glance.common import timeutils
|
from glance.common import timeutils
|
||||||
from glance.common import wsgi
|
from glance.common import wsgi
|
||||||
from glance.i18n import _, _LE
|
from glance.i18n import _, _LE, _LW
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
@ -712,3 +713,25 @@ def get_stores_from_request(req, body):
|
|||||||
for store in stores:
|
for store in stores:
|
||||||
glance_store.get_store_from_store_identifier(store)
|
glance_store.get_store_from_store_identifier(store)
|
||||||
return stores
|
return stores
|
||||||
|
|
||||||
|
|
||||||
|
def sort_image_locations(locations):
|
||||||
|
if not CONF.enabled_backends:
|
||||||
|
return location_strategy.get_ordered_locations(locations)
|
||||||
|
|
||||||
|
def get_store_weight(location):
|
||||||
|
store_id = location['metadata'].get('store')
|
||||||
|
if not store_id:
|
||||||
|
return 0
|
||||||
|
try:
|
||||||
|
store = glance_store.get_store_from_store_identifier(store_id)
|
||||||
|
except glance_store.exceptions.UnknownScheme:
|
||||||
|
msg = (_LW("Unable to find store '%s', returning "
|
||||||
|
"default weight '0'") % store_id)
|
||||||
|
LOG.warning(msg)
|
||||||
|
return 0
|
||||||
|
return store.weight if store is not None else 0
|
||||||
|
|
||||||
|
sorted_locations = sorted(locations, key=get_store_weight, reverse=True)
|
||||||
|
LOG.debug(('Sorted locations: %s'), sorted_locations)
|
||||||
|
return sorted_locations
|
||||||
|
@ -24,7 +24,7 @@ from wsme.rest import json
|
|||||||
from glance.api.v2.model.metadef_property_type import PropertyType
|
from glance.api.v2.model.metadef_property_type import PropertyType
|
||||||
from glance.common import crypt
|
from glance.common import crypt
|
||||||
from glance.common import exception
|
from glance.common import exception
|
||||||
from glance.common import location_strategy
|
from glance.common import utils as common_utils
|
||||||
import glance.domain
|
import glance.domain
|
||||||
import glance.domain.proxy
|
import glance.domain.proxy
|
||||||
from glance.i18n import _
|
from glance.i18n import _
|
||||||
@ -127,7 +127,7 @@ class ImageRepo(object):
|
|||||||
min_disk=db_image['min_disk'],
|
min_disk=db_image['min_disk'],
|
||||||
min_ram=db_image['min_ram'],
|
min_ram=db_image['min_ram'],
|
||||||
protected=db_image['protected'],
|
protected=db_image['protected'],
|
||||||
locations=location_strategy.get_ordered_locations(locations),
|
locations=common_utils.sort_image_locations(locations),
|
||||||
checksum=db_image['checksum'],
|
checksum=db_image['checksum'],
|
||||||
os_hash_algo=db_image['os_hash_algo'],
|
os_hash_algo=db_image['os_hash_algo'],
|
||||||
os_hash_value=db_image['os_hash_value'],
|
os_hash_value=db_image['os_hash_value'],
|
||||||
|
@ -131,6 +131,7 @@ class TestDiscovery(functional.SynchronousAPIBase):
|
|||||||
"id": "store1",
|
"id": "store1",
|
||||||
"default": "true",
|
"default": "true",
|
||||||
"type": "file",
|
"type": "file",
|
||||||
|
"weight": 0,
|
||||||
"properties": {
|
"properties": {
|
||||||
"data_dir": self._store_dir('store1'),
|
"data_dir": self._store_dir('store1'),
|
||||||
"chunk_size": 65536,
|
"chunk_size": 65536,
|
||||||
@ -140,6 +141,7 @@ class TestDiscovery(functional.SynchronousAPIBase):
|
|||||||
{
|
{
|
||||||
"id": "store2",
|
"id": "store2",
|
||||||
"type": "file",
|
"type": "file",
|
||||||
|
"weight": 0,
|
||||||
"properties": {
|
"properties": {
|
||||||
"data_dir": self._store_dir('store2'),
|
"data_dir": self._store_dir('store2'),
|
||||||
"chunk_size": 65536,
|
"chunk_size": 65536,
|
||||||
@ -149,6 +151,7 @@ class TestDiscovery(functional.SynchronousAPIBase):
|
|||||||
{
|
{
|
||||||
"id": "store3",
|
"id": "store3",
|
||||||
"type": "file",
|
"type": "file",
|
||||||
|
"weight": 0,
|
||||||
"properties": {
|
"properties": {
|
||||||
"data_dir": self._store_dir('store3'),
|
"data_dir": self._store_dir('store3'),
|
||||||
"chunk_size": 65536,
|
"chunk_size": 65536,
|
||||||
|
@ -7318,3 +7318,38 @@ class TestKeystoneQuotas(functional.SynchronousAPIBase):
|
|||||||
|
|
||||||
# Make sure we can still import.
|
# Make sure we can still import.
|
||||||
self._create_and_import(stores=['store1'])
|
self._create_and_import(stores=['store1'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestStoreWeight(functional.SynchronousAPIBase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestStoreWeight, self).setUp()
|
||||||
|
|
||||||
|
def test_store_weight_combinations(self):
|
||||||
|
self.start_server()
|
||||||
|
# Import image in all available stores
|
||||||
|
image_id = self._create_and_import(stores=['store1', 'store2',
|
||||||
|
'store3'])
|
||||||
|
# make sure as weight is default, we will get locations based
|
||||||
|
# on insertion order
|
||||||
|
image = self.api_get('/v2/images/%s' % image_id).json
|
||||||
|
self.assertEqual("store1,store2,store3", image['stores'])
|
||||||
|
|
||||||
|
# give highest weight to store2 then store3 and then store1
|
||||||
|
self.config(weight=200, group='store2')
|
||||||
|
self.config(weight=100, group='store3')
|
||||||
|
self.config(weight=50, group='store1')
|
||||||
|
self.start_server()
|
||||||
|
# make sure as per store weight locations will be sorted
|
||||||
|
# as store2,store3,store1
|
||||||
|
image = self.api_get('/v2/images/%s' % image_id).json
|
||||||
|
self.assertEqual("store2,store3,store1", image['stores'])
|
||||||
|
|
||||||
|
# give highest weight to store3 then store1 and then store2
|
||||||
|
self.config(weight=20, group='store2')
|
||||||
|
self.config(weight=100, group='store3')
|
||||||
|
self.config(weight=50, group='store1')
|
||||||
|
self.start_server()
|
||||||
|
# make sure as per store weight locations will be sorted
|
||||||
|
# as store3,store1,store2
|
||||||
|
image = self.api_get('/v2/images/%s' % image_id).json
|
||||||
|
self.assertEqual("store3,store1,store2", image['stores'])
|
||||||
|
@ -155,6 +155,139 @@ class TestCinderStoreUtils(base.MultiStoreClearingUnitTest):
|
|||||||
class TestUtils(test_utils.BaseTestCase):
|
class TestUtils(test_utils.BaseTestCase):
|
||||||
"""Test routines in glance.utils"""
|
"""Test routines in glance.utils"""
|
||||||
|
|
||||||
|
def test_sort_image_locations_multistore_disabled(self):
|
||||||
|
self.config(enabled_backends=None)
|
||||||
|
locations = [{
|
||||||
|
'url': 'rbd://aaaaaaaa/images/id',
|
||||||
|
'metadata': {'store': 'rbd1'}
|
||||||
|
}, {
|
||||||
|
'url': 'rbd://bbbbbbbb/images/id',
|
||||||
|
'metadata': {'store': 'rbd2'}
|
||||||
|
}, {
|
||||||
|
'url': 'rbd://cccccccc/images/id',
|
||||||
|
'metadata': {'store': 'rbd3'}
|
||||||
|
}]
|
||||||
|
mp = "glance.common.utils.glance_store.get_store_from_store_identifier"
|
||||||
|
with mock.patch(mp) as mock_get_store:
|
||||||
|
utils.sort_image_locations(locations)
|
||||||
|
|
||||||
|
# Since multistore is not enabled, it will not sort the locations
|
||||||
|
self.assertEqual(0, mock_get_store.call_count)
|
||||||
|
|
||||||
|
def test_sort_image_locations(self):
|
||||||
|
enabled_backends = {
|
||||||
|
"rbd1": "rbd",
|
||||||
|
"rbd2": "rbd",
|
||||||
|
"rbd3": "rbd"
|
||||||
|
}
|
||||||
|
self.config(enabled_backends=enabled_backends)
|
||||||
|
store.register_store_opts(CONF)
|
||||||
|
self.config(default_backend="rbd1", group="glance_store")
|
||||||
|
locations = [{
|
||||||
|
'url': 'rbd://aaaaaaaa/images/id',
|
||||||
|
'metadata': {'store': 'rbd1'}
|
||||||
|
}, {
|
||||||
|
'url': 'rbd://bbbbbbbb/images/id',
|
||||||
|
'metadata': {'store': 'rbd2'}
|
||||||
|
}, {
|
||||||
|
'url': 'rbd://cccccccc/images/id',
|
||||||
|
'metadata': {'store': 'rbd3'}
|
||||||
|
}]
|
||||||
|
mp = "glance.common.utils.glance_store.get_store_from_store_identifier"
|
||||||
|
with mock.patch(mp) as mock_get_store:
|
||||||
|
mock_store = mock_get_store.return_value
|
||||||
|
mock_store.weight = 100
|
||||||
|
utils.sort_image_locations(locations)
|
||||||
|
|
||||||
|
# Since 3 stores are configured, internal method will be called 3 times
|
||||||
|
self.assertEqual(3, mock_get_store.call_count)
|
||||||
|
|
||||||
|
def test_sort_image_locations_without_metadata(self):
|
||||||
|
enabled_backends = {
|
||||||
|
"rbd1": "rbd",
|
||||||
|
"rbd2": "rbd",
|
||||||
|
"rbd3": "rbd"
|
||||||
|
}
|
||||||
|
self.config(enabled_backends=enabled_backends)
|
||||||
|
store.register_store_opts(CONF)
|
||||||
|
self.config(default_backend="rbd1", group="glance_store")
|
||||||
|
locations = [{
|
||||||
|
'url': 'rbd://aaaaaaaa/images/id',
|
||||||
|
'metadata': {}
|
||||||
|
}, {
|
||||||
|
'url': 'rbd://bbbbbbbb/images/id',
|
||||||
|
'metadata': {}
|
||||||
|
}, {
|
||||||
|
'url': 'rbd://cccccccc/images/id',
|
||||||
|
'metadata': {}
|
||||||
|
}]
|
||||||
|
mp = "glance.common.utils.glance_store.get_store_from_store_identifier"
|
||||||
|
with mock.patch(mp) as mock_get_store:
|
||||||
|
utils.sort_image_locations(locations)
|
||||||
|
|
||||||
|
# Since 3 stores are configured, without store in metadata the internal
|
||||||
|
# method will be called 0 times
|
||||||
|
self.assertEqual(0, mock_get_store.call_count)
|
||||||
|
|
||||||
|
def test_sort_image_locations_with_partial_metadata(self):
|
||||||
|
enabled_backends = {
|
||||||
|
"rbd1": "rbd",
|
||||||
|
"rbd2": "rbd",
|
||||||
|
"rbd3": "rbd"
|
||||||
|
}
|
||||||
|
self.config(enabled_backends=enabled_backends)
|
||||||
|
store.register_store_opts(CONF)
|
||||||
|
self.config(default_backend="rbd1", group="glance_store")
|
||||||
|
locations = [{
|
||||||
|
'url': 'rbd://aaaaaaaa/images/id',
|
||||||
|
'metadata': {'store': 'rbd1'}
|
||||||
|
}, {
|
||||||
|
'url': 'rbd://bbbbbbbb/images/id',
|
||||||
|
'metadata': {}
|
||||||
|
}, {
|
||||||
|
'url': 'rbd://cccccccc/images/id',
|
||||||
|
'metadata': {}
|
||||||
|
}]
|
||||||
|
mp = "glance.common.utils.glance_store.get_store_from_store_identifier"
|
||||||
|
with mock.patch(mp) as mock_get_store:
|
||||||
|
mock_store = mock_get_store.return_value
|
||||||
|
mock_store.weight = 100
|
||||||
|
utils.sort_image_locations(locations)
|
||||||
|
|
||||||
|
# Since 3 stores are configured, but only one location has
|
||||||
|
# store in metadata the internal
|
||||||
|
# method will be called 1 time only
|
||||||
|
self.assertEqual(1, mock_get_store.call_count)
|
||||||
|
|
||||||
|
def test_sort_image_locations_unknownscheme(self):
|
||||||
|
enabled_backends = {
|
||||||
|
"rbd1": "rbd",
|
||||||
|
"rbd2": "rbd",
|
||||||
|
"rbd3": "rbd"
|
||||||
|
}
|
||||||
|
self.config(enabled_backends=enabled_backends)
|
||||||
|
store.register_store_opts(CONF)
|
||||||
|
self.config(default_backend="rbd1", group="glance_store")
|
||||||
|
locations = [{
|
||||||
|
'url': 'rbd://aaaaaaaa/images/id',
|
||||||
|
'metadata': {'store': 'rbd1'}
|
||||||
|
}, {
|
||||||
|
'url': 'rbd://bbbbbbbb/images/id',
|
||||||
|
'metadata': {'store': 'rbd2'}
|
||||||
|
}, {
|
||||||
|
'url': 'rbd://cccccccc/images/id',
|
||||||
|
'metadata': {'store': 'rbd3'}
|
||||||
|
}]
|
||||||
|
mp = "glance.common.utils.glance_store.get_store_from_store_identifier"
|
||||||
|
with mock.patch(mp) as mock_get_store:
|
||||||
|
mock_get_store.side_effect = store.UnknownScheme()
|
||||||
|
sorted_locations = utils.sort_image_locations(locations)
|
||||||
|
|
||||||
|
# Even though UnknownScheme exception is raised, processing continues
|
||||||
|
self.assertEqual(3, mock_get_store.call_count)
|
||||||
|
# Since we return 0 weight, original location order should be preserved
|
||||||
|
self.assertEqual(locations, sorted_locations)
|
||||||
|
|
||||||
def test_cooperative_reader(self):
|
def test_cooperative_reader(self):
|
||||||
"""Ensure cooperative reader class accesses all bytes of file"""
|
"""Ensure cooperative reader class accesses all bytes of file"""
|
||||||
BYTES = 1024
|
BYTES = 1024
|
||||||
|
@ -46,6 +46,7 @@ class TestInfoControllers(base.MultiStoreClearingUnitTest):
|
|||||||
self.assertIn('stores', output)
|
self.assertIn('stores', output)
|
||||||
for stores in output['stores']:
|
for stores in output['stores']:
|
||||||
self.assertIn('id', stores)
|
self.assertIn('id', stores)
|
||||||
|
self.assertNotIn('weight', stores)
|
||||||
self.assertIn(stores['id'], available_stores)
|
self.assertIn(stores['id'], available_stores)
|
||||||
|
|
||||||
def test_get_stores_read_only_store(self):
|
def test_get_stores_read_only_store(self):
|
||||||
@ -108,6 +109,20 @@ class TestInfoControllers(base.MultiStoreClearingUnitTest):
|
|||||||
expected_attribute = store_attributes[store['type']]
|
expected_attribute = store_attributes[store['type']]
|
||||||
self.assertEqual(actual_attribute, expected_attribute)
|
self.assertEqual(actual_attribute, expected_attribute)
|
||||||
|
|
||||||
|
def test_get_stores_detail_with_store_weight(self):
|
||||||
|
self.config(weight=100, group='fast')
|
||||||
|
self.config(weight=200, group='cheap')
|
||||||
|
self.config(weight=300, group='fast-rbd')
|
||||||
|
self.config(weight=400, group='fast-cinder')
|
||||||
|
self.config(weight=500, group='reliable')
|
||||||
|
|
||||||
|
req = unit_test_utils.get_fake_request(roles=['admin'])
|
||||||
|
output = self.controller.get_stores_detail(req)
|
||||||
|
self.assertEqual(len(CONF.enabled_backends), len(output['stores']))
|
||||||
|
self.assertIn('stores', output)
|
||||||
|
for store in output['stores']:
|
||||||
|
self.assertIn('weight', store)
|
||||||
|
|
||||||
def test_get_stores_detail_non_admin(self):
|
def test_get_stores_detail_non_admin(self):
|
||||||
req = unit_test_utils.get_fake_request()
|
req = unit_test_utils.get_fake_request()
|
||||||
self.assertRaises(webob.exc.HTTPForbidden,
|
self.assertRaises(webob.exc.HTTPForbidden,
|
||||||
|
7
releasenotes/notes/store-weight-3ed3ee612579bc25.yaml
Normal file
7
releasenotes/notes/store-weight-3ed3ee612579bc25.yaml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
"GET" images API will now sort image locations based on store weight
|
||||||
|
configured for each store in configuration files. Image will be
|
||||||
|
downloaded from the store having highest weight configured. For default
|
||||||
|
weight scenario the locations will remain same as per insertion order.
|
Loading…
Reference in New Issue
Block a user