vmspawn: forward signals to VM PID 1 via D-BUS when available
authorSam Leonard <sam.leonard@codethink.co.uk>
Thu, 25 Apr 2024 16:16:20 +0000 (17:16 +0100)
committerLuca Boccassi <bluca@debian.org>
Wed, 8 May 2024 22:25:02 +0000 (23:25 +0100)
Allow to gracefully shutdown by initiating the operation from
the guest

src/vmspawn/vmspawn.c

index 51f622d0a8e30d9cea9279332c334c09571d850b..05661961ed096f5420ca2a8c17560de37349f77d 100644 (file)
@@ -18,6 +18,9 @@
 #include "architecture.h"
 #include "bootspec.h"
 #include "build.h"
+#include "bus-internal.h"
+#include "bus-locator.h"
+#include "bus-wait-for-jobs.h"
 #include "chase.h"
 #include "common-signal.h"
 #include "copy.h"
@@ -58,6 +61,7 @@
 #include "signal-util.h"
 #include "socket-util.h"
 #include "stat-util.h"
+#include "stdio-util.h"
 #include "string-util.h"
 #include "strv.h"
 #include "time-util.h"
 
 #define VM_TAP_HASH_KEY SD_ID128_MAKE(01,d0,c6,4c,2b,df,24,fb,c0,f8,b2,09,7d,59,b2,93)
 
+typedef struct SSHInfo {
+        unsigned cid;
+        char *private_key_path;
+        unsigned port;
+} SSHInfo;
+
 static bool arg_quiet = false;
 static PagerFlags arg_pager_flags = 0;
 static char *arg_directory = NULL;
@@ -697,11 +707,61 @@ static int setup_notify_parent(sd_event *event, int fd, int *exit_status, sd_eve
         return 0;
 }
 
+static int bus_open_in_machine(sd_bus **ret, unsigned cid, unsigned port, const char *private_key_path) {
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        _cleanup_free_ char *ssh_escaped = NULL, *bus_address = NULL;
+        char port_str[DECIMAL_STR_MAX(unsigned)], cid_str[DECIMAL_STR_MAX(unsigned)];
+        int r;
+
+        assert(ret);
+        assert(private_key_path);
+
+        r = sd_bus_new(&bus);
+        if (r < 0)
+                return r;
+
+        const char *ssh = secure_getenv("SYSTEMD_SSH") ?: "ssh";
+        ssh_escaped = bus_address_escape(ssh);
+        if (!ssh_escaped)
+                return -ENOMEM;
+
+        xsprintf(port_str, "%u", port);
+        xsprintf(cid_str, "%u", cid);
+
+        bus_address = strjoin(
+                "unixexec:path=", ssh_escaped,
+                /* -x: Disable X11 forwarding
+                 * -T: Disable PTY allocation */
+                ",argv1=-xT",
+                ",argv2=-o,argv3=IdentitiesOnly yes",
+                ",argv4=-o,argv5=IdentityFile=", private_key_path,
+                ",argv6=-p,argv7=", port_str,
+                ",argv8=--",
+                ",argv9=root@vsock/", cid_str,
+                ",argv10=systemd-stdio-bridge"
+        );
+        if (!bus_address)
+                return -ENOMEM;
+
+        free_and_replace(bus->address, bus_address);
+        bus->bus_client = true;
+        bus->trusted = true;
+        bus->runtime_scope = RUNTIME_SCOPE_SYSTEM;
+        bus->is_local = false;
+
+        r = sd_bus_start(bus);
+        if (r < 0)
+                return r;
+
+        *ret = TAKE_PTR(bus);
+        return 0;
+}
+
 static int on_orderly_shutdown(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
         PidRef *pidref = userdata;
         int r;
 
-        /* TODO: actually talk to qemu and ask the guest to shutdown here */
+        /* Backup method to shut down the VM when D-BUS access over SSH is not available */
 
         if (pidref) {
                 r = pidref_kill(pidref, SIGKILL);
@@ -718,6 +778,61 @@ static int on_orderly_shutdown(sd_event_source *s, const struct signalfd_siginfo
         return 0;
 }
 
+static int forward_signal_to_vm_pid1(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
+        _cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *w = NULL;
+        _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+        SSHInfo *ssh_info = ASSERT_PTR(userdata);
+        const char *vm_pid1;
+        int r;
+
+        assert(s);
+        assert(si);
+
+        r = bus_open_in_machine(&bus, ssh_info->cid, ssh_info->port, ssh_info->private_key_path);
+        if (r < 0)
+                return log_error_errno(r, "Failed to connect to VM to forward signal: %m");
+
+        r = bus_wait_for_jobs_new(bus, &w);
+        if (r < 0)
+                return log_error_errno(r, "Could not watch job: %m");
+
+        r = bus_call_method(
+                        bus,
+                        bus_systemd_mgr,
+                        "GetUnitByPID",
+                        &error,
+                        NULL,
+                        "");
+        if (r < 0)
+                return log_error_errno(r, "Failed to get init process of VM: %s", bus_error_message(&error, r));
+
+        r = sd_bus_message_read(reply, "o", &vm_pid1);
+        if (r < 0)
+                return bus_log_parse_error(r);
+
+        r = bus_wait_for_jobs_one(w, vm_pid1, /* quiet */ false, NULL);
+        if (r < 0)
+                return r;
+
+        r = bus_call_method(
+                        bus,
+                        bus_systemd_mgr,
+                        "KillUnit",
+                        &error,
+                        NULL,
+                        "ssi",
+                        vm_pid1,
+                        "leader",
+                        si->ssi_signo);
+        if (r < 0)
+                return log_error_errno(r, "Failed to forward signal to PID 1 of the VM: %s", bus_error_message(&error, r));
+        log_info("Sent signal %"PRIu32" to the VM's PID 1.", si->ssi_signo);
+
+        return 0;
+}
+
 static int on_child_exit(sd_event_source *s, const siginfo_t *si, void *userdata) {
         sd_event_exit(sd_event_source_get_event(s), 0);
         return 0;
@@ -1200,6 +1315,7 @@ static int generate_ssh_keypair(const char *key_path, const char *key_type) {
 }
 
 static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
+        SSHInfo ssh_info; /* Used when talking to pid1 via SSH, but must survive until the function ends. */
         _cleanup_(ovmf_config_freep) OvmfConfig *ovmf_config = NULL;
         _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
         _cleanup_free_ char *machine = NULL, *qemu_binary = NULL, *mem = NULL, *trans_scope = NULL, *kernel = NULL;
@@ -1985,9 +2101,21 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                         return log_error_errno(r, "Failed to setup event loop to handle VSOCK notify events: %m");
         }
 
-        /* shutdown qemu when we are shutdown */
-        (void) sd_event_add_signal(event, NULL, SIGINT | SD_EVENT_SIGNAL_PROCMASK, on_orderly_shutdown, &child_pidref);
-        (void) sd_event_add_signal(event, NULL, SIGTERM | SD_EVENT_SIGNAL_PROCMASK, on_orderly_shutdown, &child_pidref);
+        /* If we have the vsock address and the SSH key, ask pid1 inside the guest to shutdown. */
+        if (child_cid != VMADDR_CID_ANY && ssh_private_key_path) {
+                ssh_info = (SSHInfo) {
+                        .cid = child_cid,
+                        .private_key_path = ssh_private_key_path,
+                        .port = 22,
+                };
+
+                (void) sd_event_add_signal(event, NULL, SIGINT | SD_EVENT_SIGNAL_PROCMASK, forward_signal_to_vm_pid1, &ssh_info);
+                (void) sd_event_add_signal(event, NULL, SIGTERM | SD_EVENT_SIGNAL_PROCMASK, forward_signal_to_vm_pid1, &ssh_info);
+        } else {
+                /* As a fallback in case SSH cannot be used, send a shutdown signal to the VMM instead. */
+                (void) sd_event_add_signal(event, NULL, SIGINT | SD_EVENT_SIGNAL_PROCMASK, on_orderly_shutdown, &child_pidref);
+                (void) sd_event_add_signal(event, NULL, SIGTERM | SD_EVENT_SIGNAL_PROCMASK, on_orderly_shutdown, &child_pidref);
+        }
 
         (void) sd_event_add_signal(event, NULL, (SIGRTMIN+18) | SD_EVENT_SIGNAL_PROCMASK, sigrtmin18_handler, NULL);