]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/cephadm/tests/test_scheduling.py
bump version to 15.2.11-pve1
[ceph.git] / ceph / src / pybind / mgr / cephadm / tests / test_scheduling.py
CommitLineData
f91f0fd5
TL
1# Disable autopep8 for this file:
2
3# fmt: off
4
9f95a23c
TL
5from typing import NamedTuple, List
6import pytest
7
f6b5b4d7 8from ceph.deployment.hostspec import HostSpec
9f95a23c
TL
9from ceph.deployment.service_spec import ServiceSpec, PlacementSpec, ServiceSpecValidationError
10
11from cephadm.module import HostAssignment
f6b5b4d7
TL
12from orchestrator import DaemonDescription, OrchestratorValidationError, OrchestratorError, HostSpec
13
14
15def wrapper(func):
16 # some odd thingy to revert the order or arguments
17 def inner(*args):
18 def inner2(expected):
19 func(expected, *args)
20 return inner2
21 return inner
22
23
24@wrapper
25def none(expected):
26 assert expected == []
27
28
29@wrapper
30def one_of(expected, *hosts):
31 if not isinstance(expected, list):
32 assert False, str(expected)
33 assert len(expected) == 1, f'one_of failed len({expected}) != 1'
34 assert expected[0] in hosts
35
36
37@wrapper
38def two_of(expected, *hosts):
39 if not isinstance(expected, list):
40 assert False, str(expected)
41 assert len(expected) == 2, f'one_of failed len({expected}) != 2'
42 matches = 0
43 for h in hosts:
44 matches += int(h in expected)
45 if matches != 2:
46 assert False, f'two of {hosts} not in {expected}'
47
48
49@wrapper
50def exactly(expected, *hosts):
51 assert expected == list(hosts)
52
53
54@wrapper
55def error(expected, kind, match):
56 assert isinstance(expected, kind), (str(expected), match)
57 assert str(expected) == match, (str(expected), match)
58
59
60@wrapper
61def _or(expected, *inners):
62 def catch(inner):
63 try:
64 inner(expected)
65 except AssertionError as e:
66 return e
67 result = [catch(i) for i in inners]
68 if None not in result:
69 assert False, f"_or failed: {expected}"
70
71
72def _always_true(_): pass
73
74
75def k(s):
76 return [e for e in s.split(' ') if e]
77
78
79def get_result(key, results):
80 def match(one):
81 for o, k in zip(one, key):
82 if o != k and o != '*':
83 return False
84 return True
85 return [v for k, v in results
86 if match(k)][0]
87
f6b5b4d7 88
f91f0fd5 89def mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count):
f6b5b4d7
TL
90
91 if spec_section == 'hosts':
f91f0fd5 92 mk_spec = lambda: ServiceSpec('mgr', placement=PlacementSpec(
f6b5b4d7
TL
93 hosts=explicit,
94 count=count,
95 ))
f6b5b4d7 96 elif spec_section == 'label':
f91f0fd5 97 mk_spec = lambda: ServiceSpec('mgr', placement=PlacementSpec(
f6b5b4d7
TL
98 label='mylabel',
99 count=count,
100 ))
f6b5b4d7
TL
101 elif spec_section == 'host_pattern':
102 pattern = {
103 'e': 'notfound',
104 '1': '1',
105 '12': '[1-2]',
106 '123': '*',
107 }[explicit_key]
f91f0fd5 108 mk_spec = lambda: ServiceSpec('mgr', placement=PlacementSpec(
f6b5b4d7
TL
109 host_pattern=pattern,
110 count=count,
111 ))
f6b5b4d7
TL
112 else:
113 assert False
f6b5b4d7 114
f91f0fd5
TL
115 hosts = [
116 HostSpec(h, labels=['mylabel']) if h in explicit else HostSpec(h)
117 for h in hosts
118 ]
119
120 return mk_spec, hosts
f6b5b4d7
TL
121
122
f91f0fd5 123def run_scheduler_test(results, mk_spec, hosts, get_daemons_func, key_elems):
f6b5b4d7
TL
124 key = ' '.join('N' if e is None else str(e) for e in key_elems)
125 try:
126 assert_res = get_result(k(key), results)
127 except IndexError:
128 try:
129 spec = mk_spec()
130 host_res = HostAssignment(
131 spec=spec,
f91f0fd5 132 hosts=hosts,
f6b5b4d7
TL
133 get_daemons_func=get_daemons_func).place()
134 if isinstance(host_res, list):
135 e = ', '.join(repr(h.hostname) for h in host_res)
136 assert False, f'`(k("{key}"), exactly({e})),` not found'
137 assert False, f'`(k("{key}"), ...),` not found'
138 except OrchestratorError as e:
139 assert False, f'`(k("{key}"), error({type(e).__name__}, {repr(str(e))})),` not found'
140
141 for _ in range(10): # scheduler has a random component
142 try:
143 spec = mk_spec()
144 host_res = HostAssignment(
145 spec=spec,
f91f0fd5 146 hosts=hosts,
f6b5b4d7
TL
147 get_daemons_func=get_daemons_func).place()
148
149 assert_res(sorted([h.hostname for h in host_res]))
150 except Exception as e:
151 assert_res(e)
152
153
154# * first match from the top wins
155# * where e=[], *=any
156#
157# + list of known hosts available for scheduling (host_key)
158# | + hosts used for explict placement (explicit_key)
159# | | + count
160# | | | + section (host, label, pattern)
161# | | | | + expected result
162# | | | | |
163test_explicit_scheduler_results = [
164 (k("* * 0 *"), error(ServiceSpecValidationError, 'num/count must be > 1')),
f91f0fd5
TL
165 (k("* e N l"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr>: No matching hosts for label mylabel')),
166 (k("* e N p"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr>: No matching hosts')),
f6b5b4d7
TL
167 (k("* e N h"), error(OrchestratorValidationError, 'placement spec is empty: no hosts, no label, no pattern, no count')),
168 (k("* e * *"), none),
f91f0fd5
TL
169 (k("1 12 * h"), error(OrchestratorValidationError, "Cannot place <ServiceSpec for service_name=mgr> on 2: Unknown hosts")),
170 (k("1 123 * h"), error(OrchestratorValidationError, "Cannot place <ServiceSpec for service_name=mgr> on 2, 3: Unknown hosts")),
f6b5b4d7
TL
171 (k("1 * * *"), exactly('1')),
172 (k("12 1 * *"), exactly('1')),
173 (k("12 12 1 *"), one_of('1', '2')),
174 (k("12 12 * *"), exactly('1', '2')),
f91f0fd5 175 (k("12 123 * h"), error(OrchestratorValidationError, "Cannot place <ServiceSpec for service_name=mgr> on 3: Unknown hosts")),
f6b5b4d7
TL
176 (k("12 123 1 *"), one_of('1', '2', '3')),
177 (k("12 123 * *"), two_of('1', '2', '3')),
178 (k("123 1 * *"), exactly('1')),
179 (k("123 12 1 *"), one_of('1', '2')),
180 (k("123 12 * *"), exactly('1', '2')),
181 (k("123 123 1 *"), one_of('1', '2', '3')),
182 (k("123 123 2 *"), two_of('1', '2', '3')),
183 (k("123 123 * *"), exactly('1', '2', '3')),
184]
185
186@pytest.mark.parametrize("spec_section_key,spec_section",
187 [
188 ('h', 'hosts'),
189 ('l', 'label'),
190 ('p', 'host_pattern'),
191 ])
192@pytest.mark.parametrize("count",
193 [
194 None,
195 0,
196 1,
197 2,
198 3,
199 ])
200@pytest.mark.parametrize("explicit_key, explicit",
201 [
202 ('e', []),
203 ('1', ['1']),
204 ('12', ['1', '2']),
205 ('123', ['1', '2', '3']),
206 ])
207@pytest.mark.parametrize("host_key, hosts",
208 [
209 ('1', ['1']),
210 ('12', ['1', '2']),
211 ('123', ['1', '2', '3']),
212 ])
213def test_explicit_scheduler(host_key, hosts,
214 explicit_key, explicit,
215 count,
216 spec_section_key, spec_section):
217
f91f0fd5 218 mk_spec, hosts = mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count)
f6b5b4d7
TL
219 run_scheduler_test(
220 results=test_explicit_scheduler_results,
221 mk_spec=mk_spec,
f91f0fd5 222 hosts=hosts,
f6b5b4d7
TL
223 get_daemons_func=lambda _: [],
224 key_elems=(host_key, explicit_key, count, spec_section_key)
225 )
226
227
228# * first match from the top wins
229# * where e=[], *=any
230#
231# + list of known hosts available for scheduling (host_key)
232# | + hosts used for explict placement (explicit_key)
233# | | + count
234# | | | + existing daemons
235# | | | | + section (host, label, pattern)
236# | | | | | + expected result
237# | | | | | |
238test_scheduler_daemons_results = [
239 (k("* 1 * * *"), exactly('1')),
f91f0fd5 240 (k("1 123 * * h"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr> on 2, 3: Unknown hosts')),
f6b5b4d7 241 (k("1 123 * * *"), exactly('1')),
f91f0fd5 242 (k("12 123 * * h"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr> on 3: Unknown hosts')),
f6b5b4d7
TL
243 (k("12 123 N * *"), exactly('1', '2')),
244 (k("12 123 1 * *"), one_of('1', '2')),
245 (k("12 123 2 * *"), exactly('1', '2')),
246 (k("12 123 3 * *"), exactly('1', '2')),
247 (k("123 123 N * *"), exactly('1', '2', '3')),
248 (k("123 123 1 e *"), one_of('1', '2', '3')),
249 (k("123 123 1 1 *"), exactly('1')),
250 (k("123 123 1 3 *"), exactly('3')),
251 (k("123 123 1 12 *"), one_of('1', '2')),
252 (k("123 123 1 112 *"), one_of('1', '2')),
253 (k("123 123 1 23 *"), one_of('2', '3')),
254 (k("123 123 1 123 *"), one_of('1', '2', '3')),
255 (k("123 123 2 e *"), two_of('1', '2', '3')),
256 (k("123 123 2 1 *"), _or(exactly('1', '2'), exactly('1', '3'))),
257 (k("123 123 2 3 *"), _or(exactly('1', '3'), exactly('2', '3'))),
258 (k("123 123 2 12 *"), exactly('1', '2')),
259 (k("123 123 2 112 *"), exactly('1', '2')),
260 (k("123 123 2 23 *"), exactly('2', '3')),
261 (k("123 123 2 123 *"), two_of('1', '2', '3')),
262 (k("123 123 3 * *"), exactly('1', '2', '3')),
263]
264
265
266@pytest.mark.parametrize("spec_section_key,spec_section",
267 [
268 ('h', 'hosts'),
269 ('l', 'label'),
270 ('p', 'host_pattern'),
271 ])
272@pytest.mark.parametrize("daemons_key, daemons",
273 [
274 ('e', []),
275 ('1', ['1']),
276 ('3', ['3']),
277 ('12', ['1', '2']),
278 ('112', ['1', '1', '2']), # deal with existing co-located daemons
279 ('23', ['2', '3']),
280 ('123', ['1', '2', '3']),
281 ])
282@pytest.mark.parametrize("count",
283 [
284 None,
285 1,
286 2,
287 3,
288 ])
289@pytest.mark.parametrize("explicit_key, explicit",
290 [
291 ('1', ['1']),
292 ('123', ['1', '2', '3']),
293 ])
294@pytest.mark.parametrize("host_key, hosts",
295 [
296 ('1', ['1']),
297 ('12', ['1', '2']),
298 ('123', ['1', '2', '3']),
299 ])
300def test_scheduler_daemons(host_key, hosts,
301 explicit_key, explicit,
302 count,
303 daemons_key, daemons,
304 spec_section_key, spec_section):
f91f0fd5 305 mk_spec, hosts = mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count)
f6b5b4d7 306 dds = [
f91f0fd5 307 DaemonDescription('mgr', d, d)
f6b5b4d7
TL
308 for d in daemons
309 ]
310 run_scheduler_test(
311 results=test_scheduler_daemons_results,
312 mk_spec=mk_spec,
f91f0fd5 313 hosts=hosts,
f6b5b4d7
TL
314 get_daemons_func=lambda _: dds,
315 key_elems=(host_key, explicit_key, count, daemons_key, spec_section_key)
316 )
317
318
f91f0fd5 319# =========================
9f95a23c
TL
320
321
322class NodeAssignmentTest(NamedTuple):
323 service_type: str
324 placement: PlacementSpec
325 hosts: List[str]
326 daemons: List[DaemonDescription]
327 expected: List[str]
328
329@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected",
330 [
331 # just hosts
332 NodeAssignmentTest(
f91f0fd5 333 'mgr',
9f95a23c
TL
334 PlacementSpec(hosts=['smithi060:[v2:172.21.15.60:3301,v1:172.21.15.60:6790]=c']),
335 ['smithi060'],
336 [],
337 ['smithi060']
338 ),
339 # all_hosts
340 NodeAssignmentTest(
f91f0fd5 341 'mgr',
9f95a23c
TL
342 PlacementSpec(host_pattern='*'),
343 'host1 host2 host3'.split(),
344 [
f91f0fd5
TL
345 DaemonDescription('mgr', 'a', 'host1'),
346 DaemonDescription('mgr', 'b', 'host2'),
9f95a23c
TL
347 ],
348 ['host1', 'host2', 'host3']
349 ),
350 # count that is bigger than the amount of hosts. Truncate to len(hosts)
351 # RGWs should not be co-located to each other.
352 NodeAssignmentTest(
353 'rgw',
354 PlacementSpec(count=4),
355 'host1 host2 host3'.split(),
356 [],
357 ['host1', 'host2', 'host3']
358 ),
359 # count + partial host list
360 NodeAssignmentTest(
f91f0fd5 361 'mgr',
9f95a23c
TL
362 PlacementSpec(count=3, hosts=['host3']),
363 'host1 host2 host3'.split(),
364 [
f91f0fd5
TL
365 DaemonDescription('mgr', 'a', 'host1'),
366 DaemonDescription('mgr', 'b', 'host2'),
9f95a23c 367 ],
f6b5b4d7 368 ['host3']
9f95a23c
TL
369 ),
370 # count 1 + partial host list
371 NodeAssignmentTest(
f91f0fd5 372 'mgr',
9f95a23c
TL
373 PlacementSpec(count=1, hosts=['host3']),
374 'host1 host2 host3'.split(),
375 [
f91f0fd5
TL
376 DaemonDescription('mgr', 'a', 'host1'),
377 DaemonDescription('mgr', 'b', 'host2'),
9f95a23c
TL
378 ],
379 ['host3']
380 ),
381 # count + partial host list + existing
382 NodeAssignmentTest(
f91f0fd5 383 'mgr',
9f95a23c
TL
384 PlacementSpec(count=2, hosts=['host3']),
385 'host1 host2 host3'.split(),
386 [
f91f0fd5 387 DaemonDescription('mgr', 'a', 'host1'),
9f95a23c 388 ],
f6b5b4d7 389 ['host3']
9f95a23c
TL
390 ),
391 # count + partial host list + existing (deterministic)
392 NodeAssignmentTest(
f91f0fd5 393 'mgr',
9f95a23c
TL
394 PlacementSpec(count=2, hosts=['host1']),
395 'host1 host2'.split(),
396 [
f91f0fd5 397 DaemonDescription('mgr', 'a', 'host1'),
9f95a23c 398 ],
f6b5b4d7 399 ['host1']
9f95a23c
TL
400 ),
401 # count + partial host list + existing (deterministic)
402 NodeAssignmentTest(
f91f0fd5 403 'mgr',
9f95a23c
TL
404 PlacementSpec(count=2, hosts=['host1']),
405 'host1 host2'.split(),
406 [
f91f0fd5 407 DaemonDescription('mgr', 'a', 'host2'),
9f95a23c 408 ],
f6b5b4d7 409 ['host1']
9f95a23c
TL
410 ),
411 # label only
412 NodeAssignmentTest(
f91f0fd5 413 'mgr',
9f95a23c
TL
414 PlacementSpec(label='foo'),
415 'host1 host2 host3'.split(),
416 [],
417 ['host1', 'host2', 'host3']
418 ),
419 # host_pattern
420 NodeAssignmentTest(
f91f0fd5
TL
421 'mgr',
422 PlacementSpec(host_pattern='mgr*'),
423 'mgrhost1 mgrhost2 datahost'.split(),
9f95a23c 424 [],
f91f0fd5 425 ['mgrhost1', 'mgrhost2']
9f95a23c
TL
426 ),
427 ])
428def test_node_assignment(service_type, placement, hosts, daemons, expected):
f6b5b4d7
TL
429 service_id = None
430 if service_type == 'rgw':
431 service_id = 'realm.zone'
432
433 spec = ServiceSpec(service_type=service_type,
434 service_id=service_id,
435 placement=placement)
436
9f95a23c 437 hosts = HostAssignment(
f6b5b4d7 438 spec=spec,
f91f0fd5 439 hosts=[HostSpec(h, labels=['foo']) for h in hosts],
9f95a23c
TL
440 get_daemons_func=lambda _: daemons).place()
441 assert sorted([h.hostname for h in hosts]) == sorted(expected)
442
e306af50 443
9f95a23c
TL
444class NodeAssignmentTest2(NamedTuple):
445 service_type: str
446 placement: PlacementSpec
447 hosts: List[str]
448 daemons: List[DaemonDescription]
449 expected_len: int
450 in_set: List[str]
451
452@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected_len,in_set",
453 [
9f95a23c
TL
454 # just count
455 NodeAssignmentTest2(
f91f0fd5 456 'mgr',
9f95a23c
TL
457 PlacementSpec(count=1),
458 'host1 host2 host3'.split(),
459 [],
460 1,
461 ['host1', 'host2', 'host3'],
462 ),
463
464 # hosts + (smaller) count
465 NodeAssignmentTest2(
f91f0fd5 466 'mgr',
9f95a23c
TL
467 PlacementSpec(count=1, hosts='host1 host2'.split()),
468 'host1 host2'.split(),
469 [],
470 1,
471 ['host1', 'host2'],
472 ),
473 # hosts + (smaller) count, existing
474 NodeAssignmentTest2(
f91f0fd5 475 'mgr',
9f95a23c
TL
476 PlacementSpec(count=1, hosts='host1 host2 host3'.split()),
477 'host1 host2 host3'.split(),
f91f0fd5 478 [DaemonDescription('mgr', 'mgr.a', 'host1'),],
9f95a23c
TL
479 1,
480 ['host1', 'host2', 'host3'],
481 ),
482 # hosts + (smaller) count, (more) existing
483 NodeAssignmentTest2(
f91f0fd5 484 'mgr',
9f95a23c
TL
485 PlacementSpec(count=1, hosts='host1 host2 host3'.split()),
486 'host1 host2 host3'.split(),
487 [
f91f0fd5
TL
488 DaemonDescription('mgr', 'a', 'host1'),
489 DaemonDescription('mgr', 'b', 'host2'),
9f95a23c
TL
490 ],
491 1,
492 ['host1', 'host2']
493 ),
494 # count + partial host list
495 NodeAssignmentTest2(
f91f0fd5 496 'mgr',
9f95a23c
TL
497 PlacementSpec(count=2, hosts=['host3']),
498 'host1 host2 host3'.split(),
499 [],
f6b5b4d7 500 1,
9f95a23c
TL
501 ['host1', 'host2', 'host3']
502 ),
503 # label + count
504 NodeAssignmentTest2(
f91f0fd5 505 'mgr',
9f95a23c
TL
506 PlacementSpec(count=1, label='foo'),
507 'host1 host2 host3'.split(),
508 [],
509 1,
510 ['host1', 'host2', 'host3']
511 ),
512 ])
513def test_node_assignment2(service_type, placement, hosts,
514 daemons, expected_len, in_set):
515 hosts = HostAssignment(
516 spec=ServiceSpec(service_type, placement=placement),
f91f0fd5 517 hosts=[HostSpec(h, labels=['foo']) for h in hosts],
9f95a23c
TL
518 get_daemons_func=lambda _: daemons).place()
519 assert len(hosts) == expected_len
520 for h in [h.hostname for h in hosts]:
521 assert h in in_set
522
523@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected_len,must_have",
524 [
525 # hosts + (smaller) count, (more) existing
526 NodeAssignmentTest2(
f91f0fd5 527 'mgr',
9f95a23c
TL
528 PlacementSpec(count=3, hosts='host3'.split()),
529 'host1 host2 host3'.split(),
530 [],
f6b5b4d7 531 1,
9f95a23c
TL
532 ['host3']
533 ),
534 # count + partial host list
535 NodeAssignmentTest2(
f91f0fd5 536 'mgr',
9f95a23c
TL
537 PlacementSpec(count=2, hosts=['host3']),
538 'host1 host2 host3'.split(),
539 [],
f6b5b4d7 540 1,
9f95a23c
TL
541 ['host3']
542 ),
543 ])
544def test_node_assignment3(service_type, placement, hosts,
545 daemons, expected_len, must_have):
546 hosts = HostAssignment(
547 spec=ServiceSpec(service_type, placement=placement),
f91f0fd5 548 hosts=[HostSpec(h) for h in hosts],
9f95a23c
TL
549 get_daemons_func=lambda _: daemons).place()
550 assert len(hosts) == expected_len
551 for h in must_have:
552 assert h in [h.hostname for h in hosts]
553
554
555@pytest.mark.parametrize("placement",
556 [
557 ('1 *'),
558 ('* label:foo'),
559 ('* host1 host2'),
560 ('hostname12hostname12hostname12hostname12hostname12hostname12hostname12'), # > 63 chars
561 ])
562def test_bad_placements(placement):
563 try:
564 s = PlacementSpec.from_string(placement.split(' '))
565 assert False
566 except ServiceSpecValidationError as e:
567 pass
568
569
570class NodeAssignmentTestBadSpec(NamedTuple):
571 service_type: str
572 placement: PlacementSpec
573 hosts: List[str]
574 daemons: List[DaemonDescription]
575 expected: str
576@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected",
577 [
578 # unknown host
579 NodeAssignmentTestBadSpec(
f91f0fd5 580 'mgr',
9f95a23c
TL
581 PlacementSpec(hosts=['unknownhost']),
582 ['knownhost'],
583 [],
f91f0fd5 584 "Cannot place <ServiceSpec for service_name=mgr> on unknownhost: Unknown hosts"
9f95a23c
TL
585 ),
586 # unknown host pattern
587 NodeAssignmentTestBadSpec(
f91f0fd5 588 'mgr',
9f95a23c
TL
589 PlacementSpec(host_pattern='unknownhost'),
590 ['knownhost'],
591 [],
f91f0fd5 592 "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts"
9f95a23c
TL
593 ),
594 # unknown label
595 NodeAssignmentTestBadSpec(
f91f0fd5 596 'mgr',
9f95a23c
TL
597 PlacementSpec(label='unknownlabel'),
598 [],
599 [],
f91f0fd5 600 "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts for label unknownlabel"
9f95a23c
TL
601 ),
602 ])
603def test_bad_specs(service_type, placement, hosts, daemons, expected):
604 with pytest.raises(OrchestratorValidationError) as e:
605 hosts = HostAssignment(
606 spec=ServiceSpec(service_type, placement=placement),
f91f0fd5 607 hosts=[HostSpec(h) for h in hosts],
9f95a23c
TL
608 get_daemons_func=lambda _: daemons).place()
609 assert str(e.value) == expected
f6b5b4d7
TL
610
611class ActiveAssignmentTest(NamedTuple):
612 service_type: str
613 placement: PlacementSpec
614 hosts: List[str]
615 daemons: List[DaemonDescription]
616 expected: List[List[str]]
617
618
619@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected",
620 [
621 ActiveAssignmentTest(
622 'mgr',
623 PlacementSpec(count=2),
624 'host1 host2 host3'.split(),
625 [
626 DaemonDescription('mgr', 'a', 'host1', is_active=True),
627 DaemonDescription('mgr', 'b', 'host2'),
628 DaemonDescription('mgr', 'c', 'host3'),
629 ],
630 [['host1', 'host2'], ['host1', 'host3']]
631 ),
632 ActiveAssignmentTest(
633 'mgr',
634 PlacementSpec(count=2),
635 'host1 host2 host3'.split(),
636 [
637 DaemonDescription('mgr', 'a', 'host1'),
638 DaemonDescription('mgr', 'b', 'host2'),
639 DaemonDescription('mgr', 'c', 'host3', is_active=True),
640 ],
641 [['host1', 'host3'], ['host2', 'host3']]
642 ),
643 ActiveAssignmentTest(
644 'mgr',
645 PlacementSpec(count=1),
646 'host1 host2 host3'.split(),
647 [
648 DaemonDescription('mgr', 'a', 'host1'),
649 DaemonDescription('mgr', 'b', 'host2', is_active=True),
650 DaemonDescription('mgr', 'c', 'host3'),
651 ],
652 [['host2']]
653 ),
654 ActiveAssignmentTest(
655 'mgr',
656 PlacementSpec(count=1),
657 'host1 host2 host3'.split(),
658 [
659 DaemonDescription('mgr', 'a', 'host1'),
660 DaemonDescription('mgr', 'b', 'host2'),
661 DaemonDescription('mgr', 'c', 'host3', is_active=True),
662 ],
663 [['host3']]
664 ),
665 ActiveAssignmentTest(
666 'mgr',
667 PlacementSpec(count=1),
668 'host1 host2 host3'.split(),
669 [
670 DaemonDescription('mgr', 'a', 'host1', is_active=True),
671 DaemonDescription('mgr', 'b', 'host2'),
672 DaemonDescription('mgr', 'c', 'host3', is_active=True),
673 ],
674 [['host1'], ['host3']]
675 ),
676 ActiveAssignmentTest(
677 'mgr',
678 PlacementSpec(count=2),
679 'host1 host2 host3'.split(),
680 [
681 DaemonDescription('mgr', 'a', 'host1'),
682 DaemonDescription('mgr', 'b', 'host2', is_active=True),
683 DaemonDescription('mgr', 'c', 'host3', is_active=True),
684 ],
685 [['host2', 'host3']]
686 ),
687 ActiveAssignmentTest(
688 'mgr',
689 PlacementSpec(count=1),
690 'host1 host2 host3'.split(),
691 [
692 DaemonDescription('mgr', 'a', 'host1', is_active=True),
693 DaemonDescription('mgr', 'b', 'host2', is_active=True),
694 DaemonDescription('mgr', 'c', 'host3', is_active=True),
695 ],
696 [['host1'], ['host2'], ['host3']]
697 ),
698 ActiveAssignmentTest(
699 'mgr',
700 PlacementSpec(count=1),
701 'host1 host2 host3'.split(),
702 [
703 DaemonDescription('mgr', 'a', 'host1', is_active=True),
704 DaemonDescription('mgr', 'a2', 'host1'),
705 DaemonDescription('mgr', 'b', 'host2'),
706 DaemonDescription('mgr', 'c', 'host3'),
707 ],
708 [['host1']]
709 ),
710 ActiveAssignmentTest(
711 'mgr',
712 PlacementSpec(count=1),
713 'host1 host2 host3'.split(),
714 [
715 DaemonDescription('mgr', 'a', 'host1', is_active=True),
716 DaemonDescription('mgr', 'a2', 'host1', is_active=True),
717 DaemonDescription('mgr', 'b', 'host2'),
718 DaemonDescription('mgr', 'c', 'host3'),
719 ],
720 [['host1']]
721 ),
722 ActiveAssignmentTest(
723 'mgr',
724 PlacementSpec(count=2),
725 'host1 host2 host3'.split(),
726 [
727 DaemonDescription('mgr', 'a', 'host1', is_active=True),
728 DaemonDescription('mgr', 'a2', 'host1'),
729 DaemonDescription('mgr', 'b', 'host2'),
730 DaemonDescription('mgr', 'c', 'host3', is_active=True),
731 ],
732 [['host1', 'host3']]
733 ),
734 # Explicit placement should override preference for active daemon
735 ActiveAssignmentTest(
736 'mgr',
737 PlacementSpec(count=1, hosts=['host1']),
738 'host1 host2 host3'.split(),
739 [
740 DaemonDescription('mgr', 'a', 'host1'),
741 DaemonDescription('mgr', 'b', 'host2'),
742 DaemonDescription('mgr', 'c', 'host3', is_active=True),
743 ],
744 [['host1']]
745 ),
746
747 ])
748def test_active_assignment(service_type, placement, hosts, daemons, expected):
f6b5b4d7
TL
749
750 spec = ServiceSpec(service_type=service_type,
751 service_id=None,
752 placement=placement)
753
754 hosts = HostAssignment(
755 spec=spec,
f91f0fd5 756 hosts=[HostSpec(h) for h in hosts],
f6b5b4d7
TL
757 get_daemons_func=lambda _: daemons).place()
758 assert sorted([h.hostname for h in hosts]) in expected
f91f0fd5
TL
759
760class OddMonsTest(NamedTuple):
761 service_type: str
762 placement: PlacementSpec
763 hosts: List[str]
764 daemons: List[DaemonDescription]
765 expected_count: int
766
767
768@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected_count",
769 [
770 OddMonsTest(
771 'mon',
772 PlacementSpec(count=5),
773 'host1 host2 host3 host4'.split(),
774 [],
775 3
776 ),
777 OddMonsTest(
778 'mon',
779 PlacementSpec(count=4),
780 'host1 host2 host3 host4 host5'.split(),
781 [],
782 3
783 ),
784 OddMonsTest(
785 'mon',
786 PlacementSpec(count=5),
787 'host1 host2 host3 host4 host5'.split(),
788 [],
789 5
790 ),
791 OddMonsTest(
792 'mon',
793 PlacementSpec(hosts='host1 host2 host3 host4'.split()),
794 'host1 host2 host3 host4 host5'.split(),
795 [],
796 3
797 ),
798 OddMonsTest(
799 'mon',
800 PlacementSpec(hosts='host1 host2 host3 host4 host5'.split()),
801 'host1 host2 host3 host4 host5'.split(),
802 [],
803 5
804 ),
805 OddMonsTest(
806 'mon',
807 PlacementSpec(host_pattern='*'),
808 'host1 host2 host3 host4'.split(),
809 [],
810 3
811 ),
812 OddMonsTest(
813 'mon',
814 PlacementSpec(count=5, hosts='host1 host2 host3 host4'.split()),
815 'host1 host2 host3 host4 host5'.split(),
816 [],
817 3
818 ),
819 OddMonsTest(
820 'mon',
821 PlacementSpec(count=2, hosts='host1 host2 host3'.split()),
822 'host1 host2 host3 host4 host5'.split(),
823 [],
824 1
825 ),
826 OddMonsTest(
827 'mon',
828 PlacementSpec(count=5),
829 'host1 host2 host3 host4'.split(),
830 [
831 DaemonDescription('mon', 'a', 'host1'),
832 DaemonDescription('mon', 'b', 'host2'),
833 DaemonDescription('mon', 'c', 'host3'),
834 ],
835 3
836 ),
837 OddMonsTest(
838 'mon',
839 PlacementSpec(count=5),
840 'host1 host2 host3 host4'.split(),
841 [
842 DaemonDescription('mon', 'a', 'host1'),
843 DaemonDescription('mon', 'b', 'host2'),
844 ],
845 3
846 ),
847 OddMonsTest(
848 'mon',
849 PlacementSpec(hosts='host1 host2 host3 host4'.split()),
850 'host1 host2 host3 host4 host5'.split(),
851 [
852 DaemonDescription('mon', 'a', 'host1'),
853 DaemonDescription('mon', 'b', 'host2'),
854 DaemonDescription('mon', 'c', 'host3'),
855 ],
856 3
857 ),
858
859 ])
860def test_odd_mons(service_type, placement, hosts, daemons, expected_count):
861
862 spec = ServiceSpec(service_type=service_type,
863 service_id=None,
864 placement=placement)
865
866 hosts = HostAssignment(
867 spec=spec,
868 hosts=[HostSpec(h) for h in hosts],
869 get_daemons_func=lambda _: daemons).place()
870 assert len(hosts) == expected_count