1 # Disable autopep8 for this file:
5 from typing
import NamedTuple
, List
, Dict
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
]
383 expected_add
: List
[str]
384 expected_remove
: List
[DaemonDescription
]
387 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected,expected_add,expected_remove",
392 PlacementSpec(hosts
=['smithi060']),
395 ['mgr:smithi060'], ['mgr:smithi060'], []
400 PlacementSpec(host_pattern
='*'),
401 'host1 host2 host3'.split(),
403 DaemonDescription('mgr', 'a', 'host1'),
404 DaemonDescription('mgr', 'b', 'host2'),
406 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
410 # all_hosts + count_per_host
413 PlacementSpec(host_pattern
='*', count_per_host
=2),
414 'host1 host2 host3'.split(),
416 DaemonDescription('mds', 'a', 'host1'),
417 DaemonDescription('mds', 'b', 'host2'),
419 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
420 ['mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
423 # count that is bigger than the amount of hosts. Truncate to len(hosts)
424 # mgr should not be co-located to each other.
427 PlacementSpec(count
=4),
428 'host1 host2 host3'.split(),
430 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
431 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
434 # count that is bigger than the amount of hosts; wrap around.
437 PlacementSpec(count
=6),
438 'host1 host2 host3'.split(),
440 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
441 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
444 # count + partial host list
447 PlacementSpec(count
=3, hosts
=['host3']),
448 'host1 host2 host3'.split(),
450 DaemonDescription('mgr', 'a', 'host1'),
451 DaemonDescription('mgr', 'b', 'host2'),
457 # count + partial host list (with colo)
460 PlacementSpec(count
=3, hosts
=['host3']),
461 'host1 host2 host3'.split(),
463 DaemonDescription('mds', 'a', 'host1'),
464 DaemonDescription('mds', 'b', 'host2'),
466 ['mds:host3', 'mds:host3', 'mds:host3'],
467 ['mds:host3', 'mds:host3', 'mds:host3'],
470 # count 1 + partial host list
473 PlacementSpec(count
=1, hosts
=['host3']),
474 'host1 host2 host3'.split(),
476 DaemonDescription('mgr', 'a', 'host1'),
477 DaemonDescription('mgr', 'b', 'host2'),
483 # count + partial host list + existing
486 PlacementSpec(count
=2, hosts
=['host3']),
487 'host1 host2 host3'.split(),
489 DaemonDescription('mgr', 'a', 'host1'),
495 # count + partial host list + existing (deterministic)
498 PlacementSpec(count
=2, hosts
=['host1']),
499 'host1 host2'.split(),
501 DaemonDescription('mgr', 'a', 'host1'),
507 # count + partial host list + existing (deterministic)
510 PlacementSpec(count
=2, hosts
=['host1']),
511 'host1 host2'.split(),
513 DaemonDescription('mgr', 'a', 'host2'),
522 PlacementSpec(label
='foo'),
523 'host1 host2 host3'.split(),
525 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
526 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
529 # label + count (truncate to host list)
532 PlacementSpec(count
=4, label
='foo'),
533 'host1 host2 host3'.split(),
535 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
536 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
539 # label + count (with colo)
542 PlacementSpec(count
=6, label
='foo'),
543 'host1 host2 host3'.split(),
545 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
546 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
549 # label only + count_per_hst
552 PlacementSpec(label
='foo', count_per_host
=3),
553 'host1 host2 host3'.split(),
555 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3',
556 'mds:host1', 'mds:host2', 'mds:host3'],
557 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3',
558 'mds:host1', 'mds:host2', 'mds:host3'],
564 PlacementSpec(host_pattern
='mgr*'),
565 'mgrhost1 mgrhost2 datahost'.split(),
567 ['mgr:mgrhost1', 'mgr:mgrhost2'],
568 ['mgr:mgrhost1', 'mgr:mgrhost2'],
571 # host_pattern + count_per_host
574 PlacementSpec(host_pattern
='mds*', count_per_host
=3),
575 'mdshost1 mdshost2 datahost'.split(),
577 ['mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2'],
578 ['mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2'],
581 # label + count_per_host + ports
584 PlacementSpec(count
=6, label
='foo'),
585 'host1 host2 host3'.split(),
587 ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)',
588 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'],
589 ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)',
590 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'],
593 # label + count_per_host + ports (+ xisting)
596 PlacementSpec(count
=6, label
='foo'),
597 'host1 host2 host3'.split(),
599 DaemonDescription('rgw', 'a', 'host1', ports
=[81]),
600 DaemonDescription('rgw', 'b', 'host2', ports
=[80]),
601 DaemonDescription('rgw', 'c', 'host1', ports
=[82]),
603 ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)',
604 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'],
605 ['rgw:host1(*:80)', 'rgw:host3(*:80)',
606 'rgw:host2(*:81)', 'rgw:host3(*:81)'],
609 # cephadm.py teuth case
612 PlacementSpec(count
=3, hosts
=['host1=y', 'host2=x']),
613 'host1 host2'.split(),
615 DaemonDescription('mgr', 'y', 'host1'),
616 DaemonDescription('mgr', 'x', 'host2'),
618 ['mgr:host1(name=y)', 'mgr:host2(name=x)'],
622 def test_node_assignment(service_type
, placement
, hosts
, daemons
,
623 expected
, expected_add
, expected_remove
):
626 if service_type
== 'rgw':
627 service_id
= 'realm.zone'
629 elif service_type
== 'mds':
633 spec
= ServiceSpec(service_type
=service_type
,
634 service_id
=service_id
,
637 all_slots
, to_add
, to_remove
= HostAssignment(
639 hosts
=[HostSpec(h
, labels
=['foo']) for h
in hosts
],
641 allow_colo
=allow_colo
,
644 got
= [str(p
) for p
in all_slots
]
652 assert num_wildcard
== len(got
)
654 got
= [str(p
) for p
in to_add
]
656 for i
in expected_add
:
662 assert num_wildcard
== len(got
)
664 assert sorted([d
.name() for d
in to_remove
]) == sorted(expected_remove
)
667 class NodeAssignmentTest2(NamedTuple
):
669 placement
: PlacementSpec
671 daemons
: List
[DaemonDescription
]
676 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected_len,in_set",
681 PlacementSpec(count
=1),
682 'host1 host2 host3'.split(),
685 ['host1', 'host2', 'host3'],
688 # hosts + (smaller) count
691 PlacementSpec(count
=1, hosts
='host1 host2'.split()),
692 'host1 host2'.split(),
697 # hosts + (smaller) count, existing
700 PlacementSpec(count
=1, hosts
='host1 host2 host3'.split()),
701 'host1 host2 host3'.split(),
702 [DaemonDescription('mgr', 'mgr.a', 'host1')],
704 ['host1', 'host2', 'host3'],
706 # hosts + (smaller) count, (more) existing
709 PlacementSpec(count
=1, hosts
='host1 host2 host3'.split()),
710 'host1 host2 host3'.split(),
712 DaemonDescription('mgr', 'a', 'host1'),
713 DaemonDescription('mgr', 'b', 'host2'),
718 # count + partial host list
721 PlacementSpec(count
=2, hosts
=['host3']),
722 'host1 host2 host3'.split(),
725 ['host1', 'host2', 'host3']
730 PlacementSpec(count
=1, label
='foo'),
731 'host1 host2 host3'.split(),
734 ['host1', 'host2', 'host3']
737 def test_node_assignment2(service_type
, placement
, hosts
,
738 daemons
, expected_len
, in_set
):
739 hosts
, to_add
, to_remove
= HostAssignment(
740 spec
=ServiceSpec(service_type
, placement
=placement
),
741 hosts
=[HostSpec(h
, labels
=['foo']) for h
in hosts
],
744 assert len(hosts
) == expected_len
745 for h
in [h
.hostname
for h
in hosts
]:
749 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected_len,must_have",
751 # hosts + (smaller) count, (more) existing
754 PlacementSpec(count
=3, hosts
='host3'.split()),
755 'host1 host2 host3'.split(),
760 # count + partial host list
763 PlacementSpec(count
=2, hosts
=['host3']),
764 'host1 host2 host3'.split(),
770 def test_node_assignment3(service_type
, placement
, hosts
,
771 daemons
, expected_len
, must_have
):
772 hosts
, to_add
, to_remove
= HostAssignment(
773 spec
=ServiceSpec(service_type
, placement
=placement
),
774 hosts
=[HostSpec(h
) for h
in hosts
],
777 assert len(hosts
) == expected_len
779 assert h
in [h
.hostname
for h
in hosts
]
782 class NodeAssignmentTest4(NamedTuple
):
784 networks
: Dict
[str, Dict
[str, Dict
[str, List
[str]]]]
785 daemons
: List
[DaemonDescription
]
787 expected_add
: List
[str]
788 expected_remove
: List
[DaemonDescription
]
791 @pytest.mark
.parametrize("spec,networks,daemons,expected,expected_add,expected_remove",
797 placement
=PlacementSpec(count
=6, label
='foo'),
798 networks
=['10.0.0.0/8'],
801 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}},
802 'host2': {'10.0.0.0/8': {'eth0': ['10.0.0.2']}},
803 'host3': {'192.168.0.0/16': {'eth0': ['192.168.0.1']}},
806 ['rgw:host1(10.0.0.1:80)', 'rgw:host2(10.0.0.2:80)',
807 'rgw:host1(10.0.0.1:81)', 'rgw:host2(10.0.0.2:81)',
808 'rgw:host1(10.0.0.1:82)', 'rgw:host2(10.0.0.2:82)'],
809 ['rgw:host1(10.0.0.1:80)', 'rgw:host2(10.0.0.2:80)',
810 'rgw:host1(10.0.0.1:81)', 'rgw:host2(10.0.0.2:81)',
811 'rgw:host1(10.0.0.1:82)', 'rgw:host2(10.0.0.2:82)'],
816 service_type
='ingress',
817 service_id
='rgw.foo',
820 virtual_ip
='10.0.0.20/8',
821 backend_service
='rgw.foo',
822 placement
=PlacementSpec(label
='foo'),
823 networks
=['10.0.0.0/8'],
826 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}},
827 'host2': {'10.0.0.0/8': {'eth1': ['10.0.0.2']}},
828 'host3': {'192.168.0.0/16': {'eth2': ['192.168.0.1']}},
831 ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)',
832 'keepalived:host1', 'keepalived:host2'],
833 ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)',
834 'keepalived:host1', 'keepalived:host2'],
839 service_type
='ingress',
840 service_id
='rgw.foo',
843 virtual_ip
='10.0.0.20/8',
844 backend_service
='rgw.foo',
845 placement
=PlacementSpec(label
='foo'),
846 networks
=['10.0.0.0/8'],
849 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}},
850 'host2': {'10.0.0.0/8': {'eth1': ['10.0.0.2']}},
851 'host3': {'192.168.0.0/16': {'eth2': ['192.168.0.1']}},
854 DaemonDescription('haproxy', 'a', 'host1', ip
='10.0.0.1',
856 DaemonDescription('keepalived', 'b', 'host2'),
857 DaemonDescription('keepalived', 'c', 'host3'),
859 ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)',
860 'keepalived:host1', 'keepalived:host2'],
861 ['haproxy:host2(10.0.0.2:443,8888)',
866 def test_node_assignment4(spec
, networks
, daemons
,
867 expected
, expected_add
, expected_remove
):
868 all_slots
, to_add
, to_remove
= HostAssignment(
870 hosts
=[HostSpec(h
, labels
=['foo']) for h
in networks
.keys()],
874 primary_daemon_type
='haproxy' if spec
.service_type
== 'ingress' else spec
.service_type
,
875 per_host_daemon_type
='keepalived' if spec
.service_type
== 'ingress' else None,
878 got
= [str(p
) for p
in all_slots
]
886 assert num_wildcard
== len(got
)
888 got
= [str(p
) for p
in to_add
]
890 for i
in expected_add
:
896 assert num_wildcard
== len(got
)
898 assert sorted([d
.name() for d
in to_remove
]) == sorted(expected_remove
)
901 @pytest.mark
.parametrize("placement",
906 ('hostname12hostname12hostname12hostname12hostname12hostname12hostname12'), # > 63 chars
908 def test_bad_placements(placement
):
910 PlacementSpec
.from_string(placement
.split(' '))
912 except SpecValidationError
:
916 class NodeAssignmentTestBadSpec(NamedTuple
):
918 placement
: PlacementSpec
920 daemons
: List
[DaemonDescription
]
924 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected",
927 NodeAssignmentTestBadSpec(
929 PlacementSpec(hosts
=['unknownhost']),
932 "Cannot place <ServiceSpec for service_name=mgr> on unknownhost: Unknown hosts"
934 # unknown host pattern
935 NodeAssignmentTestBadSpec(
937 PlacementSpec(host_pattern
='unknownhost'),
940 "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts"
943 NodeAssignmentTestBadSpec(
945 PlacementSpec(label
='unknownlabel'),
948 "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts for label unknownlabel"
951 def test_bad_specs(service_type
, placement
, hosts
, daemons
, expected
):
952 with pytest
.raises(OrchestratorValidationError
) as e
:
953 hosts
, to_add
, to_remove
= HostAssignment(
954 spec
=ServiceSpec(service_type
, placement
=placement
),
955 hosts
=[HostSpec(h
) for h
in hosts
],
958 assert str(e
.value
) == expected
961 class ActiveAssignmentTest(NamedTuple
):
963 placement
: PlacementSpec
965 daemons
: List
[DaemonDescription
]
966 expected
: List
[List
[str]]
967 expected_add
: List
[List
[str]]
968 expected_remove
: List
[List
[str]]
971 @pytest.mark
.parametrize("service_type,placement,hosts,daemons,expected,expected_add,expected_remove",
973 ActiveAssignmentTest(
975 PlacementSpec(count
=2),
976 'host1 host2 host3'.split(),
978 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
979 DaemonDescription('mgr', 'b', 'host2'),
980 DaemonDescription('mgr', 'c', 'host3'),
982 [['host1', 'host2'], ['host1', 'host3']],
984 [['mgr.b'], ['mgr.c']]
986 ActiveAssignmentTest(
988 PlacementSpec(count
=2),
989 'host1 host2 host3'.split(),
991 DaemonDescription('mgr', 'a', 'host1'),
992 DaemonDescription('mgr', 'b', 'host2'),
993 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
995 [['host1', 'host3'], ['host2', 'host3']],
997 [['mgr.a'], ['mgr.b']]
999 ActiveAssignmentTest(
1001 PlacementSpec(count
=1),
1002 'host1 host2 host3'.split(),
1004 DaemonDescription('mgr', 'a', 'host1'),
1005 DaemonDescription('mgr', 'b', 'host2', is_active
=True),
1006 DaemonDescription('mgr', 'c', 'host3'),
1010 [['mgr.a', 'mgr.c']]
1012 ActiveAssignmentTest(
1014 PlacementSpec(count
=1),
1015 'host1 host2 host3'.split(),
1017 DaemonDescription('mgr', 'a', 'host1'),
1018 DaemonDescription('mgr', 'b', 'host2'),
1019 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1023 [['mgr.a', 'mgr.b']]
1025 ActiveAssignmentTest(
1027 PlacementSpec(count
=1),
1028 'host1 host2 host3'.split(),
1030 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1031 DaemonDescription('mgr', 'b', 'host2'),
1032 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1034 [['host1'], ['host3']],
1036 [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c']]
1038 ActiveAssignmentTest(
1040 PlacementSpec(count
=2),
1041 'host1 host2 host3'.split(),
1043 DaemonDescription('mgr', 'a', 'host1'),
1044 DaemonDescription('mgr', 'b', 'host2', is_active
=True),
1045 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1047 [['host2', 'host3']],
1051 ActiveAssignmentTest(
1053 PlacementSpec(count
=1),
1054 'host1 host2 host3'.split(),
1056 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1057 DaemonDescription('mgr', 'b', 'host2', is_active
=True),
1058 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1060 [['host1'], ['host2'], ['host3']],
1062 [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c'], ['mgr.a', 'mgr.c']]
1064 ActiveAssignmentTest(
1066 PlacementSpec(count
=1),
1067 'host1 host2 host3'.split(),
1069 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1070 DaemonDescription('mgr', 'a2', 'host1'),
1071 DaemonDescription('mgr', 'b', 'host2'),
1072 DaemonDescription('mgr', 'c', 'host3'),
1076 [['mgr.a2', 'mgr.b', 'mgr.c']]
1078 ActiveAssignmentTest(
1080 PlacementSpec(count
=1),
1081 'host1 host2 host3'.split(),
1083 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1084 DaemonDescription('mgr', 'a2', 'host1', is_active
=True),
1085 DaemonDescription('mgr', 'b', 'host2'),
1086 DaemonDescription('mgr', 'c', 'host3'),
1090 [['mgr.a', 'mgr.b', 'mgr.c'], ['mgr.a2', 'mgr.b', 'mgr.c']]
1092 ActiveAssignmentTest(
1094 PlacementSpec(count
=2),
1095 'host1 host2 host3'.split(),
1097 DaemonDescription('mgr', 'a', 'host1', is_active
=True),
1098 DaemonDescription('mgr', 'a2', 'host1'),
1099 DaemonDescription('mgr', 'b', 'host2'),
1100 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1102 [['host1', 'host3']],
1104 [['mgr.a2', 'mgr.b']]
1106 # Explicit placement should override preference for active daemon
1107 ActiveAssignmentTest(
1109 PlacementSpec(count
=1, hosts
=['host1']),
1110 'host1 host2 host3'.split(),
1112 DaemonDescription('mgr', 'a', 'host1'),
1113 DaemonDescription('mgr', 'b', 'host2'),
1114 DaemonDescription('mgr', 'c', 'host3', is_active
=True),
1118 [['mgr.b', 'mgr.c']]
1122 def test_active_assignment(service_type
, placement
, hosts
, daemons
, expected
, expected_add
, expected_remove
):
1124 spec
= ServiceSpec(service_type
=service_type
,
1126 placement
=placement
)
1128 hosts
, to_add
, to_remove
= HostAssignment(
1130 hosts
=[HostSpec(h
) for h
in hosts
],
1133 assert sorted([h
.hostname
for h
in hosts
]) in expected
1134 assert sorted([h
.hostname
for h
in to_add
]) in expected_add
1135 assert sorted([h
.name() for h
in to_remove
]) in expected_remove