ebcli/display/screen.py (406 lines of code) (raw):

# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.s # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of # the License is located at # # http://aws.amazon.com/apache2.0/ # # or in the "license" file accompanying this file. This file 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. import time import sys from decimal import InvalidOperation from ebcli.objects.platform import PlatformVersion from ebcli.display import term import errno from datetime import timedelta, datetime from copy import copy from collections import OrderedDict from cement.utils.misc import minimal_logger from cement.core.exc import CaughtSignal from botocore.compat import six from ebcli.objects.solutionstack import SolutionStack from ebcli.core import io, fileoperations from ebcli.lib import utils, ec2 from ebcli.objects.exceptions import ServiceError, ValidationError, NotFoundError LOG = minimal_logger(__name__) class Screen(object): def __init__(self): self.term = None self.tables = [] self.vertical_offset = 0 self.horizontal_offset = 0 self.max_columns = 5 self.help_table = None self.mono = False self.data = None self.sort_index = None self.last_table = 'split' self.sort_reversed = False self.empty_row = 4 self.refresh = False self.env_data = None self.frozen = False def add_table(self, table): table.screen = self self.tables.append(table) def add_help_table(self, table): table.screen = self self.tables.append(table) self.help_table = table def start_screen(self, poller, env_data, refresh, mono=False, default_table='split'): self.mono = mono self.env_data = env_data self.refresh = refresh term.hide_cursor() self.turn_on_table(default_table) # Timeout at 2 hours of inactivity self.idle_time = datetime.now() timediff = timedelta(hours=2) while (datetime.now() - self.idle_time) < timediff: try: self.get_data(poller) if not self.data: return self.draw('instances') term.reset_terminal() if not refresh: return should_exit = self.handle_input() if should_exit: return except IOError as e: if e.errno == errno.EINTR: # Sometimes thrown while sleeping continue else: raise def get_data(self, poller): if not self.frozen: self.data = poller.get_fresh_data() def draw(self, key): """Formats and draws banner and tables in screen. :param key is 'instances' for health tables and 'app_versions' for versions table. """ self.data = self.sort_data(self.data) n = term.height() - 1 n = self.draw_banner(n, self.data) term.echo_line() n -= 2 # Account for empty lines before and after tables visible_count = 0 for table in self.tables: if table.visible: visible_count += 1 n -= table.header_size if visible_count != 0: visible_rows = n // visible_count if visible_rows > 0: # Dont show tables if no visible rows. self.max_columns = max([len(t.columns) for t in self.tables if t.visible]) - 1 for table in self.tables: table.draw(visible_rows, self.data[key]) self.show_help_line() def handle_input(self): t = term.get_terminal() with t.cbreak(): # Get input val = t.inkey(timeout=.5) if val: self.idle_time = datetime.now() char = str(val).upper() LOG.debug('Got val: {0}, {1}, {2}.' .format(val, val.name, val.code)) # io.echo('Got val: {},, {}, {}.'.format(val, val.name, val.code)) # time.sleep(3) if char == 'Q': if self.help_table.visible: self.turn_on_table(self.last_table) else: return True # Exit command elif char == 'X': self.replace_instance_view() elif char == 'B': self.reboot_instance_view() elif char == '1': self.turn_on_table('split') elif char == '2': self.turn_on_table('health') elif char == '3': self.turn_on_table('requests') elif char == '4': self.turn_on_table('cpu') elif char == '5': self.turn_on_table('deployments') elif char == 'H': self.show_help() elif char == 'F': self.toggle_freeze() elif char == 'P': self.snapshot_file_view() elif char == 'Z': self.mono = not self.mono elif char == '>': self.move_sort_column_right() elif char == '<': self.move_sort_column_left() elif char == '-': self.sort_reversed = True elif char == '+': self.sort_reversed = False # Scrolling elif val.name == 'KEY_DOWN': # Down arrow self.scroll_down() elif val.name == 'KEY_UP': # Up arrow self.scroll_down(reverse=True) elif val.name == 'KEY_LEFT': # Left arrow self.scroll_over(reverse=True) elif val.name == 'KEY_RIGHT': # Right arrow self.scroll_over() elif val.name == 'KEY_END': # End for table in self.tables: table.scroll_to_end() elif val.name == 'KEY_HOME': # Home for table in self.tables: table.scroll_to_beginning() # If in help window (not main screen) these keys exit elif self.help_table.visible and val.code == 361: # ESC KEY self.turn_on_table(self.last_table) def turn_on_table(self, key): # Activate correct tables for table in self.tables: if key in {'split', table.name}: table.visible = True else: table.visible = False # Activate Help table if key in {'health_help'}: self.help_table.visible = True else: self.help_table.visible = False # Reset state if change in table if key != self.last_table: self.sort_index = None self.last_table = key # Reset scroll self.horizontal_offset = 0 for table in self.tables: table.vertical_offset = 0 def snapshot_file_view(self): data_repr = self.data current_time = datetime.now().strftime("%y%m%d-%H%M%S") filename = 'health-snapshot-' + current_time + '.json' filelocation = fileoperations.get_eb_file_full_location(filename) fileoperations.write_json_dict(data_repr, filelocation) t = term.get_terminal() with t.location(y=self.empty_row, x=2): io.echo(io.bold('Snapshot file saved at: .elasticbeanstalk/' + filename), end=' ') sys.stdout.flush() time.sleep(4) def prompt_and_action(self, prompt_string, action): id = '' t = term.get_terminal() io.echo(t.normal_cursor(), end='') # Move cursor to specified empty row with t.location(y=self.empty_row, x=2), t.cbreak(): io.echo(io.bold(prompt_string), end=' ') sys.stdout.flush() val = None while not val or val.name not in {'KEY_ESCAPE', 'KEY_ENTER'}: val = t.inkey(timeout=.5) if val is None: continue elif val.is_sequence is False: id += str(val) sys.stdout.write(str(val)) sys.stdout.flush() elif val.name == 'KEY_DELETE': # Backspace if len(id) > 0: id = id[:-1] sys.stdout.write(str(t.move_left) + t.clear_eol) sys.stdout.flush() term.hide_cursor() if val.name == 'KEY_ESCAPE' or not id: return False with t.location(y=self.empty_row, x=2): sys.stdout.flush() io.echo(t.clear_eol(), end='') try: should_exit_display = action(id) if should_exit_display is None: should_exit_display = True return should_exit_display except (ServiceError, ValidationError, NotFoundError) as e: # Error messages that should be shown directly to user io.log_error(e.message) time.sleep(4) # Leave screen stable for a little return False except (IndexError, InvalidOperation, ValueError) as e: if self.poller.all_app_versions: # Error thrown in versions table max_input = len(self.poller.all_app_versions) io.log_error("Enter a number between 1 and " + str(max_input) + ".") else: io.log_error(e) time.sleep(4) return False except CaughtSignal as sig: if sig.signum == 2: LOG.debug("Caught SIGINT and exiting gracefully from action") return True except Exception as e: # Should never get thrown LOG.debug( "Exception thrown: {0},{1}. Something strange happened " "and the request could not be completed.".format( type(e), str(e) ) ) io.log_error("Something strange happened and the request could not be completed.") time.sleep(4) return False def reboot_instance_view(self): self.prompt_and_action('instance-ID to reboot:', ec2.reboot_instance) def replace_instance_view(self): self.prompt_and_action( 'instance-ID to replace:', ec2.terminate_instance ) def toggle_freeze(self): self.frozen = not self.frozen def move_sort_column_right(self): tables = [t for t in self.tables if t.visible] if self.sort_index: table_name, column_index = self.sort_index table_index = _get_table_index(tables, table_name) if len(tables[table_index].columns) > column_index + 1: self.sort_index = (table_name, column_index + 1) elif len(tables) > table_index + 1: self.sort_index = (tables[table_index + 1].name, 0) else: self.sort_index = (tables[0].name, 0) else: self.sort_index = (tables[0].name, 0) def move_sort_column_left(self): tables = [t for t in self.tables if t.visible] if self.sort_index: table_name, column_index = self.sort_index table_index = _get_table_index(tables, table_name) if column_index - 1 >= 0: self.sort_index = (table_name, column_index - 1) elif table_index - 1 >= 0: new_sort_table = tables[table_index - 1] new_sort_column = len(new_sort_table.columns) - 1 self.sort_index = (new_sort_table.name, new_sort_column) else: new_sort_table = tables[len(tables) - 1] new_sort_column = len(new_sort_table.columns) - 1 self.sort_index = (new_sort_table.name, new_sort_column) else: self.sort_index = (tables[0].name, 0) def draw_banner_first_line(self, lines, data): status = data.get('HealthStatus', 'Unknown') refresh_time = data.get('RefreshedAt', None) if refresh_time is None: timestamp = '-' countdown = ' ( now )' else: timestamp = utils.get_local_time_as_string(refresh_time) delta = utils.get_delta_from_now_and_datetime(refresh_time) diff = 11 - delta.seconds if not self.refresh: countdown = '' elif self.frozen: countdown = ' (frozen +{})'.format(delta.seconds) elif diff < 0: countdown = ' ( now )' else: countdown = " ({} secs)".format(diff) env_name = data.get('EnvironmentName') pad_length = ( term.width() - len(env_name) - len(timestamp) - len(countdown) - 1 ) if lines > 2: banner = io.bold(' {env_name}{status}{time}{cd} ') \ .format(env_name=env_name, status=status.center(pad_length), time=timestamp, cd=countdown, ) if not self.mono: banner = io.on_color(data.get('Color', 'Grey'), banner) term.echo_line(banner) lines -= 1 return lines def draw_banner(self, lines, data): data = data['environment'] lines = self.draw_banner_first_line(lines, data) return self.draw_banner_info_lines(lines, data) def draw_banner_info_lines(self, lines, data): if lines > 2: tier_type = self.env_data['Tier']['Name'] tier = '{}'.format(tier_type) try: platform_arn = self.env_data['PlatformArn'] platform_version = PlatformVersion(platform_arn) platform = ' {}/{}'.format( platform_version.platform_shorthand, platform_version.platform_version ) except KeyError: solutionstack = SolutionStack(self.env_data['SolutionStackName']) platform = ' {}'.format(solutionstack.platform_shorthand) term.echo_line('{tier}{pad}{platform} '.format( tier=tier, platform=platform, pad=' '*(term.width() - len(tier) - len(platform)) )) lines -= 1 if lines > 3: # Get instance health count instance_counts = OrderedDict([ ('total', data.get('Total', 0)), ('ok', data.get('Ok', 0)), ('warning', data.get('Warning', 0)), ('degraded', data.get('Degraded', 0)), ('severe', data.get('Severe', 0)), ('info', data.get('Info', 0)), ('pending', data.get('Pending', 0)), ('unknown', data.get('Unknown', 0) + data.get('NoData', 0)), ]) column_size = max(len(k) for k in instance_counts) + 1 term.echo_line( ''.join((s.center(column_size) for s in instance_counts)) ) term.echo_line( ''.join( ( io.bold((str(v).center(column_size))) for k, v in six.iteritems(instance_counts) ) ) ) lines -= 2 return lines def scroll_down(self, reverse=False): visible_tables = [t for t in self.tables if t.visible] if len(visible_tables) < 1: return # assumes the first table will always be the largest if not visible_tables[0].data: return # Scroll first table scrolled = visible_tables[0].scroll_down(reverse=reverse) if scrolled: for i in range(1, len(visible_tables)): assert len(visible_tables[0].data) >= len(visible_tables[i].data),'First table should be the largest' visible_tables[i].scroll_to_id(scrolled, reverse=reverse) def scroll_over(self, reverse=False): if reverse and self.horizontal_offset > 0: self.horizontal_offset -= 1 elif not reverse: # remove bounds check on upper limit to allow for text scrolling in causes self.horizontal_offset += 1 def show_help(self): self.turn_on_table('health_help') def show_help_line(self): if self.help_table.visible: text = '(press Q or ESC to return)' elif self.refresh: text = u' (Commands: {h}elp,{q}uit, {down} {up} {left} {right})'\ .format(h=io.bold('H'), q=io.bold('Q'), down=term.DOWN_ARROW, up=term.UP_ARROW, left=term.LEFT_ARROW, right=term.RIGHT_ARROW) else: text = u'' term.echo_line(text) term.echo_line(term.clear_eos()) def sort_data(self, data): new_data = copy(data) if self.sort_index: table_name, column_index = self.sort_index sort_table = next((t for t in self.tables if t.name == table_name)) sort_key = sort_table.columns[column_index].sort_key new_data['instances'].sort(key=lambda x: x.get(sort_key, '-'), reverse=self.sort_reversed) return new_data def _get_table_index(tables, table_name): for i, table in enumerate(tables): if table.name == table_name: return i return 0