Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add history commands show and list #2993

Merged
merged 1 commit into from
Nov 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/next-release/feature-clihistory-23802.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "``cli_history``",
"description": "Setting the value of ``cli_history`` to ``enabled`` in the shared config file enables the CLI to keep history of all commands ran."
}
5 changes: 5 additions & 0 deletions .changes/next-release/feature-historylist-24801.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "``history list``",
"description": "Lists all of the commands that have been run via the stored history of the CLI."
}
5 changes: 5 additions & 0 deletions .changes/next-release/feature-historyshow-42971.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "``history show``",
"description": "Shows the important events related to running a command that was recorded in the CLI history."
}
17 changes: 13 additions & 4 deletions awscli/clidriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from botocore.compat import copy_kwargs, OrderedDict
from botocore.exceptions import NoCredentialsError
from botocore.exceptions import NoRegionError
from botocore.history import get_global_history_recorder

from awscli import EnvironmentVariables, __version__
from awscli.compat import get_stderr_text_writer
Expand Down Expand Up @@ -49,11 +50,14 @@
LOG = logging.getLogger('awscli.clidriver')
LOG_FORMAT = (
'%(asctime)s - %(threadName)s - %(name)s - %(levelname)s - %(message)s')
HISTORY_RECORDER = get_global_history_recorder()


def main():
driver = create_clidriver()
return driver.main()
rc = driver.main()
HISTORY_RECORDER.record('CLI_RC', rc, 'CLI')
return rc


def create_clidriver():
Expand Down Expand Up @@ -196,7 +200,10 @@ def main(self, args=None):
# general exception handling logic as calling into the
# command table. This is why it's in the try/except clause.
self._handle_top_level_args(parsed_args)
self._emit_session_event()
self._emit_session_event(parsed_args)
HISTORY_RECORDER.record(
'CLI_VERSION', self.session.user_agent(), 'CLI')
HISTORY_RECORDER.record('CLI_ARGUMENTS', args, 'CLI')
return command_table[parsed_args.command](remaining, parsed_args)
except UnknownArgumentError as e:
sys.stderr.write("usage: %s\n" % USAGE)
Expand Down Expand Up @@ -228,13 +235,15 @@ def main(self, args=None):
err.write("\n")
return 255

def _emit_session_event(self):
def _emit_session_event(self, parsed_args):
# This event is guaranteed to run after the session has been
# initialized and a profile has been set. This was previously
# problematic because if something in CLIDriver caused the
# session components to be reset (such as session.profile = foo)
# then all the prior registered components would be removed.
self.session.emit('session-initialized', session=self.session)
self.session.emit(
'session-initialized', session=self.session,
parsed_args=parsed_args)

def _show_error(self, msg):
LOG.debug(msg, exc_info=True)
Expand Down
49 changes: 47 additions & 2 deletions awscli/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import sys
import shlex
import os
import platform
import zipfile

from botocore.compat import six
Expand All @@ -25,7 +27,9 @@
queue = six.moves.queue
shlex_quote = six.moves.shlex_quote
StringIO = six.StringIO
BytesIO = six.BytesIO
urlopen = six.moves.urllib.request.urlopen
binary_type = six.binary_type

# Most, but not all, python installations will have zlib. This is required to
# compress any files we send via a push. If we can't compress, we can still
Expand All @@ -37,6 +41,21 @@
ZIP_COMPRESSION_MODE = zipfile.ZIP_STORED


try:
import sqlite3
except ImportError:
sqlite3 = None


is_windows = sys.platform == 'win32'


if is_windows:
default_pager = 'more'
else:
default_pager = 'less -R'


class NonTranslatedStdout(object):
""" This context manager sets the line-end translation mode for stdout.

Expand Down Expand Up @@ -74,6 +93,9 @@ def ensure_text_type(s):

binary_stdin = sys.stdin.buffer

def get_binary_stdout():
return sys.stdout.buffer

def _get_text_writer(stream, errors):
return stream

Expand Down Expand Up @@ -118,6 +140,9 @@ def bytes_print(statement, stdout=None):

binary_stdin = sys.stdin

def get_binary_stdout():
return sys.stdout

def _get_text_writer(stream, errors):
# In python3, all the sys.stdout/sys.stderr streams are in text
# mode. This means they expect unicode, and will encode the
Expand Down Expand Up @@ -199,7 +224,7 @@ def compat_shell_quote(s, platform=None):
def _windows_shell_quote(s):
"""Return a Windows shell-escaped version of the string *s*

Windows has potentially bizarre rules depending on where you look. When
Windows has potentially bizarre rules depending on where you look. When
spawning a process via the Windows C runtime the rules are as follows:

https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments
Expand All @@ -208,7 +233,7 @@ def _windows_shell_quote(s):

* Only space and tab are valid delimiters
* Double quotes are the only valid quotes
* Backslash is interpreted literally unless it is part of a chain that
* Backslash is interpreted literally unless it is part of a chain that
leads up to a double quote. Then the backslashes escape the backslashes,
and if there is an odd number the final backslash escapes the quote.

Expand Down Expand Up @@ -257,3 +282,23 @@ def _windows_shell_quote(s):
# quoted.
return '"%s"' % new_s
return new_s


def get_popen_kwargs_for_pager_cmd(pager_cmd=None):
"""Returns the default pager to use dependent on platform

:rtype: str
:returns: A string represent the paging command to run based on the
platform being used.
"""
popen_kwargs = {}
if pager_cmd is None:
pager_cmd = default_pager
# Similar to what we do with the help command, we need to specify
# shell as True to make it work in the pager for Windows
if is_windows:
popen_kwargs = {'shell': True}
else:
pager_cmd = shlex.split(pager_cmd)
popen_kwargs['args'] = pager_cmd
return popen_kwargs
107 changes: 107 additions & 0 deletions awscli/customizations/history/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Copyright 2017 Amazon.com, Inc. or its affiliates. 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. 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 os
import sys
import logging

from botocore.history import get_global_history_recorder
from botocore.exceptions import ProfileNotFound

from awscli.compat import sqlite3
from awscli.customizations.commands import BasicCommand
from awscli.customizations.history.constants import HISTORY_FILENAME_ENV_VAR
from awscli.customizations.history.constants import DEFAULT_HISTORY_FILENAME
from awscli.customizations.history.db import DatabaseConnection
from awscli.customizations.history.db import DatabaseRecordWriter
from awscli.customizations.history.db import RecordBuilder
from awscli.customizations.history.db import DatabaseHistoryHandler
from awscli.customizations.history.show import ShowCommand
from awscli.customizations.history.list import ListCommand


LOG = logging.getLogger(__name__)
HISTORY_RECORDER = get_global_history_recorder()


def register_history_mode(event_handlers):
event_handlers.register(
'session-initialized', attach_history_handler)


def register_history_commands(event_handlers):
event_handlers.register(
"building-command-table.main", add_history_commands)


def attach_history_handler(session, parsed_args, **kwargs):
if _should_enable_cli_history(session, parsed_args):
LOG.debug('Enabling CLI history')

history_filename = os.environ.get(
HISTORY_FILENAME_ENV_VAR, DEFAULT_HISTORY_FILENAME)
if not os.path.isdir(os.path.dirname(history_filename)):
os.makedirs(os.path.dirname(history_filename))

connection = DatabaseConnection(history_filename)
writer = DatabaseRecordWriter(connection)
record_builder = RecordBuilder()
db_handler = DatabaseHistoryHandler(writer, record_builder)

HISTORY_RECORDER.add_handler(db_handler)
HISTORY_RECORDER.enable()


def _should_enable_cli_history(session, parsed_args):
if parsed_args.command == 'history':
return False
try:
scoped_config = session.get_scoped_config()
except ProfileNotFound:
# If the profile does not exist, cli history is definitely not
# enabled, but don't let the error get propogated as commands down
# the road may handle this such as the configure set command with
# a --profile flag set.
return False
has_history_enabled = scoped_config.get('cli_history') == 'enabled'
if has_history_enabled and sqlite3 is None:
if has_history_enabled:
sys.stderr.write(
'cli_history is enabled but sqlite3 is unavailable. '
'Unable to collect CLI history.\n'
)
return False
return has_history_enabled


def add_history_commands(command_table, session, **kwargs):
command_table['history'] = HistoryCommand(session)


class HistoryCommand(BasicCommand):
NAME = 'history'
DESCRIPTION = (
'Commands to interact with the history of AWS CLI commands ran '
'over time. To record the history of AWS CLI commands set '
'``cli_history`` to ``enabled`` in the ``~/.aws/config`` file. '
'This can be done by running:\n\n'
'``$ aws configure set cli_history enabled``'
)
SUBCOMMANDS = [
{'name': 'show', 'command_class': ShowCommand},
{'name': 'list', 'command_class': ListCommand}
]

def _run_main(self, parsed_args, parsed_globals):
if parsed_args.subcommand is None:
raise ValueError("usage: aws [options] <command> <subcommand> "
"[parameters]\naws: error: too few arguments")
63 changes: 63 additions & 0 deletions awscli/customizations/history/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright 2017 Amazon.com, Inc. or its affiliates. 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. 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 os

from awscli.compat import is_windows
from awscli.utils import is_a_tty
from awscli.utils import OutputStreamFactory

from awscli.customizations.commands import BasicCommand
from awscli.customizations.history.db import DatabaseConnection
from awscli.customizations.history.constants import HISTORY_FILENAME_ENV_VAR
from awscli.customizations.history.constants import DEFAULT_HISTORY_FILENAME
from awscli.customizations.history.db import DatabaseRecordReader


class HistorySubcommand(BasicCommand):
def __init__(self, session, db_reader=None, output_stream_factory=None):
super(HistorySubcommand, self).__init__(session)
self._db_reader = db_reader
self._output_stream_factory = output_stream_factory
if output_stream_factory is None:
self._output_stream_factory = OutputStreamFactory()

def _connect_to_history_db(self):
if self._db_reader is None:
connection = DatabaseConnection(self._get_history_db_filename())
self._db_reader = DatabaseRecordReader(connection)

def _close_history_db(self):
self._db_reader.close()

def _get_history_db_filename(self):
filename = os.environ.get(
HISTORY_FILENAME_ENV_VAR, DEFAULT_HISTORY_FILENAME)
if not os.path.exists(filename):
raise RuntimeError(
'Could not locate history. Make sure cli_history is set to '
'enabled in the ~/.aws/config file'
)
return filename

def _should_use_color(self, parsed_globals):
if parsed_globals.color == 'on':
return True
elif parsed_globals.color == 'off':
return False
return is_a_tty() and not is_windows

def _get_output_stream(self, preferred_pager=None):
if is_a_tty():
return self._output_stream_factory.get_pager_stream(
preferred_pager)
return self._output_stream_factory.get_stdout_stream()
18 changes: 18 additions & 0 deletions awscli/customizations/history/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2017 Amazon.com, Inc. or its affiliates. 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. 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 os


HISTORY_FILENAME_ENV_VAR = 'AWS_CLI_HISTORY_FILE'
DEFAULT_HISTORY_FILENAME = os.path.expanduser(
os.path.join('~', '.aws', 'cli', 'history', 'history.db'))
Loading