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