qemu-devel.nongnu.org archive mirror
 help / color / mirror / Atom feed
* [PATCH] qga: Add an interactive mode to guest-exec via VSOCK for Linux
@ 2024-05-22 15:06 Alexander Ivanov
  2024-05-22 15:23 ` Test scripts Alexander Ivanov
                   ` (2 more replies)
  0 siblings, 3 replies; 8+ messages in thread
From: Alexander Ivanov @ 2024-05-22 15:06 UTC (permalink / raw)
  To: qemu-devel; +Cc: den, michael.roth, kkostiuk, marcandre.lureau

Add an interactive mode to the guest-exec command in the QEMU Guest Agent
using the VSOCK communication mechanism. It enables interactive sessions
with the executed command in the guest, allowing real-time input/output.

Introduce "interactive" mode in the GuestExecCaptureOutputMode enumeration
and add optional "cid" and "port" fields to the guest-exec response. In
such a way user can execute guest-exec command, get CID and port number
from the response and connect to the guest server. After connection user
can communicate with the started process. All the data transmitted to the
server is redirected to stdin. Data from stdout and stderr is redirected
to the client. All data blocks are preceded by 32-bit headers (network
byte order): most significant bit contains a sign of stream (stdout - 0,
stderr - 1), all the other bits contain the payload size.

Signed-off-by: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
---
 qga/commands.c       | 272 +++++++++++++++++++++++++++++++++++++++++--
 qga/qapi-schema.json |  11 +-
 2 files changed, 273 insertions(+), 10 deletions(-)

diff --git a/qga/commands.c b/qga/commands.c
index 88c1c99fe5..377a79c816 100644
--- a/qga/commands.c
+++ b/qga/commands.c
@@ -20,6 +20,11 @@
 #include "qemu/cutils.h"
 #include "commands-common.h"
 
+#ifdef CONFIG_LINUX
+#include <sys/ioctl.h>
+#include <linux/vm_sockets.h>
+#endif
+
 /* Maximum captured guest-exec out_data/err_data - 16MB */
 #define GUEST_EXEC_MAX_OUTPUT (16 * 1024 * 1024)
 /* Allocation and I/O buffer for reading guest-exec out_data/err_data - 4KB */
@@ -92,6 +97,27 @@ struct GuestExecIOData {
 };
 typedef struct GuestExecIOData GuestExecIOData;
 
+#define GE_INT_IO_SIZE (256 * 1024)
+#define GE_INT_STREAM_MASK 0x80000000
+
+struct GEIntPacket {
+    uint32_t header;
+    gchar buf[GE_INT_IO_SIZE];
+} __attribute__((aligned(1)));
+typedef struct GEIntPacket GEIntPacket;
+
+struct GEIntData {
+    unsigned int cid;
+    unsigned int port;
+    GIOChannel *ch_srv;
+    GIOChannel *ch_clt;
+    GIOChannel *ch_in;
+    GIOChannel *ch_out;
+    GIOChannel *ch_err;
+    GEIntPacket packet;
+};
+typedef struct GEIntData GEIntData;
+
 struct GuestExecInfo {
     GPid pid;
     int64_t pid_numeric;
@@ -101,6 +127,7 @@ struct GuestExecInfo {
     GuestExecIOData in;
     GuestExecIOData out;
     GuestExecIOData err;
+    GEIntData *int_data;
     QTAILQ_ENTRY(GuestExecInfo) next;
 };
 typedef struct GuestExecInfo GuestExecInfo;
@@ -257,6 +284,194 @@ static char **guest_exec_get_args(const strList *entry, bool log)
     return args;
 }
 
+#ifdef CONFIG_LINUX
+static void guest_exec_close_channel(GIOChannel *ch)
+{
+    g_io_channel_shutdown(ch, true, NULL);
+    g_io_channel_unref(ch);
+}
+
+static void guest_exec_interactive_cleanup(GuestExecInfo *gei)
+{
+    GEIntData *data = gei->int_data;
+
+    guest_exec_close_channel(data->ch_clt);
+    guest_exec_close_channel(data->ch_srv);
+    guest_exec_close_channel(data->ch_in);
+    guest_exec_close_channel(data->ch_out);
+    guest_exec_close_channel(data->ch_err);
+
+    g_free(data);
+    gei->int_data = NULL;
+}
+
+static gboolean guest_exec_interactive_watch(GIOChannel *ch, GIOCondition cond,
+                                             gpointer data_)
+{
+    GuestExecInfo *gei = (GuestExecInfo *)data_;
+    GEIntData *data = gei->int_data;
+    gsize size, bytes_written;
+    GIOStatus gstatus;
+    GError *gerr = NULL;
+    GIOChannel *dst_ch;
+    gchar *p;
+
+    if (data == NULL) {
+        return false;
+    }
+
+    if (cond == G_IO_HUP || cond == G_IO_ERR) {
+        goto close;
+    }
+
+    gstatus = g_io_channel_read_chars(ch, data->packet.buf,
+                                      sizeof(data->packet.buf), &size, NULL);
+
+    if (gstatus == G_IO_STATUS_EOF || gstatus == G_IO_STATUS_ERROR) {
+        if (gerr) {
+            g_warning("qga: i/o error reading from a channel: %s",
+                      gerr->message);
+            g_error_free(gerr);
+        }
+        goto close;
+    }
+
+    if (ch == data->ch_clt) {
+        dst_ch = data->ch_in;
+        p = data->packet.buf;
+    } else {
+        assert(size < GE_INT_STREAM_MASK);
+
+        dst_ch = data->ch_clt;
+        p = (gchar *)&(data->packet);
+        data->packet.header = htonl(size);
+        if (ch == data->ch_err) {
+            data->packet.header |= htonl(GE_INT_STREAM_MASK);
+        }
+        size += sizeof(data->packet.header);
+    }
+
+    do {
+        gstatus = g_io_channel_write_chars(dst_ch, p, size,
+                                           &bytes_written, &gerr);
+
+        if (gstatus == G_IO_STATUS_EOF || gstatus == G_IO_STATUS_ERROR) {
+            if (gerr) {
+                g_warning("qga: i/o error writing to a channel: %s",
+                          gerr->message);
+                g_error_free(gerr);
+            }
+            goto close;
+        }
+        size -= bytes_written;
+        p += bytes_written;
+    } while (size > 0);
+
+    return true;
+
+close:
+    guest_exec_interactive_cleanup(gei);
+    return false;
+}
+
+static gboolean
+guest_exec_interactive_accept_watch(GIOChannel *ch, GIOCondition cond,
+                                    gpointer data_)
+{
+    GuestExecInfo *gei = (GuestExecInfo *)data_;
+    GEIntData *data = gei->int_data;
+    int fd;
+
+    if (cond == G_IO_HUP || cond == G_IO_ERR) {
+        goto close;
+    }
+
+    fd = accept(g_io_channel_unix_get_fd(ch), NULL, NULL);
+    if (fd < 0) {
+        goto close;
+    }
+
+    data->ch_clt = g_io_channel_unix_new(fd);
+    g_io_channel_set_encoding(data->ch_clt, NULL, NULL);
+    g_io_channel_set_buffered(data->ch_clt, false);
+    g_io_channel_set_close_on_unref(data->ch_clt, true);
+
+    g_io_add_watch(data->ch_clt, G_IO_IN | G_IO_HUP,
+                   guest_exec_interactive_watch, gei);
+    g_io_add_watch(data->ch_out, G_IO_IN | G_IO_HUP,
+                   guest_exec_interactive_watch, gei);
+    g_io_add_watch(data->ch_err, G_IO_IN | G_IO_HUP,
+                   guest_exec_interactive_watch, gei);
+    return false;
+
+close:
+    guest_exec_interactive_cleanup(gei);
+    return false;
+}
+
+static int get_cid(unsigned int *cid)
+{
+    int fd, ret;
+    fd = open("/dev/vsock", O_RDONLY);
+    if (fd == -1) {
+        return errno;
+    }
+    ret = ioctl(fd, IOCTL_VM_SOCKETS_GET_LOCAL_CID, cid);
+    close(fd);
+    return ret;
+}
+
+static int guest_exec_interactive_listen(GuestExecInfo *gei)
+{
+    struct sockaddr_vm server_addr;
+    socklen_t len;
+    int fd, res;
+    GEIntData *data = (GEIntData *)gei->int_data;
+
+    if (get_cid(&data->cid) != 0) {
+        slog("Can't get CID: %s", strerror(errno));
+        return -1;
+    }
+
+    fd = socket(AF_VSOCK, SOCK_STREAM, 0);
+    if (fd == -1) {
+        slog("Socket creation error: %s", strerror(errno));
+        return -1;
+    }
+
+    memset(&server_addr, 0, sizeof(server_addr));
+    server_addr.svm_family = AF_VSOCK;
+    server_addr.svm_port = VMADDR_PORT_ANY;
+    server_addr.svm_cid = VMADDR_CID_ANY;
+
+    if (bind(fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
+        slog("Bind error: %s", strerror(errno));
+        goto err;
+    }
+
+    len = sizeof(struct sockaddr_vm);
+    res = getsockname(fd, (struct sockaddr *)&server_addr, &len);
+    if (res == -1) {
+        slog("Can't get port: %s", strerror(errno));
+        goto err;
+    }
+
+    if (listen(fd, 1) == -1) {
+        slog("Can't listen port %d: %s", server_addr.svm_port, strerror(errno));
+        goto err;
+    }
+
+    data->port = server_addr.svm_port;
+    data->ch_srv = g_io_channel_unix_new(fd);
+    g_io_add_watch(data->ch_srv, G_IO_IN | G_IO_HUP,
+                   guest_exec_interactive_accept_watch, gei);
+    return 0;
+err:
+    close(fd);
+    return -1;
+}
+#endif
+
 static void guest_exec_child_watch(GPid pid, gint status, gpointer data)
 {
     GuestExecInfo *gei = (GuestExecInfo *)data;
@@ -424,6 +639,8 @@ GuestExec *qmp_guest_exec(const char *path,
     GSpawnFlags flags;
     bool has_output = false;
     bool has_merge = false;
+    bool interactive = false;
+
     GuestExecCaptureOutputMode output_mode;
     g_autofree uint8_t *input = NULL;
     size_t ninput = 0;
@@ -465,6 +682,11 @@ GuestExec *qmp_guest_exec(const char *path,
         has_output = true;
         has_merge = true;
         break;
+#endif
+#ifdef CONFIG_LINUX
+    case GUEST_EXEC_CAPTURE_OUTPUT_MODE_INTERACTIVE:
+        interactive = true;
+        break;
 #endif
     case GUEST_EXEC_CAPTURE_OUTPUT_MODE__MAX:
         /* Silence warning; impossible branch */
@@ -472,8 +694,10 @@ GuestExec *qmp_guest_exec(const char *path,
     }
 
     ret = g_spawn_async_with_pipes(NULL, argv, envp, flags,
-            guest_exec_task_setup, &has_merge, &pid, input_data ? &in_fd : NULL,
-            has_output ? &out_fd : NULL, has_output ? &err_fd : NULL, &gerr);
+            guest_exec_task_setup, &has_merge, &pid,
+            (input_data || interactive) ? &in_fd : NULL,
+            (has_output || interactive) ? &out_fd : NULL,
+            (has_output || interactive) ? &err_fd : NULL, &gerr);
     if (!ret) {
         error_setg(errp, QERR_QGA_COMMAND_FAILED, gerr->message);
         g_error_free(gerr);
@@ -485,9 +709,14 @@ GuestExec *qmp_guest_exec(const char *path,
 
     gei = guest_exec_info_add(pid);
     gei->has_output = has_output;
+
     g_child_watch_add(pid, guest_exec_child_watch, gei);
 
-    if (input_data) {
+    if (interactive) {
+        gei->int_data = g_malloc0(sizeof(GEIntData));
+    }
+
+    if (input_data || interactive) {
         gei->in.data = g_steal_pointer(&input);
         gei->in.size = ninput;
 #ifdef G_OS_WIN32
@@ -499,10 +728,14 @@ GuestExec *qmp_guest_exec(const char *path,
         g_io_channel_set_buffered(in_ch, false);
         g_io_channel_set_flags(in_ch, G_IO_FLAG_NONBLOCK, NULL);
         g_io_channel_set_close_on_unref(in_ch, true);
-        g_io_add_watch(in_ch, G_IO_OUT, guest_exec_input_watch, &gei->in);
+        if (interactive) {
+            gei->int_data->ch_in = in_ch;
+        } else {
+            g_io_add_watch(in_ch, G_IO_OUT, guest_exec_input_watch, &gei->in);
+        }
     }
 
-    if (has_output) {
+    if (has_output || interactive) {
 #ifdef G_OS_WIN32
         out_ch = g_io_channel_win32_new_fd(out_fd);
         err_ch = g_io_channel_win32_new_fd(err_fd);
@@ -516,12 +749,33 @@ GuestExec *qmp_guest_exec(const char *path,
         g_io_channel_set_buffered(err_ch, false);
         g_io_channel_set_close_on_unref(out_ch, true);
         g_io_channel_set_close_on_unref(err_ch, true);
-        g_io_add_watch(out_ch, G_IO_IN | G_IO_HUP,
-                guest_exec_output_watch, &gei->out);
-        g_io_add_watch(err_ch, G_IO_IN | G_IO_HUP,
-                guest_exec_output_watch, &gei->err);
+
+        if (interactive) {
+            gei->int_data->ch_out = out_ch;
+            gei->int_data->ch_err = err_ch;
+        } else {
+            g_io_add_watch(out_ch, G_IO_IN | G_IO_HUP,
+                           guest_exec_output_watch, &gei->out);
+            g_io_add_watch(err_ch, G_IO_IN | G_IO_HUP,
+                           guest_exec_output_watch, &gei->err);
+        }
     }
 
+#ifdef CONFIG_LINUX
+    if (interactive) {
+        if (guest_exec_interactive_listen(gei) != 0) {
+            QTAILQ_REMOVE(&guest_exec_state.processes, gei, next);
+            g_free(gei->int_data);
+            g_free(gei);
+            goto done;
+        }
+        ge->has_cid = true;
+        ge->cid = gei->int_data->cid;
+        ge->has_port = true;
+        ge->port = gei->int_data->port;
+    }
+#endif
+
 done:
     g_free(argv);
     g_free(envp);
diff --git a/qga/qapi-schema.json b/qga/qapi-schema.json
index d5af155007..77efa847af 100644
--- a/qga/qapi-schema.json
+++ b/qga/qapi-schema.json
@@ -1262,10 +1262,16 @@
 #
 # @pid: pid of child process in guest OS
 #
+# @cid: context identifier for interactive mode
+#
+# @port: port number for interactive mode
+#
 # Since: 2.5
 ##
 { 'struct': 'GuestExec',
-  'data': { 'pid': 'int'} }
+  'data': { 'pid': 'int',
+            '*cid': 'int',
+            '*port': 'int' } }
 
 ##
 # @GuestExecCaptureOutputMode:
@@ -1284,10 +1290,13 @@
 # @merged: capture both stdout and stderr, but merge together into
 #     out-data.  Not effective on windows guests.
 #
+# @interactive: stdin, stdout and stderr are transmitted via Vsock
+#
 # Since: 8.0
 ##
  { 'enum': 'GuestExecCaptureOutputMode',
    'data': [ 'none', 'stdout', 'stderr', 'separated',
+             { 'name': 'interactive', 'if': 'CONFIG_LINUX' },
              { 'name': 'merged', 'if': { 'not': 'CONFIG_WIN32' } } ] }
 
 ##
-- 
2.40.1



^ permalink raw reply related	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2024-05-23  9:56 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2024-05-22 15:06 [PATCH] qga: Add an interactive mode to guest-exec via VSOCK for Linux Alexander Ivanov
2024-05-22 15:23 ` Test scripts Alexander Ivanov
2024-05-23  9:56   ` Alex Bennée
2024-05-22 15:38 ` [PATCH] qga: Add an interactive mode to guest-exec via VSOCK for Linux Michal Prívozník
2024-05-22 16:10 ` Daniel P. Berrangé
2024-05-23  7:12   ` Denis V. Lunev
2024-05-23  7:50     ` Daniel P. Berrangé
2024-05-23  7:57   ` Alexander Ivanov

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).