]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/ceph_daemon.py
bump version to 12.0.3-pve3
[ceph.git] / ceph / src / pybind / ceph_daemon.py
CommitLineData
7c673cae
FG
1# -*- mode:python -*-
2# vim: ts=4 sw=4 smarttab expandtab
3
4"""
5Copyright (C) 2015 Red Hat
6
7This is free software; you can redistribute it and/or
8modify it under the terms of the GNU General Public
9License version 2, as published by the Free Software
10Foundation. See file COPYING.
11"""
12
13import sys
14import json
15import socket
16import struct
17import time
18from collections import OrderedDict
19from fcntl import ioctl
20from fnmatch import fnmatch
21from prettytable import PrettyTable, HEADER
22from signal import signal, SIGWINCH
23from termios import TIOCGWINSZ
24
25from ceph_argparse import parse_json_funcsigs, validate_command
26
27COUNTER = 0x8
28LONG_RUNNING_AVG = 0x4
29READ_CHUNK_SIZE = 4096
30
31
32def admin_socket(asok_path, cmd, format=''):
33 """
34 Send a daemon (--admin-daemon) command 'cmd'. asok_path is the
35 path to the admin socket; cmd is a list of strings; format may be
36 set to one of the formatted forms to get output in that form
37 (daemon commands don't support 'plain' output).
38 """
39
40 def do_sockio(path, cmd_bytes):
41 """ helper: do all the actual low-level stream I/O """
42 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
43 sock.connect(path)
44 try:
45 sock.sendall(cmd_bytes + b'\0')
46 len_str = sock.recv(4)
47 if len(len_str) < 4:
48 raise RuntimeError("no data returned from admin socket")
49 l, = struct.unpack(">I", len_str)
50 sock_ret = b''
51
52 got = 0
53 while got < l:
54 # recv() receives signed int, i.e max 2GB
55 # workaround by capping READ_CHUNK_SIZE per call.
56 want = min(l - got, READ_CHUNK_SIZE)
57 bit = sock.recv(want)
58 sock_ret += bit
59 got += len(bit)
60
61 except Exception as sock_e:
62 raise RuntimeError('exception: ' + str(sock_e))
63 return sock_ret
64
65 try:
66 cmd_json = do_sockio(asok_path,
67 b'{"prefix": "get_command_descriptions"}')
68 except Exception as e:
69 raise RuntimeError('exception getting command descriptions: ' + str(e))
70
71 if cmd == 'get_command_descriptions':
72 return cmd_json
73
74 sigdict = parse_json_funcsigs(cmd_json.decode('utf-8'), 'cli')
75 valid_dict = validate_command(sigdict, cmd)
76 if not valid_dict:
77 raise RuntimeError('invalid command')
78
79 if format:
80 valid_dict['format'] = format
81
82 try:
83 ret = do_sockio(asok_path, json.dumps(valid_dict).encode('utf-8'))
84 except Exception as e:
85 raise RuntimeError('exception: ' + str(e))
86
87 return ret
88
89
90def _gettermsize():
91 try:
92 rows, cols = struct.unpack('hhhh', ioctl(0, TIOCGWINSZ, 8*'\x00'))[0:2]
93 except IOError:
94 return 25, 80
95
96 return rows,cols
97
98
99class Termsize(object):
100
101 def __init__(self):
102 self.rows, self.cols = _gettermsize()
103 self.changed = False
104
105 def update(self):
106 rows, cols = _gettermsize()
107 self.changed = self.changed or (
108 (self.rows != rows) or (self.cols != cols)
109 )
110 self.rows, self.cols = rows, cols
111
112 def reset_changed(self):
113 self.changed = False
114
115 def __str__(self):
116 return '%s(%dx%d, changed %s)' % (self.__class__, self.rows, self.cols, self.changed)
117
118 def __repr__(self):
119 return 'Termsize(%d,%d,%s)' % (self.__class__, self.rows, self.cols, self.changed)
120
121
122class DaemonWatcher(object):
123 """
124 Given a Ceph daemon's admin socket path, poll its performance counters
125 and output a series of output lines showing the momentary values of
126 counters of interest (those with the 'nick' property in Ceph's schema)
127 """
128 (
129 BLACK,
130 RED,
131 GREEN,
132 YELLOW,
133 BLUE,
134 MAGENTA,
135 CYAN,
136 GRAY
137 ) = range(8)
138
139 RESET_SEQ = "\033[0m"
140 COLOR_SEQ = "\033[1;%dm"
141 COLOR_DARK_SEQ = "\033[0;%dm"
142 BOLD_SEQ = "\033[1m"
143 UNDERLINE_SEQ = "\033[4m"
144
145 def __init__(self, asok, statpats=None, min_prio=0):
146 self.asok_path = asok
147 self._colored = False
148
149 self._stats = None
150 self._schema = None
151 self._statpats = statpats
152 self._stats_that_fit = dict()
153 self._min_prio = min_prio
154 self.termsize = Termsize()
155
156 def supports_color(self, ostr):
157 """
158 Returns True if the running system's terminal supports color, and False
159 otherwise.
160 """
161 unsupported_platform = (sys.platform in ('win32', 'Pocket PC'))
162 # isatty is not always implemented, #6223.
163 is_a_tty = hasattr(ostr, 'isatty') and ostr.isatty()
164 if unsupported_platform or not is_a_tty:
165 return False
166 return True
167
168 def colorize(self, msg, color, dark=False):
169 """
170 Decorate `msg` with escape sequences to give the requested color
171 """
172 return (self.COLOR_DARK_SEQ if dark else self.COLOR_SEQ) % (30 + color) \
173 + msg + self.RESET_SEQ
174
175 def bold(self, msg):
176 """
177 Decorate `msg` with escape sequences to make it appear bold
178 """
179 return self.BOLD_SEQ + msg + self.RESET_SEQ
180
181 def format_dimless(self, n, width):
182 """
183 Format a number without units, so as to fit into `width` characters, substituting
184 an appropriate unit suffix.
185 """
186 units = [' ', 'k', 'M', 'G', 'T', 'P']
187 unit = 0
188 while len("%s" % (int(n) // (1000**unit))) > width - 1:
189 unit += 1
190
191 if unit > 0:
192 truncated_float = ("%f" % (n / (1000.0 ** unit)))[0:width - 1]
193 if truncated_float[-1] == '.':
194 truncated_float = " " + truncated_float[0:-1]
195 else:
196 truncated_float = "%{wid}d".format(wid=width-1) % n
197 formatted = "%s%s" % (truncated_float, units[unit])
198
199 if self._colored:
200 if n == 0:
201 color = self.BLACK, False
202 else:
203 color = self.YELLOW, False
204 return self.bold(self.colorize(formatted[0:-1], color[0], color[1])) \
205 + self.bold(self.colorize(formatted[-1], self.BLACK, False))
206 else:
207 return formatted
208
209 def col_width(self, nick):
210 """
211 Given the short name `nick` for a column, how many characters
212 of width should the column be allocated? Does not include spacing
213 between columns.
214 """
215 return max(len(nick), 4)
216
217 def get_stats_that_fit(self):
218 '''
219 Get a possibly-truncated list of stats to display based on
220 current terminal width. Allow breaking mid-section.
221 '''
222 current_fit = OrderedDict()
223 if self.termsize.changed or not self._stats_that_fit:
224 width = 0
225 for section_name, names in self._stats.items():
226 for name, stat_data in names.items():
227 width += self.col_width(stat_data) + 1
228 if width > self.termsize.cols:
229 break
230 if section_name not in current_fit:
231 current_fit[section_name] = OrderedDict()
232 current_fit[section_name][name] = stat_data
233 if width > self.termsize.cols:
234 break
235
236 self.termsize.reset_changed()
237 changed = current_fit and (current_fit != self._stats_that_fit)
238 if changed:
239 self._stats_that_fit = current_fit
240 return self._stats_that_fit, changed
241
242 def _print_headers(self, ostr):
243 """
244 Print a header row to `ostr`
245 """
246 header = ""
247 stats, _ = self.get_stats_that_fit()
248 for section_name, names in stats.items():
249 section_width = \
250 sum([self.col_width(x) + 1 for x in names.values()]) - 1
251 pad = max(section_width - len(section_name), 0)
252 pad_prefix = pad // 2
253 header += (pad_prefix * '-')
254 header += (section_name[0:section_width])
255 header += ((pad - pad_prefix) * '-')
256 header += ' '
257 header += "\n"
258 ostr.write(self.colorize(header, self.BLUE, True))
259
260 sub_header = ""
261 for section_name, names in stats.items():
262 for stat_name, stat_nick in names.items():
263 sub_header += self.UNDERLINE_SEQ \
264 + self.colorize(
265 stat_nick.ljust(self.col_width(stat_nick)),
266 self.BLUE) \
267 + ' '
268 sub_header = sub_header[0:-1] + self.colorize('|', self.BLUE)
269 sub_header += "\n"
270 ostr.write(sub_header)
271
272 def _print_vals(self, ostr, dump, last_dump):
273 """
274 Print a single row of values to `ostr`, based on deltas between `dump` and
275 `last_dump`.
276 """
277 val_row = ""
278 fit, changed = self.get_stats_that_fit()
279 if changed:
280 self._print_headers(ostr)
281 for section_name, names in fit.items():
282 for stat_name, stat_nick in names.items():
283 stat_type = self._schema[section_name][stat_name]['type']
284 if bool(stat_type & COUNTER):
285 n = max(dump[section_name][stat_name] -
286 last_dump[section_name][stat_name], 0)
287 elif bool(stat_type & LONG_RUNNING_AVG):
288 entries = dump[section_name][stat_name]['avgcount'] - \
289 last_dump[section_name][stat_name]['avgcount']
290 if entries:
291 n = (dump[section_name][stat_name]['sum'] -
292 last_dump[section_name][stat_name]['sum']) \
293 / float(entries)
294 n *= 1000.0 # Present in milliseconds
295 else:
296 n = 0
297 else:
298 n = dump[section_name][stat_name]
299
300 val_row += self.format_dimless(n, self.col_width(stat_nick))
301 val_row += " "
302 val_row = val_row[0:-1]
303 val_row += self.colorize("|", self.BLUE)
304 val_row = val_row[0:-len(self.colorize("|", self.BLUE))]
305 ostr.write("{0}\n".format(val_row))
306
307 def _should_include(self, sect, name, prio):
308 '''
309 boolean: should we output this stat?
310
311 1) If self._statpats exists and the name filename-glob-matches
312 anything in the list, and prio is high enough, or
313 2) If self._statpats doesn't exist and prio is high enough
314
315 then yes.
316 '''
317 if self._statpats:
318 sectname = '.'.join((sect, name))
319 if not any([
320 p for p in self._statpats
321 if fnmatch(name, p) or fnmatch(sectname, p)
322 ]):
323 return False
324
325 if self._min_prio is not None and prio is not None:
326 return (prio >= self._min_prio)
327
328 return True
329
330 def _load_schema(self):
331 """
332 Populate our instance-local copy of the daemon's performance counter
333 schema, and work out which stats we will display.
334 """
335 self._schema = json.loads(
336 admin_socket(self.asok_path, ["perf", "schema"]).decode('utf-8'),
337 object_pairs_hook=OrderedDict)
338
339 # Build list of which stats we will display
340 self._stats = OrderedDict()
341 for section_name, section_stats in self._schema.items():
342 for name, schema_data in section_stats.items():
343 prio = schema_data.get('priority', 0)
344 if self._should_include(section_name, name, prio):
345 if section_name not in self._stats:
346 self._stats[section_name] = OrderedDict()
347 self._stats[section_name][name] = schema_data['nick']
348 if not len(self._stats):
349 raise RuntimeError("no stats selected by filters")
350
351 def _handle_sigwinch(self, signo, frame):
352 self.termsize.update()
353
354 def run(self, interval, count=None, ostr=sys.stdout):
355 """
356 Print output at regular intervals until interrupted.
357
358 :param ostr: Stream to which to send output
359 """
360
361 self._load_schema()
362 self._colored = self.supports_color(ostr)
363
364 self._print_headers(ostr)
365
366 last_dump = json.loads(admin_socket(self.asok_path, ["perf", "dump"]).decode('utf-8'))
367 rows_since_header = 0
368
369 try:
370 signal(SIGWINCH, self._handle_sigwinch)
371 while True:
372 dump = json.loads(admin_socket(self.asok_path, ["perf", "dump"]).decode('utf-8'))
373 if rows_since_header >= self.termsize.rows - 2:
374 self._print_headers(ostr)
375 rows_since_header = 0
376 self._print_vals(ostr, dump, last_dump)
377 if count is not None:
378 count -= 1
379 if count <= 0:
380 break
381 rows_since_header += 1
382 last_dump = dump
383
384 # time.sleep() is interrupted by SIGWINCH; avoid that
385 end = time.time() + interval
386 while time.time() < end:
387 time.sleep(end - time.time())
388
389 except KeyboardInterrupt:
390 return
391
392 def list(self, ostr=sys.stdout):
393 """
394 Show all selected stats with section, full name, nick, and prio
395 """
396 table = PrettyTable(('section', 'name', 'nick', 'prio'))
397 table.align['section'] = 'l'
398 table.align['name'] = 'l'
399 table.align['nick'] = 'l'
400 table.align['prio'] = 'r'
401 self._load_schema()
402 for section_name, section_stats in self._stats.items():
403 for name, nick in section_stats.items():
404 prio = self._schema[section_name][name].get('priority') or 0
405 table.add_row((section_name, name, nick, prio))
406 ostr.write(table.get_string(hrules=HEADER) + '\n')