From b06f6f5fb3addfbe415296cb4f1092afe869d298 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Fri, 25 Aug 2023 11:58:12 -0400 Subject: [PATCH] Fix cc_keyboard in mantic localectl set-x11-keymap is now a disabled sub-command for new debian-based installs. Given we can no longer use this command, write /etc/default/keyboard directly as directed in man keyboard. LP: #2030788 --- cloudinit/distros/__init__.py | 2 +- cloudinit/distros/alpine.py | 2 +- cloudinit/distros/debian.py | 35 +++++++++++++-- tests/unittests/config/test_cc_keyboard.py | 50 +++++++++++++--------- 4 files changed, 63 insertions(+), 26 deletions(-) diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 4717888329a..7b83df8d472 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -1052,7 +1052,7 @@ def manage_service( cmd = list(init_cmd) + list(cmds[action]) return subp.subp(cmd, capture=True, rcs=rcs) - def set_keymap(self, layout, model, variant, options): + def set_keymap(self, layout: str, model: str, variant: str, options: str): if self.uses_systemd(): subp.subp( [ diff --git a/cloudinit/distros/alpine.py b/cloudinit/distros/alpine.py index 898a0c7c2fe..eaf1fc751a8 100644 --- a/cloudinit/distros/alpine.py +++ b/cloudinit/distros/alpine.py @@ -110,7 +110,7 @@ def _read_hostname(self, filename, default=None): def _get_localhost_ip(self): return "127.0.1.1" - def set_keymap(self, layout, model, variant, options): + def set_keymap(self, layout: str, model: str, variant: str, options: str): if not layout: msg = "Keyboard layout not specified." LOG.error(msg) diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py index 87f4cc9f127..37267559a4b 100644 --- a/cloudinit/distros/debian.py +++ b/cloudinit/distros/debian.py @@ -300,11 +300,38 @@ def update_package_sources(self): def get_primary_arch(self): return util.get_dpkg_architecture() - def set_keymap(self, layout, model, variant, options): - # Let localectl take care of updating /etc/default/keyboard - distros.Distro.set_keymap(self, layout, model, variant, options) - # Workaround for localectl not applying new settings instantly + def set_keymap(self, layout: str, model: str, variant: str, options: str): + # localectl is broken on some versions of Debian. See + # https://bugs.launchpad.net/ubuntu/+source/systemd/+bug/2030788 and + # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1038762 + # + # Instead, write the file directly. According to the keyboard(5) man + # page, this file is shared between both X and the console. + + contents = "\n".join( + [ + "# This file was generated by cloud-init", + "", + f'XKBMODEL="{model}"', + f'XKBLAYOUT="{layout}"', + f'XKBVARIANT="{variant}"', + f'XKBOPTIONS="{options}"', + "", + 'BACKSPACE="guess"', # This is provided on default installs + "", + ] + ) + util.write_file( + filename="/etc/default/keyboard", + content=contents, + mode=0o644, + omode="w", + ) + + # Due to # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=926037 + # if localectl can be used in the future, this line may still + # be needed self.manage_service("restart", "console-setup") diff --git a/tests/unittests/config/test_cc_keyboard.py b/tests/unittests/config/test_cc_keyboard.py index a17be9b7a59..079e15ba434 100644 --- a/tests/unittests/config/test_cc_keyboard.py +++ b/tests/unittests/config/test_cc_keyboard.py @@ -93,33 +93,43 @@ def setUp(self): @mock.patch("cloudinit.distros.Distro.uses_systemd") @mock.patch("cloudinit.distros.subp.subp") def test_systemd_linux_cmd(self, m_subp, m_uses_systemd, *args): - """Ubuntu runs localectl""" + """Non-Debian systems run localectl""" cfg = {"keyboard": {"layout": "us", "variant": "us"}} layout = "us" model = "pc105" variant = "us" options = "" m_uses_systemd.return_value = True - cloud = get_cloud("ubuntu") + cloud = get_cloud("fedora") cc_keyboard.handle("cc_keyboard", cfg, cloud, []) - locale_calls = [ - mock.call( - [ - "localectl", - "set-x11-keymap", - layout, - model, - variant, - options, - ] - ), - mock.call( - ["systemctl", "restart", "console-setup"], - capture=True, - rcs=None, - ), - ] - m_subp.assert_has_calls(locale_calls) + locale_call = mock.call( + [ + "localectl", + "set-x11-keymap", + layout, + model, + variant, + options, + ] + ) + assert m_subp.call_args == locale_call + + @mock.patch("cloudinit.util.write_file") + @mock.patch("cloudinit.distros.subp.subp") + def test_debian_linux_cmd(self, m_subp, m_write_file): + """localectl is broken on Debian-based systems so write conf file""" + cfg = {"keyboard": {"layout": "gb", "variant": "dvorak"}} + cloud = get_cloud("debian") + cc_keyboard.handle("cc_keyboard", cfg, cloud, []) + + m_content = m_write_file.call_args[1]["content"] + assert 'XKBMODEL="pc105"' in m_content + assert 'XKBLAYOUT="gb"' in m_content + assert 'XKBVARIANT="dvorak"' in m_content + assert "/etc/default/keyboard" == m_write_file.call_args[1]["filename"] + m_subp.assert_called_with( + ["service", "console-setup", "restart"], capture=True, rcs=None + ) @mock.patch("cloudinit.distros.subp.subp") def test_alpine_linux_cmd(self, m_subp, *args):