From: Richard Jones Date: Mon, 13 Jul 2009 12:06:26 +0000 (+0100) Subject: Guestfish feature: remote control of guestfish over a pipe. X-Git-Tag: 1.0.59~3 X-Git-Url: http://git.annexia.org/?p=libguestfs.git;a=commitdiff_plain;h=a86aa7d152ed996170714a3a4516eab58bf8b59d Guestfish feature: remote control of guestfish over a pipe. The use case is to have a long-running guestfish process in a shell script, and thus to avoid the overhead of starting guestfish each time. Do: eval `guestfish --listen` guestfish --remote somecmd guestfish --remote someothercmd guestfish --remote exit This patch also supports having multiple guestfish processes at the same time. The protocol is simple XDR messages over a Unix domain socket. --- diff --git a/.gitignore b/.gitignore index 87dab68..6ae00e9 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,8 @@ examples/to-xml fish/cmds.c fish/completion.c fish/guestfish +fish/rc_protocol.c +fish/rc_protocol.h guestfish.1 guestfish-actions.pod guestfs.3 diff --git a/TODO b/TODO index 827ad97..4133122 100644 --- a/TODO +++ b/TODO @@ -176,11 +176,3 @@ Other initrd-* commands, such as: initrd-extract initrd-replace - ----------------------------------------------------------------------- - -Control guestfish from a pipe. - -For shell scripts - they can start up a long-running guestfish process -and intermittently send it commands. Avoids the start-up overhead, -but how do we reliably signal errors? \ No newline at end of file diff --git a/fish/Makefile.am b/fish/Makefile.am index 03619f3..5255e85 100644 --- a/fish/Makefile.am +++ b/fish/Makefile.am @@ -29,10 +29,29 @@ guestfish_SOURCES = \ glob.c \ lcd.c \ more.c \ + rc.c \ + rc_protocol.c \ + rc_protocol.h \ reopen.c \ time.c +BUILT_SOURCES = \ + rc_protocol.c \ + rc_protocol.h + guestfish_CFLAGS = \ -I$(top_srcdir)/src -I$(top_builddir)/src -Wall \ -DGUESTFS_DEFAULT_PATH='"$(libdir)/guestfs"' guestfish_LDADD = $(top_builddir)/src/libguestfs.la $(LIBREADLINE) + +if HAVE_RPCGEN +rc_protocol.c: rc_protocol.x + rm -f $@-t + $(RPCGEN) -c -o $@-t $< + mv $@-t $@ + +rc_protocol.h: rc_protocol.x + rm -f $@-t + $(RPCGEN) -h -o $@-t $< + mv $@-t $@ +endif diff --git a/fish/fish.c b/fish/fish.c index a093395..4042bbc 100644 --- a/fish/fish.c +++ b/fish/fish.c @@ -69,6 +69,9 @@ int read_only = 0; int quit = 0; int verbose = 0; int echo_commands = 0; +int remote_control_listen = 0; +int remote_control = 0; +int exit_on_error = 1; int launch (guestfs_h *_g) @@ -109,8 +112,10 @@ usage (void) " -D|--no-dest-paths Don't tab-complete paths from guest fs\n" " -f|--file file Read commands from file\n" " -i|--inspector Run virt-inspector to get disk mountpoints\n" + " --listen Listen for remote commands\n" " -m|--mount dev[:mnt] Mount dev on mnt (if omitted, /)\n" " -n|--no-sync Don't autosync\n" + " --remote[=pid] Send commands to remote guestfish\n" " -r|--ro Mount read-only\n" " -v|--verbose Verbose messages\n" " -x Echo each command before executing it\n" @@ -128,9 +133,11 @@ main (int argc, char *argv[]) { "file", 1, 0, 'f' }, { "help", 0, 0, '?' }, { "inspector", 0, 0, 'i' }, + { "listen", 0, 0, 0 }, { "mount", 1, 0, 'm' }, { "no-dest-paths", 0, 0, 'D' }, { "no-sync", 0, 0, 'n' }, + { "remote", 2, 0, 0 }, { "ro", 0, 0, 'r' }, { "verbose", 0, 0, 'v' }, { "version", 0, 0, 'V' }, @@ -141,7 +148,9 @@ main (int argc, char *argv[]) struct mp *mps = NULL; struct mp *mp; char *p, *file = NULL; - int c, inspector = 0; + int c; + int inspector = 0; + int option_index; struct sigaction sa; initialize_readline (); @@ -176,10 +185,33 @@ main (int argc, char *argv[]) guestfs_set_path (g, "appliance:" GUESTFS_DEFAULT_PATH); for (;;) { - c = getopt_long (argc, argv, options, long_options, NULL); + c = getopt_long (argc, argv, options, long_options, &option_index); if (c == -1) break; switch (c) { + case 0: /* options which are long only */ + if (strcmp (long_options[option_index].name, "listen") == 0) + remote_control_listen = 1; + else if (strcmp (long_options[option_index].name, "remote") == 0) { + if (optarg) { + if (sscanf (optarg, "%d", &remote_control) != 1) { + fprintf (stderr, _("guestfish: --listen=PID: PID was not a number: %s\n"), optarg); + exit (1); + } + } else { + p = getenv ("GUESTFISH_PID"); + if (!p || sscanf (p, "%d", &remote_control) != 1) { + fprintf (stderr, _("guestfish: remote: $GUESTFISH_PID must be set to the PID of the remote process\n")); + exit (1); + } + } + } else { + fprintf (stderr, _("guestfish: unknown long option: %s (%d)\n"), + long_options[option_index].name, option_index); + exit (1); + } + break; + case 'a': if (access (optarg, R_OK) != 0) { perror (optarg); @@ -274,8 +306,8 @@ main (int argc, char *argv[]) char cmd[1024]; int r; - if (drvs || mps) { - fprintf (stderr, _("guestfish: cannot use -i option with -a or -m\n")); + if (drvs || mps || remote_control_listen || remote_control) { + fprintf (stderr, _("guestfish: cannot use -i option with -a, -m, --listen or --remote\n")); exit (1); } if (optind >= argc) { @@ -327,6 +359,24 @@ main (int argc, char *argv[]) mount_mps (mps); } + /* Remote control? */ + if (remote_control_listen && remote_control) { + fprintf (stderr, _("guestfish: cannot use --listen and --remote options at the same time\n")); + exit (1); + } + + if (remote_control_listen) { + if (optind < argc) { + fprintf (stderr, _("guestfish: extra parameters on the command line with --listen flag\n")); + exit (1); + } + if (file) { + fprintf (stderr, _("guestfish: cannot use --listen and --file options at the same time\n")); + exit (1); + } + rc_listen (); + } + /* -f (file) parameter? */ if (file) { close (0); @@ -464,7 +514,6 @@ script (int prompt) char *argv[64]; int i, len; int global_exit_on_error = !prompt; - int exit_on_error; if (prompt) printf (_("\n" @@ -641,6 +690,8 @@ cmdline (char *argv[], int optind, int argc) const char *cmd; char **params; + exit_on_error = 1; + if (optind >= argc) return; cmd = argv[optind++]; @@ -711,7 +762,12 @@ issue_command (const char *cmd, char *argv[], const char *pipecmd) for (argc = 0; argv[argc] != NULL; ++argc) ; - if (strcasecmp (cmd, "help") == 0) { + /* If --remote was set, then send this command to a remote process. */ + if (remote_control) + r = rc_remote (remote_control, cmd, argc, argv, exit_on_error); + + /* Otherwise execute it locally. */ + else if (strcasecmp (cmd, "help") == 0) { if (argc == 0) list_commands (); else diff --git a/fish/fish.h b/fish/fish.h index d0dc7a9..f911eed 100644 --- a/fish/fish.h +++ b/fish/fish.h @@ -1,4 +1,4 @@ -/* libguestfs - the guestfsd daemon +/* libguestfs - guestfish shell * Copyright (C) 2009 Red Hat Inc. * * This program is free software; you can redistribute it and/or modify @@ -77,6 +77,11 @@ extern int do_glob (const char *cmd, int argc, char *argv[]); /* in more.c */ extern int do_more (const char *cmd, int argc, char *argv[]); +/* in rc.c (remote control) */ +extern void rc_listen (void); +extern int rc_remote (int pid, const char *cmd, int argc, char *argv[], + int exit_on_error); + /* in reopen.c */ extern int do_reopen (const char *cmd, int argc, char *argv[]); diff --git a/fish/rc.c b/fish/rc.c new file mode 100644 index 0000000..0d67a62 --- /dev/null +++ b/fish/rc.c @@ -0,0 +1,272 @@ +/* guestfish - the filesystem interactive shell + * Copyright (C) 2009 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "fish.h" +#include "rc_protocol.h" + +#define UNIX_PATH_MAX 108 + +static void +create_sockpath (pid_t pid, char *sockpath, int len, struct sockaddr_un *addr) +{ + char dir[128]; + uid_t euid = geteuid (); + + snprintf (dir, sizeof dir, "/tmp/.guestfish-%d", euid); + mkdir (dir, 0700); + + snprintf (sockpath, len, "/tmp/.guestfish-%d/socket-%d", euid, pid); + + addr->sun_family = AF_UNIX; + strcpy (addr->sun_path, sockpath); +} + +/* Remote control server. */ +void +rc_listen (void) +{ + char sockpath[128]; + pid_t pid; + struct sockaddr_un addr; + int sock, s, i, fd; + FILE *fp; + XDR xdr, xdr2; + guestfish_hello hello; + guestfish_call call; + guestfish_reply reply; + char **argv; + int argc; + + memset (&hello, 0, sizeof hello); + memset (&call, 0, sizeof call); + + pid = fork (); + if (pid == -1) { + perror ("fork"); + exit (1); + } + + if (pid > 0) { + /* Parent process. */ + printf ("export GUESTFISH_PID=%d\n", pid); + fflush (stdout); + _exit (0); + } + + /* Child process. + * + * Create the listening socket for accepting commands. + * + * Unfortunately there is a small but unavoidable race here. We + * don't know the PID until after we've forked, so we cannot be + * sure the socket is created from the point of view of the parent + * (if the child is very slow). + */ + pid = getpid (); + create_sockpath (pid, sockpath, sizeof sockpath, &addr); + + sock = socket (AF_UNIX, SOCK_STREAM, 0); + if (sock == -1) { + perror ("socket"); + exit (1); + } + unlink (sockpath); + if (bind (sock, (struct sockaddr *) &addr, sizeof addr) == -1) { + perror (sockpath); + exit (1); + } + if (listen (sock, 4) == -1) { + perror ("listen"); + exit (1); + } + + /* Now close stdout and substitute /dev/null. This is necessary + * so that eval `guestfish --listen` doesn't block forever. + */ + fd = open ("/dev/null", O_WRONLY); + if (fd == -1) + perror ("/dev/null"); + else { + dup2 (fd, 1); + close (fd); + } + + /* Read commands and execute them. */ + while (!quit) { + s = accept (sock, NULL, NULL); + if (s == -1) + perror ("accept"); + else { + fp = fdopen (s, "r+"); + xdrstdio_create (&xdr, fp, XDR_DECODE); + + if (!xdr_guestfish_hello (&xdr, &hello)) { + fprintf (stderr, _("guestfish: protocol error: could not read 'hello' message\n")); + goto error; + } + + if (strcmp (hello.vers, PACKAGE_VERSION) != 0) { + fprintf (stderr, _("guestfish: protocol error: version mismatch, server version '%s' does not match client version '%s'. The two versions must match exactly.\n"), + PACKAGE_VERSION, + hello.vers); + xdr_free ((xdrproc_t) xdr_guestfish_hello, (char *) &hello); + goto error; + } + xdr_free ((xdrproc_t) xdr_guestfish_hello, (char *) &hello); + + while (xdr_guestfish_call (&xdr, &call)) { + /* We have to extend and NULL-terminate the argv array. */ + argc = call.args.args_len; + argv = realloc (call.args.args_val, (argc+1) * sizeof (char *)); + if (argv == NULL) { + perror ("realloc"); + exit (1); + } + call.args.args_val = argv; + argv[argc] = NULL; + + if (verbose) { + fprintf (stderr, "guestfish(%d): %s", pid, call.cmd); + for (i = 0; i < argc; ++i) + fprintf (stderr, " %s", argv[i]); + fprintf (stderr, "\n"); + } + + /* Run the command. */ + reply.r = issue_command (call.cmd, argv, NULL); + + xdr_free ((xdrproc_t) xdr_guestfish_call, (char *) &call); + + /* Send the reply. */ + xdrstdio_create (&xdr2, fp, XDR_ENCODE); + (void) xdr_guestfish_reply (&xdr2, &reply); + xdr_destroy (&xdr2); + + /* Exit on error? */ + if (call.exit_on_error && reply.r == -1) { + unlink (sockpath); + exit (1); + } + } + + error: + xdr_destroy (&xdr); /* NB. This doesn't close 'fp'. */ + fclose (fp); /* Closes the underlying socket 's'. */ + } + } + + unlink (sockpath); + exit (0); +} + +/* Remote control client. */ +int +rc_remote (int pid, const char *cmd, int argc, char *argv[], + int exit_on_error) +{ + guestfish_hello hello; + guestfish_call call; + guestfish_reply reply; + char sockpath[128]; + struct sockaddr_un addr; + int sock; + FILE *fp; + XDR xdr; + + memset (&reply, 0, sizeof reply); + + /* This is fine as long as we never try to xdr_free this struct. */ + hello.vers = (char *) PACKAGE_VERSION; + + /* Check the other end is still running. */ + if (kill (pid, 0) == -1) { + fprintf (stderr, _("guestfish: remote: looks like the server is not running\n")); + return -1; + } + + create_sockpath (pid, sockpath, sizeof sockpath, &addr); + + sock = socket (AF_UNIX, SOCK_STREAM, 0); + if (sock == -1) { + perror ("socket"); + return -1; + } + + if (connect (sock, (struct sockaddr *) &addr, sizeof addr) == -1) { + perror (sockpath); + fprintf (stderr, _("guestfish: remote: looks like the server is not running\n")); + close (sock); + return -1; + } + + /* Send the greeting. */ + fp = fdopen (sock, "r+"); + xdrstdio_create (&xdr, fp, XDR_ENCODE); + + if (!xdr_guestfish_hello (&xdr, &hello)) { + fprintf (stderr, _("guestfish: protocol error: could not send initial greeting to server\n")); + fclose (fp); + xdr_destroy (&xdr); + return -1; + } + + /* Send the command. The server supports reading multiple commands + * per connection, but this code only ever sends one command. + */ + call.cmd = (char *) cmd; + call.args.args_len = argc; + call.args.args_val = argv; + call.exit_on_error = exit_on_error; + if (!xdr_guestfish_call (&xdr, &call)) { + fprintf (stderr, _("guestfish: protocol error: could not send initial greeting to server\n")); + fclose (fp); + xdr_destroy (&xdr); + return -1; + } + xdr_destroy (&xdr); + + /* Wait for the reply. */ + xdrstdio_create (&xdr, fp, XDR_DECODE); + + if (!xdr_guestfish_reply (&xdr, &reply)) { + fprintf (stderr, _("guestfish: protocol error: could not decode reply from server\n")); + fclose (fp); + xdr_destroy (&xdr); + return -1; + } + + fclose (fp); + xdr_destroy (&xdr); + + return reply.r; +} diff --git a/fish/rc_protocol.x b/fish/rc_protocol.x new file mode 100644 index 0000000..9d8f0e9 --- /dev/null +++ b/fish/rc_protocol.x @@ -0,0 +1,36 @@ +/* libguestfs - guestfish remote control protocol -*- c -*- + * Copyright (C) 2009 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +typedef string str<>; + +struct guestfish_hello { + /* Client and server version strings must match exactly. We change + * this protocol whenever we want to. + */ + string vers<>; +}; + +struct guestfish_call { + string cmd<>; + str args<>; + bool exit_on_error; +}; + +struct guestfish_reply { + int r; /* 0 or -1 only. */ +}; diff --git a/guestfish.pod b/guestfish.pod index 0100311..4f8cf95 100644 --- a/guestfish.pod +++ b/guestfish.pod @@ -66,6 +66,11 @@ Remove C (in reality not such a great idea): sfdisk /dev/sda 0 0 0 , mkfs ext2 /dev/sda1 +=head2 Remote control + + eval `guestfish --listen` + guestfish --remote cmd + =head1 DESCRIPTION Guestfish is a shell and command-line tool for examining and modifying @@ -125,11 +130,17 @@ Typical usage is either: guestfish -i /dev/Guests/MyGuest -You cannot use I<-a> or I<-m> in conjunction with this option, and -options other than I<--ro> might not behave correctly. +You cannot use I<-a>, I<-m>, I<--listen> or I<--remote> in conjunction +with this option, and options other than I<--ro> might not behave +correctly. See also: L. +=item B<--listen> + +Fork into the background and listen for remote commands. See section +I below. + =item B<-m dev[:mountpoint]> | B<--mount dev[:mountpoint]> Mount the named partition or logical volume on the given mountpoint. @@ -146,6 +157,11 @@ automatically launched. Disable autosync. This is enabled by default. See the discussion of autosync in the L manpage. +=item B<--remote[=pid]> + +Send remote commands to C<$GUESTFISH_PID> or C. See section +I below. + =item B<-r> | B<--ro> This changes the C<-m> option so that mounts are done read-only @@ -323,6 +339,58 @@ If you prefix a command with a I<-> character, then that command will not cause guestfish to exit, even if that (one) command returns an error. +=head1 REMOTE CONTROL GUESTFISH OVER A SOCKET + +Guestfish can be remote-controlled over a socket. This is useful +particularly in shell scripts where you want to make several different +changes to a filesystem, but you don't want the overhead of starting +up a guestfish process each time. + +Start a guestfish server process using: + + eval `guestfish --listen` + +and then send it commands by doing: + + guestfish --remote cmd [...] + +To cause the server to exit, send it the exit command: + + guestfish --remote exit + +Note that the server will normally exit if there is an error in a +command. You can change this in the usual way. See section I. + +=head2 CONTROLLING MULTIPLE GUESTFISH PROCESSES + +The C statement sets the environment variable C<$GUESTFISH_PID>, +which is how the C<--remote> option knows where to send the commands. +You can have several guestfish listener processes running using: + + eval `guestfish --listen` + pid1=$GUESTFISH_PID + eval `guestfish --listen` + pid2=$GUESTFISH_PID + ... + guestfish --remote=$pid1 cmd + guestfish --remote=$pid2 cmd + +=head2 STANDARD OUTPUT DURING REMOTE CONTROL + +Because of limitations in the C statement, stdout from the +listener is currently redirected to C. + +Stderr is unchanged. + +=head2 REMOTE CONTROL DETAILS + +Remote control happens over a Unix domain socket called +C, where C<$UID> is the effective +user ID of the process, and C<$PID> is the process ID of the server. + +Guestfish client and server versions must match exactly. + =head1 GUESTFISH COMMANDS The commands in this section are guestfish convenience commands, in @@ -451,6 +519,12 @@ can be useful for benchmarking operations. The C command uses C<$EDITOR> as the editor. If not set, it uses C. +=item GUESTFISH_PID + +Used with the I<--remote> option to specify the remote guestfish +process to control. See section I. + =item HOME If compiled with GNU readline support, then the command history diff --git a/regressions/Makefile.am b/regressions/Makefile.am index cd575e0..0e0d520 100644 --- a/regressions/Makefile.am +++ b/regressions/Makefile.am @@ -29,6 +29,7 @@ TESTS = \ test-qemudie-midcommand.sh \ test-qemudie-killsub.sh \ test-qemudie-synch.sh \ + test-remote.sh \ test-reopen.sh SKIPPED_TESTS = \ diff --git a/regressions/test-remote.sh b/regressions/test-remote.sh new file mode 100755 index 0000000..f3c14b4 --- /dev/null +++ b/regressions/test-remote.sh @@ -0,0 +1,34 @@ +#!/bin/sh - +# libguestfs +# Copyright (C) 2009 Red Hat Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +# Test remote control of guestfish. + +set -e + +rm -f test.img + +eval `../fish/guestfish --listen` + +../fish/guestfish --remote alloc test.img 10M +../fish/guestfish --remote run +../fish/guestfish --remote sfdiskM /dev/sda , +../fish/guestfish --remote mkfs ext2 /dev/sda1 +../fish/guestfish --remote mount /dev/sda1 / +../fish/guestfish --remote exit + +rm -f test.img