]>
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, |
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 | # | | | | | | |
169 | test_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 | ]) | |
211 | def 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 | ]) | |
239 | def 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 | ]) | |
270 | def 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 | # | | | | | | | |
295 | test_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 | ]) | |
357 | def 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 | ||
379 | class 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 | 818 | def 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 |
874 | class 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 | ]) | |
944 | def 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 | ]) | |
978 | def 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 |
991 | class 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 | ]) | |
1075 | def 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 | ]) | |
1118 | def 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 | ||
1126 | class 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 | ]) | |
1161 | def 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 |
1172 | class 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 | 1333 | def 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 | ||
1350 | class 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 | ]) | |
1430 | def 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 | ||
1446 | class 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 | ]) | |
1499 | def 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 |