]>
git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/selftest/module.py
90b00628feb10c97bd32adc86bcab4113af24315
2 from mgr_module
import MgrModule
, CommandResult
, HandleCommandResult
, CLICommand
, Option
8 from code
import InteractiveInterpreter
9 from contextlib
import redirect_stderr
, redirect_stdout
10 from io
import StringIO
11 from typing
import Any
, Dict
, List
, Optional
, Tuple
14 # These workloads are things that can be requested to run inside the
16 class Workload(enum
.Enum
):
17 COMMAND_SPAM
= 'command_spam'
18 THROW_EXCEPTION
= 'throw_exception'
22 class Module(MgrModule
):
24 This module is for testing the ceph-mgr python interface from within
25 a running ceph-mgr daemon.
27 It implements a sychronous self-test command for calling the functions
28 in the MgrModule interface one by one, and a background "workload"
29 command for causing the module to perform some thrashing-type
30 activities in its serve() thread.
33 # The test code in qa/ relies on these options existing -- they
34 # are of course not really used for anything in the module
36 Option(name
='testkey'),
37 Option(name
='testlkey'),
38 Option(name
='testnewline'),
39 Option(name
='roption1'),
40 Option(name
='roption2',
43 Option(name
='rwoption1'),
44 Option(name
='rwoption2',
46 Option(name
='rwoption3',
48 Option(name
='rwoption4',
50 Option(name
='rwoption5',
52 Option(name
='rwoption6',
55 Option(name
='rwoption7',
61 def __init__(self
, *args
: Any
, **kwargs
: Any
) -> None:
62 super(Module
, self
).__init
__(*args
, **kwargs
)
63 self
._event
= threading
.Event()
64 self
._workload
: Optional
[Workload
] = None
65 self
._health
: Dict
[str, Dict
[str, Any
]] = {}
66 self
._repl
= InteractiveInterpreter(dict(mgr
=self
))
68 @CLICommand('mgr self-test python-version', perm
='r')
69 def python_version(self
) -> Tuple
[int, str, str]:
71 Query the version of the embedded Python runtime
73 major
= sys
.version_info
.major
74 minor
= sys
.version_info
.minor
75 micro
= sys
.version_info
.micro
76 return 0, f
'{major}.{minor}.{micro}', ''
78 @CLICommand('mgr self-test run')
79 def run(self
) -> Tuple
[int, str, str]:
81 Run mgr python interface tests
84 return 0, '', 'Self-test succeeded'
86 @CLICommand('mgr self-test background start')
87 def backgroun_start(self
, workload
: Workload
) -> Tuple
[int, str, str]:
89 Activate a background workload (one of command_spam, throw_exception)
91 self
._workload
= workload
93 return 0, '', 'Running `{0}` in background'.format(self
._workload
)
95 @CLICommand('mgr self-test background stop')
96 def background_stop(self
) -> Tuple
[int, str, str]:
98 Stop background workload if any is running
101 was_running
= self
._workload
102 self
._workload
= None
104 return 0, '', 'Stopping background workload `{0}`'.format(
107 return 0, '', 'No background workload was running'
109 @CLICommand('mgr self-test config get')
110 def config_get(self
, key
: str) -> Tuple
[int, str, str]:
112 Peek at a configuration value
114 return 0, str(self
.get_module_option(key
)), ''
116 @CLICommand('mgr self-test config get_localized')
117 def config_get_localized(self
, key
: str) -> Tuple
[int, str, str]:
119 Peek at a configuration value (localized variant)
121 return 0, str(self
.get_localized_module_option(key
)), ''
123 @CLICommand('mgr self-test remote')
124 def test_remote(self
) -> Tuple
[int, str, str]:
126 Test inter-module calls
128 self
._test
_remote
_calls
()
129 return 0, '', 'Successfully called'
131 @CLICommand('mgr self-test module')
132 def module(self
, module
: str) -> Tuple
[int, str, str]:
134 Run another module's self_test() method
137 r
= self
.remote(module
, "self_test")
138 except RuntimeError as e
:
139 return -1, '', "Test failed: {0}".format(e
)
141 return 0, str(r
), "Self-test OK"
143 @CLICommand('mgr self-test cluster-log')
144 def do_cluster_log(self
,
147 message
: str) -> Tuple
[int, str, str]:
149 Create an audit log record.
152 'info': self
.ClusterLogPrio
.INFO
,
153 'security': self
.ClusterLogPrio
.SEC
,
154 'warning': self
.ClusterLogPrio
.WARN
,
155 'error': self
.ClusterLogPrio
.ERROR
157 self
.cluster_log(channel
,
158 priority_map
[priority
],
160 return 0, '', 'Successfully called'
162 @CLICommand('mgr self-test health set')
163 def health_set(self
, checks
: str) -> Tuple
[int, str, str]:
165 Set a health check from a JSON-formatted description.
168 health_check
= json
.loads(checks
)
169 except Exception as e
:
170 return -1, "", "Failed to decode JSON input: {}".format(e
)
173 for check
, info
in health_check
.items():
174 self
._health
[check
] = {
175 "severity": str(info
["severity"]),
176 "summary": str(info
["summary"]),
178 "detail": [str(m
) for m
in info
["detail"]]
180 except Exception as e
:
181 return -1, "", "Invalid health check format: {}".format(e
)
183 self
.set_health_checks(self
._health
)
186 @CLICommand('mgr self-test health clear')
187 def health_clear(self
, checks
: Optional
[List
[str]] = None) -> Tuple
[int, str, str]:
189 Clear health checks by name. If no names provided, clear all.
191 if checks
is not None:
193 if check
in self
._health
:
194 del self
._health
[check
]
196 self
._health
= dict()
198 self
.set_health_checks(self
._health
)
201 @CLICommand('mgr self-test insights_set_now_offset')
202 def insights_set_now_offset(self
, hours
: int) -> Tuple
[int, str, str]:
204 Set the now time for the insights module.
206 self
.remote("insights", "testing_set_now_time_offset", hours
)
209 def _self_test(self
) -> None:
210 self
.log
.info("Running self-test procedure...")
212 self
._self
_test
_osdmap
()
213 self
._self
_test
_getters
()
214 self
._self
_test
_config
()
215 self
._self
_test
_store
()
216 self
._self
_test
_misc
()
217 self
._self
_test
_perf
_counters
()
219 def _self_test_getters(self
) -> None:
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
230 "osdmap_crush_map_text",
250 assert self
.get(obj
) is not None
252 assert self
.get("__OBJ_DNE__") is None
254 servers
= self
.list_servers()
255 for server
in servers
:
256 self
.get_server(server
['hostname']) # type: ignore
258 osdmap
= self
.get('osd_map')
259 for o
in osdmap
['osds']:
261 self
.get_metadata("osd", str(osd_id
))
263 self
.get_daemon_status("osd", "0")
265 def _self_test_config(self
) -> None:
266 # This is not a strong test (can't tell if values really
267 # persisted), it's just for the python interface bit.
269 self
.set_module_option("testkey", "testvalue")
270 assert self
.get_module_option("testkey") == "testvalue"
272 self
.set_localized_module_option("testkey", "foo")
273 assert self
.get_localized_module_option("testkey") == "foo"
275 # Must return the default value defined in MODULE_OPTIONS.
276 value
= self
.get_localized_module_option("rwoption6")
277 assert isinstance(value
, bool)
281 assert self
.get_module_option("roption1") is None
282 assert self
.get_module_option("roption1", "foobar") == "foobar"
283 assert self
.get_module_option("roption2") == "xyz"
284 assert self
.get_module_option("roption2", "foobar") == "xyz"
286 # Option type is not defined => return as string.
287 self
.set_module_option("rwoption1", 8080)
288 value
= self
.get_module_option("rwoption1")
289 assert isinstance(value
, str)
290 assert value
== "8080"
292 # Option type is defined => return as integer.
293 self
.set_module_option("rwoption2", 10)
294 value
= self
.get_module_option("rwoption2")
295 assert isinstance(value
, int)
298 # Option type is defined => return as float.
299 self
.set_module_option("rwoption3", 1.5)
300 value
= self
.get_module_option("rwoption3")
301 assert isinstance(value
, float)
304 # Option type is defined => return as string.
305 self
.set_module_option("rwoption4", "foo")
306 value
= self
.get_module_option("rwoption4")
307 assert isinstance(value
, str)
308 assert value
== "foo"
310 # Option type is defined => return as bool.
311 self
.set_module_option("rwoption5", False)
312 value
= self
.get_module_option("rwoption5")
313 assert isinstance(value
, bool)
314 assert value
is False
316 # Option value range is specified
318 self
.set_module_option("rwoption7", 43)
319 except Exception as e
:
320 assert isinstance(e
, ValueError)
322 message
= "should raise if value is not in specified range"
323 assert False, message
325 # Specified module does not exist => return None.
326 assert self
.get_module_option_ex("foo", "bar") is None
328 # Specified key does not exist => return None.
329 assert self
.get_module_option_ex("dashboard", "bar") is None
331 self
.set_module_option_ex("telemetry", "contact", "test@test.com")
332 assert self
.get_module_option_ex("telemetry", "contact") == "test@test.com"
334 # No option default value, so use the specified one.
335 assert self
.get_module_option_ex("dashboard", "password") is None
336 assert self
.get_module_option_ex("dashboard", "password", "foobar") == "foobar"
338 # Option type is not defined => return as string.
339 self
.set_module_option_ex("selftest", "rwoption1", 1234)
340 value
= self
.get_module_option_ex("selftest", "rwoption1")
341 assert isinstance(value
, str)
342 assert value
== "1234"
344 # Option type is defined => return as integer.
345 self
.set_module_option_ex("telemetry", "interval", 60)
346 value
= self
.get_module_option_ex("telemetry", "interval")
347 assert isinstance(value
, int)
350 # Option type is defined => return as bool.
351 self
.set_module_option_ex("telemetry", "leaderboard", True)
352 value
= self
.get_module_option_ex("telemetry", "leaderboard")
353 assert isinstance(value
, bool)
356 def _self_test_store(self
) -> None:
357 existing_keys
= set(self
.get_store_prefix("test").keys())
358 self
.set_store("testkey", "testvalue")
359 assert self
.get_store("testkey") == "testvalue"
361 assert (set(self
.get_store_prefix("test").keys())
362 == {"testkey"} | existing_keys
)
364 def _self_test_perf_counters(self
) -> None:
365 self
.get_perf_schema("osd", "0")
366 self
.get_counter("osd", "0", "osd.op")
368 # get_all_perf_coutners
370 def _self_test_misc(self
) -> None:
371 self
.set_uri("http://this.is.a.test.com")
372 self
.set_health_checks({})
374 def _self_test_osdmap(self
) -> None:
375 osdmap
= self
.get_osdmap()
377 osdmap
.get_crush_version()
380 inc
= osdmap
.new_incremental()
381 osdmap
.apply_incremental(inc
)
385 crush
= osdmap
.get_crush()
387 crush
.get_item_name(-1)
388 crush
.get_item_weight(-1)
390 crush
.get_take_weight_osd_map(-1)
392 # osdmap.get_pools_by_take()
393 # osdmap.calc_pg_upmaps()
394 # osdmap.map_pools_pgs_up()
396 # inc.set_osd_reweights
397 # inc.set_crush_compat_weight_set_weights
399 self
.log
.info("Finished self-test procedure.")
401 def _test_remote_calls(self
) -> None:
402 # Test making valid call
403 self
.remote("influx", "self_test")
405 # Test calling module that exists but isn't enabled
406 # (arbitrarily pick a non-always-on module to use)
407 disabled_module
= "telegraf"
408 mgr_map
= self
.get("mgr_map")
409 assert disabled_module
not in mgr_map
['modules']
411 # (This works until the Z release in about 2027)
412 latest_release
= sorted(mgr_map
['always_on_modules'].keys())[-1]
413 assert disabled_module
not in mgr_map
['always_on_modules'][latest_release
]
416 self
.remote(disabled_module
, "handle_command", {"prefix": "influx self-test"})
420 raise RuntimeError("ImportError not raised for disabled module")
422 # Test calling module that doesn't exist
424 self
.remote("idontexist", "self_test")
428 raise RuntimeError("ImportError not raised for nonexistent module")
430 # Test calling method that doesn't exist
432 self
.remote("influx", "idontexist")
436 raise RuntimeError("KeyError not raised")
438 def remote_from_orchestrator_cli_self_test(self
, what
: str) -> Any
:
440 if what
== 'OrchestratorError':
441 return orchestrator
.OrchResult(result
=None, exception
=orchestrator
.OrchestratorError('hello, world'))
442 elif what
== "ZeroDivisionError":
443 return orchestrator
.OrchResult(result
=None, exception
=ZeroDivisionError('hello, world'))
444 assert False, repr(what
)
446 def shutdown(self
) -> None:
447 self
._workload
= Workload
.SHUTDOWN
450 def _command_spam(self
) -> None:
451 self
.log
.info("Starting command_spam workload...")
452 while not self
._event
.is_set():
453 osdmap
= self
.get_osdmap()
455 count
= len(dump
['osds'])
456 i
= int(random
.random() * count
)
459 result
= CommandResult('')
460 self
.send_command(result
, 'mon', '', json
.dumps({
461 'prefix': 'osd reweight',
465 _
= osdmap
.get_crush().dump()
466 r
, outb
, outs
= result
.wait()
469 self
.log
.info("Ended command_spam workload...")
471 @CLICommand('mgr self-test eval')
473 s
: Optional
[str] = None,
474 inbuf
: Optional
[str] = None) -> HandleCommandResult
:
480 return HandleCommandResult(-1, '', 'source is not specified')
484 with
redirect_stderr(err
), redirect_stdout(out
):
485 needs_more
= self
._repl
.runsource(source
)
492 stdout
= out
.getvalue()
493 stderr
= err
.getvalue()
494 return HandleCommandResult(retval
, stdout
, stderr
)
496 def serve(self
) -> None:
498 if self
._workload
== Workload
.COMMAND_SPAM
:
500 elif self
._workload
== Workload
.SHUTDOWN
:
501 self
.log
.info("Shutting down...")
503 elif self
._workload
== Workload
.THROW_EXCEPTION
:
504 raise RuntimeError("Synthetic exception in serve")
506 self
.log
.info("Waiting for workload request...")