Merge pull request #572 from bradmw/merge-fix

Fixed traceback when not run as root
This commit is contained in:
James Kasten
2015-07-02 09:20:38 -07:00
3 changed files with 107 additions and 56 deletions
+1
View File
@@ -5,6 +5,7 @@ build/
dist/
/venv/
/.tox/
letsencrypt.log
# coverage
.coverage
+67 -40
View File
@@ -2,11 +2,13 @@
# TODO: Sanity check all input. Be sure to avoid shell code etc...
import argparse
import atexit
import functools
import logging
import logging.handlers
import os
import sys
import time
import traceback
import configargparse
import zope.component
@@ -644,8 +646,71 @@ def _setup_logging(args):
logger.info("Saving debug log to %s", log_file_name)
def main2(cli_args, args, config, plugins):
"""Continued main script execution."""
def _handle_exception(exc_type, exc_value, trace, args):
"""Logs exceptions and reports them to the user.
Args is used to determine how to display exceptions to the user. In
general, if args.debug is True, then the full exception and traceback is
shown to the user, otherwise it is suppressed. If args itself is None,
then the traceback and exception is attempted to be written to a logfile.
If this is successful, the traceback is suppressed, otherwise it is shown
to the user. sys.exit is always called with a nonzero status.
"""
logger.debug(
"Exiting abnormally:\n%s",
"".join(traceback.format_exception(exc_type, exc_value, trace)))
if issubclass(exc_type, Exception) and (args is None or not args.debug):
if args is None:
logfile = "letsencrypt.log"
try:
with open(logfile, "w") as logfd:
traceback.print_exception(
exc_type, exc_value, trace, file=logfd)
except: # pylint: disable=bare-except
sys.exit("".join(
traceback.format_exception(exc_type, exc_value, trace)))
if issubclass(exc_type, errors.Error):
sys.exit(exc_value)
elif args is None:
sys.exit(
"An unexpected error occurred. Please see the logfile '{0}' "
"for more details.".format(logfile))
else:
sys.exit(
"An unexpected error occurred. Please see the logfiles in {0} "
"for more details.".format(args.logs_dir))
else:
sys.exit("".join(
traceback.format_exception(exc_type, exc_value, trace)))
def main(cli_args=sys.argv[1:]):
"""Command line argument parsing and main script execution."""
sys.excepthook = functools.partial(_handle_exception, args=None)
# note: arg parser internally handles --help (and exits afterwards)
plugins = plugins_disco.PluginsRegistry.find_all()
args = create_parser(plugins, cli_args).parse_args(cli_args)
config = configuration.NamespaceConfig(args)
# Setup logging ASAP, otherwise "No handlers could be found for
# logger ..." TODO: this should be done before plugins discovery
for directory in config.config_dir, config.work_dir:
le_util.make_or_verify_dir(
directory, constants.CONFIG_DIRS_MODE, os.geteuid())
# TODO: logs might contain sensitive data such as contents of the
# private key! #525
le_util.make_or_verify_dir(args.logs_dir, 0o700, os.geteuid())
_setup_logging(args)
# do not log `args`, as it contains sensitive data (e.g. revoke --key)!
logger.debug("Arguments: %r", cli_args)
logger.debug("Discovered plugins: %r", plugins)
sys.excepthook = functools.partial(_handle_exception, args=args)
# Displayer
if args.text_mode:
@@ -654,10 +719,6 @@ def main2(cli_args, args, config, plugins):
displayer = display_util.NcursesDisplay()
zope.component.provideUtility(displayer)
# do not log `args`, as it contains sensitive data (e.g. revoke --key)!
logger.debug("Arguments: %r", cli_args)
logger.debug("Discovered plugins: %r", plugins)
# Reporter
report = reporter.Reporter()
zope.component.provideUtility(report)
@@ -677,39 +738,5 @@ def main2(cli_args, args, config, plugins):
return args.func(args, config, plugins)
def main(cli_args=sys.argv[1:]):
"""Command line argument parsing and main script execution."""
# note: arg parser internally handles --help (and exits afterwards)
plugins = plugins_disco.PluginsRegistry.find_all()
args = create_parser(plugins, cli_args).parse_args(cli_args)
config = configuration.NamespaceConfig(args)
# Setup logging ASAP, otherwise "No handlers could be found for
# logger ..." TODO: this should be done before plugins discovery
for directory in config.config_dir, config.work_dir:
le_util.make_or_verify_dir(
directory, constants.CONFIG_DIRS_MODE, os.geteuid())
# TODO: logs might contain sensitive data such as contents of the
# private key! #525
le_util.make_or_verify_dir(args.logs_dir, 0o700, os.geteuid())
_setup_logging(args)
def handle_exception_common():
"""Logs the exception and reraises it if in debug mode."""
logger.debug("Exiting abnormally", exc_info=True)
if args.debug:
raise
try:
return main2(cli_args, args, config, plugins)
except errors.Error as error:
handle_exception_common()
return error
except Exception: # pylint: disable=broad-except
handle_exception_common()
return ("An unexpected error occured. Please see the logfiles in {0} "
"for more details.".format(args.logs_dir))
if __name__ == "__main__":
sys.exit(main()) # pragma: no cover
+39 -16
View File
@@ -2,11 +2,14 @@
import itertools
import os
import shutil
import traceback
import tempfile
import unittest
import mock
from letsencrypt import errors
class CLITest(unittest.TestCase):
"""Tests for different commands."""
@@ -20,16 +23,13 @@ class CLITest(unittest.TestCase):
def tearDown(self):
shutil.rmtree(self.tmp_dir)
def _call(self, args, client_mock_attrs=None):
def _call(self, args):
from letsencrypt import cli
args = ['--text', '--config-dir', self.config_dir,
'--work-dir', self.work_dir, '--logs-dir', self.logs_dir] + args
with mock.patch('letsencrypt.cli.sys.stdout') as stdout:
with mock.patch('letsencrypt.cli.sys.stderr') as stderr:
with mock.patch('letsencrypt.cli.client') as client:
if client_mock_attrs:
# pylint: disable=star-args
client.configure_mock(**client_mock_attrs)
ret = cli.main(args)
return ret, stdout, stderr, client
@@ -59,19 +59,42 @@ class CLITest(unittest.TestCase):
for r in xrange(len(flags)))):
self._call(['plugins',] + list(args))
def test_exceptions(self):
from letsencrypt import errors
cmd_arg = ['config_changes']
error = [errors.Error('problem')]
attrs = {'view_config_changes.side_effect' : error}
self.assertRaises(
errors.Error, self._call, ['--debug'] + cmd_arg, attrs)
self._call(cmd_arg, attrs)
@mock.patch("letsencrypt.cli.sys")
def test_handle_exception(self, mock_sys):
# pylint: disable=protected-access
from letsencrypt import cli
mock_open = mock.mock_open()
with mock.patch("letsencrypt.cli.open", mock_open, create=True):
exception = Exception("detail")
cli._handle_exception(
Exception, exc_value=exception, trace=None, args=None)
mock_open().write.assert_called_once_with("".join(
traceback.format_exception_only(Exception, exception)))
error_msg = mock_sys.exit.call_args_list[0][0][0]
self.assertTrue("unexpected error" in error_msg)
with mock.patch("letsencrypt.cli.open", mock_open, create=True):
mock_open.side_effect = [KeyboardInterrupt]
error = errors.Error("detail")
cli._handle_exception(
errors.Error, exc_value=error, trace=None, args=None)
# assert_any_call used because sys.exit doesn't exit in cli.py
mock_sys.exit.assert_any_call("".join(
traceback.format_exception_only(errors.Error, error)))
args = mock.MagicMock(debug=False)
cli._handle_exception(
Exception, exc_value=Exception("detail"), trace=None, args=args)
error_msg = mock_sys.exit.call_args_list[-1][0][0]
self.assertTrue("unexpected error" in error_msg)
interrupt = KeyboardInterrupt("detail")
cli._handle_exception(
KeyboardInterrupt, exc_value=interrupt, trace=None, args=None)
mock_sys.exit.assert_called_with("".join(
traceback.format_exception_only(KeyboardInterrupt, interrupt)))
attrs['view_config_changes.side_effect'] = [ValueError]
self.assertRaises(
ValueError, self._call, ['--debug'] + cmd_arg, attrs)
self._call(cmd_arg, attrs)
if __name__ == '__main__':
unittest.main() # pragma: no cover