]> git.proxmox.com Git - ceph.git/blame_incremental - ceph/src/pybind/mgr/cephadm/tests/test_scheduling.py
bump version to 19.2.0-pve1
[ceph.git] / ceph / src / pybind / mgr / cephadm / tests / test_scheduling.py
... / ...
CommitLineData
1# Disable autopep8 for this file:
2
3# fmt: off
4
5from typing import NamedTuple, List, Dict, Optional
6import pytest
7
8from ceph.deployment.hostspec import HostSpec
9from ceph.deployment.service_spec import (
10 ServiceSpec,
11 PlacementSpec,
12 IngressSpec,
13 PatternType,
14 HostPattern,
15)
16from ceph.deployment.hostspec import SpecValidationError
17
18from cephadm.module import HostAssignment
19from cephadm.schedule import DaemonPlacement
20from orchestrator import DaemonDescription, OrchestratorValidationError, OrchestratorError
21
22
23def 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
33def none(expected):
34 assert expected == []
35
36
37@wrapper
38def 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
46def 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
58def exactly(expected, *hosts):
59 assert expected == list(hosts)
60
61
62@wrapper
63def error(expected, kind, match):
64 assert isinstance(expected, kind), (str(expected), match)
65 assert str(expected) == match, (str(expected), match)
66
67
68@wrapper
69def _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
80def _always_true(_):
81 pass
82
83
84def k(s):
85 return [e for e in s.split(' ') if e]
86
87
88def 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
97def 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
131def 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 ])
186def 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 ])
214def 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# | | | | |
227test_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 ])
278def 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# | | | | | |
303test_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 ])
365def 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
387class 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 ])
837def 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
894class 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 ])
946def 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
967class 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 ])
1037def 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 ])
1072def 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
1086class 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 ])
1170def 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 ])
1214def test_bad_placements(placement):
1215 try:
1216 PlacementSpec.from_string(placement.split(' '))
1217 assert False
1218 except SpecValidationError:
1219 pass
1220
1221
1222class 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 ])
1257def 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
1269class 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 ])
1430def 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
1448class 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 ])
1528def 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
1545class 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 ])
1611def 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
1650class 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 ])
1694def 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
1719class 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 ])
1741def 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