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 ServiceSpec
, PlacementSpec
, IngressSpec
10 from ceph
.deployment
.hostspec
import SpecValidationError
12 from cephadm
.module
import HostAssignment
13 from cephadm
.schedule
import DaemonPlacement
14 from orchestrator
import DaemonDescription
, OrchestratorValidationError
, OrchestratorError
18 # some odd thingy to revert the order or arguments
32 def one_of(expected
, *hosts
):
33 if not isinstance(expected
, list):
34 assert False, str(expected
)
35 assert len(expected
) == 1, f
'one_of failed len({expected}) != 1'
36 assert expected
[0] in hosts
40 def two_of(expected
, *hosts
):
41 if not isinstance(expected
, list):
42 assert False, str(expected
)
43 assert len(expected
) == 2, f
'one_of failed len({expected}) != 2'
46 matches
+= int(h
in expected
)
48 assert False, f
'two of {hosts} not in {expected}'
52 def exactly(expected
, *hosts
):
53 assert expected
== list(hosts
)
57 def error(expected
, kind
, match
):
58 assert isinstance(expected
, kind
), (str(expected
), match
)
59 assert str(expected
) == match
, (str(expected
), match
)
63 def _or(expected
, *inners
):
67 except AssertionError as e
:
69 result
= [catch(i
) for i
in inners
]
70 if None not in result
:
71 assert False, f
"_or failed: {expected}"
79 return [e
for e
in s
.split(' ') if e
]
82 def get_result(key
, results
):
84 for o
, k
in zip(one
, key
):
85 if o
!= k
and o
!= '*':
88 return [v
for k
, v
in results
if match(k
)][0]
91 def mk_spec_and_host(spec_section
, hosts
, explicit_key
, explicit
, count
):
93 if spec_section
== 'hosts':
94 mk_spec
= lambda: ServiceSpec('mgr', placement
=PlacementSpec( # noqa: E731
98 elif spec_section
== 'label':
99 mk_spec
= lambda: ServiceSpec('mgr', placement
=PlacementSpec( # noqa: E731
103 elif spec_section
== 'host_pattern':
110 mk_spec
= lambda: ServiceSpec('mgr', placement
=PlacementSpec( # noqa: E731
111 host_pattern
=pattern
,
118 HostSpec(h
, labels
=['mylabel']) if h
in explicit
else HostSpec(h
)
122 return mk_spec
, hosts
125 def run_scheduler_test(results
, mk_spec
, hosts
, daemons
, key_elems
):
126 key
= ' '.join('N' if e
is None else str(e
) for e
in key_elems
)
128 assert_res
= get_result(k(key
), results
)
132 host_res
, to_add
, to_remove
= HostAssignment(
135 unreachable_hosts
=[],
138 if isinstance(host_res
, list):
139 e
= ', '.join(repr(h
.hostname
) for h
in host_res
)
140 assert False, f
'`(k("{key}"), exactly({e})),` not found'
141 assert False, f
'`(k("{key}"), ...),` not found'
142 except OrchestratorError
as e
:
143 assert False, f
'`(k("{key}"), error({type(e).__name__}, {repr(str(e))})),` not found'
145 for _
in range(10): # scheduler has a random component
148 host_res
, to_add
, to_remove
= HostAssignment(
151 unreachable_hosts
=[],
155 assert_res(sorted([h
.hostname
for h
in host_res
]))
156 except Exception as e
:
160 # * first match from the top wins
161 # * where e=[], *=any
163 # + list of known hosts available for scheduling (host_key)
164 # | + hosts used for explict placement (explicit_key)
166 # | | | + section (host, label, pattern)
167 # | | | | + expected result
169 test_explicit_scheduler_results
= [
170 (k("* * 0 *"), error(SpecValidationError
, 'num/count must be > 1')),
171 (k("* e N l"), error(OrchestratorValidationError
, 'Cannot place <ServiceSpec for service_name=mgr>: No matching hosts for label mylabel')),
172 (k("* e N p"), error(OrchestratorValidationError
, 'Cannot place <ServiceSpec for service_name=mgr>: No matching hosts')),
173 (k("* e N h"), error(OrchestratorValidationError
, 'placement spec is empty: no hosts, no label, no pattern, no count')),
174 (k("* e * *"), none
),
175 (k("1 12 * h"), error(OrchestratorValidationError
, "Cannot place <ServiceSpec for service_name=mgr> on 2: Unknown hosts")),
176 (k("1 123 * h"), error(OrchestratorValidationError
, "Cannot place <ServiceSpec for service_name=mgr> on 2, 3: Unknown hosts")),
177 (k("1 * * *"), exactly('1')),
178 (k("12 1 * *"), exactly('1')),
179 (k("12 12 1 *"), one_of('1', '2')),
180 (k("12 12 * *"), exactly('1', '2')),
181 (k("12 123 * h"), error(OrchestratorValidationError
, "Cannot place <ServiceSpec for service_name=mgr> on 3: Unknown hosts")),
182 (k("12 123 1 *"), one_of('1', '2', '3')),
183 (k("12 123 * *"), two_of('1', '2', '3')),
184 (k("123 1 * *"), exactly('1')),
185 (k("123 12 1 *"), one_of('1', '2')),
186 (k("123 12 * *"), exactly('1', '2')),
187 (k("123 123 1 *"), one_of('1', '2', '3')),
188 (k("123 123 2 *"), two_of('1', '2', '3')),
189 (k("123 123 * *"), exactly('1', '2', '3')),
193 @pytest.mark
.parametrize("dp,n,result",
196 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[80]),
198 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[80]),
201 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[80]),
203 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[82]),
206 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[80, 90]),
208 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[82, 92]),
211 def test_daemon_placement_renumber(dp
, n
, result
):
212 assert dp
.renumber_ports(n
) == result
215 @pytest.mark
.parametrize(
219 DaemonPlacement(daemon_type
='mgr', hostname
='host1'),
220 DaemonDescription('mgr', 'a', 'host1'),
224 DaemonPlacement(daemon_type
='mgr', hostname
='host1', name
='a'),
225 DaemonDescription('mgr', 'a', 'host1'),
229 DaemonPlacement(daemon_type
='mon', hostname
='host1', name
='a'),
230 DaemonDescription('mgr', 'a', 'host1'),
234 DaemonPlacement(daemon_type
='mgr', hostname
='host1', name
='a'),
235 DaemonDescription('mgr', 'b', 'host1'),
239 def test_daemon_placement_match(dp
, dd
, result
):
240 assert dp
.matches_daemon(dd
) == result
243 @pytest.mark
.parametrize("spec_section_key,spec_section",
247 ('p', 'host_pattern'),
249 @pytest.mark
.parametrize("count",
257 @pytest.mark
.parametrize("explicit_key, explicit",
262 ('123', ['1', '2', '3']),
264 @pytest.mark
.parametrize("host_key, hosts",
268 ('123', ['1', '2', '3']),
270 def test_explicit_scheduler(host_key
, hosts
,
271 explicit_key
, explicit
,
273 spec_section_key
, spec_section
):
275 mk_spec
, hosts
= mk_spec_and_host(spec_section
, hosts
, explicit_key
, explicit
, count
)
277 results
=test_explicit_scheduler_results
,
281 key_elems
=(host_key
, explicit_key
, count
, spec_section_key
)
285 # * first match from the top wins
286 # * where e=[], *=any
288 # + list of known hosts available for scheduling (host_key)
289 # | + hosts used for explict placement (explicit_key)
291 # | | | + existing daemons
292 # | | | | + section (host, label, pattern)
293 # | | | | | + expected result
295 test_scheduler_daemons_results
= [
296 (k("* 1 * * *"), exactly('1')),
297 (k("1 123 * * h"), error(OrchestratorValidationError
, 'Cannot place <ServiceSpec for service_name=mgr> on 2, 3: Unknown hosts')),
298 (k("1 123 * * *"), exactly('1')),
299 (k("12 123 * * h"), error(OrchestratorValidationError
, 'Cannot place <ServiceSpec for service_name=mgr> on 3: Unknown hosts')),
300 (k("12 123 N * *"), exactly('1', '2')),
301 (k("12 123 1 * *"), one_of('1', '2')),
302 (k("12 123 2 * *"), exactly('1', '2')),
303 (k("12 123 3 * *"), exactly('1', '2')),
304 (k("123 123 N * *"), exactly('1', '2', '3')),
305 (k("123 123 1 e *"), one_of('1', '2', '3')),
306 (k("123 123 1 1 *"), exactly('1')),
307 (k("123 123 1 3 *"), exactly('3')),
308 (k("123 123 1 12 *"), one_of('1', '2')),
309 (k("123 123 1 112 *"), one_of('1', '2')),
310 (k("123 123 1 23 *"), one_of('2', '3')),
311 (k("123 123 1 123 *"), one_of('1', '2', '3')),
312 (k("123 123 2 e *"), two_of('1', '2', '3')),
313 (k("123 123 2 1 *"), _or(exactly('1', '2'), exactly('1', '3'))),
314 (k("123 123 2 3 *"), _or(exactly('1', '3'), exactly('2', '3'))),
315 (k("123 123 2 12 *"), exactly('1', '2')),
316 (k("123 123 2 112 *"), exactly('1', '2')),
317 (k("123 123 2 23 *"), exactly('2', '3')),
318 (k("123 123 2 123 *"), two_of('1', '2', '3')),
319 (k("123 123 3 * *"), exactly('1', '2', '3')),
323 @pytest.mark
.parametrize("spec_section_key,spec_section",
327 ('p', 'host_pattern'),
329 @pytest.mark
.parametrize("daemons_key, daemons",
335 ('112', ['1', '1', '2']), # deal with existing co-located daemons
337 ('123', ['1', '2', '3']),
339 @pytest.mark
.parametrize("count",
346 @pytest.mark
.parametrize("explicit_key, explicit",
349 ('123', ['1', '2', '3']),
351 @pytest.mark
.parametrize("host_key, hosts",
355 ('123', ['1', '2', '3']),
357 def test_scheduler_daemons(host_key
, hosts
,
358 explicit_key
, explicit
,
360 daemons_key
, daemons
,
361 spec_section_key
, spec_section
):
362 mk_spec
, hosts
= mk_spec_and_host(spec_section
, hosts
, explicit_key
, explicit
, count
)
364 DaemonDescription('mgr', d
, d
)
368 results
=test_scheduler_daemons_results
,
372 key_elems
=(host_key
, explicit_key
, count
, daemons_key
, spec_section_key
)
376 # =========================
379 class NodeAssignmentTest(NamedTuple
):
381 placement
: PlacementSpec
383 daemons
: List
[DaemonDescription
]
384 rank_map
: Optional
[Dict
[int, Dict
[int, Optional
[str]]]]
385 post_rank_map
: Optional
[Dict
[int, Dict
[int, Optional
[str]]]]
387 expected_add
: List
[str]
388 expected_remove
: List
[DaemonDescription
]
391 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,rank_map,post_rank_map,expected,expected_add,expected_remove",
396 PlacementSpec(hosts
=['smithi060']),
400 ['mgr:smithi060'], ['mgr:smithi060'], []
405 PlacementSpec(host_pattern
='*'),
406 'host1 host2 host3'.split(),
408 DaemonDescription('mgr', 'a', 'host1'),
409 DaemonDescription('mgr', 'b', 'host2'),
412 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
416 # all_hosts + count_per_host
419 PlacementSpec(host_pattern
='*', count_per_host
=2),
420 'host1 host2 host3'.split(),
422 DaemonDescription('mds', 'a', 'host1'),
423 DaemonDescription('mds', 'b', 'host2'),
426 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
427 ['mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
430 # count that is bigger than the amount of hosts. Truncate to len(hosts)
431 # mgr should not be co-located to each other.
434 PlacementSpec(count
=4),
435 'host1 host2 host3'.split(),
438 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
439 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
442 # count that is bigger than the amount of hosts; wrap around.
445 PlacementSpec(count
=6),
446 'host1 host2 host3'.split(),
449 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
450 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
453 # count + partial host list
456 PlacementSpec(count
=3, hosts
=['host3']),
457 'host1 host2 host3'.split(),
459 DaemonDescription('mgr', 'a', 'host1'),
460 DaemonDescription('mgr', 'b', 'host2'),
467 # count + partial host list (with colo)
470 PlacementSpec(count
=3, hosts
=['host3']),
471 'host1 host2 host3'.split(),
473 DaemonDescription('mds', 'a', 'host1'),
474 DaemonDescription('mds', 'b', 'host2'),
477 ['mds:host3', 'mds:host3', 'mds:host3'],
478 ['mds:host3', 'mds:host3', 'mds:host3'],
481 # count 1 + partial host list
484 PlacementSpec(count
=1, hosts
=['host3']),
485 'host1 host2 host3'.split(),
487 DaemonDescription('mgr', 'a', 'host1'),
488 DaemonDescription('mgr', 'b', 'host2'),
495 # count + partial host list + existing
498 PlacementSpec(count
=2, hosts
=['host3']),
499 'host1 host2 host3'.split(),
501 DaemonDescription('mgr', 'a', 'host1'),
508 # count + partial host list + existing (deterministic)
511 PlacementSpec(count
=2, hosts
=['host1']),
512 'host1 host2'.split(),
514 DaemonDescription('mgr', 'a', 'host1'),
521 # count + partial host list + existing (deterministic)
524 PlacementSpec(count
=2, hosts
=['host1']),
525 'host1 host2'.split(),
527 DaemonDescription('mgr', 'a', 'host2'),
537 PlacementSpec(label
='foo'),
538 'host1 host2 host3'.split(),
541 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
542 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
545 # label + count (truncate to host list)
548 PlacementSpec(count
=4, label
='foo'),
549 'host1 host2 host3'.split(),
552 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
553 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
556 # label + count (with colo)
559 PlacementSpec(count
=6, label
='foo'),
560 'host1 host2 host3'.split(),
563 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
564 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
567 # label only + count_per_hst
570 PlacementSpec(label
='foo', count_per_host
=3),
571 'host1 host2 host3'.split(),
574 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3',
575 'mds:host1', 'mds:host2', 'mds:host3'],
576 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3',
577 'mds:host1', 'mds:host2', 'mds:host3'],
583 PlacementSpec(host_pattern
='mgr*'),
584 'mgrhost1 mgrhost2 datahost'.split(),
587 ['mgr:mgrhost1', 'mgr:mgrhost2'],
588 ['mgr:mgrhost1', 'mgr:mgrhost2'],
591 # host_pattern + count_per_host
594 PlacementSpec(host_pattern
='mds*', count_per_host
=3),
595 'mdshost1 mdshost2 datahost'.split(),
598 ['mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2'],
599 ['mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2'],
602 # label + count_per_host + ports
605 PlacementSpec(count
=6, label
='foo'),
606 'host1 host2 host3'.split(),
609 ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)',
610 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'],
611 ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)',
612 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'],
615 # label + count_per_host + ports (+ xisting)
618 PlacementSpec(count
=6, label
='foo'),
619 'host1 host2 host3'.split(),
621 DaemonDescription('rgw', 'a', 'host1', ports
=[81]),
622 DaemonDescription('rgw', 'b', 'host2', ports
=[80]),
623 DaemonDescription('rgw', 'c', 'host1', ports
=[82]),
626 ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)',
627 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'],
628 ['rgw:host1(*:80)', 'rgw:host3(*:80)',
629 'rgw:host2(*:81)', 'rgw:host3(*:81)'],
632 # cephadm.py teuth case
635 PlacementSpec(count
=3, hosts
=['host1=y', 'host2=x']),
636 'host1 host2'.split(),
638 DaemonDescription('mgr', 'y', 'host1'),
639 DaemonDescription('mgr', 'x', 'host2'),
642 ['mgr:host1(name=y)', 'mgr:host2(name=x)'],
646 # note: host -> rank mapping is psuedo-random based on svc name, so these
647 # host/rank pairs may seem random but they match the nfs.mynfs seed used by
653 PlacementSpec(count
=3),
654 'host1 host2 host3'.split(),
657 {0: {0: None}, 1: {0: None}, 2: {0: None}},
658 ['nfs:host1(rank=0.0)', 'nfs:host2(rank=1.0)', 'nfs:host3(rank=2.0)'],
659 ['nfs:host1(rank=0.0)', 'nfs:host2(rank=1.0)', 'nfs:host3(rank=2.0)'],
665 PlacementSpec(count
=3),
666 'host1 host2 host3'.split(),
668 DaemonDescription('nfs', '0.1', 'host1', rank
=0, rank_generation
=1),
671 {0: {1: '0.1'}, 1: {0: None}, 2: {0: None}},
672 ['nfs:host1(rank=0.1)', 'nfs:host2(rank=1.0)', 'nfs:host3(rank=2.0)'],
673 ['nfs:host2(rank=1.0)', 'nfs:host3(rank=2.0)'],
676 # ranked, exist, different ranks
679 PlacementSpec(count
=3),
680 'host1 host2 host3'.split(),
682 DaemonDescription('nfs', '0.1', 'host1', rank
=0, rank_generation
=1),
683 DaemonDescription('nfs', '1.1', 'host2', rank
=1, rank_generation
=1),
685 {0: {1: '0.1'}, 1: {1: '1.1'}},
686 {0: {1: '0.1'}, 1: {1: '1.1'}, 2: {0: None}},
687 ['nfs:host1(rank=0.1)', 'nfs:host2(rank=1.1)', 'nfs:host3(rank=2.0)'],
688 ['nfs:host3(rank=2.0)'],
691 # ranked, exist, different ranks (2)
694 PlacementSpec(count
=3),
695 'host1 host2 host3'.split(),
697 DaemonDescription('nfs', '0.1', 'host1', rank
=0, rank_generation
=1),
698 DaemonDescription('nfs', '1.1', 'host3', rank
=1, rank_generation
=1),
700 {0: {1: '0.1'}, 1: {1: '1.1'}},
701 {0: {1: '0.1'}, 1: {1: '1.1'}, 2: {0: None}},
702 ['nfs:host1(rank=0.1)', 'nfs:host3(rank=1.1)', 'nfs:host2(rank=2.0)'],
703 ['nfs:host2(rank=2.0)'],
706 # ranked, exist, extra ranks
709 PlacementSpec(count
=3),
710 'host1 host2 host3'.split(),
712 DaemonDescription('nfs', '0.5', 'host1', rank
=0, rank_generation
=5),
713 DaemonDescription('nfs', '1.5', 'host2', rank
=1, rank_generation
=5),
714 DaemonDescription('nfs', '4.5', 'host2', rank
=4, rank_generation
=5),
716 {0: {5: '0.5'}, 1: {5: '1.5'}},
717 {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {0: None}},
718 ['nfs:host1(rank=0.5)', 'nfs:host2(rank=1.5)', 'nfs:host3(rank=2.0)'],
719 ['nfs:host3(rank=2.0)'],
722 # 25: ranked, exist, extra ranks (scale down: kill off high rank)
725 PlacementSpec(count
=2),
726 'host3 host2 host1'.split(),
728 DaemonDescription('nfs', '0.5', 'host1', rank
=0, rank_generation
=5),
729 DaemonDescription('nfs', '1.5', 'host2', rank
=1, rank_generation
=5),
730 DaemonDescription('nfs', '2.5', 'host3', rank
=2, rank_generation
=5),
732 {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {5: '2.5'}},
733 {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {5: '2.5'}},
734 ['nfs:host1(rank=0.5)', 'nfs:host2(rank=1.5)'],
738 # ranked, exist, extra ranks (scale down hosts)
741 PlacementSpec(count
=2),
742 'host1 host3'.split(),
744 DaemonDescription('nfs', '0.5', 'host1', rank
=0, rank_generation
=5),
745 DaemonDescription('nfs', '1.5', 'host2', rank
=1, rank_generation
=5),
746 DaemonDescription('nfs', '2.5', 'host3', rank
=4, rank_generation
=5),
748 {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {5: '2.5'}},
749 {0: {5: '0.5'}, 1: {5: '1.5', 6: None}, 2: {5: '2.5'}},
750 ['nfs:host1(rank=0.5)', 'nfs:host3(rank=1.6)'],
751 ['nfs:host3(rank=1.6)'],
752 ['nfs.2.5', 'nfs.1.5']
754 # ranked, exist, duplicate rank
757 PlacementSpec(count
=3),
758 'host1 host2 host3'.split(),
760 DaemonDescription('nfs', '0.0', 'host1', rank
=0, rank_generation
=0),
761 DaemonDescription('nfs', '1.1', 'host2', rank
=1, rank_generation
=1),
762 DaemonDescription('nfs', '1.2', 'host3', rank
=1, rank_generation
=2),
764 {0: {0: '0.0'}, 1: {2: '1.2'}},
765 {0: {0: '0.0'}, 1: {2: '1.2'}, 2: {0: None}},
766 ['nfs:host1(rank=0.0)', 'nfs:host3(rank=1.2)', 'nfs:host2(rank=2.0)'],
767 ['nfs:host2(rank=2.0)'],
770 # 28: ranked, all gens stale (failure during update cycle)
773 PlacementSpec(count
=2),
774 'host1 host2 host3'.split(),
776 DaemonDescription('nfs', '0.2', 'host1', rank
=0, rank_generation
=2),
777 DaemonDescription('nfs', '1.2', 'host2', rank
=1, rank_generation
=2),
779 {0: {2: '0.2'}, 1: {2: '1.2', 3: '1.3'}},
780 {0: {2: '0.2'}, 1: {2: '1.2', 3: '1.3', 4: None}},
781 ['nfs:host1(rank=0.2)', 'nfs:host2(rank=1.4)'],
782 ['nfs:host2(rank=1.4)'],
785 # ranked, not enough hosts
788 PlacementSpec(count
=4),
789 'host1 host2 host3'.split(),
791 DaemonDescription('nfs', '0.2', 'host1', rank
=0, rank_generation
=2),
792 DaemonDescription('nfs', '1.2', 'host2', rank
=1, rank_generation
=2),
794 {0: {2: '0.2'}, 1: {2: '1.2'}},
795 {0: {2: '0.2'}, 1: {2: '1.2'}, 2: {0: None}},
796 ['nfs:host1(rank=0.2)', 'nfs:host2(rank=1.2)', 'nfs:host3(rank=2.0)'],
797 ['nfs:host3(rank=2.0)'],
803 PlacementSpec(hosts
=['host2']),
804 'host1 host2'.split(),
806 DaemonDescription('nfs', '0.2', 'host1', rank
=0, rank_generation
=2),
807 DaemonDescription('nfs', '1.2', 'host2', rank
=1, rank_generation
=2),
808 DaemonDescription('nfs', '2.2', 'host3', rank
=2, rank_generation
=2),
810 {0: {2: '0.2'}, 1: {2: '1.2'}, 2: {2: '2.2'}},
811 {0: {2: '0.2', 3: None}, 1: {2: '1.2'}, 2: {2: '2.2'}},
812 ['nfs:host2(rank=0.3)'],
813 ['nfs:host2(rank=0.3)'],
814 ['nfs.0.2', 'nfs.1.2', 'nfs.2.2']
818 def test_node_assignment(service_type
, placement
, hosts
, daemons
, rank_map
, post_rank_map
,
819 expected
, expected_add
, expected_remove
):
823 if service_type
== 'rgw':
824 service_id
= 'realm.zone'
826 elif service_type
== 'mds':
829 elif service_type
== 'nfs':
831 spec
= ServiceSpec(service_type
=service_type
,
832 service_id
=service_id
,
837 spec
= ServiceSpec(service_type
=service_type
,
838 service_id
=service_id
,
841 all_slots
, to_add
, to_remove
= HostAssignment(
843 hosts
=[HostSpec(h
, labels
=['foo']) for h
in hosts
],
844 unreachable_hosts
=[],
846 allow_colo
=allow_colo
,
850 assert rank_map
== post_rank_map
852 got
= [str(p
) for p
in all_slots
]
860 assert num_wildcard
== len(got
)
862 got
= [str(p
) for p
in to_add
]
864 for i
in expected_add
:
870 assert num_wildcard
== len(got
)
872 assert sorted([d
.name() for d
in to_remove
]) == sorted(expected_remove
)
875 class NodeAssignmentTest2(NamedTuple
):
877 placement
: PlacementSpec
879 daemons
: List
[DaemonDescription
]
884 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected_len,in_set",
889 PlacementSpec(count
=1),
890 'host1 host2 host3'.split(),
893 ['host1', 'host2', 'host3'],
896 # hosts + (smaller) count
899 PlacementSpec(count
=1, hosts
='host1 host2'.split()),
900 'host1 host2'.split(),
905 # hosts + (smaller) count, existing
908 PlacementSpec(count
=1, hosts
='host1 host2 host3'.split()),
909 'host1 host2 host3'.split(),
910 [DaemonDescription('mgr', 'mgr.a', 'host1')],
912 ['host1', 'host2', 'host3'],
914 # hosts + (smaller) count, (more) existing
917 PlacementSpec(count
=1, hosts
='host1 host2 host3'.split()),
918 'host1 host2 host3'.split(),
920 DaemonDescription('mgr', 'a', 'host1'),
921 DaemonDescription('mgr', 'b', 'host2'),
926 # count + partial host list
929 PlacementSpec(count
=2, hosts
=['host3']),
930 'host1 host2 host3'.split(),
933 ['host1', 'host2', 'host3']
938 PlacementSpec(count
=1, label
='foo'),
939 'host1 host2 host3'.split(),
942 ['host1', 'host2', 'host3']
945 def test_node_assignment2(service_type
, placement
, hosts
,
946 daemons
, expected_len
, in_set
):
947 hosts
, to_add
, to_remove
= HostAssignment(
948 spec
=ServiceSpec(service_type
, placement
=placement
),
949 hosts
=[HostSpec(h
, labels
=['foo']) for h
in hosts
],
950 unreachable_hosts
=[],
953 assert len(hosts
) == expected_len
954 for h
in [h
.hostname
for h
in hosts
]:
958 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected_len,must_have",
960 # hosts + (smaller) count, (more) existing
963 PlacementSpec(count
=3, hosts
='host3'.split()),
964 'host1 host2 host3'.split(),
969 # count + partial host list
972 PlacementSpec(count
=2, hosts
=['host3']),
973 'host1 host2 host3'.split(),
979 def test_node_assignment3(service_type
, placement
, hosts
,
980 daemons
, expected_len
, must_have
):
981 hosts
, to_add
, to_remove
= HostAssignment(
982 spec
=ServiceSpec(service_type
, placement
=placement
),
983 hosts
=[HostSpec(h
) for h
in hosts
],
984 unreachable_hosts
=[],
987 assert len(hosts
) == expected_len
989 assert h
in [h
.hostname
for h
in hosts
]
992 class NodeAssignmentTest4(NamedTuple
):
994 networks
: Dict
[str, Dict
[str, Dict
[str, List
[str]]]]
995 daemons
: List
[DaemonDescription
]
997 expected_add
: List
[str]
998 expected_remove
: List
[DaemonDescription
]
1001 @pytest.mark
.parametrize("spec,networks,daemons,expected,expected_add,expected_remove",
1003 NodeAssignmentTest4(
1007 placement
=PlacementSpec(count
=6, label
='foo'),
1008 networks
=['10.0.0.0/8'],
1011 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}},
1012 'host2': {'10.0.0.0/8': {'eth0': ['10.0.0.2']}},
1013 'host3': {'192.168.0.0/16': {'eth0': ['192.168.0.1']}},
1016 ['rgw:host1(10.0.0.1:80)', 'rgw:host2(10.0.0.2:80)',
1017 'rgw:host1(10.0.0.1:81)', 'rgw:host2(10.0.0.2:81)',
1018 'rgw:host1(10.0.0.1:82)', 'rgw:host2(10.0.0.2:82)'],
1019 ['rgw:host1(10.0.0.1:80)', 'rgw:host2(10.0.0.2:80)',
1020 'rgw:host1(10.0.0.1:81)', 'rgw:host2(10.0.0.2:81)',
1021 'rgw:host1(10.0.0.1:82)', 'rgw:host2(10.0.0.2:82)'],
1024 NodeAssignmentTest4(
1026 service_type
='ingress',
1027 service_id
='rgw.foo',
1030 virtual_ip
='10.0.0.20/8',
1031 backend_service
='rgw.foo',
1032 placement
=PlacementSpec(label
='foo'),
1033 networks
=['10.0.0.0/8'],
1036 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}},
1037 'host2': {'10.0.0.0/8': {'eth1': ['10.0.0.2']}},
1038 'host3': {'192.168.0.0/16': {'eth2': ['192.168.0.1']}},
1041 ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)',
1042 'keepalived:host1', 'keepalived:host2'],
1043 ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)',
1044 'keepalived:host1', 'keepalived:host2'],
1047 NodeAssignmentTest4(
1049 service_type
='ingress',
1050 service_id
='rgw.foo',
1053 virtual_ip
='10.0.0.20/8',
1054 backend_service
='rgw.foo',
1055 placement
=PlacementSpec(label
='foo'),
1056 networks
=['10.0.0.0/8'],
1059 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}},
1060 'host2': {'10.0.0.0/8': {'eth1': ['10.0.0.2']}},
1061 'host3': {'192.168.0.0/16': {'eth2': ['192.168.0.1']}},
1064 DaemonDescription('haproxy', 'a', 'host1', ip
='10.0.0.1',
1066 DaemonDescription('keepalived', 'b', 'host2'),
1067 DaemonDescription('keepalived', 'c', 'host3'),
1069 ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)',
1070 'keepalived:host1', 'keepalived:host2'],
1071 ['haproxy:host2(10.0.0.2:443,8888)',
1072 'keepalived:host1'],
1076 def test_node_assignment4(spec
, networks
, daemons
,
1077 expected
, expected_add
, expected_remove
):
1078 all_slots
, to_add
, to_remove
= HostAssignment(
1080 hosts
=[HostSpec(h
, labels
=['foo']) for h
in networks
.keys()],
1081 unreachable_hosts
=[],
1085 primary_daemon_type
='haproxy' if spec
.service_type
== 'ingress' else spec
.service_type
,
1086 per_host_daemon_type
='keepalived' if spec
.service_type
== 'ingress' else None,
1089 got
= [str(p
) for p
in all_slots
]
1097 assert num_wildcard
== len(got
)
1099 got
= [str(p
) for p
in to_add
]
1101 for i
in expected_add
:
1107 assert num_wildcard
== len(got
)
1109 assert sorted([d
.name() for d
in to_remove
]) == sorted(expected_remove
)
1112 @pytest.mark
.parametrize("placement",
1117 ('hostname12hostname12hostname12hostname12hostname12hostname12hostname12'), # > 63 chars
1119 def test_bad_placements(placement
):
1121 PlacementSpec
.from_string(placement
.split(' '))
1123 except SpecValidationError
:
1127 class NodeAssignmentTestBadSpec(NamedTuple
):
1129 placement
: PlacementSpec
1131 daemons
: List
[DaemonDescription
]
1135 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected",
1138 NodeAssignmentTestBadSpec(
1140 PlacementSpec(hosts
=['unknownhost']),
1143 "Cannot place <ServiceSpec for service_name=mgr> on unknownhost: Unknown hosts"
1145 # unknown host pattern
1146 NodeAssignmentTestBadSpec(
1148 PlacementSpec(host_pattern
='unknownhost'),
1151 "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts"
1154 NodeAssignmentTestBadSpec(
1156 PlacementSpec(label
='unknownlabel'),
1159 "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts for label unknownlabel"
1162 def test_bad_specs(service_type
, placement
, hosts
, daemons
, expected
):
1163 with pytest
.raises(OrchestratorValidationError
) as e
:
1164 hosts
, to_add
, to_remove
= HostAssignment(
1165 spec
=ServiceSpec(service_type
, placement
=placement
),
1166 hosts
=[HostSpec(h
) for h
in hosts
],
1167 unreachable_hosts
=[],
1170 assert str(e
.value
) == expected
1173 class ActiveAssignmentTest(NamedTuple
):
1175 placement
: PlacementSpec
1177 daemons
: List
[DaemonDescription
]
1178 expected
: List
[List
[str]]
1179 expected_add
: List
[List
[str]]
1180 expected_remove
: List
[List
[str]]
1183 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected,expected_add,expected_remove",
1185 ActiveAssignmentTest(
1187 PlacementSpec(count
=2),
1188 'host1 host2 host3'.split(),
1190 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1191 DaemonDescription('mgr', 'b', 'host2'),
1192 DaemonDescription('mgr', 'c', 'host3'),
1194 [['host1', 'host2'], ['host1', 'host3']],
1196 [['mgr.b'], ['mgr.c']]
1198 ActiveAssignmentTest(
1200 PlacementSpec(count
=2),
1201 'host1 host2 host3'.split(),
1203 DaemonDescription('mgr', 'a', 'host1'),
1204 DaemonDescription('mgr', 'b', 'host2'),
1205 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1207 [['host1', 'host3'], ['host2', 'host3']],
1209 [['mgr.a'], ['mgr.b']]
1211 ActiveAssignmentTest(
1213 PlacementSpec(count
=1),
1214 'host1 host2 host3'.split(),
1216 DaemonDescription('mgr', 'a', 'host1'),
1217 DaemonDescription('mgr', 'b', 'host2', is_active
=True),
1218 DaemonDescription('mgr', 'c', 'host3'),
1222 [['mgr.a', 'mgr.c']]
1224 ActiveAssignmentTest(
1226 PlacementSpec(count
=1),
1227 'host1 host2 host3'.split(),
1229 DaemonDescription('mgr', 'a', 'host1'),
1230 DaemonDescription('mgr', 'b', 'host2'),
1231 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1235 [['mgr.a', 'mgr.b']]
1237 ActiveAssignmentTest(
1239 PlacementSpec(count
=1),
1240 'host1 host2 host3'.split(),
1242 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1243 DaemonDescription('mgr', 'b', 'host2'),
1244 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1246 [['host1'], ['host3']],
1248 [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c']]
1250 ActiveAssignmentTest(
1252 PlacementSpec(count
=2),
1253 'host1 host2 host3'.split(),
1255 DaemonDescription('mgr', 'a', 'host1'),
1256 DaemonDescription('mgr', 'b', 'host2', is_active
=True),
1257 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1259 [['host2', 'host3']],
1263 ActiveAssignmentTest(
1265 PlacementSpec(count
=1),
1266 'host1 host2 host3'.split(),
1268 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1269 DaemonDescription('mgr', 'b', 'host2', is_active
=True),
1270 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1272 [['host1'], ['host2'], ['host3']],
1274 [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c'], ['mgr.a', 'mgr.c']]
1276 ActiveAssignmentTest(
1278 PlacementSpec(count
=1),
1279 'host1 host2 host3'.split(),
1281 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1282 DaemonDescription('mgr', 'a2', 'host1'),
1283 DaemonDescription('mgr', 'b', 'host2'),
1284 DaemonDescription('mgr', 'c', 'host3'),
1288 [['mgr.a2', 'mgr.b', 'mgr.c']]
1290 ActiveAssignmentTest(
1292 PlacementSpec(count
=1),
1293 'host1 host2 host3'.split(),
1295 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1296 DaemonDescription('mgr', 'a2', 'host1', is_active
=True),
1297 DaemonDescription('mgr', 'b', 'host2'),
1298 DaemonDescription('mgr', 'c', 'host3'),
1302 [['mgr.a', 'mgr.b', 'mgr.c'], ['mgr.a2', 'mgr.b', 'mgr.c']]
1304 ActiveAssignmentTest(
1306 PlacementSpec(count
=2),
1307 'host1 host2 host3'.split(),
1309 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1310 DaemonDescription('mgr', 'a2', 'host1'),
1311 DaemonDescription('mgr', 'b', 'host2'),
1312 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1314 [['host1', 'host3']],
1316 [['mgr.a2', 'mgr.b']]
1318 # Explicit placement should override preference for active daemon
1319 ActiveAssignmentTest(
1321 PlacementSpec(count
=1, hosts
=['host1']),
1322 'host1 host2 host3'.split(),
1324 DaemonDescription('mgr', 'a', 'host1'),
1325 DaemonDescription('mgr', 'b', 'host2'),
1326 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1330 [['mgr.b', 'mgr.c']]
1334 def test_active_assignment(service_type
, placement
, hosts
, daemons
, expected
, expected_add
, expected_remove
):
1336 spec
= ServiceSpec(service_type
=service_type
,
1338 placement
=placement
)
1340 hosts
, to_add
, to_remove
= HostAssignment(
1342 hosts
=[HostSpec(h
) for h
in hosts
],
1343 unreachable_hosts
=[],
1346 assert sorted([h
.hostname
for h
in hosts
]) in expected
1347 assert sorted([h
.hostname
for h
in to_add
]) in expected_add
1348 assert sorted([h
.name() for h
in to_remove
]) in expected_remove
1351 class UnreachableHostsTest(NamedTuple
):
1353 placement
: PlacementSpec
1355 unreachables_hosts
: List
[str]
1356 daemons
: List
[DaemonDescription
]
1357 expected_add
: List
[List
[str]]
1358 expected_remove
: List
[List
[str]]
1361 @pytest.mark
.parametrize("service_type,placement,hosts,unreachable_hosts,daemons,expected_add,expected_remove",
1363 UnreachableHostsTest(
1365 PlacementSpec(count
=3),
1366 'host1 host2 host3'.split(),
1369 [['host1', 'host3']],
1372 UnreachableHostsTest(
1374 PlacementSpec(hosts
=['host3']),
1375 'host1 host2 host3'.split(),
1378 DaemonDescription('mgr', 'a', 'host1'),
1379 DaemonDescription('mgr', 'b', 'host2'),
1380 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1385 UnreachableHostsTest(
1387 PlacementSpec(count
=3),
1388 'host1 host2 host3 host4'.split(),
1391 DaemonDescription('mgr', 'a', 'host1'),
1392 DaemonDescription('mgr', 'b', 'host2'),
1393 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1398 UnreachableHostsTest(
1400 PlacementSpec(count
=1),
1401 'host1 host2 host3 host4'.split(),
1402 'host1 host3'.split(),
1404 DaemonDescription('mgr', 'a', 'host1'),
1405 DaemonDescription('mgr', 'b', 'host2'),
1406 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1411 UnreachableHostsTest(
1413 PlacementSpec(count
=3),
1414 'host1 host2 host3 host4'.split(),
1417 [['host1', 'host3', 'host4']],
1420 UnreachableHostsTest(
1422 PlacementSpec(count
=3),
1423 'host1 host2 host3 host4'.split(),
1424 'host1 host4'.split(),
1426 [['host2', 'host3']],
1431 def test_unreachable_host(service_type
, placement
, hosts
, unreachable_hosts
, daemons
, expected_add
, expected_remove
):
1433 spec
= ServiceSpec(service_type
=service_type
,
1435 placement
=placement
)
1437 hosts
, to_add
, to_remove
= HostAssignment(
1439 hosts
=[HostSpec(h
) for h
in hosts
],
1440 unreachable_hosts
=[HostSpec(h
) for h
in unreachable_hosts
],
1443 assert sorted([h
.hostname
for h
in to_add
]) in expected_add
1444 assert sorted([h
.name() for h
in to_remove
]) in expected_remove