Merge lp:~bjornt/landscape-client/apt-facade-output into lp:~landscape/landscape-client/trunk

Proposed by Björn Tillenius
Status: Merged
Approved by: Thomas Herve
Approved revision: 419
Merged at revision: 409
Proposed branch: lp:~bjornt/landscape-client/apt-facade-output
Merge into: lp:~landscape/landscape-client/trunk
Diff against target: 302 lines (+205/-12)
2 files modified
landscape/package/facade.py (+26/-3)
landscape/package/tests/test_facade.py (+179/-9)
To merge this branch: bzr merge lp:~bjornt/landscape-client/apt-facade-output
Reviewer Review Type Date Requested Status
Thomas Herve (community) Approve
Alberto Donato (community) Approve
Review via email: mp+83138@code.launchpad.net

Description of the change

Capture the fetch and dpkg output and send it back to the server, both
for succesful and failed operations.

The redirection of stdout/stderr isn't directly pretty, but I asked mvo,
and this is they way it's generally done. It's a bit complicated, since
dpkg runs in a subprocess.

To post a comment you must log in.
Revision history for this message
Alberto Donato (ack) wrote :

Looks good, +1!

review: Approve
Revision history for this message
Thomas Herve (therve) wrote :

This is terrible, but not your fault :). +1!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'landscape/package/facade.py'
--- landscape/package/facade.py 2011-11-22 10:47:13 +0000
+++ landscape/package/facade.py 2011-11-23 10:50:32 +0000
@@ -1,5 +1,7 @@
1import hashlib1import hashlib
2import os2import os
3import tempfile
4from cStringIO import StringIO
35
4from smart.transaction import (6from smart.transaction import (
5 Transaction, PolicyInstall, PolicyUpgrade, PolicyRemove, Failed)7 Transaction, PolicyInstall, PolicyUpgrade, PolicyRemove, Failed)
@@ -363,11 +365,32 @@
363 if dependencies:365 if dependencies:
364 raise DependencyError(366 raise DependencyError(
365 [version for package, version in dependencies])367 [version for package, version in dependencies])
368 fetch_output = StringIO()
369 # Redirect stdout and stderr to a file. We need to work with the
370 # file descriptors, rather than sys.stdout/stderr, since dpkg is
371 # run in a subprocess.
372 fd, install_output_path = tempfile.mkstemp()
373 old_stdout = os.dup(1)
374 old_stderr = os.dup(2)
375 os.dup2(fd, 1)
376 os.dup2(fd, 2)
366 try:377 try:
367 self._cache.commit()378 self._cache.commit(
379 fetch_progress=apt.progress.text.AcquireProgress(fetch_output))
368 except SystemError, error:380 except SystemError, error:
369 raise TransactionError(error.args[0])381 result_text = (
370 return "ok"382 fetch_output.getvalue() + read_file(install_output_path))
383 raise TransactionError(
384 error.args[0] + "\n\nPackage operation log:\n" + result_text)
385 else:
386 result_text = (
387 fetch_output.getvalue() + read_file(install_output_path))
388 finally:
389 # Restore stdout and stderr.
390 os.dup2(old_stdout, 1)
391 os.dup2(old_stderr, 2)
392 os.remove(install_output_path)
393 return result_text
371394
372 def reset_marks(self):395 def reset_marks(self):
373 """Clear the pending package operations."""396 """Clear the pending package operations."""
374397
=== modified file 'landscape/package/tests/test_facade.py'
--- landscape/package/tests/test_facade.py 2011-11-15 10:04:32 +0000
+++ landscape/package/tests/test_facade.py 2011-11-23 10:50:32 +0000
@@ -3,6 +3,7 @@
3import re3import re
4import sys4import sys
5import textwrap5import textwrap
6import tempfile
67
7from smart.control import Control8from smart.control import Control
8from smart.cache import Provides9from smart.cache import Provides
@@ -30,6 +31,26 @@
30 create_simple_repository)31 create_simple_repository)
3132
3233
34class FakeOwner(object):
35 """Fake Owner object that apt.progress.text.AcquireProgress expects."""
36
37 def __init__(self, filesize, error_text=""):
38 self.id = None
39 self.filesize = filesize
40 self.complete = False
41 self.status = None
42 self.STAT_DONE = object()
43 self.error_text = error_text
44
45
46class FakeFetchItem(object):
47 """Fake Item object that apt.progress.text.AcquireProgress expects."""
48
49 def __init__(self, owner, description):
50 self.owner = owner
51 self.description = description
52
53
33class AptFacadeTest(LandscapeTest):54class AptFacadeTest(LandscapeTest):
3455
35 helpers = [AptFacadeHelper]56 helpers = [AptFacadeHelper]
@@ -713,6 +734,157 @@
713 self.facade.reload_channels()734 self.facade.reload_channels()
714 self.assertEqual(self.facade.perform_changes(), None)735 self.assertEqual(self.facade.perform_changes(), None)
715736
737 def test_perform_changes_fetch_progress(self):
738 """
739 C{perform_changes()} captures the fetch output and returns it.
740 """
741 deb_dir = self.makeDir()
742 self._add_package_to_deb_dir(deb_dir, "foo")
743 self.facade.add_channel_apt_deb("file://%s" % deb_dir, "./")
744 self.facade.reload_channels()
745 foo = self.facade.get_packages_by_name("foo")[0]
746 self.facade.mark_install(foo)
747 fetch_item = FakeFetchItem(
748 FakeOwner(1234, error_text="Some error"), "foo package")
749
750 def commit(fetch_progress):
751 fetch_progress.start()
752 fetch_progress.fetch(fetch_item)
753 fetch_progress.fail(fetch_item)
754 fetch_progress.done(fetch_item)
755 fetch_progress.stop()
756
757 self.facade._cache.commit = commit
758 output = [
759 line.rstrip()
760 for line in self.facade.perform_changes().splitlines()
761 if line.strip()]
762 self.assertEqual(
763 ["Get:1 foo package [1234 B]",
764 "Err foo package",
765 " Some error",
766 "Fetched 0 B in 0s (0 B/s)"],
767 output)
768
769 def test_perform_changes_dpkg_output(self):
770 """
771 C{perform_changes()} captures the dpkg output and returns it.
772 """
773 deb_dir = self.makeDir()
774 self._add_package_to_deb_dir(deb_dir, "foo")
775 self.facade.add_channel_apt_deb("file://%s" % deb_dir, "./")
776 self.facade.reload_channels()
777 foo = self.facade.get_packages_by_name("foo")[0]
778 self.facade.mark_install(foo)
779
780 def commit(fetch_progress):
781 os.write(1, "Stdout output\n")
782 os.write(2, "Stderr output\n")
783 os.write(1, "Stdout output again\n")
784
785 self.facade._cache.commit = commit
786 output = [
787 line.rstrip()
788 for line in self.facade.perform_changes().splitlines()
789 if line.strip()]
790 self.assertEqual(
791 ["Stdout output", "Stderr output", "Stdout output again"], output)
792
793 def test_perform_changes_dpkg_output_error(self):
794 """
795 C{perform_changes()} captures the dpkg output and includes it in
796 the exception message, if committing the cache fails.
797 """
798 deb_dir = self.makeDir()
799 self._add_package_to_deb_dir(deb_dir, "foo")
800 self.facade.add_channel_apt_deb("file://%s" % deb_dir, "./")
801 self.facade.reload_channels()
802 foo = self.facade.get_packages_by_name("foo")[0]
803 self.facade.mark_install(foo)
804
805 def commit(fetch_progress):
806 os.write(1, "Stdout output\n")
807 os.write(2, "Stderr output\n")
808 os.write(1, "Stdout output again\n")
809 raise SystemError("Oops")
810
811 self.facade._cache.commit = commit
812 exception = self.assertRaises(
813 TransactionError, self.facade.perform_changes)
814 output = [
815 line.rstrip()
816 for line in exception.args[0].splitlines()if line.strip()]
817 self.assertEqual(
818 ["Oops", "Package operation log:", "Stdout output",
819 "Stderr output", "Stdout output again"],
820 output)
821
822 def _mock_output_restore(self):
823 """
824 Mock methods to ensure that stdout and stderr are restored,
825 after they have been captured.
826
827 Return the path to the tempfile that was used to capture the output.
828 """
829 old_stdout = os.dup(1)
830 old_stderr = os.dup(2)
831 fd, outfile = tempfile.mkstemp()
832 mkstemp = self.mocker.replace("tempfile.mkstemp")
833 mkstemp()
834 self.mocker.result((fd, outfile))
835 dup = self.mocker.replace("os.dup")
836 dup(1)
837 self.mocker.result(old_stdout)
838 dup(2)
839 self.mocker.result(old_stderr)
840 dup2 = self.mocker.replace("os.dup2")
841 dup2(old_stdout, 1)
842 self.mocker.passthrough()
843 dup2(old_stderr, 2)
844 self.mocker.passthrough()
845 return outfile
846
847 def test_perform_changes_dpkg_output_reset(self):
848 """
849 C{perform_changes()} resets stdout and stderr after the cache commit.
850 """
851 deb_dir = self.makeDir()
852 self._add_package_to_deb_dir(deb_dir, "foo")
853 self.facade.add_channel_apt_deb("file://%s" % deb_dir, "./")
854 self.facade.reload_channels()
855 foo = self.facade.get_packages_by_name("foo")[0]
856 self.facade.mark_install(foo)
857
858 outfile = self._mock_output_restore()
859 self.mocker.replay()
860 self.facade._cache.commit = lambda fetch_progress: None
861 self.facade.perform_changes()
862 # Make sure we don't leave the tempfile behind.
863 self.assertFalse(os.path.exists(outfile))
864
865 def test_perform_changes_dpkg_output_reset_error(self):
866 """
867 C{perform_changes()} resets stdout and stderr after the cache
868 commit, even if commit raises an error.
869 """
870 deb_dir = self.makeDir()
871 self._add_package_to_deb_dir(deb_dir, "foo")
872 self.facade.add_channel_apt_deb("file://%s" % deb_dir, "./")
873 self.facade.reload_channels()
874 foo = self.facade.get_packages_by_name("foo")[0]
875 self.facade.mark_install(foo)
876
877 outfile = self._mock_output_restore()
878 self.mocker.replay()
879
880 def commit(fetch_progress):
881 raise SystemError("Error")
882
883 self.facade._cache.commit = commit
884 self.assertRaises(TransactionError, self.facade.perform_changes)
885 # Make sure we don't leave the tempfile behind.
886 self.assertFalse(os.path.exists(outfile))
887
716 def test_reset_marks(self):888 def test_reset_marks(self):
717 """889 """
718 C{reset_marks()} clears things, so that there's nothing to do890 C{reset_marks()} clears things, so that there's nothing to do
@@ -791,7 +963,7 @@
791 foo1, foo2 = sorted(self.facade.get_packages_by_name("foo"))963 foo1, foo2 = sorted(self.facade.get_packages_by_name("foo"))
792 self.assertEqual(foo2, foo1.package.candidate)964 self.assertEqual(foo2, foo1.package.candidate)
793 self.facade.mark_install(foo1)965 self.facade.mark_install(foo1)
794 self.facade._cache.commit = lambda: None966 self.facade._cache.commit = lambda fetch_progress: None
795 self.facade.perform_changes()967 self.facade.perform_changes()
796 self.assertEqual(foo1, foo1.package.candidate)968 self.assertEqual(foo1, foo1.package.candidate)
797969
@@ -811,7 +983,6 @@
811 foo1, foo2, foo3 = sorted(self.facade.get_packages_by_name("foo"))983 foo1, foo2, foo3 = sorted(self.facade.get_packages_by_name("foo"))
812 self.assertEqual(foo3, foo1.package.candidate)984 self.assertEqual(foo3, foo1.package.candidate)
813 self.facade.mark_upgrade(foo1)985 self.facade.mark_upgrade(foo1)
814 self.facade._cache.commit = lambda: None
815 exception = self.assertRaises(986 exception = self.assertRaises(
816 DependencyError, self.facade.perform_changes)987 DependencyError, self.facade.perform_changes)
817 self.assertEqual([foo3], exception.packages)988 self.assertEqual([foo3], exception.packages)
@@ -850,7 +1021,6 @@
850 noauto1.package.mark_auto(False)1021 noauto1.package.mark_auto(False)
851 self.facade.mark_upgrade(auto1)1022 self.facade.mark_upgrade(auto1)
852 self.facade.mark_upgrade(noauto1)1023 self.facade.mark_upgrade(noauto1)
853 self.facade._cache.commit = lambda: None
854 self.assertRaises(DependencyError, self.facade.perform_changes)1024 self.assertRaises(DependencyError, self.facade.perform_changes)
855 self.assertTrue(auto2.package.is_auto_installed)1025 self.assertTrue(auto2.package.is_auto_installed)
856 self.assertFalse(noauto2.package.is_auto_installed)1026 self.assertFalse(noauto2.package.is_auto_installed)
@@ -862,7 +1032,7 @@
862 """1032 """
863 committed = []1033 committed = []
8641034
865 def commit():1035 def commit(fetch_progress):
866 committed.append(True)1036 committed.append(True)
8671037
868 deb_dir = self.makeDir()1038 deb_dir = self.makeDir()
@@ -887,10 +1057,10 @@
887 self.facade.reload_channels()1057 self.facade.reload_channels()
888 pkg = self.facade.get_packages_by_name("minimal")[0]1058 pkg = self.facade.get_packages_by_name("minimal")[0]
889 self.facade.mark_install(pkg)1059 self.facade.mark_install(pkg)
890 self.facade._cache.commit = lambda: None1060 self.facade._cache.commit = lambda fetch_progress: None
891 # XXX: It should return the Apt output, but that will be done1061 # An empty string is returned, since we don't call the progress
892 # later.1062 # objects, which are the ones that build the output string.
893 self.assertEqual("ok", self.facade.perform_changes())1063 self.assertEqual("", self.facade.perform_changes())
8941064
895 def test_wb_perform_changes_commit_error(self):1065 def test_wb_perform_changes_commit_error(self):
896 """1066 """
@@ -903,7 +1073,7 @@
903 [foo] = self.facade.get_packages_by_name("foo")1073 [foo] = self.facade.get_packages_by_name("foo")
904 self.facade.mark_remove(foo)1074 self.facade.mark_remove(foo)
905 cache = self.mocker.replace(self.facade._cache)1075 cache = self.mocker.replace(self.facade._cache)
906 cache.commit()1076 cache.commit(fetch_progress=ANY)
907 self.mocker.throw(SystemError("Something went wrong."))1077 self.mocker.throw(SystemError("Something went wrong."))
908 self.mocker.replay()1078 self.mocker.replay()
909 exception = self.assertRaises(TransactionError,1079 exception = self.assertRaises(TransactionError,

Subscribers

People subscribed via source and target branches

to all changes: