]>
Commit | Line | Data |
---|---|---|
f91f0fd5 TL |
1 | # Disable autopep8 for this file: |
2 | ||
3 | # fmt: off | |
4 | ||
b3b6e05e | 5 | from typing import NamedTuple, List, Dict, Optional |
9f95a23c TL |
6 | import pytest |
7 | ||
f6b5b4d7 | 8 | from ceph.deployment.hostspec import HostSpec |
f67539c2 TL |
9 | from ceph.deployment.service_spec import ServiceSpec, PlacementSpec, IngressSpec |
10 | from ceph.deployment.hostspec import SpecValidationError | |
9f95a23c TL |
11 | |
12 | from cephadm.module import HostAssignment | |
f67539c2 TL |
13 | from cephadm.schedule import DaemonPlacement |
14 | from orchestrator import DaemonDescription, OrchestratorValidationError, OrchestratorError | |
f6b5b4d7 TL |
15 | |
16 | ||
17 | def 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 | |
27 | def none(expected): | |
28 | assert expected == [] | |
29 | ||
30 | ||
31 | @wrapper | |
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 | |
37 | ||
38 | ||
39 | @wrapper | |
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' | |
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 | |
52 | def exactly(expected, *hosts): | |
53 | assert expected == list(hosts) | |
54 | ||
55 | ||
56 | @wrapper | |
57 | def error(expected, kind, match): | |
58 | assert isinstance(expected, kind), (str(expected), match) | |
59 | assert str(expected) == match, (str(expected), match) | |
60 | ||
61 | ||
62 | @wrapper | |
63 | def _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 |
74 | def _always_true(_): |
75 | pass | |
f6b5b4d7 TL |
76 | |
77 | ||
78 | def k(s): | |
79 | return [e for e in s.split(' ') if e] | |
80 | ||
81 | ||
82 | def 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 | 91 | def 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 | 125 | def 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, |
f67539c2 TL |
135 | daemons=daemons, |
136 | ).place() | |
f6b5b4d7 TL |
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' | |
143 | ||
144 | for _ in range(10): # scheduler has a random component | |
145 | try: | |
146 | spec = mk_spec() | |
f67539c2 | 147 | host_res, to_add, to_remove = HostAssignment( |
f6b5b4d7 | 148 | spec=spec, |
f91f0fd5 | 149 | hosts=hosts, |
f67539c2 TL |
150 | daemons=daemons |
151 | ).place() | |
f6b5b4d7 TL |
152 | |
153 | assert_res(sorted([h.hostname for h in host_res])) | |
154 | except Exception as e: | |
155 | assert_res(e) | |
156 | ||
157 | ||
158 | # * first match from the top wins | |
159 | # * where e=[], *=any | |
160 | # | |
161 | # + list of known hosts available for scheduling (host_key) | |
162 | # | + hosts used for explict placement (explicit_key) | |
163 | # | | + count | |
164 | # | | | + section (host, label, pattern) | |
165 | # | | | | + expected result | |
166 | # | | | | | | |
167 | test_explicit_scheduler_results = [ | |
f67539c2 | 168 | (k("* * 0 *"), error(SpecValidationError, 'num/count must be > 1')), |
f91f0fd5 TL |
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')), | |
f6b5b4d7 TL |
171 | (k("* e N h"), error(OrchestratorValidationError, 'placement spec is empty: no hosts, no label, no pattern, no count')), |
172 | (k("* e * *"), none), | |
f91f0fd5 TL |
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")), | |
f6b5b4d7 TL |
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')), | |
f91f0fd5 | 179 | (k("12 123 * h"), error(OrchestratorValidationError, "Cannot place <ServiceSpec for service_name=mgr> on 3: Unknown hosts")), |
f6b5b4d7 TL |
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')), | |
188 | ] | |
189 | ||
f67539c2 TL |
190 | |
191 | @pytest.mark.parametrize("dp,n,result", | |
192 | [ # noqa: E128 | |
193 | ( | |
194 | DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[80]), | |
195 | 0, | |
196 | DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[80]), | |
197 | ), | |
198 | ( | |
199 | DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[80]), | |
200 | 2, | |
201 | DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[82]), | |
202 | ), | |
203 | ( | |
204 | DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[80, 90]), | |
205 | 2, | |
206 | DaemonPlacement(daemon_type='mgr', hostname='host1', ports=[82, 92]), | |
207 | ), | |
208 | ]) | |
209 | def test_daemon_placement_renumber(dp, n, result): | |
210 | assert dp.renumber_ports(n) == result | |
211 | ||
212 | ||
213 | @pytest.mark.parametrize( | |
214 | 'dp,dd,result', | |
f6b5b4d7 | 215 | [ |
f67539c2 TL |
216 | ( |
217 | DaemonPlacement(daemon_type='mgr', hostname='host1'), | |
218 | DaemonDescription('mgr', 'a', 'host1'), | |
219 | True | |
220 | ), | |
221 | ( | |
222 | DaemonPlacement(daemon_type='mgr', hostname='host1', name='a'), | |
223 | DaemonDescription('mgr', 'a', 'host1'), | |
224 | True | |
225 | ), | |
226 | ( | |
227 | DaemonPlacement(daemon_type='mon', hostname='host1', name='a'), | |
228 | DaemonDescription('mgr', 'a', 'host1'), | |
229 | False | |
230 | ), | |
231 | ( | |
232 | DaemonPlacement(daemon_type='mgr', hostname='host1', name='a'), | |
233 | DaemonDescription('mgr', 'b', 'host1'), | |
234 | False | |
235 | ), | |
236 | ]) | |
237 | def test_daemon_placement_match(dp, dd, result): | |
238 | assert dp.matches_daemon(dd) == result | |
239 | ||
240 | ||
241 | @pytest.mark.parametrize("spec_section_key,spec_section", | |
242 | [ # noqa: E128 | |
f6b5b4d7 TL |
243 | ('h', 'hosts'), |
244 | ('l', 'label'), | |
245 | ('p', 'host_pattern'), | |
246 | ]) | |
247 | @pytest.mark.parametrize("count", | |
f67539c2 | 248 | [ # noqa: E128 |
f6b5b4d7 TL |
249 | None, |
250 | 0, | |
251 | 1, | |
252 | 2, | |
253 | 3, | |
254 | ]) | |
255 | @pytest.mark.parametrize("explicit_key, explicit", | |
f67539c2 | 256 | [ # noqa: E128 |
f6b5b4d7 TL |
257 | ('e', []), |
258 | ('1', ['1']), | |
259 | ('12', ['1', '2']), | |
260 | ('123', ['1', '2', '3']), | |
261 | ]) | |
262 | @pytest.mark.parametrize("host_key, hosts", | |
f67539c2 | 263 | [ # noqa: E128 |
f6b5b4d7 TL |
264 | ('1', ['1']), |
265 | ('12', ['1', '2']), | |
266 | ('123', ['1', '2', '3']), | |
267 | ]) | |
268 | def test_explicit_scheduler(host_key, hosts, | |
269 | explicit_key, explicit, | |
270 | count, | |
271 | spec_section_key, spec_section): | |
272 | ||
f91f0fd5 | 273 | mk_spec, hosts = mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count) |
f6b5b4d7 TL |
274 | run_scheduler_test( |
275 | results=test_explicit_scheduler_results, | |
276 | mk_spec=mk_spec, | |
f91f0fd5 | 277 | hosts=hosts, |
f67539c2 | 278 | daemons=[], |
f6b5b4d7 TL |
279 | key_elems=(host_key, explicit_key, count, spec_section_key) |
280 | ) | |
281 | ||
282 | ||
283 | # * first match from the top wins | |
284 | # * where e=[], *=any | |
285 | # | |
286 | # + list of known hosts available for scheduling (host_key) | |
287 | # | + hosts used for explict placement (explicit_key) | |
288 | # | | + count | |
289 | # | | | + existing daemons | |
290 | # | | | | + section (host, label, pattern) | |
291 | # | | | | | + expected result | |
292 | # | | | | | | | |
293 | test_scheduler_daemons_results = [ | |
294 | (k("* 1 * * *"), exactly('1')), | |
f91f0fd5 | 295 | (k("1 123 * * h"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr> on 2, 3: Unknown hosts')), |
f6b5b4d7 | 296 | (k("1 123 * * *"), exactly('1')), |
f91f0fd5 | 297 | (k("12 123 * * h"), error(OrchestratorValidationError, 'Cannot place <ServiceSpec for service_name=mgr> on 3: Unknown hosts')), |
f6b5b4d7 TL |
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')), | |
318 | ] | |
319 | ||
320 | ||
321 | @pytest.mark.parametrize("spec_section_key,spec_section", | |
f67539c2 | 322 | [ # noqa: E128 |
f6b5b4d7 TL |
323 | ('h', 'hosts'), |
324 | ('l', 'label'), | |
325 | ('p', 'host_pattern'), | |
326 | ]) | |
327 | @pytest.mark.parametrize("daemons_key, daemons", | |
f67539c2 | 328 | [ # noqa: E128 |
f6b5b4d7 TL |
329 | ('e', []), |
330 | ('1', ['1']), | |
331 | ('3', ['3']), | |
332 | ('12', ['1', '2']), | |
333 | ('112', ['1', '1', '2']), # deal with existing co-located daemons | |
334 | ('23', ['2', '3']), | |
335 | ('123', ['1', '2', '3']), | |
336 | ]) | |
337 | @pytest.mark.parametrize("count", | |
f67539c2 | 338 | [ # noqa: E128 |
f6b5b4d7 TL |
339 | None, |
340 | 1, | |
341 | 2, | |
342 | 3, | |
343 | ]) | |
344 | @pytest.mark.parametrize("explicit_key, explicit", | |
f67539c2 | 345 | [ # noqa: E128 |
f6b5b4d7 TL |
346 | ('1', ['1']), |
347 | ('123', ['1', '2', '3']), | |
348 | ]) | |
349 | @pytest.mark.parametrize("host_key, hosts", | |
f67539c2 | 350 | [ # noqa: E128 |
f6b5b4d7 TL |
351 | ('1', ['1']), |
352 | ('12', ['1', '2']), | |
353 | ('123', ['1', '2', '3']), | |
354 | ]) | |
355 | def test_scheduler_daemons(host_key, hosts, | |
356 | explicit_key, explicit, | |
357 | count, | |
358 | daemons_key, daemons, | |
359 | spec_section_key, spec_section): | |
f91f0fd5 | 360 | mk_spec, hosts = mk_spec_and_host(spec_section, hosts, explicit_key, explicit, count) |
f6b5b4d7 | 361 | dds = [ |
f91f0fd5 | 362 | DaemonDescription('mgr', d, d) |
f6b5b4d7 TL |
363 | for d in daemons |
364 | ] | |
365 | run_scheduler_test( | |
366 | results=test_scheduler_daemons_results, | |
367 | mk_spec=mk_spec, | |
f91f0fd5 | 368 | hosts=hosts, |
f67539c2 | 369 | daemons=dds, |
f6b5b4d7 TL |
370 | key_elems=(host_key, explicit_key, count, daemons_key, spec_section_key) |
371 | ) | |
372 | ||
373 | ||
f91f0fd5 | 374 | # ========================= |
9f95a23c TL |
375 | |
376 | ||
377 | class NodeAssignmentTest(NamedTuple): | |
378 | service_type: str | |
379 | placement: PlacementSpec | |
380 | hosts: List[str] | |
381 | daemons: List[DaemonDescription] | |
b3b6e05e TL |
382 | rank_map: Optional[Dict[int, Dict[int, Optional[str]]]] |
383 | post_rank_map: Optional[Dict[int, Dict[int, Optional[str]]]] | |
9f95a23c | 384 | expected: List[str] |
f67539c2 TL |
385 | expected_add: List[str] |
386 | expected_remove: List[DaemonDescription] | |
9f95a23c | 387 | |
f67539c2 | 388 | |
b3b6e05e | 389 | @pytest.mark.parametrize("service_type,placement,hosts,daemons,rank_map,post_rank_map,expected,expected_add,expected_remove", |
f67539c2 | 390 | [ # noqa: E128 |
9f95a23c TL |
391 | # just hosts |
392 | NodeAssignmentTest( | |
f91f0fd5 | 393 | 'mgr', |
f67539c2 | 394 | PlacementSpec(hosts=['smithi060']), |
9f95a23c TL |
395 | ['smithi060'], |
396 | [], | |
b3b6e05e | 397 | None, None, |
f67539c2 | 398 | ['mgr:smithi060'], ['mgr:smithi060'], [] |
9f95a23c TL |
399 | ), |
400 | # all_hosts | |
401 | NodeAssignmentTest( | |
f91f0fd5 | 402 | 'mgr', |
9f95a23c TL |
403 | PlacementSpec(host_pattern='*'), |
404 | 'host1 host2 host3'.split(), | |
405 | [ | |
f91f0fd5 TL |
406 | DaemonDescription('mgr', 'a', 'host1'), |
407 | DaemonDescription('mgr', 'b', 'host2'), | |
9f95a23c | 408 | ], |
b3b6e05e | 409 | None, None, |
f67539c2 TL |
410 | ['mgr:host1', 'mgr:host2', 'mgr:host3'], |
411 | ['mgr:host3'], | |
412 | [] | |
413 | ), | |
414 | # all_hosts + count_per_host | |
415 | NodeAssignmentTest( | |
416 | 'mds', | |
417 | PlacementSpec(host_pattern='*', count_per_host=2), | |
418 | 'host1 host2 host3'.split(), | |
419 | [ | |
420 | DaemonDescription('mds', 'a', 'host1'), | |
421 | DaemonDescription('mds', 'b', 'host2'), | |
422 | ], | |
b3b6e05e | 423 | None, None, |
f67539c2 TL |
424 | ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'], |
425 | ['mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'], | |
426 | [] | |
9f95a23c TL |
427 | ), |
428 | # count that is bigger than the amount of hosts. Truncate to len(hosts) | |
f67539c2 | 429 | # mgr should not be co-located to each other. |
9f95a23c | 430 | NodeAssignmentTest( |
f67539c2 | 431 | 'mgr', |
9f95a23c TL |
432 | PlacementSpec(count=4), |
433 | 'host1 host2 host3'.split(), | |
434 | [], | |
b3b6e05e | 435 | None, None, |
f67539c2 TL |
436 | ['mgr:host1', 'mgr:host2', 'mgr:host3'], |
437 | ['mgr:host1', 'mgr:host2', 'mgr:host3'], | |
438 | [] | |
439 | ), | |
440 | # count that is bigger than the amount of hosts; wrap around. | |
441 | NodeAssignmentTest( | |
442 | 'mds', | |
443 | PlacementSpec(count=6), | |
444 | 'host1 host2 host3'.split(), | |
445 | [], | |
b3b6e05e | 446 | None, None, |
f67539c2 TL |
447 | ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'], |
448 | ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'], | |
449 | [] | |
9f95a23c TL |
450 | ), |
451 | # count + partial host list | |
452 | NodeAssignmentTest( | |
f91f0fd5 | 453 | 'mgr', |
9f95a23c TL |
454 | PlacementSpec(count=3, hosts=['host3']), |
455 | 'host1 host2 host3'.split(), | |
456 | [ | |
f91f0fd5 TL |
457 | DaemonDescription('mgr', 'a', 'host1'), |
458 | DaemonDescription('mgr', 'b', 'host2'), | |
9f95a23c | 459 | ], |
b3b6e05e | 460 | None, None, |
f67539c2 TL |
461 | ['mgr:host3'], |
462 | ['mgr:host3'], | |
463 | ['mgr.a', 'mgr.b'] | |
464 | ), | |
465 | # count + partial host list (with colo) | |
466 | NodeAssignmentTest( | |
467 | 'mds', | |
468 | PlacementSpec(count=3, hosts=['host3']), | |
469 | 'host1 host2 host3'.split(), | |
470 | [ | |
471 | DaemonDescription('mds', 'a', 'host1'), | |
472 | DaemonDescription('mds', 'b', 'host2'), | |
473 | ], | |
b3b6e05e | 474 | None, None, |
f67539c2 TL |
475 | ['mds:host3', 'mds:host3', 'mds:host3'], |
476 | ['mds:host3', 'mds:host3', 'mds:host3'], | |
477 | ['mds.a', 'mds.b'] | |
9f95a23c TL |
478 | ), |
479 | # count 1 + partial host list | |
480 | NodeAssignmentTest( | |
f91f0fd5 | 481 | 'mgr', |
9f95a23c TL |
482 | PlacementSpec(count=1, hosts=['host3']), |
483 | 'host1 host2 host3'.split(), | |
484 | [ | |
f91f0fd5 TL |
485 | DaemonDescription('mgr', 'a', 'host1'), |
486 | DaemonDescription('mgr', 'b', 'host2'), | |
9f95a23c | 487 | ], |
b3b6e05e | 488 | None, None, |
f67539c2 TL |
489 | ['mgr:host3'], |
490 | ['mgr:host3'], | |
491 | ['mgr.a', 'mgr.b'] | |
9f95a23c TL |
492 | ), |
493 | # count + partial host list + existing | |
494 | NodeAssignmentTest( | |
f91f0fd5 | 495 | 'mgr', |
9f95a23c TL |
496 | PlacementSpec(count=2, hosts=['host3']), |
497 | 'host1 host2 host3'.split(), | |
498 | [ | |
f91f0fd5 | 499 | DaemonDescription('mgr', 'a', 'host1'), |
9f95a23c | 500 | ], |
b3b6e05e | 501 | None, None, |
f67539c2 TL |
502 | ['mgr:host3'], |
503 | ['mgr:host3'], | |
504 | ['mgr.a'] | |
9f95a23c TL |
505 | ), |
506 | # count + partial host list + existing (deterministic) | |
507 | NodeAssignmentTest( | |
f91f0fd5 | 508 | 'mgr', |
9f95a23c TL |
509 | PlacementSpec(count=2, hosts=['host1']), |
510 | 'host1 host2'.split(), | |
511 | [ | |
f91f0fd5 | 512 | DaemonDescription('mgr', 'a', 'host1'), |
9f95a23c | 513 | ], |
b3b6e05e | 514 | None, None, |
f67539c2 TL |
515 | ['mgr:host1'], |
516 | [], | |
517 | [] | |
9f95a23c TL |
518 | ), |
519 | # count + partial host list + existing (deterministic) | |
520 | NodeAssignmentTest( | |
f91f0fd5 | 521 | 'mgr', |
9f95a23c TL |
522 | PlacementSpec(count=2, hosts=['host1']), |
523 | 'host1 host2'.split(), | |
524 | [ | |
f91f0fd5 | 525 | DaemonDescription('mgr', 'a', 'host2'), |
9f95a23c | 526 | ], |
b3b6e05e | 527 | None, None, |
f67539c2 TL |
528 | ['mgr:host1'], |
529 | ['mgr:host1'], | |
530 | ['mgr.a'] | |
9f95a23c TL |
531 | ), |
532 | # label only | |
533 | NodeAssignmentTest( | |
f91f0fd5 | 534 | 'mgr', |
9f95a23c TL |
535 | PlacementSpec(label='foo'), |
536 | 'host1 host2 host3'.split(), | |
537 | [], | |
b3b6e05e | 538 | None, None, |
f67539c2 TL |
539 | ['mgr:host1', 'mgr:host2', 'mgr:host3'], |
540 | ['mgr:host1', 'mgr:host2', 'mgr:host3'], | |
541 | [] | |
542 | ), | |
543 | # label + count (truncate to host list) | |
544 | NodeAssignmentTest( | |
545 | 'mgr', | |
546 | PlacementSpec(count=4, label='foo'), | |
547 | 'host1 host2 host3'.split(), | |
548 | [], | |
b3b6e05e | 549 | None, None, |
f67539c2 TL |
550 | ['mgr:host1', 'mgr:host2', 'mgr:host3'], |
551 | ['mgr:host1', 'mgr:host2', 'mgr:host3'], | |
552 | [] | |
553 | ), | |
554 | # label + count (with colo) | |
555 | NodeAssignmentTest( | |
556 | 'mds', | |
557 | PlacementSpec(count=6, label='foo'), | |
558 | 'host1 host2 host3'.split(), | |
559 | [], | |
b3b6e05e | 560 | None, None, |
f67539c2 TL |
561 | ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'], |
562 | ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3'], | |
563 | [] | |
564 | ), | |
565 | # label only + count_per_hst | |
566 | NodeAssignmentTest( | |
567 | 'mds', | |
568 | PlacementSpec(label='foo', count_per_host=3), | |
569 | 'host1 host2 host3'.split(), | |
570 | [], | |
b3b6e05e | 571 | None, None, |
f67539c2 TL |
572 | ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3', |
573 | 'mds:host1', 'mds:host2', 'mds:host3'], | |
574 | ['mds:host1', 'mds:host2', 'mds:host3', 'mds:host1', 'mds:host2', 'mds:host3', | |
575 | 'mds:host1', 'mds:host2', 'mds:host3'], | |
576 | [] | |
9f95a23c TL |
577 | ), |
578 | # host_pattern | |
579 | NodeAssignmentTest( | |
f91f0fd5 TL |
580 | 'mgr', |
581 | PlacementSpec(host_pattern='mgr*'), | |
582 | 'mgrhost1 mgrhost2 datahost'.split(), | |
9f95a23c | 583 | [], |
b3b6e05e | 584 | None, None, |
f67539c2 TL |
585 | ['mgr:mgrhost1', 'mgr:mgrhost2'], |
586 | ['mgr:mgrhost1', 'mgr:mgrhost2'], | |
587 | [] | |
588 | ), | |
589 | # host_pattern + count_per_host | |
590 | NodeAssignmentTest( | |
591 | 'mds', | |
592 | PlacementSpec(host_pattern='mds*', count_per_host=3), | |
593 | 'mdshost1 mdshost2 datahost'.split(), | |
594 | [], | |
b3b6e05e | 595 | None, None, |
f67539c2 TL |
596 | ['mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2'], |
597 | ['mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2', 'mds:mdshost1', 'mds:mdshost2'], | |
598 | [] | |
599 | ), | |
600 | # label + count_per_host + ports | |
601 | NodeAssignmentTest( | |
602 | 'rgw', | |
603 | PlacementSpec(count=6, label='foo'), | |
604 | 'host1 host2 host3'.split(), | |
605 | [], | |
b3b6e05e | 606 | None, None, |
f67539c2 TL |
607 | ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)', |
608 | 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'], | |
609 | ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)', | |
610 | 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'], | |
611 | [] | |
612 | ), | |
613 | # label + count_per_host + ports (+ xisting) | |
614 | NodeAssignmentTest( | |
615 | 'rgw', | |
616 | PlacementSpec(count=6, label='foo'), | |
617 | 'host1 host2 host3'.split(), | |
618 | [ | |
619 | DaemonDescription('rgw', 'a', 'host1', ports=[81]), | |
620 | DaemonDescription('rgw', 'b', 'host2', ports=[80]), | |
621 | DaemonDescription('rgw', 'c', 'host1', ports=[82]), | |
622 | ], | |
b3b6e05e | 623 | None, None, |
f67539c2 TL |
624 | ['rgw:host1(*:80)', 'rgw:host2(*:80)', 'rgw:host3(*:80)', |
625 | 'rgw:host1(*:81)', 'rgw:host2(*:81)', 'rgw:host3(*:81)'], | |
626 | ['rgw:host1(*:80)', 'rgw:host3(*:80)', | |
627 | 'rgw:host2(*:81)', 'rgw:host3(*:81)'], | |
628 | ['rgw.c'] | |
629 | ), | |
630 | # cephadm.py teuth case | |
631 | NodeAssignmentTest( | |
632 | 'mgr', | |
633 | PlacementSpec(count=3, hosts=['host1=y', 'host2=x']), | |
634 | 'host1 host2'.split(), | |
635 | [ | |
636 | DaemonDescription('mgr', 'y', 'host1'), | |
637 | DaemonDescription('mgr', 'x', 'host2'), | |
638 | ], | |
b3b6e05e | 639 | None, None, |
f67539c2 TL |
640 | ['mgr:host1(name=y)', 'mgr:host2(name=x)'], |
641 | [], [] | |
9f95a23c | 642 | ), |
b3b6e05e TL |
643 | |
644 | # note: host -> rank mapping is psuedo-random based on svc name, so these | |
645 | # host/rank pairs may seem random but they match the nfs.mynfs seed used by | |
646 | # the test. | |
647 | ||
648 | # ranked, fresh | |
649 | NodeAssignmentTest( | |
650 | 'nfs', | |
651 | PlacementSpec(count=3), | |
652 | 'host1 host2 host3'.split(), | |
653 | [], | |
654 | {}, | |
655 | {0: {0: None}, 1: {0: None}, 2: {0: None}}, | |
656 | ['nfs:host1(rank=0.0)', 'nfs:host2(rank=1.0)', 'nfs:host3(rank=2.0)'], | |
657 | ['nfs:host1(rank=0.0)', 'nfs:host2(rank=1.0)', 'nfs:host3(rank=2.0)'], | |
658 | [] | |
659 | ), | |
660 | # 21: ranked, exist | |
661 | NodeAssignmentTest( | |
662 | 'nfs', | |
663 | PlacementSpec(count=3), | |
664 | 'host1 host2 host3'.split(), | |
665 | [ | |
666 | DaemonDescription('nfs', '0.1', 'host1', rank=0, rank_generation=1), | |
667 | ], | |
668 | {0: {1: '0.1'}}, | |
669 | {0: {1: '0.1'}, 1: {0: None}, 2: {0: None}}, | |
670 | ['nfs:host1(rank=0.1)', 'nfs:host2(rank=1.0)', 'nfs:host3(rank=2.0)'], | |
671 | ['nfs:host2(rank=1.0)', 'nfs:host3(rank=2.0)'], | |
672 | [] | |
673 | ), | |
674 | # ranked, exist, different ranks | |
675 | NodeAssignmentTest( | |
676 | 'nfs', | |
677 | PlacementSpec(count=3), | |
678 | 'host1 host2 host3'.split(), | |
679 | [ | |
680 | DaemonDescription('nfs', '0.1', 'host1', rank=0, rank_generation=1), | |
681 | DaemonDescription('nfs', '1.1', 'host2', rank=1, rank_generation=1), | |
682 | ], | |
683 | {0: {1: '0.1'}, 1: {1: '1.1'}}, | |
684 | {0: {1: '0.1'}, 1: {1: '1.1'}, 2: {0: None}}, | |
685 | ['nfs:host1(rank=0.1)', 'nfs:host2(rank=1.1)', 'nfs:host3(rank=2.0)'], | |
686 | ['nfs:host3(rank=2.0)'], | |
687 | [] | |
688 | ), | |
689 | # ranked, exist, different ranks (2) | |
690 | NodeAssignmentTest( | |
691 | 'nfs', | |
692 | PlacementSpec(count=3), | |
693 | 'host1 host2 host3'.split(), | |
694 | [ | |
695 | DaemonDescription('nfs', '0.1', 'host1', rank=0, rank_generation=1), | |
696 | DaemonDescription('nfs', '1.1', 'host3', rank=1, rank_generation=1), | |
697 | ], | |
698 | {0: {1: '0.1'}, 1: {1: '1.1'}}, | |
699 | {0: {1: '0.1'}, 1: {1: '1.1'}, 2: {0: None}}, | |
700 | ['nfs:host1(rank=0.1)', 'nfs:host3(rank=1.1)', 'nfs:host2(rank=2.0)'], | |
701 | ['nfs:host2(rank=2.0)'], | |
702 | [] | |
703 | ), | |
704 | # ranked, exist, extra ranks | |
705 | NodeAssignmentTest( | |
706 | 'nfs', | |
707 | PlacementSpec(count=3), | |
708 | 'host1 host2 host3'.split(), | |
709 | [ | |
710 | DaemonDescription('nfs', '0.5', 'host1', rank=0, rank_generation=5), | |
711 | DaemonDescription('nfs', '1.5', 'host2', rank=1, rank_generation=5), | |
712 | DaemonDescription('nfs', '4.5', 'host2', rank=4, rank_generation=5), | |
713 | ], | |
714 | {0: {5: '0.5'}, 1: {5: '1.5'}}, | |
715 | {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {0: None}}, | |
716 | ['nfs:host1(rank=0.5)', 'nfs:host2(rank=1.5)', 'nfs:host3(rank=2.0)'], | |
717 | ['nfs:host3(rank=2.0)'], | |
718 | ['nfs.4.5'] | |
719 | ), | |
720 | # 25: ranked, exist, extra ranks (scale down: kill off high rank) | |
721 | NodeAssignmentTest( | |
722 | 'nfs', | |
723 | PlacementSpec(count=2), | |
724 | 'host3 host2 host1'.split(), | |
725 | [ | |
726 | DaemonDescription('nfs', '0.5', 'host1', rank=0, rank_generation=5), | |
727 | DaemonDescription('nfs', '1.5', 'host2', rank=1, rank_generation=5), | |
728 | DaemonDescription('nfs', '2.5', 'host3', rank=2, rank_generation=5), | |
729 | ], | |
730 | {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {5: '2.5'}}, | |
731 | {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {5: '2.5'}}, | |
732 | ['nfs:host1(rank=0.5)', 'nfs:host2(rank=1.5)'], | |
733 | [], | |
734 | ['nfs.2.5'] | |
735 | ), | |
736 | # ranked, exist, extra ranks (scale down hosts) | |
737 | NodeAssignmentTest( | |
738 | 'nfs', | |
739 | PlacementSpec(count=2), | |
740 | 'host1 host3'.split(), | |
741 | [ | |
742 | DaemonDescription('nfs', '0.5', 'host1', rank=0, rank_generation=5), | |
743 | DaemonDescription('nfs', '1.5', 'host2', rank=1, rank_generation=5), | |
744 | DaemonDescription('nfs', '2.5', 'host3', rank=4, rank_generation=5), | |
745 | ], | |
746 | {0: {5: '0.5'}, 1: {5: '1.5'}, 2: {5: '2.5'}}, | |
747 | {0: {5: '0.5'}, 1: {5: '1.5', 6: None}, 2: {5: '2.5'}}, | |
748 | ['nfs:host1(rank=0.5)', 'nfs:host3(rank=1.6)'], | |
749 | ['nfs:host3(rank=1.6)'], | |
750 | ['nfs.2.5', 'nfs.1.5'] | |
751 | ), | |
752 | # ranked, exist, duplicate rank | |
753 | NodeAssignmentTest( | |
754 | 'nfs', | |
755 | PlacementSpec(count=3), | |
756 | 'host1 host2 host3'.split(), | |
757 | [ | |
758 | DaemonDescription('nfs', '0.0', 'host1', rank=0, rank_generation=0), | |
759 | DaemonDescription('nfs', '1.1', 'host2', rank=1, rank_generation=1), | |
760 | DaemonDescription('nfs', '1.2', 'host3', rank=1, rank_generation=2), | |
761 | ], | |
762 | {0: {0: '0.0'}, 1: {2: '1.2'}}, | |
763 | {0: {0: '0.0'}, 1: {2: '1.2'}, 2: {0: None}}, | |
764 | ['nfs:host1(rank=0.0)', 'nfs:host3(rank=1.2)', 'nfs:host2(rank=2.0)'], | |
765 | ['nfs:host2(rank=2.0)'], | |
766 | ['nfs.1.1'] | |
767 | ), | |
768 | # 28: ranked, all gens stale (failure during update cycle) | |
769 | NodeAssignmentTest( | |
770 | 'nfs', | |
771 | PlacementSpec(count=2), | |
772 | 'host1 host2 host3'.split(), | |
773 | [ | |
774 | DaemonDescription('nfs', '0.2', 'host1', rank=0, rank_generation=2), | |
775 | DaemonDescription('nfs', '1.2', 'host2', rank=1, rank_generation=2), | |
776 | ], | |
777 | {0: {2: '0.2'}, 1: {2: '1.2', 3: '1.3'}}, | |
778 | {0: {2: '0.2'}, 1: {2: '1.2', 3: '1.3', 4: None}}, | |
779 | ['nfs:host1(rank=0.2)', 'nfs:host2(rank=1.4)'], | |
780 | ['nfs:host2(rank=1.4)'], | |
781 | ['nfs.1.2'] | |
782 | ), | |
783 | # ranked, not enough hosts | |
784 | NodeAssignmentTest( | |
785 | 'nfs', | |
786 | PlacementSpec(count=4), | |
787 | 'host1 host2 host3'.split(), | |
788 | [ | |
789 | DaemonDescription('nfs', '0.2', 'host1', rank=0, rank_generation=2), | |
790 | DaemonDescription('nfs', '1.2', 'host2', rank=1, rank_generation=2), | |
791 | ], | |
792 | {0: {2: '0.2'}, 1: {2: '1.2'}}, | |
793 | {0: {2: '0.2'}, 1: {2: '1.2'}, 2: {0: None}}, | |
794 | ['nfs:host1(rank=0.2)', 'nfs:host2(rank=1.2)', 'nfs:host3(rank=2.0)'], | |
795 | ['nfs:host3(rank=2.0)'], | |
796 | [] | |
797 | ), | |
798 | # ranked, scale down | |
799 | NodeAssignmentTest( | |
800 | 'nfs', | |
801 | PlacementSpec(hosts=['host2']), | |
802 | 'host1 host2'.split(), | |
803 | [ | |
804 | DaemonDescription('nfs', '0.2', 'host1', rank=0, rank_generation=2), | |
805 | DaemonDescription('nfs', '1.2', 'host2', rank=1, rank_generation=2), | |
806 | DaemonDescription('nfs', '2.2', 'host3', rank=2, rank_generation=2), | |
807 | ], | |
808 | {0: {2: '0.2'}, 1: {2: '1.2'}, 2: {2: '2.2'}}, | |
809 | {0: {2: '0.2', 3: None}, 1: {2: '1.2'}, 2: {2: '2.2'}}, | |
810 | ['nfs:host2(rank=0.3)'], | |
811 | ['nfs:host2(rank=0.3)'], | |
812 | ['nfs.0.2', 'nfs.1.2', 'nfs.2.2'] | |
813 | ), | |
814 | ||
9f95a23c | 815 | ]) |
b3b6e05e | 816 | def test_node_assignment(service_type, placement, hosts, daemons, rank_map, post_rank_map, |
f67539c2 | 817 | expected, expected_add, expected_remove): |
b3b6e05e | 818 | spec = None |
f6b5b4d7 | 819 | service_id = None |
f67539c2 | 820 | allow_colo = False |
f6b5b4d7 TL |
821 | if service_type == 'rgw': |
822 | service_id = 'realm.zone' | |
f67539c2 TL |
823 | allow_colo = True |
824 | elif service_type == 'mds': | |
825 | service_id = 'myfs' | |
826 | allow_colo = True | |
b3b6e05e TL |
827 | elif service_type == 'nfs': |
828 | service_id = 'mynfs' | |
829 | spec = ServiceSpec(service_type=service_type, | |
830 | service_id=service_id, | |
831 | placement=placement, | |
832 | pool='foo') | |
833 | ||
834 | if not spec: | |
835 | spec = ServiceSpec(service_type=service_type, | |
836 | service_id=service_id, | |
837 | placement=placement) | |
f6b5b4d7 | 838 | |
f67539c2 | 839 | all_slots, to_add, to_remove = HostAssignment( |
f6b5b4d7 | 840 | spec=spec, |
f91f0fd5 | 841 | hosts=[HostSpec(h, labels=['foo']) for h in hosts], |
f67539c2 TL |
842 | daemons=daemons, |
843 | allow_colo=allow_colo, | |
b3b6e05e | 844 | rank_map=rank_map, |
f67539c2 TL |
845 | ).place() |
846 | ||
b3b6e05e TL |
847 | assert rank_map == post_rank_map |
848 | ||
f67539c2 TL |
849 | got = [str(p) for p in all_slots] |
850 | num_wildcard = 0 | |
851 | for i in expected: | |
852 | if i == '*': | |
853 | num_wildcard += 1 | |
854 | else: | |
855 | assert i in got | |
856 | got.remove(i) | |
857 | assert num_wildcard == len(got) | |
858 | ||
859 | got = [str(p) for p in to_add] | |
860 | num_wildcard = 0 | |
861 | for i in expected_add: | |
862 | if i == '*': | |
863 | num_wildcard += 1 | |
864 | else: | |
865 | assert i in got | |
866 | got.remove(i) | |
867 | assert num_wildcard == len(got) | |
868 | ||
869 | assert sorted([d.name() for d in to_remove]) == sorted(expected_remove) | |
9f95a23c | 870 | |
e306af50 | 871 | |
9f95a23c TL |
872 | class NodeAssignmentTest2(NamedTuple): |
873 | service_type: str | |
874 | placement: PlacementSpec | |
875 | hosts: List[str] | |
876 | daemons: List[DaemonDescription] | |
877 | expected_len: int | |
878 | in_set: List[str] | |
879 | ||
f67539c2 | 880 | |
9f95a23c | 881 | @pytest.mark.parametrize("service_type,placement,hosts,daemons,expected_len,in_set", |
f67539c2 | 882 | [ # noqa: E128 |
9f95a23c TL |
883 | # just count |
884 | NodeAssignmentTest2( | |
f91f0fd5 | 885 | 'mgr', |
9f95a23c TL |
886 | PlacementSpec(count=1), |
887 | 'host1 host2 host3'.split(), | |
888 | [], | |
889 | 1, | |
890 | ['host1', 'host2', 'host3'], | |
891 | ), | |
892 | ||
893 | # hosts + (smaller) count | |
894 | NodeAssignmentTest2( | |
f91f0fd5 | 895 | 'mgr', |
9f95a23c TL |
896 | PlacementSpec(count=1, hosts='host1 host2'.split()), |
897 | 'host1 host2'.split(), | |
898 | [], | |
899 | 1, | |
900 | ['host1', 'host2'], | |
901 | ), | |
902 | # hosts + (smaller) count, existing | |
903 | NodeAssignmentTest2( | |
f91f0fd5 | 904 | 'mgr', |
9f95a23c TL |
905 | PlacementSpec(count=1, hosts='host1 host2 host3'.split()), |
906 | 'host1 host2 host3'.split(), | |
f67539c2 | 907 | [DaemonDescription('mgr', 'mgr.a', 'host1')], |
9f95a23c TL |
908 | 1, |
909 | ['host1', 'host2', 'host3'], | |
910 | ), | |
911 | # hosts + (smaller) count, (more) existing | |
912 | NodeAssignmentTest2( | |
f91f0fd5 | 913 | 'mgr', |
9f95a23c TL |
914 | PlacementSpec(count=1, hosts='host1 host2 host3'.split()), |
915 | 'host1 host2 host3'.split(), | |
916 | [ | |
f91f0fd5 TL |
917 | DaemonDescription('mgr', 'a', 'host1'), |
918 | DaemonDescription('mgr', 'b', 'host2'), | |
9f95a23c TL |
919 | ], |
920 | 1, | |
921 | ['host1', 'host2'] | |
922 | ), | |
923 | # count + partial host list | |
924 | NodeAssignmentTest2( | |
f91f0fd5 | 925 | 'mgr', |
9f95a23c TL |
926 | PlacementSpec(count=2, hosts=['host3']), |
927 | 'host1 host2 host3'.split(), | |
928 | [], | |
f6b5b4d7 | 929 | 1, |
9f95a23c TL |
930 | ['host1', 'host2', 'host3'] |
931 | ), | |
932 | # label + count | |
933 | NodeAssignmentTest2( | |
f91f0fd5 | 934 | 'mgr', |
9f95a23c TL |
935 | PlacementSpec(count=1, label='foo'), |
936 | 'host1 host2 host3'.split(), | |
937 | [], | |
938 | 1, | |
939 | ['host1', 'host2', 'host3'] | |
940 | ), | |
941 | ]) | |
942 | def test_node_assignment2(service_type, placement, hosts, | |
943 | daemons, expected_len, in_set): | |
f67539c2 | 944 | hosts, to_add, to_remove = HostAssignment( |
9f95a23c | 945 | spec=ServiceSpec(service_type, placement=placement), |
f91f0fd5 | 946 | hosts=[HostSpec(h, labels=['foo']) for h in hosts], |
f67539c2 TL |
947 | daemons=daemons, |
948 | ).place() | |
9f95a23c TL |
949 | assert len(hosts) == expected_len |
950 | for h in [h.hostname for h in hosts]: | |
951 | assert h in in_set | |
952 | ||
f67539c2 | 953 | |
9f95a23c | 954 | @pytest.mark.parametrize("service_type,placement,hosts,daemons,expected_len,must_have", |
f67539c2 | 955 | [ # noqa: E128 |
9f95a23c TL |
956 | # hosts + (smaller) count, (more) existing |
957 | NodeAssignmentTest2( | |
f91f0fd5 | 958 | 'mgr', |
9f95a23c TL |
959 | PlacementSpec(count=3, hosts='host3'.split()), |
960 | 'host1 host2 host3'.split(), | |
961 | [], | |
f6b5b4d7 | 962 | 1, |
9f95a23c TL |
963 | ['host3'] |
964 | ), | |
965 | # count + partial host list | |
966 | NodeAssignmentTest2( | |
f91f0fd5 | 967 | 'mgr', |
9f95a23c TL |
968 | PlacementSpec(count=2, hosts=['host3']), |
969 | 'host1 host2 host3'.split(), | |
970 | [], | |
f6b5b4d7 | 971 | 1, |
9f95a23c TL |
972 | ['host3'] |
973 | ), | |
974 | ]) | |
975 | def test_node_assignment3(service_type, placement, hosts, | |
976 | daemons, expected_len, must_have): | |
f67539c2 | 977 | hosts, to_add, to_remove = HostAssignment( |
9f95a23c | 978 | spec=ServiceSpec(service_type, placement=placement), |
f91f0fd5 | 979 | hosts=[HostSpec(h) for h in hosts], |
f67539c2 TL |
980 | daemons=daemons, |
981 | ).place() | |
9f95a23c TL |
982 | assert len(hosts) == expected_len |
983 | for h in must_have: | |
984 | assert h in [h.hostname for h in hosts] | |
985 | ||
986 | ||
f67539c2 TL |
987 | class NodeAssignmentTest4(NamedTuple): |
988 | spec: ServiceSpec | |
989 | networks: Dict[str, Dict[str, Dict[str, List[str]]]] | |
990 | daemons: List[DaemonDescription] | |
991 | expected: List[str] | |
992 | expected_add: List[str] | |
993 | expected_remove: List[DaemonDescription] | |
994 | ||
995 | ||
996 | @pytest.mark.parametrize("spec,networks,daemons,expected,expected_add,expected_remove", | |
997 | [ # noqa: E128 | |
998 | NodeAssignmentTest4( | |
999 | ServiceSpec( | |
1000 | service_type='rgw', | |
1001 | service_id='foo', | |
1002 | placement=PlacementSpec(count=6, label='foo'), | |
1003 | networks=['10.0.0.0/8'], | |
1004 | ), | |
1005 | { | |
1006 | 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}}, | |
1007 | 'host2': {'10.0.0.0/8': {'eth0': ['10.0.0.2']}}, | |
1008 | 'host3': {'192.168.0.0/16': {'eth0': ['192.168.0.1']}}, | |
1009 | }, | |
1010 | [], | |
1011 | ['rgw:host1(10.0.0.1:80)', 'rgw:host2(10.0.0.2:80)', | |
1012 | 'rgw:host1(10.0.0.1:81)', 'rgw:host2(10.0.0.2:81)', | |
1013 | 'rgw:host1(10.0.0.1:82)', 'rgw:host2(10.0.0.2:82)'], | |
1014 | ['rgw:host1(10.0.0.1:80)', 'rgw:host2(10.0.0.2:80)', | |
1015 | 'rgw:host1(10.0.0.1:81)', 'rgw:host2(10.0.0.2:81)', | |
1016 | 'rgw:host1(10.0.0.1:82)', 'rgw:host2(10.0.0.2:82)'], | |
1017 | [] | |
1018 | ), | |
1019 | NodeAssignmentTest4( | |
1020 | IngressSpec( | |
1021 | service_type='ingress', | |
1022 | service_id='rgw.foo', | |
1023 | frontend_port=443, | |
1024 | monitor_port=8888, | |
1025 | virtual_ip='10.0.0.20/8', | |
1026 | backend_service='rgw.foo', | |
1027 | placement=PlacementSpec(label='foo'), | |
1028 | networks=['10.0.0.0/8'], | |
1029 | ), | |
1030 | { | |
1031 | 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}}, | |
1032 | 'host2': {'10.0.0.0/8': {'eth1': ['10.0.0.2']}}, | |
1033 | 'host3': {'192.168.0.0/16': {'eth2': ['192.168.0.1']}}, | |
1034 | }, | |
1035 | [], | |
1036 | ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)', | |
1037 | 'keepalived:host1', 'keepalived:host2'], | |
1038 | ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)', | |
1039 | 'keepalived:host1', 'keepalived:host2'], | |
1040 | [] | |
1041 | ), | |
1042 | NodeAssignmentTest4( | |
1043 | IngressSpec( | |
1044 | service_type='ingress', | |
1045 | service_id='rgw.foo', | |
1046 | frontend_port=443, | |
1047 | monitor_port=8888, | |
1048 | virtual_ip='10.0.0.20/8', | |
1049 | backend_service='rgw.foo', | |
1050 | placement=PlacementSpec(label='foo'), | |
1051 | networks=['10.0.0.0/8'], | |
1052 | ), | |
1053 | { | |
1054 | 'host1': {'10.0.0.0/8': {'eth0': ['10.0.0.1']}}, | |
1055 | 'host2': {'10.0.0.0/8': {'eth1': ['10.0.0.2']}}, | |
1056 | 'host3': {'192.168.0.0/16': {'eth2': ['192.168.0.1']}}, | |
1057 | }, | |
1058 | [ | |
1059 | DaemonDescription('haproxy', 'a', 'host1', ip='10.0.0.1', | |
1060 | ports=[443, 8888]), | |
1061 | DaemonDescription('keepalived', 'b', 'host2'), | |
1062 | DaemonDescription('keepalived', 'c', 'host3'), | |
1063 | ], | |
1064 | ['haproxy:host1(10.0.0.1:443,8888)', 'haproxy:host2(10.0.0.2:443,8888)', | |
1065 | 'keepalived:host1', 'keepalived:host2'], | |
1066 | ['haproxy:host2(10.0.0.2:443,8888)', | |
1067 | 'keepalived:host1'], | |
1068 | ['keepalived.c'] | |
1069 | ), | |
1070 | ]) | |
1071 | def test_node_assignment4(spec, networks, daemons, | |
1072 | expected, expected_add, expected_remove): | |
1073 | all_slots, to_add, to_remove = HostAssignment( | |
1074 | spec=spec, | |
1075 | hosts=[HostSpec(h, labels=['foo']) for h in networks.keys()], | |
1076 | daemons=daemons, | |
1077 | allow_colo=True, | |
1078 | networks=networks, | |
1079 | primary_daemon_type='haproxy' if spec.service_type == 'ingress' else spec.service_type, | |
1080 | per_host_daemon_type='keepalived' if spec.service_type == 'ingress' else None, | |
1081 | ).place() | |
1082 | ||
1083 | got = [str(p) for p in all_slots] | |
1084 | num_wildcard = 0 | |
1085 | for i in expected: | |
1086 | if i == '*': | |
1087 | num_wildcard += 1 | |
1088 | else: | |
1089 | assert i in got | |
1090 | got.remove(i) | |
1091 | assert num_wildcard == len(got) | |
1092 | ||
1093 | got = [str(p) for p in to_add] | |
1094 | num_wildcard = 0 | |
1095 | for i in expected_add: | |
1096 | if i == '*': | |
1097 | num_wildcard += 1 | |
1098 | else: | |
1099 | assert i in got | |
1100 | got.remove(i) | |
1101 | assert num_wildcard == len(got) | |
1102 | ||
1103 | assert sorted([d.name() for d in to_remove]) == sorted(expected_remove) | |
1104 | ||
1105 | ||
9f95a23c | 1106 | @pytest.mark.parametrize("placement", |
f67539c2 | 1107 | [ # noqa: E128 |
9f95a23c TL |
1108 | ('1 *'), |
1109 | ('* label:foo'), | |
1110 | ('* host1 host2'), | |
1111 | ('hostname12hostname12hostname12hostname12hostname12hostname12hostname12'), # > 63 chars | |
1112 | ]) | |
1113 | def test_bad_placements(placement): | |
1114 | try: | |
f67539c2 | 1115 | PlacementSpec.from_string(placement.split(' ')) |
9f95a23c | 1116 | assert False |
f67539c2 | 1117 | except SpecValidationError: |
9f95a23c TL |
1118 | pass |
1119 | ||
1120 | ||
1121 | class NodeAssignmentTestBadSpec(NamedTuple): | |
1122 | service_type: str | |
1123 | placement: PlacementSpec | |
1124 | hosts: List[str] | |
1125 | daemons: List[DaemonDescription] | |
1126 | expected: str | |
f67539c2 TL |
1127 | |
1128 | ||
9f95a23c | 1129 | @pytest.mark.parametrize("service_type,placement,hosts,daemons,expected", |
f67539c2 | 1130 | [ # noqa: E128 |
9f95a23c TL |
1131 | # unknown host |
1132 | NodeAssignmentTestBadSpec( | |
f91f0fd5 | 1133 | 'mgr', |
9f95a23c TL |
1134 | PlacementSpec(hosts=['unknownhost']), |
1135 | ['knownhost'], | |
1136 | [], | |
f91f0fd5 | 1137 | "Cannot place <ServiceSpec for service_name=mgr> on unknownhost: Unknown hosts" |
9f95a23c TL |
1138 | ), |
1139 | # unknown host pattern | |
1140 | NodeAssignmentTestBadSpec( | |
f91f0fd5 | 1141 | 'mgr', |
9f95a23c TL |
1142 | PlacementSpec(host_pattern='unknownhost'), |
1143 | ['knownhost'], | |
1144 | [], | |
f91f0fd5 | 1145 | "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts" |
9f95a23c TL |
1146 | ), |
1147 | # unknown label | |
1148 | NodeAssignmentTestBadSpec( | |
f91f0fd5 | 1149 | 'mgr', |
9f95a23c TL |
1150 | PlacementSpec(label='unknownlabel'), |
1151 | [], | |
1152 | [], | |
f91f0fd5 | 1153 | "Cannot place <ServiceSpec for service_name=mgr>: No matching hosts for label unknownlabel" |
9f95a23c TL |
1154 | ), |
1155 | ]) | |
1156 | def test_bad_specs(service_type, placement, hosts, daemons, expected): | |
1157 | with pytest.raises(OrchestratorValidationError) as e: | |
f67539c2 | 1158 | hosts, to_add, to_remove = HostAssignment( |
9f95a23c | 1159 | spec=ServiceSpec(service_type, placement=placement), |
f91f0fd5 | 1160 | hosts=[HostSpec(h) for h in hosts], |
f67539c2 TL |
1161 | daemons=daemons, |
1162 | ).place() | |
9f95a23c | 1163 | assert str(e.value) == expected |
f6b5b4d7 | 1164 | |
f67539c2 | 1165 | |
f6b5b4d7 TL |
1166 | class ActiveAssignmentTest(NamedTuple): |
1167 | service_type: str | |
1168 | placement: PlacementSpec | |
1169 | hosts: List[str] | |
1170 | daemons: List[DaemonDescription] | |
1171 | expected: List[List[str]] | |
f67539c2 TL |
1172 | expected_add: List[List[str]] |
1173 | expected_remove: List[List[str]] | |
f6b5b4d7 TL |
1174 | |
1175 | ||
f67539c2 | 1176 | @pytest.mark.parametrize("service_type,placement,hosts,daemons,expected,expected_add,expected_remove", |
f6b5b4d7 TL |
1177 | [ |
1178 | ActiveAssignmentTest( | |
1179 | 'mgr', | |
1180 | PlacementSpec(count=2), | |
1181 | 'host1 host2 host3'.split(), | |
1182 | [ | |
1183 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
1184 | DaemonDescription('mgr', 'b', 'host2'), | |
1185 | DaemonDescription('mgr', 'c', 'host3'), | |
1186 | ], | |
f67539c2 TL |
1187 | [['host1', 'host2'], ['host1', 'host3']], |
1188 | [[]], | |
1189 | [['mgr.b'], ['mgr.c']] | |
f6b5b4d7 TL |
1190 | ), |
1191 | ActiveAssignmentTest( | |
1192 | 'mgr', | |
1193 | PlacementSpec(count=2), | |
1194 | 'host1 host2 host3'.split(), | |
1195 | [ | |
1196 | DaemonDescription('mgr', 'a', 'host1'), | |
1197 | DaemonDescription('mgr', 'b', 'host2'), | |
1198 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1199 | ], | |
f67539c2 TL |
1200 | [['host1', 'host3'], ['host2', 'host3']], |
1201 | [[]], | |
1202 | [['mgr.a'], ['mgr.b']] | |
f6b5b4d7 TL |
1203 | ), |
1204 | ActiveAssignmentTest( | |
1205 | 'mgr', | |
1206 | PlacementSpec(count=1), | |
1207 | 'host1 host2 host3'.split(), | |
1208 | [ | |
1209 | DaemonDescription('mgr', 'a', 'host1'), | |
1210 | DaemonDescription('mgr', 'b', 'host2', is_active=True), | |
1211 | DaemonDescription('mgr', 'c', 'host3'), | |
1212 | ], | |
f67539c2 TL |
1213 | [['host2']], |
1214 | [[]], | |
1215 | [['mgr.a', 'mgr.c']] | |
f6b5b4d7 TL |
1216 | ), |
1217 | ActiveAssignmentTest( | |
1218 | 'mgr', | |
1219 | PlacementSpec(count=1), | |
1220 | 'host1 host2 host3'.split(), | |
1221 | [ | |
1222 | DaemonDescription('mgr', 'a', 'host1'), | |
1223 | DaemonDescription('mgr', 'b', 'host2'), | |
1224 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1225 | ], | |
f67539c2 TL |
1226 | [['host3']], |
1227 | [[]], | |
1228 | [['mgr.a', 'mgr.b']] | |
f6b5b4d7 TL |
1229 | ), |
1230 | ActiveAssignmentTest( | |
1231 | 'mgr', | |
1232 | PlacementSpec(count=1), | |
1233 | 'host1 host2 host3'.split(), | |
1234 | [ | |
1235 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
1236 | DaemonDescription('mgr', 'b', 'host2'), | |
1237 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1238 | ], | |
f67539c2 TL |
1239 | [['host1'], ['host3']], |
1240 | [[]], | |
1241 | [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c']] | |
f6b5b4d7 TL |
1242 | ), |
1243 | ActiveAssignmentTest( | |
1244 | 'mgr', | |
1245 | PlacementSpec(count=2), | |
1246 | 'host1 host2 host3'.split(), | |
1247 | [ | |
1248 | DaemonDescription('mgr', 'a', 'host1'), | |
1249 | DaemonDescription('mgr', 'b', 'host2', is_active=True), | |
1250 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1251 | ], | |
f67539c2 TL |
1252 | [['host2', 'host3']], |
1253 | [[]], | |
1254 | [['mgr.a']] | |
f6b5b4d7 TL |
1255 | ), |
1256 | ActiveAssignmentTest( | |
1257 | 'mgr', | |
1258 | PlacementSpec(count=1), | |
1259 | 'host1 host2 host3'.split(), | |
1260 | [ | |
1261 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
1262 | DaemonDescription('mgr', 'b', 'host2', is_active=True), | |
1263 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1264 | ], | |
f67539c2 TL |
1265 | [['host1'], ['host2'], ['host3']], |
1266 | [[]], | |
1267 | [['mgr.a', 'mgr.b'], ['mgr.b', 'mgr.c'], ['mgr.a', 'mgr.c']] | |
f6b5b4d7 TL |
1268 | ), |
1269 | ActiveAssignmentTest( | |
1270 | 'mgr', | |
1271 | PlacementSpec(count=1), | |
1272 | 'host1 host2 host3'.split(), | |
1273 | [ | |
1274 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
1275 | DaemonDescription('mgr', 'a2', 'host1'), | |
1276 | DaemonDescription('mgr', 'b', 'host2'), | |
1277 | DaemonDescription('mgr', 'c', 'host3'), | |
1278 | ], | |
f67539c2 TL |
1279 | [['host1']], |
1280 | [[]], | |
1281 | [['mgr.a2', 'mgr.b', 'mgr.c']] | |
f6b5b4d7 TL |
1282 | ), |
1283 | ActiveAssignmentTest( | |
1284 | 'mgr', | |
1285 | PlacementSpec(count=1), | |
1286 | 'host1 host2 host3'.split(), | |
1287 | [ | |
1288 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
1289 | DaemonDescription('mgr', 'a2', 'host1', is_active=True), | |
1290 | DaemonDescription('mgr', 'b', 'host2'), | |
1291 | DaemonDescription('mgr', 'c', 'host3'), | |
1292 | ], | |
f67539c2 TL |
1293 | [['host1']], |
1294 | [[]], | |
1295 | [['mgr.a', 'mgr.b', 'mgr.c'], ['mgr.a2', 'mgr.b', 'mgr.c']] | |
f6b5b4d7 TL |
1296 | ), |
1297 | ActiveAssignmentTest( | |
1298 | 'mgr', | |
1299 | PlacementSpec(count=2), | |
1300 | 'host1 host2 host3'.split(), | |
1301 | [ | |
1302 | DaemonDescription('mgr', 'a', 'host1', is_active=True), | |
1303 | DaemonDescription('mgr', 'a2', 'host1'), | |
1304 | DaemonDescription('mgr', 'b', 'host2'), | |
1305 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1306 | ], | |
f67539c2 TL |
1307 | [['host1', 'host3']], |
1308 | [[]], | |
1309 | [['mgr.a2', 'mgr.b']] | |
f6b5b4d7 TL |
1310 | ), |
1311 | # Explicit placement should override preference for active daemon | |
1312 | ActiveAssignmentTest( | |
1313 | 'mgr', | |
1314 | PlacementSpec(count=1, hosts=['host1']), | |
1315 | 'host1 host2 host3'.split(), | |
1316 | [ | |
1317 | DaemonDescription('mgr', 'a', 'host1'), | |
1318 | DaemonDescription('mgr', 'b', 'host2'), | |
1319 | DaemonDescription('mgr', 'c', 'host3', is_active=True), | |
1320 | ], | |
f67539c2 TL |
1321 | [['host1']], |
1322 | [[]], | |
1323 | [['mgr.b', 'mgr.c']] | |
f6b5b4d7 TL |
1324 | ), |
1325 | ||
1326 | ]) | |
f67539c2 | 1327 | def test_active_assignment(service_type, placement, hosts, daemons, expected, expected_add, expected_remove): |
f6b5b4d7 TL |
1328 | |
1329 | spec = ServiceSpec(service_type=service_type, | |
1330 | service_id=None, | |
1331 | placement=placement) | |
1332 | ||
f67539c2 | 1333 | hosts, to_add, to_remove = HostAssignment( |
f6b5b4d7 | 1334 | spec=spec, |
f91f0fd5 | 1335 | hosts=[HostSpec(h) for h in hosts], |
f67539c2 TL |
1336 | daemons=daemons, |
1337 | ).place() | |
f6b5b4d7 | 1338 | assert sorted([h.hostname for h in hosts]) in expected |
f67539c2 TL |
1339 | assert sorted([h.hostname for h in to_add]) in expected_add |
1340 | assert sorted([h.name() for h in to_remove]) in expected_remove |