]>
Commit | Line | Data |
---|---|---|
1 | # Disable autopep8 for this file: | |
2 | ||
3 | # fmt: off | |
4 | ||
5 | from typing import NamedTuple, List, Dict, Optional | |
6 | import pytest | |
7 | ||
8 | from ceph.deployment.hostspec import HostSpec | |
9 | from ceph.deployment.service_spec import ( | |
10 | ServiceSpec, | |
11 | PlacementSpec, | |
12 | IngressSpec, | |
13 | PatternType, | |
14 | HostPattern, | |
15 | ) | |
16 | from ceph.deployment.hostspec import SpecValidationError | |
17 | ||
18 | from cephadm.module import HostAssignment | |
19 | from cephadm.schedule import DaemonPlacement | |
20 | from orchestrator import DaemonDescription, OrchestratorValidationError, OrchestratorError | |
21 | ||
22 | ||
23 | def wrapper(func): | |
24 | # some odd thingy to revert the order or arguments | |
25 | def inner(*args): | |
26 | def inner2(expected): | |
27 | func(expected, *args) | |
28 | return inner2 | |
29 | return inner | |
30 | ||
31 | ||
32 | @wrapper | |
33 | def none(expected): | |
34 | assert expected == [] | |
35 | ||
36 | ||
37 | @wrapper | |
38 | def one_of(expected, *hosts): | |
39 | if not isinstance(expected, list): | |
40 | assert False, str(expected) | |
41 | assert len(expected) == 1, f'one_of failed len({expected}) != 1' | |
42 | assert expected[0] in hosts | |
43 | ||
44 | ||
45 | @wrapper | |
46 | def two_of(expected, *hosts): | |
47 | if not isinstance(expected, list): | |
48 | assert False, str(expected) | |
49 | assert len(expected) == 2, f'one_of failed len({expected}) != 2' | |
50 | matches = 0 | |
51 | for h in hosts: | |
52 | matches += int(h in expected) | |
53 | if matches != 2: | |
54 | assert False, f'two of {hosts} not in {expected}' | |
55 | ||
56 | ||
57 | @wrapper | |
58 | def exactly(expected, *hosts): | |
59 | assert expected == list(hosts) | |
60 | ||
61 | ||
62 | @wrapper | |
63 | def error(expected, kind, match): | |
64 | assert isinstance(expected, kind), (str(expected), match) | |
65 | assert str(expected) == match, (str(expected), match) | |
66 | ||
67 | ||
68 | @wrapper | |
69 | def _or(expected, *inners): | |
70 | def catch(inner): | |
71 | try: | |
72 | inner(expected) | |
73 | except AssertionError as e: | |
74 | return e | |
75 | result = [catch(i) for i in inners] | |
76 | if None not in result: | |
77 | assert False, f"_or failed: {expected}" | |
78 | ||
79 | ||
80 | def _always_true(_): | |
81 | pass | |
82 | ||
83 | ||
84 | def k(s): | |
85 | return [e for e in s.split(' ') if e] | |
86 | ||
87 | ||
88 | def get_result(key, results): | |
89 | def match(one): | |
90 | for o, k in zip(one, key): | |
91 | if o != k and o != '*': | |
92 | return False | |
93 | return True | |
94 | return [v for k, v in results if match(k)][0] | |
95 | ||
96 | ||
97 | def mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count): | |
98 | ||
99 | if spec_section == 'hosts': | |
100 | mk_spec = lambda: ServiceSpec('mgr', placement=PlacementSpec( # noqa: E731 | |
101 | hosts=explicit, | |
102 | count=count, | |
103 | )) | |
104 | elif spec_section == 'label': | |
105 | mk_spec = lambda: ServiceSpec('mgr', placement=PlacementSpec( # noqa: E731 | |
106 | label='mylabel', | |
107 | count=count, | |
108 | )) | |
109 | elif spec_section == 'host_pattern': | |
110 | pattern = { | |
111 | 'e': 'notfound', | |
112 | '1': '1', | |
113 | '12': '[1-2]', | |
114 | '123': '*', | |
115 | }[explicit_key] | |
116 | mk_spec = lambda: ServiceSpec('mgr', placement=PlacementSpec( # noqa: E731 | |
117 | host_pattern=pattern, | |
118 | count=count, | |
119 | )) | |
120 | else: | |
121 | assert False | |
122 | ||
123 | hosts = [ | |
124 | HostSpec(h, labels=['mylabel']) if h in explicit else HostSpec(h) | |
125 | for h in hosts | |
126 | ] | |
127 | ||
128 | return mk_spec, hosts | |
129 | ||
130 | ||
131 | def run_scheduler_test(results, mk_spec, hosts, daemons, key_elems): | |
132 | key = ' '.join('N' if e is None else str(e) for e in key_elems) | |
133 | try: | |
134 | assert_res = get_result(k(key), results) | |
135 | except IndexError: | |
136 | try: | |
137 | spec = mk_spec() | |
138 | host_res, to_add, to_remove = HostAssignment( | |
139 | spec=spec, | |
140 | hosts=hosts, | |
141 | unreachable_hosts=[], | |
142 | draining_hosts=[], | |
143 | daemons=daemons, | |
144 | ).place() | |
145 | if isinstance(host_res, list): | |
146 | e = ', '.join(repr(h.hostname) for h in host_res) | |
147 | assert False, f'`(k("{key}"), exactly({e})),` not found' | |
148 | assert False, f'`(k("{key}"), ...),` not found' | |
149 | except OrchestratorError as e: | |
150 | assert False, f'`(k("{key}"), error({type(e).__name__}, {repr(str(e))})),` not found' | |
151 | ||
152 | for _ in range(10): # scheduler has a random component | |
153 | try: | |
154 | spec = mk_spec() | |
155 | host_res, to_add, to_remove = HostAssignment( | |
156 | spec=spec, | |
157 | hosts=hosts, | |
158 | unreachable_hosts=[], | |
159 | draining_hosts=[], | |
160 | daemons=daemons | |
161 | ).place() | |
162 | ||
163 | assert_res(sorted([h.hostname for h in host_res])) | |
164 | except Exception as e: | |
165 | assert_res(e) | |
166 | ||
167 | ||
168 | @pytest.mark.parametrize("dp,n,result", | |
169 | [ # noqa: E128 | |
170 | ( | |
171 | DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[80]), | |
172 | 0, | |
173 | DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[80]), | |
174 | ), | |
175 | ( | |
176 | DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[80]), | |
177 | 2, | |
178 | DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[82]), | |
179 | ), | |
180 | ( | |
181 | DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[80, 90]), | |
182 | 2, | |
183 | DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[82, 92]), | |
184 | ), | |
185 | ]) | |
186 | def test_daemon_placement_renumber(dp, n, result): | |
187 | assert dp.renumber_ports(n) == result | |
188 | ||
189 | ||
190 | @pytest.mark.parametrize( | |
191 | 'dp,dd,result', | |
192 | [ | |
193 | ( | |
194 | DaemonPlacement(daemon_type='mgr', hostname='host1'), | |
195 | DaemonDescription('mgr', 'a', 'host1'), | |
196 | True | |
197 | ), | |
198 | ( | |
199 | DaemonPlacement(daemon_type='mgr', hostname='host1', name='a'), | |
200 | DaemonDescription('mgr', 'a', 'host1'), | |
201 | True | |
202 | ), | |
203 | ( | |
204 | DaemonPlacement(daemon_type='mon', hostname='host1', name='a'), | |
205 | DaemonDescription('mgr', 'a', 'host1'), | |
206 | False | |
207 | ), | |
208 | ( | |
209 | DaemonPlacement(daemon_type='mgr', hostname='host1', name='a'), | |
210 | DaemonDescription('mgr', 'b', 'host1'), | |
211 | False | |
212 | ), | |
213 | ]) | |
214 | def test_daemon_placement_match(dp, dd, result): | |
215 | assert dp.matches_daemon(dd) == result | |
216 | ||
217 | ||
218 | # * first match from the top wins | |
219 | # * where e=[], *=any | |
220 | # | |
221 | # + list of known hosts available for scheduling (host_key) | |
222 | # | + hosts used for explict placement (explicit_key) | |
223 | # | | + count | |
224 | # | | | + section (host, label, pattern) | |
225 | # | | | | + expected result | |
226 | # | | | | | | |
227 | test_explicit_scheduler_results = [ | |
228 | (k("* * 0 *"), error(SpecValidationError, 'num/count must be >= 1')), | |
229 | (k("* e N l"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr>: No matching hosts for label mylabel')), | |
230 | (k("* e N p"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr>: No matching hosts')), | |
231 | (k("* e N h"), error(OrchestratorValidationError, 'placement spec is empty: no hosts, no label, no pattern, no count')), | |
232 | (k("* e * *"), none), | |
233 | (k("1 12 * h"), error(OrchestratorValidationError, "Cannot place <ServiceSpec for service_name=mgr> on 2: Unknown hosts")), | |
234 | (k("1 123 * h"), error(OrchestratorValidationError, "Cannot place <ServiceSpec for service_name=mgr> on 2, 3: Unknown hosts")), | |
235 | (k("1 * * *"), exactly('1')), | |
236 | (k("12 1 * *"), exactly('1')), | |
237 | (k("12 12 1 *"), one_of('1', '2')), | |
238 | (k("12 12 * *"), exactly('1', '2')), | |
239 | (k("12 123 * h"), error(OrchestratorValidationError, "Cannot place <ServiceSpec for service_name=mgr> on 3: Unknown hosts")), | |
240 | (k("12 123 1 *"), one_of('1', '2', '3')), | |
241 | (k("12 123 * *"), two_of('1', '2', '3')), | |
242 | (k("123 1 * *"), exactly('1')), | |
243 | (k("123 12 1 *"), one_of('1', '2')), | |
244 | (k("123 12 * *"), exactly('1', '2')), | |
245 | (k("123 123 1 *"), one_of('1', '2', '3')), | |
246 | (k("123 123 2 *"), two_of('1', '2', '3')), | |
247 | (k("123 123 * *"), exactly('1', '2', '3')), | |
248 | ] | |
249 | ||
250 | ||
251 | @pytest.mark.parametrize("spec_section_key,spec_section", | |
252 | [ # noqa: E128 | |
253 | ('h', 'hosts'), | |
254 | ('l', 'label'), | |
255 | ('p', 'host_pattern'), | |
256 | ]) | |
257 | @pytest.mark.parametrize("count", | |
258 | [ # noqa: E128 | |
259 | None, | |
260 | 0, | |
261 | 1, | |
262 | 2, | |
263 | 3, | |
264 | ]) | |
265 | @pytest.mark.parametrize("explicit_key, explicit", | |
266 | [ # noqa: E128 | |
267 | ('e', []), | |
268 | ('1', ['1']), | |
269 | ('12', ['1', '2']), | |
270 | ('123', ['1', '2', '3']), | |
271 | ]) | |
272 | @pytest.mark.parametrize("host_key, hosts", | |
273 | [ # noqa: E128 | |
274 | ('1', ['1']), | |
275 | ('12', ['1', '2']), | |
276 | ('123', ['1', '2', '3']), | |
277 | ]) | |
278 | def test_explicit_scheduler(host_key, hosts, | |
279 | explicit_key, explicit, | |
280 | count, | |
281 | spec_section_key, spec_section): | |
282 | ||
283 | mk_spec, hosts = mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count) | |
284 | run_scheduler_test( | |
285 | results=test_explicit_scheduler_results, | |
286 | mk_spec=mk_spec, | |
287 | hosts=hosts, | |
288 | daemons=[], | |
289 | key_elems=(host_key, explicit_key, count, spec_section_key) | |
290 | ) | |
291 | ||
292 | ||
293 | # * first match from the top wins | |
294 | # * where e=[], *=any | |
295 | # | |
296 | # + list of known hosts available for scheduling (host_key) | |
297 | # | + hosts used for explicit placement (explicit_key) | |
298 | # | | + count | |
299 | # | | | + existing daemons | |
300 | # | | | | + section (host, label, pattern) | |
301 | # | | | | | + expected result | |
302 | # | | | | | | | |
303 | test_scheduler_daemons_results = [ | |
304 | (k("* 1 * * *"), exactly('1')), | |
305 | (k("1 123 * * h"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr> on 2, 3: Unknown hosts')), | |
306 | (k("1 123 * * *"), exactly('1')), | |
307 | (k("12 123 * * h"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr> on 3: Unknown hosts')), | |
308 | (k("12 123 N * *"), exactly('1', '2')), | |
309 | (k("12 123 1 * *"), one_of('1', '2')), | |
310 | (k("12 123 2 * *"), exactly('1', '2')), | |
311 | (k("12 123 3 * *"), exactly('1', '2')), | |
312 | (k("123 123 N * *"), exactly('1', '2', '3')), | |
313 | (k("123 123 1 e *"), one_of('1', '2', '3')), | |
314 | (k("123 123 1 1 *"), exactly('1')), | |
315 | (k("123 123 1 3 *"), exactly('3')), | |
316 | (k("123 123 1 12 *"), one_of('1', '2')), | |
317 | (k("123 123 1 112 *"), one_of('1', '2')), | |
318 | (k("123 123 1 23 *"), one_of('2', '3')), | |
319 | (k("123 123 1 123 *"), one_of('1', '2', '3')), | |
320 | (k("123 123 2 e *"), two_of('1', '2', '3')), | |
321 | (k("123 123 2 1 *"), _or(exactly('1', '2'), exactly('1', '3'))), | |
322 | (k("123 123 2 3 *"), _or(exactly('1', '3'), exactly('2', '3'))), | |
323 | (k("123 123 2 12 *"), exactly('1', '2')), | |
324 | (k("123 123 2 112 *"), exactly('1', '2')), | |
325 | (k("123 123 2 23 *"), exactly('2', '3')), | |
326 | (k("123 123 2 123 *"), two_of('1', '2', '3')), | |
327 | (k("123 123 3 * *"), exactly('1', '2', '3')), | |
328 | ] | |
329 | ||
330 | ||
331 | @pytest.mark.parametrize("spec_section_key,spec_section", | |
332 | [ # noqa: E128 | |
333 | ('h', 'hosts'), | |
334 | ('l', 'label'), | |
335 | ('p', 'host_pattern'), | |
336 | ]) | |
337 | @pytest.mark.parametrize("daemons_key, daemons", | |
338 | [ # noqa: E128 | |
339 | ('e', []), | |
340 | ('1', ['1']), | |
341 | ('3', ['3']), | |
342 | ('12', ['1', '2']), | |
343 | ('112', ['1', '1', '2']), # deal with existing co-located daemons | |
344 | ('23', ['2', '3']), | |
345 | ('123', ['1', '2', '3']), | |
346 | ]) | |
347 | @pytest.mark.parametrize("count", | |
348 | [ # noqa: E128 | |
349 | None, | |
350 | 1, | |
351 | 2, | |
352 | 3, | |
353 | ]) | |
354 | @pytest.mark.parametrize("explicit_key, explicit", | |
355 | [ # noqa: E128 | |
356 | ('1', ['1']), | |
357 | ('123', ['1', '2', '3']), | |
358 | ]) | |
359 | @pytest.mark.parametrize("host_key, hosts", | |
360 | [ # noqa: E128 | |
361 | ('1', ['1']), | |
362 | ('12', ['1', '2']), | |
363 | ('123', ['1', '2', '3']), | |
364 | ]) | |
365 | def test_scheduler_daemons(host_key, hosts, | |
366 | explicit_key, explicit, | |
367 | count, | |
368 | daemons_key, daemons, | |
369 | spec_section_key, spec_section): | |
370 | mk_spec, hosts = mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count) | |
371 | dds = [ | |
372 | DaemonDescription('mgr', d, d) | |
373 | for d in daemons | |
374 | ] | |
375 | run_scheduler_test( | |
376 | results=test_scheduler_daemons_results, | |
377 | mk_spec=mk_spec, | |
378 | hosts=hosts, | |
379 | daemons=dds, | |
380 | key_elems=(host_key, explicit_key, count, daemons_key, spec_section_key) | |
381 | ) | |
382 | ||
383 | ||
384 | # ========================= | |
385 | ||
386 | ||
387 | class NodeAssignmentTest(NamedTuple): | |
388 | service_type: str | |
389 | placement: PlacementSpec | |
390 | hosts: List[str] | |
391 | daemons: List[DaemonDescription] | |
392 | rank_map: Optional[Dict[int, Dict[int, Optional[str]]]] | |
393 | post_rank_map: Optional[Dict[int, Dict[int, Optional[str]]]] | |
394 | expected: List[str] | |
395 | expected_add: List[str] | |
396 | expected_remove: List[DaemonDescription] | |
397 | ||
398 | ||
399 | @pytest.mark.parametrize("service_type,placement,hosts,daemons,rank_map,post_rank_map,expected,expected_add,expected_remove", | |
400 | [ # noqa: E128 | |
401 | # just hosts | |
402 | NodeAssignmentTest( | |
403 | 'mgr', | |
404 | PlacementSpec(hosts=['smithi060']), | |
405 | ['smithi060'], | |
406 | [], | |
407 | None, None, | |
408 | ['mgr:smithi060'], ['mgr:smithi060'], [] | |
409 | ), | |
410 | # all_hosts | |
411 | NodeAssignmentTest( | |
412 | 'mgr', | |
413 | PlacementSpec(host_pattern='*'), | |
414 | 'host1 host2 host3'.split(), | |
415 | [ | |
416 | DaemonDescription('mgr', 'a', 'host1'), | |
417 | DaemonDescription('mgr', 'b', 'host2'), | |
418 | ], | |
419 | None, None, | |
420 | ['mgr:host1', 'mgr:host2', 'mgr:host3'], | |
421 | ['mgr:host3'], | |
422 | [] | |
423 | ), | |
424 | # all_hosts + count_per_host | |
425 | NodeAssignmentTest( | |
426 | 'mds', | |
427 | PlacementSpec(host_pattern='*', count_per_host=2), | |
428 | 'host1 host2 host3'.split(), | |
429 | [ | |
430 | DaemonDescription('mds', 'a', 'host1'), | |
431 | DaemonDescription('mds', 'b', 'host2'), | |
432 | ], | |
433 | None, None, | |
434 | ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'], | |
435 | ['mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'], | |
436 | [] | |
437 | ), | |
438 | # count that is bigger than the amount of hosts. Truncate to len(hosts) | |
439 | # mgr should not be co-located to each other. | |
440 | NodeAssignmentTest( | |
441 | 'mgr', | |
442 | PlacementSpec(count=4), | |
443 | 'host1 host2 host3'.split(), | |
444 | [], | |
445 | None, None, | |
446 | ['mgr:host1', 'mgr:host2', 'mgr:host3'], | |
447 | ['mgr:host1', 'mgr:host2', 'mgr:host3'], | |
448 | [] | |
449 | ), | |
450 | # count that is bigger than the amount of hosts; wrap around. | |
451 | NodeAssignmentTest( | |
452 | 'mds', | |
453 | PlacementSpec(count=6), | |
454 | 'host1 host2 host3'.split(), | |
455 | [], | |
456 | None, None, | |
457 | ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'], | |
458 | ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'], | |
459 | [] | |
460 | ), | |
461 | # count + partial host list | |
462 | NodeAssignmentTest( | |
463 | 'mgr', | |
464 | PlacementSpec(count=3, hosts=['host3']), | |
465 | 'host1 host2 host3'.split(), | |
466 | [ | |
467 | DaemonDescription('mgr', 'a', 'host1'), | |
468 | DaemonDescription('mgr', 'b', 'host2'), | |
469 | ], | |
470 | None, None, | |
471 | ['mgr:host3'], | |
472 | ['mgr:host3'], | |
473 | ['mgr.a', 'mgr.b'] | |
474 | ), | |
475 | # count + partial host list (with colo) | |
476 | NodeAssignmentTest( | |
477 | 'mds', | |
478 | PlacementSpec(count=3, hosts=['host3']), | |
479 | 'host1 host2 host3'.split(), | |
480 | [ | |
481 | DaemonDescription('mds', 'a', 'host1'), | |
482 | DaemonDescription('mds', 'b', 'host2'), | |
483 | ], | |
484 | None, None, | |
485 | ['mds:host3', 'mds:host3', 'mds:host3'], | |
486 | ['mds:host3', 'mds:host3', 'mds:host3'], | |
487 | ['mds.a', 'mds.b'] | |
488 | ), | |
489 | # count 1 + partial host list | |
490 | NodeAssignmentTest( | |
491 | 'mgr', | |
492 | PlacementSpec(count=1, hosts=['host3']), | |
493 | 'host1 host2 host3'.split(), | |
494 | [ | |
495 | DaemonDescription('mgr', 'a', 'host1'), | |
496 | DaemonDescription('mgr', 'b', 'host2'), | |
497 | ], | |
498 | None, None, | |
499 | ['mgr:host3'], | |
500 | ['mgr:host3'], | |
501 | ['mgr.a', 'mgr.b'] | |
502 | ), | |
503 | # count + partial host list + existing | |
504 | NodeAssignmentTest( | |
505 | 'mgr', | |
506 | PlacementSpec(count=2, hosts=['host3']), | |
507 | 'host1 host2 host3'.split(), | |
508 | [ | |
509 | DaemonDescription('mgr', 'a', 'host1'), | |
510 | ], | |
511 | None, None, | |
512 | ['mgr:host3'], | |
513 | ['mgr:host3'], | |
514 | ['mgr.a'] | |
515 | ), | |
516 | # count + partial host list + existing (deterministic) | |
517 | NodeAssignmentTest( | |
518 | 'mgr', | |
519 | PlacementSpec(count=2, hosts=['host1']), | |
520 | 'host1 host2'.split(), | |
521 | [ | |
522 | DaemonDescription('mgr', 'a', 'host1'), | |
523 | ], | |
524 | None, None, | |
525 | ['mgr:host1'], | |
526 | [], | |
527 | [] | |
528 | ), | |
529 | # count + partial host list + existing (deterministic) | |
530 | NodeAssignmentTest( | |
531 | 'mgr', | |
532 | PlacementSpec(count=2, hosts=['host1']), | |
533 | 'host1 host2'.split(), | |
534 | [ | |
535 | DaemonDescription('mgr', 'a', 'host2'), | |
536 | ], | |
537 | None, None, | |
538 | ['mgr:host1'], | |
539 | ['mgr:host1'], | |
540 | ['mgr.a'] | |
541 | ), | |
542 | # label only | |
543 | NodeAssignmentTest( | |
544 | 'mgr', | |
545 | PlacementSpec(label='foo'), | |
546 | 'host1 host2 host3'.split(), | |
547 | [], | |
548 | None, None, | |
549 | ['mgr:host1', 'mgr:host2', 'mgr:host3'], | |
550 | ['mgr:host1', 'mgr:host2', 'mgr:host3'], | |
551 | [] | |
552 | ), | |
553 | # label + count (truncate to host list) | |
554 | NodeAssignmentTest( | |
555 | 'mgr', | |
556 | PlacementSpec(count=4, label='foo'), | |
557 | 'host1 host2 host3'.split(), | |
558 | [], | |
559 | None, None, | |
560 | ['mgr:host1', 'mgr:host2', 'mgr:host3'], | |
561 | ['mgr:host1', 'mgr:host2', 'mgr:host3'], | |
562 | [] | |
563 | ), | |
564 | # label + count (with colo) | |
565 | NodeAssignmentTest( | |
566 | 'mds', | |
567 | PlacementSpec(count=6, label='foo'), | |
568 | 'host1 host2 host3'.split(), | |
569 | [], | |
570 | None, None, | |
571 | ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'], | |
572 | ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'], | |
573 | [] | |
574 | ), | |
575 | # label only + count_per_hst | |
576 | NodeAssignmentTest( | |
577 | 'mds', | |
578 | PlacementSpec(label='foo', count_per_host=3), | |
579 | 'host1 host2 host3'.split(), | |
580 | [], | |
581 | None, None, | |
582 | ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3', | |
583 | 'mds:host1', 'mds:host2', 'mds:host3'], | |
584 | ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3', | |
585 | 'mds:host1', 'mds:host2', 'mds:host3'], | |
586 | [] | |
587 | ), | |
588 | # host_pattern | |
589 | NodeAssignmentTest( | |
590 | 'mgr', | |
591 | PlacementSpec(host_pattern='mgr*'), | |
592 | 'mgrhost1 mgrhost2 datahost'.split(), | |
593 | [], | |
594 | None, None, | |
595 | ['mgr:mgrhost1', 'mgr:mgrhost2'], | |
596 | ['mgr:mgrhost1', 'mgr:mgrhost2'], | |
597 | [] | |
598 | ), | |
599 | # host_pattern + count_per_host | |
600 | NodeAssignmentTest( | |
601 | 'mds', | |
602 | PlacementSpec(host_pattern='mds*', count_per_host=3), | |
603 | 'mdshost1 mdshost2 datahost'.split(), | |
604 | [], | |
605 | None, None, | |
606 | ['mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2'], | |
607 | ['mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2'], | |
608 | [] | |
609 | ), | |
610 | # label + count_per_host + ports | |
611 | NodeAssignmentTest( | |
612 | 'rgw', | |
613 | PlacementSpec(count=6, label='foo'), | |
614 | 'host1 host2 host3'.split(), | |
615 | [], | |
616 | None, None, | |
617 | ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)', | |
618 | 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'], | |
619 | ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)', | |
620 | 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'], | |
621 | [] | |
622 | ), | |
623 | # label + count_per_host + ports (+ existing) | |
624 | NodeAssignmentTest( | |
625 | 'rgw', | |
626 | PlacementSpec(count=6, label='foo'), | |
627 | 'host1 host2 host3'.split(), | |
628 | [ | |
629 | DaemonDescription('rgw', 'a', 'host1', ports=[81]), | |
630 | DaemonDescription('rgw', 'b', 'host2', ports=[80]), | |
631 | DaemonDescription('rgw', 'c', 'host1', ports=[82]), | |
632 | ], | |
633 | None, None, | |
634 | ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)', | |
635 | 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'], | |
636 | ['rgw:host1(*:80)', 'rgw:host3(*:80)', | |
637 | 'rgw:host2(*:81)', 'rgw:host3(*:81)'], | |
638 | ['rgw.c'] | |
639 | ), | |
640 | # label + host pattern | |
641 | # Note all hosts will get the "foo" label, we are checking | |
642 | # that it also filters on the host pattern when label is provided | |
643 | NodeAssignmentTest( | |
644 | 'mgr', | |
645 | PlacementSpec(label='foo', host_pattern='mgr*'), | |
646 | 'mgr1 mgr2 osd1'.split(), | |
647 | [], | |
648 | None, None, | |
649 | ['mgr:mgr1', 'mgr:mgr2'], ['mgr:mgr1', 'mgr:mgr2'], [] | |
650 | ), | |
651 | # cephadm.py teuth case | |
652 | NodeAssignmentTest( | |
653 | 'mgr', | |
654 | PlacementSpec(count=3, hosts=['host1=y', 'host2=x']), | |
655 | 'host1 host2'.split(), | |
656 | [ | |
657 | DaemonDescription('mgr', 'y', 'host1'), | |
658 | DaemonDescription('mgr', 'x', 'host2'), | |
659 | ], | |
660 | None, None, | |
661 | ['mgr:host1(name=y)', 'mgr:host2(name=x)'], | |
662 | [], [] | |
663 | ), | |
664 | ||
665 | # note: host -> rank mapping is psuedo-random based on svc name, so these | |
666 | # host/rank pairs may seem random but they match the nfs.mynfs seed used by | |
667 | # the test. | |
668 | ||
669 | # ranked, fresh | |
670 | NodeAssignmentTest( | |
671 | 'nfs', | |
672 | PlacementSpec(count=3), | |
673 | 'host1 host2 host3'.split(), | |
674 | [], | |
675 | {}, | |
676 | {0: {0: None}, 1: {0: None}, 2: {0: None}}, | |
677 | ['nfs:host3(rank=0.0)', 'nfs:host2(rank=1.0)', 'nfs:host1(rank=2.0)'], | |
678 | ['nfs:host3(rank=0.0)', 'nfs:host2(rank=1.0)', 'nfs:host1(rank=2.0)'], | |
679 | [] | |
680 | ), | |
681 | # 21: ranked, exist | |
682 | NodeAssignmentTest( | |
683 | 'nfs', | |
684 | PlacementSpec(count=3), | |
685 | 'host1 host2 host3'.split(), | |
686 | [ | |
687 | DaemonDescription('nfs', '0.1', 'host1', rank=0, rank_generation=1), | |
688 | ], | |
689 | {0: {1: '0.1'}}, | |
690 | {0: {1: '0.1'}, 1: {0: None}, 2: {0: None}}, | |
691 | ['nfs:host1(rank=0.1)', 'nfs:host3(rank=1.0)', 'nfs:host2(rank=2.0)'], | |
692 | ['nfs:host3(rank=1.0)', 'nfs:host2(rank=2.0)'], | |
693 | [] | |
694 | ), | |
695 | # ranked, exist, different ranks | |
696 | NodeAssignmentTest( | |
697 | 'nfs', | |
698 | PlacementSpec(count=3), | |
699 | 'host1 host2 host3'.split(), | |
700 | [ | |
701 | DaemonDescription('nfs', '0.1', 'host1', rank=0, rank_generation=1), | |
702 | DaemonDescription('nfs', '1.1', 'host2', rank=1, rank_generation=1), | |
703 | ], | |
704 | {0: {1: '0.1'}, 1: {1: '1.1'}}, | |
705 | {0: {1: '0.1'}, 1: {1: '1.1'}, 2: {0: None}}, | |
706 | ['nfs:host1(rank=0.1)', 'nfs:host2(rank=1.1)', 'nfs:host3(rank=2.0)'], | |
707 | ['nfs:host3(rank=2.0)'], | |
708 | [] | |
709 | ), | |
710 | # ranked, exist, different ranks (2) | |
711 | NodeAssignmentTest( | |
712 | 'nfs', | |
713 | PlacementSpec(count=3), | |
714 | 'host1 host2 host3'.split(), | |
715 | [ | |
716 | DaemonDescription('nfs', '0.1', 'host1', rank=0, rank_generation=1), | |
717 | DaemonDescription('nfs', '1.1', 'host3', rank=1, rank_generation=1), | |
718 | ], | |
719 | {0: {1: '0.1'}, 1: {1: '1.1'}}, | |
720 | {0: {1: '0.1'}, 1: {1: '1.1'}, 2: {0: None}}, | |
721 | ['nfs:host1(rank=0.1)', 'nfs:host3(rank=1.1)', 'nfs:host2(rank=2.0)'], | |
722 | ['nfs:host2(rank=2.0)'], | |
723 | [] | |
724 | ), | |
725 | # ranked, exist, extra ranks | |
726 | NodeAssignmentTest( | |
727 | 'nfs', | |
728 | PlacementSpec(count=3), | |
729 | 'host1 host2 host3'.split(), | |
730 | [ | |
731 | DaemonDescription('nfs', '0.5', 'host1', rank=0, rank_generation=5), | |
732 | DaemonDescription('nfs', '1.5', 'host2', rank=1, rank_generation=5), | |
733 | DaemonDescription('nfs', '4.5', 'host2', rank=4, rank_generation=5), | |
734 | ], | |
735 | {0: {5: '0.5'}, 1: {5: '1.5'}}, | |
736 | {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {0: None}}, | |
737 | ['nfs:host1(rank=0.5)', 'nfs:host2(rank=1.5)', 'nfs:host3(rank=2.0)'], | |
738 | ['nfs:host3(rank=2.0)'], | |
739 | ['nfs.4.5'] | |
740 | ), | |
741 | # 25: ranked, exist, extra ranks (scale down: kill off high rank) | |
742 | NodeAssignmentTest( | |
743 | 'nfs', | |
744 | PlacementSpec(count=2), | |
745 | 'host3 host2 host1'.split(), | |
746 | [ | |
747 | DaemonDescription('nfs', '0.5', 'host1', rank=0, rank_generation=5), | |
748 | DaemonDescription('nfs', '1.5', 'host2', rank=1, rank_generation=5), | |
749 | DaemonDescription('nfs', '2.5', 'host3', rank=2, rank_generation=5), | |
750 | ], | |
751 | {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {5: '2.5'}}, | |
752 | {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {5: '2.5'}}, | |
753 | ['nfs:host1(rank=0.5)', 'nfs:host2(rank=1.5)'], | |
754 | [], | |
755 | ['nfs.2.5'] | |
756 | ), | |
757 | # ranked, exist, extra ranks (scale down hosts) | |
758 | NodeAssignmentTest( | |
759 | 'nfs', | |
760 | PlacementSpec(count=2), | |
761 | 'host1 host3'.split(), | |
762 | [ | |
763 | DaemonDescription('nfs', '0.5', 'host1', rank=0, rank_generation=5), | |
764 | DaemonDescription('nfs', '1.5', 'host2', rank=1, rank_generation=5), | |
765 | DaemonDescription('nfs', '2.5', 'host3', rank=4, rank_generation=5), | |
766 | ], | |
767 | {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {5: '2.5'}}, | |
768 | {0: {5: '0.5'}, 1: {5: '1.5', 6: None}, 2: {5: '2.5'}}, | |
769 | ['nfs:host1(rank=0.5)', 'nfs:host3(rank=1.6)'], | |
770 | ['nfs:host3(rank=1.6)'], | |
771 | ['nfs.2.5', 'nfs.1.5'] | |
772 | ), | |
773 | # ranked, exist, duplicate rank | |
774 | NodeAssignmentTest( | |
775 | 'nfs', | |
776 | PlacementSpec(count=3), | |
777 | 'host1 host2 host3'.split(), | |
778 | [ | |
779 | DaemonDescription('nfs', '0.0', 'host1', rank=0, rank_generation=0), | |
780 | DaemonDescription('nfs', '1.1', 'host2', rank=1, rank_generation=1), | |
781 | DaemonDescription('nfs', '1.2', 'host3', rank=1, rank_generation=2), | |
782 | ], | |
783 | {0: {0: '0.0'}, 1: {2: '1.2'}}, | |
784 | {0: {0: '0.0'}, 1: {2: '1.2'}, 2: {0: None}}, | |
785 | ['nfs:host1(rank=0.0)', 'nfs:host3(rank=1.2)', 'nfs:host2(rank=2.0)'], | |
786 | ['nfs:host2(rank=2.0)'], | |
787 | ['nfs.1.1'] | |
788 | ), | |
789 | # 28: ranked, all gens stale (failure during update cycle) | |
790 | NodeAssignmentTest( | |
791 | 'nfs', | |
792 | PlacementSpec(count=2), | |
793 | 'host1 host2 host3'.split(), | |
794 | [ | |
795 | DaemonDescription('nfs', '0.2', 'host1', rank=0, rank_generation=2), | |
796 | DaemonDescription('nfs', '1.2', 'host2', rank=1, rank_generation=2), | |
797 | ], | |
798 | {0: {2: '0.2'}, 1: {2: '1.2', 3: '1.3'}}, | |
799 | {0: {2: '0.2'}, 1: {2: '1.2', 3: '1.3', 4: None}}, | |
800 | ['nfs:host1(rank=0.2)', 'nfs:host3(rank=1.4)'], | |
801 | ['nfs:host3(rank=1.4)'], | |
802 | ['nfs.1.2'] | |
803 | ), | |
804 | # ranked, not enough hosts | |
805 | NodeAssignmentTest( | |
806 | 'nfs', | |
807 | PlacementSpec(count=4), | |
808 | 'host1 host2 host3'.split(), | |
809 | [ | |
810 | DaemonDescription('nfs', '0.2', 'host1', rank=0, rank_generation=2), | |
811 | DaemonDescription('nfs', '1.2', 'host2', rank=1, rank_generation=2), | |
812 | ], | |
813 | {0: {2: '0.2'}, 1: {2: '1.2'}}, | |
814 | {0: {2: '0.2'}, 1: {2: '1.2'}, 2: {0: None}}, | |
815 | ['nfs:host1(rank=0.2)', 'nfs:host2(rank=1.2)', 'nfs:host3(rank=2.0)'], | |
816 | ['nfs:host3(rank=2.0)'], | |
817 | [] | |
818 | ), | |
819 | # ranked, scale down | |
820 | NodeAssignmentTest( | |
821 | 'nfs', | |
822 | PlacementSpec(hosts=['host2']), | |
823 | 'host1 host2'.split(), | |
824 | [ | |
825 | DaemonDescription('nfs', '0.2', 'host1', rank=0, rank_generation=2), | |
826 | DaemonDescription('nfs', '1.2', 'host2', rank=1, rank_generation=2), | |
827 | DaemonDescription('nfs', '2.2', 'host3', rank=2, rank_generation=2), | |
828 | ], | |
829 | {0: {2: '0.2'}, 1: {2: '1.2'}, 2: {2: '2.2'}}, | |
830 | {0: {2: '0.2', 3: None}, 1: {2: '1.2'}, 2: {2: '2.2'}}, | |
831 | ['nfs:host2(rank=0.3)'], | |
832 | ['nfs:host2(rank=0.3)'], | |
833 | ['nfs.0.2', 'nfs.1.2', 'nfs.2.2'] | |
834 | ), | |
835 | ||
836 | ]) | |
837 | def test_node_assignment(service_type, placement, hosts, daemons, rank_map, post_rank_map, | |
838 | expected, expected_add, expected_remove): | |
839 | spec = None | |
840 | service_id = None | |
841 | allow_colo = False | |
842 | if service_type == 'rgw': | |
843 | service_id = 'realm.zone' | |
844 | allow_colo = True | |
845 | elif service_type == 'mds': | |
846 | service_id = 'myfs' | |
847 | allow_colo = True | |
848 | elif service_type == 'nfs': | |
849 | service_id = 'mynfs' | |
850 | spec = ServiceSpec(service_type=service_type, | |
851 | service_id=service_id, | |
852 | placement=placement) | |
853 | ||
854 | if not spec: | |
855 | spec = ServiceSpec(service_type=service_type, | |
856 | service_id=service_id, | |
857 | placement=placement) | |
858 | ||
859 | all_slots, to_add, to_remove = HostAssignment( | |
860 | spec=spec, | |
861 | hosts=[HostSpec(h, labels=['foo']) for h in hosts], | |
862 | unreachable_hosts=[], | |
863 | draining_hosts=[], | |
864 | daemons=daemons, | |
865 | allow_colo=allow_colo, | |
866 | rank_map=rank_map, | |
867 | ).place() | |
868 | ||
869 | assert rank_map == post_rank_map | |
870 | ||
871 | got = [str(p) for p in all_slots] | |
872 | num_wildcard = 0 | |
873 | for i in expected: | |
874 | if i == '*': | |
875 | num_wildcard += 1 | |
876 | else: | |
877 | assert i in got | |
878 | got.remove(i) | |
879 | assert num_wildcard == len(got) | |
880 | ||
881 | got = [str(p) for p in to_add] | |
882 | num_wildcard = 0 | |
883 | for i in expected_add: | |
884 | if i == '*': | |
885 | num_wildcard += 1 | |
886 | else: | |
887 | assert i in got | |
888 | got.remove(i) | |
889 | assert num_wildcard == len(got) | |
890 | ||
891 | assert sorted([d.name() for d in to_remove]) == sorted(expected_remove) | |
892 | ||
893 | ||
894 | class NodeAssignmentTest5(NamedTuple): | |
895 | service_type: str | |
896 | placement: PlacementSpec | |
897 | available_hosts: List[str] | |
898 | candidates_hosts: List[str] | |
899 | ||
900 | ||
901 | @pytest.mark.parametrize("service_type, placement, available_hosts, expected_candidates", | |
902 | [ # noqa: E128 | |
903 | NodeAssignmentTest5( | |
904 | 'alertmanager', | |
905 | PlacementSpec(hosts='host1 host2 host3 host4'.split()), | |
906 | 'host1 host2 host3 host4'.split(), | |
907 | 'host3 host1 host4 host2'.split(), | |
908 | ), | |
909 | NodeAssignmentTest5( | |
910 | 'prometheus', | |
911 | PlacementSpec(hosts='host1 host2 host3 host4'.split()), | |
912 | 'host1 host2 host3 host4'.split(), | |
913 | 'host3 host2 host4 host1'.split(), | |
914 | ), | |
915 | NodeAssignmentTest5( | |
916 | 'grafana', | |
917 | PlacementSpec(hosts='host1 host2 host3 host4'.split()), | |
918 | 'host1 host2 host3 host4'.split(), | |
919 | 'host1 host2 host4 host3'.split(), | |
920 | ), | |
921 | NodeAssignmentTest5( | |
922 | 'mgr', | |
923 | PlacementSpec(hosts='host1 host2 host3 host4'.split()), | |
924 | 'host1 host2 host3 host4'.split(), | |
925 | 'host4 host2 host1 host3'.split(), | |
926 | ), | |
927 | NodeAssignmentTest5( | |
928 | 'mon', | |
929 | PlacementSpec(hosts='host1 host2 host3 host4'.split()), | |
930 | 'host1 host2 host3 host4'.split(), | |
931 | 'host1 host3 host4 host2'.split(), | |
932 | ), | |
933 | NodeAssignmentTest5( | |
934 | 'rgw', | |
935 | PlacementSpec(hosts='host1 host2 host3 host4'.split()), | |
936 | 'host1 host2 host3 host4'.split(), | |
937 | 'host1 host3 host2 host4'.split(), | |
938 | ), | |
939 | NodeAssignmentTest5( | |
940 | 'cephfs-mirror', | |
941 | PlacementSpec(hosts='host1 host2 host3 host4'.split()), | |
942 | 'host1 host2 host3 host4'.split(), | |
943 | 'host4 host3 host1 host2'.split(), | |
944 | ), | |
945 | ]) | |
946 | def test_node_assignment_random_shuffle(service_type, placement, available_hosts, expected_candidates): | |
947 | spec = None | |
948 | service_id = None | |
949 | allow_colo = False | |
950 | spec = ServiceSpec(service_type=service_type, | |
951 | service_id=service_id, | |
952 | placement=placement) | |
953 | ||
954 | candidates = HostAssignment( | |
955 | spec=spec, | |
956 | hosts=[HostSpec(h, labels=['foo']) for h in available_hosts], | |
957 | unreachable_hosts=[], | |
958 | draining_hosts=[], | |
959 | daemons=[], | |
960 | allow_colo=allow_colo, | |
961 | ).get_candidates() | |
962 | ||
963 | candidates_hosts = [h.hostname for h in candidates] | |
964 | assert candidates_hosts == expected_candidates | |
965 | ||
966 | ||
967 | class NodeAssignmentTest2(NamedTuple): | |
968 | service_type: str | |
969 | placement: PlacementSpec | |
970 | hosts: List[str] | |
971 | daemons: List[DaemonDescription] | |
972 | expected_len: int | |
973 | in_set: List[str] | |
974 | ||
975 | ||
976 | @pytest.mark.parametrize("service_type,placement,hosts,daemons,expected_len,in_set", | |
977 | [ # noqa: E128 | |
978 | # just count | |
979 | NodeAssignmentTest2( | |
980 | 'mgr', | |
981 | PlacementSpec(count=1), | |
982 | 'host1 host2 host3'.split(), | |
983 | [], | |
984 | 1, | |
985 | ['host1', 'host2', 'host3'], | |
986 | ), | |
987 | ||
988 | # hosts + (smaller) count | |
989 | NodeAssignmentTest2( | |
990 | 'mgr', | |
991 | PlacementSpec(count=1, hosts='host1 host2'.split()), | |
992 | 'host1 host2'.split(), | |
993 | [], | |
994 | 1, | |
995 | ['host1', 'host2'], | |
996 | ), | |
997 | # hosts + (smaller) count, existing | |
998 | NodeAssignmentTest2( | |
999 | 'mgr', | |
1000 | PlacementSpec(count=1, hosts='host1 host2 host3'.split()), | |
1001 | 'host1 host2 host3'.split(), | |
1002 | [DaemonDescription('mgr', 'mgr.a', 'host1')], | |
1003 | 1, | |
1004 | ['host1', 'host2', 'host3'], | |
1005 | ), | |
1006 | # hosts + (smaller) count, (more) existing | |
1007 | NodeAssignmentTest2( | |
1008 | 'mgr', | |
1009 | PlacementSpec(count=1, hosts='host1 host2 host3'.split()), | |
1010 | 'host1 host2 host3'.split(), | |
1011 | [ | |
1012 | DaemonDescription('mgr', 'a', 'host1'), | |
1013 | DaemonDescription('mgr', 'b', 'host2'), | |
1014 | ], | |
1015 | 1, | |
1016 | ['host1', 'host2'] | |
1017 | ), | |
1018 | # count + partial host list | |
1019 | NodeAssignmentTest2( | |
1020 | 'mgr', | |
1021 | PlacementSpec(count=2, hosts=['host3']), | |
1022 | 'host1 host2 host3'.split(), | |
1023 | [], | |
1024 | 1, | |
1025 | ['host1', 'host2', 'host3'] | |
1026 | ), | |
1027 | # label + count | |
1028 | NodeAssignmentTest2( | |
1029 | 'mgr', | |
1030 | PlacementSpec(count=1, label='foo'), | |
1031 | 'host1 host2 host3'.split(), | |
1032 | [], | |
1033 | 1, | |
1034 | ['host1', 'host2', 'host3'] | |
1035 | ), | |
1036 | ]) | |
1037 | def test_node_assignment2(service_type, placement, hosts, | |
1038 | daemons, expected_len, in_set): | |
1039 | hosts, to_add, to_remove = HostAssignment( | |
1040 | spec=ServiceSpec(service_type, placement=placement), | |
1041 | hosts=[HostSpec(h, labels=['foo']) for h in hosts], | |
1042 | unreachable_hosts=[], | |
1043 | draining_hosts=[], | |
1044 | daemons=daemons, | |
1045 | ).place() | |
1046 | assert len(hosts) == expected_len | |
1047 | for h in [h.hostname for h in hosts]: | |
1048 | assert h in in_set | |
1049 | ||
1050 | ||
1051 | @pytest.mark.parametrize("service_type,placement,hosts,daemons,expected_len,must_have", | |
1052 | [ # noqa: E128 | |
1053 | # hosts + (smaller) count, (more) existing | |
1054 | NodeAssignmentTest2( | |
1055 | 'mgr', | |
1056 | PlacementSpec(count=3, hosts='host3'.split()), | |
1057 | 'host1 host2 host3'.split(), | |
1058 | [], | |
1059 | 1, | |
1060 | ['host3'] | |
1061 | ), | |
1062 | # count + partial host list | |
1063 | NodeAssignmentTest2( | |
1064 | 'mgr', | |
1065 | PlacementSpec(count=2, hosts=['host3']), | |
1066 | 'host1 host2 host3'.split(), | |
1067 | [], | |
1068 | 1, | |
1069 | ['host3'] | |
1070 | ), | |
1071 | ]) | |
1072 | def test_node_assignment3(service_type, placement, hosts, | |
1073 | daemons, expected_len, must_have): | |
1074 | hosts, to_add, to_remove = HostAssignment( | |
1075 | spec=ServiceSpec(service_type, placement=placement), | |
1076 | hosts=[HostSpec(h) for h in hosts], | |
1077 | unreachable_hosts=[], | |
1078 | draining_hosts=[], | |
1079 | daemons=daemons, | |
1080 | ).place() | |
1081 | assert len(hosts) == expected_len | |
1082 | for h in must_have: | |
1083 | assert h in [h.hostname for h in hosts] | |
1084 | ||
1085 | ||
1086 | class NodeAssignmentTest4(NamedTuple): | |
1087 | spec: ServiceSpec | |
1088 | networks: Dict[str, Dict[str, Dict[str, List[str]]]] | |
1089 | daemons: List[DaemonDescription] | |
1090 | expected: List[str] | |
1091 | expected_add: List[str] | |
1092 | expected_remove: List[DaemonDescription] | |
1093 | ||
1094 | ||
1095 | @pytest.mark.parametrize("spec,networks,daemons,expected,expected_add,expected_remove", | |
1096 | [ # noqa: E128 | |
1097 | NodeAssignmentTest4( | |
1098 | ServiceSpec( | |
1099 | service_type='rgw', | |
1100 | service_id='foo', | |
1101 | placement=PlacementSpec(count=6, label='foo'), | |
1102 | networks=['10.0.0.0/8'], | |
1103 | ), | |
1104 | { | |
1105 | 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}}, | |
1106 | 'host2': {'10.0.0.0/8': {'eth0': ['10.0.0.2']}}, | |
1107 | 'host3': {'192.168.0.0/16': {'eth0': ['192.168.0.1']}}, | |
1108 | }, | |
1109 | [], | |
1110 | ['rgw:host1(10.0.0.1:80)', 'rgw:host2(10.0.0.2:80)', | |
1111 | 'rgw:host1(10.0.0.1:81)', 'rgw:host2(10.0.0.2:81)', | |
1112 | 'rgw:host1(10.0.0.1:82)', 'rgw:host2(10.0.0.2:82)'], | |
1113 | ['rgw:host1(10.0.0.1:80)', 'rgw:host2(10.0.0.2:80)', | |
1114 | 'rgw:host1(10.0.0.1:81)', 'rgw:host2(10.0.0.2:81)', | |
1115 | 'rgw:host1(10.0.0.1:82)', 'rgw:host2(10.0.0.2:82)'], | |
1116 | [] | |
1117 | ), | |
1118 | NodeAssignmentTest4( | |
1119 | IngressSpec( | |
1120 | service_type='ingress', | |
1121 | service_id='rgw.foo', | |
1122 | frontend_port=443, | |
1123 | monitor_port=8888, | |
1124 | virtual_ip='10.0.0.20/8', | |
1125 | backend_service='rgw.foo', | |
1126 | placement=PlacementSpec(label='foo'), | |
1127 | networks=['10.0.0.0/8'], | |
1128 | ), | |
1129 | { | |
1130 | 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}}, | |
1131 | 'host2': {'10.0.0.0/8': {'eth1': ['10.0.0.2']}}, | |
1132 | 'host3': {'192.168.0.0/16': {'eth2': ['192.168.0.1']}}, | |
1133 | }, | |
1134 | [], | |
1135 | ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)', | |
1136 | 'keepalived:host1', 'keepalived:host2'], | |
1137 | ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)', | |
1138 | 'keepalived:host1', 'keepalived:host2'], | |
1139 | [] | |
1140 | ), | |
1141 | NodeAssignmentTest4( | |
1142 | IngressSpec( | |
1143 | service_type='ingress', | |
1144 | service_id='rgw.foo', | |
1145 | frontend_port=443, | |
1146 | monitor_port=8888, | |
1147 | virtual_ip='10.0.0.20/8', | |
1148 | backend_service='rgw.foo', | |
1149 | placement=PlacementSpec(label='foo'), | |
1150 | networks=['10.0.0.0/8'], | |
1151 | ), | |
1152 | { | |
1153 | 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}}, | |
1154 | 'host2': {'10.0.0.0/8': {'eth1': ['10.0.0.2']}}, | |
1155 | 'host3': {'192.168.0.0/16': {'eth2': ['192.168.0.1']}}, | |
1156 | }, | |
1157 | [ | |
1158 | DaemonDescription('haproxy', 'a', 'host1', ip='10.0.0.1', | |
1159 | ports=[443, 8888]), | |
1160 | DaemonDescription('keepalived', 'b', 'host2'), | |
1161 | DaemonDescription('keepalived', 'c', 'host3'), | |
1162 | ], | |
1163 | ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)', | |
1164 | 'keepalived:host1', 'keepalived:host2'], | |
1165 | ['haproxy:host2(10.0.0.2:443,8888)', | |
1166 | 'keepalived:host1'], | |
1167 | ['keepalived.c'] | |
1168 | ), | |
1169 | ]) | |
1170 | def test_node_assignment4(spec, networks, daemons, | |
1171 | expected, expected_add, expected_remove): | |
1172 | all_slots, to_add, to_remove = HostAssignment( | |
1173 | spec=spec, | |
1174 | hosts=[HostSpec(h, labels=['foo']) for h in networks.keys()], | |
1175 | unreachable_hosts=[], | |
1176 | draining_hosts=[], | |
1177 | daemons=daemons, | |
1178 | allow_colo=True, | |
1179 | networks=networks, | |
1180 | primary_daemon_type='haproxy' if spec.service_type == 'ingress' else spec.service_type, | |
1181 | per_host_daemon_type='keepalived' if spec.service_type == 'ingress' else None, | |
1182 | ).place() | |
1183 | ||
1184 | got = [str(p) for p in all_slots] | |
1185 | num_wildcard = 0 | |
1186 | for i in expected: | |
1187 | if i == '*': | |
1188 | num_wildcard += 1 | |
1189 | else: | |
1190 | assert i in got | |
1191 | got.remove(i) | |
1192 | assert num_wildcard == len(got) | |
1193 | ||
1194 | got = [str(p) for p in to_add] | |
1195 | num_wildcard = 0 | |
1196 | for i in expected_add: | |
1197 | if i == '*': | |
1198 | num_wildcard += 1 | |
1199 | else: | |
1200 | assert i in got | |
1201 | got.remove(i) | |
1202 | assert num_wildcard == len(got) | |
1203 | ||
1204 | assert sorted([d.name() for d in to_remove]) == sorted(expected_remove) | |
1205 | ||
1206 | ||
1207 | @pytest.mark.parametrize("placement", | |
1208 | [ # noqa: E128 | |
1209 | ('1 *'), | |
1210 | ('* label:foo'), | |
1211 | ('* host1 host2'), | |
1212 | ('hostname12hostname12hostname12hostname12hostname12hostname12hostname12'), # > 63 chars | |
1213 | ]) | |
1214 | def test_bad_placements(placement): | |
1215 | try: | |
1216 | PlacementSpec.from_string(placement.split(' ')) | |
1217 | assert False | |
1218 | except SpecValidationError: | |
1219 | pass | |
1220 | ||
1221 | ||
1222 | class NodeAssignmentTestBadSpec(NamedTuple): | |
1223 | service_type: str | |
1224 | placement: PlacementSpec | |
1225 | hosts: List[str] | |
1226 | daemons: List[DaemonDescription] | |
1227 | expected: str | |
1228 | ||
1229 | ||
1230 | @pytest.mark.parametrize("service_type,placement,hosts,daemons,expected", | |
1231 | [ # noqa: E128 | |
1232 | # unknown host | |
1233 | NodeAssignmentTestBadSpec( | |
1234 | 'mgr', | |
1235 | PlacementSpec(hosts=['unknownhost']), | |
1236 | ['knownhost'], | |
1237 | [], | |
1238 | "Cannot place <ServiceSpec for service_name=mgr> on unknownhost: Unknown hosts" | |
1239 | ), | |
1240 | # unknown host pattern | |
1241 | NodeAssignmentTestBadSpec( | |
1242 | 'mgr', | |
1243 | PlacementSpec(host_pattern='unknownhost'), | |
1244 | ['knownhost'], | |
1245 | [], | |
1246 | "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts" | |
1247 | ), | |
1248 | # unknown label | |
1249 | NodeAssignmentTestBadSpec( | |
1250 | 'mgr', | |
1251 | PlacementSpec(label='unknownlabel'), | |
1252 | [], | |
1253 | [], | |
1254 | "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts for label unknownlabel" | |
1255 | ), | |
1256 | ]) | |
1257 | def test_bad_specs(service_type, placement, hosts, daemons, expected): | |
1258 | with pytest.raises(OrchestratorValidationError) as e: | |
1259 | hosts, to_add, to_remove = HostAssignment( | |
1260 | spec=ServiceSpec(service_type, placement=placement), | |
1261 | hosts=[HostSpec(h) for h in hosts], | |
1262 | unreachable_hosts=[], | |
1263 | draining_hosts=[], | |
1264 | daemons=daemons, | |
1265 | ).place() | |
1266 | assert str(e.value) == expected | |
1267 | ||
1268 | ||
1269 | class ActiveAssignmentTest(NamedTuple): | |
1270 | service_type: str | |
1271 | placement: PlacementSpec | |
1272 | hosts: List[str] | |
1273 | daemons: List[DaemonDescription] | |
1274 | expected: List[List[str]] | |
1275 | expected_add: List[List[str]] | |
1276 | expected_remove: List[List[str]] | |
1277 | ||
1278 | ||
1279 | @pytest.mark.parametrize("service_type,placement,hosts,daemons,expected,expected_add,expected_remove", | |
1280 | [ | |
1281 | ActiveAssignmentTest( | |
1282 | 'mgr', | |
1283 | PlacementSpec(count=2), | |
1284 | 'host1 host2 host3'.split(), | |
1285 | [ | |
1286 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
1287 | DaemonDescription('mgr', 'b', 'host2'), | |
1288 | DaemonDescription('mgr', 'c', 'host3'), | |
1289 | ], | |
1290 | [['host1', 'host2'], ['host1', 'host3']], | |
1291 | [[]], | |
1292 | [['mgr.b'], ['mgr.c']] | |
1293 | ), | |
1294 | ActiveAssignmentTest( | |
1295 | 'mgr', | |
1296 | PlacementSpec(count=2), | |
1297 | 'host1 host2 host3'.split(), | |
1298 | [ | |
1299 | DaemonDescription('mgr', 'a', 'host1'), | |
1300 | DaemonDescription('mgr', 'b', 'host2'), | |
1301 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1302 | ], | |
1303 | [['host1', 'host3'], ['host2', 'host3']], | |
1304 | [[]], | |
1305 | [['mgr.a'], ['mgr.b']] | |
1306 | ), | |
1307 | ActiveAssignmentTest( | |
1308 | 'mgr', | |
1309 | PlacementSpec(count=1), | |
1310 | 'host1 host2 host3'.split(), | |
1311 | [ | |
1312 | DaemonDescription('mgr', 'a', 'host1'), | |
1313 | DaemonDescription('mgr', 'b', 'host2', is_active=True), | |
1314 | DaemonDescription('mgr', 'c', 'host3'), | |
1315 | ], | |
1316 | [['host2']], | |
1317 | [[]], | |
1318 | [['mgr.a', 'mgr.c']] | |
1319 | ), | |
1320 | ActiveAssignmentTest( | |
1321 | 'mgr', | |
1322 | PlacementSpec(count=1), | |
1323 | 'host1 host2 host3'.split(), | |
1324 | [ | |
1325 | DaemonDescription('mgr', 'a', 'host1'), | |
1326 | DaemonDescription('mgr', 'b', 'host2'), | |
1327 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1328 | ], | |
1329 | [['host3']], | |
1330 | [[]], | |
1331 | [['mgr.a', 'mgr.b']] | |
1332 | ), | |
1333 | ActiveAssignmentTest( | |
1334 | 'mgr', | |
1335 | PlacementSpec(count=1), | |
1336 | 'host1 host2 host3'.split(), | |
1337 | [ | |
1338 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
1339 | DaemonDescription('mgr', 'b', 'host2'), | |
1340 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1341 | ], | |
1342 | [['host1'], ['host3']], | |
1343 | [[]], | |
1344 | [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c']] | |
1345 | ), | |
1346 | ActiveAssignmentTest( | |
1347 | 'mgr', | |
1348 | PlacementSpec(count=2), | |
1349 | 'host1 host2 host3'.split(), | |
1350 | [ | |
1351 | DaemonDescription('mgr', 'a', 'host1'), | |
1352 | DaemonDescription('mgr', 'b', 'host2', is_active=True), | |
1353 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1354 | ], | |
1355 | [['host2', 'host3']], | |
1356 | [[]], | |
1357 | [['mgr.a']] | |
1358 | ), | |
1359 | ActiveAssignmentTest( | |
1360 | 'mgr', | |
1361 | PlacementSpec(count=1), | |
1362 | 'host1 host2 host3'.split(), | |
1363 | [ | |
1364 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
1365 | DaemonDescription('mgr', 'b', 'host2', is_active=True), | |
1366 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1367 | ], | |
1368 | [['host1'], ['host2'], ['host3']], | |
1369 | [[]], | |
1370 | [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c'], ['mgr.a', 'mgr.c']] | |
1371 | ), | |
1372 | ActiveAssignmentTest( | |
1373 | 'mgr', | |
1374 | PlacementSpec(count=1), | |
1375 | 'host1 host2 host3'.split(), | |
1376 | [ | |
1377 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
1378 | DaemonDescription('mgr', 'a2', 'host1'), | |
1379 | DaemonDescription('mgr', 'b', 'host2'), | |
1380 | DaemonDescription('mgr', 'c', 'host3'), | |
1381 | ], | |
1382 | [['host1']], | |
1383 | [[]], | |
1384 | [['mgr.a2', 'mgr.b', 'mgr.c']] | |
1385 | ), | |
1386 | ActiveAssignmentTest( | |
1387 | 'mgr', | |
1388 | PlacementSpec(count=1), | |
1389 | 'host1 host2 host3'.split(), | |
1390 | [ | |
1391 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
1392 | DaemonDescription('mgr', 'a2', 'host1', is_active=True), | |
1393 | DaemonDescription('mgr', 'b', 'host2'), | |
1394 | DaemonDescription('mgr', 'c', 'host3'), | |
1395 | ], | |
1396 | [['host1']], | |
1397 | [[]], | |
1398 | [['mgr.a', 'mgr.b', 'mgr.c'], ['mgr.a2', 'mgr.b', 'mgr.c']] | |
1399 | ), | |
1400 | ActiveAssignmentTest( | |
1401 | 'mgr', | |
1402 | PlacementSpec(count=2), | |
1403 | 'host1 host2 host3'.split(), | |
1404 | [ | |
1405 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
1406 | DaemonDescription('mgr', 'a2', 'host1'), | |
1407 | DaemonDescription('mgr', 'b', 'host2'), | |
1408 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1409 | ], | |
1410 | [['host1', 'host3']], | |
1411 | [[]], | |
1412 | [['mgr.a2', 'mgr.b']] | |
1413 | ), | |
1414 | # Explicit placement should override preference for active daemon | |
1415 | ActiveAssignmentTest( | |
1416 | 'mgr', | |
1417 | PlacementSpec(count=1, hosts=['host1']), | |
1418 | 'host1 host2 host3'.split(), | |
1419 | [ | |
1420 | DaemonDescription('mgr', 'a', 'host1'), | |
1421 | DaemonDescription('mgr', 'b', 'host2'), | |
1422 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1423 | ], | |
1424 | [['host1']], | |
1425 | [[]], | |
1426 | [['mgr.b', 'mgr.c']] | |
1427 | ), | |
1428 | ||
1429 | ]) | |
1430 | def test_active_assignment(service_type, placement, hosts, daemons, expected, expected_add, expected_remove): | |
1431 | ||
1432 | spec = ServiceSpec(service_type=service_type, | |
1433 | service_id=None, | |
1434 | placement=placement) | |
1435 | ||
1436 | hosts, to_add, to_remove = HostAssignment( | |
1437 | spec=spec, | |
1438 | hosts=[HostSpec(h) for h in hosts], | |
1439 | unreachable_hosts=[], | |
1440 | draining_hosts=[], | |
1441 | daemons=daemons, | |
1442 | ).place() | |
1443 | assert sorted([h.hostname for h in hosts]) in expected | |
1444 | assert sorted([h.hostname for h in to_add]) in expected_add | |
1445 | assert sorted([h.name() for h in to_remove]) in expected_remove | |
1446 | ||
1447 | ||
1448 | class UnreachableHostsTest(NamedTuple): | |
1449 | service_type: str | |
1450 | placement: PlacementSpec | |
1451 | hosts: List[str] | |
1452 | unreachables_hosts: List[str] | |
1453 | daemons: List[DaemonDescription] | |
1454 | expected_add: List[List[str]] | |
1455 | expected_remove: List[List[str]] | |
1456 | ||
1457 | ||
1458 | @pytest.mark.parametrize("service_type,placement,hosts,unreachable_hosts,daemons,expected_add,expected_remove", | |
1459 | [ | |
1460 | UnreachableHostsTest( | |
1461 | 'mgr', | |
1462 | PlacementSpec(count=3), | |
1463 | 'host1 host2 host3'.split(), | |
1464 | ['host2'], | |
1465 | [], | |
1466 | [['host1', 'host3']], | |
1467 | [[]], | |
1468 | ), | |
1469 | UnreachableHostsTest( | |
1470 | 'mgr', | |
1471 | PlacementSpec(hosts=['host3']), | |
1472 | 'host1 host2 host3'.split(), | |
1473 | ['host1'], | |
1474 | [ | |
1475 | DaemonDescription('mgr', 'a', 'host1'), | |
1476 | DaemonDescription('mgr', 'b', 'host2'), | |
1477 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1478 | ], | |
1479 | [[]], | |
1480 | [['mgr.b']], | |
1481 | ), | |
1482 | UnreachableHostsTest( | |
1483 | 'mgr', | |
1484 | PlacementSpec(count=3), | |
1485 | 'host1 host2 host3 host4'.split(), | |
1486 | ['host1'], | |
1487 | [ | |
1488 | DaemonDescription('mgr', 'a', 'host1'), | |
1489 | DaemonDescription('mgr', 'b', 'host2'), | |
1490 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1491 | ], | |
1492 | [[]], | |
1493 | [[]], | |
1494 | ), | |
1495 | UnreachableHostsTest( | |
1496 | 'mgr', | |
1497 | PlacementSpec(count=1), | |
1498 | 'host1 host2 host3 host4'.split(), | |
1499 | 'host1 host3'.split(), | |
1500 | [ | |
1501 | DaemonDescription('mgr', 'a', 'host1'), | |
1502 | DaemonDescription('mgr', 'b', 'host2'), | |
1503 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1504 | ], | |
1505 | [[]], | |
1506 | [['mgr.b']], | |
1507 | ), | |
1508 | UnreachableHostsTest( | |
1509 | 'mgr', | |
1510 | PlacementSpec(count=3), | |
1511 | 'host1 host2 host3 host4'.split(), | |
1512 | ['host2'], | |
1513 | [], | |
1514 | [['host1', 'host3', 'host4']], | |
1515 | [[]], | |
1516 | ), | |
1517 | UnreachableHostsTest( | |
1518 | 'mgr', | |
1519 | PlacementSpec(count=3), | |
1520 | 'host1 host2 host3 host4'.split(), | |
1521 | 'host1 host4'.split(), | |
1522 | [], | |
1523 | [['host2', 'host3']], | |
1524 | [[]], | |
1525 | ), | |
1526 | ||
1527 | ]) | |
1528 | def test_unreachable_host(service_type, placement, hosts, unreachable_hosts, daemons, expected_add, expected_remove): | |
1529 | ||
1530 | spec = ServiceSpec(service_type=service_type, | |
1531 | service_id=None, | |
1532 | placement=placement) | |
1533 | ||
1534 | hosts, to_add, to_remove = HostAssignment( | |
1535 | spec=spec, | |
1536 | hosts=[HostSpec(h) for h in hosts], | |
1537 | unreachable_hosts=[HostSpec(h) for h in unreachable_hosts], | |
1538 | draining_hosts=[], | |
1539 | daemons=daemons, | |
1540 | ).place() | |
1541 | assert sorted([h.hostname for h in to_add]) in expected_add | |
1542 | assert sorted([h.name() for h in to_remove]) in expected_remove | |
1543 | ||
1544 | ||
1545 | class RescheduleFromOfflineTest(NamedTuple): | |
1546 | service_type: str | |
1547 | placement: PlacementSpec | |
1548 | hosts: List[str] | |
1549 | maintenance_hosts: List[str] | |
1550 | offline_hosts: List[str] | |
1551 | daemons: List[DaemonDescription] | |
1552 | expected_add: List[List[str]] | |
1553 | expected_remove: List[List[str]] | |
1554 | ||
1555 | ||
1556 | @pytest.mark.parametrize("service_type,placement,hosts,maintenance_hosts,offline_hosts,daemons,expected_add,expected_remove", | |
1557 | [ | |
1558 | RescheduleFromOfflineTest( | |
1559 | 'nfs', | |
1560 | PlacementSpec(count=2), | |
1561 | 'host1 host2 host3'.split(), | |
1562 | [], | |
1563 | ['host2'], | |
1564 | [ | |
1565 | DaemonDescription('nfs', 'a', 'host1'), | |
1566 | DaemonDescription('nfs', 'b', 'host2'), | |
1567 | ], | |
1568 | [['host3']], | |
1569 | [[]], | |
1570 | ), | |
1571 | RescheduleFromOfflineTest( | |
1572 | 'nfs', | |
1573 | PlacementSpec(count=2), | |
1574 | 'host1 host2 host3'.split(), | |
1575 | ['host2'], | |
1576 | [], | |
1577 | [ | |
1578 | DaemonDescription('nfs', 'a', 'host1'), | |
1579 | DaemonDescription('nfs', 'b', 'host2'), | |
1580 | ], | |
1581 | [[]], | |
1582 | [[]], | |
1583 | ), | |
1584 | RescheduleFromOfflineTest( | |
1585 | 'mon', | |
1586 | PlacementSpec(count=2), | |
1587 | 'host1 host2 host3'.split(), | |
1588 | [], | |
1589 | ['host2'], | |
1590 | [ | |
1591 | DaemonDescription('mon', 'a', 'host1'), | |
1592 | DaemonDescription('mon', 'b', 'host2'), | |
1593 | ], | |
1594 | [[]], | |
1595 | [[]], | |
1596 | ), | |
1597 | RescheduleFromOfflineTest( | |
1598 | 'ingress', | |
1599 | PlacementSpec(count=1), | |
1600 | 'host1 host2'.split(), | |
1601 | [], | |
1602 | ['host2'], | |
1603 | [ | |
1604 | DaemonDescription('haproxy', 'b', 'host2'), | |
1605 | DaemonDescription('keepalived', 'b', 'host2'), | |
1606 | ], | |
1607 | [['host1']], | |
1608 | [[]], | |
1609 | ), | |
1610 | ]) | |
1611 | def test_remove_from_offline(service_type, placement, hosts, maintenance_hosts, offline_hosts, daemons, expected_add, expected_remove): | |
1612 | ||
1613 | if service_type == 'ingress': | |
1614 | spec = \ | |
1615 | IngressSpec( | |
1616 | service_type='ingress', | |
1617 | service_id='nfs-ha.foo', | |
1618 | frontend_port=443, | |
1619 | monitor_port=8888, | |
1620 | virtual_ip='10.0.0.20/8', | |
1621 | backend_service='nfs-ha.foo', | |
1622 | placement=placement, | |
1623 | ) | |
1624 | else: | |
1625 | spec = \ | |
1626 | ServiceSpec( | |
1627 | service_type=service_type, | |
1628 | service_id='test', | |
1629 | placement=placement, | |
1630 | ) | |
1631 | ||
1632 | host_specs = [HostSpec(h) for h in hosts] | |
1633 | for h in host_specs: | |
1634 | if h.hostname in offline_hosts: | |
1635 | h.status = 'offline' | |
1636 | if h.hostname in maintenance_hosts: | |
1637 | h.status = 'maintenance' | |
1638 | ||
1639 | hosts, to_add, to_remove = HostAssignment( | |
1640 | spec=spec, | |
1641 | hosts=host_specs, | |
1642 | unreachable_hosts=[h for h in host_specs if h.status], | |
1643 | draining_hosts=[], | |
1644 | daemons=daemons, | |
1645 | ).place() | |
1646 | assert sorted([h.hostname for h in to_add]) in expected_add | |
1647 | assert sorted([h.name() for h in to_remove]) in expected_remove | |
1648 | ||
1649 | ||
1650 | class DrainExplicitPlacementTest(NamedTuple): | |
1651 | service_type: str | |
1652 | placement: PlacementSpec | |
1653 | hosts: List[str] | |
1654 | maintenance_hosts: List[str] | |
1655 | offline_hosts: List[str] | |
1656 | draining_hosts: List[str] | |
1657 | daemons: List[DaemonDescription] | |
1658 | expected_add: List[List[str]] | |
1659 | expected_remove: List[List[str]] | |
1660 | ||
1661 | ||
1662 | @pytest.mark.parametrize("service_type,placement,hosts,maintenance_hosts,offline_hosts,draining_hosts,daemons,expected_add,expected_remove", | |
1663 | [ | |
1664 | DrainExplicitPlacementTest( | |
1665 | 'crash', | |
1666 | PlacementSpec(hosts='host1 host2 host3'.split()), | |
1667 | 'host1 host2 host3 host4'.split(), | |
1668 | [], | |
1669 | [], | |
1670 | ['host3'], | |
1671 | [ | |
1672 | DaemonDescription('crash', 'host1', 'host1'), | |
1673 | DaemonDescription('crash', 'host2', 'host2'), | |
1674 | DaemonDescription('crash', 'host3', 'host3'), | |
1675 | ], | |
1676 | [[]], | |
1677 | [['crash.host3']], | |
1678 | ), | |
1679 | DrainExplicitPlacementTest( | |
1680 | 'crash', | |
1681 | PlacementSpec(hosts='host1 host2 host3 host4'.split()), | |
1682 | 'host1 host2 host3 host4'.split(), | |
1683 | [], | |
1684 | [], | |
1685 | ['host1', 'host4'], | |
1686 | [ | |
1687 | DaemonDescription('crash', 'host1', 'host1'), | |
1688 | DaemonDescription('crash', 'host3', 'host3'), | |
1689 | ], | |
1690 | [['host2']], | |
1691 | [['crash.host1']], | |
1692 | ), | |
1693 | ]) | |
1694 | def test_drain_from_explict_placement(service_type, placement, hosts, maintenance_hosts, offline_hosts, draining_hosts, daemons, expected_add, expected_remove): | |
1695 | ||
1696 | spec = ServiceSpec(service_type=service_type, | |
1697 | service_id='test', | |
1698 | placement=placement) | |
1699 | ||
1700 | host_specs = [HostSpec(h) for h in hosts] | |
1701 | draining_host_specs = [HostSpec(h) for h in draining_hosts] | |
1702 | for h in host_specs: | |
1703 | if h.hostname in offline_hosts: | |
1704 | h.status = 'offline' | |
1705 | if h.hostname in maintenance_hosts: | |
1706 | h.status = 'maintenance' | |
1707 | ||
1708 | hosts, to_add, to_remove = HostAssignment( | |
1709 | spec=spec, | |
1710 | hosts=host_specs, | |
1711 | unreachable_hosts=[h for h in host_specs if h.status], | |
1712 | draining_hosts=draining_host_specs, | |
1713 | daemons=daemons, | |
1714 | ).place() | |
1715 | assert sorted([h.hostname for h in to_add]) in expected_add | |
1716 | assert sorted([h.name() for h in to_remove]) in expected_remove | |
1717 | ||
1718 | ||
1719 | class RegexHostPatternTest(NamedTuple): | |
1720 | service_type: str | |
1721 | placement: PlacementSpec | |
1722 | hosts: List[str] | |
1723 | expected_add: List[List[str]] | |
1724 | ||
1725 | ||
1726 | @pytest.mark.parametrize("service_type,placement,hosts,expected_add", | |
1727 | [ | |
1728 | RegexHostPatternTest( | |
1729 | 'crash', | |
1730 | PlacementSpec(host_pattern=HostPattern(pattern='host1|host3', pattern_type=PatternType.regex)), | |
1731 | 'host1 host2 host3 host4'.split(), | |
1732 | ['host1', 'host3'], | |
1733 | ), | |
1734 | RegexHostPatternTest( | |
1735 | 'crash', | |
1736 | PlacementSpec(host_pattern=HostPattern(pattern='host[2-4]', pattern_type=PatternType.regex)), | |
1737 | 'host1 host2 host3 host4'.split(), | |
1738 | ['host2', 'host3', 'host4'], | |
1739 | ), | |
1740 | ]) | |
1741 | def test_placement_regex_host_pattern(service_type, placement, hosts, expected_add): | |
1742 | spec = ServiceSpec(service_type=service_type, | |
1743 | service_id='test', | |
1744 | placement=placement) | |
1745 | ||
1746 | host_specs = [HostSpec(h) for h in hosts] | |
1747 | ||
1748 | hosts, to_add, to_remove = HostAssignment( | |
1749 | spec=spec, | |
1750 | hosts=host_specs, | |
1751 | unreachable_hosts=[], | |
1752 | draining_hosts=[], | |
1753 | daemons=[], | |
1754 | ).place() | |
1755 | assert sorted([h.hostname for h in to_add]) == expected_add |