]>
Commit | Line | Data |
---|---|---|
f91f0fd5 TL |
1 | # Disable autopep8 for this file: |
2 | ||
3 | # fmt: off | |
4 | ||
9f95a23c TL |
5 | from typing import NamedTuple, List |
6 | import pytest | |
7 | ||
f6b5b4d7 | 8 | from ceph.deployment.hostspec import HostSpec |
9f95a23c TL |
9 | from ceph.deployment.service_spec import ServiceSpec, PlacementSpec, ServiceSpecValidationError |
10 | ||
11 | from cephadm.module import HostAssignment | |
f6b5b4d7 TL |
12 | from orchestrator import DaemonDescription, OrchestratorValidationError, OrchestratorError, HostSpec |
13 | ||
14 | ||
15 | def wrapper(func): | |
16 | # some odd thingy to revert the order or arguments | |
17 | def inner(*args): | |
18 | def inner2(expected): | |
19 | func(expected, *args) | |
20 | return inner2 | |
21 | return inner | |
22 | ||
23 | ||
24 | @wrapper | |
25 | def none(expected): | |
26 | assert expected == [] | |
27 | ||
28 | ||
29 | @wrapper | |
30 | def one_of(expected, *hosts): | |
31 | if not isinstance(expected, list): | |
32 | assert False, str(expected) | |
33 | assert len(expected) == 1, f'one_of failed len({expected}) != 1' | |
34 | assert expected[0] in hosts | |
35 | ||
36 | ||
37 | @wrapper | |
38 | def two_of(expected, *hosts): | |
39 | if not isinstance(expected, list): | |
40 | assert False, str(expected) | |
41 | assert len(expected) == 2, f'one_of failed len({expected}) != 2' | |
42 | matches = 0 | |
43 | for h in hosts: | |
44 | matches += int(h in expected) | |
45 | if matches != 2: | |
46 | assert False, f'two of {hosts} not in {expected}' | |
47 | ||
48 | ||
49 | @wrapper | |
50 | def exactly(expected, *hosts): | |
51 | assert expected == list(hosts) | |
52 | ||
53 | ||
54 | @wrapper | |
55 | def error(expected, kind, match): | |
56 | assert isinstance(expected, kind), (str(expected), match) | |
57 | assert str(expected) == match, (str(expected), match) | |
58 | ||
59 | ||
60 | @wrapper | |
61 | def _or(expected, *inners): | |
62 | def catch(inner): | |
63 | try: | |
64 | inner(expected) | |
65 | except AssertionError as e: | |
66 | return e | |
67 | result = [catch(i) for i in inners] | |
68 | if None not in result: | |
69 | assert False, f"_or failed: {expected}" | |
70 | ||
71 | ||
72 | def _always_true(_): pass | |
73 | ||
74 | ||
75 | def k(s): | |
76 | return [e for e in s.split(' ') if e] | |
77 | ||
78 | ||
79 | def get_result(key, results): | |
80 | def match(one): | |
81 | for o, k in zip(one, key): | |
82 | if o != k and o != '*': | |
83 | return False | |
84 | return True | |
85 | return [v for k, v in results | |
86 | if match(k)][0] | |
87 | ||
f6b5b4d7 | 88 | |
f91f0fd5 | 89 | def mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count): |
f6b5b4d7 TL |
90 | |
91 | if spec_section == 'hosts': | |
f91f0fd5 | 92 | mk_spec = lambda: ServiceSpec('mgr', placement=PlacementSpec( |
f6b5b4d7 TL |
93 | hosts=explicit, |
94 | count=count, | |
95 | )) | |
f6b5b4d7 | 96 | elif spec_section == 'label': |
f91f0fd5 | 97 | mk_spec = lambda: ServiceSpec('mgr', placement=PlacementSpec( |
f6b5b4d7 TL |
98 | label='mylabel', |
99 | count=count, | |
100 | )) | |
f6b5b4d7 TL |
101 | elif spec_section == 'host_pattern': |
102 | pattern = { | |
103 | 'e': 'notfound', | |
104 | '1': '1', | |
105 | '12': '[1-2]', | |
106 | '123': '*', | |
107 | }[explicit_key] | |
f91f0fd5 | 108 | mk_spec = lambda: ServiceSpec('mgr', placement=PlacementSpec( |
f6b5b4d7 TL |
109 | host_pattern=pattern, |
110 | count=count, | |
111 | )) | |
f6b5b4d7 TL |
112 | else: |
113 | assert False | |
f6b5b4d7 | 114 | |
f91f0fd5 TL |
115 | hosts = [ |
116 | HostSpec(h, labels=['mylabel']) if h in explicit else HostSpec(h) | |
117 | for h in hosts | |
118 | ] | |
119 | ||
120 | return mk_spec, hosts | |
f6b5b4d7 TL |
121 | |
122 | ||
f91f0fd5 | 123 | def run_scheduler_test(results, mk_spec, hosts, get_daemons_func, key_elems): |
f6b5b4d7 TL |
124 | key = ' '.join('N' if e is None else str(e) for e in key_elems) |
125 | try: | |
126 | assert_res = get_result(k(key), results) | |
127 | except IndexError: | |
128 | try: | |
129 | spec = mk_spec() | |
130 | host_res = HostAssignment( | |
131 | spec=spec, | |
f91f0fd5 | 132 | hosts=hosts, |
f6b5b4d7 TL |
133 | get_daemons_func=get_daemons_func).place() |
134 | if isinstance(host_res, list): | |
135 | e = ', '.join(repr(h.hostname) for h in host_res) | |
136 | assert False, f'`(k("{key}"), exactly({e})),` not found' | |
137 | assert False, f'`(k("{key}"), ...),` not found' | |
138 | except OrchestratorError as e: | |
139 | assert False, f'`(k("{key}"), error({type(e).__name__}, {repr(str(e))})),` not found' | |
140 | ||
141 | for _ in range(10): # scheduler has a random component | |
142 | try: | |
143 | spec = mk_spec() | |
144 | host_res = HostAssignment( | |
145 | spec=spec, | |
f91f0fd5 | 146 | hosts=hosts, |
f6b5b4d7 TL |
147 | get_daemons_func=get_daemons_func).place() |
148 | ||
149 | assert_res(sorted([h.hostname for h in host_res])) | |
150 | except Exception as e: | |
151 | assert_res(e) | |
152 | ||
153 | ||
154 | # * first match from the top wins | |
155 | # * where e=[], *=any | |
156 | # | |
157 | # + list of known hosts available for scheduling (host_key) | |
158 | # | + hosts used for explict placement (explicit_key) | |
159 | # | | + count | |
160 | # | | | + section (host, label, pattern) | |
161 | # | | | | + expected result | |
162 | # | | | | | | |
163 | test_explicit_scheduler_results = [ | |
164 | (k("* * 0 *"), error(ServiceSpecValidationError, 'num/count must be > 1')), | |
f91f0fd5 TL |
165 | (k("* e N l"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr>: No matching hosts for label mylabel')), |
166 | (k("* e N p"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr>: No matching hosts')), | |
f6b5b4d7 TL |
167 | (k("* e N h"), error(OrchestratorValidationError, 'placement spec is empty: no hosts, no label, no pattern, no count')), |
168 | (k("* e * *"), none), | |
f91f0fd5 TL |
169 | (k("1 12 * h"), error(OrchestratorValidationError, "Cannot place <ServiceSpec for service_name=mgr> on 2: Unknown hosts")), |
170 | (k("1 123 * h"), error(OrchestratorValidationError, "Cannot place <ServiceSpec for service_name=mgr> on 2, 3: Unknown hosts")), | |
f6b5b4d7 TL |
171 | (k("1 * * *"), exactly('1')), |
172 | (k("12 1 * *"), exactly('1')), | |
173 | (k("12 12 1 *"), one_of('1', '2')), | |
174 | (k("12 12 * *"), exactly('1', '2')), | |
f91f0fd5 | 175 | (k("12 123 * h"), error(OrchestratorValidationError, "Cannot place <ServiceSpec for service_name=mgr> on 3: Unknown hosts")), |
f6b5b4d7 TL |
176 | (k("12 123 1 *"), one_of('1', '2', '3')), |
177 | (k("12 123 * *"), two_of('1', '2', '3')), | |
178 | (k("123 1 * *"), exactly('1')), | |
179 | (k("123 12 1 *"), one_of('1', '2')), | |
180 | (k("123 12 * *"), exactly('1', '2')), | |
181 | (k("123 123 1 *"), one_of('1', '2', '3')), | |
182 | (k("123 123 2 *"), two_of('1', '2', '3')), | |
183 | (k("123 123 * *"), exactly('1', '2', '3')), | |
184 | ] | |
185 | ||
186 | @pytest.mark.parametrize("spec_section_key,spec_section", | |
187 | [ | |
188 | ('h', 'hosts'), | |
189 | ('l', 'label'), | |
190 | ('p', 'host_pattern'), | |
191 | ]) | |
192 | @pytest.mark.parametrize("count", | |
193 | [ | |
194 | None, | |
195 | 0, | |
196 | 1, | |
197 | 2, | |
198 | 3, | |
199 | ]) | |
200 | @pytest.mark.parametrize("explicit_key, explicit", | |
201 | [ | |
202 | ('e', []), | |
203 | ('1', ['1']), | |
204 | ('12', ['1', '2']), | |
205 | ('123', ['1', '2', '3']), | |
206 | ]) | |
207 | @pytest.mark.parametrize("host_key, hosts", | |
208 | [ | |
209 | ('1', ['1']), | |
210 | ('12', ['1', '2']), | |
211 | ('123', ['1', '2', '3']), | |
212 | ]) | |
213 | def test_explicit_scheduler(host_key, hosts, | |
214 | explicit_key, explicit, | |
215 | count, | |
216 | spec_section_key, spec_section): | |
217 | ||
f91f0fd5 | 218 | mk_spec, hosts = mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count) |
f6b5b4d7 TL |
219 | run_scheduler_test( |
220 | results=test_explicit_scheduler_results, | |
221 | mk_spec=mk_spec, | |
f91f0fd5 | 222 | hosts=hosts, |
f6b5b4d7 TL |
223 | get_daemons_func=lambda _: [], |
224 | key_elems=(host_key, explicit_key, count, spec_section_key) | |
225 | ) | |
226 | ||
227 | ||
228 | # * first match from the top wins | |
229 | # * where e=[], *=any | |
230 | # | |
231 | # + list of known hosts available for scheduling (host_key) | |
232 | # | + hosts used for explict placement (explicit_key) | |
233 | # | | + count | |
234 | # | | | + existing daemons | |
235 | # | | | | + section (host, label, pattern) | |
236 | # | | | | | + expected result | |
237 | # | | | | | | | |
238 | test_scheduler_daemons_results = [ | |
239 | (k("* 1 * * *"), exactly('1')), | |
f91f0fd5 | 240 | (k("1 123 * * h"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr> on 2, 3: Unknown hosts')), |
f6b5b4d7 | 241 | (k("1 123 * * *"), exactly('1')), |
f91f0fd5 | 242 | (k("12 123 * * h"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr> on 3: Unknown hosts')), |
f6b5b4d7 TL |
243 | (k("12 123 N * *"), exactly('1', '2')), |
244 | (k("12 123 1 * *"), one_of('1', '2')), | |
245 | (k("12 123 2 * *"), exactly('1', '2')), | |
246 | (k("12 123 3 * *"), exactly('1', '2')), | |
247 | (k("123 123 N * *"), exactly('1', '2', '3')), | |
248 | (k("123 123 1 e *"), one_of('1', '2', '3')), | |
249 | (k("123 123 1 1 *"), exactly('1')), | |
250 | (k("123 123 1 3 *"), exactly('3')), | |
251 | (k("123 123 1 12 *"), one_of('1', '2')), | |
252 | (k("123 123 1 112 *"), one_of('1', '2')), | |
253 | (k("123 123 1 23 *"), one_of('2', '3')), | |
254 | (k("123 123 1 123 *"), one_of('1', '2', '3')), | |
255 | (k("123 123 2 e *"), two_of('1', '2', '3')), | |
256 | (k("123 123 2 1 *"), _or(exactly('1', '2'), exactly('1', '3'))), | |
257 | (k("123 123 2 3 *"), _or(exactly('1', '3'), exactly('2', '3'))), | |
258 | (k("123 123 2 12 *"), exactly('1', '2')), | |
259 | (k("123 123 2 112 *"), exactly('1', '2')), | |
260 | (k("123 123 2 23 *"), exactly('2', '3')), | |
261 | (k("123 123 2 123 *"), two_of('1', '2', '3')), | |
262 | (k("123 123 3 * *"), exactly('1', '2', '3')), | |
263 | ] | |
264 | ||
265 | ||
266 | @pytest.mark.parametrize("spec_section_key,spec_section", | |
267 | [ | |
268 | ('h', 'hosts'), | |
269 | ('l', 'label'), | |
270 | ('p', 'host_pattern'), | |
271 | ]) | |
272 | @pytest.mark.parametrize("daemons_key, daemons", | |
273 | [ | |
274 | ('e', []), | |
275 | ('1', ['1']), | |
276 | ('3', ['3']), | |
277 | ('12', ['1', '2']), | |
278 | ('112', ['1', '1', '2']), # deal with existing co-located daemons | |
279 | ('23', ['2', '3']), | |
280 | ('123', ['1', '2', '3']), | |
281 | ]) | |
282 | @pytest.mark.parametrize("count", | |
283 | [ | |
284 | None, | |
285 | 1, | |
286 | 2, | |
287 | 3, | |
288 | ]) | |
289 | @pytest.mark.parametrize("explicit_key, explicit", | |
290 | [ | |
291 | ('1', ['1']), | |
292 | ('123', ['1', '2', '3']), | |
293 | ]) | |
294 | @pytest.mark.parametrize("host_key, hosts", | |
295 | [ | |
296 | ('1', ['1']), | |
297 | ('12', ['1', '2']), | |
298 | ('123', ['1', '2', '3']), | |
299 | ]) | |
300 | def test_scheduler_daemons(host_key, hosts, | |
301 | explicit_key, explicit, | |
302 | count, | |
303 | daemons_key, daemons, | |
304 | spec_section_key, spec_section): | |
f91f0fd5 | 305 | mk_spec, hosts = mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count) |
f6b5b4d7 | 306 | dds = [ |
f91f0fd5 | 307 | DaemonDescription('mgr', d, d) |
f6b5b4d7 TL |
308 | for d in daemons |
309 | ] | |
310 | run_scheduler_test( | |
311 | results=test_scheduler_daemons_results, | |
312 | mk_spec=mk_spec, | |
f91f0fd5 | 313 | hosts=hosts, |
f6b5b4d7 TL |
314 | get_daemons_func=lambda _: dds, |
315 | key_elems=(host_key, explicit_key, count, daemons_key, spec_section_key) | |
316 | ) | |
317 | ||
318 | ||
f91f0fd5 | 319 | # ========================= |
9f95a23c TL |
320 | |
321 | ||
322 | class NodeAssignmentTest(NamedTuple): | |
323 | service_type: str | |
324 | placement: PlacementSpec | |
325 | hosts: List[str] | |
326 | daemons: List[DaemonDescription] | |
327 | expected: List[str] | |
328 | ||
329 | @pytest.mark.parametrize("service_type,placement,hosts,daemons,expected", | |
330 | [ | |
331 | # just hosts | |
332 | NodeAssignmentTest( | |
f91f0fd5 | 333 | 'mgr', |
9f95a23c TL |
334 | PlacementSpec(hosts=['smithi060:[v2:172.21.15.60:3301,v1:172.21.15.60:6790]=c']), |
335 | ['smithi060'], | |
336 | [], | |
337 | ['smithi060'] | |
338 | ), | |
339 | # all_hosts | |
340 | NodeAssignmentTest( | |
f91f0fd5 | 341 | 'mgr', |
9f95a23c TL |
342 | PlacementSpec(host_pattern='*'), |
343 | 'host1 host2 host3'.split(), | |
344 | [ | |
f91f0fd5 TL |
345 | DaemonDescription('mgr', 'a', 'host1'), |
346 | DaemonDescription('mgr', 'b', 'host2'), | |
9f95a23c TL |
347 | ], |
348 | ['host1', 'host2', 'host3'] | |
349 | ), | |
350 | # count that is bigger than the amount of hosts. Truncate to len(hosts) | |
351 | # RGWs should not be co-located to each other. | |
352 | NodeAssignmentTest( | |
353 | 'rgw', | |
354 | PlacementSpec(count=4), | |
355 | 'host1 host2 host3'.split(), | |
356 | [], | |
357 | ['host1', 'host2', 'host3'] | |
358 | ), | |
359 | # count + partial host list | |
360 | NodeAssignmentTest( | |
f91f0fd5 | 361 | 'mgr', |
9f95a23c TL |
362 | PlacementSpec(count=3, hosts=['host3']), |
363 | 'host1 host2 host3'.split(), | |
364 | [ | |
f91f0fd5 TL |
365 | DaemonDescription('mgr', 'a', 'host1'), |
366 | DaemonDescription('mgr', 'b', 'host2'), | |
9f95a23c | 367 | ], |
f6b5b4d7 | 368 | ['host3'] |
9f95a23c TL |
369 | ), |
370 | # count 1 + partial host list | |
371 | NodeAssignmentTest( | |
f91f0fd5 | 372 | 'mgr', |
9f95a23c TL |
373 | PlacementSpec(count=1, hosts=['host3']), |
374 | 'host1 host2 host3'.split(), | |
375 | [ | |
f91f0fd5 TL |
376 | DaemonDescription('mgr', 'a', 'host1'), |
377 | DaemonDescription('mgr', 'b', 'host2'), | |
9f95a23c TL |
378 | ], |
379 | ['host3'] | |
380 | ), | |
381 | # count + partial host list + existing | |
382 | NodeAssignmentTest( | |
f91f0fd5 | 383 | 'mgr', |
9f95a23c TL |
384 | PlacementSpec(count=2, hosts=['host3']), |
385 | 'host1 host2 host3'.split(), | |
386 | [ | |
f91f0fd5 | 387 | DaemonDescription('mgr', 'a', 'host1'), |
9f95a23c | 388 | ], |
f6b5b4d7 | 389 | ['host3'] |
9f95a23c TL |
390 | ), |
391 | # count + partial host list + existing (deterministic) | |
392 | NodeAssignmentTest( | |
f91f0fd5 | 393 | 'mgr', |
9f95a23c TL |
394 | PlacementSpec(count=2, hosts=['host1']), |
395 | 'host1 host2'.split(), | |
396 | [ | |
f91f0fd5 | 397 | DaemonDescription('mgr', 'a', 'host1'), |
9f95a23c | 398 | ], |
f6b5b4d7 | 399 | ['host1'] |
9f95a23c TL |
400 | ), |
401 | # count + partial host list + existing (deterministic) | |
402 | NodeAssignmentTest( | |
f91f0fd5 | 403 | 'mgr', |
9f95a23c TL |
404 | PlacementSpec(count=2, hosts=['host1']), |
405 | 'host1 host2'.split(), | |
406 | [ | |
f91f0fd5 | 407 | DaemonDescription('mgr', 'a', 'host2'), |
9f95a23c | 408 | ], |
f6b5b4d7 | 409 | ['host1'] |
9f95a23c TL |
410 | ), |
411 | # label only | |
412 | NodeAssignmentTest( | |
f91f0fd5 | 413 | 'mgr', |
9f95a23c TL |
414 | PlacementSpec(label='foo'), |
415 | 'host1 host2 host3'.split(), | |
416 | [], | |
417 | ['host1', 'host2', 'host3'] | |
418 | ), | |
419 | # host_pattern | |
420 | NodeAssignmentTest( | |
f91f0fd5 TL |
421 | 'mgr', |
422 | PlacementSpec(host_pattern='mgr*'), | |
423 | 'mgrhost1 mgrhost2 datahost'.split(), | |
9f95a23c | 424 | [], |
f91f0fd5 | 425 | ['mgrhost1', 'mgrhost2'] |
9f95a23c TL |
426 | ), |
427 | ]) | |
428 | def test_node_assignment(service_type, placement, hosts, daemons, expected): | |
f6b5b4d7 TL |
429 | service_id = None |
430 | if service_type == 'rgw': | |
431 | service_id = 'realm.zone' | |
432 | ||
433 | spec = ServiceSpec(service_type=service_type, | |
434 | service_id=service_id, | |
435 | placement=placement) | |
436 | ||
9f95a23c | 437 | hosts = HostAssignment( |
f6b5b4d7 | 438 | spec=spec, |
f91f0fd5 | 439 | hosts=[HostSpec(h, labels=['foo']) for h in hosts], |
9f95a23c TL |
440 | get_daemons_func=lambda _: daemons).place() |
441 | assert sorted([h.hostname for h in hosts]) == sorted(expected) | |
442 | ||
e306af50 | 443 | |
9f95a23c TL |
444 | class NodeAssignmentTest2(NamedTuple): |
445 | service_type: str | |
446 | placement: PlacementSpec | |
447 | hosts: List[str] | |
448 | daemons: List[DaemonDescription] | |
449 | expected_len: int | |
450 | in_set: List[str] | |
451 | ||
452 | @pytest.mark.parametrize("service_type,placement,hosts,daemons,expected_len,in_set", | |
453 | [ | |
9f95a23c TL |
454 | # just count |
455 | NodeAssignmentTest2( | |
f91f0fd5 | 456 | 'mgr', |
9f95a23c TL |
457 | PlacementSpec(count=1), |
458 | 'host1 host2 host3'.split(), | |
459 | [], | |
460 | 1, | |
461 | ['host1', 'host2', 'host3'], | |
462 | ), | |
463 | ||
464 | # hosts + (smaller) count | |
465 | NodeAssignmentTest2( | |
f91f0fd5 | 466 | 'mgr', |
9f95a23c TL |
467 | PlacementSpec(count=1, hosts='host1 host2'.split()), |
468 | 'host1 host2'.split(), | |
469 | [], | |
470 | 1, | |
471 | ['host1', 'host2'], | |
472 | ), | |
473 | # hosts + (smaller) count, existing | |
474 | NodeAssignmentTest2( | |
f91f0fd5 | 475 | 'mgr', |
9f95a23c TL |
476 | PlacementSpec(count=1, hosts='host1 host2 host3'.split()), |
477 | 'host1 host2 host3'.split(), | |
f91f0fd5 | 478 | [DaemonDescription('mgr', 'mgr.a', 'host1'),], |
9f95a23c TL |
479 | 1, |
480 | ['host1', 'host2', 'host3'], | |
481 | ), | |
482 | # hosts + (smaller) count, (more) existing | |
483 | NodeAssignmentTest2( | |
f91f0fd5 | 484 | 'mgr', |
9f95a23c TL |
485 | PlacementSpec(count=1, hosts='host1 host2 host3'.split()), |
486 | 'host1 host2 host3'.split(), | |
487 | [ | |
f91f0fd5 TL |
488 | DaemonDescription('mgr', 'a', 'host1'), |
489 | DaemonDescription('mgr', 'b', 'host2'), | |
9f95a23c TL |
490 | ], |
491 | 1, | |
492 | ['host1', 'host2'] | |
493 | ), | |
494 | # count + partial host list | |
495 | NodeAssignmentTest2( | |
f91f0fd5 | 496 | 'mgr', |
9f95a23c TL |
497 | PlacementSpec(count=2, hosts=['host3']), |
498 | 'host1 host2 host3'.split(), | |
499 | [], | |
f6b5b4d7 | 500 | 1, |
9f95a23c TL |
501 | ['host1', 'host2', 'host3'] |
502 | ), | |
503 | # label + count | |
504 | NodeAssignmentTest2( | |
f91f0fd5 | 505 | 'mgr', |
9f95a23c TL |
506 | PlacementSpec(count=1, label='foo'), |
507 | 'host1 host2 host3'.split(), | |
508 | [], | |
509 | 1, | |
510 | ['host1', 'host2', 'host3'] | |
511 | ), | |
512 | ]) | |
513 | def test_node_assignment2(service_type, placement, hosts, | |
514 | daemons, expected_len, in_set): | |
515 | hosts = HostAssignment( | |
516 | spec=ServiceSpec(service_type, placement=placement), | |
f91f0fd5 | 517 | hosts=[HostSpec(h, labels=['foo']) for h in hosts], |
9f95a23c TL |
518 | get_daemons_func=lambda _: daemons).place() |
519 | assert len(hosts) == expected_len | |
520 | for h in [h.hostname for h in hosts]: | |
521 | assert h in in_set | |
522 | ||
523 | @pytest.mark.parametrize("service_type,placement,hosts,daemons,expected_len,must_have", | |
524 | [ | |
525 | # hosts + (smaller) count, (more) existing | |
526 | NodeAssignmentTest2( | |
f91f0fd5 | 527 | 'mgr', |
9f95a23c TL |
528 | PlacementSpec(count=3, hosts='host3'.split()), |
529 | 'host1 host2 host3'.split(), | |
530 | [], | |
f6b5b4d7 | 531 | 1, |
9f95a23c TL |
532 | ['host3'] |
533 | ), | |
534 | # count + partial host list | |
535 | NodeAssignmentTest2( | |
f91f0fd5 | 536 | 'mgr', |
9f95a23c TL |
537 | PlacementSpec(count=2, hosts=['host3']), |
538 | 'host1 host2 host3'.split(), | |
539 | [], | |
f6b5b4d7 | 540 | 1, |
9f95a23c TL |
541 | ['host3'] |
542 | ), | |
543 | ]) | |
544 | def test_node_assignment3(service_type, placement, hosts, | |
545 | daemons, expected_len, must_have): | |
546 | hosts = HostAssignment( | |
547 | spec=ServiceSpec(service_type, placement=placement), | |
f91f0fd5 | 548 | hosts=[HostSpec(h) for h in hosts], |
9f95a23c TL |
549 | get_daemons_func=lambda _: daemons).place() |
550 | assert len(hosts) == expected_len | |
551 | for h in must_have: | |
552 | assert h in [h.hostname for h in hosts] | |
553 | ||
554 | ||
555 | @pytest.mark.parametrize("placement", | |
556 | [ | |
557 | ('1 *'), | |
558 | ('* label:foo'), | |
559 | ('* host1 host2'), | |
560 | ('hostname12hostname12hostname12hostname12hostname12hostname12hostname12'), # > 63 chars | |
561 | ]) | |
562 | def test_bad_placements(placement): | |
563 | try: | |
564 | s = PlacementSpec.from_string(placement.split(' ')) | |
565 | assert False | |
566 | except ServiceSpecValidationError as e: | |
567 | pass | |
568 | ||
569 | ||
570 | class NodeAssignmentTestBadSpec(NamedTuple): | |
571 | service_type: str | |
572 | placement: PlacementSpec | |
573 | hosts: List[str] | |
574 | daemons: List[DaemonDescription] | |
575 | expected: str | |
576 | @pytest.mark.parametrize("service_type,placement,hosts,daemons,expected", | |
577 | [ | |
578 | # unknown host | |
579 | NodeAssignmentTestBadSpec( | |
f91f0fd5 | 580 | 'mgr', |
9f95a23c TL |
581 | PlacementSpec(hosts=['unknownhost']), |
582 | ['knownhost'], | |
583 | [], | |
f91f0fd5 | 584 | "Cannot place <ServiceSpec for service_name=mgr> on unknownhost: Unknown hosts" |
9f95a23c TL |
585 | ), |
586 | # unknown host pattern | |
587 | NodeAssignmentTestBadSpec( | |
f91f0fd5 | 588 | 'mgr', |
9f95a23c TL |
589 | PlacementSpec(host_pattern='unknownhost'), |
590 | ['knownhost'], | |
591 | [], | |
f91f0fd5 | 592 | "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts" |
9f95a23c TL |
593 | ), |
594 | # unknown label | |
595 | NodeAssignmentTestBadSpec( | |
f91f0fd5 | 596 | 'mgr', |
9f95a23c TL |
597 | PlacementSpec(label='unknownlabel'), |
598 | [], | |
599 | [], | |
f91f0fd5 | 600 | "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts for label unknownlabel" |
9f95a23c TL |
601 | ), |
602 | ]) | |
603 | def test_bad_specs(service_type, placement, hosts, daemons, expected): | |
604 | with pytest.raises(OrchestratorValidationError) as e: | |
605 | hosts = HostAssignment( | |
606 | spec=ServiceSpec(service_type, placement=placement), | |
f91f0fd5 | 607 | hosts=[HostSpec(h) for h in hosts], |
9f95a23c TL |
608 | get_daemons_func=lambda _: daemons).place() |
609 | assert str(e.value) == expected | |
f6b5b4d7 TL |
610 | |
611 | class ActiveAssignmentTest(NamedTuple): | |
612 | service_type: str | |
613 | placement: PlacementSpec | |
614 | hosts: List[str] | |
615 | daemons: List[DaemonDescription] | |
616 | expected: List[List[str]] | |
617 | ||
618 | ||
619 | @pytest.mark.parametrize("service_type,placement,hosts,daemons,expected", | |
620 | [ | |
621 | ActiveAssignmentTest( | |
622 | 'mgr', | |
623 | PlacementSpec(count=2), | |
624 | 'host1 host2 host3'.split(), | |
625 | [ | |
626 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
627 | DaemonDescription('mgr', 'b', 'host2'), | |
628 | DaemonDescription('mgr', 'c', 'host3'), | |
629 | ], | |
630 | [['host1', 'host2'], ['host1', 'host3']] | |
631 | ), | |
632 | ActiveAssignmentTest( | |
633 | 'mgr', | |
634 | PlacementSpec(count=2), | |
635 | 'host1 host2 host3'.split(), | |
636 | [ | |
637 | DaemonDescription('mgr', 'a', 'host1'), | |
638 | DaemonDescription('mgr', 'b', 'host2'), | |
639 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
640 | ], | |
641 | [['host1', 'host3'], ['host2', 'host3']] | |
642 | ), | |
643 | ActiveAssignmentTest( | |
644 | 'mgr', | |
645 | PlacementSpec(count=1), | |
646 | 'host1 host2 host3'.split(), | |
647 | [ | |
648 | DaemonDescription('mgr', 'a', 'host1'), | |
649 | DaemonDescription('mgr', 'b', 'host2', is_active=True), | |
650 | DaemonDescription('mgr', 'c', 'host3'), | |
651 | ], | |
652 | [['host2']] | |
653 | ), | |
654 | ActiveAssignmentTest( | |
655 | 'mgr', | |
656 | PlacementSpec(count=1), | |
657 | 'host1 host2 host3'.split(), | |
658 | [ | |
659 | DaemonDescription('mgr', 'a', 'host1'), | |
660 | DaemonDescription('mgr', 'b', 'host2'), | |
661 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
662 | ], | |
663 | [['host3']] | |
664 | ), | |
665 | ActiveAssignmentTest( | |
666 | 'mgr', | |
667 | PlacementSpec(count=1), | |
668 | 'host1 host2 host3'.split(), | |
669 | [ | |
670 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
671 | DaemonDescription('mgr', 'b', 'host2'), | |
672 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
673 | ], | |
674 | [['host1'], ['host3']] | |
675 | ), | |
676 | ActiveAssignmentTest( | |
677 | 'mgr', | |
678 | PlacementSpec(count=2), | |
679 | 'host1 host2 host3'.split(), | |
680 | [ | |
681 | DaemonDescription('mgr', 'a', 'host1'), | |
682 | DaemonDescription('mgr', 'b', 'host2', is_active=True), | |
683 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
684 | ], | |
685 | [['host2', 'host3']] | |
686 | ), | |
687 | ActiveAssignmentTest( | |
688 | 'mgr', | |
689 | PlacementSpec(count=1), | |
690 | 'host1 host2 host3'.split(), | |
691 | [ | |
692 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
693 | DaemonDescription('mgr', 'b', 'host2', is_active=True), | |
694 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
695 | ], | |
696 | [['host1'], ['host2'], ['host3']] | |
697 | ), | |
698 | ActiveAssignmentTest( | |
699 | 'mgr', | |
700 | PlacementSpec(count=1), | |
701 | 'host1 host2 host3'.split(), | |
702 | [ | |
703 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
704 | DaemonDescription('mgr', 'a2', 'host1'), | |
705 | DaemonDescription('mgr', 'b', 'host2'), | |
706 | DaemonDescription('mgr', 'c', 'host3'), | |
707 | ], | |
708 | [['host1']] | |
709 | ), | |
710 | ActiveAssignmentTest( | |
711 | 'mgr', | |
712 | PlacementSpec(count=1), | |
713 | 'host1 host2 host3'.split(), | |
714 | [ | |
715 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
716 | DaemonDescription('mgr', 'a2', 'host1', is_active=True), | |
717 | DaemonDescription('mgr', 'b', 'host2'), | |
718 | DaemonDescription('mgr', 'c', 'host3'), | |
719 | ], | |
720 | [['host1']] | |
721 | ), | |
722 | ActiveAssignmentTest( | |
723 | 'mgr', | |
724 | PlacementSpec(count=2), | |
725 | 'host1 host2 host3'.split(), | |
726 | [ | |
727 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
728 | DaemonDescription('mgr', 'a2', 'host1'), | |
729 | DaemonDescription('mgr', 'b', 'host2'), | |
730 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
731 | ], | |
732 | [['host1', 'host3']] | |
733 | ), | |
734 | # Explicit placement should override preference for active daemon | |
735 | ActiveAssignmentTest( | |
736 | 'mgr', | |
737 | PlacementSpec(count=1, hosts=['host1']), | |
738 | 'host1 host2 host3'.split(), | |
739 | [ | |
740 | DaemonDescription('mgr', 'a', 'host1'), | |
741 | DaemonDescription('mgr', 'b', 'host2'), | |
742 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
743 | ], | |
744 | [['host1']] | |
745 | ), | |
746 | ||
747 | ]) | |
748 | def test_active_assignment(service_type, placement, hosts, daemons, expected): | |
f6b5b4d7 TL |
749 | |
750 | spec = ServiceSpec(service_type=service_type, | |
751 | service_id=None, | |
752 | placement=placement) | |
753 | ||
754 | hosts = HostAssignment( | |
755 | spec=spec, | |
f91f0fd5 | 756 | hosts=[HostSpec(h) for h in hosts], |
f6b5b4d7 TL |
757 | get_daemons_func=lambda _: daemons).place() |
758 | assert sorted([h.hostname for h in hosts]) in expected | |
f91f0fd5 TL |
759 | |
760 | class OddMonsTest(NamedTuple): | |
761 | service_type: str | |
762 | placement: PlacementSpec | |
763 | hosts: List[str] | |
764 | daemons: List[DaemonDescription] | |
765 | expected_count: int | |
766 | ||
767 | ||
768 | @pytest.mark.parametrize("service_type,placement,hosts,daemons,expected_count", | |
769 | [ | |
770 | OddMonsTest( | |
771 | 'mon', | |
772 | PlacementSpec(count=5), | |
773 | 'host1 host2 host3 host4'.split(), | |
774 | [], | |
775 | 3 | |
776 | ), | |
777 | OddMonsTest( | |
778 | 'mon', | |
779 | PlacementSpec(count=4), | |
780 | 'host1 host2 host3 host4 host5'.split(), | |
781 | [], | |
782 | 3 | |
783 | ), | |
784 | OddMonsTest( | |
785 | 'mon', | |
786 | PlacementSpec(count=5), | |
787 | 'host1 host2 host3 host4 host5'.split(), | |
788 | [], | |
789 | 5 | |
790 | ), | |
791 | OddMonsTest( | |
792 | 'mon', | |
793 | PlacementSpec(hosts='host1 host2 host3 host4'.split()), | |
794 | 'host1 host2 host3 host4 host5'.split(), | |
795 | [], | |
796 | 3 | |
797 | ), | |
798 | OddMonsTest( | |
799 | 'mon', | |
800 | PlacementSpec(hosts='host1 host2 host3 host4 host5'.split()), | |
801 | 'host1 host2 host3 host4 host5'.split(), | |
802 | [], | |
803 | 5 | |
804 | ), | |
805 | OddMonsTest( | |
806 | 'mon', | |
807 | PlacementSpec(host_pattern='*'), | |
808 | 'host1 host2 host3 host4'.split(), | |
809 | [], | |
810 | 3 | |
811 | ), | |
812 | OddMonsTest( | |
813 | 'mon', | |
814 | PlacementSpec(count=5, hosts='host1 host2 host3 host4'.split()), | |
815 | 'host1 host2 host3 host4 host5'.split(), | |
816 | [], | |
817 | 3 | |
818 | ), | |
819 | OddMonsTest( | |
820 | 'mon', | |
821 | PlacementSpec(count=2, hosts='host1 host2 host3'.split()), | |
822 | 'host1 host2 host3 host4 host5'.split(), | |
823 | [], | |
824 | 1 | |
825 | ), | |
826 | OddMonsTest( | |
827 | 'mon', | |
828 | PlacementSpec(count=5), | |
829 | 'host1 host2 host3 host4'.split(), | |
830 | [ | |
831 | DaemonDescription('mon', 'a', 'host1'), | |
832 | DaemonDescription('mon', 'b', 'host2'), | |
833 | DaemonDescription('mon', 'c', 'host3'), | |
834 | ], | |
835 | 3 | |
836 | ), | |
837 | OddMonsTest( | |
838 | 'mon', | |
839 | PlacementSpec(count=5), | |
840 | 'host1 host2 host3 host4'.split(), | |
841 | [ | |
842 | DaemonDescription('mon', 'a', 'host1'), | |
843 | DaemonDescription('mon', 'b', 'host2'), | |
844 | ], | |
845 | 3 | |
846 | ), | |
847 | OddMonsTest( | |
848 | 'mon', | |
849 | PlacementSpec(hosts='host1 host2 host3 host4'.split()), | |
850 | 'host1 host2 host3 host4 host5'.split(), | |
851 | [ | |
852 | DaemonDescription('mon', 'a', 'host1'), | |
853 | DaemonDescription('mon', 'b', 'host2'), | |
854 | DaemonDescription('mon', 'c', 'host3'), | |
855 | ], | |
856 | 3 | |
857 | ), | |
858 | ||
859 | ]) | |
860 | def test_odd_mons(service_type, placement, hosts, daemons, expected_count): | |
861 | ||
862 | spec = ServiceSpec(service_type=service_type, | |
863 | service_id=None, | |
864 | placement=placement) | |
865 | ||
866 | hosts = HostAssignment( | |
867 | spec=spec, | |
868 | hosts=[HostSpec(h) for h in hosts], | |
869 | get_daemons_func=lambda _: daemons).place() | |
870 | assert len(hosts) == expected_count |