From 5864217bf933927982ea3af2d93c2baccbaa3ba4 Mon Sep 17 00:00:00 2001
From: Andrew Lee <andrew.lee@metaswitch.com>
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

