vmspawn: use our own ptyfwd code for the console of a VM
authorLennart Poettering <lennart@poettering.net>
Fri, 23 Feb 2024 11:20:55 +0000 (12:20 +0100)
committerLennart Poettering <lennart@poettering.net>
Mon, 26 Feb 2024 10:54:37 +0000 (11:54 +0100)
Let's make systemd-nspawn use our own ptyfwd logic to handle the TTY by
default.

This adds a new setting --console=, inspired by nspawn's setting of the
same name. If --console=interactive= is used, then we'll do the TTY
dance on our own via ptyfwd, and thus get tinting, our usual hotkey
handling and similar.

Since qemu's own console is useful too, let's keep it around via
--console=native.

FInally, replace the --qemu-gui switch by --console=gui.

man/systemd-vmspawn.xml
src/basic/glyph-util.c
src/basic/glyph-util.h
src/test/test-locale-util.c
src/vmspawn/vmspawn-settings.c
src/vmspawn/vmspawn-settings.h
src/vmspawn/vmspawn.c

index ed4dfc8bfab01ed17d314937d34c1464f3a46da4..d7fee0538ac8c5491a700a305fa07c7c0af6abc2 100644 (file)
           </listitem>
         </varlistentry>
 
-        <varlistentry>
-          <term><option>--qemu-gui</option></term>
-
-          <listitem><para>Start QEMU in graphical mode.</para>
-
-          <xi:include href="version-info.xml" xpointer="v255"/></listitem>
-        </varlistentry>
-
         <varlistentry>
           <term><option>-n</option></term>
           <term><option>--network-tap</option></term>
       </variablelist>
     </refsect2>
 
+    <refsect2>
+      <title>Input/Output Options</title>
+
+      <variablelist>
+        <varlistentry>
+          <term><option>--console=</option><replaceable>MODE</replaceable></term>
+
+          <listitem><para>Configures how to set up the console of the VM. Takes one of
+          <literal>interactive</literal>, <literal>read-only</literal>, <literal>native</literal>,
+          <literal>gui</literal>. Defaults to <literal>interactive</literal>. <literal>interactive</literal>
+          provides an interactive terminal interface to the VM. <literal>read-only</literal> is similar, but
+          is strictly read-only, i.e. does not accept any input from the user. <literal>native</literal> also
+          provides a TTY-based interface, but uses qemu native implementation (which means the qemu monitor
+          is available). <literal>gui</literal> shows the qemu graphical UI.</para>
+
+          <xi:include href="version-info.xml" xpointer="v256"/></listitem>
+        </varlistentry>
+
+        <varlistentry>
+          <term><option>--background=<replaceable>COLOR</replaceable></option></term>
+
+          <listitem><para>Change the terminal background color to the specified ANSI color as long as the VM
+          runs. The color specified should be an ANSI X3.64 SGR background color, i.e. strings such as
+          <literal>40</literal>, <literal>41</literal>, …, <literal>47</literal>, <literal>48;2;…</literal>,
+          <literal>48;5;…</literal>. See <ulink
+          url="https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters">ANSI
+          Escape Code (Wikipedia)</ulink> for details. Assign an empty string to disable any coloring. This
+          only has an effect in <option>--console=interactive</option> and
+          <option>--console=read-only</option> modes.</para>
+
+          <xi:include href="version-info.xml" xpointer="v256"/>
+          </listitem>
+        </varlistentry>
+      </variablelist>
+    </refsect2>
+
     <refsect2>
       <title>Credentials</title>
 
index 2cec3d82cf0b140d06e164d043e7d58c3b54cd7d..b6b0f40ca6f378c62761f1f22f652f836c2f63e8 100644 (file)
@@ -77,6 +77,7 @@ const char *special_glyph_full(SpecialGlyph code, bool force_utf) {
                         [SPECIAL_GLYPH_RED_CIRCLE]              = "o",
                         [SPECIAL_GLYPH_YELLOW_CIRCLE]           = "o",
                         [SPECIAL_GLYPH_BLUE_CIRCLE]             = "o",
+                        [SPECIAL_GLYPH_GREEN_CIRCLE]            = "o",
                 },
 
                 /* UTF-8 */
@@ -143,6 +144,7 @@ const char *special_glyph_full(SpecialGlyph code, bool force_utf) {
                         [SPECIAL_GLYPH_RED_CIRCLE]              = u8"🔴",
                         [SPECIAL_GLYPH_YELLOW_CIRCLE]           = u8"🟡",
                         [SPECIAL_GLYPH_BLUE_CIRCLE]             = u8"🔵",
+                        [SPECIAL_GLYPH_GREEN_CIRCLE]            = u8"🟢",
                 },
         };
 
index e476fefe943c5bbf1c575da25d6fa0b46785a209..2f70b187fcd7062eacf1f329604c84e6381c18f6 100644 (file)
@@ -52,6 +52,7 @@ typedef enum SpecialGlyph {
         SPECIAL_GLYPH_RED_CIRCLE,
         SPECIAL_GLYPH_YELLOW_CIRCLE,
         SPECIAL_GLYPH_BLUE_CIRCLE,
+        SPECIAL_GLYPH_GREEN_CIRCLE,
         _SPECIAL_GLYPH_MAX,
         _SPECIAL_GLYPH_INVALID = -EINVAL,
 } SpecialGlyph;
index dd9a8134bf2e5ca0730c14e6659acac0be83946c..67d9c7e65cdfd786eac8267c284958cd9864f6d4 100644 (file)
@@ -82,7 +82,7 @@ TEST(keymaps) {
 
 #define dump_glyph(x) log_info(STRINGIFY(x) ": %s", special_glyph(x))
 TEST(dump_special_glyphs) {
-        assert_cc(SPECIAL_GLYPH_BLUE_CIRCLE + 1 == _SPECIAL_GLYPH_MAX);
+        assert_cc(SPECIAL_GLYPH_GREEN_CIRCLE + 1 == _SPECIAL_GLYPH_MAX);
 
         log_info("is_locale_utf8: %s", yes_no(is_locale_utf8()));
 
@@ -130,6 +130,7 @@ TEST(dump_special_glyphs) {
         dump_glyph(SPECIAL_GLYPH_RED_CIRCLE);
         dump_glyph(SPECIAL_GLYPH_YELLOW_CIRCLE);
         dump_glyph(SPECIAL_GLYPH_BLUE_CIRCLE);
+        dump_glyph(SPECIAL_GLYPH_GREEN_CIRCLE);
 }
 
 DEFINE_TEST_MAIN(LOG_INFO);
index cb1a463781b6dcb3744aa2cc6f255f9d7d4f7714..780df553aac3d99a3aba97981a93afde5deda008 100644 (file)
@@ -1,3 +1,13 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
+#include "string-table.h"
 #include "vmspawn-settings.h"
+
+static const char *const console_mode_table[_CONSOLE_MODE_MAX] = {
+        [CONSOLE_INTERACTIVE] = "interactive",
+        [CONSOLE_READ_ONLY]   = "read-only",
+        [CONSOLE_NATIVE]      = "native",
+        [CONSOLE_GUI]         = "gui",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(console_mode, ConsoleMode);
index 60ea10e6de01ef45d17bc79989efce3f35a4094c..fe23aa23cf7bc3b47de42b5d072b1ca993e17e24 100644 (file)
@@ -1,8 +1,20 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 #pragma once
 
+#include <errno.h>
 #include <stdint.h>
 
+#include "macro.h"
+
+typedef enum ConsoleMode {
+        CONSOLE_INTERACTIVE,    /* ptyfwd */
+        CONSOLE_READ_ONLY,      /* ptyfwd, but in read-only mode */
+        CONSOLE_NATIVE,         /* qemu's native TTY handling */
+        CONSOLE_GUI,            /* qemu's graphical UI */
+        _CONSOLE_MODE_MAX,
+        _CONSOLE_MODE_INVALID = -EINVAL,
+} ConsoleMode;
+
 typedef enum SettingsMask {
         SETTING_START_MODE        = UINT64_C(1) << 0,
         SETTING_BIND_MOUNTS       = UINT64_C(1) << 11,
@@ -10,3 +22,6 @@ typedef enum SettingsMask {
         SETTING_CREDENTIALS       = UINT64_C(1) << 30,
         _SETTING_FORCE_ENUM_WIDTH = UINT64_MAX
 } SettingsMask;
+
+const char *console_mode_to_string(ConsoleMode m) _const_;
+ConsoleMode console_mode_from_string(const char *s) _pure_;
index 6c2d943daa9dc4c0b58a090c07bd5f3480cce403..ce7f1ef2e3ee8f8ae08d816d62d5358c10fcc70a 100644 (file)
@@ -46,6 +46,7 @@
 #include "path-util.h"
 #include "pretty-print.h"
 #include "process-util.h"
+#include "ptyfwd.h"
 #include "random-util.h"
 #include "rm-rf.h"
 #include "signal-util.h"
@@ -73,7 +74,7 @@ static unsigned arg_vsock_cid = VMADDR_CID_ANY;
 static int arg_tpm = -1;
 static char *arg_linux = NULL;
 static char **arg_initrds = NULL;
-static bool arg_qemu_gui = false;
+static ConsoleMode arg_console_mode = CONSOLE_INTERACTIVE;
 static NetworkStack arg_network_stack = NETWORK_STACK_NONE;
 static int arg_secure_boot = -1;
 static MachineCredentialContext arg_credentials = {};
@@ -87,6 +88,7 @@ static bool arg_runtime_directory_created = false;
 static bool arg_privileged = false;
 static char **arg_kernel_cmdline_extra = NULL;
 static char **arg_extra_drives = NULL;
+static char *arg_background = NULL;
 
 STATIC_DESTRUCTOR_REGISTER(arg_directory, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
@@ -101,6 +103,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_runtime_mounts, runtime_mount_context_done);
 STATIC_DESTRUCTOR_REGISTER(arg_forward_journal, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_kernel_cmdline_extra, strv_freep);
 STATIC_DESTRUCTOR_REGISTER(arg_extra_drives, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_background, freep);
 
 static int help(void) {
         _cleanup_free_ char *link = NULL;
@@ -130,7 +133,6 @@ static int help(void) {
                "     --tpm=BOOL            Enable use of a virtual TPM\n"
                "     --linux=PATH          Specify the linux kernel for direct kernel boot\n"
                "     --initrd=PATH         Specify the initrd for direct kernel boot\n"
-               "     --qemu-gui            Start QEMU in graphical mode\n"
                "  -n --network-tap         Create a TAP device for networking\n"
                "     --network-user-mode   Use user mode networking\n"
                "     --secure-boot=BOOL    Enable searching for firmware supporting SecureBoot\n"
@@ -150,6 +152,9 @@ static int help(void) {
                "\n%3$sIntegration:%4$s\n"
                "     --forward-journal=FILE|DIR\n"
                "                           Forward the VM's journal to the host\n"
+               "\n%3$sInput/Output:%4$s\n"
+               "     --console=MODE        Console mode (interactive, native, gui)\n"
+               "     --background=COLOR    Set ANSI color for background\n"
                "\n%3$sCredentials:%4$s\n"
                "     --set-credential=ID:VALUE\n"
                "                           Pass a credential with literal value to the VM\n"
@@ -190,6 +195,8 @@ static int parse_argv(int argc, char *argv[]) {
                 ARG_SET_CREDENTIAL,
                 ARG_LOAD_CREDENTIAL,
                 ARG_FIRMWARE,
+                ARG_CONSOLE,
+                ARG_BACKGROUND,
         };
 
         static const struct option options[] = {
@@ -212,7 +219,8 @@ static int parse_argv(int argc, char *argv[]) {
                 { "tpm",               required_argument, NULL, ARG_TPM               },
                 { "linux",             required_argument, NULL, ARG_LINUX             },
                 { "initrd",            required_argument, NULL, ARG_INITRD            },
-                { "qemu-gui",          no_argument,       NULL, ARG_QEMU_GUI          },
+                { "console",           required_argument, NULL, ARG_CONSOLE           },
+                { "qemu-gui",          no_argument,       NULL, ARG_QEMU_GUI          }, /* compat option */
                 { "network-tap",       no_argument,       NULL, 'n'                   },
                 { "network-user-mode", no_argument,       NULL, ARG_NETWORK_USER_MODE },
                 { "bind",              required_argument, NULL, ARG_BIND              },
@@ -224,6 +232,7 @@ static int parse_argv(int argc, char *argv[]) {
                 { "set-credential",    required_argument, NULL, ARG_SET_CREDENTIAL    },
                 { "load-credential",   required_argument, NULL, ARG_LOAD_CREDENTIAL   },
                 { "firmware",          required_argument, NULL, ARG_FIRMWARE          },
+                { "background",        required_argument, NULL, ARG_BACKGROUND        },
                 {}
         };
 
@@ -344,8 +353,15 @@ static int parse_argv(int argc, char *argv[]) {
                         break;
                 }
 
+                case ARG_CONSOLE:
+                        arg_console_mode = console_mode_from_string(optarg);
+                        if (arg_console_mode < 0)
+                                return log_error_errno(arg_console_mode, "Failed to parse specified console mode: %s", optarg);
+
+                        break;
+
                 case ARG_QEMU_GUI:
-                        arg_qemu_gui = true;
+                        arg_console_mode = CONSOLE_GUI;
                         break;
 
                 case 'n':
@@ -438,6 +454,12 @@ static int parse_argv(int argc, char *argv[]) {
 
                         break;
 
+                case ARG_BACKGROUND:
+                        r = free_and_strdup_warn(&arg_background, optarg);
+                        if (r < 0)
+                                return r;
+                        break;
+
                 case '?':
                         return -EINVAL;
 
@@ -1030,6 +1052,25 @@ static int merge_initrds(char **ret) {
         return 0;
 }
 
+static void set_window_title(PTYForward *f) {
+        _cleanup_free_ char *hn = NULL, *dot = NULL;
+
+        assert(f);
+
+        (void) gethostname_strict(&hn);
+
+        if (emoji_enabled())
+                dot = strjoin(special_glyph(SPECIAL_GLYPH_GREEN_CIRCLE), " ");
+
+        if (hn)
+                (void) pty_forward_set_titlef(f, "%sVirtual Machine %s on %s", strempty(dot), arg_machine, hn);
+        else
+                (void) pty_forward_set_titlef(f, "%sVirtual Machine %s", strempty(dot), arg_machine);
+
+        if (dot)
+                (void) pty_forward_set_title_prefix(f, dot);
+}
+
 static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
         _cleanup_(ovmf_config_freep) OvmfConfig *ovmf_config = NULL;
         _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
@@ -1222,12 +1263,54 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
         if (r < 0)
                 return log_oom();
 
-        if (arg_qemu_gui)
+        _cleanup_close_ int master = -EBADF;
+        PTYForwardFlags ptyfwd_flags = 0;
+        switch (arg_console_mode) {
+
+        case CONSOLE_READ_ONLY:
+                ptyfwd_flags |= PTY_FORWARD_READ_ONLY;
+
+                _fallthrough_;
+
+        case CONSOLE_INTERACTIVE:  {
+                _cleanup_free_ char *pty_path = NULL;
+
+                master = posix_openpt(O_RDWR|O_NOCTTY|O_CLOEXEC|O_NONBLOCK);
+                if (master < 0)
+                        return log_error_errno(errno, "Failed to acquire pseudo tty: %m");
+
+                r = ptsname_malloc(master, &pty_path);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to determine tty name: %m");
+
+                if (unlockpt(master) < 0)
+                        return log_error_errno(errno, "Failed to unlock tty: %m");
+
+                if (strv_extend_many(
+                                &cmdline,
+                                "-nographic",
+                                "-nodefaults",
+                                "-chardev") < 0)
+                        return log_oom();
+
+                if (strv_extendf(&cmdline,
+                                 "serial,id=console,path=%s", pty_path) < 0)
+                        return log_oom();
+
+                r = strv_extend_many(
+                                &cmdline,
+                                "-serial", "chardev:console");
+                break;
+        }
+
+        case CONSOLE_GUI:
                 r = strv_extend_many(
                                 &cmdline,
                                 "-vga",
                                 "virtio");
-        else
+                break;
+
+        case CONSOLE_NATIVE:
                 r = strv_extend_many(
                                 &cmdline,
                                 "-nographic",
@@ -1235,6 +1318,11 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                                 "-chardev", "stdio,mux=on,id=console,signal=off",
                                 "-serial", "chardev:console",
                                 "-mon", "console");
+                break;
+
+        default:
+                assert_not_reached();
+        }
         if (r < 0)
                 return log_oom();
 
@@ -1583,7 +1671,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                 log_debug("Executing: %s", joined);
         }
 
-        assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD) >= 0);
+        assert_se(sigprocmask_many(SIG_BLOCK, /* old_sigset=*/ NULL, SIGCHLD, SIGWINCH) >= 0);
 
         _cleanup_(sd_event_source_unrefp) sd_event_source *notify_event_source = NULL;
         _cleanup_(sd_event_unrefp) sd_event *event = NULL;
@@ -1635,6 +1723,26 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
         /* Exit when the child exits */
         (void) event_add_child_pidref(event, NULL, &child_pidref, WEXITED, on_child_exit, NULL);
 
+        _cleanup_(pty_forward_freep) PTYForward *forward = NULL;
+        if (master >= 0) {
+                r = pty_forward_new(event, master, ptyfwd_flags, &forward);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to create PTY forwarder: %m");
+
+                if (!arg_background) {
+                        _cleanup_free_ char *bg = NULL;
+
+                        r = terminal_tint_color(130 /* green */, &bg);
+                        if (r < 0)
+                                log_debug_errno(r, "Failed to determine terminal background color, not tinting.");
+                        else
+                                (void) pty_forward_set_background_color(forward, bg);
+                } else if (!isempty(arg_background))
+                        (void) pty_forward_set_background_color(forward, arg_background);
+
+                set_window_title(forward);
+        }
+
         r = sd_event_loop(event);
         if (r < 0)
                 return log_error_errno(r, "Failed to run event loop: %m");
@@ -1740,15 +1848,20 @@ static int run(int argc, char *argv[]) {
         if (r < 0)
                 return r;
 
-        if (!arg_quiet) {
+        if (!arg_quiet && arg_console_mode != CONSOLE_GUI) {
                 _cleanup_free_ char *u = NULL;
                 const char *vm_path = arg_image ?: arg_directory;
                 (void) terminal_urlify_path(vm_path, vm_path, &u);
 
-                log_info("%s %sSpawning VM %s on %s.%s\n"
-                         "%s %sPress %sCtrl-a x%s to kill VM.%s",
-                         special_glyph(SPECIAL_GLYPH_LIGHT_SHADE), ansi_grey(), arg_machine, u ?: vm_path, ansi_normal(),
-                         special_glyph(SPECIAL_GLYPH_LIGHT_SHADE), ansi_grey(), ansi_highlight(), ansi_grey(), ansi_normal());
+                log_info("%s %sSpawning VM %s on %s.%s",
+                         special_glyph(SPECIAL_GLYPH_LIGHT_SHADE), ansi_grey(), arg_machine, u ?: vm_path, ansi_normal());
+
+                if (arg_console_mode == CONSOLE_INTERACTIVE)
+                        log_info("%s %sPress %sCtrl-]%s three times within 1s to kill VM.%s",
+                                 special_glyph(SPECIAL_GLYPH_LIGHT_SHADE), ansi_grey(), ansi_highlight(), ansi_grey(), ansi_normal());
+                else if (arg_console_mode == CONSOLE_NATIVE)
+                        log_info("%s %sPress %sCtrl-a x%s to kill VM.%s",
+                                 special_glyph(SPECIAL_GLYPH_LIGHT_SHADE), ansi_grey(), ansi_highlight(), ansi_grey(), ansi_normal());
         }
 
         r = sd_listen_fds_with_names(true, &names);