From 18374b5b7d3154e0b8b8a07e3590f6eee762b58e Mon Sep 17 00:00:00 2001 From: "Richard W.M. Jones" Date: Tue, 23 Nov 2010 12:05:04 +0000 Subject: [PATCH] df: Rewrite virt-df in C. I have diffed the output from the original virt-df with this new version, and they agree very closely. Some differences: - Old virt-df have a divide-by-zero error in cases where the number of used inodes was 0. New virt-df fixes this. - New virt-df uses gnulib human_readable library which displays numbers to 3 significant figures for -h output (old version used an ad hoc function). --- .gitignore | 4 + HACKING | 3 + Makefile.am | 4 +- configure.ac | 1 + df/Makefile.am | 84 ++++++ df/README | 17 ++ df/df.c | 158 ++++++++++ df/domains.c | 436 +++++++++++++++++++++++++++ df/main.c | 306 +++++++++++++++++++ df/output.c | 242 +++++++++++++++ df/run-df-locally | 52 ++++ {tools => df}/test-virt-df.sh | 0 df/virt-df.h | 42 +++ df/virt-df.pod | 229 +++++++++++++++ po/POTFILES.in | 5 +- tools/Makefile.am | 8 +- tools/virt-df | 669 ------------------------------------------ 17 files changed, 1584 insertions(+), 676 deletions(-) create mode 100644 df/Makefile.am create mode 100644 df/README create mode 100644 df/df.c create mode 100644 df/domains.c create mode 100644 df/main.c create mode 100644 df/output.c create mode 100755 df/run-df-locally rename {tools => df}/test-virt-df.sh (100%) create mode 100644 df/virt-df.h create mode 100755 df/virt-df.pod delete mode 100755 tools/virt-df diff --git a/.gitignore b/.gitignore index 3416fbb..f86affa 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,10 @@ daemon/optgroups.h daemon/stubs.c depcomp .deps +df/stamp-virt-df.pod +df/virt-df +df/virt-df.1 +df/virt-df.static emptydisk examples/create_disk examples/guestfs-examples.3 diff --git a/HACKING b/HACKING index 05fb97b..3ea0e4a 100644 --- a/HACKING +++ b/HACKING @@ -84,6 +84,9 @@ csharp/ daemon/ The daemon that runs inside the guest and carries out actions. +df/ + 'virt-df' command and documentation. + examples/ The examples. diff --git a/Makefile.am b/Makefile.am index 538e833..7dd0e97 100644 --- a/Makefile.am +++ b/Makefile.am @@ -36,7 +36,7 @@ SUBDIRS += gnulib/tests capitests regressions test-tool SUBDIRS += fish # virt-tools in C. -SUBDIRS += cat inspector +SUBDIRS += cat df inspector # Language bindings. if HAVE_PERL @@ -221,6 +221,8 @@ bindist: cp cat/virt-cat.static $(BINTMPDIR)$(bindir)/virt-cat cp cat/virt-filesystems.static $(BINTMPDIR)$(bindir)/virt-filesystems cp cat/virt-ls.static $(BINTMPDIR)$(bindir)/virt-ls + $(MAKE) -C df virt-df.static + cp df/virt-df.static $(BINTMPDIR)$(bindir)/virt-df $(MAKE) -C inspector virt-inspector.static cp inspector/virt-inspector.static $(BINTMPDIR)$(bindir)/virt-inspector (cd $(BINTMPDIR) && tar cf - .) | \ diff --git a/configure.ac b/configure.ac index ef93b64..2543339 100644 --- a/configure.ac +++ b/configure.ac @@ -868,6 +868,7 @@ AC_CONFIG_FILES([Makefile php/Makefile csharp/Makefile cat/Makefile + df/Makefile ocaml/META perl/Makefile.PL]) AC_OUTPUT diff --git a/df/Makefile.am b/df/Makefile.am new file mode 100644 index 0000000..f148e6c --- /dev/null +++ b/df/Makefile.am @@ -0,0 +1,84 @@ +# libguestfs virt-df +# Copyright (C) 2010 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 $(top_srcdir)/subdir-rules.mk + +EXTRA_DIST = \ + README \ + run-df-locally \ + test-virt-df.sh \ + virt-df.pod + +CLEANFILES = stamp-virt-df.pod + +bin_PROGRAMS = virt-df + +SHARED_SOURCE_FILES = \ + ../fish/inspect.c \ + ../fish/keys.c \ + ../fish/options.h \ + ../fish/options.c \ + ../fish/virt.c + +virt_df_SOURCES = \ + $(SHARED_SOURCE_FILES) \ + virt-df.h \ + domains.c \ + df.c \ + main.c \ + output.c + +virt_df_CFLAGS = \ + -I$(top_srcdir)/src -I$(top_builddir)/src \ + -I$(top_srcdir)/fish \ + -I$(srcdir)/../gnulib/lib -I../gnulib/lib \ + -DLOCALEBASEDIR=\""$(datadir)/locale"\" \ + $(WARN_CFLAGS) $(WERROR_CFLAGS) + +virt_df_LDADD = \ + $(top_builddir)/src/libguestfs.la \ + ../gnulib/lib/libgnu.la -lm + +# Manual pages and HTML files for the website. +man_MANS = virt-df.1 +noinst_DATA = $(top_builddir)/html/virt-df.1.html + +virt-df.1 $(top_builddir)/html/virt-df.1.html: stamp-virt-df.pod + +stamp-virt-df.pod: virt-df.pod + $(top_srcdir)/podwrapper.sh \ + --man virt-df.1 \ + --html $(top_builddir)/html/virt-df.1.html \ + $< + touch $@ + +# Tests. + +random_val := $(shell awk 'BEGIN{srand(); print 1+int(255*rand())}' < /dev/null) + +TESTS_ENVIRONMENT = \ + MALLOC_PERTURB_=$(random_val) \ + LD_LIBRARY_PATH=$(top_builddir)/src/.libs \ + LIBGUESTFS_PATH=$(top_builddir)/appliance + +TESTS = test-virt-df.sh + +# Build a partly-static binary (for the binary distribution). + +virt-df.static$(EXEEXT): $(virt_df_OBJECTS) $(virt_df_DEPENDENCIES) + $(top_srcdir)/relink-static.sh \ + $(virt_df_LINK) $(virt_df_OBJECTS) -static $(virt_df_LDADD) $(virt_df_LIBS) $(LIBVIRT_LIBS) $(LIBXML2_LIBS) -lpcre -lhivex -lmagic -lz -lm diff --git a/df/README b/df/README new file mode 100644 index 0000000..f928f2d --- /dev/null +++ b/df/README @@ -0,0 +1,17 @@ +This is the third rewrite of the virt-df program. It very much +follows the outline of the Perl program which this replaced in +libguestfs 1.7.14. + +main.c - main program + +domains.c - dealing with libvirt, only used if libvirt is around + at compile time + +df.c - getting the stats from libguestfs + +output.c - writing the output, CSV output + +virt-df.h - header file + +Note this also uses the shared options parsing code in +'fish/options.[ch]'. diff --git a/df/df.c b/df/df.c new file mode 100644 index 0000000..c2db970 --- /dev/null +++ b/df/df.c @@ -0,0 +1,158 @@ +/* virt-df + * Copyright (C) 2010 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 + +#ifdef HAVE_LIBVIRT +#include +#include +#endif + +#include "progname.h" + +#include "guestfs.h" +#include "options.h" +#include "virt-df.h" + +static void try_df (const char *name, const char *uuid, const char *dev, int offset); +static int find_dev_in_devices (const char *dev, char **devices); + +/* Since we want this function to be robust against very bad failure + * cases (hello, https://bugzilla.kernel.org/show_bug.cgi?id=18792) it + * won't exit on guestfs failures. + */ +int +df_on_handle (const char *name, const char *uuid, char **devices, int offset) +{ + int ret = -1; + size_t i; + char **fses = NULL; + int free_devices = 0, is_lv; + + if (verbose) { + fprintf (stderr, "df_on_handle %s devices=", name); + if (devices) { + fputc ('[', stderr); + for (i = 0; devices[i] != NULL; ++i) { + if (i > 0) + fputc (' ', stderr); + fputs (devices[i], stderr); + } + fputc (']', stderr); + } + else + fprintf (stderr, "null"); + fputc ('\n', stderr); + } + + if (devices == NULL) { + devices = guestfs_list_devices (g); + if (devices == NULL) + goto cleanup; + free_devices = 1; + } else { + /* Mask LVM for just the devices in the set. */ + if (guestfs_lvm_set_filter (g, devices) == -1) + goto cleanup; + } + + /* list-filesystems will return filesystems on every device ... */ + fses = guestfs_list_filesystems (g); + if (fses == NULL) + goto cleanup; + + /* ... so we need to filter out only the devices we are interested in. */ + for (i = 0; fses[i] != NULL; i += 2) { + if (STRNEQ (fses[i+1], "") && + STRNEQ (fses[i+1], "swap") && + STRNEQ (fses[i+1], "unknown")) { + is_lv = guestfs_is_lv (g, fses[i]); + if (is_lv > 0) /* LVs are OK because of the LVM filter */ + try_df (name, uuid, fses[i], -1); + else if (is_lv == 0) { + if (find_dev_in_devices (fses[i], devices)) + try_df (name, uuid, fses[i], offset); + } + } + } + + ret = 0; + + cleanup: + if (fses) { + for (i = 0; fses[i] != NULL; ++i) + free (fses[i]); + free (fses); + } + + if (free_devices) { + for (i = 0; devices[i] != NULL; ++i) + free (devices[i]); + free (devices); + } + + return ret; +} + +static int +find_dev_in_devices (const char *dev, char **devices) +{ + size_t i; + + for (i = 0; devices[i] != NULL; ++i) { + if (STRPREFIX (dev, devices[i])) + return 1; + } + + return 0; +} + +static void +try_df (const char *name, const char *uuid, + const char *dev, int offset) +{ + struct guestfs_statvfs *stat = NULL; + guestfs_error_handler_cb old_error_cb; + void *old_error_data; + + if (verbose) + fprintf (stderr, "try_df %s %s %d\n", name, dev, offset); + + /* Try mounting and stating the device. This might reasonably fail, + * so don't show errors. + */ + old_error_cb = guestfs_get_error_handler (g, &old_error_data); + guestfs_set_error_handler (g, NULL, NULL); + + if (guestfs_mount_ro (g, dev, "/") == 0) { + stat = guestfs_statvfs (g, "/"); + guestfs_umount_all (g); + } + + guestfs_set_error_handler (g, old_error_cb, old_error_data); + + if (stat) { + print_stat (name, uuid, dev, offset, stat); + guestfs_free_statvfs (stat); + } +} diff --git a/df/domains.c b/df/domains.c new file mode 100644 index 0000000..5bc7c42 --- /dev/null +++ b/df/domains.c @@ -0,0 +1,436 @@ +/* virt-df + * Copyright (C) 2010 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 + +#ifdef HAVE_LIBVIRT +#include +#include +#endif + +#include "progname.h" + +#define GUESTFS_PRIVATE_FOR_EACH_DISK 1 + +#include "guestfs.h" +#include "options.h" +#include "virt-df.h" + +#ifdef HAVE_LIBVIRT + +/* Limit the number of devices we will ever add to the appliance. The + * overall limit in current libguestfs is 25: 26 = number of letters + * in the English alphabet since we are only confident that + * /dev/sd[a-z] will work because of various limits, minus 1 because + * that may be used by the ext2 initial filesystem. (RHBZ#635373). + */ +#define MAX_DISKS 25 + +/* The list of domains and disks that we build up in + * get_domains_from_libvirt. + */ +struct disk { + struct disk *next; + char *filename; + char *format; /* could be NULL */ +}; + +struct domain { + char *name; + char *uuid; + struct disk *disks; + size_t nr_disks; +}; + +struct domain *domains = NULL; +size_t nr_domains; + +static int +compare_domain_names (const void *p1, const void *p2) +{ + const struct domain *d1 = p1; + const struct domain *d2 = p2; + + return strcmp (d1->name, d2->name); +} + +static void +free_domain (struct domain *domain) +{ + struct disk *disk, *next; + + for (disk = domain->disks; disk; disk = next) { + next = disk->next; + free (disk->filename); + free (disk->format); + free (disk); + } + + free (domain->name); + free (domain->uuid); +} + +static void add_domains_by_id (virConnectPtr conn, int *ids, size_t n); +static void add_domains_by_name (virConnectPtr conn, char **names, size_t n); +static void add_domain (virDomainPtr dom); +static int add_disk (guestfs_h *g, const char *filename, const char *format, void *domain_vp); +static void multi_df (struct domain *, size_t n); + +void +get_domains_from_libvirt (void) +{ + virErrorPtr err; + virConnectPtr conn; + int n; + size_t i, j, nr_disks_added; + + nr_domains = 0; + domains = NULL; + + /* Get the list of all domains. */ + conn = virConnectOpenReadOnly (libvirt_uri); + if (!conn) { + err = virGetLastError (); + fprintf (stderr, + _("%s: could not connect to libvirt (code %d, domain %d): %s"), + program_name, err->code, err->domain, err->message); + exit (EXIT_FAILURE); + } + + n = virConnectNumOfDomains (conn); + if (n == -1) { + err = virGetLastError (); + fprintf (stderr, + _("%s: could not get number of running domains (code %d, domain %d): %s"), + program_name, err->code, err->domain, err->message); + exit (EXIT_FAILURE); + } + + int ids[n]; + n = virConnectListDomains (conn, ids, n); + if (n == -1) { + err = virGetLastError (); + fprintf (stderr, + _("%s: could not list running domains (code %d, domain %d): %s"), + program_name, err->code, err->domain, err->message); + exit (EXIT_FAILURE); + } + + add_domains_by_id (conn, ids, n); + + n = virConnectNumOfDefinedDomains (conn); + if (n == -1) { + err = virGetLastError (); + fprintf (stderr, + _("%s: could not get number of inactive domains (code %d, domain %d): %s"), + program_name, err->code, err->domain, err->message); + exit (EXIT_FAILURE); + } + + char *names[n]; + n = virConnectListDefinedDomains (conn, names, n); + if (n == -1) { + err = virGetLastError (); + fprintf (stderr, + _("%s: could not list inactive domains (code %d, domain %d): %s"), + program_name, err->code, err->domain, err->message); + exit (EXIT_FAILURE); + } + + add_domains_by_name (conn, names, n); + + /* You must free these even though the libvirt documentation doesn't + * mention it. + */ + for (i = 0; i < (size_t) n; ++i) + free (names[i]); + + virConnectClose (conn); + + /* No domains? */ + if (nr_domains == 0) + return; + + /* Sort the domains alphabetically by name for display. */ + qsort (domains, nr_domains, sizeof (struct domain), compare_domain_names); + + print_title (); + + /* To minimize the number of times we have to launch the appliance, + * shuffle as many domains together as we can, but not exceeding + * MAX_DISKS per request. If --one-per-guest was requested then only + * request disks from a single guest each time. + * Interesting application for NP-complete knapsack problem here. + */ + if (one_per_guest) { + for (i = 0; i < nr_domains; ++i) + multi_df (&domains[i], 1); + } else { + for (i = 0; i < nr_domains; /**/) { + nr_disks_added = 0; + + /* Make a request with domains [i..j-1]. */ + for (j = i; j < nr_domains; ++j) { + if (nr_disks_added + domains[j].nr_disks > MAX_DISKS) + break; + nr_disks_added += domains[j].nr_disks; + } + multi_df (&domains[i], j-i); + + i = j; + } + } + + /* Free up domains structure. */ + for (i = 0; i < nr_domains; ++i) + free_domain (&domains[i]); + free (domains); +} + +static void +add_domains_by_id (virConnectPtr conn, int *ids, size_t n) +{ + size_t i; + virDomainPtr dom; + + for (i = 0; i < n; ++i) { + if (ids[i] != 0) { /* RHBZ#538041 */ + dom = virDomainLookupByID (conn, ids[i]); + if (dom) { /* transient errors are possible here, ignore them */ + add_domain (dom); + virDomainFree (dom); + } + } + } +} + +static void +add_domains_by_name (virConnectPtr conn, char **names, size_t n) +{ + size_t i; + virDomainPtr dom; + + for (i = 0; i < n; ++i) { + dom = virDomainLookupByName (conn, names[i]); + if (dom) { /* transient errors are possible here, ignore them */ + add_domain (dom); + virDomainFree (dom); + } + } +} + +static void +add_domain (virDomainPtr dom) +{ + struct domain *domain; + + domains = realloc (domains, (nr_domains + 1) * sizeof (struct domain)); + if (domains == NULL) { + perror ("realloc"); + exit (EXIT_FAILURE); + } + + domain = &domains[nr_domains]; + nr_domains++; + + domain->name = strdup (virDomainGetName (dom)); + if (domain->name == NULL) { + perror ("strdup"); + exit (EXIT_FAILURE); + } + + char uuid[VIR_UUID_STRING_BUFLEN]; + if (virDomainGetUUIDString (dom, uuid) == 0) { + domain->uuid = strdup (uuid); + if (domain->uuid == NULL) { + perror ("strdup"); + exit (EXIT_FAILURE); + } + } + else + domain->uuid = NULL; + + domain->disks = NULL; + int n = guestfs___for_each_disk (g, dom, add_disk, domain); + if (n == -1) + exit (EXIT_FAILURE); + domain->nr_disks = n; + + if (domain->nr_disks > MAX_DISKS) { + fprintf (stderr, + _("%s: ignoring %s, it has too many disks (%zu > %d)"), + program_name, domain->name, domain->nr_disks, MAX_DISKS); + free_domain (domain); + nr_domains--; + return; + } +} + +static int +add_disk (guestfs_h *g, const char *filename, const char *format, + void *domain_vp) +{ + struct domain *domain = domain_vp; + struct disk *disk; + + disk = malloc (sizeof *disk); + if (disk == NULL) { + perror ("malloc"); + return -1; + } + + disk->next = domain->disks; + domain->disks = disk; + + disk->filename = strdup (filename); + if (disk->filename == NULL) { + perror ("malloc"); + return -1; + } + if (format) { + disk->format = strdup (format); + if (disk->format == NULL) { + perror ("malloc"); + return -1; + } + } + else + disk->format = NULL; + + return 0; +} + +static size_t +count_strings (char **argv) +{ + size_t i; + + for (i = 0; argv[i] != NULL; ++i) + ; + return i; +} + +static void reset_guestfs_handle (void); +static void add_disks_to_handle_reverse (struct disk *disk); + +/* Perform 'df' operation on the domain(s) given in the list. */ +static void +multi_df (struct domain *domains, size_t n) +{ + size_t i; + size_t nd; + int r; + char **devices; + + /* Add all the disks to the handle (since they were added in reverse + * order, we must add them here in reverse too). + */ + for (i = 0; i < n; ++i) + add_disks_to_handle_reverse (domains[i].disks); + + /* Launch the handle. */ + if (guestfs_launch (g) == -1) + exit (EXIT_FAILURE); + + devices = guestfs_list_devices (g); + if (devices == NULL) + exit (EXIT_FAILURE); + + /* Check the number of disks we think we added is the same as the + * number of devices returned by libguestfs. + */ + nd = 0; + for (i = 0; i < n; ++i) + nd += domains[i].nr_disks; + assert (nd == count_strings (devices)); + + nd = 0; + for (i = 0; i < n; ++i) { + /* So that &devices[nd] is a NULL-terminated list of strings. */ + char *p = devices[nd + domains[i].nr_disks]; + devices[nd + domains[i].nr_disks] = NULL; + + r = df_on_handle (domains[i].name, domains[i].uuid, &devices[nd], nd); + + /* Restore devices to original. */ + devices[nd + domains[i].nr_disks] = p; + nd += domains[i].nr_disks; + + /* Something broke in df_on_handle. Give up on the remaining + * devices for this handle, but keep going on the next handle. + */ + if (r == -1) + break; + } + + for (i = 0; devices[i] != NULL; ++i) + free (devices[i]); + free (devices); + + /* Reset the handle. */ + reset_guestfs_handle (); +} + +static void +add_disks_to_handle_reverse (struct disk *disk) +{ + if (disk == NULL) + return; + + add_disks_to_handle_reverse (disk->next); + + struct guestfs_add_drive_opts_argv optargs = { .bitmask = 0 }; + + optargs.bitmask |= GUESTFS_ADD_DRIVE_OPTS_READONLY_BITMASK; + optargs.readonly = 1; + + if (disk->format) { + optargs.bitmask |= GUESTFS_ADD_DRIVE_OPTS_FORMAT_BITMASK; + optargs.format = disk->format; + } + + if (guestfs_add_drive_opts_argv (g, disk->filename, &optargs) == -1) + exit (EXIT_FAILURE); +} + +/* Close and reopen the libguestfs handle. */ +static void +reset_guestfs_handle (void) +{ + /* Copy the settings from the old handle. */ + int verbose = guestfs_get_verbose (g); + int trace = guestfs_get_trace (g); + + guestfs_close (g); + + g = guestfs_create (); + if (g == NULL) { + fprintf (stderr, _("guestfs_create: failed to create handle\n")); + exit (EXIT_FAILURE); + } + + guestfs_set_verbose (g, verbose); + guestfs_set_trace (g, trace); +} + +#endif diff --git a/df/main.c b/df/main.c new file mode 100644 index 0000000..9565464 --- /dev/null +++ b/df/main.c @@ -0,0 +1,306 @@ +/* virt-df + * Copyright (C) 2010 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 + +#ifdef HAVE_LIBVIRT +#include +#include +#endif + +#include "progname.h" + +#include "guestfs.h" +#include "options.h" +#include "virt-df.h" + +/* These globals are shared with options.c. */ +guestfs_h *g; + +int read_only = 1; +int verbose = 0; +int keys_from_stdin = 0; +int echo_keys = 0; +const char *libvirt_uri = NULL; +int inspector = 0; + +int csv = 0; /* --csv */ +int human = 0; /* --human-readable|-h */ +int inodes = 0; /* --inodes */ +int one_per_guest = 0; /* --one-per-guest */ +int uuid = 0; /* --uuid */ + +static inline char * +bad_cast (char const *s) +{ + return (char *) s; +} + +static void __attribute__((noreturn)) +usage (int status) +{ + if (status != EXIT_SUCCESS) + fprintf (stderr, _("Try `%s --help' for more information.\n"), + program_name); + else { + fprintf (stdout, + _("%s: display free space on virtual filesystems\n" + "Copyright (C) 2010 Red Hat Inc.\n" + "Usage:\n" + " %s [--options] -d domname\n" + " %s [--options] -a disk.img [-a disk.img ...]\n" + "Options:\n" + " -a|--add image Add image\n" + " -c|--connect uri Specify libvirt URI for -d option\n" + " --csv Output as Comma-Separated Values\n" + " -d|--domain guest Add disks from libvirt guest\n" + " --format[=raw|..] Force disk format for -a option\n" + " -h|--human-readable Human-readable sizes in --long output\n" + " --help Display brief help\n" + " -i|--inodes Display inodes\n" + " --one-per-guest Separate appliance per guest\n" + " --uuid Add UUIDs to --long output\n" + " -v|--verbose Verbose messages\n" + " -V|--version Display version and exit\n" + " -x Trace libguestfs API calls\n" + "For more information, see the manpage %s(1).\n"), + program_name, program_name, program_name, + program_name); + } + exit (status); +} + +int +main (int argc, char *argv[]) +{ + /* Set global program name that is not polluted with libtool artifacts. */ + set_program_name (argv[0]); + + setlocale (LC_ALL, ""); + bindtextdomain (PACKAGE, LOCALEBASEDIR); + textdomain (PACKAGE); + + enum { HELP_OPTION = CHAR_MAX + 1 }; + + static const char *options = "a:c:d:hivVx"; + static const struct option long_options[] = { + { "add", 1, 0, 'a' }, + { "connect", 1, 0, 'c' }, + { "csv", 0, 0, 0 }, + { "domain", 1, 0, 'd' }, + { "format", 2, 0, 0 }, + { "help", 0, 0, HELP_OPTION }, + { "human-readable", 0, 0, 'h' }, + { "inodes", 0, 0, 'i' }, + { "one-per-guest", 0, 0, 0 }, + { "uuid", 0, 0, 0 }, + { "verbose", 0, 0, 'v' }, + { "version", 0, 0, 'V' }, + { 0, 0, 0, 0 } + }; + struct drv *drvs = NULL; + struct drv *drv; + const char *format = NULL; + int c; + int option_index; + + g = guestfs_create (); + if (g == NULL) { + fprintf (stderr, _("guestfs_create: failed to create handle\n")); + exit (EXIT_FAILURE); + } + + argv[0] = bad_cast (program_name); + + for (;;) { + c = getopt_long (argc, argv, options, long_options, &option_index); + if (c == -1) break; + + switch (c) { + case 0: /* options which are long only */ + if (STREQ (long_options[option_index].name, "format")) { + if (!optarg || STREQ (optarg, "")) + format = NULL; + else + format = optarg; + } else if (STREQ (long_options[option_index].name, "csv")) { + csv = 1; + } else if (STREQ (long_options[option_index].name, "one-per-guest")) { + one_per_guest = 1; + } else if (STREQ (long_options[option_index].name, "uuid")) { + uuid = 1; + } else { + fprintf (stderr, _("%s: unknown long option: %s (%d)\n"), + program_name, long_options[option_index].name, option_index); + exit (EXIT_FAILURE); + } + break; + + case 'a': + OPTION_a; + break; + + case 'c': + OPTION_c; + break; + + case 'd': + OPTION_d; + break; + + case 'h': + human = 1; + break; + + case 'i': + inodes = 1; + break; + + case 'v': + OPTION_v; + break; + + case 'V': + OPTION_V; + break; + + case 'x': + OPTION_x; + break; + + case HELP_OPTION: + usage (EXIT_SUCCESS); + + default: + usage (EXIT_FAILURE); + } + } + + /* Old-style syntax? There were no -a or -d options in the old + * virt-df which is how we detect this. + */ + if (drvs == NULL) { + while (optind < argc) { + if (strchr (argv[optind], '/') || + access (argv[optind], F_OK) == 0) { /* simulate -a option */ + drv = malloc (sizeof (struct drv)); + if (!drv) { + perror ("malloc"); + exit (EXIT_FAILURE); + } + drv->type = drv_a; + drv->a.filename = argv[optind]; + drv->a.format = NULL; + drv->next = drvs; + drvs = drv; + } else { /* simulate -d option */ + drv = malloc (sizeof (struct drv)); + if (!drv) { + perror ("malloc"); + exit (EXIT_FAILURE); + } + drv->type = drv_d; + drv->d.guest = argv[optind]; + drv->next = drvs; + drvs = drv; + } + + optind++; + } + } + + /* These are really constants, but they have to be variables for the + * options parsing code. Assert here that they have known-good + * values. + */ + assert (read_only == 1); + assert (inspector == 0); + + /* Must be no extra arguments on the command line. */ + if (optind != argc) + usage (EXIT_FAILURE); + + /* -h and --csv doesn't make sense. Spreadsheets will corrupt these + * fields. (RHBZ#600977). + */ + if (human && csv) { + fprintf (stderr, _("%s: you cannot use -h and --csv options together.\n"), + program_name); + exit (EXIT_FAILURE); + } + + /* If the user didn't specify any drives, then we ask libvirt for + * the full list of guests and drives, which we add in batches. + */ + if (drvs == NULL) { +#ifdef HAVE_LIBVIRT + get_domains_from_libvirt (); +#else + fprintf (stderr, _("%s: compiled without support for libvirt.\n"), + program_name); + exit (EXIT_FAILURE); +#endif + } + else { + const char *name; + + /* Add domains/drives from the command line (for a single guest). */ + add_drives (drvs, 'a'); + + if (guestfs_launch (g) == -1) + exit (EXIT_FAILURE); + + print_title (); + + /* Synthesize a display name. */ + switch (drvs->type) { + case drv_a: + name = strrchr (drvs->a.filename, '/'); + break; + case drv_d: + name = drvs->d.guest; + break; + case drv_N: + default: + abort (); + } + + /* XXX regression: in the Perl version we cached the UUID from the + * libvirt domain handle so it was available to us here. In this + * version the libvirt domain handle is hidden inside + * guestfs_add_domain so the UUID is not available easily for + * single '-d' command-line options. + */ + (void) df_on_handle (name, NULL, NULL, 0); + + /* Free up data structures, no longer needed after this point. */ + free_drives (drvs); + } + + guestfs_close (g); + + exit (EXIT_SUCCESS); +} diff --git a/df/output.c b/df/output.c new file mode 100644 index 0000000..52ce063 --- /dev/null +++ b/df/output.c @@ -0,0 +1,242 @@ +/* virt-df + * Copyright (C) 2010 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 + +#ifdef HAVE_LIBVIRT +#include +#include +#endif + +#include "c-ctype.h" +#include "human.h" +#include "progname.h" + +#include "guestfs.h" +#include "options.h" +#include "virt-df.h" + +static void write_csv_field (const char *field); + +void +print_title (void) +{ + const char *cols[6]; + size_t i; + + cols[0] = _("VirtualMachine"); + cols[1] = _("Filesystem"); + if (!inodes) { + if (!human) + cols[2] = _("1K-blocks"); + else + cols[2] = _("Size"); + cols[3] = _("Used"); + cols[4] = _("Available"); + cols[5] = _("Use%"); + } else { + cols[2] = _("Inodes"); + cols[3] = _("IUsed"); + cols[4] = _("IFree"); + cols[5] = _("IUse%"); + } + + if (!csv) { + /* ignore cols[0] in this mode */ + printf ("%-36s%10s %10s %10s %5s\n", + cols[1], cols[2], cols[3], cols[4], cols[5]); + } + else { + size_t i; + + for (i = 0; i < 6; ++i) { + if (i > 0) + putchar (','); + write_csv_field (cols[i]); + } + putchar ('\n'); + } +} + +static void canonical_device (char *dev, int offset); + +void +print_stat (const char *name, const char *uuid_param, + const char *dev_param, int offset, + const struct guestfs_statvfs *stat) +{ + /* First two columns are always 'name' and 'dev', followed by four + * other data columns. In text mode the 'name' and 'dev' are + * combined into a single 'name:dev' column. In CSV mode they are + * kept as two separate columns. In UUID mode the name might be + * replaced by 'uuid', if available. + */ +#define MAX_LEN (LONGEST_HUMAN_READABLE > 128 ? LONGEST_HUMAN_READABLE : 128) + char buf[4][MAX_LEN]; + const char *cols[4]; + int64_t factor, v; + float percent; + int hopts = human_round_to_nearest|human_autoscale|human_base_1024|human_SI; + size_t i, len; + + /* Make the device canonical. */ + len = strlen (dev_param) + 1; + char dev[len]; + strcpy (dev, dev_param); + if (offset >= 0) + canonical_device (dev, offset); + + if (!inodes) { /* 1K blocks */ + if (!human) { + factor = stat->bsize / 1024; + + v = stat->blocks * factor; + snprintf (buf[0], MAX_LEN, "%" PRIi64, v); + cols[0] = buf[0]; + v = (stat->blocks - stat->bfree) * factor; + snprintf (buf[1], MAX_LEN, "%" PRIi64, v); + cols[1] = buf[1]; + v = stat->bavail * factor; + snprintf (buf[2], MAX_LEN, "%" PRIi64, v); + cols[2] = buf[2]; + } else { + cols[0] = + human_readable ((uintmax_t) stat->blocks, buf[0], + hopts, stat->bsize, 1); + v = stat->blocks - stat->bfree; + cols[1] = + human_readable ((uintmax_t) v, buf[1], hopts, stat->bsize, 1); + cols[2] = + human_readable ((uintmax_t) stat->bavail, buf[2], + hopts, stat->bsize, 1); + } + + if (stat->blocks != 0) + percent = 100. - 100. * stat->bfree / stat->blocks; + else + percent = 0; + } + else { /* inodes */ + snprintf (buf[0], MAX_LEN, "%" PRIi64, stat->files); + cols[0] = buf[0]; + snprintf (buf[1], MAX_LEN, "%" PRIi64, stat->files - stat->ffree); + cols[1] = buf[1]; + snprintf (buf[2], MAX_LEN, "%" PRIi64, stat->ffree); + cols[2] = buf[2]; + + if (stat->files != 0) + percent = 100. - 100. * stat->ffree / stat->files; + else + percent = 0; + } + + if (!csv) + /* Use 'ceil' on the percentage in order to emulate what df itself does. */ + snprintf (buf[3], MAX_LEN, "%3.0f%%", ceil (percent)); + else + snprintf (buf[3], MAX_LEN, "%.1f", percent); + cols[3] = buf[3]; + +#undef MAX_LEN + + if (uuid && uuid_param) + name = uuid_param; + + if (!csv) { + len = strlen (name) + strlen (dev) + 1; + printf ("%s:%s", name, dev); + if (len <= 36) { + for (i = len; i < 36; ++i) + putchar (' '); + } else { + printf ("\n "); + } + + printf ("%10s %10s %10s %5s\n", cols[0], cols[1], cols[2], cols[3]); + } + else { + write_csv_field (name); + putchar (','); + write_csv_field (dev); + + for (i = 0; i < 4; ++i) { + putchar (','); + write_csv_field (cols[i]); + } + + putchar ('\n'); + } +} + +/* /dev/vda1 -> /dev/sda, adjusting the device offset. */ +static void +canonical_device (char *dev, int offset) +{ + if (STRPREFIX (dev, "/dev/") && + (dev[5] == 'h' || dev[5] == 'v') && + dev[6] == 'd' && + c_isalpha (dev[7]) && + (c_isdigit (dev[8]) || dev[8] == '\0')) { + dev[5] = 's'; + dev[7] -= offset; + } +} + +/* Function to quote CSV fields on output without requiring an + * external module. + */ +static void +write_csv_field (const char *field) +{ + size_t i, len; + int needs_quoting = 0; + + len = strlen (field); + + for (i = 0; i < len; ++i) { + if (field[i] == ' ' || field[i] == '"' || + field[i] == '\n' || field[i] == ',') { + needs_quoting = 1; + break; + } + } + + if (!needs_quoting) { + printf ("%s", field); + return; + } + + /* Quoting for CSV fields. */ + putchar ('"'); + for (i = 0; i < len; ++i) { + if (field[i] == '"') { + putchar ('"'); + putchar ('"'); + } else + putchar (field[i]); + } + putchar ('"'); +} diff --git a/df/run-df-locally b/df/run-df-locally new file mode 100755 index 0000000..2456ab5 --- /dev/null +++ b/df/run-df-locally @@ -0,0 +1,52 @@ +#!/usr/bin/perl +# 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. + +# This script sets up the environment so you can run virt-* tools in +# place without needing to do 'make install' first. You can also run +# the tools by creating a symlink to this script and putting it in +# your path. +# +# Use it like this: +# ./run-df-locally [usual virt-df args ...] + +use strict; +use warnings; + +use File::Basename qw(dirname); +use File::Spec; +use Cwd qw(abs_path); + +my $path = $0; + +# Follow symlinks until we get to the real file +while(-l $path) { + my $link = readlink($path) or die "readlink: $path: $!"; + if(File::Spec->file_name_is_absolute($link)) { + $path = $link; + } else { + $path = File::Spec->catfile(dirname($path), $link); + } +} + +# Get the absolute path of the parent directory +$path = abs_path(dirname($path).'/..'); + +$ENV{LD_LIBRARY_PATH} = $path.'/src/.libs'; +$ENV{LIBGUESTFS_PATH} = $path.'/appliance'; + +#print (join " ", ("$path/df/virt-df", @ARGV), "\n"); +exec("$path/df/virt-df", @ARGV); diff --git a/tools/test-virt-df.sh b/df/test-virt-df.sh similarity index 100% rename from tools/test-virt-df.sh rename to df/test-virt-df.sh diff --git a/df/virt-df.h b/df/virt-df.h new file mode 100644 index 0000000..4a3179e --- /dev/null +++ b/df/virt-df.h @@ -0,0 +1,42 @@ +/* virt-df + * Copyright (C) 2010 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. + */ + +#ifndef GUESTFS_VIRT_DF_ +#define GUESTFS_VIRT_DF_ + +extern guestfs_h *g; +extern const char *libvirt_uri; /* --connect */ +extern int csv; /* --csv */ +extern int human; /* --human-readable|-h */ +extern int inodes; /* --inodes */ +extern int one_per_guest; /* --one-per-guest */ +extern int uuid; /* --uuid */ + +/* df.c */ +extern int df_on_handle (const char *name, const char *uuid, char **devices, int offset); + +/* domains.c */ +#ifdef HAVE_LIBVIRT +extern void get_domains_from_libvirt (void); +#endif + +/* output.c */ +extern void print_title (void); +extern void print_stat (const char *name, const char *uuid, const char *dev, int offset, const struct guestfs_statvfs *stat); + +#endif /* GUESTFS_VIRT_DF_ */ diff --git a/df/virt-df.pod b/df/virt-df.pod new file mode 100755 index 0000000..5d06f9b --- /dev/null +++ b/df/virt-df.pod @@ -0,0 +1,229 @@ +=encoding utf8 + +=head1 NAME + +virt-df - Display free space on virtual filesystems + +=head1 SYNOPSIS + + virt-df [--options] + + virt-df [--options] -d domname + + virt-df [--options] -a disk.img [-a disk.img ...] + +Old style: + + virt-df [--options] domname + + virt-df [--options] disk.img [disk.img ...] + +=head1 DESCRIPTION + +C is a command line tool to display free space on virtual +machine filesystems. Unlike other tools, it doesn't just display the +amount of space allocated to a virtual machine, but can look inside +the virtual machine to see how much space is really being used. + +It is like the L command, but for virtual machines, except that +it also works for Windows virtual machines. + +If used without any arguments, C checks with libvirt to get a +list of all active and inactive guests, and performs a C-type +operation on each one in turn, printing out the results. + +If used with any argument(s), C performs a C-type +operation on either the single named libvirt domain, or on the disk +image(s) listed on the command line (which must all belong to a single +VM). In this mode (with arguments), C will I. If you want to run on multiple guests, then you have +to invoke C multiple times. + +Use the C<--csv> option to get a format which can be easily parsed by +other programs. Other options are mostly similar to standard C +options. See below for the complete list. + +=head1 OPTIONS + +=over 4 + +=item B<--help> + +Display brief help. + +=item B<-a> file + +=item B<--add> file + +Add I which should be a disk image from a virtual machine. If +the virtual machine has multiple block devices, you must supply all of +them with separate I<-a> options. + +The format of the disk image is auto-detected. To override this and +force a particular format use the I<--format=..> option. + +=item B<-c> URI + +=item B<--connect> URI + +If using libvirt, connect to the given I. If omitted, then we +connect to the default libvirt hypervisor. + +If you specify guest block devices directly (I<-a>), then libvirt is +not used at all. + +=item B<-d> guest + +=item B<--domain> guest + +Add all the disks from the named libvirt guest. + +=item B<--format=raw|qcow2|..> + +=item B<--format> + +The default for the I<-a> option is to auto-detect the format of the +disk image. Using this forces the disk format for I<-a> options which +follow on the command line. Using I<--format> with no argument +switches back to auto-detection for subsequent I<-a> options. + +For example: + + virt-df --format=raw -a disk.img + +forces raw format (no auto-detection) for C. + + virt-df --format=raw -a disk.img --format -a another.img + +forces raw format (no auto-detection) for C and reverts to +auto-detection for C. + +If you have untrusted raw-format guest disk images, you should use +this option to specify the disk format. This avoids a possible +security problem with malicious guests (CVE-2010-3851). See also +L. + +=item B<-h> + +=item B<--human-readable> + +Print sizes in human-readable format. + +You are not allowed to use I<-h> and I<--csv> at the same time. + +=item B<--inodes> | B<-i> + +Print inodes instead of blocks. + +=item B<--one-per-guest> + +Run one libguestfs appliance per guest. Normally C will +add the disks from several guests to a single libguestfs appliance. + +You might use this option in the following circumstances: + +=over 4 + +=item * + +If you think an untrusted guest might actively try to exploit the +libguestfs appliance kernel, then this prevents one guest from +interfering with the stats printed for another guest. + +=item * + +If the kernel has a bug which stops it from accessing a +filesystem in one guest (see for example RHBZ#635373) then +this allows libguestfs to continue and report stats for further +guests. + +=back + +=item B<--uuid> + +Print UUIDs instead of names. This is useful for following +a guest even when the guest is migrated or renamed, or when +two guests happen to have the same name. + +Note that only domains that we fetch from libvirt come with UUIDs. +For disk images, we still print the disk image name even when +this option is specified. + +=item B<-v> + +=item B<--verbose> + +Enable verbose messages for debugging. + +=item B<-V> + +=item B<--version> + +Display version number and exit. + +=item B<-x> + +Enable tracing of libguestfs API calls. + +=back + +=head1 NOTE ABOUT CSV FORMAT + +Comma-separated values (CSV) is a deceptive format. It I like +it should be easy to parse, but it is definitely not easy to parse. + +Myth: Just split fields at commas. Reality: This does I work +reliably. This example has two columns: + + "foo,bar",baz + +Myth: Read the file one line at a time. Reality: This does I +work reliably. This example has one row: + + "foo + bar",baz + +For shell scripts, use C (L +also packaged in major Linux distributions). + +For other languages, use a CSV processing library (eg. C +for Perl or Python's built-in csv library). + +Most spreadsheets and databases can import CSV directly. + +=head1 SHELL QUOTING + +Libvirt guest names can contain arbitrary characters, some of which +have meaning to the shell such as C<#> and space. You may need to +quote or escape these characters on the command line. See the shell +manual page L for details. + +=head1 SEE ALSO + +L, +L, +L, +L, +L. + +=head1 AUTHOR + +Richard W.M. Jones L + +=head1 COPYRIGHT + +Copyright (C) 2009-2010 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. diff --git a/po/POTFILES.in b/po/POTFILES.in index 61bf435..5072fc7 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -74,6 +74,10 @@ daemon/wc.c daemon/xattr.c daemon/zero.c daemon/zerofree.c +df/df.c +df/domains.c +df/main.c +df/output.c fish/alloc.c fish/cmds.c fish/cmds_gperf.c @@ -137,7 +141,6 @@ src/proto.c src/virt.c test-tool/helper.c test-tool/test-tool.c -tools/virt-df.pl tools/virt-edit.pl tools/virt-list-filesystems.pl tools/virt-list-partitions.pl diff --git a/tools/Makefile.am b/tools/Makefile.am index 4ab8369..256d15c 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -18,7 +18,6 @@ include $(top_srcdir)/subdir-rules.mk tools = \ - df \ edit \ list-filesystems \ list-partitions \ @@ -41,10 +40,10 @@ bin_SCRIPTS = $(tools:%=virt-%) # Manual pages and HTML files for the website. -# XXX Bug in automake? If you list virt-df.1 explicitly, then it +# XXX Bug in automake? If you list virt-edit.1 explicitly, then it # builds and installs the man pages. However if this is removed, # then the man pages are neither built nor installed. -man_MANS = virt-df.1 $(patsubst %,virt-%.1,$(filter-out df,$(tools))) +man_MANS = virt-edit.1 $(patsubst %,virt-%.1,$(filter-out edit,$(tools))) noinst_DATA = $(tools:%=$(top_builddir)/html/virt-%.1.html) @@ -68,8 +67,7 @@ TESTS_ENVIRONMENT = \ LIBGUESTFS_PATH=$(top_builddir)/appliance \ PERL5LIB=$(top_builddir)/perl/blib/lib:$(top_builddir)/perl/blib/arch -TESTS = test-virt-df.sh \ - test-virt-list-filesystems.sh \ +TESTS = test-virt-list-filesystems.sh \ test-virt-make-fs.sh \ test-virt-resize.sh \ test-virt-tar.sh diff --git a/tools/virt-df b/tools/virt-df deleted file mode 100755 index 6ec1c1a..0000000 --- a/tools/virt-df +++ /dev/null @@ -1,669 +0,0 @@ -#!/usr/bin/perl -w -# virt-df -# Copyright (C) 2009-2010 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. - -use warnings; -use strict; - -use Sys::Guestfs; -use Sys::Guestfs::Lib qw(feature_available); - -use Pod::Usage; -use Getopt::Long; -use File::Basename qw(basename); -use POSIX qw(ceil); - -use Locale::TextDomain 'libguestfs'; - -=encoding utf8 - -=head1 NAME - -virt-df - Display free space on virtual filesystems - -=head1 SYNOPSIS - - virt-df [--options] - - virt-df [--options] domname - - virt-df [--options] disk.img [disk.img ...] - -=head1 DESCRIPTION - -C is a command line tool to display free space on virtual -machine filesystems. Unlike other tools, it doesn't just display the -amount of space allocated to a virtual machine, but can look inside -the virtual machine to see how much space is really being used. - -It is like the L command, but for virtual machines, except that -it also works for Windows virtual machines. - -If used without any arguments, C checks with libvirt to get a -list of all active and inactive guests, and performs a C-type -operation on each one in turn, printing out the results. - -If used with any argument(s), C performs a C-type -operation on either the single named libvirt domain, or on the disk -image(s) listed on the command line (which must all belong to a single -VM). In this mode (with arguments), C will I. If you want to run on multiple guests, then you have -to invoke C multiple times. - -Use the C<--csv> option to get a format which can be easily parsed by -other programs. Other options are mostly similar to standard C -options. See below for the complete list. - -=head1 OPTIONS - -=over 4 - -=cut - -my $help; - -=item B<--help> - -Display brief help. - -=cut - -my $version; - -=item B<--version> - -Display version number and exit. - -=cut - -my $uri; - -=item B<--connect URI> | B<-c URI> - -If using libvirt, connect to the given I. If omitted, then we -connect to the default libvirt hypervisor. - -If you specify guest block devices directly, then libvirt is not used -at all. - -=cut - -my $csv; - -=item B<--csv> - -Write out the results in CSV format (comma-separated values). This format -can be imported easily into databases and spreadsheets, but -read L below. - -=cut - -my $format; - -=item B<--format> raw - -Specify the format of disk images given on the command line. If this -is omitted then the format is autodetected from the content of the -disk image. - -If disk images are requested from libvirt, then this program asks -libvirt for this information. In this case, the value of the format -parameter is ignored. - -If working with untrusted raw-format guest disk images, you should -ensure the format is always specified. - -=cut - -my $human; - -=item B<--human-readable> | B<-h> - -Print sizes in human-readable format. - -You are not allowed to use I<-h> and I<--csv> at the same time. - -=cut - -my $inodes; - -=item B<--inodes> | B<-i> - -Print inodes instead of blocks. - -=cut - -my $one_per_guest; - -=item B<--one-per-guest> - -Run one libguestfs appliance per guest. Normally C will -add the disks from several guests to a single libguestfs appliance. - -You might use this option in the following circumstances: - -=over 4 - -=item * - -If you think an untrusted guest might actively try to exploit the -libguestfs appliance kernel, then this prevents one guest from -interfering with the stats printed for another guest. - -=item * - -If the kernel has a bug which stops it from accessing a -filesystem in one guest (see for example RHBZ#635373) then -this allows libguestfs to continue and report stats for further -guests. - -=back - -=cut - -my $uuid; - -=item B<--uuid> - -Print UUIDs instead of names. This is useful for following -a guest even when the guest is migrated or renamed, or when -two guests happen to have the same name. - -Note that only domains that we fetch from libvirt come with UUIDs. -For disk images, we still print the disk image name even when -this option is specified. - -=back - -=cut - -GetOptions ("help|?" => \$help, - "version" => \$version, - "connect|c=s" => \$uri, - "csv" => \$csv, - "format=s" => \$format, - "human-readable|human|h" => \$human, - "inodes|i" => \$inodes, - "one-per-guest" => \$one_per_guest, - "uuid" => \$uuid, - ) or pod2usage (2); -pod2usage (1) if $help; -if ($version) { - my $g = Sys::Guestfs->new (); - my %h = $g->version (); - print "$h{major}.$h{minor}.$h{release}$h{extra}\n"; - exit -} - -# RHBZ#600977 -die __"virt-df: cannot use -h and --csv options together\n" if $human && $csv; - -# RHBZ#635373 -# -# Limit the number of devices we will ever add to the appliance. The -# overall limit in current libguestfs is 25: 26 = number of letters in -# the English alphabet since we are only confident that /dev/sd[a-z] -# will work because of various limits, minus 1 because that may be -# used by the ext2 initial filesystem. -my $max_disks = 25; - -# Get the list of domains and block devices. -# -# We can't use Sys::Guestfs::Lib::open_guest here because we want to -# create the libguestfs handle/appliance as few times as possible. -# -# If virt-df is called with no parameters, then run the libvirt -# equivalent of "virsh list --all", get the XML for each domain, and -# get the disk devices. -# -# If virt-df is called with parameters, assume it must either be a -# single disk image filename, a list of disk image filenames, or a -# single libvirt guest name. Construct disk devices accordingly. - -my @domains = (); - -if (@ARGV == 0) { # No params, use libvirt. - my $conn; - - if ($uri) { - $conn = Sys::Virt->new (readonly => 1, address => $uri); - } else { - $conn = Sys::Virt->new (readonly => 1); - } - - my @doms = $conn->list_defined_domains (); - push @doms, $conn->list_domains (); - - # https://bugzilla.redhat.com/show_bug.cgi?id=538041 - @doms = grep { $_->get_id () != 0 } @doms; - - exit 0 unless @doms; - - foreach my $dom (@doms) { - my @disks = get_disks_from_libvirt ($dom); - push @domains, { dom => $dom, - name => $dom->get_name (), - uuid => $dom->get_uuid_string (), - disks => \@disks } - } -} elsif (@ARGV == 1) { # One param, could be disk image or domname. - if (-e $ARGV[0]) { - push @domains, { name => basename ($ARGV[0]), - disks => [ [ $ARGV[0], $format ] ] } - } else { - my $conn; - - if ($uri) { - $conn = Sys::Virt->new (readonly => 1, address => $uri); - } else { - $conn = Sys::Virt->new (readonly => 1); - } - - my $dom = $conn->get_domain_by_name ($ARGV[0]) - or die __x("{name} is not the name of a libvirt domain\n", - name => $ARGV[0]); - my @disks = get_disks_from_libvirt ($dom); - push @domains, { dom => $dom, - name => $dom->get_name (), - uuid => $dom->get_uuid_string (), - disks => \@disks } - } -} else { # >= 2 params, all disk images. - my @disks = map { [ $_, $format ] } @ARGV; - push @domains, { name => basename ($ARGV[0]), - disks => \@disks } -} - -sub get_disks_from_libvirt -{ - my $dom = shift; - my $xml = $dom->get_xml_description (); - - my $p = XML::XPath->new (xml => $xml); - my $nodes = $p->find ('//devices/disk'); - - my @disks; - my $node; - foreach $node ($nodes->get_nodelist) { - # The filename can be in dev or file attribute, hence: - my $filename = $p->find ('./source/@dev', $node); - unless ($filename) { - $filename = $p->find ('./source/@file', $node); - next unless $filename; - } - $filename = $filename->to_literal; - - # Get the disk format (may not be set). - my $format = $p->find ('./driver/@type', $node); - $format = $format->to_literal if $format; - - push @disks, [ $filename, $format ]; - } - - # Code in Sys::Guestfs::Lib dies here if there are no disks at all. - - return @disks; -} - -# Sort the domains by name for display. -@domains = sort { $a->{name} cmp $b->{name} } @domains; - -# Since we got this far, we're somewhat sure we're going to -# get to print the result, so display the title. -print_title (); - -# To minimize the number of times we have to launch the appliance, -# shuffle as many domains together as we can, but not exceeding -# MAX_DISKS per request. If --one-per-guest was requested then only -# request disks from a single guest each time. -if ($one_per_guest) { - foreach (@domains) { - my @request = ( $_ ); - multi_df (@request); - } -} else { - while (@domains) { - my $n = 0; # number of disks added so far - my @request = (); - while (@domains) { - my $c = @{$domains[0]->{disks}}; - if ($c > $max_disks) { - warn __x("virt-df: ignoring {name}, it has too many disks ({c} > {max})", - name => $domains[0]->{name}, - c => $c, max => $max_disks); - next; - } - last if $n + $c > $max_disks; - $n += $c; - push @request, shift (@domains); - } - multi_df (@request); - } -} - -sub multi_df -{ - local $_; - eval { - my $g = Sys::Guestfs->new (); - - my ($d, $disk); - - foreach $d (@_) { - foreach $disk (@{$d->{disks}}) { - my $filename = $disk->[0]; - my $format = $disk->[1]; - my @args = ($filename); - push @args, readonly => 1; - push @args, format => $format if defined $format; - $g->add_drive_opts (@args); - } - } - - $g->launch (); - my $has_lvm2 = feature_available ($g, "lvm2"); - - my @devices = $g->list_devices (); - my @partitions = $g->list_partitions (); - - my $n = 0; - foreach $d (@_) { - my $name = $d->{name}; - my $uuid = $d->{uuid}; - my $nr_disks = @{$d->{disks}}; - - # Filter LVM to only the devices applying to the original domain. - my @devs = @devices[$n .. $n+$nr_disks-1]; - $g->lvm_set_filter (\@devs) if $has_lvm2; - - # Find which whole devices (RHBZ#590167), partitions and LVs - # contain mountable filesystems. Stat those which are - # mountable, and ignore the others. - foreach (@devs) { - try_df ($name, $uuid, $g, $_, canonical_dev ($_, $n)); - } - foreach (filter_partitions (\@devs, @partitions)) { - try_df ($name, $uuid, $g, $_, canonical_dev ($_, $n)); - } - if ($has_lvm2) { - foreach ($g->lvs ()) { - try_df ($name, $uuid, $g, $_); - } - } - - $n += $nr_disks; - } - }; - warn if $@; -} - -sub filter_partitions -{ - my $devs = shift; - my @devs = @$devs; - my @r; - - foreach my $p (@_) { - foreach my $d (@devs) { - if ($p =~ /^$d\d/) { - push @r, $p; - last; - } - } - } - - return @r; -} - -# Calculate the canonical name for a device. -# eg: /dev/vdb1 when offset = 1 -# => canonical name is /dev/sda1 -sub canonical_dev -{ - local $_; - my $dev = shift; - my $offset = shift; - - return $dev unless $dev =~ m{^/dev/.d([a-z])(\d*)$}; - my $disk = $1; - my $partnum = $2; - - $disk = chr (ord ($disk) - $offset); - - return "/dev/sd$disk$partnum" -} - -sub try_df -{ - local $_; - my $domname = shift; - my $domuuid = shift; - my $g = shift; - my $dev = shift; - my $display = shift || $dev; - - my %stat; - eval { - $g->mount_ro ($dev, "/"); - %stat = $g->statvfs ("/"); - }; - if (!$@) { - print_stat ($domname, $domuuid, $display, \%stat); - } - $g->umount_all (); -} - -sub print_stat -{ - my $domname = shift; - my $domuuid = shift; - my $dev = shift; - my $stat = shift; - - my @cols; - if (!$uuid || !defined $domuuid) { - push @cols, $domname; - } else { - push @cols, $domuuid; - } - push @cols, $dev; - - if (!$inodes) { - my $bsize = $stat->{bsize}; # block size - my $blocks = $stat->{blocks}; # total number of blocks - my $bfree = $stat->{bfree}; # blocks free (total) - my $bavail = $stat->{bavail}; # blocks free (for non-root users) - - my $factor = $bsize / 1024; - - push @cols, $blocks*$factor; # total 1K blocks - push @cols, ($blocks-$bfree)*$factor; # total 1K blocks used - push @cols, $bavail*$factor; # total 1K blocks available - - push @cols, 100.0 - 100.0 * $bfree / $blocks; - - if ($human) { - $cols[2] = human_size ($cols[2]); - $cols[3] = human_size ($cols[3]); - $cols[4] = human_size ($cols[4]); - } - } else { - my $files = $stat->{files}; # total number of inodes - my $ffree = $stat->{ffree}; # inodes free (total) - my $favail = $stat->{favail}; # inodes free (for non-root users) - - push @cols, $files; - push @cols, $files-$ffree; - push @cols, $ffree; - - push @cols, 100.0 - 100.0 * $ffree / $files; - } - - print_cols (@cols); -} - -sub print_title -{ - my @cols = (__"Virtual Machine", __"Filesystem"); - if (!$inodes) { - if (!$human) { - push @cols, __"1K-blocks"; - } else { - push @cols, __"Size"; - } - push @cols, __"Used"; - push @cols, __"Available"; - push @cols, __"Use%"; - } else { - push @cols, __"Inodes"; - push @cols, __"IUsed"; - push @cols, __"IFree"; - push @cols, __"IUse%"; - } - - if (!$csv) { - # ignore $cols[0] in this mode - printf "%-36s%10s %10s %10s %5s\n", - $cols[1], $cols[2], $cols[3], $cols[4], $cols[5]; - } else { - # Columns don't need special CSV quoting. - print (join (",", @cols), "\n"); - } -} - -sub print_cols -{ - if (!$csv) { - my $label = sprintf "%s:%s", $_[0], $_[1]; - - printf ("%-36s", $label); - print "\n"," "x36 if length ($label) > 36; - - # Use 'ceil' on the percentage in order to emulate - # what df itself does. - my $percent = sprintf "%3d%%", ceil($_[5]); - - printf ("%10s %10s %10s %5s\n", $_[2], $_[3], $_[4], $percent); - } else { - # Need to quote libvirt domain and filesystem. - my $dom = shift; - my $fs = shift; - print csv_quote($dom), ",", csv_quote($fs), ","; - printf ("%d,%d,%d,%.1f%%\n", @_); - } -} - -# Convert a number of 1K blocks to a human-readable number. -sub human_size -{ - local $_ = shift; - - if ($_ < 1024) { - sprintf "%dK", $_; - } elsif ($_ < 1024 * 1024) { - sprintf "%.1fM", ($_ / 1024); - } else { - sprintf "%.1fG", ($_ / 1024 / 1024); - } -} - -# Quote field for CSV without using an external module. -sub csv_quote -{ - local $_ = shift; - - my $needs_quoting = /[ ",\n\0]/; - return $_ unless $needs_quoting; - - my $i; - my $out = '"'; - for ($i = 0; $i < length; ++$i) { - my $c = substr $_, $i, 1; - if ($c eq '"') { - $out .= '""'; - } elsif ($c eq '\0') { - $out .= '"0'; - } else { - $out .= $c; - } - } - $out .= '"'; - - return $out; -} - -=head1 NOTE ABOUT CSV FORMAT - -Comma-separated values (CSV) is a deceptive format. It I like -it should be easy to parse, but it is definitely not easy to parse. - -Myth: Just split fields at commas. Reality: This does I work -reliably. This example has two columns: - - "foo,bar",baz - -Myth: Read the file one line at a time. Reality: This does I -work reliably. This example has one row: - - "foo - bar",baz - -For shell scripts, use C (L -also packaged in major Linux distributions). - -For other languages, use a CSV processing library (eg. C -for Perl or Python's built-in csv library). - -Most spreadsheets and databases can import CSV directly. - -=head1 SHELL QUOTING - -Libvirt guest names can contain arbitrary characters, some of which -have meaning to the shell such as C<#> and space. You may need to -quote or escape these characters on the command line. See the shell -manual page L for details. - -=head1 SEE ALSO - -L, -L, -L, -L, -L, -L. - -=head1 AUTHOR - -Richard W.M. Jones L - -=head1 COPYRIGHT - -Copyright (C) 2009-2010 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. -- 1.8.3.1