Merge ~ajorgens/cloud-init:pipe-cat into cloud-init:master

Proposed by Andrew Jorgensen
Status: Work in progress
Proposed branch: ~ajorgens/cloud-init:pipe-cat
Merge into: cloud-init:master
Diff against target: 111 lines (+35/-4)
3 files modified
cloudinit/distros/rhel.py (+1/-1)
cloudinit/util.py (+29/-3)
tests/unittests/test_util.py (+5/-0)
Reviewer Review Type Date Requested Status
Scott Moser Needs Fixing
Server Team CI bot continuous-integration Approve
Review via email: mp+325512@code.launchpad.net

Commit message

util: Disconnect subp from a tty

Some processes are less chatty when they don't find a terminal. Yum is a prime example. Without this it can display progress bars and fill up your console logs with garbage, making debugging harder.

Description of the change

util: Disconnect subp from a tty

Some processes are less chatty when they don't find a terminal. Yum is a prime example. Without this it can display progress bars and fill up your console logs with garbage, making debugging harder.

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

It seems like there is probably a better (or at very least less expensive) way to do this than to incur the cost of a subprocess.

I suspect
a.) change 'yum' to be invoked with '--quiet' or the like
b.) close and re-open stdout or something like that. yum is probably using 'isatty' to change its behavior.

Revision history for this message
Andrew Jorgensen (ajorgens) wrote :

a.) Unfortunately, asking Yum to be quiet means it won't give you any of the debugging information you need when it goes wrong.

b.) Possibly there's a better way to disconnect it from the terminal, but what isatty does is ask the system if stdout is associated with a terminal, so unless you make stdout something other than the one associated with the terminal it will answer that it's a tty. In other words, it has to be piped through something. Piping it through cat seemed the lesser of two evils compared to spinning off a thread (or greenlet or whatever) to consume stdout and spool it to the console.

I'm definitely open to advice on how to do this a better way, if you want to ask around.

Revision history for this message
Andrew Jorgensen (ajorgens) wrote :

Hmm. I think I've found a better way, pending more testing.

~ajorgens/cloud-init:pipe-cat updated
10676d5... by Andrew Jorgensen

read/write stdout instead of piping through cat

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Andrew Jorgensen (ajorgens) wrote :

New revision avoiding spawning cat, but emulating subprocess.communicate() instead.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
~ajorgens/cloud-init:pipe-cat updated
4d09af6... by Andrew Jorgensen

Use a no_tty option instead of always disconnecting from tty

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

Andrew,
thanks. I think the code looks good
But a few things

a.) maybe 'detach_tty' as the name? 'no_tty=False' seems to imply that there would be a tty, when we are not actually creating one.
b.) need some sort of test, at least to push it through here. it might be tricky to do, but i'd like to run a subp 'detach_tty=False' and get some subprocess to believe it is a tty.
   subp(['bash', '-c', '[ "-t" 1 ] && echo attached to tty || echo not attached to tty'])
   the difficulty is that in any c-i or automated environment, we're probably not attached to a tty. but we do need some test of this code path.
c.) i think best to take the detach_tty path only if we *have* a tty.
   if os.isatty(sys.stdout):
      ... go the detach route
   else:
      ... no need to detach

Revision history for this message
Scott Moser (smoser) wrote :

we need *some* test of this though, even if we have to mock os.isatty to get reliable tests.

review: Needs Fixing
Revision history for this message
Andrew Jorgensen (ajorgens) wrote :

Agreed we need a unit test of this code path. I'll see what I can do.

Revision history for this message
Scott Moser (smoser) wrote :

I'll move this to 'work in progress' for now set back to Needs Review when you're ready.
thanks.

Revision history for this message
Andrew Jorgensen (ajorgens) wrote :

So... I spent a lot of time yesterday trying to use pty.openpty() to make a unit test for this, and I give up, at least for now. I'm unsure if the code is bad or if the unit test is impossible.

In Amazon Linux we're using the redirect_output thing to pipe everything through tee anyway, so this is likely a non-issue for us. If I feel ambitious or have a great idea how else to solve this I might do that some day, but for now I'm dropping it.

~ajorgens/cloud-init:pipe-cat updated
7941e58... by Andrew Jorgensen

work-in-progress - broken

Unmerged commits

7941e58... by Andrew Jorgensen

work-in-progress - broken

4d09af6... by Andrew Jorgensen

Use a no_tty option instead of always disconnecting from tty

10676d5... by Andrew Jorgensen

read/write stdout instead of piping through cat

c97434e... by Andrew Jorgensen

rhel: Use the pipe_cat option for Yum commands

5675c65... by Andrew Jorgensen

util: Add an option for subp to pipe the process through cat

Some processes are less chatty when piped through cat, because they won't
detect a terminal (yum being a prime example).

Reviewed-by: Matt Nierzwicki <email address hidden>
Reviewed-by: Ethan Faust <email address hidden>
[<email address hidden>: rebase onto 0.7.9]

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py
index 1fecb61..03f02de 100644
--- a/cloudinit/distros/rhel.py
+++ b/cloudinit/distros/rhel.py
@@ -212,7 +212,7 @@ class Distro(distros.Distro):
212 cmd.extend(pkglist)212 cmd.extend(pkglist)
213213
214 # Allow the output of this to flow outwards (ie not be captured)214 # Allow the output of this to flow outwards (ie not be captured)
215 util.subp(cmd, capture=False)215 util.subp(cmd, capture=False, detach_tty=True)
216216
217 def update_package_sources(self):217 def update_package_sources(self):
218 self._runner.run("update-sources", self.package_command,218 self._runner.run("update-sources", self.package_command,
diff --git a/cloudinit/util.py b/cloudinit/util.py
index c93b6d7..9f5715d 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -1,10 +1,12 @@
1# Copyright (C) 2012 Canonical Ltd.1# Copyright (C) 2012 Canonical Ltd.
2# Copyright (C) 2012, 2013 Hewlett-Packard Development Company, L.P.2# Copyright (C) 2012, 2013 Hewlett-Packard Development Company, L.P.
3# Copyright (C) 2012 Yahoo! Inc.3# Copyright (C) 2012 Yahoo! Inc.
4# Copyright (C) 2017 Amazon.com, Inc. or its affiliates.
4#5#
5# Author: Scott Moser <scott.moser@canonical.com>6# Author: Scott Moser <scott.moser@canonical.com>
6# Author: Juerg Haefliger <juerg.haefliger@hp.com>7# Author: Juerg Haefliger <juerg.haefliger@hp.com>
7# Author: Joshua Harlow <harlowja@yahoo-inc.com>8# Author: Joshua Harlow <harlowja@yahoo-inc.com>
9# Author: Andrew Jorgensen <ajorgens@amazon.com>
8#10#
9# This file is part of cloud-init. See LICENSE file for license information.11# This file is part of cloud-init. See LICENSE file for license information.
1012
@@ -1771,13 +1773,18 @@ def delete_dir_contents(dirname):
17711773
17721774
1773def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,1775def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,
1774 logstring=False, decode="replace", target=None, update_env=None):1776 logstring=False, decode="replace", target=None, update_env=None,
1777 detach_tty=False):
17751778
1776 # not supported in cloud-init (yet), for now kept in the call signature1779 # not supported in cloud-init (yet), for now kept in the call signature
1777 # to ease maintaining code shared between cloud-init and curtin1780 # to ease maintaining code shared between cloud-init and curtin
1778 if target is not None:1781 if target is not None:
1779 raise ValueError("target arg not supported by cloud-init")1782 raise ValueError("target arg not supported by cloud-init")
17801783
1784 if detach_tty and not sys.stdout.isatty():
1785 LOG.debug("No TTY detected, ignoring detach_tty")
1786 detach_tty = False
1787
1781 if rcs is None:1788 if rcs is None:
1782 rcs = [0]1789 rcs = [0]
17831790
@@ -1803,8 +1810,9 @@ def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,
1803 stdin = None1810 stdin = None
1804 stdout = None1811 stdout = None
1805 stderr = None1812 stderr = None
1806 if capture:1813 if detach_tty or capture:
1807 stdout = subprocess.PIPE1814 stdout = subprocess.PIPE
1815 if capture:
1808 stderr = subprocess.PIPE1816 stderr = subprocess.PIPE
1809 if data is None:1817 if data is None:
1810 # using devnull assures any reads get null, rather1818 # using devnull assures any reads get null, rather
@@ -1819,7 +1827,25 @@ def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,
1819 sp = subprocess.Popen(args, stdout=stdout,1827 sp = subprocess.Popen(args, stdout=stdout,
1820 stderr=stderr, stdin=stdin,1828 stderr=stderr, stdin=stdin,
1821 env=env, shell=shell)1829 env=env, shell=shell)
1822 (out, err) = sp.communicate(data)1830
1831 # Some processes are less chatty when disconnected from a tty.
1832 # (capture=True already doesn't have a tty)
1833 if detach_tty and not capture:
1834 (out, err) = (None, None)
1835 if data is not None:
1836 try:
1837 sp.stdin.write(data)
1838 # Borrowed from cpython 2.7 subprocess.py
1839 except IOError as e:
1840 if e.errno != errno.EPIPE and e.errno != errno.EINVAL:
1841 raise
1842 sp.stdin.close()
1843 while sp.poll() is None:
1844 sys.stdout.write(sp.stdout.readline())
1845 sys.stdout.flush()
1846 sp.stdout.close()
1847 else:
1848 (out, err) = sp.communicate(data)
18231849
1824 # Just ensure blank instead of none.1850 # Just ensure blank instead of none.
1825 if not out and capture:1851 if not out and capture:
diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
index a73fd26..37354b4 100644
--- a/tests/unittests/test_util.py
+++ b/tests/unittests/test_util.py
@@ -4,6 +4,7 @@ from __future__ import print_function
44
5import logging5import logging
6import os6import os
7import pty
7import shutil8import shutil
8import stat9import stat
9import tempfile10import tempfile
@@ -627,6 +628,10 @@ class TestSubp(helpers.TestCase):
627 self.assertEqual(628 self.assertEqual(
628 ['FOO=BAR', 'HOME=/myhome', 'K1=V1', 'K2=V2'], out.splitlines())629 ['FOO=BAR', 'HOME=/myhome', 'K1=V1', 'K2=V2'], out.splitlines())
629630
631 def test_subp_detach_tty():
632 with pty.openpty() as (master, slave):
633 pass
634
630 def test_returns_none_if_no_capture(self):635 def test_returns_none_if_no_capture(self):
631 (out, err) = util.subp(self.stdin2out, data=b'', capture=False)636 (out, err) = util.subp(self.stdin2out, data=b'', capture=False)
632 self.assertIsNone(err)637 self.assertIsNone(err)

Subscribers

People subscribed via source and target branches