]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
vmspawn: Add --console-transport= option to select serial vs virtio-serial
authorDaan De Meyer <daan@amutable.com>
Thu, 2 Apr 2026 09:57:46 +0000 (09:57 +0000)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 2 Apr 2026 18:43:42 +0000 (20:43 +0200)
Add a --console-transport= option that selects between virtio-serial
(the default, appearing as /dev/hvc0) and a regular serial port
(appearing as /dev/ttyS0 or /dev/ttyAMA0 depending on architecture).

This is primarily useful for testing purposes, for example to test
sd-stub's automatic console= kernel command line parameter handling. It
allows verifying that the guest OS correctly handles serial console
configurations without virtio.

When serial transport is selected, -serial chardev:console is used on
the QEMU command line to connect the chardev to the platform's default
serial device. This cannot be done via the QEMU config file as on some
platforms (e.g. ARM) the serial device is a sysbus device that can only
be connected via serial_hd() which is populated by -serial.

man/systemd-vmspawn.xml
src/vmspawn/vmspawn-settings.c
src/vmspawn/vmspawn-settings.h
src/vmspawn/vmspawn-util.h
src/vmspawn/vmspawn.c

index 129f5ba14199fad7ea6672af5d257cf0c9ee843d..9feb7407ca6ee20f0cb0ec2431fdb4bbc8cf8538 100644 (file)
           <xi:include href="version-info.xml" xpointer="v256"/></listitem>
         </varlistentry>
 
+        <varlistentry>
+          <term><option>--console-transport=<replaceable>TRANSPORT</replaceable></option></term>
+
+          <listitem><para>Configures the transport to use for the VM console. Takes one of
+          <literal>virtio</literal> or <literal>serial</literal>. Defaults to <literal>virtio</literal>.
+          <literal>virtio</literal> uses a virtio-serial device, which appears as
+          <filename>/dev/hvc0</filename> in the VM. <literal>serial</literal> uses a regular serial port,
+          which appears as <filename>/dev/ttyS0</filename> (or <filename>/dev/ttyAMA0</filename> on ARM) in the VM. This option only has an effect in
+          <option>--console=interactive</option>, <option>--console=read-only</option>, and
+          <option>--console=native</option> modes.</para>
+
+          <xi:include href="version-info.xml" xpointer="v262"/></listitem>
+        </varlistentry>
+
         <varlistentry>
           <term><option>--background=<replaceable>COLOR</replaceable></option></term>
 
index 776b590252ee56a5434b9146edc72112d34c3788..56a07b3f6f01dc611587d7f32edad1b868d386ac 100644 (file)
@@ -37,3 +37,10 @@ static const char *const console_mode_table[_CONSOLE_MODE_MAX] = {
 };
 
 DEFINE_STRING_TABLE_LOOKUP(console_mode, ConsoleMode);
+
+static const char *const console_transport_table[_CONSOLE_TRANSPORT_MAX] = {
+        [CONSOLE_TRANSPORT_VIRTIO] = "virtio",
+        [CONSOLE_TRANSPORT_SERIAL] = "serial",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(console_transport, ConsoleTransport);
index b897f148e131a0d5a0734664447a2234dc64ce37..83d28725359df863f6744099a039ea548d646900 100644 (file)
@@ -42,6 +42,13 @@ typedef enum ConsoleMode {
         _CONSOLE_MODE_INVALID = -EINVAL,
 } ConsoleMode;
 
+typedef enum ConsoleTransport {
+        CONSOLE_TRANSPORT_VIRTIO,       /* virtio-serial (hvc0) */
+        CONSOLE_TRANSPORT_SERIAL,       /* regular serial port (ttyS0/ttyAMA0) */
+        _CONSOLE_TRANSPORT_MAX,
+        _CONSOLE_TRANSPORT_INVALID = -EINVAL,
+} ConsoleTransport;
+
 typedef enum SettingsMask {
         SETTING_START_MODE        = UINT64_C(1) << 0,
         SETTING_MACHINE_ID        = UINT64_C(1) << 6,
@@ -53,5 +60,6 @@ typedef enum SettingsMask {
 } SettingsMask;
 
 DECLARE_STRING_TABLE_LOOKUP(console_mode, ConsoleMode);
+DECLARE_STRING_TABLE_LOOKUP(console_transport, ConsoleTransport);
 DECLARE_STRING_TABLE_LOOKUP(disk_type, DiskType);
 DECLARE_STRING_TABLE_LOOKUP(image_format, ImageFormat);
index 90efd936612240796f8de6af05ea29b075227c74..9fec6641aa3d0d75327c80de56e6fd0242a06e57 100644 (file)
 #  define QEMU_MACHINE_TYPE "none"
 #endif
 
+#if defined(__arm__) || defined(__aarch64__)
+#  define QEMU_SERIAL_CONSOLE_NAME "ttyAMA0"
+#else
+#  define QEMU_SERIAL_CONSOLE_NAME "ttyS0"
+#endif
+
 typedef struct OvmfConfig {
         char *path;
         char *format;
index 1891ac8bad742aadf0b3e00081ee8b7f54e27fa8..85165e1af7a98ef707ba2c5d2baddacb7b45d9a1 100644 (file)
@@ -137,6 +137,7 @@ static int arg_tpm = -1;
 static char *arg_linux = NULL;
 static char **arg_initrds = NULL;
 static ConsoleMode arg_console_mode = CONSOLE_INTERACTIVE;
+static ConsoleTransport arg_console_transport = CONSOLE_TRANSPORT_VIRTIO;
 static NetworkStack arg_network_stack = NETWORK_STACK_NONE;
 static MachineCredentialContext arg_credentials = {};
 static uid_t arg_uid_shift = UID_INVALID, arg_uid_range = 0x10000U;
@@ -291,6 +292,8 @@ static int help(void) {
                "\n%3$sInput/Output:%4$s\n"
                "     --console=MODE        Console mode (interactive, native, gui, read-only\n"
                "                           or headless)\n"
+               "     --console-transport=TRANSPORT\n"
+               "                           Console transport (virtio or serial)\n"
                "     --background=COLOR    Set ANSI color for background\n"
                "\n%3$sCredentials:%4$s\n"
                "     --set-credential=ID:VALUE\n"
@@ -375,6 +378,7 @@ static int parse_argv(int argc, char *argv[]) {
                 ARG_USER,
                 ARG_IMAGE_FORMAT,
                 ARG_IMAGE_DISK_TYPE,
+                ARG_CONSOLE_TRANSPORT,
         };
 
         static const struct option options[] = {
@@ -402,6 +406,7 @@ static int parse_argv(int argc, char *argv[]) {
                 { "linux",             required_argument, NULL, ARG_LINUX             },
                 { "initrd",            required_argument, NULL, ARG_INITRD            },
                 { "console",           required_argument, NULL, ARG_CONSOLE           },
+                { "console-transport", required_argument, NULL, ARG_CONSOLE_TRANSPORT },
                 { "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 },
@@ -578,6 +583,13 @@ static int parse_argv(int argc, char *argv[]) {
 
                         break;
 
+                case ARG_CONSOLE_TRANSPORT:
+                        arg_console_transport = console_transport_from_string(optarg);
+                        if (arg_console_transport < 0)
+                                return log_error_errno(arg_console_transport, "Failed to parse specified console transport: %s", optarg);
+
+                        break;
+
                 case ARG_QEMU_GUI:
                         arg_console_mode = CONSOLE_GUI;
                         break;
@@ -2588,11 +2600,6 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                 if (r < 0)
                         return log_oom();
 
-                r = qemu_config_section(config_file, "device", "vmspawn-virtio-serial-pci",
-                                        "driver", "virtio-serial-pci");
-                if (r < 0)
-                        return r;
-
                 /* Enable mux for native console so the QEMU monitor is accessible via Ctrl-a c */
                 r = qemu_config_section(config_file, "chardev", "console",
                                         "backend", "serial",
@@ -2601,12 +2608,6 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                 if (r < 0)
                         return r;
 
-                r = qemu_config_section(config_file, "device", "virtconsole0",
-                                        "driver", "virtconsole",
-                                        "chardev", "console");
-                if (r < 0)
-                        return r;
-
                 if (arg_console_mode == CONSOLE_NATIVE) {
                         r = qemu_config_section(config_file, "mon", "mon0",
                                                 "chardev", "console");
@@ -2655,6 +2656,29 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                 assert_not_reached();
         }
 
+        if (!IN_SET(arg_console_mode, CONSOLE_GUI, CONSOLE_HEADLESS)) {
+                if (arg_console_transport == CONSOLE_TRANSPORT_SERIAL) {
+                        /* Use -serial to connect the chardev to the platform's default serial
+                         * device (e.g. isa-serial on x86, PL011 on ARM). On some platforms the
+                         * serial device is a sysbus device that can only be connected via
+                         * serial_hd() which is populated by -serial, not via the config file. */
+                        r = strv_extend_many(&cmdline, "-serial", "chardev:console");
+                        if (r < 0)
+                                return log_oom();
+                } else {
+                        r = qemu_config_section(config_file, "device", "vmspawn-virtio-serial-pci",
+                                                "driver", "virtio-serial-pci");
+                        if (r < 0)
+                                return r;
+
+                        r = qemu_config_section(config_file, "device", "virtconsole0",
+                                                "driver", "virtconsole",
+                                                "chardev", "console");
+                        if (r < 0)
+                                return r;
+                }
+        }
+
         r = qemu_config_section(config_file, "drive", "ovmf-code",
                                 "if", "pflash",
                                 "format", ovmf_config_format(ovmf_config),
@@ -3028,7 +3052,9 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
         }
 
         if (!IN_SET(arg_console_mode, CONSOLE_GUI, CONSOLE_HEADLESS)) {
-                r = strv_prepend(&arg_kernel_cmdline_extra, "console=hvc0");
+                r = strv_prepend(&arg_kernel_cmdline_extra,
+                                 arg_console_transport == CONSOLE_TRANSPORT_SERIAL ?
+                                 "console=" QEMU_SERIAL_CONSOLE_NAME : "console=hvc0");
                 if (r < 0)
                         return log_oom();