1
0
mirror of https://github.com/systemd/systemd.git synced 2025-05-27 21:05:55 +03:00

vmspawn: use our own ptyfwd code for the console of a VM

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.
This commit is contained in:
Lennart Poettering 2024-02-23 12:20:55 +01:00
parent 2f7f08005b
commit 795ec90cda
7 changed files with 191 additions and 21 deletions

View File

@ -205,14 +205,6 @@
</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>
@ -361,6 +353,42 @@
</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>

View 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"🟢",
},
};

View 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;

View 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);

View 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);

View 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_;

View 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);