blob: 47f119b6461fed4491163bb1e2b92a7b87a0d79e [file] [log] [blame]
Srikanth Vavilapalli1725e492014-12-01 17:50:52 -08001#
2# Copyright (c) 2010,2011,2012,2013 Big Switch Networks, Inc.
3#
4# Licensed under the Eclipse Public License, Version 1.0 (the
5# "License"); you may not use this file except in compliance with the
6# License. You may obtain a copy of the License at
7#
8# http://www.eclipse.org/legal/epl-v10.html
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied. See the License for the specific language governing
14# permissions and limitations under the License.
15#
16
17#
18# PRETTYPRINT
19#
20# This module contains classes that help formatting text for the CLI,
21# including tables, individual records etc.
22#
23# Formatting information is stored in table_info, a dict of dicts.
24
25import model_info_list
26import os
27import array
28import datetime
29import utif
30import re
31import fmtcnv
32
33class PrettyPrinter():
34 table_info = None # Annotated table format. This is a dict of dicts. Set by cli.
35
36 def __init__(self, bs):
37 self.sdnsh = bs
38 self.table_info = {}
39 self.format_version = {}
40 self.format_added_modules = {}
41
42 self.add_format('MODEL',
43 'model_info',
44 model_info_list.model_info_dict)
45
46
47 def add_format(self, name, origin, format_dict):
48 """
49 Add a format to the saved formats, from the dictionary
50 with 'name'. The 'name' can help identify the source
51 for this format when errors arise
52 """
53
54 # very interesting items in a format are the field_orderings,
55 # and the fields description. It could make sense to only
56 # allow specific items from the fields, to prevent field-output
57 # formatting from using uninnteded database fields.
58 for (format_name, format) in format_dict.items():
59 if not format_name in self.table_info:
60 self.table_info[format_name] = { }
61 # combine details: top level items
62 for (item_name, item_value) in format.items():
63 if item_name == 'fields':
64 if not item_name in self.table_info[format_name]:
65 self.table_info[format_name][item_name] = {}
66 for (term_name, term_value) in item_value.items():
67 if not term_name in self.table_info[format_name][item_name]:
68 self.table_info[format_name][item_name][term_name] = {}
69 self.table_info[format_name][item_name][term_name].update(term_value)
70 if item_name == 'field-orderings':
71 if not item_name in self.table_info[format_name]:
72 self.table_info[format_name][item_name] = {}
73 self.table_info[format_name][item_name].update(item_value)
74 #
75 # save the name of the source for this format
76 if not 'self' in self.table_info[format_name]:
77 self.table_info[format_name]['self'] = name
78 if not 'origin' in self.table_info[format_name]:
79 self.table_info[format_name]['origin'] = origin
80 #
81 # Add the 'Idx' field if it's not there.
82 if format_name in self.table_info:
83 if not 'fields' in self.table_info[format_name]:
84 self.table_info[format_name]['fields'] = {}
85 if not 'Idx' in self.table_info[format_name]['fields']:
86 self.table_info[format_name]['fields']['Idx'] = {
87 'verbose-name' : '#',
88 'type' : 'CharField'
89 }
90
91
92 def add_module_name(self, version, module):
93 """
94 Save some state about the version/module
95 """
96 if version not in self.format_version:
97 self.format_version[version] = [module]
98 else:
99 self.format_version[version].append(module)
100
101
102 def add_formats_from_module(self, version, module):
103 self.add_module_name(version, module)
104 for name in dir(module):
105 if re.match(r'.*_FORMAT$', name):
106 if module.__name__ not in self.format_added_modules:
107 self.format_added_modules[module.__name__] = [name]
108 if name not in self.format_added_modules[module.__name__]:
109 self.format_added_modules[module.__name__].append(name)
110 self.add_format(name, module.__name__, getattr(module, name))
111
112
113 # Utility Functions
114 def get_field_info(self, obj_type_info, field_name):
115 return obj_type_info['fields'].get(field_name, None)
116
117
118 def format_to_alias_update(self, display_format, update):
119 """
120 Given a format, find all the formatters, and for each
121 formatter, determine which aliases need to be updated.
122 """
123 if display_format not in self.table_info:
124 # this will fail soon enough when final output is attempted
125 return
126 format_dict = self.table_info[display_format]
127 if not 'fields' in format_dict:
128 return
129
130 for (field_name, field_dict) in format_dict['fields'].items():
131 if 'formatter' in field_dict:
132 fmtcnv.formatter_to_alias_update(field_dict['formatter'], update)
133 elif 'entry_formatter' in field_dict:
134 fmtcnv.formatter_to_alias_update(field_dict['entry-formatter'], update)
135 return update
136
137
138 def formats(self):
139 return self.table_info.keys()
140
141 def format_details(self):
142 """
143 Return a table of formats suitable as a format_table parameter
144 """
145 return [ { 'format' : x,
146 'format dict' : self.table_info[x]['self'],
147 'origin' : self.table_info[x]['origin'] }
148
149 for x in self.formats()]
150
151
152 def format_as_header(self, field):
153 # LOOK! could prob do something fancy to handle camelCapStrings or underscore_strings
154 return field[0].capitalize() + field[1:]
155
156
157 def get_header_for_field(self, obj_type_info, field_name):
158 field_info = self.get_field_info(obj_type_info, field_name)
159 if not field_info:
160 return self.format_as_header(field_name)
161 default_header = self.format_as_header(field_name)
162 return field_info.get('verbose-name', default_header)
163
164
165 def format_table(self, data_list, display_format = None, field_ordering="default"):
166 """
167 Takes in list of dicts and generates nice table, e.g.
168
169 id slice_id MAC Address ip Switch ID
170 ------------------------------------------------
171 1 1 00:00:00:00:01:03 10013 150861407404
172 2 2 00:00:00:00:01:01 10011 150861407404
173 3 1 00:00:00:00:02:03 10023 150866955514
174
175 @param data_list - a list of dicts
176 @param format describes the format of the output table to display
177 @param field_ordering is the field_ordering identifier in the model,
178 there can be multiple ("default", "brief", "detailed")
179
180 """
181
182 #
183 # first, determine a list of fields to be printed, then using that list,
184 # determine the fields width of each field, including calling any formatting
185 # function, then format the result
186 #
187 # during the format computation, replace the value with the 'formatted value'
188 # to prevent multiple calls to the same formatting funcition
189 #
190 if not data_list or not type(data_list) == list:
191 if type(data_list) == dict and "error_type" in data_list:
192 return data_list.get("description", "Internal error")
193 return "None."
194
195 format_info = self.table_info.get(display_format, None)
196
197 if self.sdnsh.description:
198 if format_info == None:
199 print 'format_table: missing format %s' % display_format
200 else:
201 format_from = format_info.get('self', '')
202 print 'format_table: %s %s %d entries' % (
203 display_format, format_from, len(data_list))
204
205 field_widths = {}
206 field_headers = {}
207 fields_to_print = []
208
209 #
210 # do the 'figur'n for which fields will get printed.
211 #
212 # to determine the length, call the formatting function, and replace the
213 # value for that field with the updated value; then 'cypher the length.
214 #
215 # note that field_widths.keys() are all the possible fields
216 # check if the headers makes any field wider and set fields_to_print
217 #
218 if format_info:
219 if 'field-orderings' in format_info:
220 fields_to_print = format_info['field-orderings'].get(field_ordering, [])
221 if len(fields_to_print) == 0: # either no field_orderings or couldn't find specific
222 fields_to_print = format_info['fields'].keys()
223 for f in fields_to_print:
224 header = self.get_header_for_field(format_info, f)
225 field_headers[f] = header
226 field_widths[f] = max(len(header), field_widths.get(f, 0))
227 # LOOK! not done now... add in extra fields discovered in data_list if desired
228 # right now, fields_to_print is a projection on the data
229 else:
230 # get fields_to_print from the field names in data_list,
231 # which is (intended to be) a list of dictionaries
232 all_fields = utif.unique_list_from_list(sum([x.keys()
233 for x in data_list], []))
234 fields_to_print = sorted(all_fields)
235
236 if self.sdnsh.description:
237 print 'format_table: field order "%s" fields %s' % \
238 (field_ordering, fields_to_print)
239
240 #
241 # generate a fields_to_print ordered list with field_widths for each
242 # by going through all data and then using field_ordering if avail.
243 #
244 row_index = 0
245 for row in data_list:
246 row_index += 1
247 if not 'Idx' in row:
248 row['Idx'] = row_index
249 for key in fields_to_print:
250 #for (k,v) in row.items():
251 if format_info:
252 # don't worry about header here - do that soon below
253 info = self.get_field_info(format_info, key)
254 if info and info.get('formatter'):
255 row[key] = str(info['formatter'](row.get(key, ''), row))
256 w = len(row[key])
257 else:
258 w = len(str(row.get(key, '')))
259 else:
260 field_headers[key] = self.format_as_header(key)
261 w = max(len(str(row[key])), len(field_headers[key]))
262 field_widths[key] = max(w, field_widths.get(key, 0))
263
264 #
265 # generate the format_str and header lines based on fields_to_print
266 #
267 format_str_per_field = []
268 for f in fields_to_print:
269 format_str_per_field.append("%%(%s)-%ds" % (f, field_widths[f]))
270
271 row_format_str = " ".join(format_str_per_field) + "\n"
272
273 #
274 # finally print! only caveat is to handle sparse data with a blank_dict
275 # let result be a list, and append new strings to generate the final result,
276 # (for better python performance)
277 #
278 result= []
279 result.append(" ".join(format_str_per_field) % field_headers + "\n")
280 result.append("|".join(["-"*field_widths[f] for f in fields_to_print]) + "\n") # I <3 python too
281
282 blank_dict = dict([(f,"") for f in fields_to_print])
283 for row in data_list:
284 result.append(row_format_str % dict(blank_dict, **row))
285
286 return ''.join(result)
287
288
289 def format_entry(self, data, display_format=None, field_ordering="default", debug=False):
290 """
291 Takes in parsed JSON object, generates nice single entry printout,
292 intended for 'details' display
293
294 @param data list of dictionaries, values for output
295 @param format name of format description to use for printing
296 @param field_ordering list of field to print
297 @param debug print values of compound keys
298 """
299 if not data:
300 return "None."
301 elif type(data) == dict and "error_type" in data:
302 return data.get("description", "Internal error")
303
304 format_info = self.table_info.get(display_format, None)
305
306 # Print. Pretty please.
307 if format_info:
308 fields = format_info['fields']
309 else:
310 fields = dict([[x, {}] for x in data.keys()])
311 format_info = { 'fields' : fields }
312 if self.sdnsh.description:
313 print "format_entry: Missing format ", display_format, fields
314
315 # Find widest data field name
316 label_w = len( max(data, key=lambda x:len(x)) )
317 if format_info:
318 verbose_len = max([len(self.get_header_for_field(format_info, x)) for x in fields.keys()])
319 # This isn't exactly right, the verbose names for the fields ought to be replaced first
320 label_w = max(verbose_len, label_w)
321 label_str = "%%-%ds :" % label_w
322
323 # Use format_info for this table to order fields if possible
324 fields_to_print = None
325 if format_info:
326 if 'field-orderings' in format_info:
327 fields_to_print = format_info['field-orderings'].get(field_ordering, [])
328 else:
329 if self.sdnsh.description:
330 print 'Error: internal: %s field ordering %s not present for %s' % \
331 (display_format, field_ordering, format_info)
332 if fields_to_print == None or len(fields_to_print) == 0:
333 # either no field_orderings or couldn't find specific
334 fields_to_print = format_info['fields'].keys()
335 else:
336 fields_to_print = sorted(fields.keys())
337
338 result = ""
339 tmp_merged_dict = dict([(f,"") for f in fields_to_print], **data)
340 blank_dict = dict([(f,"") for f in fields_to_print])
341
342 # first print the requested fields
343 all_fields_in_correct_order = list(fields_to_print)
344 # then the remaining fields
345 all_fields_in_correct_order.extend([x for x in fields.keys()
346 if x not in fields_to_print])
347 # give all the formatter's a shot, save the updates
348 updated = {}
349 for e in all_fields_in_correct_order:
350 if format_info:
351 info = self.get_field_info(format_info, e)
352 if not info:
353 continue
354 if 'entry-formatter' in info:
355 updated[e] = info['entry-formatter'](
356 tmp_merged_dict.get(e, ''), tmp_merged_dict)
357 elif 'formatter' in info:
358 updated[e] = info['formatter'](
359 tmp_merged_dict.get(e, ''), tmp_merged_dict)
360
361 tmp_merged_dict.update(updated)
362
363 data.update(updated)
364 all_fields_in_correct_order = filter(lambda x: x in data,
365 all_fields_in_correct_order)
366 for e in all_fields_in_correct_order:
367 if format_info:
368 info = self.get_field_info(format_info, e)
369 if not debug and info and 'help_text' in info and info['help_text'][0] == '#':
370 # sdnsh._is_compound_key(), please skip display of compound key
371 continue
372 result += (label_str % self.get_header_for_field(format_info, e)+" "+
373 str(tmp_merged_dict[e]) + "\n");
374
375 return result[:-1]
376
377 def get_terminal_size(self):
378 def ioctl_GWINSZ(fd):
379 try:
380 import fcntl, termios, struct, os
381 cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ,
382 '1234'))
383 except:
384 return None
385 return cr
386 cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
387 if not cr:
388 try:
389 fd = os.open(os.ctermid(), os.O_RDONLY)
390 cr = ioctl_GWINSZ(fd)
391 os.close(fd)
392 except:
393 pass
394 if not cr:
395 try:
396 cr = (os.environ['LINES'], os.environ['COLUMNS'])
397 except:
398 cr = (24, 80)
399
400 if (cr[1] == 0 or cr[0] == 0):
401 return (80, 24)
402
403 return int(cr[1]), int(cr[0])
404
405 def format_time_series_graph(self, data, obj_type=None, field_ordering="default"):
406 if not data:
407 return "None."
408 elif type(data) == dict and "error_type" in data:
409 return data.get("description", "Internal error")
410
411 obj_type_info = self.table_info.get(obj_type, None)
412
413 if self.sdnsh.description:
414 print "format_time_series_graph", obj_type, obj_type_info
415 yunits = None
416 ylabel = "Value"
417 if (obj_type_info):
418 ylabel = obj_type_info['fields']['value']['verbose-name']
419 if 'units' in obj_type_info['fields']['value']:
420 yunits = obj_type_info['fields']['value']['units']
421
422
423 miny = None
424 maxy = None
425 minx = None
426 maxx = None
427
428 for (x,y) in data:
429 if miny == None or y < miny:
430 miny = y
431 if maxy == None or y > maxy:
432 maxy = y
433 if minx == None or x < minx:
434 minx = x
435 if maxx == None or x > maxx:
436 maxx = x
437
438 if (yunits == '%'):
439 maxy = 100
440
441 if isinstance(maxy, float) and maxy < 10.0:
442 axisyw = len('%.5f' % maxy)
443 else:
444 axisyw = len('%s' % maxy)
445
446 (twidth, theight) = self.get_terminal_size()
447
448 width = twidth-axisyw
449 height = theight-5
450
451 ybucket = float(maxy)/height;
452
453 xbucket = (maxx-minx)/width;
454 if (xbucket == 0):
455 minx = maxx - 1800000
456 maxx += 1800000
457 xbucket = maxx/width;
458
459 graph = array.array('c')
460 graph.fromstring(' ' * (width * height))
461 for (x,y) in data:
462 if (ybucket == 0):
463 yc = height
464 else:
465 yc = int(round((maxy-y)/ybucket))
466 if (xbucket == 0):
467 xy = width
468 else:
469 xc = int(round((x-minx)/xbucket))
470
471 if (yc < 0):
472 yc = 0
473 if (yc >= height):
474 yc = height-1
475 if (xc < 0):
476 xc = 0
477 if (xc >= width):
478 xc = width-1
479
480 #print (xc,yc, x, y, yc*width + xc)
481 for i in range(yc,height):
482 graph[i*width + xc] = '#'
483
484 b = '%s\n' % (ylabel)
485
486 if isinstance(maxy, float) and maxy < 10.0:
487 form = '%%%d.5f|%%s\n'
488 else:
489 form = '%%%ds|%%s\n'
490
491 for i in range(0,height-1):
492 ylabel = maxy - i*ybucket
493 if not isinstance(maxy, float) or maxy >= 10.0:
494 ylabel = int(round(ylabel))
495 b += (form % axisyw) % \
496 (ylabel,
497 ''.join(graph[i*width:(i+1)*width-1]))
498 b += (form % axisyw) % \
499 (0, ''.join(graph[(height-1)*width:height*width-1]).replace(' ', '_'))
500
501 b += '%s' % (' ' * axisyw)
502 d = ' ' * axisyw
503
504 olddate = None
505 interval = (maxx - minx)/(width/7.0)
506 for i in range(0, width/7):
507 curtimestamp = minx + interval*i
508
509 if i == width/7-1:
510 df = (' ' * (width % 7)) + " %m/%d^"
511 tf = (' ' * (width % 7)) + " %H:%M^"
512 curtimestamp = maxx
513 else:
514 df = "^%m/%d "
515 tf = "^%H:%M "
516
517 curtime = datetime.datetime.fromtimestamp(curtimestamp/1000.0)
518 date = curtime.strftime(df)
519 b += curtime.strftime(tf)
520 if (date != olddate):
521 olddate = date
522 d += date
523 else:
524 d += ' ' * 7
525
526 b += '\n%s\n' % d
527 b += '%s%sTime' % (' ' * axisyw, ' ' * (width/2-2))
528
529 return b