# Copyright 2013-2017 Wind River, Inc # Copyright 2012 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. try: import tsconfig.tsconfig as tsc is_remote = False except Exception: is_remote = True import argparse import copy import dateutil import math import os import prettytable import re import six import sys import textwrap import uuid from prettytable import ALL from prettytable import FRAME from prettytable import NONE from datetime import datetime from dateutil import parser from cgtsclient import exc from cgtsclient.openstack.common import importutils from functools import wraps # noinspection PyProtectedMember from wrapping_formatters import _get_width from cgtsclient.common import wrapping_formatters from six.moves import input class HelpFormatter(argparse.HelpFormatter): def start_section(self, heading): # Title-case the headings heading = '%s%s' % (heading[0].upper(), heading[1:]) super(HelpFormatter, self).start_section(heading) # noinspection PyUnusedLocal def _wrapping_formatter_callback_decorator(subparser, command, callback): """ - Adds the --nowrap option to a CLI command. This option, when on, deactivates word wrapping. - Decorates the command's callback function in order to process the nowrap flag :param subparser: :return: decorated callback """ try: subparser.add_argument('--nowrap', action='store_true', help='No wordwrapping of output') except Exception as e: # exception happens when nowrap option already configured # for command - so get out with callback undecorated return callback def no_wrap_decorator_builder(callback): def process_callback_with_no_wrap(cc, args={}): no_wrap = args.nowrap # turn on/off wrapping formatters when outputting CLI results wrapping_formatters.set_no_wrap(no_wrap) return callback(cc, args=args) return process_callback_with_no_wrap decorated_callback = no_wrap_decorator_builder(callback) return decorated_callback def _does_command_need_no_wrap(callback): if callback.__name__.startswith("do_") and \ callback.__name__.endswith("_list"): return True if callback.__name__ in \ ['donot_config_ntp_list', 'donot_config_ptp_list', 'do_host_apply_memprofile', 'do_host_apply_cpuprofile', 'do_host_apply_ifprofile', 'do_host_apply_profile', 'do_host_apply_storprofile', 'donot_config_oam_list', 'donot_dns_list', 'do_host_cpu_modify', 'do_event_suppress', 'do_event_unsuppress', 'do_event_unsuppress_all']: return True return False def define_command(subparsers, command, callback, cmd_mapper): '''Define a command in the subparsers collection. :param subparsers: subparsers collection where the command will go :param command: command name :param callback: function that will be used to process the command ''' desc = callback.__doc__ or '' help = desc.strip().split('\n')[0] arguments = getattr(callback, 'arguments', []) subparser = subparsers.add_parser(command, help=help, description=desc, add_help=False, formatter_class=HelpFormatter) subparser.add_argument('-h', '--help', action='help', help=argparse.SUPPRESS) # Are we a list command? if _does_command_need_no_wrap(callback): # then decorate it with wrapping data formatter functionality func = _wrapping_formatter_callback_decorator(subparser, command, callback) else: func = callback cmd_mapper[command] = subparser for (args, kwargs) in arguments: subparser.add_argument(*args, **kwargs) subparser.set_defaults(func=func) def define_commands_from_module(subparsers, command_module, cmd_mapper): '''Find all methods beginning with 'do_' in a module, and add them as commands into a subparsers collection. ''' for method_name in (a for a in dir(command_module) if a.startswith('do_')): # Commands should be hypen-separated instead of underscores. command = method_name[3:].replace('_', '-') callback = getattr(command_module, method_name) define_command(subparsers, command, callback, cmd_mapper) # Decorator for cli-args def arg(*args, **kwargs): def _decorator(func): # Because of the sematics of decorator composition if we just append # to the options list positional options will appear to be backwards. func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs)) return func return _decorator def prettytable_builder(field_names=None, **kwargs): return WRPrettyTable(field_names, **kwargs) # noinspection PyUnusedLocal def wordwrap_header(field, field_label, formatter): """ Given a field label (the header text for one column) and the word wrapping formatter for a column, this function asks the formatter for the desired column width and then performs a wordwrap of field_label :param field: the field name associated with the field_label :param field_label: field_label to word wrap :param formatter: the field formatter :return: word wrapped field_label """ if wrapping_formatters.is_nowrap_set(): return field_label if not wrapping_formatters.WrapperFormatter.is_wrapper_formatter(formatter): return field_label # go to the column's formatter and ask it what the width should be wrapper_formatter = formatter.wrapper_formatter actual_width = wrapper_formatter.get_actual_column_char_len(wrapper_formatter.get_calculated_desired_width()) # now word wrap based on column width wrapped_header = textwrap.fill(field_label, actual_width) return wrapped_header def pretty_choice_list(l): return ', '.join("'%s'" % i for i in l) def _sort_for_list(objs, fields, formatters={}, sortby=0, reversesort=False): # Sort only if necessary if sortby is None: return objs rows_to_sort = copy.deepcopy(objs) sort_field = fields[sortby] # figure out sort key function if sort_field in formatters: field_formatter = formatters[sort_field] if wrapping_formatters.WrapperFormatter.is_wrapper_formatter(field_formatter): sort_key = lambda o: field_formatter.wrapper_formatter.get_unwrapped_field_value(o) else: sort_key = lambda o: field_formatter(o) else: sort_key = lambda o: getattr(o, sort_field, '') rows_to_sort.sort(reverse=reversesort, key=sort_key) return rows_to_sort def default_printer(s): print s def pt_builder(field_labels, fields, formatters, paging, printer=default_printer): """ returns an object that 'fronts' a prettyTable object that can handle paging as well as automatically falling back to not word wrapping when word wrapping does not cause the output to fit the terminal width. """ class PT_Builder(object): def __init__(self, field_labels, fields, formatters, no_paging): self.objs_in_pt = [] self.unwrapped_field_labels = field_labels self.fields = fields self.formatters = formatters self.header_height = 0 self.terminal_width, self.terminal_height = get_terminal_size() self.terminal_lines_left = self.terminal_height self.paging = not no_paging self.paged_rows_added = 0 self.pt = None self.quit = False def add_row(self, obj): if self.quit: return False if not self.pt: self.build_pretty_table() return self._row_add(obj) def __add_row_and_obj(self, row, obj): self.pt.add_row(row) self.objs_in_pt.append(obj) def _row_add(self, obj): row = _build_row_from_object(self.fields, self.formatters, obj) if not paging: self.__add_row_and_obj(row, obj) return True rheight = row_height(row) if (self.terminal_lines_left - rheight) >= 0 or self.paged_rows_added == 0: self.__add_row_and_obj(row, obj) self.terminal_lines_left -= rheight else: printer(self.get_string()) if self.terminal_lines_left > 0: printer("\n" * (self.terminal_lines_left - 1)) s = input("Press Enter to continue or 'q' to exit...") if s == 'q': self.quit = True return False self.terminal_lines_left = self.terminal_height - self.header_height self.build_pretty_table() self.__add_row_and_obj(row, obj) self.terminal_lines_left -= rheight self.paged_rows_added += 1 def get_string(self): if not self.pt: self.build_pretty_table() objs = copy.copy(self.objs_in_pt) self.objs_in_pt = [] output = self.pt.get_string() if wrapping_formatters.is_nowrap_set(): return output output_width = _get_width(output) if output_width <= self.terminal_width: return output # At this point pretty Table (self.pt) does not fit the terminal width so let's # temporarily turn wrapping off, rebuild the pretty Table with the data unwrapped. orig_no_wrap_settings = wrapping_formatters.set_no_wrap_on_formatters(True, self.formatters) self.build_pretty_table() for o in objs: self.add_row(o) wrapping_formatters.unset_no_wrap_on_formatters(orig_no_wrap_settings) return self.pt.get_string() def build_pretty_table(self): field_labels = [wordwrap_header(field, field_label, formatter) for field, field_label, formatter in zip(self.fields, self.unwrapped_field_labels, [formatters.get(f, None) for f in self.fields])] self.pt = prettytable_builder(field_labels, caching=False, print_empty=False) self.pt.align = 'l' # 2 header border lines + 1 bottom border + 1 prompt + header data height self.header_height = 2 + 1 + 1 + row_height(field_labels) self.terminal_lines_left = self.terminal_height - self.header_height return self.pt def done(self): if self.quit: return if not self.paging or (self.terminal_lines_left < self.terminal_height - self.header_height): printer(self.get_string()) return PT_Builder(field_labels, fields, formatters, not paging) def parse_date(string_data): """Parses a date-like input string into a timezone aware Python datetime. """ if not isinstance(string_data, six.string_types): return string_data pattern = r'(\d{4}-\d{2}-\d{2}[T ])?\d{2}:\d{2}:\d{2}(\.\d{6})?Z?' def convert_date(matchobj): formats = ["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%SZ"] datestring = matchobj.group(0) if datestring: for format in formats: try: datetime.strptime(datestring, format) datestring += "+0000" parsed = parser.parse(datestring) converted = parsed.astimezone(dateutil.tz.tzlocal()) converted = datetime.strftime(converted, format) return converted except Exception: pass return datestring return re.sub(pattern, convert_date, string_data) def print_list(objs, fields, field_labels, formatters={}, sortby=0, reversesort=False, no_wrap_fields=[], printer=default_printer): # print_list() is the same as print_long_list() with paging turned off return print_long_list(objs, fields, field_labels, formatters=formatters, sortby=sortby, reversesort=reversesort, no_wrap_fields=no_wrap_fields, no_paging=True, printer=printer) def _build_row_from_object(fields, formatters, o): """ takes an object o and converts to an array of values compatible with the input for prettyTable.add_row(row) """ row = [] for field in fields: if field in formatters: data = parse_date(getattr(o, field, '')) setattr(o, field, data) data = formatters[field](o) row.append(data) else: data = parse_date(getattr(o, field, '')) row.append(data) return row def print_tuple_list(tuples, tuple_labels=[], formatters={}): pt = prettytable.PrettyTable(['Property', 'Value'], caching=False, print_empty=False) pt.align = 'l' if not tuple_labels: for t in tuples: if len(t) == 2: f, v = t v = parse_date(v) if f in formatters: v = formatters[f](v) pt.add_row([f, v]) else: for t, l in zip(tuples, tuple_labels): if len(t) == 2: f, v = t v = parse_date(v) if f in formatters: v = formatters[f](v) pt.add_row([l, v]) print pt.get_string() def str_height(text): if not text: return 1 lines = str(text).split("\n") height = len(lines) return height def row_height(texts): if not texts or len(texts) == 0: return 1 height = max(str_height(text) for text in texts) return height def print_long_list(objs, fields, field_labels, formatters={}, sortby=0, reversesort=False, no_wrap_fields=[], no_paging=False, printer=default_printer): formatters = wrapping_formatters.as_wrapping_formatters(objs, fields, field_labels, formatters, no_wrap_fields=no_wrap_fields) objs = _sort_for_list(objs, fields, formatters=formatters, sortby=sortby, reversesort=reversesort) pt = pt_builder(field_labels, fields, formatters, not no_paging, printer=printer) for o in objs: pt.add_row(o) pt.done() def print_dict(d, dict_property="Property", wrap=0): pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False, print_empty=False) pt.align = 'l' for k, v in sorted(d.iteritems()): v = parse_date(v) # convert dict to str to check length if isinstance(v, dict): v = str(v) if wrap > 0: v = textwrap.fill(six.text_type(v), wrap) # if value has a newline, add in multiple rows # e.g. fault with stacktrace if v and isinstance(v, six.string_types) and r'\n' in v: lines = v.strip().split(r'\n') col1 = k for line in lines: pt.add_row([col1, line]) col1 = '' else: pt.add_row([k, v]) print pt.get_string() def find_resource(manager, name_or_id): """Helper for the _find_* methods.""" # first try to get entity as integer id try: if isinstance(name_or_id, int) or name_or_id.isdigit(): return manager.get(int(name_or_id)) except exc.NotFound: pass # now try to get entity as uuid try: uuid.UUID(str(name_or_id)) return manager.get(name_or_id) except (ValueError, exc.NotFound): pass # finally try to find entity by name try: return manager.find(name=name_or_id) except exc.NotFound: msg = "No %s with a name or ID of '%s' exists." % \ (manager.resource_class.__name__.lower(), name_or_id) raise exc.CommandError(msg) def string_to_bool(arg): return arg.strip().lower() in ('t', 'true', 'yes', '1') def env(*vars, **kwargs): """Search for the first defined of possibly many env vars Returns the first environment variable defined in vars, or returns the default defined in kwargs. """ for v in vars: value = os.environ.get(v, None) if value: return value return kwargs.get('default', '') def import_versioned_module(version, submodule=None): module = 'cgtsclient.v%s' % version if submodule: module = '.'.join((module, submodule)) return importutils.import_module(module) def args_array_to_dict(kwargs, key_to_convert): values_to_convert = kwargs.get(key_to_convert) if values_to_convert: try: kwargs[key_to_convert] = dict(v.split("=", 1) for v in values_to_convert) except ValueError: raise exc.CommandError('%s must be a list of KEY=VALUE not "%s"' % (key_to_convert, values_to_convert)) return kwargs def args_array_to_patch(op, attributes): patch = [] for attr in attributes: # Sanitize if not attr.startswith('/'): attr = '/' + attr if op in ['add', 'replace']: try: path, value = attr.split("=", 1) patch.append({'op': op, 'path': path, 'value': value}) except ValueError: raise exc.CommandError('Attributes must be a list of ' 'PATH=VALUE not "%s"' % attr) elif op == "remove": # For remove only the key is needed patch.append({'op': op, 'path': attr}) else: raise exc.CommandError('Unknown PATCH operation: %s' % op) return patch def dict_to_patch(values, op='replace'): patch = [] for key, value in values.iteritems(): path = '/' + key patch.append({'op': op, 'path': path, 'value': value}) return patch def exit(msg=''): if msg: print >> sys.stderr, msg sys.exit(1) def objectify(func): """Mimic an object given a dictionary. Given a dictionary, create an object and make sure that each of its keys are accessible via attributes. Ignore everything if the given value is not a dictionary. :param func: A dictionary or another kind of object. :returns: Either the created object or the given value. >>> obj = {'old_key': 'old_value'} >>> oobj = objectify(obj) >>> oobj['new_key'] = 'new_value' >>> print oobj['old_key'], oobj['new_key'], oobj.old_key, oobj.new_key >>> @objectify ... def func(): ... return {'old_key': 'old_value'} >>> obj = func() >>> obj['new_key'] = 'new_value' >>> print obj['old_key'], obj['new_key'], obj.old_key, obj.new_key """ def create_object(value): if isinstance(value, dict): # Build a simple generic object. class Object(dict): def __setitem__(self, key, val): setattr(self, key, val) return super(Object, self).__setitem__(key, val) # Create that simple generic object. ret_obj = Object() # Assign the attributes given the dictionary keys. for key, val in value.iteritems(): ret_obj[key] = val setattr(ret_obj, key, val) return ret_obj else: return value # If func is a function, wrap around and act like a decorator. if hasattr(func, '__call__'): @wraps(func) def wrapper(*args, **kwargs): """Wrapper function for the decorator. :returns: The return value of the decorated function. """ value = func(*args, **kwargs) return create_object(value) return wrapper # Else just try to objectify the value given. else: return create_object(func) def is_uuid_like(val): """Returns validation of a value as a UUID. For our purposes, a UUID is canonical form string: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaa """ try: return str(uuid.UUID(val)) == val except (TypeError, ValueError, AttributeError): return False def get_terminal_size(): """Returns a tuple (x, y) representing the width(x) and the height(x) in characters of the terminal window. """ def ioctl_GWINSZ(fd): try: import fcntl import struct import termios cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) except Exception: return None if cr == (0, 0): return None if cr == (0, 0): return None return cr cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) if not cr: try: fd = os.open(os.ctermid(), os.O_RDONLY) cr = ioctl_GWINSZ(fd) os.close(fd) except Exception: pass if not cr: cr = (os.environ.get('LINES', 25), os.environ.get('COLUMNS', 80)) return int(cr[1]), int(cr[0]) def normalize_field_data(obj, fields): for f in fields: if hasattr(obj, f): data = getattr(obj, f, '') try: data = str(data) except UnicodeEncodeError: setattr(obj, f, data.encode('utf-8')) class WRPrettyTable(prettytable.PrettyTable): """A PrettyTable that allows word wrapping of its headers.""" def __init__(self, field_names=None, **kwargs): super(WRPrettyTable, self).__init__(field_names, **kwargs) def _stringify_header(self, options): """ This overridden version of _stringify_header can wrap its header data. It leverages the functionality in _stringify_row to perform this task. :returns string of header, including border text """ bits = [] if options["border"]: if options["hrules"] in (ALL, FRAME): bits.append(self._hrule) bits.append("\n") # For tables with no data or field names if not self._field_names: if options["vrules"] in (ALL, FRAME): bits.append(options["vertical_char"]) bits.append(options["vertical_char"]) else: bits.append(" ") bits.append(" ") header_row_data = [] for field in self._field_names: if options["fields"] and field not in options["fields"]: continue if self._header_style == "cap": fieldname = field.capitalize() elif self._header_style == "title": fieldname = field.title() elif self._header_style == "upper": fieldname = field.upper() elif self._header_style == "lower": fieldname = field.lower() else: fieldname = field header_row_data.append(fieldname) # output actual header row data, word wrap when necessary bits.append(self._stringify_row(header_row_data, options)) if options["border"] and options["hrules"] != NONE: bits.append("\n") bits.append(self._hrule) return "".join(bits) def extract_keypairs(args): attributes = {} for parms in args.attributes: for parm in parms: # Check that there is a '=' if parm.find('=') > -1: (key, value) = parm.split('=', 1) else: key = parm value = None attributes[key] = value return attributes def size_unit_conversion(size, step): """ This function converts size from a smaller unit (e.g. KiB) to a larger unit (e.g. GiB). :param size: Size value to convert from one unit to another :param step: Power of 2^10. e.g. From Byte to MiB is 2 steps. From MiB to GiB is 1 step. :returns: The return value is a float with 3 digits after the decimal point. """ return math.floor(float(size) / (1024 ** step) * 1000) / 1000.0 def _get_system_info(cc): """Gets the system mode and type""" if is_remote: system_info = cc.isystem.list()[0] return system_info.system_type, system_info.system_mode else: return tsc.system_type, tsc.system_mode