]>
git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/ceph_daemon.py
2 # vim: ts=4 sw=4 smarttab expandtab
5 Copyright (C) 2015 Red Hat
7 This is free software; you can redistribute it and/or
8 modify it under the terms of the GNU General Public
9 License version 2, as published by the Free Software
10 Foundation. See file COPYING.
18 from collections
import OrderedDict
19 from fcntl
import ioctl
20 from fnmatch
import fnmatch
21 from prettytable
import PrettyTable
, HEADER
22 from signal
import signal
, Signals
, SIGWINCH
23 from termios
import TIOCGWINSZ
24 from types
import FrameType
25 from typing
import Any
, Callable
, Dict
, List
, Optional
, Sequence
, TextIO
, Tuple
27 from ceph_argparse
import parse_json_funcsigs
, validate_command
30 LONG_RUNNING_AVG
= 0x4
31 READ_CHUNK_SIZE
= 4096
34 def admin_socket(asok_path
: str,
36 format
: Optional
[str] = '') -> bytes
:
38 Send a daemon (--admin-daemon) command 'cmd'. asok_path is the
39 path to the admin socket; cmd is a list of strings; format may be
40 set to one of the formatted forms to get output in that form
41 (daemon commands don't support 'plain' output).
44 def do_sockio(path
: str, cmd_bytes
: bytes
) -> bytes
:
45 """ helper: do all the actual low-level stream I/O """
46 sock
= socket
.socket(socket
.AF_UNIX
, socket
.SOCK_STREAM
)
49 sock
.sendall(cmd_bytes
+ b
'\0')
50 len_str
= sock
.recv(4)
52 raise RuntimeError("no data returned from admin socket")
53 l
, = struct
.unpack(">I", len_str
)
58 # recv() receives signed int, i.e max 2GB
59 # workaround by capping READ_CHUNK_SIZE per call.
60 want
= min(l
- got
, READ_CHUNK_SIZE
)
65 except Exception as sock_e
:
66 raise RuntimeError('exception: ' + str(sock_e
))
70 cmd_json
= do_sockio(asok_path
,
71 b
'{"prefix": "get_command_descriptions"}')
72 except Exception as e
:
73 raise RuntimeError('exception getting command descriptions: ' + str(e
))
75 sigdict
= parse_json_funcsigs(cmd_json
.decode('utf-8'), 'cli')
76 valid_dict
= validate_command(sigdict
, cmd
)
78 raise RuntimeError('invalid command')
81 valid_dict
['format'] = format
84 ret
= do_sockio(asok_path
, json
.dumps(valid_dict
).encode('utf-8'))
85 except Exception as e
:
86 raise RuntimeError('exception: ' + str(e
))
91 class Termsize(object):
92 DEFAULT_SIZE
= (25, 80)
94 def __init__(self
) -> None:
95 self
.rows
, self
.cols
= self
._gettermsize
()
98 def _gettermsize(self
) -> Tuple
[int, int]:
100 fd
= sys
.stdin
.fileno()
101 sz
= struct
.pack('hhhh', 0, 0, 0, 0)
102 rows
, cols
= struct
.unpack('hhhh', ioctl(fd
, TIOCGWINSZ
, sz
))[:2]
105 return self
.DEFAULT_SIZE
107 def update(self
) -> None:
108 rows
, cols
= self
._gettermsize
()
110 self
.changed
= (self
.rows
, self
.cols
) != (rows
, cols
)
111 self
.rows
, self
.cols
= rows
, cols
113 def reset_changed(self
) -> None:
116 def __str__(self
) -> str:
117 return '%s(%dx%d, changed %s)' % (self
.__class
__,
118 self
.rows
, self
.cols
, self
.changed
)
120 def __repr__(self
) -> str:
121 return '%s(%d,%d,%s)' % (self
.__class
__,
122 self
.rows
, self
.cols
, self
.changed
)
125 class DaemonWatcher(object):
127 Given a Ceph daemon's admin socket path, poll its performance counters
128 and output a series of output lines showing the momentary values of
129 counters of interest (those with the 'nick' property in Ceph's schema)
142 RESET_SEQ
= "\033[0m"
143 COLOR_SEQ
= "\033[1;%dm"
144 COLOR_DARK_SEQ
= "\033[0;%dm"
146 UNDERLINE_SEQ
= "\033[4m"
150 statpats
: Optional
[Sequence
[str]] = None,
151 min_prio
: int = 0) -> None:
152 self
.asok_path
= asok
153 self
._colored
= False
155 self
._stats
: Optional
[Dict
[str, dict]] = None
157 self
._statpats
= statpats
158 self
._stats
_that
_fit
: Dict
[str, dict] = OrderedDict()
159 self
._min
_prio
= min_prio
160 self
.termsize
= Termsize()
162 def supports_color(self
, ostr
: TextIO
) -> bool:
164 Returns True if the running system's terminal supports color, and False
167 unsupported_platform
= (sys
.platform
in ('win32', 'Pocket PC'))
168 # isatty is not always implemented, #6223.
169 is_a_tty
= hasattr(ostr
, 'isatty') and ostr
.isatty()
170 if unsupported_platform
or not is_a_tty
:
177 dark
: bool = False) -> str:
179 Decorate `msg` with escape sequences to give the requested color
181 return (self
.COLOR_DARK_SEQ
if dark
else self
.COLOR_SEQ
) % (30 + color
) \
182 + msg
+ self
.RESET_SEQ
184 def bold(self
, msg
: str) -> str:
186 Decorate `msg` with escape sequences to make it appear bold
188 return self
.BOLD_SEQ
+ msg
+ self
.RESET_SEQ
190 def format_dimless(self
, n
: int, width
: int) -> str:
192 Format a number without units, so as to fit into `width` characters, substituting
193 an appropriate unit suffix.
195 units
= [' ', 'k', 'M', 'G', 'T', 'P', 'E', 'Z']
197 while len("%s" % (int(n
) // (1000**unit
))) > width
- 1:
198 if unit
>= len(units
) - 1:
203 truncated_float
= ("%f" % (n
/ (1000.0 ** unit
)))[0:width
- 1]
204 if truncated_float
[-1] == '.':
205 truncated_float
= " " + truncated_float
[0:-1]
207 truncated_float
= "%{wid}d".format(wid
=width
-1) % n
208 formatted
= "%s%s" % (truncated_float
, units
[unit
])
212 color
= self
.BLACK
, False
214 color
= self
.YELLOW
, False
215 return self
.bold(self
.colorize(formatted
[0:-1], color
[0], color
[1])) \
216 + self
.bold(self
.colorize(formatted
[-1], self
.YELLOW
, False))
220 def col_width(self
, nick
: str) -> int:
222 Given the short name `nick` for a column, how many characters
223 of width should the column be allocated? Does not include spacing
226 return max(len(nick
), 4)
228 def get_stats_that_fit(self
) -> Tuple
[Dict
[str, dict], bool]:
230 Get a possibly-truncated list of stats to display based on
231 current terminal width. Allow breaking mid-section.
233 current_fit
: Dict
[str, dict] = OrderedDict()
234 if self
.termsize
.changed
or not self
._stats
_that
_fit
:
236 assert self
._stats
is not None
237 for section_name
, names
in self
._stats
.items():
238 for name
, stat_data
in names
.items():
239 width
+= self
.col_width(stat_data
) + 1
240 if width
> self
.termsize
.cols
:
242 if section_name
not in current_fit
:
243 current_fit
[section_name
] = OrderedDict()
244 current_fit
[section_name
][name
] = stat_data
245 if width
> self
.termsize
.cols
:
248 self
.termsize
.reset_changed()
249 changed
= bool(current_fit
) and (current_fit
!= self
._stats
_that
_fit
)
251 self
._stats
_that
_fit
= current_fit
252 return self
._stats
_that
_fit
, changed
254 def _print_headers(self
, ostr
: TextIO
) -> None:
256 Print a header row to `ostr`
259 stats
, _
= self
.get_stats_that_fit()
260 for section_name
, names
in stats
.items():
262 sum([self
.col_width(x
) + 1 for x
in names
.values()]) - 1
263 pad
= max(section_width
- len(section_name
), 0)
264 pad_prefix
= pad
// 2
265 header
+= (pad_prefix
* '-')
266 header
+= (section_name
[0:section_width
])
267 header
+= ((pad
- pad_prefix
) * '-')
270 ostr
.write(self
.colorize(header
, self
.BLUE
, True))
273 for section_name
, names
in stats
.items():
274 for stat_name
, stat_nick
in names
.items():
275 sub_header
+= self
.UNDERLINE_SEQ \
277 stat_nick
.ljust(self
.col_width(stat_nick
)),
280 sub_header
= sub_header
[0:-1] + self
.colorize('|', self
.BLUE
)
282 ostr
.write(sub_header
)
284 def _print_vals(self
,
286 dump
: Dict
[str, Any
],
287 last_dump
: Dict
[str, Any
]) -> None:
289 Print a single row of values to `ostr`, based on deltas between `dump` and
293 fit
, changed
= self
.get_stats_that_fit()
295 self
._print
_headers
(ostr
)
296 for section_name
, names
in fit
.items():
297 for stat_name
, stat_nick
in names
.items():
298 assert self
._schema
is not None
299 stat_type
= self
._schema
[section_name
][stat_name
]['type']
300 if bool(stat_type
& COUNTER
):
301 n
= max(dump
[section_name
][stat_name
] -
302 last_dump
[section_name
][stat_name
], 0)
303 elif bool(stat_type
& LONG_RUNNING_AVG
):
304 entries
= dump
[section_name
][stat_name
]['avgcount'] - \
305 last_dump
[section_name
][stat_name
]['avgcount']
307 n
= (dump
[section_name
][stat_name
]['sum'] -
308 last_dump
[section_name
][stat_name
]['sum']) \
310 n
*= 1000.0 # Present in milliseconds
314 n
= dump
[section_name
][stat_name
]
316 val_row
+= self
.format_dimless(int(n
),
317 self
.col_width(stat_nick
))
319 val_row
= val_row
[0:-1]
320 val_row
+= self
.colorize("|", self
.BLUE
)
321 val_row
= val_row
[0:-len(self
.colorize("|", self
.BLUE
))]
322 ostr
.write("{0}\n".format(val_row
))
324 def _should_include(self
, sect
: str, name
: str, prio
: int) -> bool:
326 boolean: should we output this stat?
328 1) If self._statpats exists and the name filename-glob-matches
329 anything in the list, and prio is high enough, or
330 2) If self._statpats doesn't exist and prio is high enough
335 sectname
= '.'.join((sect
, name
))
337 p
for p
in self
._statpats
338 if fnmatch(name
, p
) or fnmatch(sectname
, p
)
342 if self
._min
_prio
is not None and prio
is not None:
343 return (prio
>= self
._min
_prio
)
347 def _load_schema(self
) -> None:
349 Populate our instance-local copy of the daemon's performance counter
350 schema, and work out which stats we will display.
352 self
._schema
= json
.loads(
353 admin_socket(self
.asok_path
, ["perf", "schema"]).decode('utf-8'),
354 object_pairs_hook
=OrderedDict
)
356 # Build list of which stats we will display
357 self
._stats
= OrderedDict()
358 assert self
._schema
is not None
359 for section_name
, section_stats
in self
._schema
.items():
360 for name
, schema_data
in section_stats
.items():
361 prio
= schema_data
.get('priority', 0)
362 if self
._should
_include
(section_name
, name
, prio
):
363 if section_name
not in self
._stats
:
364 self
._stats
[section_name
] = OrderedDict()
365 self
._stats
[section_name
][name
] = schema_data
['nick']
366 if not len(self
._stats
):
367 raise RuntimeError("no stats selected by filters")
369 def _handle_sigwinch(self
,
371 frame
: FrameType
) -> None:
372 self
.termsize
.update()
376 count
: Optional
[int] = None,
377 ostr
: TextIO
= sys
.stdout
) -> None:
379 Print output at regular intervals until interrupted.
381 :param ostr: Stream to which to send output
385 self
._colored
= self
.supports_color(ostr
)
387 self
._print
_headers
(ostr
)
389 last_dump
= json
.loads(admin_socket(self
.asok_path
, ["perf", "dump"]).decode('utf-8'))
390 rows_since_header
= 0
393 signal(SIGWINCH
, self
._handle
_sigwinch
)
395 dump
= json
.loads(admin_socket(self
.asok_path
, ["perf", "dump"]).decode('utf-8'))
396 if rows_since_header
>= self
.termsize
.rows
- 2:
397 self
._print
_headers
(ostr
)
398 rows_since_header
= 0
399 self
._print
_vals
(ostr
, dump
, last_dump
)
400 if count
is not None:
404 rows_since_header
+= 1
407 # time.sleep() is interrupted by SIGWINCH; avoid that
408 end
= time
.time() + interval
409 while time
.time() < end
:
410 time
.sleep(end
- time
.time())
412 except KeyboardInterrupt:
415 def list(self
, ostr
: TextIO
= sys
.stdout
) -> None:
417 Show all selected stats with section, full name, nick, and prio
419 table
= PrettyTable(('section', 'name', 'nick', 'prio'))
420 table
.align
['section'] = 'l'
421 table
.align
['name'] = 'l'
422 table
.align
['nick'] = 'l'
423 table
.align
['prio'] = 'r'
425 assert self
._stats
is not None
426 assert self
._schema
is not None
427 for section_name
, section_stats
in self
._stats
.items():
428 for name
, nick
in section_stats
.items():
429 prio
= self
._schema
[section_name
][name
].get('priority') or 0
430 table
.add_row((section_name
, name
, nick
, prio
))
431 ostr
.write(table
.get_string(hrules
=HEADER
) + '\n')