Skip to content

Commit

Permalink
More improvements for custodia-cli error reporting
Browse files Browse the repository at this point in the history
* Print command and name but not secret value
* Show host or unquoted path to socket
* Print inner exception for ConnectionError. It contains useful
  information, e.g. TLS / cert errors.

$ PYTHONPATH=$(pwd) python -m custodia.cli ls /
ERROR: Custodia command 'ls /' failed.
Failed to connect to Unix socket '/var/run/custodia/custodia.sock':
    ('Connection aborted.', error(2, 'No such file or directory'))

$ PYTHONPATH=$(pwd) python -m custodia.cli --server http://localhost ls /
ERROR: Custodia command 'ls /' failed.
Failed to connect to 'localhost' (http):
    HTTPConnectionPool(host='localhost', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0x7fcb9aea2e10>: Failed to establish a new connection: [Errno 111] Connection refused',))

$ PYTHONPATH=$(pwd) python3 -m custodia.cli --server https://wrong.host.badssl.com/ ls /
ERROR: Custodia command 'ls /' failed.
Failed to connect to 'wrong.host.badssl.com' (https):
    hostname 'wrong.host.badssl.com' doesn't match either of '*.badssl.com', 'badssl.com'

Closes: #131
Signed-off-by: Christian Heimes <[email protected]>
Reviewed-by: Raildo Mascena <[email protected]>
Closes: #144
  • Loading branch information
tiran authored and simo5 committed Mar 27, 2017
1 parent 2260706 commit baacf4f
Showing 1 changed file with 93 additions and 31 deletions.
124 changes: 93 additions & 31 deletions custodia/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,43 @@
# Copyright (C) 2016 Custodia Project Contributors - see LICENSE file
from __future__ import absolute_import, print_function

import argparse
import logging
import operator
import os
import sys
import traceback

try:
# pylint: disable=import-error
from urllib import quote as url_escape
except ImportError:
# pylint: disable=import-error,no-name-in-module
from urllib.parse import quote as url_escape

import pkg_resources


from requests.exceptions import ConnectionError
from requests.exceptions import HTTPError as RequestsHTTPError

import six

from custodia import log
from custodia.client import CustodiaSimpleClient
from custodia.compat import unquote, url_escape, urlparse

if six.PY2:
from StringIO import StringIO
else:
from io import StringIO

try:
from json import JSONDecodeError
except ImportError:
# Python <= 3.4 has no JSONDecodeError
JSONDecodeError = ValueError


log.warn_provisional(__name__)

# exit codes
E_HTTP_ERROR = 1
E_CONNECTION_ERROR = 2
E_JSON_ERROR = 3
E_OTHER = 100


main_parser = argparse.ArgumentParser(
prog='custodia-cli',
Expand Down Expand Up @@ -113,44 +126,56 @@ def handle_name_value(args):
parser_create_container.add_argument('name', type=str, help='key')
parser_create_container.set_defaults(
func=handle_name,
command='create_container')
command='create_container',
sub='mkdir',
)

parser_delete_container = subparsers.add_parser(
'rmdir',
help='Delete a container')
parser_delete_container.add_argument('name', type=str, help='key')
parser_delete_container.set_defaults(
func=handle_name,
command='delete_container')
command='delete_container',
sub='rmdir',
)

parser_list_container = subparsers.add_parser(
'ls', help='List content of a container')
parser_list_container.add_argument('name', type=str, help='key')
parser_list_container.set_defaults(
func=handle_name,
command='list_container')
command='list_container',
sub='ls',
)

parser_get_secret = subparsers.add_parser(
'get', help='Get secret')
parser_get_secret.add_argument('name', type=str, help='key')
parser_get_secret.set_defaults(
func=handle_name,
command='get_secret')
command='get_secret',
sub='get',
)

parser_set_secret = subparsers.add_parser(
'set', help='Set secret')
parser_set_secret.add_argument('name', type=str, help='key')
parser_set_secret.add_argument('value', type=str, help='value')
parser_set_secret.set_defaults(
command='set_secret',
func=handle_name_value)
func=handle_name_value,
sub='set'
)

parser_del_secret = subparsers.add_parser(
'del', help='Delete a secret')
parser_del_secret.add_argument('name', type=str, help='key')
parser_del_secret.set_defaults(
func=handle_name,
command='del_secret')
command='del_secret',
sub='del',
)


# plugins
Expand All @@ -176,7 +201,49 @@ def handle_plugins(args):
'plugins', help='List plugins')
parser_plugins.set_defaults(
func=handle_plugins,
command='plugins')
command='plugins',
sub='plugins',
name=None,
)


def error_message(args, exc):
out = StringIO()
parts = urlparse(args.server)

if args.debug:
traceback.print_exc(file=out)
out.write('\n')

out.write("ERROR: Custodia command '{args.sub} {args.name}' failed.\n")
if args.verbose:
out.write("Custodia server '{args.server}'.\n")

if isinstance(exc, RequestsHTTPError):
errcode = E_HTTP_ERROR
out.write("{exc.__class__.__name__}: {exc}\n")
elif isinstance(exc, ConnectionError):
errcode = E_CONNECTION_ERROR
if parts.scheme == 'http+unix':
out.write("Failed to connect to Unix socket '{unix_path}':\n")
else:
out.write("Failed to connect to '{parts.netloc}' "
"({parts.scheme}):\n")
# ConnectionError always contains an inner exception
out.write(" {exc.args[0]}\n")
elif isinstance(exc, JSONDecodeError):
errcode = E_JSON_ERROR
out.write("Server returned invalid JSON response:\n")
out.write(" {exc}\n")
else:
errcode = E_OTHER
out.write("{exc.__class__.__name__}: {exc}\n")

msg = out.getvalue()
if not msg.endswith('\n'):
msg += '\n'
return errcode, msg.format(args=args, exc=exc, parts=parts,
unix_path=unquote(parts.netloc))


def main():
Expand Down Expand Up @@ -206,23 +273,18 @@ def main():
if args.certfile:
args.client_conn.set_client_cert(args.certfile, args.keyfile)
args.client_conn.headers['CUSTODIA_CERT_AUTH'] = 'true'

try:
result = args.func(args)
except RequestsHTTPError as e:
return main_parser.exit(1, str(e))
except ConnectionError:
connection_error_msg = "Unable to connect to the server via " \
"{}".format(args.server)
return main_parser.exit(2, connection_error_msg)
except Exception as e: # pylint: disable=broad-except
if args.verbose:
traceback.print_exc(file=sys.stderr)
return main_parser.exit(100, str(e))
if result is not None:
if isinstance(result, list):
print('\n'.join(result))
else:
print(result)
except BaseException as e:
errcode, msg = error_message(args, e)
main_parser.exit(errcode, msg)
else:
if result is not None:
if isinstance(result, list):
print('\n'.join(result))
else:
print(result)


if __name__ == '__main__':
Expand Down

0 comments on commit baacf4f

Please sign in to comment.