Add nbdkit bmaplogger plugin.
[virt-bmap.git] / examiner.c
1 /* virt-bmap examiner plugin
2  * Copyright (C) 2014 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17  */
18
19 /* This is an nbdkit plugin which implements the guts of the 'virt-bmap'
20  * command.
21  *
22  * IMPORTANT NOTES:
23  *
24  * Do not try to use this plugin directly!  Use the 'virt-bmap'
25  * wrapper script which sets up the arguments correctly and calls
26  * nbdkit.
27  *
28  * This is a bit of a weird nbdkit plugin, because it calls itself.
29  * Do not use it as an example of how to write plugins!  See the
30  * nbdkit 'example*.c' files supplied in the nbdkit source, and also
31  * read the nbdkit-plugin(3) documentation.
32  */
33
34 #include <config.h>
35
36 #include <stdio.h>
37 #include <stdlib.h>
38 #include <stdint.h>
39 #include <inttypes.h>
40 #include <string.h>
41 #include <signal.h>
42 #include <fcntl.h>
43 #include <unistd.h>
44 #include <sys/mman.h>
45 #include <sys/types.h>
46 #include <sys/stat.h>
47 #include <assert.h>
48
49 #include <pthread.h>
50
51 #include <nbdkit-plugin.h>
52
53 #include <guestfs.h>
54
55 #include "cleanups.h"
56 #include "ranges.h"
57 #include "visit.h"
58
59 static char *output = NULL;
60 static char *disk = NULL;
61 static const char *socket = NULL;
62 static const char *format = "raw";
63 static int fd = -1;
64 static int64_t size = -1;
65 static int thread_running = 0;
66 static pthread_t thread;
67
68 static struct thread_info {
69   guestfs_h *g;
70   int ret;
71 } thread_info;
72
73 /* Current object being mapped. */
74 static pthread_mutex_t current_object_mutex = PTHREAD_MUTEX_INITIALIZER;
75 static char *current_object = NULL;
76
77 /* Ranges.  NB: acquire current_object_mutex before accessing. */
78 static void *ranges = NULL;
79
80 static void *start_thread (void *);
81
82 static int
83 bmap_config (const char *key, const char *value)
84 {
85   if (strcmp (key, "disk") == 0) {
86     free (disk);
87     disk = nbdkit_absolute_path (value);
88     if (disk == NULL)
89       return -1;
90   }
91   else if (strcmp (key, "format") == 0) {
92     format = value;
93   }
94   else if (strcmp (key, "output") == 0) {
95     free (output);
96     output = nbdkit_absolute_path (value);
97     if (output == NULL)
98       return -1;
99   }
100   else if (strcmp (key, "socket") == 0) {
101     socket = value;
102   }
103   else {
104     nbdkit_error ("unknown parameter '%s'", key);
105     return -1;
106   }
107
108   return 0;
109 }
110
111 static int
112 bmap_config_complete (void)
113 {
114   struct stat statbuf;
115   guestfs_h *g;
116   int err;
117
118   if (!output || !disk || !socket) {
119     nbdkit_error ("missing parameters: are you using the 'virt-bmap' wrapper?");
120     return -1;
121   }
122
123   ranges = new_ranges ();
124
125   fd = open (disk, O_RDONLY);
126   if (fd == -1) {
127     nbdkit_error ("%s: %m", disk);
128     return -1;
129   }
130
131   if (fstat (fd, &statbuf) == -1) {
132     nbdkit_error ("fstat: %m");
133     return -1;
134   }
135   size = statbuf.st_size;
136
137   /* Open the guestfs handle synchronously so we can print errors. */
138   g = guestfs_create ();
139   if (!g) {
140     nbdkit_error ("guestfs_create: %m");
141     return -1;
142   }
143
144   /* Start the guestfs thread. */
145   memset (&thread_info, 0, sizeof thread_info);
146   thread_info.g = g;
147   err = pthread_create (&thread, NULL, start_thread, &thread_info);
148   if (err != 0) {
149     nbdkit_error ("cannot start guestfs thread: %s", strerror (err));
150     return -1;
151   }
152   thread_running = 1;
153
154   return 0;
155 }
156
157 static void
158 bmap_unload (void)
159 {
160   int err;
161   void *retv;
162
163   free_ranges (ranges);
164
165   if (thread_running) {
166     err = pthread_join (thread, &retv);
167     if (err != 0)
168       fprintf (stderr, "cannot join guestfs thread: %s\n", strerror (err));
169     if (* (int *) retv != 0)
170       fprintf (stderr, "ERROR: failed to construct block map, see earlier errors\n");
171     /* unfortunately we can't return the correct exit code here XXX */
172   }
173   if (fd >= 0)
174     close (fd);
175   free (output);
176   free (disk);
177 }
178
179 #define bmap_config_help                                        \
180   "output=<OUTPUT>     Output filename (block map)\n"           \
181   "disk=<DISK>         Input disk filename"                     \
182   "format=raw|qcow2|.. Format of input disk (default: raw)\n"
183
184 /* The per-connection handle. */
185 struct bmap_handle {
186   /* empty */
187 };
188
189 /* Create the per-connection handle. */
190 static void *
191 bmap_open (int readonly)
192 {
193   struct bmap_handle *h;
194
195   if (!readonly) {
196     nbdkit_error ("handle must be opened readonly");
197     return NULL;
198   }
199
200   h = malloc (sizeof *h);
201   if (h == NULL) {
202     nbdkit_error ("malloc: %m");
203     return NULL;
204   }
205
206   return h;
207 }
208
209 /* Free up the per-connection handle. */
210 static void
211 bmap_close (void *handle)
212 {
213   struct bmap_handle *h = handle;
214
215   free (h);
216 }
217
218 #define THREAD_MODEL NBDKIT_THREAD_MODEL_SERIALIZE_ALL_REQUESTS
219
220 /* Get the file size. */
221 static int64_t
222 bmap_get_size (void *handle)
223 {
224   return size;
225 }
226
227 /* Mark the start and end of guestfs bmap operations.  This is called
228  * from the guestfs thread, so we must lock any shared data structures.
229  */
230 static void
231 mark_start (const char *object)
232 {
233   pthread_mutex_lock (&current_object_mutex);
234   free (current_object);
235   current_object = strdup (object);
236   if (current_object == NULL)
237     abort ();
238   pthread_mutex_unlock (&current_object_mutex);
239 }
240
241 static void
242 mark_end (void)
243 {
244   pthread_mutex_lock (&current_object_mutex);
245   free (current_object);
246   current_object = NULL;
247   pthread_mutex_unlock (&current_object_mutex);
248 }
249
250 static void
251 add_range (uint64_t offset, uint32_t count)
252 {
253   pthread_mutex_lock (&current_object_mutex);
254   if (current_object)
255     insert_range (ranges, offset, offset+count, current_object);
256   pthread_mutex_unlock (&current_object_mutex);
257 }
258
259 /* Read data from the file. */
260 static int
261 bmap_pread (void *handle, void *buf, uint32_t count, uint64_t offset)
262 {
263   ssize_t r;
264
265   add_range (offset, count);
266
267   if (lseek (fd, offset, SEEK_SET) == -1) {
268     nbdkit_error ("lseek: %m");
269     return -1;
270   }
271
272   while (count > 0) {
273     r = read (fd, buf, count);
274     if (r == -1) {
275       nbdkit_error ("read: %m");
276       return -1;
277     }
278     if (r == 0) {
279       nbdkit_error ("read: unexpected end of file");
280       return -1;
281     }
282     count -= r;
283     buf += r;
284   }
285
286   return 0;
287 }
288
289 /* This is the guestfs thread which calls back into nbdkit. */
290
291 static int count_partitions = 0;
292 static int count_lvs = 0;
293 static int count_filesystems = 0;
294 static int count_regular = 0;
295 static int count_directory = 0;
296
297 static int examine_devices (guestfs_h *g);
298 static int examine_partitions (guestfs_h *g);
299 static int examine_lvs (guestfs_h *g);
300 static int examine_filesystems (guestfs_h *g);
301 static int examine_filesystem (guestfs_h *g, const char *dev, const char *type);
302 static int visit_fn (const char *dir, const char *name, const struct guestfs_statns *stat, const struct guestfs_xattr_list *xattrs, void *opaque);
303 static int ranges_to_output (void);
304
305 static void *
306 start_thread (void *infov)
307 {
308   struct thread_info *info = infov;
309   guestfs_h *g = info->g;
310   size_t i;
311   CLEANUP_FREE char *server = NULL;
312   const char *servers[2];
313
314   info->ret = -1;
315
316   /* Wait for the socket to appear, ie. for nbdkit to start up. */
317   for (i = 0; i < 10; ++i) {
318     if (access (socket, R_OK) == 0)
319       goto nbdkit_started;
320     sleep (1);
321   }
322
323   fprintf (stderr, "timed out waiting for nbdkit server to start up\n");
324   goto error;
325
326  nbdkit_started:
327   if (asprintf (&server, "unix:%s", socket) == -1) {
328     perror ("asprintf");
329     goto error;
330   }
331
332   servers[0] = server;
333   servers[1] = NULL;
334
335   if (guestfs_add_drive_opts (g, "" /* export name */,
336                               GUESTFS_ADD_DRIVE_OPTS_READONLY, 1,
337                               GUESTFS_ADD_DRIVE_OPTS_FORMAT, format,
338                               GUESTFS_ADD_DRIVE_OPTS_PROTOCOL, "nbd",
339                               GUESTFS_ADD_DRIVE_OPTS_SERVER, servers,
340                               -1) == -1)
341     goto error;
342
343   if (guestfs_launch (g) == -1)
344     goto error;
345
346   /* Examine non-filesystem objects. */
347   if (examine_devices (g) == -1)
348     goto error;
349   if (examine_partitions (g) == -1)
350     goto error;
351   if (examine_lvs (g) == -1)
352     goto error;
353
354   /* Examine filesystems. */
355   if (examine_filesystems (g) == -1)
356     goto error;
357
358   /* Convert ranges to final output file. */
359   printf ("virt-bmap: writing %s\n", output);
360   if (ranges_to_output () == -1)
361     goto error;
362
363   /* Print summary. */
364   printf ("virt-bmap: successfully examined %d partitions, %d logical volumes,\n"
365           "           %d filesystems, %d directories, %d files\n"
366           "virt-bmap: output written to %s\n",
367           count_partitions, count_lvs,
368           count_filesystems, count_directory, count_regular,
369           output);
370   info->ret = 0;
371  error:
372   guestfs_close (g);
373
374   /* Kill the nbdkit process so it exits.  The nbdkit process is us,
375    * so we're killing ourself here.
376    */
377   kill (getpid (), SIGTERM);
378
379   return &info->ret;
380 }
381
382 static int
383 examine_devices (guestfs_h *g)
384 {
385   CLEANUP_FREE_STRING_LIST char **devices = NULL;
386   size_t i;
387
388   /* Get list of devices. */
389   devices = guestfs_list_devices (g);
390   if (devices == NULL)
391     return -1;
392
393   for (i = 0; devices[i] != NULL; ++i) {
394     CLEANUP_FREE char *object = NULL;
395     int64_t size;
396
397     printf ("virt-bmap: examining %s ...\n", devices[i]);
398
399     if (asprintf (&object, "v %s", devices[i]) == -1)
400       return -1;
401
402     /* We don't actually bother to examine the device, which would be
403      * slow and pointless.  We just mark it in the map.
404      */
405     size = guestfs_blockdev_getsize64 (g, devices[i]);
406     if (size == -1)
407       return -1;
408
409     pthread_mutex_lock (&current_object_mutex);
410     insert_range (ranges, 0, size, object);
411     pthread_mutex_unlock (&current_object_mutex);
412   }
413
414   return 0;
415 }
416
417 static int
418 examine_partitions (guestfs_h *g)
419 {
420   CLEANUP_FREE_STRING_LIST char **parts = NULL;
421   size_t i;
422   int r;
423
424   /* Get partitions. */
425   parts = guestfs_list_partitions (g);
426   if (parts == NULL)
427     return -1;
428
429   for (i = 0; parts[i] != NULL; ++i) {
430     CLEANUP_FREE char *object = NULL;
431
432     printf ("virt-bmap: examining %s ...\n", parts[i]);
433     count_partitions++;
434
435     if (asprintf (&object, "p %s", parts[i]) == -1)
436       return -1;
437
438     if (guestfs_bmap_device (g, parts[i]) == -1)
439       return -1;
440     mark_start (object);
441     r = guestfs_bmap (g);
442     mark_end ();
443     if (r == -1) return -1;
444   }
445
446   return 0;
447 }
448
449 static int
450 examine_lvs (guestfs_h *g)
451 {
452   CLEANUP_FREE_STRING_LIST char **lvs = NULL;
453   size_t i;
454   int r;
455
456   /* Get LVs. */
457   lvs = guestfs_lvs (g);
458   if (lvs == NULL)
459     return -1;
460
461   for (i = 0; lvs[i] != NULL; ++i) {
462     CLEANUP_FREE char *object = NULL;
463
464     printf ("virt-bmap: examining %s ...\n", lvs[i]);
465     count_lvs++;
466
467     if (asprintf (&object, "l %s", lvs[i]) == -1)
468       return -1;
469
470     if (guestfs_bmap_device (g, lvs[i]) == -1)
471       return -1;
472     mark_start (object);
473     r = guestfs_bmap (g);
474     mark_end ();
475     if (r == -1) return -1;
476   }
477
478   return 0;
479 }
480
481 static int
482 examine_filesystems (guestfs_h *g)
483 {
484   CLEANUP_FREE_STRING_LIST char **filesystems = NULL;
485   size_t i;
486
487   /* Get the filesystems in the disk image. */
488   filesystems = guestfs_list_filesystems (g);
489   if (filesystems == NULL)
490     return -1;
491
492   for (i = 0; filesystems[i] != NULL; i += 2) {
493     if (examine_filesystem (g, filesystems[i], filesystems[i+1]) == -1)
494       return -1;
495   }
496
497   return 0;
498 }
499
500 static size_t
501 count_strings (char *const *argv)
502 {
503   size_t r;
504
505   for (r = 0; argv[r]; ++r)
506     ;
507
508   return r;
509 }
510
511 struct visit_context {
512   guestfs_h *g;
513   const char *dev;              /* filesystem */
514   size_t nr_files;              /* used for progress bar */
515   size_t files_processed;
516 };
517
518 static int
519 examine_filesystem (guestfs_h *g, const char *dev, const char *type)
520 {
521   int r;
522
523   /* Try to mount it. */
524   guestfs_push_error_handler (g, NULL, NULL);
525   r = guestfs_mount_ro (g, dev, "/");
526   guestfs_pop_error_handler (g);
527   if (r == 0) {
528     struct visit_context context;
529     CLEANUP_FREE_STRING_LIST char **files = NULL;
530
531     /* Mountable, so examine the filesystem. */
532     printf ("virt-bmap: examining filesystem on %s (%s) ...\n", dev, type);
533     count_filesystems++;
534
535     /* Read how many files/directories there are so we can estimate
536      * progress.
537      */
538     files = guestfs_find (g, "/");
539
540     /* Set filesystem readahead to 0, but ignore error if not possible. */
541     guestfs_push_error_handler (g, NULL, NULL);
542     guestfs_blockdev_setra (g, dev, 0);
543     guestfs_pop_error_handler (g);
544
545     context.g = g;
546     context.dev = dev;
547     context.nr_files = count_strings (files);
548     context.files_processed = 0;
549     if (visit (g, "/", visit_fn, &context) == -1)
550       return -1;
551   }
552
553   guestfs_umount_all (g);
554
555   return 0;
556 }
557
558 static int
559 visit_fn (const char *dir, const char *name,
560           const struct guestfs_statns *stat,
561           const struct guestfs_xattr_list *xattrs,
562           void *contextv)
563 {
564   struct visit_context *context = contextv;
565   guestfs_h *g = context->g;
566   char type = '?';
567   CLEANUP_FREE char *path = NULL, *object = NULL;
568   int r;
569
570   context->files_processed++;
571   if ((context->files_processed & 255) == 0) {
572     printf ("%zu/%zu (%.1f%%)        \r",
573             context->files_processed, context->nr_files,
574             100.0 * context->files_processed / context->nr_files);
575     fflush (stdout);
576   }
577
578   path = full_path (dir, name);
579   if (path == NULL)
580     return -1;
581
582   if (is_reg (stat->st_mode))
583     type = 'f';
584   else if (is_dir (stat->st_mode))
585     type = 'd';
586
587   /* Note that the object string is structured (space-separated
588    * fields), and this is copied directly to the output file.
589    */
590   if (asprintf (&object, "%c %s %s", type, context->dev, path) == -1)
591     return -1;
592
593   if (type == 'f') {            /* regular file */
594     count_regular++;
595   bmap_file:
596     if (guestfs_bmap_file (g, path) == -1)
597       return -1;
598     mark_start (object);
599     r = guestfs_bmap (g);
600     mark_end ();
601     if (r == -1) return -1;
602   }
603   else if (type == 'd') {       /* directory */
604     count_directory++;
605     goto bmap_file;
606   }
607
608   return 0;
609 }
610
611 /* Convert ranges to output file format. */
612 static void print_range (uint64_t start, uint64_t end, const char *object, void *opaque);
613
614 static int
615 ranges_to_output (void)
616 {
617   FILE *fp;
618
619   pthread_mutex_lock (&current_object_mutex);
620
621   /* Write out the ranges to 'output'. */
622   fp = fopen (output, "w");
623   if (fp == NULL) {
624     perror (output);
625     return -1;
626   }
627
628   iter_range (ranges, print_range, fp);
629
630   fclose (fp);
631
632   pthread_mutex_unlock (&current_object_mutex);
633
634   return 0;
635 }
636
637 static void
638 print_range (uint64_t start, uint64_t end, const char *object, void *opaque)
639 {
640   FILE *fp = opaque;
641
642   /* Note that the initial '1' is meant to signify the first disk.
643    * Currently we can only map a single disk, but in future we
644    * should be able to handle multiple disks.
645    */
646   fprintf (fp, "1 %" PRIx64 " %" PRIx64 " %s\n", start, end, object);
647 }
648
649 /* Register the nbdkit plugin. */
650
651 static struct nbdkit_plugin plugin = {
652   .name              = "virtbmapexaminer",
653   .version           = PACKAGE_VERSION,
654   .config            = bmap_config,
655   .config_help       = bmap_config_help,
656   .config_complete   = bmap_config_complete,
657   .open              = bmap_open,
658   .close             = bmap_close,
659   .get_size          = bmap_get_size,
660   .pread             = bmap_pread,
661   .unload            = bmap_unload,
662 };
663
664 NBDKIT_REGISTER_PLUGIN(plugin)