test-tool: Document the -t command line option.
[libguestfs.git] / edit / virt-edit.c
1 /* virt-edit
2  * Copyright (C) 2009-2011 Red Hat Inc.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17  */
18
19 #include <config.h>
20
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <inttypes.h>
25 #include <unistd.h>
26 #include <locale.h>
27 #include <getopt.h>
28 #include <assert.h>
29 #include <libintl.h>
30 #include <sys/time.h>
31 #include <sys/stat.h>
32 #include <utime.h>
33
34 #include "progname.h"
35 #include "xvasprintf.h"
36 #include "c-ctype.h"
37
38 #include "guestfs.h"
39 #include "options.h"
40
41 /* Currently open libguestfs handle. */
42 guestfs_h *g;
43
44 int read_only = 0;
45 int live = 0;
46 int verbose = 0;
47 int keys_from_stdin = 0;
48 int echo_keys = 0;
49 const char *libvirt_uri = NULL;
50 int inspector = 1;
51
52 static const char *backup_extension = NULL;
53 static const char *perl_expr = NULL;
54
55 static void edit (const char *filename, const char *root);
56 static char *edit_interactively (const char *tmpfile);
57 static char *edit_non_interactively (const char *tmpfile);
58 static int is_windows (guestfs_h *g, const char *root);
59 static char *windows_path (guestfs_h *g, const char *root, const char *filename);
60 static char *generate_random_name (const char *filename);
61 static char *generate_backup_name (const char *filename);
62
63 static inline char *
64 bad_cast (char const *s)
65 {
66   return (char *) s;
67 }
68
69 static void __attribute__((noreturn))
70 usage (int status)
71 {
72   if (status != EXIT_SUCCESS)
73     fprintf (stderr, _("Try `%s --help' for more information.\n"),
74              program_name);
75   else {
76     fprintf (stdout,
77            _("%s: Edit a file in a virtual machine\n"
78              "Copyright (C) 2009-2011 Red Hat Inc.\n"
79              "Usage:\n"
80              "  %s [--options] -d domname file [file ...]\n"
81              "  %s [--options] -a disk.img [-a disk.img ...] file [file ...]\n"
82              "Options:\n"
83              "  -a|--add image       Add image\n"
84              "  -b|--backup .ext     Backup original as original.ext\n"
85              "  -c|--connect uri     Specify libvirt URI for -d option\n"
86              "  -d|--domain guest    Add disks from libvirt guest\n"
87              "  --echo-keys          Don't turn off echo for passphrases\n"
88              "  -e|--expr expr       Non-interactive editing using Perl expr\n"
89              "  --format[=raw|..]    Force disk format for -a option\n"
90              "  --help               Display brief help\n"
91              "  --keys-from-stdin    Read passphrases from stdin\n"
92              "  -v|--verbose         Verbose messages\n"
93              "  -V|--version         Display version and exit\n"
94              "  -x                   Trace libguestfs API calls\n"
95              "For more information, see the manpage %s(1).\n"),
96              program_name, program_name, program_name,
97              program_name);
98   }
99   exit (status);
100 }
101
102 int
103 main (int argc, char *argv[])
104 {
105   /* Set global program name that is not polluted with libtool artifacts.  */
106   set_program_name (argv[0]);
107
108   setlocale (LC_ALL, "");
109   bindtextdomain (PACKAGE, LOCALEBASEDIR);
110   textdomain (PACKAGE);
111
112   /* We use random(3) below. */
113   srandom (time (NULL));
114
115   enum { HELP_OPTION = CHAR_MAX + 1 };
116
117   static const char *options = "a:b:c:d:e:vVx";
118   static const struct option long_options[] = {
119     { "add", 1, 0, 'a' },
120     { "backup", 1, 0, 'b' },
121     { "connect", 1, 0, 'c' },
122     { "domain", 1, 0, 'd' },
123     { "echo-keys", 0, 0, 0 },
124     { "expr", 1, 0, 'e' },
125     { "format", 2, 0, 0 },
126     { "help", 0, 0, HELP_OPTION },
127     { "keys-from-stdin", 0, 0, 0 },
128     { "verbose", 0, 0, 'v' },
129     { "version", 0, 0, 'V' },
130     { 0, 0, 0, 0 }
131   };
132   struct drv *drvs = NULL;
133   struct drv *drv;
134   const char *format = NULL;
135   int c;
136   int option_index;
137   char *root, **roots;
138
139   g = guestfs_create ();
140   if (g == NULL) {
141     fprintf (stderr, _("guestfs_create: failed to create handle\n"));
142     exit (EXIT_FAILURE);
143   }
144
145   argv[0] = bad_cast (program_name);
146
147   for (;;) {
148     c = getopt_long (argc, argv, options, long_options, &option_index);
149     if (c == -1) break;
150
151     switch (c) {
152     case 0:                     /* options which are long only */
153       if (STREQ (long_options[option_index].name, "keys-from-stdin")) {
154         keys_from_stdin = 1;
155       } else if (STREQ (long_options[option_index].name, "echo-keys")) {
156         echo_keys = 1;
157       } else if (STREQ (long_options[option_index].name, "format")) {
158         if (!optarg || STREQ (optarg, ""))
159           format = NULL;
160         else
161           format = optarg;
162       } else {
163         fprintf (stderr, _("%s: unknown long option: %s (%d)\n"),
164                  program_name, long_options[option_index].name, option_index);
165         exit (EXIT_FAILURE);
166       }
167       break;
168
169     case 'a':
170       OPTION_a;
171       break;
172
173     case 'b':
174       if (backup_extension) {
175         fprintf (stderr, _("%s: -b option given multiple times\n"),
176                  program_name);
177         exit (EXIT_FAILURE);
178       }
179       backup_extension = optarg;
180       break;
181
182     case 'c':
183       OPTION_c;
184       break;
185
186     case 'd':
187       OPTION_d;
188       break;
189
190     case 'e':
191       if (perl_expr) {
192         fprintf (stderr, _("%s: -e option given multiple times\n"),
193                  program_name);
194         exit (EXIT_FAILURE);
195       }
196       perl_expr = optarg;
197       break;
198
199     case 'h':
200       usage (EXIT_SUCCESS);
201
202     case 'v':
203       OPTION_v;
204       break;
205
206     case 'V':
207       OPTION_V;
208       break;
209
210     case 'x':
211       OPTION_x;
212       break;
213
214     case HELP_OPTION:
215       usage (EXIT_SUCCESS);
216
217     default:
218       usage (EXIT_FAILURE);
219     }
220   }
221
222   /* Old-style syntax?  There were no -a or -d options in the old
223    * virt-edit which is how we detect this.
224    */
225   if (drvs == NULL) {
226     /* argc - 1 because last parameter is the single filename. */
227     while (optind < argc - 1) {
228       if (strchr (argv[optind], '/') ||
229           access (argv[optind], F_OK) == 0) { /* simulate -a option */
230         drv = malloc (sizeof (struct drv));
231         if (!drv) {
232           perror ("malloc");
233           exit (EXIT_FAILURE);
234         }
235         drv->type = drv_a;
236         drv->a.filename = argv[optind];
237         drv->a.format = NULL;
238         drv->next = drvs;
239         drvs = drv;
240       } else {                  /* simulate -d option */
241         drv = malloc (sizeof (struct drv));
242         if (!drv) {
243           perror ("malloc");
244           exit (EXIT_FAILURE);
245         }
246         drv->type = drv_d;
247         drv->d.guest = argv[optind];
248         drv->next = drvs;
249         drvs = drv;
250       }
251
252       optind++;
253     }
254   }
255
256   /* These are really constants, but they have to be variables for the
257    * options parsing code.  Assert here that they have known-good
258    * values.
259    */
260   assert (read_only == 0);
261   assert (inspector == 1);
262   assert (live == 0);
263
264   /* User must specify at least one filename on the command line. */
265   if (optind >= argc || argc - optind < 1)
266     usage (EXIT_FAILURE);
267
268   /* User must have specified some drives. */
269   if (drvs == NULL)
270     usage (EXIT_FAILURE);
271
272   /* Add drives. */
273   add_drives (drvs, 'a');
274
275   if (guestfs_launch (g) == -1)
276     exit (EXIT_FAILURE);
277
278   inspect_mount ();
279
280   /* Free up data structures, no longer needed after this point. */
281   free_drives (drvs);
282
283   /* Get root mountpoint. */
284   roots = guestfs_inspect_get_roots (g);
285   if (!roots)
286     exit (EXIT_FAILURE);
287   /* see fish/inspect.c:inspect_mount */
288   assert (roots[0] != NULL && roots[1] == NULL);
289   root = roots[0];
290   free (roots);
291
292   while (optind < argc) {
293     edit (argv[optind], root);
294     optind++;
295   }
296
297   free (root);
298
299   /* Cleanly unmount the disks after editing. */
300   if (guestfs_umount_all (g) == -1 || guestfs_sync (g) == -1)
301     exit (EXIT_FAILURE);
302
303   guestfs_close (g);
304
305   exit (EXIT_SUCCESS);
306 }
307
308 static void
309 edit (const char *filename, const char *root)
310 {
311   char *filename_to_free = NULL;
312   const char *tmpdir = guestfs_tmpdir ();
313   char tmpfile[strlen (tmpdir) + 32];
314   sprintf (tmpfile, "%s/virteditXXXXXX", tmpdir);
315   int fd;
316   char fdbuf[32];
317   char *upload_from = NULL;
318   char *newname = NULL;
319   char *backupname = NULL;
320
321   /* Windows?  Special handling is required. */
322   if (is_windows (g, root))
323     filename = filename_to_free = windows_path (g, root, filename);
324
325   /* Download the file to a temporary. */
326   fd = mkstemp (tmpfile);
327   if (fd == -1) {
328     perror ("mkstemp");
329     exit (EXIT_FAILURE);
330   }
331
332   snprintf (fdbuf, sizeof fdbuf, "/dev/fd/%d", fd);
333
334   if (guestfs_download (g, filename, fdbuf) == -1)
335     goto error;
336
337   if (close (fd) == -1) {
338     perror (tmpfile);
339     goto error;
340   }
341
342   if (!perl_expr)
343     upload_from = edit_interactively (tmpfile);
344   else
345     upload_from = edit_non_interactively (tmpfile);
346
347   /* We don't always need to upload: upload_from could be NULL because
348    * the user closed the editor without changing the file.
349    */
350   if (upload_from) {
351     /* Upload to a new file in the same directory, so if it fails we
352      * don't end up with a partially written file.  Give the new file
353      * a completely random name so we have only a tiny chance of
354      * overwriting some existing file.
355      */
356     newname = generate_random_name (filename);
357
358     if (guestfs_upload (g, upload_from, newname) == -1)
359       goto error;
360
361     /* Backup or overwrite the file. */
362     if (backup_extension) {
363       backupname = generate_backup_name (filename);
364       if (guestfs_mv (g, filename, backupname) == -1)
365         goto error;
366     }
367     if (guestfs_mv (g, newname, filename) == -1)
368       goto error;
369   }
370
371   unlink (tmpfile);
372   free (filename_to_free);
373   free (upload_from);
374   free (newname);
375   free (backupname);
376   return;
377
378  error:
379   unlink (tmpfile);
380   exit (EXIT_FAILURE);
381 }
382
383 static char *
384 edit_interactively (const char *tmpfile)
385 {
386   struct utimbuf times;
387   struct stat oldstat, newstat;
388   const char *editor;
389   char *cmd;
390   int r;
391   char *ret;
392
393   /* Set the time back a few seconds on the original file.  This is so
394    * that if the user is very fast at editing, or if EDITOR is an
395    * automatic editor, then the edit might happen within the 1 second
396    * granularity of mtime, and we would think the file hasn't changed.
397    */
398   if (stat (tmpfile, &oldstat) == -1) {
399     perror (tmpfile);
400     exit (EXIT_FAILURE);
401   }
402
403   times.actime = oldstat.st_atime - 5;
404   times.modtime = oldstat.st_mtime - 5;
405   if (utime (tmpfile, &times) == -1) {
406     perror ("utimes");
407     exit (EXIT_FAILURE);
408   }
409
410   if (stat (tmpfile, &oldstat) == -1) {
411     perror (tmpfile);
412     exit (EXIT_FAILURE);
413   }
414
415   editor = getenv ("EDITOR");
416   if (editor == NULL)
417     editor = "vi";
418
419   if (asprintf (&cmd, "%s %s", editor, tmpfile) == -1) {
420     perror ("asprintf");
421     exit (EXIT_FAILURE);
422   }
423
424   if (verbose)
425     fprintf (stderr, "%s\n", cmd);
426
427   r = system (cmd);
428   if (r == -1 || WEXITSTATUS (r) != 0)
429     exit (EXIT_FAILURE);
430
431   free (cmd);
432
433   if (stat (tmpfile, &newstat) == -1) {
434     perror (tmpfile);
435     exit (EXIT_FAILURE);
436   }
437
438   if (oldstat.st_ctime == newstat.st_ctime &&
439       oldstat.st_mtime == newstat.st_mtime) {
440     printf ("File not changed.\n");
441     return NULL;
442   }
443
444   ret = strdup (tmpfile);
445   if (!ret) {
446     perror ("strdup");
447     exit (EXIT_FAILURE);
448   }
449
450   return ret;
451 }
452
453 static char *
454 edit_non_interactively (const char *tmpfile)
455 {
456   char *cmd, *outfile, *ret;
457   int r;
458
459   assert (perl_expr != NULL);
460
461   /* Pass the expression to Perl via the environment.  This sidesteps
462    * any quoting problems with the already complex Perl command line.
463    */
464   setenv ("virt_edit_expr", perl_expr, 1);
465
466   /* Call out to a canned Perl script. */
467   if (asprintf (&cmd,
468                 "perl -e '"
469                 "$lineno = 0; "
470                 "$expr = $ENV{virt_edit_expr}; "
471                 "while (<STDIN>) { "
472                 "  $lineno++; "
473                 "  eval $expr; "
474                 "  die if $@; "
475                 "  print STDOUT $_ or die \"print: $!\"; "
476                 "} "
477                 "close STDOUT or die \"close: $!\"; "
478                 "' < %s > %s.out",
479                 tmpfile, tmpfile) == -1) {
480     perror ("asprintf");
481     exit (EXIT_FAILURE);
482   }
483
484   if (verbose)
485     fprintf (stderr, "%s\n", cmd);
486
487   r = system (cmd);
488   if (r == -1 || WEXITSTATUS (r) != 0)
489     exit (EXIT_FAILURE);
490
491   free (cmd);
492
493   if (asprintf (&outfile, "%s.out", tmpfile) == -1) {
494     perror ("asprintf");
495     exit (EXIT_FAILURE);
496   }
497
498   if (rename (outfile, tmpfile) == -1) {
499     perror ("rename");
500     exit (EXIT_FAILURE);
501   }
502
503   free (outfile);
504
505   ret = strdup (tmpfile);
506   if (!ret) {
507     perror ("strdup");
508     exit (EXIT_FAILURE);
509   }
510
511   return ret; /* caller will free */
512 }
513
514 static int
515 is_windows (guestfs_h *g, const char *root)
516 {
517   char *type;
518   int w;
519
520   type = guestfs_inspect_get_type (g, root);
521   if (!type)
522     return 0;
523
524   w = STREQ (type, "windows");
525   free (type);
526   return w;
527 }
528
529 static void mount_drive_letter (char drive_letter, const char *root);
530
531 static char *
532 windows_path (guestfs_h *g, const char *root, const char *path)
533 {
534   char *ret;
535   size_t i;
536
537   /* If there is a drive letter, rewrite the path. */
538   if (c_isalpha (path[0]) && path[1] == ':') {
539     char drive_letter = c_tolower (path[0]);
540     /* This returns the newly allocated string. */
541     mount_drive_letter (drive_letter, root);
542     ret = strdup (path + 2);
543     if (ret == NULL) {
544       perror ("strdup");
545       exit (EXIT_FAILURE);
546     }
547   }
548   else if (!*path) {
549     ret = strdup ("/");
550     if (ret == NULL) {
551       perror ("strdup");
552       exit (EXIT_FAILURE);
553     }
554   }
555   else {
556     ret = strdup (path);
557     if (ret == NULL) {
558       perror ("strdup");
559       exit (EXIT_FAILURE);
560     }
561   }
562
563   /* Blindly convert any backslashes into forward slashes.  Is this good? */
564   for (i = 0; i < strlen (ret); ++i)
565     if (ret[i] == '\\')
566       ret[i] = '/';
567
568   char *t = guestfs_case_sensitive_path (g, ret);
569   free (ret);
570   ret = t;
571
572   return ret;
573 }
574
575 static void
576 mount_drive_letter (char drive_letter, const char *root)
577 {
578   char **drives;
579   char *device;
580   size_t i;
581
582   /* Resolve the drive letter using the drive mappings table. */
583   drives = guestfs_inspect_get_drive_mappings (g, root);
584   if (drives == NULL || drives[0] == NULL) {
585     fprintf (stderr, _("%s: to use Windows drive letters, this must be a Windows guest\n"),
586              program_name);
587     exit (EXIT_FAILURE);
588   }
589
590   device = NULL;
591   for (i = 0; drives[i] != NULL; i += 2) {
592     if (c_tolower (drives[i][0]) == drive_letter && drives[i][1] == '\0') {
593       device = drives[i+1];
594       break;
595     }
596   }
597
598   if (device == NULL) {
599     fprintf (stderr, _("%s: drive '%c:' not found.\n"),
600              program_name, drive_letter);
601     exit (EXIT_FAILURE);
602   }
603
604   /* Unmount current disk and remount device. */
605   if (guestfs_umount_all (g) == -1)
606     exit (EXIT_FAILURE);
607
608   if (guestfs_mount_options (g, "", device, "/") == -1)
609     exit (EXIT_FAILURE);
610
611   for (i = 0; drives[i] != NULL; ++i)
612     free (drives[i]);
613   free (drives);
614   /* Don't need to free (device) because that string was in the
615    * drives array.
616    */
617 }
618
619 static char
620 random_char (void)
621 {
622   char c[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
623   return c[random () % (sizeof c - 1)];
624 }
625
626 static char *
627 generate_random_name (const char *filename)
628 {
629   char *ret, *p;
630   size_t i;
631
632   ret = malloc (strlen (filename) + 16);
633   if (!ret) {
634     perror ("malloc");
635     exit (EXIT_FAILURE);
636   }
637   strcpy (ret, filename);
638
639   p = strrchr (ret, '/');
640   assert (p);
641   p++;
642
643   /* Because of "+ 16" above, there should be enough space in the
644    * output buffer to write 8 random characters here.
645    */
646   for (i = 0; i < 8; ++i)
647     *p++ = random_char ();
648   *p++ = '\0';
649
650   return ret; /* caller will free */
651 }
652
653 static char *
654 generate_backup_name (const char *filename)
655 {
656   char *ret;
657
658   assert (backup_extension != NULL);
659
660   if (asprintf (&ret, "%s%s", filename, backup_extension) == -1) {
661     perror ("asprintf");
662     exit (EXIT_FAILURE);
663   }
664
665   return ret; /* caller will free */
666 }