]>
Commit | Line | Data |
---|---|---|
3efd9988 | 1 | |
9f95a23c | 2 | from mgr_module import MgrModule, CommandResult |
3efd9988 FG |
3 | import threading |
4 | import random | |
5 | import json | |
6 | import errno | |
11fdf7f2 | 7 | import six |
3efd9988 FG |
8 | |
9 | ||
10 | class Module(MgrModule): | |
11 | """ | |
12 | This module is for testing the ceph-mgr python interface from within | |
13 | a running ceph-mgr daemon. | |
14 | ||
15 | It implements a sychronous self-test command for calling the functions | |
16 | in the MgrModule interface one by one, and a background "workload" | |
17 | command for causing the module to perform some thrashing-type | |
18 | activities in its serve() thread. | |
19 | """ | |
20 | ||
11fdf7f2 TL |
21 | # These workloads are things that can be requested to run inside the |
22 | # serve() function | |
3efd9988 | 23 | WORKLOAD_COMMAND_SPAM = "command_spam" |
11fdf7f2 | 24 | WORKLOAD_THROW_EXCEPTION = "throw_exception" |
3efd9988 FG |
25 | SHUTDOWN = "shutdown" |
26 | ||
11fdf7f2 TL |
27 | WORKLOADS = (WORKLOAD_COMMAND_SPAM, WORKLOAD_THROW_EXCEPTION) |
28 | ||
29 | # The test code in qa/ relies on these options existing -- they | |
30 | # are of course not really used for anything in the module | |
31 | MODULE_OPTIONS = [ | |
32 | {'name': 'testkey'}, | |
33 | {'name': 'testlkey'}, | |
34 | {'name': 'testnewline'}, | |
35 | {'name': 'roption1'}, | |
36 | {'name': 'roption2', 'type': 'str', 'default': 'xyz'}, | |
37 | {'name': 'rwoption1'}, | |
38 | {'name': 'rwoption2', 'type': 'int'}, | |
39 | {'name': 'rwoption3', 'type': 'float'}, | |
40 | {'name': 'rwoption4', 'type': 'str'}, | |
41 | {'name': 'rwoption5', 'type': 'bool'}, | |
42 | {'name': 'rwoption6', 'type': 'bool', 'default': True} | |
43 | ] | |
3efd9988 FG |
44 | |
45 | COMMANDS = [ | |
46 | { | |
47 | "cmd": "mgr self-test run", | |
48 | "desc": "Run mgr python interface tests", | |
11fdf7f2 | 49 | "perm": "rw" |
3efd9988 FG |
50 | }, |
51 | { | |
52 | "cmd": "mgr self-test background start name=workload,type=CephString", | |
53 | "desc": "Activate a background workload (one of {0})".format( | |
54 | ", ".join(WORKLOADS)), | |
11fdf7f2 | 55 | "perm": "rw" |
3efd9988 FG |
56 | }, |
57 | { | |
58 | "cmd": "mgr self-test background stop", | |
59 | "desc": "Stop background workload if any is running", | |
11fdf7f2 TL |
60 | "perm": "rw" |
61 | }, | |
62 | { | |
63 | "cmd": "mgr self-test config get name=key,type=CephString", | |
64 | "desc": "Peek at a configuration value", | |
65 | "perm": "rw" | |
66 | }, | |
67 | { | |
68 | "cmd": "mgr self-test config get_localized name=key,type=CephString", | |
69 | "desc": "Peek at a configuration value (localized variant)", | |
70 | "perm": "rw" | |
71 | }, | |
72 | { | |
73 | "cmd": "mgr self-test remote", | |
74 | "desc": "Test inter-module calls", | |
75 | "perm": "rw" | |
76 | }, | |
77 | { | |
78 | "cmd": "mgr self-test module name=module,type=CephString", | |
79 | "desc": "Run another module's self_test() method", | |
80 | "perm": "rw" | |
81 | }, | |
82 | { | |
83 | "cmd": "mgr self-test health set name=checks,type=CephString", | |
84 | "desc": "Set a health check from a JSON-formatted description.", | |
85 | "perm": "rw" | |
86 | }, | |
87 | { | |
88 | "cmd": "mgr self-test health clear name=checks,type=CephString,n=N,req=False", | |
89 | "desc": "Clear health checks by name. If no names provided, clear all.", | |
90 | "perm": "rw" | |
91 | }, | |
92 | { | |
93 | "cmd": "mgr self-test insights_set_now_offset name=hours,type=CephString", | |
94 | "desc": "Set the now time for the insights module.", | |
95 | "perm": "rw" | |
96 | }, | |
97 | { | |
98 | "cmd": "mgr self-test cluster-log name=channel,type=CephString " | |
99 | "name=priority,type=CephString " | |
100 | "name=message,type=CephString", | |
101 | "desc": "Create an audit log record.", | |
102 | "perm": "rw" | |
3efd9988 FG |
103 | }, |
104 | ] | |
105 | ||
3efd9988 FG |
106 | def __init__(self, *args, **kwargs): |
107 | super(Module, self).__init__(*args, **kwargs) | |
108 | self._event = threading.Event() | |
109 | self._workload = None | |
11fdf7f2 | 110 | self._health = {} |
3efd9988 | 111 | |
11fdf7f2 | 112 | def handle_command(self, inbuf, command): |
3efd9988 FG |
113 | if command['prefix'] == 'mgr self-test run': |
114 | self._self_test() | |
115 | return 0, '', 'Self-test succeeded' | |
116 | ||
117 | elif command['prefix'] == 'mgr self-test background start': | |
118 | if command['workload'] not in self.WORKLOADS: | |
119 | return (-errno.EINVAL, '', | |
120 | "Workload not found '{0}'".format(command['workload'])) | |
121 | self._workload = command['workload'] | |
122 | self._event.set() | |
123 | return 0, '', 'Running `{0}` in background'.format(self._workload) | |
124 | ||
125 | elif command['prefix'] == 'mgr self-test background stop': | |
126 | if self._workload: | |
127 | was_running = self._workload | |
128 | self._workload = None | |
129 | self._event.set() | |
130 | return 0, '', 'Stopping background workload `{0}`'.format( | |
131 | was_running) | |
132 | else: | |
133 | return 0, '', 'No background workload was running' | |
11fdf7f2 TL |
134 | elif command['prefix'] == 'mgr self-test config get': |
135 | return 0, str(self.get_module_option(command['key'])), '' | |
136 | elif command['prefix'] == 'mgr self-test config get_localized': | |
137 | return 0, str(self.get_localized_module_option(command['key'])), '' | |
138 | elif command['prefix'] == 'mgr self-test remote': | |
139 | self._test_remote_calls() | |
140 | return 0, '', 'Successfully called' | |
141 | elif command['prefix'] == 'mgr self-test module': | |
142 | try: | |
143 | r = self.remote(command['module'], "self_test") | |
144 | except RuntimeError as e: | |
494da23a | 145 | return -1, '', "Test failed: {0}".format(e) |
11fdf7f2 TL |
146 | else: |
147 | return 0, str(r), "Self-test OK" | |
148 | elif command['prefix'] == 'mgr self-test health set': | |
149 | return self._health_set(inbuf, command) | |
150 | elif command['prefix'] == 'mgr self-test health clear': | |
151 | return self._health_clear(inbuf, command) | |
152 | elif command['prefix'] == 'mgr self-test insights_set_now_offset': | |
153 | return self._insights_set_now_offset(inbuf, command) | |
154 | elif command['prefix'] == 'mgr self-test cluster-log': | |
155 | priority_map = { | |
156 | 'info': self.CLUSTER_LOG_PRIO_INFO, | |
157 | 'security': self.CLUSTER_LOG_PRIO_SEC, | |
158 | 'warning': self.CLUSTER_LOG_PRIO_WARN, | |
159 | 'error': self.CLUSTER_LOG_PRIO_ERROR | |
160 | } | |
161 | self.cluster_log(command['channel'], | |
162 | priority_map[command['priority']], | |
163 | command['message']) | |
164 | return 0, '', 'Successfully called' | |
3efd9988 FG |
165 | else: |
166 | return (-errno.EINVAL, '', | |
167 | "Command not found '{0}'".format(command['prefix'])) | |
168 | ||
11fdf7f2 TL |
169 | def _health_set(self, inbuf, command): |
170 | try: | |
171 | checks = json.loads(command["checks"]) | |
172 | except Exception as e: | |
494da23a | 173 | return -1, "", "Failed to decode JSON input: {}".format(e) |
11fdf7f2 TL |
174 | |
175 | try: | |
176 | for check, info in six.iteritems(checks): | |
177 | self._health[check] = { | |
178 | "severity": str(info["severity"]), | |
179 | "summary": str(info["summary"]), | |
9f95a23c | 180 | "count": 123, |
11fdf7f2 TL |
181 | "detail": [str(m) for m in info["detail"]] |
182 | } | |
183 | except Exception as e: | |
494da23a | 184 | return -1, "", "Invalid health check format: {}".format(e) |
11fdf7f2 TL |
185 | |
186 | self.set_health_checks(self._health) | |
187 | return 0, "", "" | |
188 | ||
189 | def _health_clear(self, inbuf, command): | |
190 | if "checks" in command: | |
191 | for check in command["checks"]: | |
192 | if check in self._health: | |
193 | del self._health[check] | |
194 | else: | |
195 | self._health = dict() | |
196 | ||
197 | self.set_health_checks(self._health) | |
198 | return 0, "", "" | |
199 | ||
200 | def _insights_set_now_offset(self, inbuf, command): | |
201 | try: | |
494da23a | 202 | hours = int(command["hours"]) |
11fdf7f2 | 203 | except Exception as e: |
494da23a | 204 | return -1, "", "Timestamp must be numeric: {}".format(e) |
11fdf7f2 TL |
205 | |
206 | self.remote("insights", "testing_set_now_time_offset", hours) | |
207 | return 0, "", "" | |
208 | ||
3efd9988 FG |
209 | def _self_test(self): |
210 | self.log.info("Running self-test procedure...") | |
211 | ||
212 | self._self_test_osdmap() | |
213 | self._self_test_getters() | |
214 | self._self_test_config() | |
11fdf7f2 | 215 | self._self_test_store() |
3efd9988 FG |
216 | self._self_test_misc() |
217 | self._self_test_perf_counters() | |
218 | ||
219 | def _self_test_getters(self): | |
220 | self.version | |
221 | self.get_context() | |
222 | self.get_mgr_id() | |
223 | ||
224 | # In this function, we will assume that the system is in a steady | |
225 | # state, i.e. if a server/service appears in one call, it will | |
226 | # not have gone by the time we call another function referring to it | |
227 | ||
228 | objects = [ | |
229 | "fs_map", | |
230 | "osdmap_crush_map_text", | |
231 | "osd_map", | |
232 | "config", | |
233 | "mon_map", | |
234 | "service_map", | |
235 | "osd_metadata", | |
236 | "pg_summary", | |
237 | "pg_status", | |
238 | "pg_dump", | |
9f95a23c | 239 | "pg_ready", |
3efd9988 | 240 | "df", |
9f95a23c TL |
241 | "pg_stats", |
242 | "pool_stats", | |
3efd9988 | 243 | "osd_stats", |
9f95a23c | 244 | "osd_ping_times", |
3efd9988 FG |
245 | "health", |
246 | "mon_status", | |
247 | "mgr_map" | |
248 | ] | |
249 | for obj in objects: | |
11fdf7f2 TL |
250 | assert self.get(obj) is not None |
251 | ||
252 | assert self.get("__OBJ_DNE__") is None | |
3efd9988 FG |
253 | |
254 | servers = self.list_servers() | |
255 | for server in servers: | |
256 | self.get_server(server['hostname']) | |
257 | ||
258 | osdmap = self.get('osd_map') | |
259 | for o in osdmap['osds']: | |
260 | osd_id = o['osd'] | |
261 | self.get_metadata("osd", str(osd_id)) | |
262 | ||
263 | self.get_daemon_status("osd", "0") | |
264 | #send_command | |
265 | ||
266 | def _self_test_config(self): | |
267 | # This is not a strong test (can't tell if values really | |
268 | # persisted), it's just for the python interface bit. | |
269 | ||
11fdf7f2 TL |
270 | self.set_module_option("testkey", "testvalue") |
271 | assert self.get_module_option("testkey") == "testvalue" | |
272 | ||
273 | self.set_localized_module_option("testkey", "foo") | |
274 | assert self.get_localized_module_option("testkey") == "foo" | |
275 | ||
276 | # Must return the default value defined in MODULE_OPTIONS. | |
277 | value = self.get_localized_module_option("rwoption6") | |
278 | assert isinstance(value, bool) | |
279 | assert value is True | |
280 | ||
281 | # Use default value. | |
282 | assert self.get_module_option("roption1") is None | |
283 | assert self.get_module_option("roption1", "foobar") == "foobar" | |
284 | assert self.get_module_option("roption2") == "xyz" | |
285 | assert self.get_module_option("roption2", "foobar") == "xyz" | |
286 | ||
287 | # Option type is not defined => return as string. | |
288 | self.set_module_option("rwoption1", 8080) | |
289 | value = self.get_module_option("rwoption1") | |
290 | assert isinstance(value, str) | |
291 | assert value == "8080" | |
292 | ||
293 | # Option type is defined => return as integer. | |
294 | self.set_module_option("rwoption2", 10) | |
295 | value = self.get_module_option("rwoption2") | |
296 | assert isinstance(value, int) | |
297 | assert value == 10 | |
298 | ||
299 | # Option type is defined => return as float. | |
300 | self.set_module_option("rwoption3", 1.5) | |
301 | value = self.get_module_option("rwoption3") | |
302 | assert isinstance(value, float) | |
303 | assert value == 1.5 | |
304 | ||
305 | # Option type is defined => return as string. | |
306 | self.set_module_option("rwoption4", "foo") | |
307 | value = self.get_module_option("rwoption4") | |
308 | assert isinstance(value, str) | |
309 | assert value == "foo" | |
310 | ||
311 | # Option type is defined => return as bool. | |
312 | self.set_module_option("rwoption5", False) | |
313 | value = self.get_module_option("rwoption5") | |
314 | assert isinstance(value, bool) | |
315 | assert value is False | |
316 | ||
317 | # Specified module does not exist => return None. | |
318 | assert self.get_module_option_ex("foo", "bar") is None | |
319 | ||
320 | # Specified key does not exist => return None. | |
321 | assert self.get_module_option_ex("dashboard", "bar") is None | |
322 | ||
323 | self.set_module_option_ex("telemetry", "contact", "test@test.com") | |
324 | assert self.get_module_option_ex("telemetry", "contact") == "test@test.com" | |
325 | ||
326 | # No option default value, so use the specified one. | |
327 | assert self.get_module_option_ex("dashboard", "password") is None | |
328 | assert self.get_module_option_ex("dashboard", "password", "foobar") == "foobar" | |
329 | ||
330 | # Option type is not defined => return as string. | |
331 | self.set_module_option_ex("selftest", "rwoption1", 1234) | |
332 | value = self.get_module_option_ex("selftest", "rwoption1") | |
333 | assert isinstance(value, str) | |
334 | assert value == "1234" | |
335 | ||
336 | # Option type is defined => return as integer. | |
337 | self.set_module_option_ex("telemetry", "interval", 60) | |
338 | value = self.get_module_option_ex("telemetry", "interval") | |
339 | assert isinstance(value, int) | |
340 | assert value == 60 | |
341 | ||
342 | # Option type is defined => return as bool. | |
343 | self.set_module_option_ex("telemetry", "leaderboard", True) | |
344 | value = self.get_module_option_ex("telemetry", "leaderboard") | |
345 | assert isinstance(value, bool) | |
346 | assert value is True | |
347 | ||
348 | def _self_test_store(self): | |
349 | existing_keys = set(self.get_store_prefix("test").keys()) | |
350 | self.set_store("testkey", "testvalue") | |
351 | assert self.get_store("testkey") == "testvalue" | |
352 | ||
353 | assert sorted(self.get_store_prefix("test").keys()) == sorted( | |
354 | list({"testkey"} | existing_keys)) | |
3efd9988 | 355 | |
3efd9988 FG |
356 | |
357 | def _self_test_perf_counters(self): | |
358 | self.get_perf_schema("osd", "0") | |
359 | self.get_counter("osd", "0", "osd.op") | |
360 | #get_counter | |
361 | #get_all_perf_coutners | |
362 | ||
363 | def _self_test_misc(self): | |
364 | self.set_uri("http://this.is.a.test.com") | |
365 | self.set_health_checks({}) | |
366 | ||
367 | def _self_test_osdmap(self): | |
368 | osdmap = self.get_osdmap() | |
369 | osdmap.get_epoch() | |
370 | osdmap.get_crush_version() | |
371 | osdmap.dump() | |
372 | ||
373 | inc = osdmap.new_incremental() | |
374 | osdmap.apply_incremental(inc) | |
375 | inc.get_epoch() | |
376 | inc.dump() | |
377 | ||
378 | crush = osdmap.get_crush() | |
379 | crush.dump() | |
380 | crush.get_item_name(-1) | |
381 | crush.get_item_weight(-1) | |
382 | crush.find_takes() | |
383 | crush.get_take_weight_osd_map(-1) | |
384 | ||
385 | #osdmap.get_pools_by_take() | |
386 | #osdmap.calc_pg_upmaps() | |
387 | #osdmap.map_pools_pgs_up() | |
388 | ||
389 | #inc.set_osd_reweights | |
390 | #inc.set_crush_compat_weight_set_weights | |
391 | ||
392 | self.log.info("Finished self-test procedure.") | |
393 | ||
11fdf7f2 TL |
394 | def _test_remote_calls(self): |
395 | # Test making valid call | |
396 | self.remote("influx", "handle_command", "", {"prefix": "influx self-test"}) | |
397 | ||
398 | # Test calling module that exists but isn't enabled | |
399 | # (arbitrarily pick a non-always-on module to use) | |
400 | disabled_module = "telegraf" | |
401 | mgr_map = self.get("mgr_map") | |
402 | assert disabled_module not in mgr_map['modules'] | |
403 | ||
404 | # (This works until the Z release in about 2027) | |
405 | latest_release = sorted(mgr_map['always_on_modules'].keys())[-1] | |
406 | assert disabled_module not in mgr_map['always_on_modules'][latest_release] | |
407 | ||
408 | try: | |
409 | self.remote(disabled_module, "handle_command", {"prefix": "influx self-test"}) | |
410 | except ImportError: | |
411 | pass | |
412 | else: | |
413 | raise RuntimeError("ImportError not raised for disabled module") | |
414 | ||
415 | # Test calling module that doesn't exist | |
416 | try: | |
417 | self.remote("idontexist", "handle_command", {"prefix": "influx self-test"}) | |
418 | except ImportError: | |
419 | pass | |
420 | else: | |
421 | raise RuntimeError("ImportError not raised for nonexistent module") | |
422 | ||
423 | # Test calling method that doesn't exist | |
424 | try: | |
425 | self.remote("influx", "idontexist", {"prefix": "influx self-test"}) | |
426 | except NameError: | |
427 | pass | |
428 | else: | |
429 | raise RuntimeError("KeyError not raised") | |
430 | ||
9f95a23c TL |
431 | def remote_from_orchestrator_cli_self_test(self, what): |
432 | import orchestrator | |
433 | if what == 'OrchestratorError': | |
434 | c = orchestrator.TrivialReadCompletion(result=None) | |
f6b5b4d7 | 435 | c.fail(orchestrator.OrchestratorError('hello, world')) |
9f95a23c TL |
436 | return c |
437 | elif what == "ZeroDivisionError": | |
438 | c = orchestrator.TrivialReadCompletion(result=None) | |
f6b5b4d7 | 439 | c.fail(ZeroDivisionError('hello, world')) |
9f95a23c TL |
440 | return c |
441 | assert False, repr(what) | |
11fdf7f2 | 442 | |
3efd9988 FG |
443 | def shutdown(self): |
444 | self._workload = self.SHUTDOWN | |
445 | self._event.set() | |
446 | ||
447 | def _command_spam(self): | |
448 | self.log.info("Starting command_spam workload...") | |
449 | while not self._event.is_set(): | |
450 | osdmap = self.get_osdmap() | |
451 | dump = osdmap.dump() | |
452 | count = len(dump['osds']) | |
453 | i = int(random.random() * count) | |
454 | w = random.random() | |
455 | ||
456 | result = CommandResult('') | |
457 | self.send_command(result, 'mon', '', json.dumps({ | |
458 | 'prefix': 'osd reweight', | |
459 | 'id': i, | |
460 | 'weight': w | |
461 | }), '') | |
462 | ||
463 | crush = osdmap.get_crush().dump() | |
464 | r, outb, outs = result.wait() | |
465 | ||
466 | self._event.clear() | |
467 | self.log.info("Ended command_spam workload...") | |
468 | ||
469 | def serve(self): | |
470 | while True: | |
471 | if self._workload == self.WORKLOAD_COMMAND_SPAM: | |
472 | self._command_spam() | |
473 | elif self._workload == self.SHUTDOWN: | |
474 | self.log.info("Shutting down...") | |
475 | break | |
11fdf7f2 TL |
476 | elif self._workload == self.WORKLOAD_THROW_EXCEPTION: |
477 | raise RuntimeError("Synthetic exception in serve") | |
3efd9988 FG |
478 | else: |
479 | self.log.info("Waiting for workload request...") | |
480 | self._event.wait() | |
481 | self._event.clear() |