1 # Disable autopep8 for this file:
5 from typing
import NamedTuple
, List
, Dict
, Optional
8 from ceph
.deployment
.hostspec
import HostSpec
9 from ceph
.deployment
.service_spec
import (
16 from ceph
.deployment
.hostspec
import SpecValidationError
18 from cephadm
.module
import HostAssignment
19 from cephadm
.schedule
import DaemonPlacement
20 from orchestrator
import DaemonDescription
, OrchestratorValidationError
, OrchestratorError
24 # some odd thingy to revert the order or arguments
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
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'
52 matches
+= int(h
in expected
)
54 assert False, f
'two of {hosts} not in {expected}'
58 def exactly(expected
, *hosts
):
59 assert expected
== list(hosts
)
63 def error(expected
, kind
, match
):
64 assert isinstance(expected
, kind
), (str(expected
), match
)
65 assert str(expected
) == match
, (str(expected
), match
)
69 def _or(expected
, *inners
):
73 except AssertionError as e
:
75 result
= [catch(i
) for i
in inners
]
76 if None not in result
:
77 assert False, f
"_or failed: {expected}"
85 return [e
for e
in s
.split(' ') if e
]
88 def get_result(key
, results
):
90 for o
, k
in zip(one
, key
):
91 if o
!= k
and o
!= '*':
94 return [v
for k
, v
in results
if match(k
)][0]
97 def mk_spec_and_host(spec_section
, hosts
, explicit_key
, explicit
, count
):
99 if spec_section
== 'hosts':
100 mk_spec
= lambda: ServiceSpec('mgr', placement
=PlacementSpec( # noqa: E731
104 elif spec_section
== 'label':
105 mk_spec
= lambda: ServiceSpec('mgr', placement
=PlacementSpec( # noqa: E731
109 elif spec_section
== 'host_pattern':
116 mk_spec
= lambda: ServiceSpec('mgr', placement
=PlacementSpec( # noqa: E731
117 host_pattern
=pattern
,
124 HostSpec(h
, labels
=['mylabel']) if h
in explicit
else HostSpec(h
)
128 return mk_spec
, hosts
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
)
134 assert_res
= get_result(k(key
), results
)
138 host_res
, to_add
, to_remove
= HostAssignment(
141 unreachable_hosts
=[],
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'
152 for _
in range(10): # scheduler has a random component
155 host_res
, to_add
, to_remove
= HostAssignment(
158 unreachable_hosts
=[],
163 assert_res(sorted([h
.hostname
for h
in host_res
]))
164 except Exception as e
:
168 @pytest.mark
.parametrize("dp,n,result",
171 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[80]),
173 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[80]),
176 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[80]),
178 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[82]),
181 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[80, 90]),
183 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[82, 92]),
186 def test_daemon_placement_renumber(dp
, n
, result
):
187 assert dp
.renumber_ports(n
) == result
190 @pytest.mark
.parametrize(
194 DaemonPlacement(daemon_type
='mgr', hostname
='host1'),
195 DaemonDescription('mgr', 'a', 'host1'),
199 DaemonPlacement(daemon_type
='mgr', hostname
='host1', name
='a'),
200 DaemonDescription('mgr', 'a', 'host1'),
204 DaemonPlacement(daemon_type
='mon', hostname
='host1', name
='a'),
205 DaemonDescription('mgr', 'a', 'host1'),
209 DaemonPlacement(daemon_type
='mgr', hostname
='host1', name
='a'),
210 DaemonDescription('mgr', 'b', 'host1'),
214 def test_daemon_placement_match(dp
, dd
, result
):
215 assert dp
.matches_daemon(dd
) == result
218 # * first match from the top wins
219 # * where e=[], *=any
221 # + list of known hosts available for scheduling (host_key)
222 # | + hosts used for explict placement (explicit_key)
224 # | | | + section (host, label, pattern)
225 # | | | | + expected result
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')),
251 @pytest.mark
.parametrize("spec_section_key,spec_section",
255 ('p', 'host_pattern'),
257 @pytest.mark
.parametrize("count",
265 @pytest.mark
.parametrize("explicit_key, explicit",
270 ('123', ['1', '2', '3']),
272 @pytest.mark
.parametrize("host_key, hosts",
276 ('123', ['1', '2', '3']),
278 def test_explicit_scheduler(host_key
, hosts
,
279 explicit_key
, explicit
,
281 spec_section_key
, spec_section
):
283 mk_spec
, hosts
= mk_spec_and_host(spec_section
, hosts
, explicit_key
, explicit
, count
)
285 results
=test_explicit_scheduler_results
,
289 key_elems
=(host_key
, explicit_key
, count
, spec_section_key
)
293 # * first match from the top wins
294 # * where e=[], *=any
296 # + list of known hosts available for scheduling (host_key)
297 # | + hosts used for explicit placement (explicit_key)
299 # | | | + existing daemons
300 # | | | | + section (host, label, pattern)
301 # | | | | | + expected result
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')),
331 @pytest.mark
.parametrize("spec_section_key,spec_section",
335 ('p', 'host_pattern'),
337 @pytest.mark
.parametrize("daemons_key, daemons",
343 ('112', ['1', '1', '2']), # deal with existing co-located daemons
345 ('123', ['1', '2', '3']),
347 @pytest.mark
.parametrize("count",
354 @pytest.mark
.parametrize("explicit_key, explicit",
357 ('123', ['1', '2', '3']),
359 @pytest.mark
.parametrize("host_key, hosts",
363 ('123', ['1', '2', '3']),
365 def test_scheduler_daemons(host_key
, hosts
,
366 explicit_key
, explicit
,
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
)
372 DaemonDescription('mgr', d
, d
)
376 results
=test_scheduler_daemons_results
,
380 key_elems
=(host_key
, explicit_key
, count
, daemons_key
, spec_section_key
)
384 # =========================
387 class NodeAssignmentTest(NamedTuple
):
389 placement
: PlacementSpec
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]]]]
395 expected_add
: List
[str]
396 expected_remove
: List
[DaemonDescription
]
399 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,rank_map,post_rank_map,expected,expected_add,expected_remove",
404 PlacementSpec(hosts
=['smithi060']),
408 ['mgr:smithi060'], ['mgr:smithi060'], []
413 PlacementSpec(host_pattern
='*'),
414 'host1 host2 host3'.split(),
416 DaemonDescription('mgr', 'a', 'host1'),
417 DaemonDescription('mgr', 'b', 'host2'),
420 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
424 # all_hosts + count_per_host
427 PlacementSpec(host_pattern
='*', count_per_host
=2),
428 'host1 host2 host3'.split(),
430 DaemonDescription('mds', 'a', 'host1'),
431 DaemonDescription('mds', 'b', 'host2'),
434 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
435 ['mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
438 # count that is bigger than the amount of hosts. Truncate to len(hosts)
439 # mgr should not be co-located to each other.
442 PlacementSpec(count
=4),
443 'host1 host2 host3'.split(),
446 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
447 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
450 # count that is bigger than the amount of hosts; wrap around.
453 PlacementSpec(count
=6),
454 'host1 host2 host3'.split(),
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'],
461 # count + partial host list
464 PlacementSpec(count
=3, hosts
=['host3']),
465 'host1 host2 host3'.split(),
467 DaemonDescription('mgr', 'a', 'host1'),
468 DaemonDescription('mgr', 'b', 'host2'),
475 # count + partial host list (with colo)
478 PlacementSpec(count
=3, hosts
=['host3']),
479 'host1 host2 host3'.split(),
481 DaemonDescription('mds', 'a', 'host1'),
482 DaemonDescription('mds', 'b', 'host2'),
485 ['mds:host3', 'mds:host3', 'mds:host3'],
486 ['mds:host3', 'mds:host3', 'mds:host3'],
489 # count 1 + partial host list
492 PlacementSpec(count
=1, hosts
=['host3']),
493 'host1 host2 host3'.split(),
495 DaemonDescription('mgr', 'a', 'host1'),
496 DaemonDescription('mgr', 'b', 'host2'),
503 # count + partial host list + existing
506 PlacementSpec(count
=2, hosts
=['host3']),
507 'host1 host2 host3'.split(),
509 DaemonDescription('mgr', 'a', 'host1'),
516 # count + partial host list + existing (deterministic)
519 PlacementSpec(count
=2, hosts
=['host1']),
520 'host1 host2'.split(),
522 DaemonDescription('mgr', 'a', 'host1'),
529 # count + partial host list + existing (deterministic)
532 PlacementSpec(count
=2, hosts
=['host1']),
533 'host1 host2'.split(),
535 DaemonDescription('mgr', 'a', 'host2'),
545 PlacementSpec(label
='foo'),
546 'host1 host2 host3'.split(),
549 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
550 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
553 # label + count (truncate to host list)
556 PlacementSpec(count
=4, label
='foo'),
557 'host1 host2 host3'.split(),
560 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
561 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
564 # label + count (with colo)
567 PlacementSpec(count
=6, label
='foo'),
568 'host1 host2 host3'.split(),
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'],
575 # label only + count_per_hst
578 PlacementSpec(label
='foo', count_per_host
=3),
579 'host1 host2 host3'.split(),
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'],
591 PlacementSpec(host_pattern
='mgr*'),
592 'mgrhost1 mgrhost2 datahost'.split(),
595 ['mgr:mgrhost1', 'mgr:mgrhost2'],
596 ['mgr:mgrhost1', 'mgr:mgrhost2'],
599 # host_pattern + count_per_host
602 PlacementSpec(host_pattern
='mds*', count_per_host
=3),
603 'mdshost1 mdshost2 datahost'.split(),
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'],
610 # label + count_per_host + ports
613 PlacementSpec(count
=6, label
='foo'),
614 'host1 host2 host3'.split(),
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)'],
623 # label + count_per_host + ports (+ existing)
626 PlacementSpec(count
=6, label
='foo'),
627 'host1 host2 host3'.split(),
629 DaemonDescription('rgw', 'a', 'host1', ports
=[81]),
630 DaemonDescription('rgw', 'b', 'host2', ports
=[80]),
631 DaemonDescription('rgw', 'c', 'host1', ports
=[82]),
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)'],
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
645 PlacementSpec(label
='foo', host_pattern
='mgr*'),
646 'mgr1 mgr2 osd1'.split(),
649 ['mgr:mgr1', 'mgr:mgr2'], ['mgr:mgr1', 'mgr:mgr2'], []
651 # cephadm.py teuth case
654 PlacementSpec(count
=3, hosts
=['host1=y', 'host2=x']),
655 'host1 host2'.split(),
657 DaemonDescription('mgr', 'y', 'host1'),
658 DaemonDescription('mgr', 'x', 'host2'),
661 ['mgr:host1(name=y)', 'mgr:host2(name=x)'],
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
672 PlacementSpec(count
=3),
673 'host1 host2 host3'.split(),
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)'],
684 PlacementSpec(count
=3),
685 'host1 host2 host3'.split(),
687 DaemonDescription('nfs', '0.1', 'host1', rank
=0, rank_generation
=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)'],
695 # ranked, exist, different ranks
698 PlacementSpec(count
=3),
699 'host1 host2 host3'.split(),
701 DaemonDescription('nfs', '0.1', 'host1', rank
=0, rank_generation
=1),
702 DaemonDescription('nfs', '1.1', 'host2', rank
=1, rank_generation
=1),
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)'],
710 # ranked, exist, different ranks (2)
713 PlacementSpec(count
=3),
714 'host1 host2 host3'.split(),
716 DaemonDescription('nfs', '0.1', 'host1', rank
=0, rank_generation
=1),
717 DaemonDescription('nfs', '1.1', 'host3', rank
=1, rank_generation
=1),
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)'],
725 # ranked, exist, extra ranks
728 PlacementSpec(count
=3),
729 'host1 host2 host3'.split(),
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),
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)'],
741 # 25: ranked, exist, extra ranks (scale down: kill off high rank)
744 PlacementSpec(count
=2),
745 'host3 host2 host1'.split(),
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),
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)'],
757 # ranked, exist, extra ranks (scale down hosts)
760 PlacementSpec(count
=2),
761 'host1 host3'.split(),
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),
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']
773 # ranked, exist, duplicate rank
776 PlacementSpec(count
=3),
777 'host1 host2 host3'.split(),
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),
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)'],
789 # 28: ranked, all gens stale (failure during update cycle)
792 PlacementSpec(count
=2),
793 'host1 host2 host3'.split(),
795 DaemonDescription('nfs', '0.2', 'host1', rank
=0, rank_generation
=2),
796 DaemonDescription('nfs', '1.2', 'host2', rank
=1, rank_generation
=2),
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)'],
804 # ranked, not enough hosts
807 PlacementSpec(count
=4),
808 'host1 host2 host3'.split(),
810 DaemonDescription('nfs', '0.2', 'host1', rank
=0, rank_generation
=2),
811 DaemonDescription('nfs', '1.2', 'host2', rank
=1, rank_generation
=2),
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)'],
822 PlacementSpec(hosts
=['host2']),
823 'host1 host2'.split(),
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),
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']
837 def test_node_assignment(service_type
, placement
, hosts
, daemons
, rank_map
, post_rank_map
,
838 expected
, expected_add
, expected_remove
):
842 if service_type
== 'rgw':
843 service_id
= 'realm.zone'
845 elif service_type
== 'mds':
848 elif service_type
== 'nfs':
850 spec
= ServiceSpec(service_type
=service_type
,
851 service_id
=service_id
,
855 spec
= ServiceSpec(service_type
=service_type
,
856 service_id
=service_id
,
859 all_slots
, to_add
, to_remove
= HostAssignment(
861 hosts
=[HostSpec(h
, labels
=['foo']) for h
in hosts
],
862 unreachable_hosts
=[],
865 allow_colo
=allow_colo
,
869 assert rank_map
== post_rank_map
871 got
= [str(p
) for p
in all_slots
]
879 assert num_wildcard
== len(got
)
881 got
= [str(p
) for p
in to_add
]
883 for i
in expected_add
:
889 assert num_wildcard
== len(got
)
891 assert sorted([d
.name() for d
in to_remove
]) == sorted(expected_remove
)
894 class NodeAssignmentTest5(NamedTuple
):
896 placement
: PlacementSpec
897 available_hosts
: List
[str]
898 candidates_hosts
: List
[str]
901 @pytest.mark
.parametrize("service_type, placement, available_hosts, expected_candidates",
905 PlacementSpec(hosts
='host1 host2 host3 host4'.split()),
906 'host1 host2 host3 host4'.split(),
907 'host3 host1 host4 host2'.split(),
911 PlacementSpec(hosts
='host1 host2 host3 host4'.split()),
912 'host1 host2 host3 host4'.split(),
913 'host3 host2 host4 host1'.split(),
917 PlacementSpec(hosts
='host1 host2 host3 host4'.split()),
918 'host1 host2 host3 host4'.split(),
919 'host1 host2 host4 host3'.split(),
923 PlacementSpec(hosts
='host1 host2 host3 host4'.split()),
924 'host1 host2 host3 host4'.split(),
925 'host4 host2 host1 host3'.split(),
929 PlacementSpec(hosts
='host1 host2 host3 host4'.split()),
930 'host1 host2 host3 host4'.split(),
931 'host1 host3 host4 host2'.split(),
935 PlacementSpec(hosts
='host1 host2 host3 host4'.split()),
936 'host1 host2 host3 host4'.split(),
937 'host1 host3 host2 host4'.split(),
941 PlacementSpec(hosts
='host1 host2 host3 host4'.split()),
942 'host1 host2 host3 host4'.split(),
943 'host4 host3 host1 host2'.split(),
946 def test_node_assignment_random_shuffle(service_type
, placement
, available_hosts
, expected_candidates
):
950 spec
= ServiceSpec(service_type
=service_type
,
951 service_id
=service_id
,
954 candidates
= HostAssignment(
956 hosts
=[HostSpec(h
, labels
=['foo']) for h
in available_hosts
],
957 unreachable_hosts
=[],
960 allow_colo
=allow_colo
,
963 candidates_hosts
= [h
.hostname
for h
in candidates
]
964 assert candidates_hosts
== expected_candidates
967 class NodeAssignmentTest2(NamedTuple
):
969 placement
: PlacementSpec
971 daemons
: List
[DaemonDescription
]
976 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected_len,in_set",
981 PlacementSpec(count
=1),
982 'host1 host2 host3'.split(),
985 ['host1', 'host2', 'host3'],
988 # hosts + (smaller) count
991 PlacementSpec(count
=1, hosts
='host1 host2'.split()),
992 'host1 host2'.split(),
997 # hosts + (smaller) count, existing
1000 PlacementSpec(count
=1, hosts
='host1 host2 host3'.split()),
1001 'host1 host2 host3'.split(),
1002 [DaemonDescription('mgr', 'mgr.a', 'host1')],
1004 ['host1', 'host2', 'host3'],
1006 # hosts + (smaller) count, (more) existing
1007 NodeAssignmentTest2(
1009 PlacementSpec(count
=1, hosts
='host1 host2 host3'.split()),
1010 'host1 host2 host3'.split(),
1012 DaemonDescription('mgr', 'a', 'host1'),
1013 DaemonDescription('mgr', 'b', 'host2'),
1018 # count + partial host list
1019 NodeAssignmentTest2(
1021 PlacementSpec(count
=2, hosts
=['host3']),
1022 'host1 host2 host3'.split(),
1025 ['host1', 'host2', 'host3']
1028 NodeAssignmentTest2(
1030 PlacementSpec(count
=1, label
='foo'),
1031 'host1 host2 host3'.split(),
1034 ['host1', 'host2', 'host3']
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
=[],
1046 assert len(hosts
) == expected_len
1047 for h
in [h
.hostname
for h
in hosts
]:
1051 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected_len,must_have",
1053 # hosts + (smaller) count, (more) existing
1054 NodeAssignmentTest2(
1056 PlacementSpec(count
=3, hosts
='host3'.split()),
1057 'host1 host2 host3'.split(),
1062 # count + partial host list
1063 NodeAssignmentTest2(
1065 PlacementSpec(count
=2, hosts
=['host3']),
1066 'host1 host2 host3'.split(),
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
=[],
1081 assert len(hosts
) == expected_len
1083 assert h
in [h
.hostname
for h
in hosts
]
1086 class NodeAssignmentTest4(NamedTuple
):
1088 networks
: Dict
[str, Dict
[str, Dict
[str, List
[str]]]]
1089 daemons
: List
[DaemonDescription
]
1091 expected_add
: List
[str]
1092 expected_remove
: List
[DaemonDescription
]
1095 @pytest.mark
.parametrize("spec,networks,daemons,expected,expected_add,expected_remove",
1097 NodeAssignmentTest4(
1101 placement
=PlacementSpec(count
=6, label
='foo'),
1102 networks
=['10.0.0.0/8'],
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']}},
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)'],
1118 NodeAssignmentTest4(
1120 service_type
='ingress',
1121 service_id
='rgw.foo',
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'],
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']}},
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'],
1141 NodeAssignmentTest4(
1143 service_type
='ingress',
1144 service_id
='rgw.foo',
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'],
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']}},
1158 DaemonDescription('haproxy', 'a', 'host1', ip
='10.0.0.1',
1160 DaemonDescription('keepalived', 'b', 'host2'),
1161 DaemonDescription('keepalived', 'c', 'host3'),
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'],
1170 def test_node_assignment4(spec
, networks
, daemons
,
1171 expected
, expected_add
, expected_remove
):
1172 all_slots
, to_add
, to_remove
= HostAssignment(
1174 hosts
=[HostSpec(h
, labels
=['foo']) for h
in networks
.keys()],
1175 unreachable_hosts
=[],
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,
1184 got
= [str(p
) for p
in all_slots
]
1192 assert num_wildcard
== len(got
)
1194 got
= [str(p
) for p
in to_add
]
1196 for i
in expected_add
:
1202 assert num_wildcard
== len(got
)
1204 assert sorted([d
.name() for d
in to_remove
]) == sorted(expected_remove
)
1207 @pytest.mark
.parametrize("placement",
1212 ('hostname12hostname12hostname12hostname12hostname12hostname12hostname12'), # > 63 chars
1214 def test_bad_placements(placement
):
1216 PlacementSpec
.from_string(placement
.split(' '))
1218 except SpecValidationError
:
1222 class NodeAssignmentTestBadSpec(NamedTuple
):
1224 placement
: PlacementSpec
1226 daemons
: List
[DaemonDescription
]
1230 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected",
1233 NodeAssignmentTestBadSpec(
1235 PlacementSpec(hosts
=['unknownhost']),
1238 "Cannot place <ServiceSpec for service_name=mgr> on unknownhost: Unknown hosts"
1240 # unknown host pattern
1241 NodeAssignmentTestBadSpec(
1243 PlacementSpec(host_pattern
='unknownhost'),
1246 "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts"
1249 NodeAssignmentTestBadSpec(
1251 PlacementSpec(label
='unknownlabel'),
1254 "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts for label unknownlabel"
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
=[],
1266 assert str(e
.value
) == expected
1269 class ActiveAssignmentTest(NamedTuple
):
1271 placement
: PlacementSpec
1273 daemons
: List
[DaemonDescription
]
1274 expected
: List
[List
[str]]
1275 expected_add
: List
[List
[str]]
1276 expected_remove
: List
[List
[str]]
1279 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected,expected_add,expected_remove",
1281 ActiveAssignmentTest(
1283 PlacementSpec(count
=2),
1284 'host1 host2 host3'.split(),
1286 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1287 DaemonDescription('mgr', 'b', 'host2'),
1288 DaemonDescription('mgr', 'c', 'host3'),
1290 [['host1', 'host2'], ['host1', 'host3']],
1292 [['mgr.b'], ['mgr.c']]
1294 ActiveAssignmentTest(
1296 PlacementSpec(count
=2),
1297 'host1 host2 host3'.split(),
1299 DaemonDescription('mgr', 'a', 'host1'),
1300 DaemonDescription('mgr', 'b', 'host2'),
1301 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1303 [['host1', 'host3'], ['host2', 'host3']],
1305 [['mgr.a'], ['mgr.b']]
1307 ActiveAssignmentTest(
1309 PlacementSpec(count
=1),
1310 'host1 host2 host3'.split(),
1312 DaemonDescription('mgr', 'a', 'host1'),
1313 DaemonDescription('mgr', 'b', 'host2', is_active
=True),
1314 DaemonDescription('mgr', 'c', 'host3'),
1318 [['mgr.a', 'mgr.c']]
1320 ActiveAssignmentTest(
1322 PlacementSpec(count
=1),
1323 'host1 host2 host3'.split(),
1325 DaemonDescription('mgr', 'a', 'host1'),
1326 DaemonDescription('mgr', 'b', 'host2'),
1327 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1331 [['mgr.a', 'mgr.b']]
1333 ActiveAssignmentTest(
1335 PlacementSpec(count
=1),
1336 'host1 host2 host3'.split(),
1338 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1339 DaemonDescription('mgr', 'b', 'host2'),
1340 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1342 [['host1'], ['host3']],
1344 [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c']]
1346 ActiveAssignmentTest(
1348 PlacementSpec(count
=2),
1349 'host1 host2 host3'.split(),
1351 DaemonDescription('mgr', 'a', 'host1'),
1352 DaemonDescription('mgr', 'b', 'host2', is_active
=True),
1353 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1355 [['host2', 'host3']],
1359 ActiveAssignmentTest(
1361 PlacementSpec(count
=1),
1362 'host1 host2 host3'.split(),
1364 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1365 DaemonDescription('mgr', 'b', 'host2', is_active
=True),
1366 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1368 [['host1'], ['host2'], ['host3']],
1370 [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c'], ['mgr.a', 'mgr.c']]
1372 ActiveAssignmentTest(
1374 PlacementSpec(count
=1),
1375 'host1 host2 host3'.split(),
1377 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1378 DaemonDescription('mgr', 'a2', 'host1'),
1379 DaemonDescription('mgr', 'b', 'host2'),
1380 DaemonDescription('mgr', 'c', 'host3'),
1384 [['mgr.a2', 'mgr.b', 'mgr.c']]
1386 ActiveAssignmentTest(
1388 PlacementSpec(count
=1),
1389 'host1 host2 host3'.split(),
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'),
1398 [['mgr.a', 'mgr.b', 'mgr.c'], ['mgr.a2', 'mgr.b', 'mgr.c']]
1400 ActiveAssignmentTest(
1402 PlacementSpec(count
=2),
1403 'host1 host2 host3'.split(),
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),
1410 [['host1', 'host3']],
1412 [['mgr.a2', 'mgr.b']]
1414 # Explicit placement should override preference for active daemon
1415 ActiveAssignmentTest(
1417 PlacementSpec(count
=1, hosts
=['host1']),
1418 'host1 host2 host3'.split(),
1420 DaemonDescription('mgr', 'a', 'host1'),
1421 DaemonDescription('mgr', 'b', 'host2'),
1422 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1426 [['mgr.b', 'mgr.c']]
1430 def test_active_assignment(service_type
, placement
, hosts
, daemons
, expected
, expected_add
, expected_remove
):
1432 spec
= ServiceSpec(service_type
=service_type
,
1434 placement
=placement
)
1436 hosts
, to_add
, to_remove
= HostAssignment(
1438 hosts
=[HostSpec(h
) for h
in hosts
],
1439 unreachable_hosts
=[],
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
1448 class UnreachableHostsTest(NamedTuple
):
1450 placement
: PlacementSpec
1452 unreachables_hosts
: List
[str]
1453 daemons
: List
[DaemonDescription
]
1454 expected_add
: List
[List
[str]]
1455 expected_remove
: List
[List
[str]]
1458 @pytest.mark
.parametrize("service_type,placement,hosts,unreachable_hosts,daemons,expected_add,expected_remove",
1460 UnreachableHostsTest(
1462 PlacementSpec(count
=3),
1463 'host1 host2 host3'.split(),
1466 [['host1', 'host3']],
1469 UnreachableHostsTest(
1471 PlacementSpec(hosts
=['host3']),
1472 'host1 host2 host3'.split(),
1475 DaemonDescription('mgr', 'a', 'host1'),
1476 DaemonDescription('mgr', 'b', 'host2'),
1477 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1482 UnreachableHostsTest(
1484 PlacementSpec(count
=3),
1485 'host1 host2 host3 host4'.split(),
1488 DaemonDescription('mgr', 'a', 'host1'),
1489 DaemonDescription('mgr', 'b', 'host2'),
1490 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1495 UnreachableHostsTest(
1497 PlacementSpec(count
=1),
1498 'host1 host2 host3 host4'.split(),
1499 'host1 host3'.split(),
1501 DaemonDescription('mgr', 'a', 'host1'),
1502 DaemonDescription('mgr', 'b', 'host2'),
1503 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1508 UnreachableHostsTest(
1510 PlacementSpec(count
=3),
1511 'host1 host2 host3 host4'.split(),
1514 [['host1', 'host3', 'host4']],
1517 UnreachableHostsTest(
1519 PlacementSpec(count
=3),
1520 'host1 host2 host3 host4'.split(),
1521 'host1 host4'.split(),
1523 [['host2', 'host3']],
1528 def test_unreachable_host(service_type
, placement
, hosts
, unreachable_hosts
, daemons
, expected_add
, expected_remove
):
1530 spec
= ServiceSpec(service_type
=service_type
,
1532 placement
=placement
)
1534 hosts
, to_add
, to_remove
= HostAssignment(
1536 hosts
=[HostSpec(h
) for h
in hosts
],
1537 unreachable_hosts
=[HostSpec(h
) for h
in unreachable_hosts
],
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
1545 class RescheduleFromOfflineTest(NamedTuple
):
1547 placement
: PlacementSpec
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]]
1556 @pytest.mark
.parametrize("service_type,placement,hosts,maintenance_hosts,offline_hosts,daemons,expected_add,expected_remove",
1558 RescheduleFromOfflineTest(
1560 PlacementSpec(count
=2),
1561 'host1 host2 host3'.split(),
1565 DaemonDescription('nfs', 'a', 'host1'),
1566 DaemonDescription('nfs', 'b', 'host2'),
1571 RescheduleFromOfflineTest(
1573 PlacementSpec(count
=2),
1574 'host1 host2 host3'.split(),
1578 DaemonDescription('nfs', 'a', 'host1'),
1579 DaemonDescription('nfs', 'b', 'host2'),
1584 RescheduleFromOfflineTest(
1586 PlacementSpec(count
=2),
1587 'host1 host2 host3'.split(),
1591 DaemonDescription('mon', 'a', 'host1'),
1592 DaemonDescription('mon', 'b', 'host2'),
1597 RescheduleFromOfflineTest(
1599 PlacementSpec(count
=1),
1600 'host1 host2'.split(),
1604 DaemonDescription('haproxy', 'b', 'host2'),
1605 DaemonDescription('keepalived', 'b', 'host2'),
1611 def test_remove_from_offline(service_type
, placement
, hosts
, maintenance_hosts
, offline_hosts
, daemons
, expected_add
, expected_remove
):
1613 if service_type
== 'ingress':
1616 service_type
='ingress',
1617 service_id
='nfs-ha.foo',
1620 virtual_ip
='10.0.0.20/8',
1621 backend_service
='nfs-ha.foo',
1622 placement
=placement
,
1627 service_type
=service_type
,
1629 placement
=placement
,
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'
1639 hosts
, to_add
, to_remove
= HostAssignment(
1642 unreachable_hosts
=[h
for h
in host_specs
if h
.status
],
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
1650 class DrainExplicitPlacementTest(NamedTuple
):
1652 placement
: PlacementSpec
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]]
1662 @pytest.mark
.parametrize("service_type,placement,hosts,maintenance_hosts,offline_hosts,draining_hosts,daemons,expected_add,expected_remove",
1664 DrainExplicitPlacementTest(
1666 PlacementSpec(hosts
='host1 host2 host3'.split()),
1667 'host1 host2 host3 host4'.split(),
1672 DaemonDescription('crash', 'host1', 'host1'),
1673 DaemonDescription('crash', 'host2', 'host2'),
1674 DaemonDescription('crash', 'host3', 'host3'),
1679 DrainExplicitPlacementTest(
1681 PlacementSpec(hosts
='host1 host2 host3 host4'.split()),
1682 'host1 host2 host3 host4'.split(),
1687 DaemonDescription('crash', 'host1', 'host1'),
1688 DaemonDescription('crash', 'host3', 'host3'),
1694 def test_drain_from_explict_placement(service_type
, placement
, hosts
, maintenance_hosts
, offline_hosts
, draining_hosts
, daemons
, expected_add
, expected_remove
):
1696 spec
= ServiceSpec(service_type
=service_type
,
1698 placement
=placement
)
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'
1708 hosts
, to_add
, to_remove
= HostAssignment(
1711 unreachable_hosts
=[h
for h
in host_specs
if h
.status
],
1712 draining_hosts
=draining_host_specs
,
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
1719 class RegexHostPatternTest(NamedTuple
):
1721 placement
: PlacementSpec
1723 expected_add
: List
[List
[str]]
1726 @pytest.mark
.parametrize("service_type,placement,hosts,expected_add",
1728 RegexHostPatternTest(
1730 PlacementSpec(host_pattern
=HostPattern(pattern
='host1|host3', pattern_type
=PatternType
.regex
)),
1731 'host1 host2 host3 host4'.split(),
1734 RegexHostPatternTest(
1736 PlacementSpec(host_pattern
=HostPattern(pattern
='host[2-4]', pattern_type
=PatternType
.regex
)),
1737 'host1 host2 host3 host4'.split(),
1738 ['host2', 'host3', 'host4'],
1741 def test_placement_regex_host_pattern(service_type
, placement
, hosts
, expected_add
):
1742 spec
= ServiceSpec(service_type
=service_type
,
1744 placement
=placement
)
1746 host_specs
= [HostSpec(h
) for h
in hosts
]
1748 hosts
, to_add
, to_remove
= HostAssignment(
1751 unreachable_hosts
=[],
1755 assert sorted([h
.hostname
for h
in to_add
]) == expected_add