Skip to content

Commit 9fdd4d5

Browse files
authored
Merge pull request #175 from sapcc/k8s_sharding
K8S volumes to be scheduled in the same shard
2 parents 1cd271a + 4b25cd0 commit 9fdd4d5

File tree

4 files changed

+162
-20
lines changed

4 files changed

+162
-20
lines changed

cinder/db/api.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,12 @@ def volume_get_all(context, marker=None, limit=None, sort_keys=None,
281281
offset=offset)
282282

283283

284+
def get_host_by_volume_metadata(key, value, filters=None):
285+
"""Returns the host with the most volumes matching volume metadata."""
286+
return IMPL.get_host_by_volume_metadata(key, value,
287+
filters=filters)
288+
289+
284290
def calculate_resource_count(context, resource_type, filters):
285291
return IMPL.calculate_resource_count(context, resource_type, filters)
286292

cinder/db/sqlalchemy/api.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2176,6 +2176,38 @@ def volume_get_all(context, marker=None, limit=None, sort_keys=None,
21762176
return query.all()
21772177

21782178

2179+
def get_host_by_volume_metadata(meta_key, meta_value, filters=None):
2180+
session = get_session()
2181+
count_label = func.count().label("n")
2182+
query = session.query(
2183+
func.substring_index(models.Volume.host, '@', 1).label("h"),
2184+
count_label
2185+
).join(
2186+
models.VolumeMetadata,
2187+
models.VolumeMetadata.volume_id == models.Volume.id
2188+
).filter(
2189+
models.VolumeMetadata.key == meta_key,
2190+
models.VolumeMetadata.value == meta_value,
2191+
models.Volume.deleted == 0,
2192+
models.Volume.host.isnot(None)
2193+
)
2194+
2195+
if filters:
2196+
az = filters.get('availability_zone')
2197+
if az:
2198+
query = query.filter(
2199+
models.Volume.availability_zone == az)
2200+
2201+
query = query.group_by("h")\
2202+
.order_by(desc(count_label)).limit(1)
2203+
2204+
with session.begin():
2205+
result = query.first()
2206+
if result:
2207+
return result[0]
2208+
return None
2209+
2210+
21792211
@require_context
21802212
def get_volume_summary(context, project_only, filters=None):
21812213
"""Retrieves all volumes summary.

cinder/scheduler/filters/shard_filter.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
from oslo_config import cfg
2020
from oslo_log import log as logging
2121

22+
from cinder import db
2223
from cinder.scheduler import filters
2324
from cinder.service_auth import SERVICE_USER_GROUP
2425
from cinder import utils as cinder_utils
26+
from cinder.volume import volume_utils
2527

2628

2729
LOG = logging.getLogger(__name__)
@@ -40,6 +42,8 @@
4042
service_type='identity')
4143
CONF.register_opts(keystone_opts, group=KEYSTONE_GROUP)
4244

45+
CSI_CLUSTER_METADATA_KEY = 'cinder.csi.openstack.org/cluster'
46+
4347

4448
class ShardFilter(filters.BaseBackendFilter):
4549
"""Filters backends by shard of the project
@@ -142,12 +146,60 @@ def _get_shards(self, project_id):
142146

143147
return self._PROJECT_SHARD_CACHE.get(project_id)
144148

145-
def backend_passes(self, backend_state, filter_properties):
146-
# We only need the shard filter for vmware based pools
149+
def _is_vmware(self, backend_state):
147150
if backend_state.vendor_name != 'VMware':
151+
return False
152+
return True
153+
154+
def filter_all(self, filter_obj_list, filter_properties):
155+
backends = self._filter_by_k8s_cluster(filter_obj_list,
156+
filter_properties)
157+
158+
return [b for b in backends if
159+
self._backend_passes(b, filter_properties)]
160+
161+
def _filter_by_k8s_cluster(self, backends, filter_properties):
162+
spec = filter_properties.get('request_spec', {})
163+
vol_props = spec.get('volume_properties', {})
164+
project_id = vol_props.get('project_id', None)
165+
metadata = vol_props.get('metadata', {})
166+
167+
is_vmware = any(self._is_vmware(b) for b in backends)
168+
if (not metadata or not project_id
169+
or spec.get('snapshot_id')
170+
or not is_vmware):
171+
return backends
172+
173+
cluster_name = metadata.get(CSI_CLUSTER_METADATA_KEY)
174+
if not cluster_name:
175+
return backends
176+
177+
props = spec.get('resource_properties', {})
178+
availability_zone = props.get('availability_zone')
179+
query_filters = None
180+
if availability_zone:
181+
query_filters = {'availability_zone': availability_zone}
182+
183+
k8s_host = db.get_host_by_volume_metadata(
184+
key=CSI_CLUSTER_METADATA_KEY,
185+
value=cluster_name,
186+
filters=query_filters)
187+
188+
if not k8s_host:
189+
return backends
190+
191+
return [
192+
b for b in backends if
193+
(not self._is_vmware(b) or
194+
volume_utils.extract_host(b.host, 'host') == k8s_host)
195+
]
196+
197+
def _backend_passes(self, backend_state, filter_properties):
198+
# We only need the shard filter for vmware based pools
199+
if not self._is_vmware(backend_state):
148200
LOG.info(
149-
"Shard Filter ignoring backend %s as it's not vmware based"
150-
" driver", backend_state.backend_id)
201+
"Shard Filter ignoring backend %s as it's not "
202+
"vmware based driver", backend_state.backend_id)
151203
return True
152204

153205
spec = filter_properties.get('request_spec', {})

cinder/tests/unit/scheduler/test_shard_filter.py

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import time
1515
from unittest import mock
1616

17+
from cinder import context
18+
from cinder.tests.unit import fake_constants
1719
from cinder.tests.unit.scheduler import fakes
1820
from cinder.tests.unit.scheduler.test_host_filters \
1921
import BackendFiltersTestCase
@@ -37,6 +39,8 @@ def setUp(self):
3739
}
3840
}
3941
}
42+
self.context = context.RequestContext(fake_constants.USER_ID,
43+
fake_constants.PROJECT_ID)
4044

4145
@mock.patch('cinder.scheduler.filters.shard_filter.'
4246
'ShardFilter._update_cache')
@@ -77,7 +81,7 @@ def test_shard_project_not_found(self, mock_update_cache):
7781
host = fakes.FakeBackendState('host1',
7882
{'capabilities': caps,
7983
'vendor_name': VMWARE_VENDOR})
80-
self.assertFalse(self.filt_cls.backend_passes(host, self.props))
84+
self.backend_no_pass(host, self.props)
8185

8286
def test_snapshot(self):
8387
snap_props = {
@@ -90,7 +94,7 @@ def test_snapshot(self):
9094
host = fakes.FakeBackendState('host1',
9195
{'capabilities': caps,
9296
'vendor_name': VMWARE_VENDOR})
93-
self.assertTrue(self.filt_cls.backend_passes(host, snap_props))
97+
self.backend_passes(host, snap_props)
9498

9599
def test_snapshot_None(self):
96100
snap_props = {
@@ -103,57 +107,57 @@ def test_snapshot_None(self):
103107
host = fakes.FakeBackendState('host1',
104108
{'capabilities': caps,
105109
'vendor_name': VMWARE_VENDOR})
106-
self.assertFalse(self.filt_cls.backend_passes(host, snap_props))
110+
self.backend_no_pass(host, snap_props)
107111

108112
def test_shard_project_no_shards(self):
109113
caps = {'vcenter-shard': 'vc-a-1'}
110114
self.filt_cls._PROJECT_SHARD_CACHE['foo'] = []
111115
host = fakes.FakeBackendState('host1',
112116
{'capabilities': caps,
113117
'vendor_name': VMWARE_VENDOR})
114-
self.assertFalse(self.filt_cls.backend_passes(host, self.props))
118+
self.backend_no_pass(host, self.props)
115119

116120
def test_backend_without_shard(self):
117121
host = fakes.FakeBackendState('host1', {'vendor_name': VMWARE_VENDOR})
118-
self.assertFalse(self.filt_cls.backend_passes(host, self.props))
122+
self.backend_no_pass(host, self.props)
119123

120124
def test_backend_shards_dont_match(self):
121125
caps = {'vcenter-shard': 'vc-a-1'}
122126
host = fakes.FakeBackendState('host1',
123127
{'capabilities': caps,
124128
'vendor_name': VMWARE_VENDOR})
125-
self.assertFalse(self.filt_cls.backend_passes(host, self.props))
129+
self.backend_no_pass(host, self.props)
126130

127131
def test_backend_shards_match(self):
128132
caps = {'vcenter-shard': 'vc-b-0'}
129133
host = fakes.FakeBackendState('host1',
130134
{'capabilities': caps,
131135
'vendor_name': VMWARE_VENDOR})
132-
self.assertTrue(self.filt_cls.backend_passes(host, self.props))
136+
self.backend_passes(host, self.props)
133137

134138
def test_shard_override_matches(self):
135139
caps = {'vcenter-shard': 'vc-a-1'}
136140
host = fakes.FakeBackendState('host1',
137141
{'capabilities': caps,
138142
'vendor_name': VMWARE_VENDOR})
139143
self.props['scheduler_hints'] = {'vcenter-shard': 'vc-a-1'}
140-
self.assertTrue(self.filt_cls.backend_passes(host, self.props))
144+
self.backend_passes(host, self.props)
141145

142146
def test_shard_override_no_match(self):
143147
caps = {'vcenter-shard': 'vc-a-0'}
144148
host = fakes.FakeBackendState('host1',
145149
{'capabilities': caps,
146150
'vendor_name': VMWARE_VENDOR})
147151
self.props['scheduler_hints'] = {'vcenter-shard': 'vc-a-1'}
148-
self.assertFalse(self.filt_cls.backend_passes(host, self.props))
152+
self.backend_no_pass(host, self.props)
149153

150154
def test_shard_override_no_data(self):
151155
caps = {'vcenter-shard': 'vc-a-0'}
152156
host = fakes.FakeBackendState('host1',
153157
{'capabilities': caps,
154158
'vendor_name': VMWARE_VENDOR})
155159
self.props['scheduler_hints'] = {'vcenter-shard': None}
156-
self.assertFalse(self.filt_cls.backend_passes(host, self.props))
160+
self.backend_no_pass(host, self.props)
157161

158162
def test_sharding_enabled_any_backend_match(self):
159163
self.filt_cls._PROJECT_SHARD_CACHE['baz'] = ['sharding_enabled']
@@ -162,7 +166,7 @@ def test_sharding_enabled_any_backend_match(self):
162166
host = fakes.FakeBackendState('host1',
163167
{'capabilities': caps,
164168
'vendor_name': VMWARE_VENDOR})
165-
self.assertTrue(self.filt_cls.backend_passes(host, self.props))
169+
self.backend_passes(host, self.props)
166170

167171
def test_sharding_enabled_and_single_shard_any_backend_match(self):
168172
self.filt_cls._PROJECT_SHARD_CACHE['baz'] = ['sharding_enabled',
@@ -172,7 +176,7 @@ def test_sharding_enabled_and_single_shard_any_backend_match(self):
172176
host = fakes.FakeBackendState('host1',
173177
{'capabilities': caps,
174178
'vendor_name': VMWARE_VENDOR})
175-
self.assertTrue(self.filt_cls.backend_passes(host, self.props))
179+
self.backend_passes(host, self.props)
176180

177181
def test_scheduler_hints_override_sharding_enabled(self):
178182
self.filt_cls._PROJECT_SHARD_CACHE['baz'] = ['sharding_enabled']
@@ -182,12 +186,12 @@ def test_scheduler_hints_override_sharding_enabled(self):
182186
host = fakes.FakeBackendState('host0',
183187
{'capabilities': caps0,
184188
'vendor_name': VMWARE_VENDOR})
185-
self.assertFalse(self.filt_cls.backend_passes(host, self.props))
189+
self.backend_no_pass(host, self.props)
186190
caps1 = {'vcenter-shard': 'vc-a-1'}
187191
host = fakes.FakeBackendState('host1',
188192
{'capabilities': caps1,
189193
'vendor_name': VMWARE_VENDOR})
190-
self.assertTrue(self.filt_cls.backend_passes(host, self.props))
194+
self.backend_passes(host, self.props)
191195

192196
def test_noop_for_find_backend_by_connector_with_hint(self):
193197
"""Check if we pass any backend
@@ -204,7 +208,7 @@ def test_noop_for_find_backend_by_connector_with_hint(self):
204208
'vendor_name': VMWARE_VENDOR})
205209
self.props['scheduler_hints'] = {'vcenter-shard': 'vc-a-1'}
206210
self.props['request_spec']['operation'] = 'find_backend_for_connector'
207-
self.assertTrue(self.filt_cls.backend_passes(host, self.props))
211+
self.backend_passes(host, self.props)
208212

209213
def test_noop_for_find_backend_by_connector_without_hint(self):
210214
"""Check if we pass any backend
@@ -221,4 +225,52 @@ def test_noop_for_find_backend_by_connector_without_hint(self):
221225
{'capabilities': caps,
222226
'vendor_name': VMWARE_VENDOR})
223227
self.props['request_spec']['operation'] = 'find_backend_for_connector'
224-
self.assertTrue(self.filt_cls.backend_passes(host, self.props))
228+
self.backend_passes(host, self.props)
229+
230+
@mock.patch('cinder.context.get_admin_context')
231+
@mock.patch('cinder.db.get_host_by_volume_metadata')
232+
def test_same_shard_for_k8s_volumes(self, mock_get_hosts,
233+
mock_get_context):
234+
CSI_KEY = 'cinder.csi.openstack.org/cluster'
235+
all_backends = [
236+
fakes.FakeBackendState(
237+
'volume-vc-a-0@backend#pool1',
238+
{'capabilities': {'vcenter-shard': 'vc-a-0'},
239+
'vendor_name': VMWARE_VENDOR}),
240+
fakes.FakeBackendState(
241+
'volume-vc-a-1@backend#pool2',
242+
{'capabilities': {'vcenter-shard': 'vc-a-1'},
243+
'vendor_name': VMWARE_VENDOR}),
244+
]
245+
mock_get_context.return_value = self.context
246+
fake_meta = {
247+
CSI_KEY: 'cluster-1',
248+
}
249+
mock_get_hosts.return_value = 'volume-vc-a-1'
250+
self.filt_cls._PROJECT_SHARD_CACHE['baz'] = ['sharding_enabled',
251+
'vc-a-1']
252+
filter_props = dict(self.props)
253+
filter_props['request_spec']['volume_properties'].update({
254+
'project_id': 'baz',
255+
'metadata': fake_meta
256+
})
257+
filter_props['request_spec']['resource_properties'] = {
258+
'availability_zone': 'az-1'
259+
}
260+
261+
filtered = self.filt_cls.filter_all(all_backends, filter_props)
262+
263+
mock_get_hosts.assert_called_once_with(
264+
key=CSI_KEY, value=fake_meta[CSI_KEY], filters={
265+
'availability_zone': 'az-1'
266+
})
267+
self.assertEqual(len(filtered), 1)
268+
self.assertEqual('volume-vc-a-1@backend#pool2', filtered[0].host)
269+
270+
def backend_passes(self, backend, filter_properties):
271+
filtered = self.filt_cls.filter_all([backend], filter_properties)
272+
self.assertEqual(backend, filtered[0])
273+
274+
def backend_no_pass(self, backend, filter_properties):
275+
filtered = self.filt_cls.filter_all([backend], filter_properties)
276+
self.assertEqual(0, len(filtered))

0 commit comments

Comments
 (0)