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