@@ -22,9 +22,12 @@ the IP address, hostname, and services (description and status).
2222import curses
2323import curses .ascii
2424import curses .textpad
25+ import re
2526import sys
2627import signal
2728import os
29+ import shutil
30+ import tempfile
2831import logging
2932import subprocess
3033from typing import List , Any , Tuple , Generator
@@ -102,11 +105,66 @@ def get_keyboard_layout() -> str:
102105
103106def set_keyboard_layout (layout : str ) -> subprocess .CompletedProcess :
104107 """
105- Set the keyboard layout based on the user selection.
108+ Set the keyboard layout by editing /etc/default/keyboard and applying it with setupcon.
109+ This avoids localectl (which is disabled on Debian/Ubuntu builds).
110+
111+ Notes:
112+ - Does NOT trigger update-initramfs; only applies to the running system's console. Delphix
113+ doesn't have TTY interactions before pivot-root, e.g. encrypted root passphrase, and
114+ rescue shell can be done with the default "us" layout.
106115 """
107- cmd : List [str ] = ['localectl' , 'set-x11-keymap' , layout , 'pc105' ]
108- subprocess .run (cmd , shell = False , check = True )
109- return subprocess .run ('setupcon' , shell = False , check = True )
116+ kb_path = "/etc/default/keyboard"
117+ kb_backup = kb_path + ".bak"
118+
119+ # Read current content (if file doesn't exist, start with a minimal stub)
120+ try :
121+ with open (kb_path , "r" , encoding = "utf-8" ) as f :
122+ lines = f .readlines ()
123+ except FileNotFoundError :
124+ lines = [
125+ 'XKBMODEL="pc105"\n ' ,
126+ f'XKBLAYOUT="{ layout } "\n ' ,
127+ 'XKBVARIANT=""\n ' ,
128+ 'XKBOPTIONS=""\n ' ,
129+ 'BACKSPACE="guess"\n ' ,
130+ ]
131+ else :
132+ # Update or append XKBLAYOUT line
133+ updated = False
134+ pattern = re .compile (r'^\s*XKBLAYOUT\s*=' )
135+ for i , line in enumerate (lines ):
136+ if pattern .match (line ):
137+ lines [i ] = f'XKBLAYOUT="{ layout } "\n '
138+ updated = True
139+ break
140+ if not updated :
141+ # Append if not present
142+ lines .append (f'XKBLAYOUT="{ layout } "\n ' )
143+
144+ # Atomic write with backup
145+ os .makedirs (os .path .dirname (kb_path ), exist_ok = True )
146+ if os .path .exists (kb_path ):
147+ shutil .copy2 (kb_path , kb_backup )
148+
149+ fd , tmp = tempfile .mkstemp (prefix = ".keyboard." ,
150+ dir = os .path .dirname (kb_path ))
151+ try :
152+ with os .fdopen (fd , "w" , encoding = "utf-8" ) as f :
153+ f .writelines (lines )
154+ os .replace (tmp , kb_path )
155+ except Exception :
156+ # Clean temp file and restore from backup if we wrote a partial file
157+ try :
158+ os .remove (tmp )
159+ except OSError :
160+ pass
161+ if os .path .exists (kb_backup ):
162+ shutil .copy2 (kb_backup , kb_path )
163+ raise
164+
165+ # Apply to current console immediately (does not affect serial consoles)
166+ # Use --force so it rebuilds/loads even if it thinks nothing changed.
167+ return subprocess .run (["setupcon" , "--force" ], shell = False , check = True )
110168
111169
112170def get_valid_keyboard_layouts () -> List [str ]:
0 commit comments