Srikanth Vavilapalli | 1725e49 | 2014-12-01 17:50:52 -0800 | [diff] [blame] | 1 | # |
| 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 | |
| 25 | import model_info_list |
| 26 | import os |
| 27 | import array |
| 28 | import datetime |
| 29 | import utif |
| 30 | import re |
| 31 | import fmtcnv |
| 32 | |
| 33 | class 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 |