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(
137 if isinstance(host_res
, list):
138 e
= ', '.join(repr(h
.hostname
) for h
in host_res
)
139 assert False, f
'`(k("{key}"), exactly({e})),` not found'
140 assert False, f
'`(k("{key}"), ...),` not found'
141 except OrchestratorError
as e
:
142 assert False, f
'`(k("{key}"), error({type(e).__name__}, {repr(str(e))})),` not found'
144 for _
in range(10): # scheduler has a random component
147 host_res
, to_add
, to_remove
= HostAssignment(
153 assert_res(sorted([h
.hostname
for h
in host_res
]))
154 except Exception as e
:
158 # * first match from the top wins
159 # * where e=[], *=any
161 # + list of known hosts available for scheduling (host_key)
162 # | + hosts used for explict placement (explicit_key)
164 # | | | + section (host, label, pattern)
165 # | | | | + expected result
167 test_explicit_scheduler_results
= [
168 (k("* * 0 *"), error(SpecValidationError
, 'num/count must be > 1')),
169 (k("* e N l"), error(OrchestratorValidationError
, 'Cannot place <ServiceSpec for service_name=mgr>: No matching hosts for label mylabel')),
170 (k("* e N p"), error(OrchestratorValidationError
, 'Cannot place <ServiceSpec for service_name=mgr>: No matching hosts')),
171 (k("* e N h"), error(OrchestratorValidationError
, 'placement spec is empty: no hosts, no label, no pattern, no count')),
172 (k("* e * *"), none
),
173 (k("1 12 * h"), error(OrchestratorValidationError
, "Cannot place <ServiceSpec for service_name=mgr> on 2: Unknown hosts")),
174 (k("1 123 * h"), error(OrchestratorValidationError
, "Cannot place <ServiceSpec for service_name=mgr> on 2, 3: Unknown hosts")),
175 (k("1 * * *"), exactly('1')),
176 (k("12 1 * *"), exactly('1')),
177 (k("12 12 1 *"), one_of('1', '2')),
178 (k("12 12 * *"), exactly('1', '2')),
179 (k("12 123 * h"), error(OrchestratorValidationError
, "Cannot place <ServiceSpec for service_name=mgr> on 3: Unknown hosts")),
180 (k("12 123 1 *"), one_of('1', '2', '3')),
181 (k("12 123 * *"), two_of('1', '2', '3')),
182 (k("123 1 * *"), exactly('1')),
183 (k("123 12 1 *"), one_of('1', '2')),
184 (k("123 12 * *"), exactly('1', '2')),
185 (k("123 123 1 *"), one_of('1', '2', '3')),
186 (k("123 123 2 *"), two_of('1', '2', '3')),
187 (k("123 123 * *"), exactly('1', '2', '3')),
191 @pytest.mark
.parametrize("dp,n,result",
194 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[80]),
196 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[80]),
199 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[80]),
201 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[82]),
204 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[80, 90]),
206 DaemonPlacement(daemon_type
='mgr', hostname
='host1', ports
=[82, 92]),
209 def test_daemon_placement_renumber(dp
, n
, result
):
210 assert dp
.renumber_ports(n
) == result
213 @pytest.mark
.parametrize(
217 DaemonPlacement(daemon_type
='mgr', hostname
='host1'),
218 DaemonDescription('mgr', 'a', 'host1'),
222 DaemonPlacement(daemon_type
='mgr', hostname
='host1', name
='a'),
223 DaemonDescription('mgr', 'a', 'host1'),
227 DaemonPlacement(daemon_type
='mon', hostname
='host1', name
='a'),
228 DaemonDescription('mgr', 'a', 'host1'),
232 DaemonPlacement(daemon_type
='mgr', hostname
='host1', name
='a'),
233 DaemonDescription('mgr', 'b', 'host1'),
237 def test_daemon_placement_match(dp
, dd
, result
):
238 assert dp
.matches_daemon(dd
) == result
241 @pytest.mark
.parametrize("spec_section_key,spec_section",
245 ('p', 'host_pattern'),
247 @pytest.mark
.parametrize("count",
255 @pytest.mark
.parametrize("explicit_key, explicit",
260 ('123', ['1', '2', '3']),
262 @pytest.mark
.parametrize("host_key, hosts",
266 ('123', ['1', '2', '3']),
268 def test_explicit_scheduler(host_key
, hosts
,
269 explicit_key
, explicit
,
271 spec_section_key
, spec_section
):
273 mk_spec
, hosts
= mk_spec_and_host(spec_section
, hosts
, explicit_key
, explicit
, count
)
275 results
=test_explicit_scheduler_results
,
279 key_elems
=(host_key
, explicit_key
, count
, spec_section_key
)
283 # * first match from the top wins
284 # * where e=[], *=any
286 # + list of known hosts available for scheduling (host_key)
287 # | + hosts used for explict placement (explicit_key)
289 # | | | + existing daemons
290 # | | | | + section (host, label, pattern)
291 # | | | | | + expected result
293 test_scheduler_daemons_results
= [
294 (k("* 1 * * *"), exactly('1')),
295 (k("1 123 * * h"), error(OrchestratorValidationError
, 'Cannot place <ServiceSpec for service_name=mgr> on 2, 3: Unknown hosts')),
296 (k("1 123 * * *"), exactly('1')),
297 (k("12 123 * * h"), error(OrchestratorValidationError
, 'Cannot place <ServiceSpec for service_name=mgr> on 3: Unknown hosts')),
298 (k("12 123 N * *"), exactly('1', '2')),
299 (k("12 123 1 * *"), one_of('1', '2')),
300 (k("12 123 2 * *"), exactly('1', '2')),
301 (k("12 123 3 * *"), exactly('1', '2')),
302 (k("123 123 N * *"), exactly('1', '2', '3')),
303 (k("123 123 1 e *"), one_of('1', '2', '3')),
304 (k("123 123 1 1 *"), exactly('1')),
305 (k("123 123 1 3 *"), exactly('3')),
306 (k("123 123 1 12 *"), one_of('1', '2')),
307 (k("123 123 1 112 *"), one_of('1', '2')),
308 (k("123 123 1 23 *"), one_of('2', '3')),
309 (k("123 123 1 123 *"), one_of('1', '2', '3')),
310 (k("123 123 2 e *"), two_of('1', '2', '3')),
311 (k("123 123 2 1 *"), _or(exactly('1', '2'), exactly('1', '3'))),
312 (k("123 123 2 3 *"), _or(exactly('1', '3'), exactly('2', '3'))),
313 (k("123 123 2 12 *"), exactly('1', '2')),
314 (k("123 123 2 112 *"), exactly('1', '2')),
315 (k("123 123 2 23 *"), exactly('2', '3')),
316 (k("123 123 2 123 *"), two_of('1', '2', '3')),
317 (k("123 123 3 * *"), exactly('1', '2', '3')),
321 @pytest.mark
.parametrize("spec_section_key,spec_section",
325 ('p', 'host_pattern'),
327 @pytest.mark
.parametrize("daemons_key, daemons",
333 ('112', ['1', '1', '2']), # deal with existing co-located daemons
335 ('123', ['1', '2', '3']),
337 @pytest.mark
.parametrize("count",
344 @pytest.mark
.parametrize("explicit_key, explicit",
347 ('123', ['1', '2', '3']),
349 @pytest.mark
.parametrize("host_key, hosts",
353 ('123', ['1', '2', '3']),
355 def test_scheduler_daemons(host_key
, hosts
,
356 explicit_key
, explicit
,
358 daemons_key
, daemons
,
359 spec_section_key
, spec_section
):
360 mk_spec
, hosts
= mk_spec_and_host(spec_section
, hosts
, explicit_key
, explicit
, count
)
362 DaemonDescription('mgr', d
, d
)
366 results
=test_scheduler_daemons_results
,
370 key_elems
=(host_key
, explicit_key
, count
, daemons_key
, spec_section_key
)
374 # =========================
377 class NodeAssignmentTest(NamedTuple
):
379 placement
: PlacementSpec
381 daemons
: List
[DaemonDescription
]
382 rank_map
: Optional
[Dict
[int, Dict
[int, Optional
[str]]]]
383 post_rank_map
: Optional
[Dict
[int, Dict
[int, Optional
[str]]]]
385 expected_add
: List
[str]
386 expected_remove
: List
[DaemonDescription
]
389 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,rank_map,post_rank_map,expected,expected_add,expected_remove",
394 PlacementSpec(hosts
=['smithi060']),
398 ['mgr:smithi060'], ['mgr:smithi060'], []
403 PlacementSpec(host_pattern
='*'),
404 'host1 host2 host3'.split(),
406 DaemonDescription('mgr', 'a', 'host1'),
407 DaemonDescription('mgr', 'b', 'host2'),
410 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
414 # all_hosts + count_per_host
417 PlacementSpec(host_pattern
='*', count_per_host
=2),
418 'host1 host2 host3'.split(),
420 DaemonDescription('mds', 'a', 'host1'),
421 DaemonDescription('mds', 'b', 'host2'),
424 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
425 ['mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
428 # count that is bigger than the amount of hosts. Truncate to len(hosts)
429 # mgr should not be co-located to each other.
432 PlacementSpec(count
=4),
433 'host1 host2 host3'.split(),
436 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
437 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
440 # count that is bigger than the amount of hosts; wrap around.
443 PlacementSpec(count
=6),
444 'host1 host2 host3'.split(),
447 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
448 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
451 # count + partial host list
454 PlacementSpec(count
=3, hosts
=['host3']),
455 'host1 host2 host3'.split(),
457 DaemonDescription('mgr', 'a', 'host1'),
458 DaemonDescription('mgr', 'b', 'host2'),
465 # count + partial host list (with colo)
468 PlacementSpec(count
=3, hosts
=['host3']),
469 'host1 host2 host3'.split(),
471 DaemonDescription('mds', 'a', 'host1'),
472 DaemonDescription('mds', 'b', 'host2'),
475 ['mds:host3', 'mds:host3', 'mds:host3'],
476 ['mds:host3', 'mds:host3', 'mds:host3'],
479 # count 1 + partial host list
482 PlacementSpec(count
=1, hosts
=['host3']),
483 'host1 host2 host3'.split(),
485 DaemonDescription('mgr', 'a', 'host1'),
486 DaemonDescription('mgr', 'b', 'host2'),
493 # count + partial host list + existing
496 PlacementSpec(count
=2, hosts
=['host3']),
497 'host1 host2 host3'.split(),
499 DaemonDescription('mgr', 'a', 'host1'),
506 # count + partial host list + existing (deterministic)
509 PlacementSpec(count
=2, hosts
=['host1']),
510 'host1 host2'.split(),
512 DaemonDescription('mgr', 'a', 'host1'),
519 # count + partial host list + existing (deterministic)
522 PlacementSpec(count
=2, hosts
=['host1']),
523 'host1 host2'.split(),
525 DaemonDescription('mgr', 'a', 'host2'),
535 PlacementSpec(label
='foo'),
536 'host1 host2 host3'.split(),
539 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
540 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
543 # label + count (truncate to host list)
546 PlacementSpec(count
=4, label
='foo'),
547 'host1 host2 host3'.split(),
550 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
551 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
554 # label + count (with colo)
557 PlacementSpec(count
=6, label
='foo'),
558 'host1 host2 host3'.split(),
561 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
562 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
565 # label only + count_per_hst
568 PlacementSpec(label
='foo', count_per_host
=3),
569 'host1 host2 host3'.split(),
572 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3',
573 'mds:host1', 'mds:host2', 'mds:host3'],
574 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3',
575 'mds:host1', 'mds:host2', 'mds:host3'],
581 PlacementSpec(host_pattern
='mgr*'),
582 'mgrhost1 mgrhost2 datahost'.split(),
585 ['mgr:mgrhost1', 'mgr:mgrhost2'],
586 ['mgr:mgrhost1', 'mgr:mgrhost2'],
589 # host_pattern + count_per_host
592 PlacementSpec(host_pattern
='mds*', count_per_host
=3),
593 'mdshost1 mdshost2 datahost'.split(),
596 ['mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2'],
597 ['mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2'],
600 # label + count_per_host + ports
603 PlacementSpec(count
=6, label
='foo'),
604 'host1 host2 host3'.split(),
607 ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)',
608 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'],
609 ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)',
610 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'],
613 # label + count_per_host + ports (+ xisting)
616 PlacementSpec(count
=6, label
='foo'),
617 'host1 host2 host3'.split(),
619 DaemonDescription('rgw', 'a', 'host1', ports
=[81]),
620 DaemonDescription('rgw', 'b', 'host2', ports
=[80]),
621 DaemonDescription('rgw', 'c', 'host1', ports
=[82]),
624 ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)',
625 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'],
626 ['rgw:host1(*:80)', 'rgw:host3(*:80)',
627 'rgw:host2(*:81)', 'rgw:host3(*:81)'],
630 # cephadm.py teuth case
633 PlacementSpec(count
=3, hosts
=['host1=y', 'host2=x']),
634 'host1 host2'.split(),
636 DaemonDescription('mgr', 'y', 'host1'),
637 DaemonDescription('mgr', 'x', 'host2'),
640 ['mgr:host1(name=y)', 'mgr:host2(name=x)'],
644 # note: host -> rank mapping is psuedo-random based on svc name, so these
645 # host/rank pairs may seem random but they match the nfs.mynfs seed used by
651 PlacementSpec(count
=3),
652 'host1 host2 host3'.split(),
655 {0: {0: None}, 1: {0: None}, 2: {0: None}},
656 ['nfs:host1(rank=0.0)', 'nfs:host2(rank=1.0)', 'nfs:host3(rank=2.0)'],
657 ['nfs:host1(rank=0.0)', 'nfs:host2(rank=1.0)', 'nfs:host3(rank=2.0)'],
663 PlacementSpec(count
=3),
664 'host1 host2 host3'.split(),
666 DaemonDescription('nfs', '0.1', 'host1', rank
=0, rank_generation
=1),
669 {0: {1: '0.1'}, 1: {0: None}, 2: {0: None}},
670 ['nfs:host1(rank=0.1)', 'nfs:host2(rank=1.0)', 'nfs:host3(rank=2.0)'],
671 ['nfs:host2(rank=1.0)', 'nfs:host3(rank=2.0)'],
674 # ranked, exist, different ranks
677 PlacementSpec(count
=3),
678 'host1 host2 host3'.split(),
680 DaemonDescription('nfs', '0.1', 'host1', rank
=0, rank_generation
=1),
681 DaemonDescription('nfs', '1.1', 'host2', rank
=1, rank_generation
=1),
683 {0: {1: '0.1'}, 1: {1: '1.1'}},
684 {0: {1: '0.1'}, 1: {1: '1.1'}, 2: {0: None}},
685 ['nfs:host1(rank=0.1)', 'nfs:host2(rank=1.1)', 'nfs:host3(rank=2.0)'],
686 ['nfs:host3(rank=2.0)'],
689 # ranked, exist, different ranks (2)
692 PlacementSpec(count
=3),
693 'host1 host2 host3'.split(),
695 DaemonDescription('nfs', '0.1', 'host1', rank
=0, rank_generation
=1),
696 DaemonDescription('nfs', '1.1', 'host3', rank
=1, rank_generation
=1),
698 {0: {1: '0.1'}, 1: {1: '1.1'}},
699 {0: {1: '0.1'}, 1: {1: '1.1'}, 2: {0: None}},
700 ['nfs:host1(rank=0.1)', 'nfs:host3(rank=1.1)', 'nfs:host2(rank=2.0)'],
701 ['nfs:host2(rank=2.0)'],
704 # ranked, exist, extra ranks
707 PlacementSpec(count
=3),
708 'host1 host2 host3'.split(),
710 DaemonDescription('nfs', '0.5', 'host1', rank
=0, rank_generation
=5),
711 DaemonDescription('nfs', '1.5', 'host2', rank
=1, rank_generation
=5),
712 DaemonDescription('nfs', '4.5', 'host2', rank
=4, rank_generation
=5),
714 {0: {5: '0.5'}, 1: {5: '1.5'}},
715 {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {0: None}},
716 ['nfs:host1(rank=0.5)', 'nfs:host2(rank=1.5)', 'nfs:host3(rank=2.0)'],
717 ['nfs:host3(rank=2.0)'],
720 # 25: ranked, exist, extra ranks (scale down: kill off high rank)
723 PlacementSpec(count
=2),
724 'host3 host2 host1'.split(),
726 DaemonDescription('nfs', '0.5', 'host1', rank
=0, rank_generation
=5),
727 DaemonDescription('nfs', '1.5', 'host2', rank
=1, rank_generation
=5),
728 DaemonDescription('nfs', '2.5', 'host3', rank
=2, rank_generation
=5),
730 {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {5: '2.5'}},
731 {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {5: '2.5'}},
732 ['nfs:host1(rank=0.5)', 'nfs:host2(rank=1.5)'],
736 # ranked, exist, extra ranks (scale down hosts)
739 PlacementSpec(count
=2),
740 'host1 host3'.split(),
742 DaemonDescription('nfs', '0.5', 'host1', rank
=0, rank_generation
=5),
743 DaemonDescription('nfs', '1.5', 'host2', rank
=1, rank_generation
=5),
744 DaemonDescription('nfs', '2.5', 'host3', rank
=4, rank_generation
=5),
746 {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {5: '2.5'}},
747 {0: {5: '0.5'}, 1: {5: '1.5', 6: None}, 2: {5: '2.5'}},
748 ['nfs:host1(rank=0.5)', 'nfs:host3(rank=1.6)'],
749 ['nfs:host3(rank=1.6)'],
750 ['nfs.2.5', 'nfs.1.5']
752 # ranked, exist, duplicate rank
755 PlacementSpec(count
=3),
756 'host1 host2 host3'.split(),
758 DaemonDescription('nfs', '0.0', 'host1', rank
=0, rank_generation
=0),
759 DaemonDescription('nfs', '1.1', 'host2', rank
=1, rank_generation
=1),
760 DaemonDescription('nfs', '1.2', 'host3', rank
=1, rank_generation
=2),
762 {0: {0: '0.0'}, 1: {2: '1.2'}},
763 {0: {0: '0.0'}, 1: {2: '1.2'}, 2: {0: None}},
764 ['nfs:host1(rank=0.0)', 'nfs:host3(rank=1.2)', 'nfs:host2(rank=2.0)'],
765 ['nfs:host2(rank=2.0)'],
768 # 28: ranked, all gens stale (failure during update cycle)
771 PlacementSpec(count
=2),
772 'host1 host2 host3'.split(),
774 DaemonDescription('nfs', '0.2', 'host1', rank
=0, rank_generation
=2),
775 DaemonDescription('nfs', '1.2', 'host2', rank
=1, rank_generation
=2),
777 {0: {2: '0.2'}, 1: {2: '1.2', 3: '1.3'}},
778 {0: {2: '0.2'}, 1: {2: '1.2', 3: '1.3', 4: None}},
779 ['nfs:host1(rank=0.2)', 'nfs:host2(rank=1.4)'],
780 ['nfs:host2(rank=1.4)'],
783 # ranked, not enough hosts
786 PlacementSpec(count
=4),
787 'host1 host2 host3'.split(),
789 DaemonDescription('nfs', '0.2', 'host1', rank
=0, rank_generation
=2),
790 DaemonDescription('nfs', '1.2', 'host2', rank
=1, rank_generation
=2),
792 {0: {2: '0.2'}, 1: {2: '1.2'}},
793 {0: {2: '0.2'}, 1: {2: '1.2'}, 2: {0: None}},
794 ['nfs:host1(rank=0.2)', 'nfs:host2(rank=1.2)', 'nfs:host3(rank=2.0)'],
795 ['nfs:host3(rank=2.0)'],
801 PlacementSpec(hosts
=['host2']),
802 'host1 host2'.split(),
804 DaemonDescription('nfs', '0.2', 'host1', rank
=0, rank_generation
=2),
805 DaemonDescription('nfs', '1.2', 'host2', rank
=1, rank_generation
=2),
806 DaemonDescription('nfs', '2.2', 'host3', rank
=2, rank_generation
=2),
808 {0: {2: '0.2'}, 1: {2: '1.2'}, 2: {2: '2.2'}},
809 {0: {2: '0.2', 3: None}, 1: {2: '1.2'}, 2: {2: '2.2'}},
810 ['nfs:host2(rank=0.3)'],
811 ['nfs:host2(rank=0.3)'],
812 ['nfs.0.2', 'nfs.1.2', 'nfs.2.2']
816 def test_node_assignment(service_type
, placement
, hosts
, daemons
, rank_map
, post_rank_map
,
817 expected
, expected_add
, expected_remove
):
821 if service_type
== 'rgw':
822 service_id
= 'realm.zone'
824 elif service_type
== 'mds':
827 elif service_type
== 'nfs':
829 spec
= ServiceSpec(service_type
=service_type
,
830 service_id
=service_id
,
835 spec
= ServiceSpec(service_type
=service_type
,
836 service_id
=service_id
,
839 all_slots
, to_add
, to_remove
= HostAssignment(
841 hosts
=[HostSpec(h
, labels
=['foo']) for h
in hosts
],
843 allow_colo
=allow_colo
,
847 assert rank_map
== post_rank_map
849 got
= [str(p
) for p
in all_slots
]
857 assert num_wildcard
== len(got
)
859 got
= [str(p
) for p
in to_add
]
861 for i
in expected_add
:
867 assert num_wildcard
== len(got
)
869 assert sorted([d
.name() for d
in to_remove
]) == sorted(expected_remove
)
872 class NodeAssignmentTest2(NamedTuple
):
874 placement
: PlacementSpec
876 daemons
: List
[DaemonDescription
]
881 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected_len,in_set",
886 PlacementSpec(count
=1),
887 'host1 host2 host3'.split(),
890 ['host1', 'host2', 'host3'],
893 # hosts + (smaller) count
896 PlacementSpec(count
=1, hosts
='host1 host2'.split()),
897 'host1 host2'.split(),
902 # hosts + (smaller) count, existing
905 PlacementSpec(count
=1, hosts
='host1 host2 host3'.split()),
906 'host1 host2 host3'.split(),
907 [DaemonDescription('mgr', 'mgr.a', 'host1')],
909 ['host1', 'host2', 'host3'],
911 # hosts + (smaller) count, (more) existing
914 PlacementSpec(count
=1, hosts
='host1 host2 host3'.split()),
915 'host1 host2 host3'.split(),
917 DaemonDescription('mgr', 'a', 'host1'),
918 DaemonDescription('mgr', 'b', 'host2'),
923 # count + partial host list
926 PlacementSpec(count
=2, hosts
=['host3']),
927 'host1 host2 host3'.split(),
930 ['host1', 'host2', 'host3']
935 PlacementSpec(count
=1, label
='foo'),
936 'host1 host2 host3'.split(),
939 ['host1', 'host2', 'host3']
942 def test_node_assignment2(service_type
, placement
, hosts
,
943 daemons
, expected_len
, in_set
):
944 hosts
, to_add
, to_remove
= HostAssignment(
945 spec
=ServiceSpec(service_type
, placement
=placement
),
946 hosts
=[HostSpec(h
, labels
=['foo']) for h
in hosts
],
949 assert len(hosts
) == expected_len
950 for h
in [h
.hostname
for h
in hosts
]:
954 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected_len,must_have",
956 # hosts + (smaller) count, (more) existing
959 PlacementSpec(count
=3, hosts
='host3'.split()),
960 'host1 host2 host3'.split(),
965 # count + partial host list
968 PlacementSpec(count
=2, hosts
=['host3']),
969 'host1 host2 host3'.split(),
975 def test_node_assignment3(service_type
, placement
, hosts
,
976 daemons
, expected_len
, must_have
):
977 hosts
, to_add
, to_remove
= HostAssignment(
978 spec
=ServiceSpec(service_type
, placement
=placement
),
979 hosts
=[HostSpec(h
) for h
in hosts
],
982 assert len(hosts
) == expected_len
984 assert h
in [h
.hostname
for h
in hosts
]
987 class NodeAssignmentTest4(NamedTuple
):
989 networks
: Dict
[str, Dict
[str, Dict
[str, List
[str]]]]
990 daemons
: List
[DaemonDescription
]
992 expected_add
: List
[str]
993 expected_remove
: List
[DaemonDescription
]
996 @pytest.mark
.parametrize("spec,networks,daemons,expected,expected_add,expected_remove",
1002 placement
=PlacementSpec(count
=6, label
='foo'),
1003 networks
=['10.0.0.0/8'],
1006 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}},
1007 'host2': {'10.0.0.0/8': {'eth0': ['10.0.0.2']}},
1008 'host3': {'192.168.0.0/16': {'eth0': ['192.168.0.1']}},
1011 ['rgw:host1(10.0.0.1:80)', 'rgw:host2(10.0.0.2:80)',
1012 'rgw:host1(10.0.0.1:81)', 'rgw:host2(10.0.0.2:81)',
1013 'rgw:host1(10.0.0.1:82)', 'rgw:host2(10.0.0.2:82)'],
1014 ['rgw:host1(10.0.0.1:80)', 'rgw:host2(10.0.0.2:80)',
1015 'rgw:host1(10.0.0.1:81)', 'rgw:host2(10.0.0.2:81)',
1016 'rgw:host1(10.0.0.1:82)', 'rgw:host2(10.0.0.2:82)'],
1019 NodeAssignmentTest4(
1021 service_type
='ingress',
1022 service_id
='rgw.foo',
1025 virtual_ip
='10.0.0.20/8',
1026 backend_service
='rgw.foo',
1027 placement
=PlacementSpec(label
='foo'),
1028 networks
=['10.0.0.0/8'],
1031 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}},
1032 'host2': {'10.0.0.0/8': {'eth1': ['10.0.0.2']}},
1033 'host3': {'192.168.0.0/16': {'eth2': ['192.168.0.1']}},
1036 ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)',
1037 'keepalived:host1', 'keepalived:host2'],
1038 ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)',
1039 'keepalived:host1', 'keepalived:host2'],
1042 NodeAssignmentTest4(
1044 service_type
='ingress',
1045 service_id
='rgw.foo',
1048 virtual_ip
='10.0.0.20/8',
1049 backend_service
='rgw.foo',
1050 placement
=PlacementSpec(label
='foo'),
1051 networks
=['10.0.0.0/8'],
1054 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}},
1055 'host2': {'10.0.0.0/8': {'eth1': ['10.0.0.2']}},
1056 'host3': {'192.168.0.0/16': {'eth2': ['192.168.0.1']}},
1059 DaemonDescription('haproxy', 'a', 'host1', ip
='10.0.0.1',
1061 DaemonDescription('keepalived', 'b', 'host2'),
1062 DaemonDescription('keepalived', 'c', 'host3'),
1064 ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)',
1065 'keepalived:host1', 'keepalived:host2'],
1066 ['haproxy:host2(10.0.0.2:443,8888)',
1067 'keepalived:host1'],
1071 def test_node_assignment4(spec
, networks
, daemons
,
1072 expected
, expected_add
, expected_remove
):
1073 all_slots
, to_add
, to_remove
= HostAssignment(
1075 hosts
=[HostSpec(h
, labels
=['foo']) for h
in networks
.keys()],
1079 primary_daemon_type
='haproxy' if spec
.service_type
== 'ingress' else spec
.service_type
,
1080 per_host_daemon_type
='keepalived' if spec
.service_type
== 'ingress' else None,
1083 got
= [str(p
) for p
in all_slots
]
1091 assert num_wildcard
== len(got
)
1093 got
= [str(p
) for p
in to_add
]
1095 for i
in expected_add
:
1101 assert num_wildcard
== len(got
)
1103 assert sorted([d
.name() for d
in to_remove
]) == sorted(expected_remove
)
1106 @pytest.mark
.parametrize("placement",
1111 ('hostname12hostname12hostname12hostname12hostname12hostname12hostname12'), # > 63 chars
1113 def test_bad_placements(placement
):
1115 PlacementSpec
.from_string(placement
.split(' '))
1117 except SpecValidationError
:
1121 class NodeAssignmentTestBadSpec(NamedTuple
):
1123 placement
: PlacementSpec
1125 daemons
: List
[DaemonDescription
]
1129 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected",
1132 NodeAssignmentTestBadSpec(
1134 PlacementSpec(hosts
=['unknownhost']),
1137 "Cannot place <ServiceSpec for service_name=mgr> on unknownhost: Unknown hosts"
1139 # unknown host pattern
1140 NodeAssignmentTestBadSpec(
1142 PlacementSpec(host_pattern
='unknownhost'),
1145 "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts"
1148 NodeAssignmentTestBadSpec(
1150 PlacementSpec(label
='unknownlabel'),
1153 "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts for label unknownlabel"
1156 def test_bad_specs(service_type
, placement
, hosts
, daemons
, expected
):
1157 with pytest
.raises(OrchestratorValidationError
) as e
:
1158 hosts
, to_add
, to_remove
= HostAssignment(
1159 spec
=ServiceSpec(service_type
, placement
=placement
),
1160 hosts
=[HostSpec(h
) for h
in hosts
],
1163 assert str(e
.value
) == expected
1166 class ActiveAssignmentTest(NamedTuple
):
1168 placement
: PlacementSpec
1170 daemons
: List
[DaemonDescription
]
1171 expected
: List
[List
[str]]
1172 expected_add
: List
[List
[str]]
1173 expected_remove
: List
[List
[str]]
1176 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected,expected_add,expected_remove",
1178 ActiveAssignmentTest(
1180 PlacementSpec(count
=2),
1181 'host1 host2 host3'.split(),
1183 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1184 DaemonDescription('mgr', 'b', 'host2'),
1185 DaemonDescription('mgr', 'c', 'host3'),
1187 [['host1', 'host2'], ['host1', 'host3']],
1189 [['mgr.b'], ['mgr.c']]
1191 ActiveAssignmentTest(
1193 PlacementSpec(count
=2),
1194 'host1 host2 host3'.split(),
1196 DaemonDescription('mgr', 'a', 'host1'),
1197 DaemonDescription('mgr', 'b', 'host2'),
1198 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1200 [['host1', 'host3'], ['host2', 'host3']],
1202 [['mgr.a'], ['mgr.b']]
1204 ActiveAssignmentTest(
1206 PlacementSpec(count
=1),
1207 'host1 host2 host3'.split(),
1209 DaemonDescription('mgr', 'a', 'host1'),
1210 DaemonDescription('mgr', 'b', 'host2', is_active
=True),
1211 DaemonDescription('mgr', 'c', 'host3'),
1215 [['mgr.a', 'mgr.c']]
1217 ActiveAssignmentTest(
1219 PlacementSpec(count
=1),
1220 'host1 host2 host3'.split(),
1222 DaemonDescription('mgr', 'a', 'host1'),
1223 DaemonDescription('mgr', 'b', 'host2'),
1224 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1228 [['mgr.a', 'mgr.b']]
1230 ActiveAssignmentTest(
1232 PlacementSpec(count
=1),
1233 'host1 host2 host3'.split(),
1235 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1236 DaemonDescription('mgr', 'b', 'host2'),
1237 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1239 [['host1'], ['host3']],
1241 [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c']]
1243 ActiveAssignmentTest(
1245 PlacementSpec(count
=2),
1246 'host1 host2 host3'.split(),
1248 DaemonDescription('mgr', 'a', 'host1'),
1249 DaemonDescription('mgr', 'b', 'host2', is_active
=True),
1250 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1252 [['host2', 'host3']],
1256 ActiveAssignmentTest(
1258 PlacementSpec(count
=1),
1259 'host1 host2 host3'.split(),
1261 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1262 DaemonDescription('mgr', 'b', 'host2', is_active
=True),
1263 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1265 [['host1'], ['host2'], ['host3']],
1267 [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c'], ['mgr.a', 'mgr.c']]
1269 ActiveAssignmentTest(
1271 PlacementSpec(count
=1),
1272 'host1 host2 host3'.split(),
1274 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1275 DaemonDescription('mgr', 'a2', 'host1'),
1276 DaemonDescription('mgr', 'b', 'host2'),
1277 DaemonDescription('mgr', 'c', 'host3'),
1281 [['mgr.a2', 'mgr.b', 'mgr.c']]
1283 ActiveAssignmentTest(
1285 PlacementSpec(count
=1),
1286 'host1 host2 host3'.split(),
1288 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1289 DaemonDescription('mgr', 'a2', 'host1', is_active
=True),
1290 DaemonDescription('mgr', 'b', 'host2'),
1291 DaemonDescription('mgr', 'c', 'host3'),
1295 [['mgr.a', 'mgr.b', 'mgr.c'], ['mgr.a2', 'mgr.b', 'mgr.c']]
1297 ActiveAssignmentTest(
1299 PlacementSpec(count
=2),
1300 'host1 host2 host3'.split(),
1302 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1303 DaemonDescription('mgr', 'a2', 'host1'),
1304 DaemonDescription('mgr', 'b', 'host2'),
1305 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1307 [['host1', 'host3']],
1309 [['mgr.a2', 'mgr.b']]
1311 # Explicit placement should override preference for active daemon
1312 ActiveAssignmentTest(
1314 PlacementSpec(count
=1, hosts
=['host1']),
1315 'host1 host2 host3'.split(),
1317 DaemonDescription('mgr', 'a', 'host1'),
1318 DaemonDescription('mgr', 'b', 'host2'),
1319 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1323 [['mgr.b', 'mgr.c']]
1327 def test_active_assignment(service_type
, placement
, hosts
, daemons
, expected
, expected_add
, expected_remove
):
1329 spec
= ServiceSpec(service_type
=service_type
,
1331 placement
=placement
)
1333 hosts
, to_add
, to_remove
= HostAssignment(
1335 hosts
=[HostSpec(h
) for h
in hosts
],
1338 assert sorted([h
.hostname
for h
in hosts
]) in expected
1339 assert sorted([h
.hostname
for h
in to_add
]) in expected_add
1340 assert sorted([h
.name() for h
in to_remove
]) in expected_remove