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