]>
Commit | Line | Data |
---|---|---|
3efd9988 | 1 | import ceph_module # noqa |
3efd9988 | 2 | |
494da23a TL |
3 | try: |
4 | from typing import Set, Tuple, Iterator, Any | |
5 | except ImportError: | |
6 | # just for type checking | |
7 | pass | |
7c673cae | 8 | import logging |
11fdf7f2 | 9 | import json |
1adf2230 | 10 | import six |
7c673cae | 11 | import threading |
11fdf7f2 TL |
12 | from collections import defaultdict, namedtuple |
13 | import rados | |
81eedcae | 14 | import re |
11fdf7f2 TL |
15 | import time |
16 | ||
17 | PG_STATES = [ | |
18 | "active", | |
19 | "clean", | |
20 | "down", | |
21 | "recovery_unfound", | |
22 | "backfill_unfound", | |
23 | "scrubbing", | |
24 | "degraded", | |
25 | "inconsistent", | |
26 | "peering", | |
27 | "repair", | |
28 | "recovering", | |
29 | "forced_recovery", | |
30 | "backfill_wait", | |
31 | "incomplete", | |
32 | "stale", | |
33 | "remapped", | |
34 | "deep", | |
35 | "backfilling", | |
36 | "forced_backfill", | |
37 | "backfill_toofull", | |
38 | "recovery_wait", | |
39 | "recovery_toofull", | |
40 | "undersized", | |
41 | "activating", | |
42 | "peered", | |
43 | "snaptrim", | |
44 | "snaptrim_wait", | |
45 | "snaptrim_error", | |
46 | "creating", | |
47 | "unknown"] | |
3efd9988 FG |
48 | |
49 | ||
50 | class CPlusPlusHandler(logging.Handler): | |
51 | def __init__(self, module_inst): | |
52 | super(CPlusPlusHandler, self).__init__() | |
53 | self._module = module_inst | |
54 | ||
55 | def emit(self, record): | |
56 | if record.levelno <= logging.DEBUG: | |
57 | ceph_level = 20 | |
58 | elif record.levelno <= logging.INFO: | |
59 | ceph_level = 4 | |
60 | elif record.levelno <= logging.WARNING: | |
61 | ceph_level = 1 | |
62 | else: | |
63 | ceph_level = 0 | |
64 | ||
65 | self._module._ceph_log(ceph_level, self.format(record)) | |
66 | ||
67 | ||
11fdf7f2 TL |
68 | def configure_logger(module_inst, module_name): |
69 | """ | |
70 | Create and configure the logger with the specified module. | |
3efd9988 | 71 | |
11fdf7f2 TL |
72 | A handler will be added to the root logger which will redirect |
73 | the messages from all loggers (incl. 3rd party libraries) to the | |
74 | Ceph log. | |
3efd9988 | 75 | |
11fdf7f2 TL |
76 | :param module_inst: The module instance. |
77 | :type module_inst: instance | |
78 | :param module_name: The module name. | |
79 | :type module_name: str | |
80 | :return: Return the logger with the specified name. | |
81 | """ | |
82 | logger = logging.getLogger(module_name) | |
83 | # Don't filter any logs at the python level, leave it to C++. | |
84 | # FIXME: We should learn the log level from C++ land, and then | |
85 | # avoid calling the C++ level log when we know a message | |
86 | # is of an insufficient level to be ultimately output. | |
87 | logger.setLevel(logging.DEBUG) # Don't use NOTSET | |
88 | ||
89 | root_logger = logging.getLogger() | |
90 | # Add handler to the root logger, thus this module and all | |
91 | # 3rd party libraries will log their messages to the Ceph log. | |
92 | root_logger.addHandler(CPlusPlusHandler(module_inst)) | |
93 | # Set the log level to ``ERROR`` to ensure that we only get | |
94 | # those message from 3rd party libraries (only effective if | |
95 | # they use the default log level ``NOTSET``). | |
96 | # Check https://docs.python.org/3/library/logging.html#logging.Logger.setLevel | |
97 | # for more information about how the effective log level is | |
98 | # determined. | |
99 | root_logger.setLevel(logging.ERROR) | |
3efd9988 FG |
100 | |
101 | return logger | |
7c673cae FG |
102 | |
103 | ||
11fdf7f2 TL |
104 | def unconfigure_logger(module_name=None): |
105 | """ | |
106 | :param module_name: The module name. Defaults to ``None``. | |
107 | :type module_name: str or None | |
108 | """ | |
109 | logger = logging.getLogger(module_name) | |
110 | rm_handlers = [ | |
111 | h for h in logger.handlers if isinstance(h, CPlusPlusHandler)] | |
3efd9988 FG |
112 | for h in rm_handlers: |
113 | logger.removeHandler(h) | |
114 | ||
11fdf7f2 | 115 | |
7c673cae FG |
116 | class CommandResult(object): |
117 | """ | |
118 | Use with MgrModule.send_command | |
119 | """ | |
11fdf7f2 TL |
120 | |
121 | def __init__(self, tag=None): | |
7c673cae FG |
122 | self.ev = threading.Event() |
123 | self.outs = "" | |
124 | self.outb = "" | |
125 | self.r = 0 | |
126 | ||
127 | # This is just a convenience for notifications from | |
128 | # C++ land, to avoid passing addresses around in messages. | |
11fdf7f2 | 129 | self.tag = tag if tag else "" |
7c673cae FG |
130 | |
131 | def complete(self, r, outb, outs): | |
132 | self.r = r | |
133 | self.outb = outb | |
134 | self.outs = outs | |
135 | self.ev.set() | |
136 | ||
137 | def wait(self): | |
138 | self.ev.wait() | |
139 | return self.r, self.outb, self.outs | |
140 | ||
141 | ||
11fdf7f2 TL |
142 | class HandleCommandResult(namedtuple('HandleCommandResult', ['retval', 'stdout', 'stderr'])): |
143 | def __new__(cls, retval=0, stdout="", stderr=""): | |
144 | """ | |
145 | Tuple containing the result of `handle_command()` | |
146 | ||
147 | Only write to stderr if there is an error, or in extraordinary circumstances | |
148 | ||
149 | Avoid having `ceph foo bar` commands say "did foo bar" on success unless there | |
150 | is critical information to include there. | |
151 | ||
152 | Everything programmatically consumable should be put on stdout | |
153 | ||
154 | :param retval: return code. E.g. 0 or -errno.EINVAL | |
155 | :type retval: int | |
156 | :param stdout: data of this result. | |
157 | :type stdout: str | |
158 | :param stderr: Typically used for error messages. | |
159 | :type stderr: str | |
160 | """ | |
161 | return super(HandleCommandResult, cls).__new__(cls, retval, stdout, stderr) | |
162 | ||
163 | ||
3efd9988 FG |
164 | class OSDMap(ceph_module.BasePyOSDMap): |
165 | def get_epoch(self): | |
166 | return self._get_epoch() | |
167 | ||
168 | def get_crush_version(self): | |
169 | return self._get_crush_version() | |
170 | ||
171 | def dump(self): | |
172 | return self._dump() | |
173 | ||
11fdf7f2 TL |
174 | def get_pools(self): |
175 | # FIXME: efficient implementation | |
176 | d = self._dump() | |
177 | return dict([(p['pool'], p) for p in d['pools']]) | |
178 | ||
179 | def get_pools_by_name(self): | |
180 | # FIXME: efficient implementation | |
181 | d = self._dump() | |
182 | return dict([(p['pool_name'], p) for p in d['pools']]) | |
183 | ||
3efd9988 FG |
184 | def new_incremental(self): |
185 | return self._new_incremental() | |
186 | ||
187 | def apply_incremental(self, inc): | |
188 | return self._apply_incremental(inc) | |
189 | ||
190 | def get_crush(self): | |
191 | return self._get_crush() | |
192 | ||
193 | def get_pools_by_take(self, take): | |
194 | return self._get_pools_by_take(take).get('pools', []) | |
195 | ||
196 | def calc_pg_upmaps(self, inc, | |
11fdf7f2 TL |
197 | max_deviation=.01, max_iterations=10, pools=None): |
198 | if pools is None: | |
199 | pools = [] | |
3efd9988 FG |
200 | return self._calc_pg_upmaps( |
201 | inc, | |
202 | max_deviation, max_iterations, pools) | |
203 | ||
204 | def map_pool_pgs_up(self, poolid): | |
205 | return self._map_pool_pgs_up(poolid) | |
206 | ||
11fdf7f2 TL |
207 | def pg_to_up_acting_osds(self, pool_id, ps): |
208 | return self._pg_to_up_acting_osds(pool_id, ps) | |
209 | ||
210 | def pool_raw_used_rate(self, pool_id): | |
211 | return self._pool_raw_used_rate(pool_id) | |
212 | ||
213 | def get_ec_profile(self, name): | |
214 | # FIXME: efficient implementation | |
215 | d = self._dump() | |
216 | return d['erasure_code_profiles'].get(name, None) | |
217 | ||
218 | ||
3efd9988 FG |
219 | class OSDMapIncremental(ceph_module.BasePyOSDMapIncremental): |
220 | def get_epoch(self): | |
221 | return self._get_epoch() | |
222 | ||
223 | def dump(self): | |
224 | return self._dump() | |
225 | ||
226 | def set_osd_reweights(self, weightmap): | |
227 | """ | |
228 | weightmap is a dict, int to float. e.g. { 0: .9, 1: 1.0, 3: .997 } | |
229 | """ | |
230 | return self._set_osd_reweights(weightmap) | |
231 | ||
232 | def set_crush_compat_weight_set_weights(self, weightmap): | |
233 | """ | |
234 | weightmap is a dict, int to float. devices only. e.g., | |
235 | { 0: 3.4, 1: 3.3, 2: 3.334 } | |
236 | """ | |
237 | return self._set_crush_compat_weight_set_weights(weightmap) | |
238 | ||
11fdf7f2 | 239 | |
3efd9988 | 240 | class CRUSHMap(ceph_module.BasePyCRUSH): |
b32b8144 | 241 | ITEM_NONE = 0x7fffffff |
11fdf7f2 | 242 | DEFAULT_CHOOSE_ARGS = '-1' |
b32b8144 | 243 | |
3efd9988 FG |
244 | def dump(self): |
245 | return self._dump() | |
246 | ||
247 | def get_item_weight(self, item): | |
248 | return self._get_item_weight(item) | |
249 | ||
250 | def get_item_name(self, item): | |
251 | return self._get_item_name(item) | |
252 | ||
253 | def find_takes(self): | |
254 | return self._find_takes().get('takes', []) | |
255 | ||
256 | def get_take_weight_osd_map(self, root): | |
257 | uglymap = self._get_take_weight_osd_map(root) | |
11fdf7f2 TL |
258 | return {int(k): v for k, v in six.iteritems(uglymap.get('weights', {}))} |
259 | ||
260 | @staticmethod | |
261 | def have_default_choose_args(dump): | |
262 | return CRUSHMap.DEFAULT_CHOOSE_ARGS in dump.get('choose_args', {}) | |
263 | ||
264 | @staticmethod | |
265 | def get_default_choose_args(dump): | |
266 | return dump.get('choose_args').get(CRUSHMap.DEFAULT_CHOOSE_ARGS, []) | |
267 | ||
268 | def get_rule(self, rule_name): | |
269 | # TODO efficient implementation | |
270 | for rule in self.dump()['rules']: | |
271 | if rule_name == rule['rule_name']: | |
272 | return rule | |
273 | ||
274 | return None | |
275 | ||
276 | def get_rule_by_id(self, rule_id): | |
277 | for rule in self.dump()['rules']: | |
278 | if rule['rule_id'] == rule_id: | |
279 | return rule | |
280 | ||
281 | return None | |
282 | ||
283 | def get_rule_root(self, rule_name): | |
284 | rule = self.get_rule(rule_name) | |
285 | if rule is None: | |
286 | return None | |
287 | ||
288 | try: | |
289 | first_take = [s for s in rule['steps'] if s['op'] == 'take'][0] | |
290 | except IndexError: | |
291 | self.log.warn("CRUSH rule '{0}' has no 'take' step".format( | |
292 | rule_name)) | |
293 | return None | |
294 | else: | |
295 | return first_take['item'] | |
296 | ||
297 | def get_osds_under(self, root_id): | |
298 | # TODO don't abuse dump like this | |
299 | d = self.dump() | |
300 | buckets = dict([(b['id'], b) for b in d['buckets']]) | |
301 | ||
302 | osd_list = [] | |
303 | ||
304 | def accumulate(b): | |
305 | for item in b['items']: | |
306 | if item['id'] >= 0: | |
307 | osd_list.append(item['id']) | |
308 | else: | |
309 | try: | |
310 | accumulate(buckets[item['id']]) | |
311 | except KeyError: | |
312 | pass | |
313 | ||
314 | accumulate(buckets[root_id]) | |
315 | ||
316 | return osd_list | |
317 | ||
318 | def device_class_counts(self): | |
319 | result = defaultdict(int) | |
320 | # TODO don't abuse dump like this | |
321 | d = self.dump() | |
322 | for device in d['devices']: | |
323 | cls = device.get('class', None) | |
324 | result[cls] += 1 | |
325 | ||
326 | return dict(result) | |
327 | ||
328 | ||
329 | class CLICommand(object): | |
330 | COMMANDS = {} | |
331 | ||
332 | def __init__(self, prefix, args="", desc="", perm="rw"): | |
333 | self.prefix = prefix | |
334 | self.args = args | |
335 | self.args_dict = {} | |
336 | self.desc = desc | |
337 | self.perm = perm | |
338 | self.func = None | |
339 | self._parse_args() | |
340 | ||
341 | def _parse_args(self): | |
342 | if not self.args: | |
343 | return | |
344 | args = self.args.split(" ") | |
345 | for arg in args: | |
346 | arg_desc = arg.strip().split(",") | |
347 | arg_d = {} | |
348 | for kv in arg_desc: | |
349 | k, v = kv.split("=") | |
350 | if k != "name": | |
351 | arg_d[k] = v | |
352 | else: | |
353 | self.args_dict[v] = arg_d | |
354 | ||
355 | def __call__(self, func): | |
356 | self.func = func | |
357 | self.COMMANDS[self.prefix] = self | |
358 | return self.func | |
359 | ||
360 | def call(self, mgr, cmd_dict, inbuf): | |
361 | kwargs = {} | |
362 | for a, d in self.args_dict.items(): | |
363 | if 'req' in d and d['req'] == "false" and a not in cmd_dict: | |
364 | continue | |
365 | kwargs[a.replace("-", "_")] = cmd_dict[a] | |
366 | if inbuf: | |
367 | kwargs['inbuf'] = inbuf | |
368 | return self.func(mgr, **kwargs) | |
369 | ||
370 | @classmethod | |
371 | def dump_cmd_list(cls): | |
372 | return [{ | |
373 | 'cmd': '{} {}'.format(cmd.prefix, cmd.args), | |
374 | 'desc': cmd.desc, | |
375 | 'perm': cmd.perm | |
376 | } for _, cmd in cls.COMMANDS.items()] | |
377 | ||
378 | ||
379 | def CLIReadCommand(prefix, args="", desc=""): | |
380 | return CLICommand(prefix, args, desc, "r") | |
381 | ||
382 | ||
383 | def CLIWriteCommand(prefix, args="", desc=""): | |
384 | return CLICommand(prefix, args, desc, "w") | |
385 | ||
386 | ||
387 | def _get_localized_key(prefix, key): | |
388 | return '{}/{}'.format(prefix, key) | |
389 | ||
390 | ||
391 | class Option(dict): | |
392 | """ | |
393 | Helper class to declare options for MODULE_OPTIONS list. | |
394 | ||
395 | Caveat: it uses argument names matching Python keywords (type, min, max), | |
396 | so any further processing should happen in a separate method. | |
397 | ||
398 | TODO: type validation. | |
399 | """ | |
400 | ||
401 | def __init__( | |
402 | self, name, | |
403 | default=None, | |
404 | type='str', | |
405 | desc=None, longdesc=None, | |
406 | min=None, max=None, | |
407 | enum_allowed=None, | |
408 | see_also=None, | |
409 | tags=None, | |
410 | runtime=False, | |
411 | ): | |
412 | super(Option, self).__init__( | |
413 | (k, v) for k, v in vars().items() | |
414 | if k != 'self' and v is not None) | |
415 | ||
92f5a8d4 TL |
416 | class Command(dict): |
417 | """ | |
418 | Helper class to declare options for COMMANDS list. | |
419 | ||
420 | It also allows to specify prefix and args separately, as well as storing a | |
421 | handler callable. | |
422 | ||
423 | Usage: | |
424 | >>> Command(prefix="example", | |
425 | ... args="name=arg,type=CephInt", | |
426 | ... perm='w', | |
427 | ... desc="Blah") | |
428 | {'poll': False, 'cmd': 'example name=arg,type=CephInt', 'perm': 'w', 'desc': 'Blah'} | |
429 | """ | |
430 | ||
431 | def __init__( | |
432 | self, | |
433 | prefix, | |
434 | args=None, | |
435 | perm="rw", | |
436 | desc=None, | |
437 | poll=False, | |
438 | handler=None | |
439 | ): | |
440 | super(Command, self).__init__( | |
441 | cmd=prefix + (' ' + args if args else ''), | |
442 | perm=perm, | |
443 | desc=desc, | |
444 | poll=poll) | |
445 | self.prefix = prefix | |
446 | self.args = args | |
447 | self.handler = handler | |
448 | ||
449 | def register(self, instance=False): | |
450 | """ | |
451 | Register a CLICommand handler. It allows an instance to register bound | |
452 | methods. In that case, the mgr instance is not passed, and it's expected | |
453 | to be available in the class instance. | |
454 | It also uses HandleCommandResult helper to return a wrapped a tuple of 3 | |
455 | items. | |
456 | """ | |
457 | return CLICommand( | |
458 | prefix=self.prefix, | |
459 | args=self.args, | |
460 | desc=self['desc'], | |
461 | perm=self['perm'] | |
462 | )( | |
463 | func=lambda mgr, *args, **kwargs: HandleCommandResult(*self.handler( | |
464 | *((instance or mgr,) + args), **kwargs)) | |
465 | ) | |
466 | ||
3efd9988 FG |
467 | |
468 | class MgrStandbyModule(ceph_module.BaseMgrStandbyModule): | |
469 | """ | |
470 | Standby modules only implement a serve and shutdown method, they | |
471 | are not permitted to implement commands and they do not receive | |
472 | any notifications. | |
473 | ||
b32b8144 | 474 | They only have access to the mgrmap (for accessing service URI info |
3efd9988 FG |
475 | from their active peer), and to configuration settings (read only). |
476 | """ | |
477 | ||
11fdf7f2 TL |
478 | MODULE_OPTIONS = [] |
479 | MODULE_OPTION_DEFAULTS = {} | |
480 | ||
3efd9988 FG |
481 | def __init__(self, module_name, capsule): |
482 | super(MgrStandbyModule, self).__init__(capsule) | |
483 | self.module_name = module_name | |
484 | self._logger = configure_logger(self, module_name) | |
11fdf7f2 TL |
485 | # see also MgrModule.__init__() |
486 | for o in self.MODULE_OPTIONS: | |
487 | if 'default' in o: | |
488 | if 'type' in o: | |
489 | self.MODULE_OPTION_DEFAULTS[o['name']] = o['default'] | |
490 | else: | |
491 | self.MODULE_OPTION_DEFAULTS[o['name']] = str(o['default']) | |
3efd9988 FG |
492 | |
493 | def __del__(self): | |
11fdf7f2 | 494 | unconfigure_logger() |
3efd9988 FG |
495 | |
496 | @property | |
497 | def log(self): | |
498 | return self._logger | |
499 | ||
500 | def serve(self): | |
501 | """ | |
502 | The serve method is mandatory for standby modules. | |
503 | :return: | |
504 | """ | |
505 | raise NotImplementedError() | |
506 | ||
507 | def get_mgr_id(self): | |
508 | return self._ceph_get_mgr_id() | |
509 | ||
11fdf7f2 | 510 | def get_module_option(self, key, default=None): |
b32b8144 FG |
511 | """ |
512 | Retrieve the value of a persistent configuration setting | |
513 | ||
514 | :param str key: | |
515 | :param default: the default value of the config if it is not found | |
516 | :return: str | |
517 | """ | |
11fdf7f2 | 518 | r = self._ceph_get_module_option(key) |
b32b8144 | 519 | if r is None: |
11fdf7f2 | 520 | return self.MODULE_OPTION_DEFAULTS.get(key, default) |
b32b8144 FG |
521 | else: |
522 | return r | |
523 | ||
11fdf7f2 TL |
524 | def get_ceph_option(self, key): |
525 | return self._ceph_get_option(key) | |
526 | ||
527 | def get_store(self, key): | |
528 | """ | |
529 | Retrieve the value of a persistent KV store entry | |
530 | ||
531 | :param key: String | |
532 | :return: Byte string or None | |
533 | """ | |
534 | return self._ceph_get_store(key) | |
3efd9988 FG |
535 | |
536 | def get_active_uri(self): | |
537 | return self._ceph_get_active_uri() | |
538 | ||
11fdf7f2 TL |
539 | def get_localized_module_option(self, key, default=None): |
540 | r = self._ceph_get_module_option(key, self.get_mgr_id()) | |
3efd9988 | 541 | if r is None: |
11fdf7f2 TL |
542 | return self.MODULE_OPTION_DEFAULTS.get(key, default) |
543 | else: | |
544 | return r | |
3efd9988 | 545 | |
3efd9988 FG |
546 | |
547 | class MgrModule(ceph_module.BaseMgrModule): | |
7c673cae | 548 | COMMANDS = [] |
11fdf7f2 TL |
549 | MODULE_OPTIONS = [] |
550 | MODULE_OPTION_DEFAULTS = {} | |
7c673cae | 551 | |
3efd9988 FG |
552 | # Priority definitions for perf counters |
553 | PRIO_CRITICAL = 10 | |
554 | PRIO_INTERESTING = 8 | |
555 | PRIO_USEFUL = 5 | |
556 | PRIO_UNINTERESTING = 2 | |
557 | PRIO_DEBUGONLY = 0 | |
558 | ||
559 | # counter value types | |
560 | PERFCOUNTER_TIME = 1 | |
561 | PERFCOUNTER_U64 = 2 | |
562 | ||
563 | # counter types | |
564 | PERFCOUNTER_LONGRUNAVG = 4 | |
565 | PERFCOUNTER_COUNTER = 8 | |
566 | PERFCOUNTER_HISTOGRAM = 0x10 | |
28e407b8 | 567 | PERFCOUNTER_TYPE_MASK = ~3 |
7c673cae | 568 | |
1adf2230 AA |
569 | # units supported |
570 | BYTES = 0 | |
571 | NONE = 1 | |
11fdf7f2 TL |
572 | |
573 | # Cluster log priorities | |
574 | CLUSTER_LOG_PRIO_DEBUG = 0 | |
575 | CLUSTER_LOG_PRIO_INFO = 1 | |
576 | CLUSTER_LOG_PRIO_SEC = 2 | |
577 | CLUSTER_LOG_PRIO_WARN = 3 | |
578 | CLUSTER_LOG_PRIO_ERROR = 4 | |
579 | ||
3efd9988 FG |
580 | def __init__(self, module_name, py_modules_ptr, this_ptr): |
581 | self.module_name = module_name | |
7c673cae | 582 | |
3efd9988 FG |
583 | # If we're taking over from a standby module, let's make sure |
584 | # its logger was unconfigured before we hook ours up | |
11fdf7f2 | 585 | unconfigure_logger() |
3efd9988 | 586 | self._logger = configure_logger(self, module_name) |
7c673cae | 587 | |
3efd9988 | 588 | super(MgrModule, self).__init__(py_modules_ptr, this_ptr) |
7c673cae | 589 | |
3efd9988 | 590 | self._version = self._ceph_get_version() |
7c673cae | 591 | |
3efd9988 | 592 | self._perf_schema_cache = None |
7c673cae | 593 | |
11fdf7f2 TL |
594 | # Keep a librados instance for those that need it. |
595 | self._rados = None | |
596 | ||
597 | for o in self.MODULE_OPTIONS: | |
598 | if 'default' in o: | |
599 | if 'type' in o: | |
600 | # we'll assume the declared type matches the | |
601 | # supplied default value's type. | |
602 | self.MODULE_OPTION_DEFAULTS[o['name']] = o['default'] | |
603 | else: | |
604 | # module not declaring it's type, so normalize the | |
605 | # default value to be a string for consistent behavior | |
606 | # with default and user-supplied option values. | |
607 | self.MODULE_OPTION_DEFAULTS[o['name']] = str(o['default']) | |
608 | ||
3efd9988 | 609 | def __del__(self): |
11fdf7f2 | 610 | unconfigure_logger() |
3efd9988 | 611 | |
11fdf7f2 TL |
612 | @classmethod |
613 | def _register_commands(cls): | |
614 | cls.COMMANDS.extend(CLICommand.dump_cmd_list()) | |
7c673cae FG |
615 | |
616 | @property | |
617 | def log(self): | |
618 | return self._logger | |
619 | ||
11fdf7f2 TL |
620 | def cluster_log(self, channel, priority, message): |
621 | """ | |
622 | :param channel: The log channel. This can be 'cluster', 'audit', ... | |
623 | :type channel: str | |
624 | :param priority: The log message priority. This can be | |
625 | CLUSTER_LOG_PRIO_DEBUG, CLUSTER_LOG_PRIO_INFO, | |
626 | CLUSTER_LOG_PRIO_SEC, CLUSTER_LOG_PRIO_WARN or | |
627 | CLUSTER_LOG_PRIO_ERROR. | |
628 | :type priority: int | |
629 | :param message: The message to log. | |
630 | :type message: str | |
631 | """ | |
632 | self._ceph_cluster_log(channel, priority, message) | |
633 | ||
7c673cae FG |
634 | @property |
635 | def version(self): | |
636 | return self._version | |
637 | ||
92f5a8d4 TL |
638 | @property |
639 | def release_name(self): | |
640 | """ | |
641 | Get the release name of the Ceph version, e.g. 'nautilus' or 'octopus'. | |
642 | :return: Returns the release name of the Ceph version in lower case. | |
643 | :rtype: str | |
644 | """ | |
645 | return self._ceph_get_release_name() | |
646 | ||
3efd9988 FG |
647 | def get_context(self): |
648 | """ | |
649 | :return: a Python capsule containing a C++ CephContext pointer | |
650 | """ | |
651 | return self._ceph_get_context() | |
652 | ||
7c673cae FG |
653 | def notify(self, notify_type, notify_id): |
654 | """ | |
655 | Called by the ceph-mgr service to notify the Python plugin | |
656 | that new state is available. | |
11fdf7f2 TL |
657 | |
658 | :param notify_type: string indicating what kind of notification, | |
659 | such as osd_map, mon_map, fs_map, mon_status, | |
660 | health, pg_summary, command, service_map | |
661 | :param notify_id: string (may be empty) that optionally specifies | |
662 | which entity is being notified about. With | |
663 | "command" notifications this is set to the tag | |
664 | ``from send_command``. | |
665 | """ | |
666 | pass | |
667 | ||
668 | def config_notify(self): | |
669 | """ | |
670 | Called by the ceph-mgr service to notify the Python plugin | |
671 | that the configuration may have changed. Modules will want to | |
672 | refresh any configuration values stored in config variables. | |
7c673cae FG |
673 | """ |
674 | pass | |
675 | ||
676 | def serve(self): | |
677 | """ | |
678 | Called by the ceph-mgr service to start any server that | |
679 | is provided by this Python plugin. The implementation | |
680 | of this function should block until ``shutdown`` is called. | |
681 | ||
682 | You *must* implement ``shutdown`` if you implement ``serve`` | |
683 | """ | |
684 | pass | |
685 | ||
686 | def shutdown(self): | |
687 | """ | |
688 | Called by the ceph-mgr service to request that this | |
689 | module drop out of its serve() function. You do not | |
690 | need to implement this if you do not implement serve() | |
691 | ||
692 | :return: None | |
693 | """ | |
11fdf7f2 TL |
694 | if self._rados: |
695 | self._rados.shutdown() | |
7c673cae FG |
696 | |
697 | def get(self, data_name): | |
698 | """ | |
11fdf7f2 TL |
699 | Called by the plugin to fetch named cluster-wide objects from ceph-mgr. |
700 | ||
701 | :param str data_name: Valid things to fetch are osd_crush_map_text, | |
702 | osd_map, osd_map_tree, osd_map_crush, config, mon_map, fs_map, | |
703 | osd_metadata, pg_summary, io_rate, pg_dump, df, osd_stats, | |
704 | health, mon_status, devices, device <devid>. | |
705 | ||
706 | Note: | |
707 | All these structures have their own JSON representations: experiment | |
708 | or look at the C++ ``dump()`` methods to learn about them. | |
7c673cae | 709 | """ |
3efd9988 | 710 | return self._ceph_get(data_name) |
7c673cae | 711 | |
94b18763 | 712 | def _stattype_to_str(self, stattype): |
11fdf7f2 | 713 | |
94b18763 FG |
714 | typeonly = stattype & self.PERFCOUNTER_TYPE_MASK |
715 | if typeonly == 0: | |
716 | return 'gauge' | |
717 | if typeonly == self.PERFCOUNTER_LONGRUNAVG: | |
718 | # this lie matches the DaemonState decoding: only val, no counts | |
719 | return 'counter' | |
720 | if typeonly == self.PERFCOUNTER_COUNTER: | |
721 | return 'counter' | |
722 | if typeonly == self.PERFCOUNTER_HISTOGRAM: | |
723 | return 'histogram' | |
11fdf7f2 | 724 | |
94b18763 FG |
725 | return '' |
726 | ||
81eedcae TL |
727 | def _perfpath_to_path_labels(self, daemon, path): |
728 | label_names = ("ceph_daemon",) | |
729 | labels = (daemon,) | |
730 | ||
731 | if daemon.startswith('rbd-mirror.'): | |
732 | match = re.match( | |
733 | r'^rbd_mirror_([^/]+)/(?:(?:([^/]+)/)?)(.*)\.(replay(?:_bytes|_latency)?)$', | |
734 | path | |
735 | ) | |
736 | if match: | |
737 | path = 'rbd_mirror_' + match.group(4) | |
738 | pool = match.group(1) | |
739 | namespace = match.group(2) or '' | |
740 | image = match.group(3) | |
741 | label_names += ('pool', 'namespace', 'image') | |
742 | labels += (pool, namespace, image) | |
743 | ||
744 | return path, label_names, labels, | |
745 | ||
28e407b8 AA |
746 | def _perfvalue_to_value(self, stattype, value): |
747 | if stattype & self.PERFCOUNTER_TIME: | |
748 | # Convert from ns to seconds | |
749 | return value / 1000000000.0 | |
750 | else: | |
751 | return value | |
752 | ||
1adf2230 AA |
753 | def _unit_to_str(self, unit): |
754 | if unit == self.NONE: | |
755 | return "/s" | |
756 | elif unit == self.BYTES: | |
11fdf7f2 TL |
757 | return "B/s" |
758 | ||
759 | @staticmethod | |
760 | def to_pretty_iec(n): | |
761 | for bits, suffix in [(60, 'Ei'), (50, 'Pi'), (40, 'Ti'), (30, 'Gi'), | |
762 | (20, 'Mi'), (10, 'Ki')]: | |
763 | if n > 10 << bits: | |
764 | return str(n >> bits) + ' ' + suffix | |
765 | return str(n) + ' ' | |
766 | ||
767 | @staticmethod | |
768 | def get_pretty_row(elems, width): | |
769 | """ | |
770 | Takes an array of elements and returns a string with those elements | |
771 | formatted as a table row. Useful for polling modules. | |
772 | ||
773 | :param elems: the elements to be printed | |
774 | :param width: the width of the terminal | |
775 | """ | |
776 | n = len(elems) | |
777 | column_width = int(width / n) | |
778 | ||
779 | ret = '|' | |
780 | for elem in elems: | |
781 | ret += '{0:>{w}} |'.format(elem, w=column_width - 2) | |
782 | ||
783 | return ret | |
784 | ||
785 | def get_pretty_header(self, elems, width): | |
786 | """ | |
787 | Like ``get_pretty_row`` but adds dashes, to be used as a table title. | |
788 | ||
789 | :param elems: the elements to be printed | |
790 | :param width: the width of the terminal | |
791 | """ | |
792 | n = len(elems) | |
793 | column_width = int(width / n) | |
794 | ||
795 | # dash line | |
796 | ret = '+' | |
797 | for i in range(0, n): | |
798 | ret += '-' * (column_width - 1) + '+' | |
799 | ret += '\n' | |
800 | ||
801 | # title | |
802 | ret += self.get_pretty_row(elems, width) | |
803 | ret += '\n' | |
804 | ||
805 | # dash line | |
806 | ret += '+' | |
807 | for i in range(0, n): | |
808 | ret += '-' * (column_width - 1) + '+' | |
809 | ret += '\n' | |
810 | ||
811 | return ret | |
812 | ||
7c673cae FG |
813 | def get_server(self, hostname): |
814 | """ | |
11fdf7f2 TL |
815 | Called by the plugin to fetch metadata about a particular hostname from |
816 | ceph-mgr. | |
817 | ||
818 | This is information that ceph-mgr has gleaned from the daemon metadata | |
819 | reported by daemons running on a particular server. | |
7c673cae | 820 | |
11fdf7f2 | 821 | :param hostname: a hostname |
7c673cae | 822 | """ |
3efd9988 | 823 | return self._ceph_get_server(hostname) |
7c673cae | 824 | |
c07f9fc5 FG |
825 | def get_perf_schema(self, svc_type, svc_name): |
826 | """ | |
827 | Called by the plugin to fetch perf counter schema info. | |
828 | svc_name can be nullptr, as can svc_type, in which case | |
829 | they are wildcards | |
830 | ||
11fdf7f2 TL |
831 | :param str svc_type: |
832 | :param str svc_name: | |
c07f9fc5 FG |
833 | :return: list of dicts describing the counters requested |
834 | """ | |
3efd9988 | 835 | return self._ceph_get_perf_schema(svc_type, svc_name) |
c07f9fc5 | 836 | |
7c673cae FG |
837 | def get_counter(self, svc_type, svc_name, path): |
838 | """ | |
11fdf7f2 TL |
839 | Called by the plugin to fetch the latest performance counter data for a |
840 | particular counter on a particular service. | |
7c673cae | 841 | |
11fdf7f2 TL |
842 | :param str svc_type: |
843 | :param str svc_name: | |
844 | :param str path: a period-separated concatenation of the subsystem and the | |
845 | counter name, for example "mds.inodes". | |
846 | :return: A list of two-tuples of (timestamp, value) is returned. This may be | |
847 | empty if no data is available. | |
7c673cae | 848 | """ |
3efd9988 | 849 | return self._ceph_get_counter(svc_type, svc_name, path) |
7c673cae | 850 | |
11fdf7f2 TL |
851 | def get_latest_counter(self, svc_type, svc_name, path): |
852 | """ | |
853 | Called by the plugin to fetch only the newest performance counter data | |
854 | pointfor a particular counter on a particular service. | |
855 | ||
856 | :param str svc_type: | |
857 | :param str svc_name: | |
858 | :param str path: a period-separated concatenation of the subsystem and the | |
859 | counter name, for example "mds.inodes". | |
860 | :return: A list of two-tuples of (timestamp, value) is returned. This may be | |
861 | empty if no data is available. | |
862 | """ | |
863 | return self._ceph_get_latest_counter(svc_type, svc_name, path) | |
864 | ||
7c673cae FG |
865 | def list_servers(self): |
866 | """ | |
11fdf7f2 TL |
867 | Like ``get_server``, but gives information about all servers (i.e. all |
868 | unique hostnames that have been mentioned in daemon metadata) | |
869 | ||
870 | :return: a list of information about all servers | |
871 | :rtype: list | |
7c673cae | 872 | """ |
3efd9988 | 873 | return self._ceph_get_server(None) |
7c673cae FG |
874 | |
875 | def get_metadata(self, svc_type, svc_id): | |
876 | """ | |
11fdf7f2 | 877 | Fetch the daemon metadata for a particular service. |
7c673cae | 878 | |
11fdf7f2 TL |
879 | ceph-mgr fetches metadata asynchronously, so are windows of time during |
880 | addition/removal of services where the metadata is not available to | |
881 | modules. ``None`` is returned if no metadata is available. | |
882 | ||
883 | :param str svc_type: service type (e.g., 'mds', 'osd', 'mon') | |
884 | :param str svc_id: service id. convert OSD integer IDs to strings when | |
885 | calling this | |
886 | :rtype: dict, or None if no metadata found | |
7c673cae | 887 | """ |
3efd9988 | 888 | return self._ceph_get_metadata(svc_type, svc_id) |
7c673cae | 889 | |
224ce89b WB |
890 | def get_daemon_status(self, svc_type, svc_id): |
891 | """ | |
892 | Fetch the latest status for a particular service daemon. | |
893 | ||
11fdf7f2 TL |
894 | This method may return ``None`` if no status information is |
895 | available, for example because the daemon hasn't fully started yet. | |
896 | ||
224ce89b WB |
897 | :param svc_type: string (e.g., 'rgw') |
898 | :param svc_id: string | |
11fdf7f2 | 899 | :return: dict, or None if the service is not found |
224ce89b | 900 | """ |
3efd9988 | 901 | return self._ceph_get_daemon_status(svc_type, svc_id) |
224ce89b | 902 | |
11fdf7f2 TL |
903 | def mon_command(self, cmd_dict): |
904 | """ | |
905 | Helper for modules that do simple, synchronous mon command | |
906 | execution. | |
907 | ||
908 | See send_command for general case. | |
909 | ||
910 | :return: status int, out std, err str | |
911 | """ | |
912 | ||
913 | t1 = time.time() | |
914 | result = CommandResult() | |
915 | self.send_command(result, "mon", "", json.dumps(cmd_dict), "") | |
916 | r = result.wait() | |
917 | t2 = time.time() | |
918 | ||
919 | self.log.debug("mon_command: '{0}' -> {1} in {2:.3f}s".format( | |
920 | cmd_dict['prefix'], r[0], t2 - t1 | |
921 | )) | |
922 | ||
923 | return r | |
924 | ||
7c673cae FG |
925 | def send_command(self, *args, **kwargs): |
926 | """ | |
927 | Called by the plugin to send a command to the mon | |
928 | cluster. | |
11fdf7f2 TL |
929 | |
930 | :param CommandResult result: an instance of the ``CommandResult`` | |
931 | class, defined in the same module as MgrModule. This acts as a | |
932 | completion and stores the output of the command. Use | |
933 | ``CommandResult.wait()`` if you want to block on completion. | |
934 | :param str svc_type: | |
935 | :param str svc_id: | |
936 | :param str command: a JSON-serialized command. This uses the same | |
937 | format as the ceph command line, which is a dictionary of command | |
938 | arguments, with the extra ``prefix`` key containing the command | |
939 | name itself. Consult MonCommands.h for available commands and | |
940 | their expected arguments. | |
941 | :param str tag: used for nonblocking operation: when a command | |
942 | completes, the ``notify()`` callback on the MgrModule instance is | |
943 | triggered, with notify_type set to "command", and notify_id set to | |
944 | the tag of the command. | |
7c673cae | 945 | """ |
3efd9988 | 946 | self._ceph_send_command(*args, **kwargs) |
7c673cae | 947 | |
c07f9fc5 FG |
948 | def set_health_checks(self, checks): |
949 | """ | |
c07f9fc5 FG |
950 | Set the module's current map of health checks. Argument is a |
951 | dict of check names to info, in this form: | |
952 | ||
11fdf7f2 TL |
953 | :: |
954 | ||
c07f9fc5 FG |
955 | { |
956 | 'CHECK_FOO': { | |
957 | 'severity': 'warning', # or 'error' | |
958 | 'summary': 'summary string', | |
959 | 'detail': [ 'list', 'of', 'detail', 'strings' ], | |
960 | }, | |
961 | 'CHECK_BAR': { | |
962 | 'severity': 'error', | |
963 | 'summary': 'bars are bad', | |
964 | 'detail': [ 'too hard' ], | |
965 | }, | |
966 | } | |
967 | ||
968 | :param list: dict of health check dicts | |
969 | """ | |
3efd9988 | 970 | self._ceph_set_health_checks(checks) |
c07f9fc5 | 971 | |
11fdf7f2 TL |
972 | def _handle_command(self, inbuf, cmd): |
973 | if cmd['prefix'] not in CLICommand.COMMANDS: | |
974 | return self.handle_command(inbuf, cmd) | |
975 | return CLICommand.COMMANDS[cmd['prefix']].call(self, cmd, inbuf) | |
976 | ||
977 | def handle_command(self, inbuf, cmd): | |
7c673cae FG |
978 | """ |
979 | Called by ceph-mgr to request the plugin to handle one | |
980 | of the commands that it declared in self.COMMANDS | |
981 | ||
982 | Return a status code, an output buffer, and an | |
983 | output string. The output buffer is for data results, | |
984 | the output string is for informative text. | |
985 | ||
11fdf7f2 TL |
986 | :param inbuf: content of any "-i <file>" supplied to ceph cli |
987 | :type inbuf: str | |
988 | :param cmd: from Ceph's cmdmap_t | |
989 | :type cmd: dict | |
7c673cae | 990 | |
11fdf7f2 | 991 | :return: HandleCommandResult or a 3-tuple of (int, str, str) |
7c673cae FG |
992 | """ |
993 | ||
994 | # Should never get called if they didn't declare | |
995 | # any ``COMMANDS`` | |
996 | raise NotImplementedError() | |
997 | ||
31f18b77 FG |
998 | def get_mgr_id(self): |
999 | """ | |
11fdf7f2 TL |
1000 | Retrieve the name of the manager daemon where this plugin |
1001 | is currently being executed (i.e. the active manager). | |
31f18b77 FG |
1002 | |
1003 | :return: str | |
1004 | """ | |
3efd9988 | 1005 | return self._ceph_get_mgr_id() |
31f18b77 | 1006 | |
11fdf7f2 TL |
1007 | def get_ceph_option(self, key): |
1008 | return self._ceph_get_option(key) | |
7c673cae | 1009 | |
11fdf7f2 TL |
1010 | def _validate_module_option(self, key): |
1011 | """ | |
1012 | Helper: don't allow get/set config callers to | |
1013 | access config options that they didn't declare | |
1014 | in their schema. | |
7c673cae | 1015 | """ |
11fdf7f2 TL |
1016 | if key not in [o['name'] for o in self.MODULE_OPTIONS]: |
1017 | raise RuntimeError("Config option '{0}' is not in {1}.MODULE_OPTIONS". | |
1018 | format(key, self.__class__.__name__)) | |
1019 | ||
1020 | def _get_module_option(self, key, default, localized_prefix=""): | |
1021 | r = self._ceph_get_module_option(self.module_name, key, | |
1022 | localized_prefix) | |
3efd9988 | 1023 | if r is None: |
11fdf7f2 | 1024 | return self.MODULE_OPTION_DEFAULTS.get(key, default) |
3efd9988 FG |
1025 | else: |
1026 | return r | |
7c673cae | 1027 | |
11fdf7f2 TL |
1028 | def get_module_option(self, key, default=None): |
1029 | """ | |
1030 | Retrieve the value of a persistent configuration setting | |
1031 | ||
1032 | :param str key: | |
1033 | :param str default: | |
1034 | :return: str | |
1035 | """ | |
1036 | self._validate_module_option(key) | |
1037 | return self._get_module_option(key, default) | |
1038 | ||
1039 | def get_module_option_ex(self, module, key, default=None): | |
1040 | """ | |
1041 | Retrieve the value of a persistent configuration setting | |
1042 | for the specified module. | |
1043 | ||
1044 | :param str module: The name of the module, e.g. 'dashboard' | |
1045 | or 'telemetry'. | |
1046 | :param str key: The configuration key, e.g. 'server_addr'. | |
1047 | :param str,None default: The default value to use when the | |
1048 | returned value is ``None``. Defaults to ``None``. | |
1049 | :return: str,int,bool,float,None | |
1050 | """ | |
1051 | if module == self.module_name: | |
1052 | self._validate_module_option(key) | |
1053 | r = self._ceph_get_module_option(module, key) | |
1054 | return default if r is None else r | |
1055 | ||
1056 | def get_store_prefix(self, key_prefix): | |
31f18b77 | 1057 | """ |
11fdf7f2 TL |
1058 | Retrieve a dict of KV store keys to values, where the keys |
1059 | have the given prefix | |
31f18b77 | 1060 | |
11fdf7f2 | 1061 | :param str key_prefix: |
31f18b77 FG |
1062 | :return: str |
1063 | """ | |
11fdf7f2 | 1064 | return self._ceph_get_store_prefix(key_prefix) |
31f18b77 | 1065 | |
11fdf7f2 TL |
1066 | def _set_localized(self, key, val, setter): |
1067 | return setter(_get_localized_key(self.get_mgr_id(), key), val) | |
1068 | ||
1069 | def get_localized_module_option(self, key, default=None): | |
224ce89b WB |
1070 | """ |
1071 | Retrieve localized configuration for this ceph-mgr instance | |
11fdf7f2 TL |
1072 | :param str key: |
1073 | :param str default: | |
224ce89b WB |
1074 | :return: str |
1075 | """ | |
11fdf7f2 TL |
1076 | self._validate_module_option(key) |
1077 | return self._get_module_option(key, default, self.get_mgr_id()) | |
224ce89b | 1078 | |
11fdf7f2 | 1079 | def _set_module_option(self, key, val): |
eafe8130 TL |
1080 | return self._ceph_set_module_option(self.module_name, key, |
1081 | None if val is None else str(val)) | |
224ce89b | 1082 | |
11fdf7f2 | 1083 | def set_module_option(self, key, val): |
7c673cae FG |
1084 | """ |
1085 | Set the value of a persistent configuration setting | |
1086 | ||
11fdf7f2 TL |
1087 | :param str key: |
1088 | :type val: str | None | |
7c673cae | 1089 | """ |
11fdf7f2 TL |
1090 | self._validate_module_option(key) |
1091 | return self._set_module_option(key, val) | |
7c673cae | 1092 | |
11fdf7f2 TL |
1093 | def set_module_option_ex(self, module, key, val): |
1094 | """ | |
1095 | Set the value of a persistent configuration setting | |
1096 | for the specified module. | |
1097 | ||
1098 | :param str module: | |
1099 | :param str key: | |
1100 | :param str val: | |
1101 | """ | |
1102 | if module == self.module_name: | |
1103 | self._validate_module_option(key) | |
1104 | return self._ceph_set_module_option(module, key, str(val)) | |
1105 | ||
1106 | def set_localized_module_option(self, key, val): | |
224ce89b WB |
1107 | """ |
1108 | Set localized configuration for this ceph-mgr instance | |
11fdf7f2 TL |
1109 | :param str key: |
1110 | :param str val: | |
224ce89b WB |
1111 | :return: str |
1112 | """ | |
11fdf7f2 TL |
1113 | self._validate_module_option(key) |
1114 | return self._set_localized(key, val, self._set_module_option) | |
224ce89b | 1115 | |
11fdf7f2 | 1116 | def set_store(self, key, val): |
7c673cae | 1117 | """ |
11fdf7f2 TL |
1118 | Set a value in this module's persistent key value store. |
1119 | If val is None, remove key from store | |
7c673cae | 1120 | |
11fdf7f2 TL |
1121 | :param str key: |
1122 | :param str val: | |
7c673cae | 1123 | """ |
11fdf7f2 | 1124 | self._ceph_set_store(key, val) |
7c673cae | 1125 | |
11fdf7f2 | 1126 | def get_store(self, key, default=None): |
7c673cae | 1127 | """ |
11fdf7f2 | 1128 | Get a value from this module's persistent key value store |
7c673cae | 1129 | """ |
11fdf7f2 TL |
1130 | r = self._ceph_get_store(key) |
1131 | if r is None: | |
1132 | return default | |
7c673cae | 1133 | else: |
11fdf7f2 TL |
1134 | return r |
1135 | ||
1136 | def get_localized_store(self, key, default=None): | |
1137 | r = self._ceph_get_store(_get_localized_key(self.get_mgr_id(), key)) | |
1138 | if r is None: | |
1139 | r = self._ceph_get_store(key) | |
1140 | if r is None: | |
1141 | r = default | |
1142 | return r | |
1143 | ||
1144 | def set_localized_store(self, key, val): | |
1145 | return self._set_localized(key, val, self.set_store) | |
224ce89b WB |
1146 | |
1147 | def self_test(self): | |
1148 | """ | |
1149 | Run a self-test on the module. Override this function and implement | |
1150 | a best as possible self-test for (automated) testing of the module | |
11fdf7f2 TL |
1151 | |
1152 | Indicate any failures by raising an exception. This does not have | |
1153 | to be pretty, it's mainly for picking up regressions during | |
1154 | development, rather than use in the field. | |
1155 | ||
1156 | :return: None, or an advisory string for developer interest, such | |
1157 | as a json dump of some state. | |
224ce89b WB |
1158 | """ |
1159 | pass | |
3efd9988 FG |
1160 | |
1161 | def get_osdmap(self): | |
1162 | """ | |
1163 | Get a handle to an OSDMap. If epoch==0, get a handle for the latest | |
1164 | OSDMap. | |
1165 | :return: OSDMap | |
1166 | """ | |
1167 | return self._ceph_get_osdmap() | |
1168 | ||
11fdf7f2 TL |
1169 | def get_latest(self, daemon_type, daemon_name, counter): |
1170 | data = self.get_latest_counter( | |
1171 | daemon_type, daemon_name, counter)[counter] | |
1172 | if data: | |
1173 | return data[1] | |
1174 | else: | |
1175 | return 0 | |
1176 | ||
1177 | def get_latest_avg(self, daemon_type, daemon_name, counter): | |
1178 | data = self.get_latest_counter( | |
1179 | daemon_type, daemon_name, counter)[counter] | |
1180 | if data: | |
1181 | return data[1], data[2] | |
1182 | else: | |
1183 | return 0, 0 | |
1184 | ||
1185 | def get_all_perf_counters(self, prio_limit=PRIO_USEFUL, | |
1186 | services=("mds", "mon", "osd", | |
81eedcae | 1187 | "rbd-mirror", "rgw", "tcmu-runner")): |
3efd9988 FG |
1188 | """ |
1189 | Return the perf counters currently known to this ceph-mgr | |
1190 | instance, filtered by priority equal to or greater than `prio_limit`. | |
1191 | ||
11fdf7f2 | 1192 | The result is a map of string to dict, associating services |
3efd9988 FG |
1193 | (like "osd.123") with their counters. The counter |
1194 | dict for each service maps counter paths to a counter | |
1195 | info structure, which is the information from | |
1196 | the schema, plus an additional "value" member with the latest | |
1197 | value. | |
1198 | """ | |
1199 | ||
1200 | result = defaultdict(dict) | |
1201 | ||
3efd9988 FG |
1202 | for server in self.list_servers(): |
1203 | for service in server['services']: | |
11fdf7f2 | 1204 | if service['type'] not in services: |
3efd9988 FG |
1205 | continue |
1206 | ||
1207 | schema = self.get_perf_schema(service['type'], service['id']) | |
1208 | if not schema: | |
1209 | self.log.warn("No perf counter schema for {0}.{1}".format( | |
1210 | service['type'], service['id'] | |
1211 | )) | |
1212 | continue | |
1213 | ||
1214 | # Value is returned in a potentially-multi-service format, | |
1215 | # get just the service we're asking about | |
11fdf7f2 TL |
1216 | svc_full_name = "{0}.{1}".format( |
1217 | service['type'], service['id']) | |
3efd9988 FG |
1218 | schema = schema[svc_full_name] |
1219 | ||
1220 | # Populate latest values | |
1221 | for counter_path, counter_schema in schema.items(): | |
1222 | # self.log.debug("{0}: {1}".format( | |
1223 | # counter_path, json.dumps(counter_schema) | |
1224 | # )) | |
1225 | if counter_schema['priority'] < prio_limit: | |
1226 | continue | |
1227 | ||
28e407b8 AA |
1228 | counter_info = dict(counter_schema) |
1229 | ||
1230 | # Also populate count for the long running avgs | |
1231 | if counter_schema['type'] & self.PERFCOUNTER_LONGRUNAVG: | |
11fdf7f2 | 1232 | v, c = self.get_latest_avg( |
28e407b8 AA |
1233 | service['type'], |
1234 | service['id'], | |
1235 | counter_path | |
1236 | ) | |
1237 | counter_info['value'], counter_info['count'] = v, c | |
1238 | result[svc_full_name][counter_path] = counter_info | |
1239 | else: | |
11fdf7f2 | 1240 | counter_info['value'] = self.get_latest( |
28e407b8 AA |
1241 | service['type'], |
1242 | service['id'], | |
1243 | counter_path | |
1244 | ) | |
1245 | ||
3efd9988 FG |
1246 | result[svc_full_name][counter_path] = counter_info |
1247 | ||
1248 | self.log.debug("returning {0} counter".format(len(result))) | |
1249 | ||
1250 | return result | |
1251 | ||
1252 | def set_uri(self, uri): | |
1253 | """ | |
1254 | If the module exposes a service, then call this to publish the | |
1255 | address once it is available. | |
1256 | ||
1257 | :return: a string | |
1258 | """ | |
1259 | return self._ceph_set_uri(uri) | |
94b18763 FG |
1260 | |
1261 | def have_mon_connection(self): | |
1262 | """ | |
1263 | Check whether this ceph-mgr daemon has an open connection | |
1264 | to a monitor. If it doesn't, then it's likely that the | |
1265 | information we have about the cluster is out of date, | |
1266 | and/or the monitor cluster is down. | |
1267 | """ | |
1268 | ||
28e407b8 | 1269 | return self._ceph_have_mon_connection() |
11fdf7f2 TL |
1270 | |
1271 | def update_progress_event(self, evid, desc, progress): | |
1272 | return self._ceph_update_progress_event(str(evid), str(desc), float(progress)) | |
1273 | ||
1274 | def complete_progress_event(self, evid): | |
1275 | return self._ceph_complete_progress_event(str(evid)) | |
1276 | ||
1277 | def clear_all_progress_events(self): | |
1278 | return self._ceph_clear_all_progress_events() | |
1279 | ||
1280 | @property | |
1281 | def rados(self): | |
1282 | """ | |
1283 | A librados instance to be shared by any classes within | |
1284 | this mgr module that want one. | |
1285 | """ | |
1286 | if self._rados: | |
1287 | return self._rados | |
1288 | ||
1289 | ctx_capsule = self.get_context() | |
1290 | self._rados = rados.Rados(context=ctx_capsule) | |
1291 | self._rados.connect() | |
1292 | ||
1293 | return self._rados | |
1294 | ||
1295 | @staticmethod | |
1296 | def can_run(): | |
1297 | """ | |
1298 | Implement this function to report whether the module's dependencies | |
1299 | are met. For example, if the module needs to import a particular | |
1300 | dependency to work, then use a try/except around the import at | |
1301 | file scope, and then report here if the import failed. | |
1302 | ||
1303 | This will be called in a blocking way from the C++ code, so do not | |
1304 | do any I/O that could block in this function. | |
1305 | ||
1306 | :return a 2-tuple consisting of a boolean and explanatory string | |
1307 | """ | |
1308 | ||
1309 | return True, "" | |
1310 | ||
1311 | def remote(self, module_name, method_name, *args, **kwargs): | |
1312 | """ | |
1313 | Invoke a method on another module. All arguments, and the return | |
1314 | value from the other module must be serializable. | |
1315 | ||
1316 | Limitation: Do not import any modules within the called method. | |
1317 | Otherwise you will get an error in Python 2:: | |
1318 | ||
1319 | RuntimeError('cannot unmarshal code objects in restricted execution mode',) | |
1320 | ||
1321 | ||
1322 | ||
1323 | :param module_name: Name of other module. If module isn't loaded, | |
1324 | an ImportError exception is raised. | |
1325 | :param method_name: Method name. If it does not exist, a NameError | |
1326 | exception is raised. | |
1327 | :param args: Argument tuple | |
1328 | :param kwargs: Keyword argument dict | |
1329 | :raises RuntimeError: **Any** error raised within the method is converted to a RuntimeError | |
1330 | :raises ImportError: No such module | |
1331 | """ | |
1332 | return self._ceph_dispatch_remote(module_name, method_name, | |
1333 | args, kwargs) | |
1334 | ||
1335 | def add_osd_perf_query(self, query): | |
1336 | """ | |
1337 | Register an OSD perf query. Argument is a | |
1338 | dict of the query parameters, in this form: | |
1339 | ||
1340 | :: | |
1341 | ||
1342 | { | |
1343 | 'key_descriptor': [ | |
1344 | {'type': subkey_type, 'regex': regex_pattern}, | |
1345 | ... | |
1346 | ], | |
1347 | 'performance_counter_descriptors': [ | |
1348 | list, of, descriptor, types | |
1349 | ], | |
1350 | 'limit': {'order_by': performance_counter_type, 'max_count': n}, | |
1351 | } | |
1352 | ||
1353 | Valid subkey types: | |
1354 | 'client_id', 'client_address', 'pool_id', 'namespace', 'osd_id', | |
1355 | 'pg_id', 'object_name', 'snap_id' | |
1356 | Valid performance counter types: | |
1357 | 'ops', 'write_ops', 'read_ops', 'bytes', 'write_bytes', 'read_bytes', | |
1358 | 'latency', 'write_latency', 'read_latency' | |
1359 | ||
1360 | :param object query: query | |
1361 | :rtype: int (query id) | |
1362 | """ | |
1363 | return self._ceph_add_osd_perf_query(query) | |
1364 | ||
1365 | def remove_osd_perf_query(self, query_id): | |
1366 | """ | |
1367 | Unregister an OSD perf query. | |
1368 | ||
1369 | :param int query_id: query ID | |
1370 | """ | |
1371 | return self._ceph_remove_osd_perf_query(query_id) | |
1372 | ||
1373 | def get_osd_perf_counters(self, query_id): | |
1374 | """ | |
1375 | Get stats collected for an OSD perf query. | |
1376 | ||
1377 | :param int query_id: query ID | |
1378 | """ | |
1379 | return self._ceph_get_osd_perf_counters(query_id) | |
494da23a | 1380 | |
92f5a8d4 TL |
1381 | def is_authorized(self, arguments): |
1382 | """ | |
1383 | Verifies that the current session caps permit executing the py service | |
1384 | or current module with the provided arguments. This provides a generic | |
1385 | way to allow modules to restrict by more fine-grained controls (e.g. | |
1386 | pools). | |
1387 | ||
1388 | :param arguments: dict of key/value arguments to test | |
1389 | """ | |
1390 | return self._ceph_is_authorized(arguments) | |
1391 | ||
494da23a TL |
1392 | |
1393 | class PersistentStoreDict(object): | |
1394 | def __init__(self, mgr, prefix): | |
1395 | # type: (MgrModule, str) -> None | |
1396 | self.mgr = mgr | |
1397 | self.prefix = prefix + '.' | |
1398 | ||
1399 | def _mk_store_key(self, key): | |
1400 | return self.prefix + key | |
1401 | ||
1402 | def __missing__(self, key): | |
1403 | # KeyError won't work for the `in` operator. | |
1404 | # https://docs.python.org/3/reference/expressions.html#membership-test-details | |
1405 | raise IndexError('PersistentStoreDict: "{}" not found'.format(key)) | |
1406 | ||
1407 | def clear(self): | |
1408 | # Don't make any assumptions about the content of the values. | |
1409 | for item in six.iteritems(self.mgr.get_store_prefix(self.prefix)): | |
1410 | k, _ = item | |
1411 | self.mgr.set_store(k, None) | |
1412 | ||
1413 | def __getitem__(self, item): | |
1414 | # type: (str) -> Any | |
1415 | key = self._mk_store_key(item) | |
1416 | try: | |
1417 | val = self.mgr.get_store(key) | |
1418 | if val is None: | |
1419 | self.__missing__(key) | |
1420 | return json.loads(val) | |
1421 | except (KeyError, AttributeError, IndexError, ValueError, TypeError): | |
1422 | logging.getLogger(__name__).exception('failed to deserialize') | |
1423 | self.mgr.set_store(key, None) | |
1424 | raise | |
1425 | ||
1426 | def __setitem__(self, item, value): | |
1427 | # type: (str, Any) -> None | |
1428 | """ | |
1429 | value=None is not allowed, as it will remove the key. | |
1430 | """ | |
1431 | key = self._mk_store_key(item) | |
1432 | self.mgr.set_store(key, json.dumps(value) if value is not None else None) | |
1433 | ||
1434 | def __delitem__(self, item): | |
1435 | self[item] = None | |
1436 | ||
1437 | def __len__(self): | |
1438 | return len(self.keys()) | |
1439 | ||
1440 | def items(self): | |
1441 | # type: () -> Iterator[Tuple[str, Any]] | |
1442 | prefix_len = len(self.prefix) | |
1443 | try: | |
1444 | for item in six.iteritems(self.mgr.get_store_prefix(self.prefix)): | |
1445 | k, v = item | |
1446 | yield k[prefix_len:], json.loads(v) | |
1447 | except (KeyError, AttributeError, IndexError, ValueError, TypeError): | |
1448 | logging.getLogger(__name__).exception('failed to deserialize') | |
1449 | self.clear() | |
1450 | ||
1451 | def keys(self): | |
1452 | # type: () -> Set[str] | |
1453 | return {item[0] for item in self.items()} | |
1454 | ||
1455 | def __iter__(self): | |
1456 | return iter(self.keys()) |