]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/cephadm/tests/test_scheduling.py
import ceph quincy 17.2.1
[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
b3b6e05e 5from typing import NamedTuple, List, Dict, Optional
9f95a23c
TL
6import pytest
7
f6b5b4d7 8from ceph.deployment.hostspec import HostSpec
f67539c2
TL
9from ceph.deployment.service_spec import ServiceSpec, PlacementSpec, IngressSpec
10from ceph.deployment.hostspec import SpecValidationError
9f95a23c
TL
11
12from cephadm.module import HostAssignment
f67539c2
TL
13from cephadm.schedule import DaemonPlacement
14from orchestrator import DaemonDescription, OrchestratorValidationError, OrchestratorError
f6b5b4d7
TL
15
16
17def wrapper(func):
18 # some odd thingy to revert the order or arguments
19 def inner(*args):
20 def inner2(expected):
21 func(expected, *args)
22 return inner2
23 return inner
24
25
26@wrapper
27def none(expected):
28 assert expected == []
29
30
31@wrapper
32def 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
37
38
39@wrapper
40def 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'
44 matches = 0
45 for h in hosts:
46 matches += int(h in expected)
47 if matches != 2:
48 assert False, f'two of {hosts} not in {expected}'
49
50
51@wrapper
52def exactly(expected, *hosts):
53 assert expected == list(hosts)
54
55
56@wrapper
57def error(expected, kind, match):
58 assert isinstance(expected, kind), (str(expected), match)
59 assert str(expected) == match, (str(expected), match)
60
61
62@wrapper
63def _or(expected, *inners):
64 def catch(inner):
65 try:
66 inner(expected)
67 except AssertionError as e:
68 return e
69 result = [catch(i) for i in inners]
70 if None not in result:
71 assert False, f"_or failed: {expected}"
72
73
f67539c2
TL
74def _always_true(_):
75 pass
f6b5b4d7
TL
76
77
78def k(s):
79 return [e for e in s.split(' ') if e]
80
81
82def get_result(key, results):
83 def match(one):
84 for o, k in zip(one, key):
85 if o != k and o != '*':
86 return False
87 return True
f67539c2 88 return [v for k, v in results if match(k)][0]
f6b5b4d7 89
f6b5b4d7 90
f91f0fd5 91def mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count):
f6b5b4d7
TL
92
93 if spec_section == 'hosts':
f67539c2
TL
94 mk_spec = lambda: ServiceSpec('mgr', placement=PlacementSpec( # noqa: E731
95 hosts=explicit,
96 count=count,
97 ))
f6b5b4d7 98 elif spec_section == 'label':
f67539c2 99 mk_spec = lambda: ServiceSpec('mgr', placement=PlacementSpec( # noqa: E731
f6b5b4d7
TL
100 label='mylabel',
101 count=count,
102 ))
f6b5b4d7
TL
103 elif spec_section == 'host_pattern':
104 pattern = {
105 'e': 'notfound',
106 '1': '1',
107 '12': '[1-2]',
108 '123': '*',
109 }[explicit_key]
f67539c2
TL
110 mk_spec = lambda: ServiceSpec('mgr', placement=PlacementSpec( # noqa: E731
111 host_pattern=pattern,
112 count=count,
113 ))
f6b5b4d7
TL
114 else:
115 assert False
f6b5b4d7 116
f91f0fd5 117 hosts = [
f67539c2
TL
118 HostSpec(h, labels=['mylabel']) if h in explicit else HostSpec(h)
119 for h in hosts
120 ]
f91f0fd5
TL
121
122 return mk_spec, hosts
f6b5b4d7
TL
123
124
f67539c2 125def run_scheduler_test(results, mk_spec, hosts, daemons, key_elems):
f6b5b4d7
TL
126 key = ' '.join('N' if e is None else str(e) for e in key_elems)
127 try:
128 assert_res = get_result(k(key), results)
129 except IndexError:
130 try:
131 spec = mk_spec()
f67539c2 132 host_res, to_add, to_remove = HostAssignment(
f6b5b4d7 133 spec=spec,
f91f0fd5 134 hosts=hosts,
522d829b 135 unreachable_hosts=[],
f67539c2
TL
136 daemons=daemons,
137 ).place()
f6b5b4d7
TL
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'
144
145 for _ in range(10): # scheduler has a random component
146 try:
147 spec = mk_spec()
f67539c2 148 host_res, to_add, to_remove = HostAssignment(
f6b5b4d7 149 spec=spec,
f91f0fd5 150 hosts=hosts,
522d829b 151 unreachable_hosts=[],
f67539c2
TL
152 daemons=daemons
153 ).place()
f6b5b4d7
TL
154
155 assert_res(sorted([h.hostname for h in host_res]))
156 except Exception as e:
157 assert_res(e)
158
159
160# * first match from the top wins
161# * where e=[], *=any
162#
163# + list of known hosts available for scheduling (host_key)
164# | + hosts used for explict placement (explicit_key)
165# | | + count
166# | | | + section (host, label, pattern)
167# | | | | + expected result
168# | | | | |
169test_explicit_scheduler_results = [
20effc67 170 (k("* * 0 *"), error(SpecValidationError, 'num/count must be >= 1')),
f91f0fd5
TL
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')),
f6b5b4d7
TL
173 (k("* e N h"), error(OrchestratorValidationError, 'placement spec is empty: no hosts, no label, no pattern, no count')),
174 (k("* e * *"), none),
f91f0fd5
TL
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")),
f6b5b4d7
TL
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')),
f91f0fd5 181 (k("12 123 * h"), error(OrchestratorValidationError, "Cannot place <ServiceSpec for service_name=mgr> on 3: Unknown hosts")),
f6b5b4d7
TL
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')),
190]
191
f67539c2
TL
192
193@pytest.mark.parametrize("dp,n,result",
194 [ # noqa: E128
195 (
196 DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[80]),
197 0,
198 DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[80]),
199 ),
200 (
201 DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[80]),
202 2,
203 DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[82]),
204 ),
205 (
206 DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[80, 90]),
207 2,
208 DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[82, 92]),
209 ),
210 ])
211def test_daemon_placement_renumber(dp, n, result):
212 assert dp.renumber_ports(n) == result
213
214
215@pytest.mark.parametrize(
216 'dp,dd,result',
f6b5b4d7 217 [
f67539c2
TL
218 (
219 DaemonPlacement(daemon_type='mgr', hostname='host1'),
220 DaemonDescription('mgr', 'a', 'host1'),
221 True
222 ),
223 (
224 DaemonPlacement(daemon_type='mgr', hostname='host1', name='a'),
225 DaemonDescription('mgr', 'a', 'host1'),
226 True
227 ),
228 (
229 DaemonPlacement(daemon_type='mon', hostname='host1', name='a'),
230 DaemonDescription('mgr', 'a', 'host1'),
231 False
232 ),
233 (
234 DaemonPlacement(daemon_type='mgr', hostname='host1', name='a'),
235 DaemonDescription('mgr', 'b', 'host1'),
236 False
237 ),
238 ])
239def test_daemon_placement_match(dp, dd, result):
240 assert dp.matches_daemon(dd) == result
241
242
243@pytest.mark.parametrize("spec_section_key,spec_section",
244 [ # noqa: E128
f6b5b4d7
TL
245 ('h', 'hosts'),
246 ('l', 'label'),
247 ('p', 'host_pattern'),
248 ])
249@pytest.mark.parametrize("count",
f67539c2 250 [ # noqa: E128
f6b5b4d7
TL
251 None,
252 0,
253 1,
254 2,
255 3,
256 ])
257@pytest.mark.parametrize("explicit_key, explicit",
f67539c2 258 [ # noqa: E128
f6b5b4d7
TL
259 ('e', []),
260 ('1', ['1']),
261 ('12', ['1', '2']),
262 ('123', ['1', '2', '3']),
263 ])
264@pytest.mark.parametrize("host_key, hosts",
f67539c2 265 [ # noqa: E128
f6b5b4d7
TL
266 ('1', ['1']),
267 ('12', ['1', '2']),
268 ('123', ['1', '2', '3']),
269 ])
270def test_explicit_scheduler(host_key, hosts,
271 explicit_key, explicit,
272 count,
273 spec_section_key, spec_section):
274
f91f0fd5 275 mk_spec, hosts = mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count)
f6b5b4d7
TL
276 run_scheduler_test(
277 results=test_explicit_scheduler_results,
278 mk_spec=mk_spec,
f91f0fd5 279 hosts=hosts,
f67539c2 280 daemons=[],
f6b5b4d7
TL
281 key_elems=(host_key, explicit_key, count, spec_section_key)
282 )
283
284
285# * first match from the top wins
286# * where e=[], *=any
287#
288# + list of known hosts available for scheduling (host_key)
289# | + hosts used for explict placement (explicit_key)
290# | | + count
291# | | | + existing daemons
292# | | | | + section (host, label, pattern)
293# | | | | | + expected result
294# | | | | | |
295test_scheduler_daemons_results = [
296 (k("* 1 * * *"), exactly('1')),
f91f0fd5 297 (k("1 123 * * h"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr> on 2, 3: Unknown hosts')),
f6b5b4d7 298 (k("1 123 * * *"), exactly('1')),
f91f0fd5 299 (k("12 123 * * h"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr> on 3: Unknown hosts')),
f6b5b4d7
TL
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')),
320]
321
322
323@pytest.mark.parametrize("spec_section_key,spec_section",
f67539c2 324 [ # noqa: E128
f6b5b4d7
TL
325 ('h', 'hosts'),
326 ('l', 'label'),
327 ('p', 'host_pattern'),
328 ])
329@pytest.mark.parametrize("daemons_key, daemons",
f67539c2 330 [ # noqa: E128
f6b5b4d7
TL
331 ('e', []),
332 ('1', ['1']),
333 ('3', ['3']),
334 ('12', ['1', '2']),
335 ('112', ['1', '1', '2']), # deal with existing co-located daemons
336 ('23', ['2', '3']),
337 ('123', ['1', '2', '3']),
338 ])
339@pytest.mark.parametrize("count",
f67539c2 340 [ # noqa: E128
f6b5b4d7
TL
341 None,
342 1,
343 2,
344 3,
345 ])
346@pytest.mark.parametrize("explicit_key, explicit",
f67539c2 347 [ # noqa: E128
f6b5b4d7
TL
348 ('1', ['1']),
349 ('123', ['1', '2', '3']),
350 ])
351@pytest.mark.parametrize("host_key, hosts",
f67539c2 352 [ # noqa: E128
f6b5b4d7
TL
353 ('1', ['1']),
354 ('12', ['1', '2']),
355 ('123', ['1', '2', '3']),
356 ])
357def test_scheduler_daemons(host_key, hosts,
358 explicit_key, explicit,
359 count,
360 daemons_key, daemons,
361 spec_section_key, spec_section):
f91f0fd5 362 mk_spec, hosts = mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count)
f6b5b4d7 363 dds = [
f91f0fd5 364 DaemonDescription('mgr', d, d)
f6b5b4d7
TL
365 for d in daemons
366 ]
367 run_scheduler_test(
368 results=test_scheduler_daemons_results,
369 mk_spec=mk_spec,
f91f0fd5 370 hosts=hosts,
f67539c2 371 daemons=dds,
f6b5b4d7
TL
372 key_elems=(host_key, explicit_key, count, daemons_key, spec_section_key)
373 )
374
375
f91f0fd5 376# =========================
9f95a23c
TL
377
378
379class NodeAssignmentTest(NamedTuple):
380 service_type: str
381 placement: PlacementSpec
382 hosts: List[str]
383 daemons: List[DaemonDescription]
b3b6e05e
TL
384 rank_map: Optional[Dict[int, Dict[int, Optional[str]]]]
385 post_rank_map: Optional[Dict[int, Dict[int, Optional[str]]]]
9f95a23c 386 expected: List[str]
f67539c2
TL
387 expected_add: List[str]
388 expected_remove: List[DaemonDescription]
9f95a23c 389
f67539c2 390
b3b6e05e 391@pytest.mark.parametrize("service_type,placement,hosts,daemons,rank_map,post_rank_map,expected,expected_add,expected_remove",
f67539c2 392 [ # noqa: E128
9f95a23c
TL
393 # just hosts
394 NodeAssignmentTest(
f91f0fd5 395 'mgr',
f67539c2 396 PlacementSpec(hosts=['smithi060']),
9f95a23c
TL
397 ['smithi060'],
398 [],
b3b6e05e 399 None, None,
f67539c2 400 ['mgr:smithi060'], ['mgr:smithi060'], []
9f95a23c
TL
401 ),
402 # all_hosts
403 NodeAssignmentTest(
f91f0fd5 404 'mgr',
9f95a23c
TL
405 PlacementSpec(host_pattern='*'),
406 'host1 host2 host3'.split(),
407 [
f91f0fd5
TL
408 DaemonDescription('mgr', 'a', 'host1'),
409 DaemonDescription('mgr', 'b', 'host2'),
9f95a23c 410 ],
b3b6e05e 411 None, None,
f67539c2
TL
412 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
413 ['mgr:host3'],
414 []
415 ),
416 # all_hosts + count_per_host
417 NodeAssignmentTest(
418 'mds',
419 PlacementSpec(host_pattern='*', count_per_host=2),
420 'host1 host2 host3'.split(),
421 [
422 DaemonDescription('mds', 'a', 'host1'),
423 DaemonDescription('mds', 'b', 'host2'),
424 ],
b3b6e05e 425 None, None,
f67539c2
TL
426 ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
427 ['mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'],
428 []
9f95a23c
TL
429 ),
430 # count that is bigger than the amount of hosts. Truncate to len(hosts)
f67539c2 431 # mgr should not be co-located to each other.
9f95a23c 432 NodeAssignmentTest(
f67539c2 433 'mgr',
9f95a23c
TL
434 PlacementSpec(count=4),
435 'host1 host2 host3'.split(),
436 [],
b3b6e05e 437 None, None,
f67539c2
TL
438 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
439 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
440 []
441 ),
442 # count that is bigger than the amount of hosts; wrap around.
443 NodeAssignmentTest(
444 'mds',
445 PlacementSpec(count=6),
446 'host1 host2 host3'.split(),
447 [],
b3b6e05e 448 None, None,
f67539c2
TL
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'],
451 []
9f95a23c
TL
452 ),
453 # count + partial host list
454 NodeAssignmentTest(
f91f0fd5 455 'mgr',
9f95a23c
TL
456 PlacementSpec(count=3, hosts=['host3']),
457 'host1 host2 host3'.split(),
458 [
f91f0fd5
TL
459 DaemonDescription('mgr', 'a', 'host1'),
460 DaemonDescription('mgr', 'b', 'host2'),
9f95a23c 461 ],
b3b6e05e 462 None, None,
f67539c2
TL
463 ['mgr:host3'],
464 ['mgr:host3'],
465 ['mgr.a', 'mgr.b']
466 ),
467 # count + partial host list (with colo)
468 NodeAssignmentTest(
469 'mds',
470 PlacementSpec(count=3, hosts=['host3']),
471 'host1 host2 host3'.split(),
472 [
473 DaemonDescription('mds', 'a', 'host1'),
474 DaemonDescription('mds', 'b', 'host2'),
475 ],
b3b6e05e 476 None, None,
f67539c2
TL
477 ['mds:host3', 'mds:host3', 'mds:host3'],
478 ['mds:host3', 'mds:host3', 'mds:host3'],
479 ['mds.a', 'mds.b']
9f95a23c
TL
480 ),
481 # count 1 + partial host list
482 NodeAssignmentTest(
f91f0fd5 483 'mgr',
9f95a23c
TL
484 PlacementSpec(count=1, hosts=['host3']),
485 'host1 host2 host3'.split(),
486 [
f91f0fd5
TL
487 DaemonDescription('mgr', 'a', 'host1'),
488 DaemonDescription('mgr', 'b', 'host2'),
9f95a23c 489 ],
b3b6e05e 490 None, None,
f67539c2
TL
491 ['mgr:host3'],
492 ['mgr:host3'],
493 ['mgr.a', 'mgr.b']
9f95a23c
TL
494 ),
495 # count + partial host list + existing
496 NodeAssignmentTest(
f91f0fd5 497 'mgr',
9f95a23c
TL
498 PlacementSpec(count=2, hosts=['host3']),
499 'host1 host2 host3'.split(),
500 [
f91f0fd5 501 DaemonDescription('mgr', 'a', 'host1'),
9f95a23c 502 ],
b3b6e05e 503 None, None,
f67539c2
TL
504 ['mgr:host3'],
505 ['mgr:host3'],
506 ['mgr.a']
9f95a23c
TL
507 ),
508 # count + partial host list + existing (deterministic)
509 NodeAssignmentTest(
f91f0fd5 510 'mgr',
9f95a23c
TL
511 PlacementSpec(count=2, hosts=['host1']),
512 'host1 host2'.split(),
513 [
f91f0fd5 514 DaemonDescription('mgr', 'a', 'host1'),
9f95a23c 515 ],
b3b6e05e 516 None, None,
f67539c2
TL
517 ['mgr:host1'],
518 [],
519 []
9f95a23c
TL
520 ),
521 # count + partial host list + existing (deterministic)
522 NodeAssignmentTest(
f91f0fd5 523 'mgr',
9f95a23c
TL
524 PlacementSpec(count=2, hosts=['host1']),
525 'host1 host2'.split(),
526 [
f91f0fd5 527 DaemonDescription('mgr', 'a', 'host2'),
9f95a23c 528 ],
b3b6e05e 529 None, None,
f67539c2
TL
530 ['mgr:host1'],
531 ['mgr:host1'],
532 ['mgr.a']
9f95a23c
TL
533 ),
534 # label only
535 NodeAssignmentTest(
f91f0fd5 536 'mgr',
9f95a23c
TL
537 PlacementSpec(label='foo'),
538 'host1 host2 host3'.split(),
539 [],
b3b6e05e 540 None, None,
f67539c2
TL
541 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
542 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
543 []
544 ),
545 # label + count (truncate to host list)
546 NodeAssignmentTest(
547 'mgr',
548 PlacementSpec(count=4, label='foo'),
549 'host1 host2 host3'.split(),
550 [],
b3b6e05e 551 None, None,
f67539c2
TL
552 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
553 ['mgr:host1', 'mgr:host2', 'mgr:host3'],
554 []
555 ),
556 # label + count (with colo)
557 NodeAssignmentTest(
558 'mds',
559 PlacementSpec(count=6, label='foo'),
560 'host1 host2 host3'.split(),
561 [],
b3b6e05e 562 None, None,
f67539c2
TL
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'],
565 []
566 ),
567 # label only + count_per_hst
568 NodeAssignmentTest(
569 'mds',
570 PlacementSpec(label='foo', count_per_host=3),
571 'host1 host2 host3'.split(),
572 [],
b3b6e05e 573 None, None,
f67539c2
TL
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'],
578 []
9f95a23c
TL
579 ),
580 # host_pattern
581 NodeAssignmentTest(
f91f0fd5
TL
582 'mgr',
583 PlacementSpec(host_pattern='mgr*'),
584 'mgrhost1 mgrhost2 datahost'.split(),
9f95a23c 585 [],
b3b6e05e 586 None, None,
f67539c2
TL
587 ['mgr:mgrhost1', 'mgr:mgrhost2'],
588 ['mgr:mgrhost1', 'mgr:mgrhost2'],
589 []
590 ),
591 # host_pattern + count_per_host
592 NodeAssignmentTest(
593 'mds',
594 PlacementSpec(host_pattern='mds*', count_per_host=3),
595 'mdshost1 mdshost2 datahost'.split(),
596 [],
b3b6e05e 597 None, None,
f67539c2
TL
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'],
600 []
601 ),
602 # label + count_per_host + ports
603 NodeAssignmentTest(
604 'rgw',
605 PlacementSpec(count=6, label='foo'),
606 'host1 host2 host3'.split(),
607 [],
b3b6e05e 608 None, None,
f67539c2
TL
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)'],
613 []
614 ),
615 # label + count_per_host + ports (+ xisting)
616 NodeAssignmentTest(
617 'rgw',
618 PlacementSpec(count=6, label='foo'),
619 'host1 host2 host3'.split(),
620 [
621 DaemonDescription('rgw', 'a', 'host1', ports=[81]),
622 DaemonDescription('rgw', 'b', 'host2', ports=[80]),
623 DaemonDescription('rgw', 'c', 'host1', ports=[82]),
624 ],
b3b6e05e 625 None, None,
f67539c2
TL
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)'],
630 ['rgw.c']
631 ),
632 # cephadm.py teuth case
633 NodeAssignmentTest(
634 'mgr',
635 PlacementSpec(count=3, hosts=['host1=y', 'host2=x']),
636 'host1 host2'.split(),
637 [
638 DaemonDescription('mgr', 'y', 'host1'),
639 DaemonDescription('mgr', 'x', 'host2'),
640 ],
b3b6e05e 641 None, None,
f67539c2
TL
642 ['mgr:host1(name=y)', 'mgr:host2(name=x)'],
643 [], []
9f95a23c 644 ),
b3b6e05e
TL
645
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
648 # the test.
649
650 # ranked, fresh
651 NodeAssignmentTest(
652 'nfs',
653 PlacementSpec(count=3),
654 'host1 host2 host3'.split(),
655 [],
656 {},
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)'],
660 []
661 ),
662 # 21: ranked, exist
663 NodeAssignmentTest(
664 'nfs',
665 PlacementSpec(count=3),
666 'host1 host2 host3'.split(),
667 [
668 DaemonDescription('nfs', '0.1', 'host1', rank=0, rank_generation=1),
669 ],
670 {0: {1: '0.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)'],
674 []
675 ),
676 # ranked, exist, different ranks
677 NodeAssignmentTest(
678 'nfs',
679 PlacementSpec(count=3),
680 'host1 host2 host3'.split(),
681 [
682 DaemonDescription('nfs', '0.1', 'host1', rank=0, rank_generation=1),
683 DaemonDescription('nfs', '1.1', 'host2', rank=1, rank_generation=1),
684 ],
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)'],
689 []
690 ),
691 # ranked, exist, different ranks (2)
692 NodeAssignmentTest(
693 'nfs',
694 PlacementSpec(count=3),
695 'host1 host2 host3'.split(),
696 [
697 DaemonDescription('nfs', '0.1', 'host1', rank=0, rank_generation=1),
698 DaemonDescription('nfs', '1.1', 'host3', rank=1, rank_generation=1),
699 ],
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)'],
704 []
705 ),
706 # ranked, exist, extra ranks
707 NodeAssignmentTest(
708 'nfs',
709 PlacementSpec(count=3),
710 'host1 host2 host3'.split(),
711 [
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),
715 ],
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)'],
720 ['nfs.4.5']
721 ),
722 # 25: ranked, exist, extra ranks (scale down: kill off high rank)
723 NodeAssignmentTest(
724 'nfs',
725 PlacementSpec(count=2),
726 'host3 host2 host1'.split(),
727 [
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),
731 ],
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)'],
735 [],
736 ['nfs.2.5']
737 ),
738 # ranked, exist, extra ranks (scale down hosts)
739 NodeAssignmentTest(
740 'nfs',
741 PlacementSpec(count=2),
742 'host1 host3'.split(),
743 [
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),
747 ],
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']
753 ),
754 # ranked, exist, duplicate rank
755 NodeAssignmentTest(
756 'nfs',
757 PlacementSpec(count=3),
758 'host1 host2 host3'.split(),
759 [
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),
763 ],
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)'],
768 ['nfs.1.1']
769 ),
770 # 28: ranked, all gens stale (failure during update cycle)
771 NodeAssignmentTest(
772 'nfs',
773 PlacementSpec(count=2),
774 'host1 host2 host3'.split(),
775 [
776 DaemonDescription('nfs', '0.2', 'host1', rank=0, rank_generation=2),
777 DaemonDescription('nfs', '1.2', 'host2', rank=1, rank_generation=2),
778 ],
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)'],
783 ['nfs.1.2']
784 ),
785 # ranked, not enough hosts
786 NodeAssignmentTest(
787 'nfs',
788 PlacementSpec(count=4),
789 'host1 host2 host3'.split(),
790 [
791 DaemonDescription('nfs', '0.2', 'host1', rank=0, rank_generation=2),
792 DaemonDescription('nfs', '1.2', 'host2', rank=1, rank_generation=2),
793 ],
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)'],
798 []
799 ),
800 # ranked, scale down
801 NodeAssignmentTest(
802 'nfs',
803 PlacementSpec(hosts=['host2']),
804 'host1 host2'.split(),
805 [
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),
809 ],
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']
815 ),
816
9f95a23c 817 ])
b3b6e05e 818def test_node_assignment(service_type, placement, hosts, daemons, rank_map, post_rank_map,
f67539c2 819 expected, expected_add, expected_remove):
b3b6e05e 820 spec = None
f6b5b4d7 821 service_id = None
f67539c2 822 allow_colo = False
f6b5b4d7
TL
823 if service_type == 'rgw':
824 service_id = 'realm.zone'
f67539c2
TL
825 allow_colo = True
826 elif service_type == 'mds':
827 service_id = 'myfs'
828 allow_colo = True
b3b6e05e
TL
829 elif service_type == 'nfs':
830 service_id = 'mynfs'
831 spec = ServiceSpec(service_type=service_type,
832 service_id=service_id,
a4b75251 833 placement=placement)
b3b6e05e
TL
834
835 if not spec:
836 spec = ServiceSpec(service_type=service_type,
837 service_id=service_id,
838 placement=placement)
f6b5b4d7 839
f67539c2 840 all_slots, to_add, to_remove = HostAssignment(
f6b5b4d7 841 spec=spec,
f91f0fd5 842 hosts=[HostSpec(h, labels=['foo']) for h in hosts],
522d829b 843 unreachable_hosts=[],
f67539c2
TL
844 daemons=daemons,
845 allow_colo=allow_colo,
b3b6e05e 846 rank_map=rank_map,
f67539c2
TL
847 ).place()
848
b3b6e05e
TL
849 assert rank_map == post_rank_map
850
f67539c2
TL
851 got = [str(p) for p in all_slots]
852 num_wildcard = 0
853 for i in expected:
854 if i == '*':
855 num_wildcard += 1
856 else:
857 assert i in got
858 got.remove(i)
859 assert num_wildcard == len(got)
860
861 got = [str(p) for p in to_add]
862 num_wildcard = 0
863 for i in expected_add:
864 if i == '*':
865 num_wildcard += 1
866 else:
867 assert i in got
868 got.remove(i)
869 assert num_wildcard == len(got)
870
871 assert sorted([d.name() for d in to_remove]) == sorted(expected_remove)
9f95a23c 872
e306af50 873
9f95a23c
TL
874class NodeAssignmentTest2(NamedTuple):
875 service_type: str
876 placement: PlacementSpec
877 hosts: List[str]
878 daemons: List[DaemonDescription]
879 expected_len: int
880 in_set: List[str]
881
f67539c2 882
9f95a23c 883@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected_len,in_set",
f67539c2 884 [ # noqa: E128
9f95a23c
TL
885 # just count
886 NodeAssignmentTest2(
f91f0fd5 887 'mgr',
9f95a23c
TL
888 PlacementSpec(count=1),
889 'host1 host2 host3'.split(),
890 [],
891 1,
892 ['host1', 'host2', 'host3'],
893 ),
894
895 # hosts + (smaller) count
896 NodeAssignmentTest2(
f91f0fd5 897 'mgr',
9f95a23c
TL
898 PlacementSpec(count=1, hosts='host1 host2'.split()),
899 'host1 host2'.split(),
900 [],
901 1,
902 ['host1', 'host2'],
903 ),
904 # hosts + (smaller) count, existing
905 NodeAssignmentTest2(
f91f0fd5 906 'mgr',
9f95a23c
TL
907 PlacementSpec(count=1, hosts='host1 host2 host3'.split()),
908 'host1 host2 host3'.split(),
f67539c2 909 [DaemonDescription('mgr', 'mgr.a', 'host1')],
9f95a23c
TL
910 1,
911 ['host1', 'host2', 'host3'],
912 ),
913 # hosts + (smaller) count, (more) existing
914 NodeAssignmentTest2(
f91f0fd5 915 'mgr',
9f95a23c
TL
916 PlacementSpec(count=1, hosts='host1 host2 host3'.split()),
917 'host1 host2 host3'.split(),
918 [
f91f0fd5
TL
919 DaemonDescription('mgr', 'a', 'host1'),
920 DaemonDescription('mgr', 'b', 'host2'),
9f95a23c
TL
921 ],
922 1,
923 ['host1', 'host2']
924 ),
925 # count + partial host list
926 NodeAssignmentTest2(
f91f0fd5 927 'mgr',
9f95a23c
TL
928 PlacementSpec(count=2, hosts=['host3']),
929 'host1 host2 host3'.split(),
930 [],
f6b5b4d7 931 1,
9f95a23c
TL
932 ['host1', 'host2', 'host3']
933 ),
934 # label + count
935 NodeAssignmentTest2(
f91f0fd5 936 'mgr',
9f95a23c
TL
937 PlacementSpec(count=1, label='foo'),
938 'host1 host2 host3'.split(),
939 [],
940 1,
941 ['host1', 'host2', 'host3']
942 ),
943 ])
944def test_node_assignment2(service_type, placement, hosts,
945 daemons, expected_len, in_set):
f67539c2 946 hosts, to_add, to_remove = HostAssignment(
9f95a23c 947 spec=ServiceSpec(service_type, placement=placement),
f91f0fd5 948 hosts=[HostSpec(h, labels=['foo']) for h in hosts],
522d829b 949 unreachable_hosts=[],
f67539c2
TL
950 daemons=daemons,
951 ).place()
9f95a23c
TL
952 assert len(hosts) == expected_len
953 for h in [h.hostname for h in hosts]:
954 assert h in in_set
955
f67539c2 956
9f95a23c 957@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected_len,must_have",
f67539c2 958 [ # noqa: E128
9f95a23c
TL
959 # hosts + (smaller) count, (more) existing
960 NodeAssignmentTest2(
f91f0fd5 961 'mgr',
9f95a23c
TL
962 PlacementSpec(count=3, hosts='host3'.split()),
963 'host1 host2 host3'.split(),
964 [],
f6b5b4d7 965 1,
9f95a23c
TL
966 ['host3']
967 ),
968 # count + partial host list
969 NodeAssignmentTest2(
f91f0fd5 970 'mgr',
9f95a23c
TL
971 PlacementSpec(count=2, hosts=['host3']),
972 'host1 host2 host3'.split(),
973 [],
f6b5b4d7 974 1,
9f95a23c
TL
975 ['host3']
976 ),
977 ])
978def test_node_assignment3(service_type, placement, hosts,
979 daemons, expected_len, must_have):
f67539c2 980 hosts, to_add, to_remove = HostAssignment(
9f95a23c 981 spec=ServiceSpec(service_type, placement=placement),
f91f0fd5 982 hosts=[HostSpec(h) for h in hosts],
522d829b 983 unreachable_hosts=[],
f67539c2
TL
984 daemons=daemons,
985 ).place()
9f95a23c
TL
986 assert len(hosts) == expected_len
987 for h in must_have:
988 assert h in [h.hostname for h in hosts]
989
990
f67539c2
TL
991class NodeAssignmentTest4(NamedTuple):
992 spec: ServiceSpec
993 networks: Dict[str, Dict[str, Dict[str, List[str]]]]
994 daemons: List[DaemonDescription]
995 expected: List[str]
996 expected_add: List[str]
997 expected_remove: List[DaemonDescription]
998
999
1000@pytest.mark.parametrize("spec,networks,daemons,expected,expected_add,expected_remove",
1001 [ # noqa: E128
1002 NodeAssignmentTest4(
1003 ServiceSpec(
1004 service_type='rgw',
1005 service_id='foo',
1006 placement=PlacementSpec(count=6, label='foo'),
1007 networks=['10.0.0.0/8'],
1008 ),
1009 {
1010 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}},
1011 'host2': {'10.0.0.0/8': {'eth0': ['10.0.0.2']}},
1012 'host3': {'192.168.0.0/16': {'eth0': ['192.168.0.1']}},
1013 },
1014 [],
1015 ['rgw:host1(10.0.0.1:80)', 'rgw:host2(10.0.0.2:80)',
1016 'rgw:host1(10.0.0.1:81)', 'rgw:host2(10.0.0.2:81)',
1017 'rgw:host1(10.0.0.1:82)', 'rgw:host2(10.0.0.2:82)'],
1018 ['rgw:host1(10.0.0.1:80)', 'rgw:host2(10.0.0.2:80)',
1019 'rgw:host1(10.0.0.1:81)', 'rgw:host2(10.0.0.2:81)',
1020 'rgw:host1(10.0.0.1:82)', 'rgw:host2(10.0.0.2:82)'],
1021 []
1022 ),
1023 NodeAssignmentTest4(
1024 IngressSpec(
1025 service_type='ingress',
1026 service_id='rgw.foo',
1027 frontend_port=443,
1028 monitor_port=8888,
1029 virtual_ip='10.0.0.20/8',
1030 backend_service='rgw.foo',
1031 placement=PlacementSpec(label='foo'),
1032 networks=['10.0.0.0/8'],
1033 ),
1034 {
1035 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}},
1036 'host2': {'10.0.0.0/8': {'eth1': ['10.0.0.2']}},
1037 'host3': {'192.168.0.0/16': {'eth2': ['192.168.0.1']}},
1038 },
1039 [],
1040 ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)',
1041 'keepalived:host1', 'keepalived:host2'],
1042 ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)',
1043 'keepalived:host1', 'keepalived:host2'],
1044 []
1045 ),
1046 NodeAssignmentTest4(
1047 IngressSpec(
1048 service_type='ingress',
1049 service_id='rgw.foo',
1050 frontend_port=443,
1051 monitor_port=8888,
1052 virtual_ip='10.0.0.20/8',
1053 backend_service='rgw.foo',
1054 placement=PlacementSpec(label='foo'),
1055 networks=['10.0.0.0/8'],
1056 ),
1057 {
1058 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}},
1059 'host2': {'10.0.0.0/8': {'eth1': ['10.0.0.2']}},
1060 'host3': {'192.168.0.0/16': {'eth2': ['192.168.0.1']}},
1061 },
1062 [
1063 DaemonDescription('haproxy', 'a', 'host1', ip='10.0.0.1',
1064 ports=[443, 8888]),
1065 DaemonDescription('keepalived', 'b', 'host2'),
1066 DaemonDescription('keepalived', 'c', 'host3'),
1067 ],
1068 ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)',
1069 'keepalived:host1', 'keepalived:host2'],
1070 ['haproxy:host2(10.0.0.2:443,8888)',
1071 'keepalived:host1'],
1072 ['keepalived.c']
1073 ),
1074 ])
1075def test_node_assignment4(spec, networks, daemons,
1076 expected, expected_add, expected_remove):
1077 all_slots, to_add, to_remove = HostAssignment(
1078 spec=spec,
1079 hosts=[HostSpec(h, labels=['foo']) for h in networks.keys()],
522d829b 1080 unreachable_hosts=[],
f67539c2
TL
1081 daemons=daemons,
1082 allow_colo=True,
1083 networks=networks,
1084 primary_daemon_type='haproxy' if spec.service_type == 'ingress' else spec.service_type,
1085 per_host_daemon_type='keepalived' if spec.service_type == 'ingress' else None,
1086 ).place()
1087
1088 got = [str(p) for p in all_slots]
1089 num_wildcard = 0
1090 for i in expected:
1091 if i == '*':
1092 num_wildcard += 1
1093 else:
1094 assert i in got
1095 got.remove(i)
1096 assert num_wildcard == len(got)
1097
1098 got = [str(p) for p in to_add]
1099 num_wildcard = 0
1100 for i in expected_add:
1101 if i == '*':
1102 num_wildcard += 1
1103 else:
1104 assert i in got
1105 got.remove(i)
1106 assert num_wildcard == len(got)
1107
1108 assert sorted([d.name() for d in to_remove]) == sorted(expected_remove)
1109
1110
9f95a23c 1111@pytest.mark.parametrize("placement",
f67539c2 1112 [ # noqa: E128
9f95a23c
TL
1113 ('1 *'),
1114 ('* label:foo'),
1115 ('* host1 host2'),
1116 ('hostname12hostname12hostname12hostname12hostname12hostname12hostname12'), # > 63 chars
1117 ])
1118def test_bad_placements(placement):
1119 try:
f67539c2 1120 PlacementSpec.from_string(placement.split(' '))
9f95a23c 1121 assert False
f67539c2 1122 except SpecValidationError:
9f95a23c
TL
1123 pass
1124
1125
1126class NodeAssignmentTestBadSpec(NamedTuple):
1127 service_type: str
1128 placement: PlacementSpec
1129 hosts: List[str]
1130 daemons: List[DaemonDescription]
1131 expected: str
f67539c2
TL
1132
1133
9f95a23c 1134@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected",
f67539c2 1135 [ # noqa: E128
9f95a23c
TL
1136 # unknown host
1137 NodeAssignmentTestBadSpec(
f91f0fd5 1138 'mgr',
9f95a23c
TL
1139 PlacementSpec(hosts=['unknownhost']),
1140 ['knownhost'],
1141 [],
f91f0fd5 1142 "Cannot place <ServiceSpec for service_name=mgr> on unknownhost: Unknown hosts"
9f95a23c
TL
1143 ),
1144 # unknown host pattern
1145 NodeAssignmentTestBadSpec(
f91f0fd5 1146 'mgr',
9f95a23c
TL
1147 PlacementSpec(host_pattern='unknownhost'),
1148 ['knownhost'],
1149 [],
f91f0fd5 1150 "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts"
9f95a23c
TL
1151 ),
1152 # unknown label
1153 NodeAssignmentTestBadSpec(
f91f0fd5 1154 'mgr',
9f95a23c
TL
1155 PlacementSpec(label='unknownlabel'),
1156 [],
1157 [],
f91f0fd5 1158 "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts for label unknownlabel"
9f95a23c
TL
1159 ),
1160 ])
1161def test_bad_specs(service_type, placement, hosts, daemons, expected):
1162 with pytest.raises(OrchestratorValidationError) as e:
f67539c2 1163 hosts, to_add, to_remove = HostAssignment(
9f95a23c 1164 spec=ServiceSpec(service_type, placement=placement),
f91f0fd5 1165 hosts=[HostSpec(h) for h in hosts],
522d829b 1166 unreachable_hosts=[],
f67539c2
TL
1167 daemons=daemons,
1168 ).place()
9f95a23c 1169 assert str(e.value) == expected
f6b5b4d7 1170
f67539c2 1171
f6b5b4d7
TL
1172class ActiveAssignmentTest(NamedTuple):
1173 service_type: str
1174 placement: PlacementSpec
1175 hosts: List[str]
1176 daemons: List[DaemonDescription]
1177 expected: List[List[str]]
f67539c2
TL
1178 expected_add: List[List[str]]
1179 expected_remove: List[List[str]]
f6b5b4d7
TL
1180
1181
f67539c2 1182@pytest.mark.parametrize("service_type,placement,hosts,daemons,expected,expected_add,expected_remove",
f6b5b4d7
TL
1183 [
1184 ActiveAssignmentTest(
1185 'mgr',
1186 PlacementSpec(count=2),
1187 'host1 host2 host3'.split(),
1188 [
1189 DaemonDescription('mgr', 'a', 'host1', is_active=True),
1190 DaemonDescription('mgr', 'b', 'host2'),
1191 DaemonDescription('mgr', 'c', 'host3'),
1192 ],
f67539c2
TL
1193 [['host1', 'host2'], ['host1', 'host3']],
1194 [[]],
1195 [['mgr.b'], ['mgr.c']]
f6b5b4d7
TL
1196 ),
1197 ActiveAssignmentTest(
1198 'mgr',
1199 PlacementSpec(count=2),
1200 'host1 host2 host3'.split(),
1201 [
1202 DaemonDescription('mgr', 'a', 'host1'),
1203 DaemonDescription('mgr', 'b', 'host2'),
1204 DaemonDescription('mgr', 'c', 'host3', is_active=True),
1205 ],
f67539c2
TL
1206 [['host1', 'host3'], ['host2', 'host3']],
1207 [[]],
1208 [['mgr.a'], ['mgr.b']]
f6b5b4d7
TL
1209 ),
1210 ActiveAssignmentTest(
1211 'mgr',
1212 PlacementSpec(count=1),
1213 'host1 host2 host3'.split(),
1214 [
1215 DaemonDescription('mgr', 'a', 'host1'),
1216 DaemonDescription('mgr', 'b', 'host2', is_active=True),
1217 DaemonDescription('mgr', 'c', 'host3'),
1218 ],
f67539c2
TL
1219 [['host2']],
1220 [[]],
1221 [['mgr.a', 'mgr.c']]
f6b5b4d7
TL
1222 ),
1223 ActiveAssignmentTest(
1224 'mgr',
1225 PlacementSpec(count=1),
1226 'host1 host2 host3'.split(),
1227 [
1228 DaemonDescription('mgr', 'a', 'host1'),
1229 DaemonDescription('mgr', 'b', 'host2'),
1230 DaemonDescription('mgr', 'c', 'host3', is_active=True),
1231 ],
f67539c2
TL
1232 [['host3']],
1233 [[]],
1234 [['mgr.a', 'mgr.b']]
f6b5b4d7
TL
1235 ),
1236 ActiveAssignmentTest(
1237 'mgr',
1238 PlacementSpec(count=1),
1239 'host1 host2 host3'.split(),
1240 [
1241 DaemonDescription('mgr', 'a', 'host1', is_active=True),
1242 DaemonDescription('mgr', 'b', 'host2'),
1243 DaemonDescription('mgr', 'c', 'host3', is_active=True),
1244 ],
f67539c2
TL
1245 [['host1'], ['host3']],
1246 [[]],
1247 [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c']]
f6b5b4d7
TL
1248 ),
1249 ActiveAssignmentTest(
1250 'mgr',
1251 PlacementSpec(count=2),
1252 'host1 host2 host3'.split(),
1253 [
1254 DaemonDescription('mgr', 'a', 'host1'),
1255 DaemonDescription('mgr', 'b', 'host2', is_active=True),
1256 DaemonDescription('mgr', 'c', 'host3', is_active=True),
1257 ],
f67539c2
TL
1258 [['host2', 'host3']],
1259 [[]],
1260 [['mgr.a']]
f6b5b4d7
TL
1261 ),
1262 ActiveAssignmentTest(
1263 'mgr',
1264 PlacementSpec(count=1),
1265 'host1 host2 host3'.split(),
1266 [
1267 DaemonDescription('mgr', 'a', 'host1', is_active=True),
1268 DaemonDescription('mgr', 'b', 'host2', is_active=True),
1269 DaemonDescription('mgr', 'c', 'host3', is_active=True),
1270 ],
f67539c2
TL
1271 [['host1'], ['host2'], ['host3']],
1272 [[]],
1273 [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c'], ['mgr.a', 'mgr.c']]
f6b5b4d7
TL
1274 ),
1275 ActiveAssignmentTest(
1276 'mgr',
1277 PlacementSpec(count=1),
1278 'host1 host2 host3'.split(),
1279 [
1280 DaemonDescription('mgr', 'a', 'host1', is_active=True),
1281 DaemonDescription('mgr', 'a2', 'host1'),
1282 DaemonDescription('mgr', 'b', 'host2'),
1283 DaemonDescription('mgr', 'c', 'host3'),
1284 ],
f67539c2
TL
1285 [['host1']],
1286 [[]],
1287 [['mgr.a2', 'mgr.b', 'mgr.c']]
f6b5b4d7
TL
1288 ),
1289 ActiveAssignmentTest(
1290 'mgr',
1291 PlacementSpec(count=1),
1292 'host1 host2 host3'.split(),
1293 [
1294 DaemonDescription('mgr', 'a', 'host1', is_active=True),
1295 DaemonDescription('mgr', 'a2', 'host1', is_active=True),
1296 DaemonDescription('mgr', 'b', 'host2'),
1297 DaemonDescription('mgr', 'c', 'host3'),
1298 ],
f67539c2
TL
1299 [['host1']],
1300 [[]],
1301 [['mgr.a', 'mgr.b', 'mgr.c'], ['mgr.a2', 'mgr.b', 'mgr.c']]
f6b5b4d7
TL
1302 ),
1303 ActiveAssignmentTest(
1304 'mgr',
1305 PlacementSpec(count=2),
1306 'host1 host2 host3'.split(),
1307 [
1308 DaemonDescription('mgr', 'a', 'host1', is_active=True),
1309 DaemonDescription('mgr', 'a2', 'host1'),
1310 DaemonDescription('mgr', 'b', 'host2'),
1311 DaemonDescription('mgr', 'c', 'host3', is_active=True),
1312 ],
f67539c2
TL
1313 [['host1', 'host3']],
1314 [[]],
1315 [['mgr.a2', 'mgr.b']]
f6b5b4d7
TL
1316 ),
1317 # Explicit placement should override preference for active daemon
1318 ActiveAssignmentTest(
1319 'mgr',
1320 PlacementSpec(count=1, hosts=['host1']),
1321 'host1 host2 host3'.split(),
1322 [
1323 DaemonDescription('mgr', 'a', 'host1'),
1324 DaemonDescription('mgr', 'b', 'host2'),
1325 DaemonDescription('mgr', 'c', 'host3', is_active=True),
1326 ],
f67539c2
TL
1327 [['host1']],
1328 [[]],
1329 [['mgr.b', 'mgr.c']]
f6b5b4d7
TL
1330 ),
1331
1332 ])
f67539c2 1333def test_active_assignment(service_type, placement, hosts, daemons, expected, expected_add, expected_remove):
f6b5b4d7
TL
1334
1335 spec = ServiceSpec(service_type=service_type,
1336 service_id=None,
1337 placement=placement)
1338
f67539c2 1339 hosts, to_add, to_remove = HostAssignment(
f6b5b4d7 1340 spec=spec,
f91f0fd5 1341 hosts=[HostSpec(h) for h in hosts],
522d829b 1342 unreachable_hosts=[],
f67539c2
TL
1343 daemons=daemons,
1344 ).place()
f6b5b4d7 1345 assert sorted([h.hostname for h in hosts]) in expected
f67539c2
TL
1346 assert sorted([h.hostname for h in to_add]) in expected_add
1347 assert sorted([h.name() for h in to_remove]) in expected_remove
522d829b
TL
1348
1349
1350class UnreachableHostsTest(NamedTuple):
1351 service_type: str
1352 placement: PlacementSpec
1353 hosts: List[str]
1354 unreachables_hosts: List[str]
1355 daemons: List[DaemonDescription]
1356 expected_add: List[List[str]]
1357 expected_remove: List[List[str]]
1358
1359
1360@pytest.mark.parametrize("service_type,placement,hosts,unreachable_hosts,daemons,expected_add,expected_remove",
1361 [
1362 UnreachableHostsTest(
1363 'mgr',
1364 PlacementSpec(count=3),
1365 'host1 host2 host3'.split(),
1366 ['host2'],
1367 [],
1368 [['host1', 'host3']],
1369 [[]],
1370 ),
1371 UnreachableHostsTest(
1372 'mgr',
1373 PlacementSpec(hosts=['host3']),
1374 'host1 host2 host3'.split(),
1375 ['host1'],
1376 [
1377 DaemonDescription('mgr', 'a', 'host1'),
1378 DaemonDescription('mgr', 'b', 'host2'),
1379 DaemonDescription('mgr', 'c', 'host3', is_active=True),
1380 ],
1381 [[]],
1382 [['mgr.b']],
1383 ),
1384 UnreachableHostsTest(
1385 'mgr',
1386 PlacementSpec(count=3),
1387 'host1 host2 host3 host4'.split(),
1388 ['host1'],
1389 [
1390 DaemonDescription('mgr', 'a', 'host1'),
1391 DaemonDescription('mgr', 'b', 'host2'),
1392 DaemonDescription('mgr', 'c', 'host3', is_active=True),
1393 ],
1394 [[]],
1395 [[]],
1396 ),
1397 UnreachableHostsTest(
1398 'mgr',
1399 PlacementSpec(count=1),
1400 'host1 host2 host3 host4'.split(),
1401 'host1 host3'.split(),
1402 [
1403 DaemonDescription('mgr', 'a', 'host1'),
1404 DaemonDescription('mgr', 'b', 'host2'),
1405 DaemonDescription('mgr', 'c', 'host3', is_active=True),
1406 ],
1407 [[]],
1408 [['mgr.b']],
1409 ),
1410 UnreachableHostsTest(
1411 'mgr',
1412 PlacementSpec(count=3),
1413 'host1 host2 host3 host4'.split(),
1414 ['host2'],
1415 [],
1416 [['host1', 'host3', 'host4']],
1417 [[]],
1418 ),
1419 UnreachableHostsTest(
1420 'mgr',
1421 PlacementSpec(count=3),
1422 'host1 host2 host3 host4'.split(),
1423 'host1 host4'.split(),
1424 [],
1425 [['host2', 'host3']],
1426 [[]],
1427 ),
1428
1429 ])
1430def test_unreachable_host(service_type, placement, hosts, unreachable_hosts, daemons, expected_add, expected_remove):
1431
1432 spec = ServiceSpec(service_type=service_type,
1433 service_id=None,
1434 placement=placement)
1435
1436 hosts, to_add, to_remove = HostAssignment(
1437 spec=spec,
1438 hosts=[HostSpec(h) for h in hosts],
1439 unreachable_hosts=[HostSpec(h) for h in unreachable_hosts],
1440 daemons=daemons,
1441 ).place()
1442 assert sorted([h.hostname for h in to_add]) in expected_add
1443 assert sorted([h.name() for h in to_remove]) in expected_remove
33c7a0ef
TL
1444
1445
1446class RescheduleFromOfflineTest(NamedTuple):
1447 service_type: str
1448 placement: PlacementSpec
1449 hosts: List[str]
1450 maintenance_hosts: List[str]
1451 offline_hosts: List[str]
1452 daemons: List[DaemonDescription]
1453 expected_add: List[List[str]]
1454 expected_remove: List[List[str]]
1455
1456
1457@pytest.mark.parametrize("service_type,placement,hosts,maintenance_hosts,offline_hosts,daemons,expected_add,expected_remove",
1458 [
1459 RescheduleFromOfflineTest(
1460 'nfs',
1461 PlacementSpec(count=2),
1462 'host1 host2 host3'.split(),
1463 [],
1464 ['host2'],
1465 [
1466 DaemonDescription('nfs', 'a', 'host1'),
1467 DaemonDescription('nfs', 'b', 'host2'),
1468 ],
1469 [['host3']],
1470 [[]],
1471 ),
1472 RescheduleFromOfflineTest(
1473 'nfs',
1474 PlacementSpec(count=2),
1475 'host1 host2 host3'.split(),
1476 ['host2'],
1477 [],
1478 [
1479 DaemonDescription('nfs', 'a', 'host1'),
1480 DaemonDescription('nfs', 'b', 'host2'),
1481 ],
1482 [[]],
1483 [[]],
1484 ),
1485 RescheduleFromOfflineTest(
1486 'mon',
1487 PlacementSpec(count=2),
1488 'host1 host2 host3'.split(),
1489 [],
1490 ['host2'],
1491 [
1492 DaemonDescription('mon', 'a', 'host1'),
1493 DaemonDescription('mon', 'b', 'host2'),
1494 ],
1495 [[]],
1496 [[]],
1497 ),
1498 ])
1499def test_remove_from_offline(service_type, placement, hosts, maintenance_hosts, offline_hosts, daemons, expected_add, expected_remove):
1500
1501 spec = ServiceSpec(service_type=service_type,
1502 service_id='test',
1503 placement=placement)
1504
1505 host_specs = [HostSpec(h) for h in hosts]
1506 for h in host_specs:
1507 if h.hostname in offline_hosts:
1508 h.status = 'offline'
1509 if h.hostname in maintenance_hosts:
1510 h.status = 'maintenance'
1511
1512 hosts, to_add, to_remove = HostAssignment(
1513 spec=spec,
1514 hosts=host_specs,
1515 unreachable_hosts=[h for h in host_specs if h.status],
1516 daemons=daemons,
1517 ).place()
1518 assert sorted([h.hostname for h in to_add]) in expected_add
1519 assert sorted([h.name() for h in to_remove]) in expected_remove