Merge lp:~gz/juju-ci-tools/utility_logged_exception into lp:juju-ci-tools

Proposed by Martin Packman
Status: Merged
Approved by: Martin Packman
Approved revision: 1737
Merged at revision: 1736
Proposed branch: lp:~gz/juju-ci-tools/utility_logged_exception
Merge into: lp:juju-ci-tools
Diff against target: 171 lines (+81/-20)
3 files modified
deploy_stack.py (+4/-20)
tests/test_utility.py (+49/-0)
utility.py (+28/-0)
To merge this branch: bzr merge lp:~gz/juju-ci-tools/utility_logged_exception
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+311197@code.launchpad.net

Commit message

Add logged_exception helper to utility factored out from deploy_stack

Description of the change

Pulls the LoggedException code from deploy_stack to a more reusable location.

There is one relevent functional change here, the call site in BoostrapManager.runtime_context now does not wrap SystemExit or KeyboardInterrupt. This feels correct to me, but I am perhaps missing something. We may want specific KeyboardInterrupt handling given SIGINT is our top level timeout method.

Not done in this branch, switching deploy_stack to using a named logger rather than logging directly, and using the new functions in other tests.

To post a comment you must log in.
1737. By Martin Packman

Also wrap KeyboardInterrupt as SIGINT is used for timeout

Revision history for this message
Aaron Bentley (abentley) wrote :

Thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'deploy_stack.py'
--- deploy_stack.py 2016-11-14 22:18:53 +0000
+++ deploy_stack.py 2016-11-17 18:55:17 +0000
@@ -65,6 +65,7 @@
65 configure_logging,65 configure_logging,
66 ensure_deleted,66 ensure_deleted,
67 ensure_dir,67 ensure_dir,
68 logged_exception,
68 LoggedException,69 LoggedException,
69 PortTimeoutError,70 PortTimeoutError,
70 print_now,71 print_now,
@@ -753,16 +754,6 @@
753 self.controller_strategy.tear_down()754 self.controller_strategy.tear_down()
754 self.has_controller = False755 self.has_controller = False
755756
756 def _log_and_wrap_exception(self, exc):
757 logging.exception(exc)
758 stdout = getattr(exc, 'output', None)
759 stderr = getattr(exc, 'stderr', None)
760 if stdout or stderr:
761 logging.info(
762 'Output from exception:\nstdout:\n%s\nstderr:\n%s',
763 stdout, stderr)
764 return LoggedException(exc)
765
766 @contextmanager757 @contextmanager
767 def bootstrap_context(self, machines, omit_config=None):758 def bootstrap_context(self, machines, omit_config=None):
768 """Context for bootstrapping a state server."""759 """Context for bootstrapping a state server."""
@@ -845,13 +836,11 @@
845 Tear down. (self.keep_env is ignored.)836 Tear down. (self.keep_env is ignored.)
846 """837 """
847 try:838 try:
848 try:
849 yield
850 # If an exception is raised that indicates an error, log it839 # If an exception is raised that indicates an error, log it
851 # before tearing down so that the error is closely tied to840 # before tearing down so that the error is closely tied to
852 # the failed operation.841 # the failed operation.
853 except Exception as e:842 with logged_exception(logging):
854 raise self._log_and_wrap_exception(e)843 yield
855 except:844 except:
856 # If run from a windows machine may not have ssh to get845 # If run from a windows machine may not have ssh to get
857 # logs846 # logs
@@ -873,18 +862,13 @@
873 control is yielded.862 control is yielded.
874 """863 """
875 try:864 try:
876 try:865 with logged_exception(logging):
877 if len(self.known_hosts) == 0:866 if len(self.known_hosts) == 0:
878 self.known_hosts.update(867 self.known_hosts.update(
879 self.controller_strategy.get_hosts())868 self.controller_strategy.get_hosts())
880 if addable_machines is not None:869 if addable_machines is not None:
881 self.client.add_ssh_machines(addable_machines)870 self.client.add_ssh_machines(addable_machines)
882 yield871 yield
883 # avoid logging GeneratorExit
884 except GeneratorExit:
885 raise
886 except BaseException as e:
887 raise self._log_and_wrap_exception(e)
888 except:872 except:
889 if self.has_controller:873 if self.has_controller:
890 safe_print_status(self.client)874 safe_print_status(self.client)
891875
=== modified file 'tests/test_utility.py'
--- tests/test_utility.py 2016-11-08 15:17:26 +0000
+++ tests/test_utility.py 2016-11-17 18:55:17 +0000
@@ -18,6 +18,7 @@
1818
19from mock import (19from mock import (
20 call,20 call,
21 Mock,
21 patch,22 patch,
22 )23 )
2324
@@ -35,6 +36,9 @@
35 get_deb_arch,36 get_deb_arch,
36 get_winrm_certs,37 get_winrm_certs,
37 is_ipv6_address,38 is_ipv6_address,
39 log_and_wrap_exception,
40 logged_exception,
41 LoggedException,
38 quote,42 quote,
39 run_command,43 run_command,
40 scoped_environ,44 scoped_environ,
@@ -714,3 +718,48 @@
714 qualified_model_name('admin/default', 'admin'),718 qualified_model_name('admin/default', 'admin'),
715 'admin/default'719 'admin/default'
716 )720 )
721
722
723class TestLoggedException(TestCase):
724
725 def test_no_error_no_log(self):
726 mock_logger = Mock(spec_set=[])
727 with logged_exception(mock_logger):
728 pass
729
730 def test_exception_logged_and_wrapped(self):
731 mock_logger = Mock(spec=['exception'])
732 err = Exception('some error')
733 with self.assertRaises(LoggedException) as ctx:
734 with logged_exception(mock_logger):
735 raise err
736 self.assertIs(ctx.exception.exception, err)
737 mock_logger.exception.assert_called_once_with(err)
738
739 def test_generator_exit_not_wrapped(self):
740 mock_logger = Mock(spec_set=[])
741 with self.assertRaises(GeneratorExit):
742 with logged_exception(mock_logger):
743 raise GeneratorExit
744
745 def test_keyboard_interrupt_wrapped(self):
746 mock_logger = Mock(spec=['exception'])
747 err = KeyboardInterrupt()
748 with self.assertRaises(LoggedException) as ctx:
749 with logged_exception(mock_logger):
750 raise err
751 self.assertIs(ctx.exception.exception, err)
752 mock_logger.exception.assert_called_once_with(err)
753
754 def test_output_logged(self):
755 mock_logger = Mock(spec=['exception', 'info'])
756 err = Exception('some error')
757 err.output = 'some output'
758 with self.assertRaises(LoggedException) as ctx:
759 with logged_exception(mock_logger):
760 raise err
761 self.assertIs(ctx.exception.exception, err)
762 mock_logger.exception.assert_called_once_with(err)
763 mock_logger.info.assert_called_once_with(
764 'Output from exception:\nstdout:\n%s\nstderr:\n%s', 'some output',
765 None)
717766
=== modified file 'utility.py'
--- utility.py 2016-11-16 20:28:18 +0000
+++ utility.py 2016-11-17 18:55:17 +0000
@@ -526,3 +526,31 @@
526def get_unit_ipaddress(client, unit_name):526def get_unit_ipaddress(client, unit_name):
527 status = client.get_status()527 status = client.get_status()
528 return status.get_unit(unit_name)['public-address']528 return status.get_unit(unit_name)['public-address']
529
530
531def log_and_wrap_exception(logger, exc):
532 """"Record exc details to logger and return wrapped in LoggedException."""
533 logger.exception(exc)
534 stdout = getattr(exc, 'output', None)
535 stderr = getattr(exc, 'stderr', None)
536 if stdout or stderr:
537 logger.info(
538 'Output from exception:\nstdout:\n%s\nstderr:\n%s',
539 stdout, stderr)
540 return LoggedException(exc)
541
542
543@contextmanager
544def logged_exception(logger):
545 """
546 Record exceptions in managed context to logger and reraise LoggedException.
547
548 Note that BaseException classes like SystemExit, KeyboardInterrupt, and
549 GeneratorExit are not wrapped.
550 """
551 try:
552 yield
553 except LoggedException:
554 raise
555 except (Exception, KeyboardInterrupt) as e:
556 raise log_and_wrap_exception(logger, e)

Subscribers

People subscribed via source and target branches