]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/selftest/module.py
update download target update for octopus release
[ceph.git] / ceph / src / pybind / mgr / selftest / module.py
CommitLineData
3efd9988 1
494da23a 2from mgr_module import MgrModule, CommandResult, PersistentStoreDict
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"]),
180 "detail": [str(m) for m in info["detail"]]
181 }
182 except Exception as e:
494da23a 183 return -1, "", "Invalid health check format: {}".format(e)
11fdf7f2
TL
184
185 self.set_health_checks(self._health)
186 return 0, "", ""
187
188 def _health_clear(self, inbuf, command):
189 if "checks" in command:
190 for check in command["checks"]:
191 if check in self._health:
192 del self._health[check]
193 else:
194 self._health = dict()
195
196 self.set_health_checks(self._health)
197 return 0, "", ""
198
199 def _insights_set_now_offset(self, inbuf, command):
200 try:
494da23a 201 hours = int(command["hours"])
11fdf7f2 202 except Exception as e:
494da23a 203 return -1, "", "Timestamp must be numeric: {}".format(e)
11fdf7f2
TL
204
205 self.remote("insights", "testing_set_now_time_offset", hours)
206 return 0, "", ""
207
3efd9988
FG
208 def _self_test(self):
209 self.log.info("Running self-test procedure...")
210
211 self._self_test_osdmap()
212 self._self_test_getters()
213 self._self_test_config()
11fdf7f2 214 self._self_test_store()
3efd9988
FG
215 self._self_test_misc()
216 self._self_test_perf_counters()
494da23a 217 self._self_persistent_store_dict()
3efd9988
FG
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",
239 "df",
240 "osd_stats",
241 "health",
242 "mon_status",
243 "mgr_map"
244 ]
245 for obj in objects:
11fdf7f2
TL
246 assert self.get(obj) is not None
247
248 assert self.get("__OBJ_DNE__") is None
3efd9988
FG
249
250 servers = self.list_servers()
251 for server in servers:
252 self.get_server(server['hostname'])
253
254 osdmap = self.get('osd_map')
255 for o in osdmap['osds']:
256 osd_id = o['osd']
257 self.get_metadata("osd", str(osd_id))
258
259 self.get_daemon_status("osd", "0")
260 #send_command
261
262 def _self_test_config(self):
263 # This is not a strong test (can't tell if values really
264 # persisted), it's just for the python interface bit.
265
11fdf7f2
TL
266 self.set_module_option("testkey", "testvalue")
267 assert self.get_module_option("testkey") == "testvalue"
268
269 self.set_localized_module_option("testkey", "foo")
270 assert self.get_localized_module_option("testkey") == "foo"
271
272 # Must return the default value defined in MODULE_OPTIONS.
273 value = self.get_localized_module_option("rwoption6")
274 assert isinstance(value, bool)
275 assert value is True
276
277 # Use default value.
278 assert self.get_module_option("roption1") is None
279 assert self.get_module_option("roption1", "foobar") == "foobar"
280 assert self.get_module_option("roption2") == "xyz"
281 assert self.get_module_option("roption2", "foobar") == "xyz"
282
283 # Option type is not defined => return as string.
284 self.set_module_option("rwoption1", 8080)
285 value = self.get_module_option("rwoption1")
286 assert isinstance(value, str)
287 assert value == "8080"
288
289 # Option type is defined => return as integer.
290 self.set_module_option("rwoption2", 10)
291 value = self.get_module_option("rwoption2")
292 assert isinstance(value, int)
293 assert value == 10
294
295 # Option type is defined => return as float.
296 self.set_module_option("rwoption3", 1.5)
297 value = self.get_module_option("rwoption3")
298 assert isinstance(value, float)
299 assert value == 1.5
300
301 # Option type is defined => return as string.
302 self.set_module_option("rwoption4", "foo")
303 value = self.get_module_option("rwoption4")
304 assert isinstance(value, str)
305 assert value == "foo"
306
307 # Option type is defined => return as bool.
308 self.set_module_option("rwoption5", False)
309 value = self.get_module_option("rwoption5")
310 assert isinstance(value, bool)
311 assert value is False
312
313 # Specified module does not exist => return None.
314 assert self.get_module_option_ex("foo", "bar") is None
315
316 # Specified key does not exist => return None.
317 assert self.get_module_option_ex("dashboard", "bar") is None
318
319 self.set_module_option_ex("telemetry", "contact", "test@test.com")
320 assert self.get_module_option_ex("telemetry", "contact") == "test@test.com"
321
322 # No option default value, so use the specified one.
323 assert self.get_module_option_ex("dashboard", "password") is None
324 assert self.get_module_option_ex("dashboard", "password", "foobar") == "foobar"
325
326 # Option type is not defined => return as string.
327 self.set_module_option_ex("selftest", "rwoption1", 1234)
328 value = self.get_module_option_ex("selftest", "rwoption1")
329 assert isinstance(value, str)
330 assert value == "1234"
331
332 # Option type is defined => return as integer.
333 self.set_module_option_ex("telemetry", "interval", 60)
334 value = self.get_module_option_ex("telemetry", "interval")
335 assert isinstance(value, int)
336 assert value == 60
337
338 # Option type is defined => return as bool.
339 self.set_module_option_ex("telemetry", "leaderboard", True)
340 value = self.get_module_option_ex("telemetry", "leaderboard")
341 assert isinstance(value, bool)
342 assert value is True
343
344 def _self_test_store(self):
345 existing_keys = set(self.get_store_prefix("test").keys())
346 self.set_store("testkey", "testvalue")
347 assert self.get_store("testkey") == "testvalue"
348
349 assert sorted(self.get_store_prefix("test").keys()) == sorted(
350 list({"testkey"} | existing_keys))
3efd9988 351
3efd9988
FG
352
353 def _self_test_perf_counters(self):
354 self.get_perf_schema("osd", "0")
355 self.get_counter("osd", "0", "osd.op")
356 #get_counter
357 #get_all_perf_coutners
358
359 def _self_test_misc(self):
360 self.set_uri("http://this.is.a.test.com")
361 self.set_health_checks({})
362
363 def _self_test_osdmap(self):
364 osdmap = self.get_osdmap()
365 osdmap.get_epoch()
366 osdmap.get_crush_version()
367 osdmap.dump()
368
369 inc = osdmap.new_incremental()
370 osdmap.apply_incremental(inc)
371 inc.get_epoch()
372 inc.dump()
373
374 crush = osdmap.get_crush()
375 crush.dump()
376 crush.get_item_name(-1)
377 crush.get_item_weight(-1)
378 crush.find_takes()
379 crush.get_take_weight_osd_map(-1)
380
381 #osdmap.get_pools_by_take()
382 #osdmap.calc_pg_upmaps()
383 #osdmap.map_pools_pgs_up()
384
385 #inc.set_osd_reweights
386 #inc.set_crush_compat_weight_set_weights
387
388 self.log.info("Finished self-test procedure.")
389
494da23a
TL
390 def _self_persistent_store_dict(self):
391 self.test_dict = PersistentStoreDict(self, 'test_dict')
392 for i in "abcde":
393 self.test_dict[i] = {i:1}
394 assert self.test_dict.keys() == set("abcde")
395 assert 'a' in self.test_dict
396 del self.test_dict['a']
397 assert self.test_dict.keys() == set("bcde"), self.test_dict.keys()
398 assert 'a' not in self.test_dict
399 self.test_dict.clear()
400 assert not self.test_dict, dict(self.test_dict.items())
401 self.set_store('test_dict.a', 'invalid json')
402 try:
403 self.test_dict['a']
404 assert False
405 except ValueError:
406 pass
407 assert not self.test_dict, dict(self.test_dict.items())
408
11fdf7f2
TL
409 def _test_remote_calls(self):
410 # Test making valid call
411 self.remote("influx", "handle_command", "", {"prefix": "influx self-test"})
412
413 # Test calling module that exists but isn't enabled
414 # (arbitrarily pick a non-always-on module to use)
415 disabled_module = "telegraf"
416 mgr_map = self.get("mgr_map")
417 assert disabled_module not in mgr_map['modules']
418
419 # (This works until the Z release in about 2027)
420 latest_release = sorted(mgr_map['always_on_modules'].keys())[-1]
421 assert disabled_module not in mgr_map['always_on_modules'][latest_release]
422
423 try:
424 self.remote(disabled_module, "handle_command", {"prefix": "influx self-test"})
425 except ImportError:
426 pass
427 else:
428 raise RuntimeError("ImportError not raised for disabled module")
429
430 # Test calling module that doesn't exist
431 try:
432 self.remote("idontexist", "handle_command", {"prefix": "influx self-test"})
433 except ImportError:
434 pass
435 else:
436 raise RuntimeError("ImportError not raised for nonexistent module")
437
438 # Test calling method that doesn't exist
439 try:
440 self.remote("influx", "idontexist", {"prefix": "influx self-test"})
441 except NameError:
442 pass
443 else:
444 raise RuntimeError("KeyError not raised")
445
446
3efd9988
FG
447 def shutdown(self):
448 self._workload = self.SHUTDOWN
449 self._event.set()
450
451 def _command_spam(self):
452 self.log.info("Starting command_spam workload...")
453 while not self._event.is_set():
454 osdmap = self.get_osdmap()
455 dump = osdmap.dump()
456 count = len(dump['osds'])
457 i = int(random.random() * count)
458 w = random.random()
459
460 result = CommandResult('')
461 self.send_command(result, 'mon', '', json.dumps({
462 'prefix': 'osd reweight',
463 'id': i,
464 'weight': w
465 }), '')
466
467 crush = osdmap.get_crush().dump()
468 r, outb, outs = result.wait()
469
470 self._event.clear()
471 self.log.info("Ended command_spam workload...")
472
473 def serve(self):
474 while True:
475 if self._workload == self.WORKLOAD_COMMAND_SPAM:
476 self._command_spam()
477 elif self._workload == self.SHUTDOWN:
478 self.log.info("Shutting down...")
479 break
11fdf7f2
TL
480 elif self._workload == self.WORKLOAD_THROW_EXCEPTION:
481 raise RuntimeError("Synthetic exception in serve")
3efd9988
FG
482 else:
483 self.log.info("Waiting for workload request...")
484 self._event.wait()
485 self._event.clear()