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