]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/selftest/module.py
import 15.2.5
[ceph.git] / ceph / src / pybind / mgr / selftest / module.py
CommitLineData
3efd9988 1
9f95a23c 2from mgr_module import MgrModule, CommandResult
3efd9988
FG
3import threading
4import random
5import json
6import errno
11fdf7f2 7import six
3efd9988
FG
8
9
10class 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()