From 5864217bf933927982ea3af2d93c2baccbaa3ba4 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 7 Apr 2022 21:52:44 +0100 Subject: [PATCH 3/8] =?UTF-8?q?BUG=201473527:=20module=20ssh-authkey-finge?= =?UTF-8?q?rprints=20fails=20Input/output=20error=E2=80=A6=20(#1340)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reference:https://github.com/canonical/cloud-init/commit/fa53c7f4086f5937bc9bd328dba9f91ca73b6614 Conflict:tools/.github-cla-signers not change. Don't error if we cannot log to /dev/console We've seen instances on VMware of serial consoles not being set up correctly by the kernel, making /dev/ttyS0 not set up correctly, and hence /dev/console not writeable to. In such circumstances, cloud-init should not fail, instead it should gracefully fall back to logging to stdout. The only time cloud-init tries to write to `/dev/console` is in the `multi_log` command- which is called by the ssh-authkey-fingerprints module LP: #1473527 --- cloudinit/util.py | 33 +++++++++++++++++++++++++-------- tests/unittests/test_util.py | 27 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/cloudinit/util.py b/cloudinit/util.py index ef1b588..d5e8277 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -359,20 +359,37 @@ def find_modules(root_dir): return entries +def write_to_console(conpath, text): + with open(conpath, "w") as wfh: + wfh.write(text) + wfh.flush() + + def multi_log(text, console=True, stderr=True, log=None, log_level=logging.DEBUG, fallback_to_stdout=True): if stderr: sys.stderr.write(text) if console: conpath = "/dev/console" + writing_to_console_worked = False if os.path.exists(conpath): - with open(conpath, 'w') as wfh: - wfh.write(text) - wfh.flush() - elif fallback_to_stdout: - # A container may lack /dev/console (arguably a container bug). If - # it does not exist, then write output to stdout. this will result - # in duplicate stderr and stdout messages if stderr was True. + try: + write_to_console(conpath, text) + writing_to_console_worked = True + except OSError: + console_error = "Failed to write to /dev/console" + sys.stdout.write(f"{console_error}\n") + if log: + log.log(logging.WARNING, console_error) + + if fallback_to_stdout and not writing_to_console_worked: + # A container may lack /dev/console (arguably a container bug). + # Additionally, /dev/console may not be writable to on a VM (again + # likely a VM bug or virtualization bug). + # + # If either of these is the case, then write output to stdout. + # This will result in duplicate stderr and stdout messages if + # stderr was True. # # even though upstart or systemd might have set up output to go to # /dev/console, the user may have configured elsewhere via @@ -1948,7 +1965,7 @@ def write_file( omode="wb", preserve_mode=False, *, - ensure_dir_exists=True + ensure_dir_exists=True, ): """ Writes a file with the given content and sets the file mode as specified. diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py index bc30c90..0b01337 100644 --- a/tests/unittests/test_util.py +++ b/tests/unittests/test_util.py @@ -576,6 +576,33 @@ class TestMultiLog(helpers.FilesystemMockingTestCase): util.multi_log('something', fallback_to_stdout=False) self.assertEqual('', self.stdout.getvalue()) + @mock.patch( + "cloudinit.util.write_to_console", + mock.Mock(side_effect=OSError("Failed to write to console")), + ) + def test_logs_go_to_stdout_if_writing_to_console_fails_and_fallback_true( + self, + ): + self._createConsole(self.root) + util.multi_log("something", fallback_to_stdout=True) + self.assertEqual( + "Failed to write to /dev/console\nsomething", + self.stdout.getvalue(), + ) + + @mock.patch( + "cloudinit.util.write_to_console", + mock.Mock(side_effect=OSError("Failed to write to console")), + ) + def test_logs_go_nowhere_if_writing_to_console_fails_and_fallback_false( + self, + ): + self._createConsole(self.root) + util.multi_log("something", fallback_to_stdout=False) + self.assertEqual( + "Failed to write to /dev/console\n", self.stdout.getvalue() + ) + def test_logs_go_to_log_if_given(self): log = mock.MagicMock() logged_string = 'something very important' -- 2.40.0