New tools: virt-resize and virt-list-partitions.
authorRichard Jones <rjones@redhat.com>
Mon, 22 Mar 2010 18:38:31 +0000 (18:38 +0000)
committerRichard Jones <rjones@redhat.com>
Tue, 23 Mar 2010 21:56:14 +0000 (21:56 +0000)
Virt-resize is the main contribution here, a program which can
be used to expand and shrink partitions in disk images.

Virt-list-partitions is used as an ancillary tool for planning
resize operations.

.gitignore
Makefile.am
po/POTFILES.in
tools/Makefile.am
tools/virt-list-filesystems
tools/virt-list-partitions [new file with mode: 0755]
tools/virt-resize [new file with mode: 0644]

index 5914499..6a47234 100644 (file)
@@ -91,8 +91,10 @@ html/virt-df.1.html
 html/virt-edit.1.html
 html/virt-inspector.1.html
 html/virt-list-filesystems.1.html
+html/virt-list-partitions.1.html
 html/virt-ls.1.html
 html/virt-rescue.1.html
+html/virt-resize.1.html
 html/virt-tar.1.html
 html/virt-win-reg.1.html
 images/100kallnewlines
index 87da5a8..3ed7815 100644 (file)
@@ -129,8 +129,10 @@ HTMLFILES = \
        html/virt-edit.1.html \
        html/virt-inspector.1.html \
        html/virt-list-filesystems.1.html \
+       html/virt-list-partitions.1.html \
        html/virt-ls.1.html \
        html/virt-rescue.1.html \
+       html/virt-resize.1.html \
        html/virt-tar.1.html \
        html/virt-win-reg.1.html \
        html/recipes.html \
@@ -165,8 +167,10 @@ all-local:
            -name 'virt-edit' -o \
            -name 'virt-inspector' -o \
            -name 'virt-list-filesystems' -o \
+           -name 'virt-list-partitions' -o \
            -name 'virt-ls' -o \
            -name 'virt-rescue' -o \
+           -name 'virt-resize' -o \
            -name 'virt-tar' -o \
            -name 'virt-win-reg' | \
        grep -v '^perl/blib/' | \
index c88abc5..92106b6 100644 (file)
@@ -102,7 +102,9 @@ tools/virt-cat
 tools/virt-df
 tools/virt-edit
 tools/virt-list-filesystems
+tools/virt-list-partitions
 tools/virt-ls
 tools/virt-rescue
+tools/virt-resize
 tools/virt-tar
 tools/virt-win-reg
index 6e6872c..943b1eb 100644 (file)
@@ -17,7 +17,7 @@
 
 include $(top_srcdir)/subdir-rules.mk
 
-tools = cat df edit list-filesystems ls rescue tar win-reg
+tools = cat df edit list-filesystems list-partitions ls rescue resize tar win-reg
 
 EXTRA_DIST = \
        run-locally \
index 0d52745..f0ee45f 100755 (executable)
@@ -194,6 +194,7 @@ L<guestfs(3)>,
 L<guestfish(1)>,
 L<virt-cat(1)>,
 L<virt-tar(1)>,
+L<virt-list-partitions(1)>,
 L<Sys::Guestfs(3)>,
 L<Sys::Guestfs::Lib(3)>,
 L<Sys::Virt(3)>,
diff --git a/tools/virt-list-partitions b/tools/virt-list-partitions
new file mode 100755 (executable)
index 0000000..b8bc0cc
--- /dev/null
@@ -0,0 +1,224 @@
+#!/usr/bin/perl -w
+# virt-list-partitions
+# 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.
+
+use warnings;
+use strict;
+
+use Sys::Guestfs;
+use Sys::Guestfs::Lib qw(open_guest);
+use Pod::Usage;
+use Getopt::Long;
+use Locale::TextDomain 'libguestfs';
+
+=encoding utf8
+
+=head1 NAME
+
+virt-list-partitions - List partitions in a virtual machine or disk image
+
+=head1 SYNOPSIS
+
+ virt-list-partitions [--options] domname
+
+ virt-list-partitions [--options] disk.img [disk.img ...]
+
+=head1 DESCRIPTION
+
+C<virt-list-partitions> is a command line tool to list
+the partitions that are contained in a virtual machine or
+disk image.  It is mainly useful as a first step to using
+L<virt-resize(1)>.
+
+C<virt-list-partitions> is just a simple wrapper around
+L<libguestfs(3)> functionality.  For more complex cases you should
+look at the L<guestfish(1)> tool.
+
+=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<URI>.  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 $long;
+
+=item B<-l> | B<--long>
+
+With this option, C<virt-list-partitions> displays the type
+and size of each partition too (where "type" means C<ext3>, C<pv> etc.)
+
+=cut
+
+my $human;
+
+=item B<-h> | B<--human-readable>
+
+Show sizes in human-readable form (eg. "1G").
+
+=back
+
+=cut
+
+# Configure bundling, otherwise '-lh' is unrecognized.
+Getopt::Long::Configure ("bundling");
+
+GetOptions ("help|?" => \$help,
+            "version" => \$version,
+            "connect|c=s" => \$uri,
+            "long|l" => \$long,
+            "human-readable|h" => \$human,
+    ) 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
+}
+
+pod2usage (__"virt-list-partitions: no image or VM name given")
+    if @ARGV <= 0;
+
+my $g;
+if ($uri) {
+    $g = open_guest (\@ARGV, address => $uri);
+} else {
+    $g = open_guest (\@ARGV);
+}
+
+$g->launch ();
+
+# List of partitions and sizes.
+my @partitions;
+my @devices = $g->list_devices ();
+foreach my $dev (@devices) {
+    my @p = $g->part_list ($dev);
+    foreach (@p) {
+        $_->{name} = canonicalize ("$dev" . $_->{part_num});
+        push @partitions, $_;
+    }
+}
+
+# Print them.
+foreach my $part (@partitions) {
+    print $part->{name};
+
+    if ($long) {
+        my $type;
+        eval {
+            $type = $g->vfs_type ($part->{name});
+        };
+        $type ||= "unknown";
+        $type = "pv" if $type eq "LVM2_member";
+        print " $type ";
+        if ($human) {
+            print (human_size($part->{part_size}));
+        } else {
+            print $part->{part_size};
+        }
+    }
+    print "\n";
+}
+
+# The reverse of device name translation, see
+# BLOCK DEVICE NAMING in guestfs(3).
+sub canonicalize
+{
+    local $_ = shift;
+
+    if (m{^/dev/[hv]d([a-z]\d)$}) {
+        return "/dev/sd$1";
+    }
+    $_;
+}
+
+# Convert a number of bytes to a human-readable number.
+sub human_size
+{
+    local $_ = shift;
+
+    $_ /= 1024;                 # blocks
+
+    if ($_ < 1024) {
+        sprintf "%dK", $_;
+    } elsif ($_ < 1024 * 1024) {
+        sprintf "%.1fM", ($_ / 1024);
+    } else {
+        sprintf "%.1fG", ($_ / 1024 / 1024);
+    }
+}
+
+=head1 SEE ALSO
+
+L<guestfs(3)>,
+L<guestfish(1)>,
+L<virt-list-filesystems(1)>,
+L<virt-resize(1)>,
+L<Sys::Guestfs(3)>,
+L<Sys::Guestfs::Lib(3)>,
+L<Sys::Virt(3)>,
+L<http://libguestfs.org/>.
+
+=head1 AUTHOR
+
+Richard W.M. Jones L<http://et.redhat.com/~rjones/>
+
+=head1 COPYRIGHT
+
+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.
diff --git a/tools/virt-resize b/tools/virt-resize
new file mode 100644 (file)
index 0000000..1c4006a
--- /dev/null
@@ -0,0 +1,983 @@
+#!/usr/bin/perl -w
+# virt-resize
+# 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.
+
+use warnings;
+use strict;
+
+use Sys::Guestfs;
+use Fcntl qw(S_ISREG SEEK_SET);
+use POSIX qw(floor);
+use Pod::Usage;
+use Getopt::Long;
+use Data::Dumper;
+use Locale::TextDomain 'libguestfs';
+
+$Data::Dumper::Sortkeys = 1;
+
+die __"virt-resize: sorry this program does not work on a 32 bit host\n"
+    if ~1 == 4294967294;
+
+=encoding utf8
+
+=head1 NAME
+
+virt-resize - Resize a virtual machine disk
+
+=head1 SYNOPSIS
+
+ virt-resize [--resize /dev/sdaN=[+/-]<size>[%]] [--expand /dev/sdaN]
+   [--shrink /dev/sdaN] [--ignore /dev/sdaN] [--delete /dev/sdaN] [...]
+   indisk outdisk
+
+=head1 DESCRIPTION
+
+Virt-resize is a tool which can resize a virtual machine disk, making
+it larger or smaller overall, and resizing or deleting any partitions
+contained within.
+
+Virt-resize B<cannot> resize disk images in-place.  Virt-resize
+B<should not> be used on live virtual machines - for consistent
+results, shut the virtual machine down before resizing it.
+
+If you are not familiar with the associated tools:
+L<virt-list-partitions(1)>,
+L<virt-list-filesystems(1)> and
+L<virt-df(1)>,
+we recommend you go and read those manual pages first.
+
+=head2 BASIC USAGE
+
+This describes the common case where you want to expand an image to
+give your guest more space.  Shrinking images is considerably more
+complicated (unfortunately).
+
+=over 4
+
+=item 1. Locate disk image
+
+Locate the disk image that you want to resize.  It could be in a local
+file or device.  If the guest is managed by libvirt, you can use
+C<virsh dumpxml> like this to find the disk image name:
+
+ # virsh dumpxml guestname | xpath /domain/devices/disk/source
+ Found 1 nodes:
+ -- NODE --
+ <source dev="/dev/vg/lv_guest" />
+
+=item 2. Look at current sizing
+
+Use L<virt-list-partitions(1)> to display the current partitions and
+sizes:
+
+ # virt-list-partitions -lh /dev/vg/lv_guest
+ /dev/sda1 ext3 101.9M
+ /dev/sda2 pv 7.9G
+
+(This example is a virtual machine with an 8 GB disk which we would
+like to expand up to 10 GB).
+
+=item 3. Create destination disk
+
+Virt-resize cannot do in-place disk modifications.  You have to have
+space to store the resized destination disk.
+
+To store the resized disk image in a file, create a file of a suitable
+size:
+
+ # rm -f outdisk
+ # truncate -s 10G outdisk
+
+Use L<lvcreate(1)> to create a logical volume:
+
+ # lvcreate -L 10G -n lv_name vg_name
+
+Or use L<virsh(1)> vol-create-as to create a libvirt storage volume:
+
+ # virsh pool-list
+ # virsh vol-create-as poolname newvol 10G
+
+=item 4. Resize
+
+ virt-resize indisk outdisk
+
+This command just copies disk image C<indisk> to disk image C<outdisk>
+I<without> resizing or changing any existing partitions.  If
+C<outdisk> is larger, then an extra, empty partition is created at the
+end of the disk covering the extra space.  If C<outdisk> is smaller,
+then it will give an error.
+
+To resize, you need to pass extra options (for the full list see the
+L</OPTIONS> section below).
+
+L</--expand> is the most useful option.  It expands the named
+partition within the disk to fill any extra space:
+
+ virt-resize --expand /dev/sda2 indisk outdisk
+
+(In this case, an extra partition is I<not> created at the end of the
+disk, because there will be no unused space).
+
+If /dev/sda2 in the image contains a filesystem or LVM PV, then
+this content is B<not> automatically resized.  You can resize it
+afterwards either using L<guestfish(1)> (offline) or using commands
+inside the guest (online resizing).
+
+L</--resize> is the other commonly used option.  The following would
+increase the size of /dev/sda1 by 200M, and expand /dev/sda2
+to fill the rest of the available space:
+
+ virt-resize --resize /dev/sda1=+200M --expand /dev/sda2 \
+   indisk outdisk
+
+Other options are covered below.
+
+=item 5. Test
+
+Thoroughly test the new disk image I<before> discarding the old one.
+
+If you are using libvirt, edit the XML to point at the new disk:
+
+ # virsh edit guestname
+
+Change E<lt>source ...E<gt>, see
+L<http://libvirt.org/formatdomain.html#elementsDisks>
+
+Then start up the domain with the new, resized disk:
+
+ # virsh start guestname
+
+and check that it still works.
+
+Note that to see the extra space in the guest, you may need to use
+guest commands to resize PVs, LVs and/or filesystems to fit the extra
+space available.  Three common guest commands for doing this for Linux
+guests are L<pvresize(8)>, L<lvresize(8)> and L<resize2fs(8)>.  It is
+also possible to do this offline (eg. for scripting changes) using
+L<guestfish(1)>.
+
+=back
+
+=head1 OPTIONS
+
+=over 4
+
+=cut
+
+my $help;
+
+=item B<--help>
+
+Display help.
+
+=cut
+
+my $version;
+
+=item B<--version>
+
+Display version number and exit.
+
+=cut
+
+my @resize;
+
+=item B<--resize part=size>
+
+Resize the named partition (expanding or shrinking it) so that it has
+the given size.
+
+C<size> can be expressed as an absolute number followed by
+b/K/M/G/T/P/E to mean bytes, Kilobytes, Megabytes, Gigabytes,
+Terabytes, Petabytes or Exabytes; or as a percentage of the current
+size; or as a relative number or percentage.  For example:
+
+ --resize /dev/sda2=10G
+
+ --resize /dev/sda4=90%
+
+ --resize /dev/sda2=+1G
+
+ --resize /dev/sda2=-200M
+
+ --resize /dev/sda1=+128K
+
+ --resize /dev/sda1=+10%
+
+ --resize /dev/sda1=-10%
+
+You can increase the size of any partition.
+
+You can I<only> B<decrease> the size of partitions that contain
+filesystems or PVs which have already been shrunk.  Virt-resize will
+check this has been done before proceeding, or else will print an
+error (see also C<--resize-force>).
+
+You can give this option multiple times.
+
+=cut
+
+my @resize_force;
+
+=item B<--resize-force part=size>
+
+This is the same as C<--resize> except that it will let you decrease
+the size of any partition.  Generally this means you will lose any
+data which was at the end of the partition you shrink, but you may not
+care about that (eg. if shrinking an unused partition, or if you can
+easily recreate it such as a swap partition).
+
+See also the C<--ignore> option.
+
+=cut
+
+my $expand;
+
+=item B<--expand part>
+
+Expand the named partition so it uses up all extra space (space left
+over after any other resize changes that you request have been done).
+
+Any filesystem inside the partition is I<not> expanded.  You will need
+to expand the filesystem (or PV) to fit the extra space either using
+L<guestfish(1)> (offline) or online guest tools.
+
+Note that you cannot use C<--expand> and C<--shrink> together.
+
+=cut
+
+my $shrink;
+
+=item B<--shrink part>
+
+Shrink the named partition until the overall disk image fits in the
+destination.  The named partition B<must> contain a filesystem or PV
+which has already been shrunk using another tool (eg. L<guestfish(1)>
+or other online tools).  Virt-resize will check this and give an error
+if it has not been done.
+
+The amount by which the overall disk must be shrunk (after carrying
+out all other operations requested by the user) is called the
+"deficit".  For example, a straight copy (assume no other operations)
+from a 5GB disk image to a 4GB disk image results in a 1GB deficit.
+In this case, virt-resize would give an error unless the user
+specified a partition to shrink and that partition had more than a
+gigabyte of free space.
+
+Note that you cannot use C<--expand> and C<--shrink> together.
+
+=cut
+
+my @ignore;
+
+=item B<--ignore part>
+
+Ignore the named partition.  Effectively this means the partition is
+allocated on the destination disk, but the content is not copied
+across from the source disk.  The content of the partition will be
+blank (all zero bytes).
+
+You can give this option multiple times.
+
+=cut
+
+my @delete;
+
+=item B<--delete part>
+
+Delete the named partition.  It would be more accurate to describe
+this as "don't copy it over", since virt-resize doesn't do in-place
+changes and the original disk image is left intact.
+
+Note that when you delete a partition, then anything contained in the
+partition is also deleted.  Furthermore, this causes any partitions
+that come after to be I<renumbered>, which can easily make your guest
+unbootable.
+
+You can give this option multiple times.
+
+=cut
+
+my $copy_boot_loader = 1;
+
+=item B<--no-copy-boot-loader>
+
+By default, virt-resize copies over some sectors at the start of the
+disk (up to the beginning of the first partition).  Commonly these
+sectors contain the Master Boot Record (MBR) and the boot loader, and
+are required in order for the guest to boot correctly.
+
+If you specify this flag, then this initial copy is not done.  You may
+need to reinstall the boot loader in this case.
+
+=cut
+
+my $extra_partition = 1;
+my $min_extra_partition = 10 * 1024 * 1024; # see below
+
+=item B<--no-extra-partition>
+
+By default, virt-resize creates an extra partition if there is any
+extra, unused space after all resizing has happened.  Use this option
+to prevent the extra partition from being created.  If you do this
+then the extra space will be inaccessible until you run fdisk, parted,
+or some other partitioning tool in the guest.
+
+Note that if the surplus space is smaller than 10 MB, no extra
+partition will be created.
+
+=cut
+
+my $debug;
+
+=item B<-d> | B<--debug>
+
+Enable debugging messages.
+
+=cut
+
+my $dryrun;
+
+=item B<-n> | B<--dryrun>
+
+Print a summary of what would be done, but don't do anything.
+
+=cut
+
+my $quiet;
+
+=item B<-q> | B<--quiet>
+
+Don't print the summary.
+
+=back
+
+=cut
+
+GetOptions ("help|?" => \$help,
+            "version" => \$version,
+            "resize=s" => \@resize,
+            "resize-force=s" => \@resize_force,
+            "expand=s" => \$expand,
+            "shrink=s" => \$shrink,
+            "ignore=s" => \@ignore,
+            "delete=s" => \@delete,
+            "copy-boot-loader!" => \$copy_boot_loader,
+            "extra-partition!" => \$extra_partition,
+            "d|debug" => \$debug,
+            "n|dryrun" => \$dryrun,
+            "q|quiet" => \$quiet,
+    ) 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
+}
+
+die "virt-resize [--options] indisk outdisk\n" unless @ARGV == 2;
+
+# Check in and out images exist.
+my $infile = $ARGV[0];
+my $outfile = $ARGV[1];
+die __x("virt-resize: {file}: does not exist or is not readable\n", file => $infile)
+    unless -r $infile;
+die __x("virt-resize: {file}: does not exist or is not writable\nYou have to create the destination disk before running this program.\nPlease read the virt-resize(1) manpage for more information.\n", file => $outfile)
+    unless -w $outfile;
+
+my @s;
+@s = stat $infile;
+my $insize = S_ISREG ($s[2]) ? $s[7] : host_blockdevsize ($infile);
+@s = stat $outfile;
+my $outsize = S_ISREG ($s[2]) ? $s[7] : host_blockdevsize ($outfile);
+
+if ($debug) {
+    print "$infile size $insize bytes\n";
+    print "$outfile size $outsize bytes\n";
+}
+
+die __x("virt-resize: {file}: file is too small to be a disk image ({sz} bytes)\n",
+        file => $infile, sz => $insize)
+    if $insize < 64 * 512;
+die __x("virt-resize: {file}: file is too small to be a disk image ({sz} bytes)\n",
+        file => $outfile, sz => $outsize)
+    if $outsize < 64 * 512;
+
+# Copy the boot loader across.
+do_copy_boot_loader () if $copy_boot_loader;
+
+sub do_copy_boot_loader
+{
+    print "copying boot loader ...\n" if $debug;
+    open IFILE, $infile or die "$infile: $!";
+    my $s;
+    my $r = sysread (IFILE, $s, 64 * 512) or die "$infile: $!";
+    die "$infile: short read" if $r < 64 * 512;
+    open OFILE, "+<$outfile" or die "$outfile: $!";
+    sysseek OFILE, 0, SEEK_SET or die "$outfile: seek: $!";
+    $r = syswrite (OFILE, $s, 64 * 512) or die "$outfile: $!";
+    die "$outfile: short write" if $r < 64 * 512;
+}
+
+# Add them to the handle and launch the appliance.
+my $g;
+launch_guestfs ();
+
+sub launch_guestfs
+{
+    $g = Sys::Guestfs->new ();
+    $g->set_trace (1) if $debug;
+    $g->add_drive_ro ($infile);
+    $g->add_drive ($outfile);
+    $g->launch ();
+}
+
+my $sectsize = $g->blockdev_getss ("/dev/sdb");
+
+# Get the partitions on the source disk.
+my @partitions;
+my %partitions;
+check_source_disk ();
+
+sub check_source_disk
+{
+    local $_;
+
+    # Partitions and PVs.
+    my @p = $g->part_list ("/dev/sda");
+    foreach (@p) {
+        my $name = "/dev/sda" . $_->{part_num};
+        push @partitions, $name;
+
+        my %h = %$_;
+        $h{name} = $name;
+        $partitions{$name} = \%h;
+    }
+}
+
+# Examine each partition.
+my @pvs_full = $g->pvs_full ();
+examine_partition ($_) foreach @partitions;
+
+sub examine_partition
+{
+    local $_;
+    my $part = shift;
+
+    # What is it?
+    my $type = "unknown";
+    eval {
+        $type = $g->vfs_type ($part);
+    };
+    $partitions{$part}->{type} = $type;
+
+    # Can we get the actual size of this object (ie. to find out if it
+    # is smaller than the container for shrinking)?
+    my $fssize;
+    if ($type eq "LVM2_member") { # LVM PV
+        foreach (@pvs_full) {
+            $fssize = $_->{pv_size}
+              if canonicalize ($_->{pv_name}) eq $part;
+        }
+    } else {                    # Something mountable?
+        eval {
+            $g->mount_ro ($part, "/");
+
+            my %stat = $g->statvfs ("/");
+            $fssize = $stat{bsize} * $stat{blocks};
+        };
+
+        eval {
+            $g->umount_all ();
+        };
+    }
+
+    # This might be undef if we didn't successfully find the size.  In
+    # that case user won't be allowed to shrink this partition except
+    # by forcing it.
+    $partitions{$part}->{fssize} = $fssize;
+}
+
+if ($debug) {
+    print "partitions found: ", join (", ", @partitions), "\n";
+    foreach my $part (@partitions) {
+        print "$part:\n";
+        foreach (sort keys %{$partitions{$part}}) {
+            print("\t", $_, " = ",
+                  defined ($partitions{$part}->{$_})
+                  ? $partitions{$part}->{$_} : "undef",
+                  "\n");
+        }
+    }
+}
+
+sub find_partition
+{
+    local $_ = shift;
+    my $option = shift;
+
+    $_ = "/dev/$_" unless $_ =~ m{^/dev};
+    $_ = canonicalize ($_);
+
+    unless (exists $partitions{$_}) {
+        die __x("{p}: partition not found in the source disk image, when using the '{opt}' command line option\n",
+                p => $_,
+                opt => $option)
+    }
+
+    if ($partitions{$_}->{ignore}) {
+        die __x("{p}: partition ignored, you cannot use it in another command line argument\n",
+                p => $_)
+    }
+    if ($partitions{$_}->{delete}) {
+        die __x("{p}: partition deleted, you cannot use it in another command line argument\n",
+                p => $_)
+    }
+
+    return $_;
+}
+
+# Handle --ignore.
+do_ignore ($_) foreach @ignore;
+
+sub do_ignore
+{
+    local $_ = shift;
+    $_ = find_partition ($_, "--ignore");
+    $partitions{$_}->{ignore} = 1;
+}
+
+# Handle --delete.
+do_delete ($_) foreach @delete;
+
+sub do_delete
+{
+    local $_ = shift;
+    $_ = find_partition ($_, "--delete");
+    $partitions{$_}->{delete} = 1;
+}
+
+# Handle --resize and --resize-force.
+do_resize ($_, 0, "--resize") foreach @resize;
+do_resize ($_, 1, "--resize-force") foreach @resize_force;
+
+sub do_resize
+{
+    local $_ = shift;
+    my $force = shift;
+    my $option = shift;
+
+    # Argument is "part=size" ...
+    my ($part, $sizefield) = split /=/, $_, 2;
+    $part = find_partition ($part, $option);
+
+    if (exists $partitions{$part}->{newsize}) {
+        die __x("{p}: this partition has already been marked for resizing\n",
+                p => $part);
+    }
+
+    # Parse the size field.
+    my $oldsize = $partitions{$part}->{part_size};
+    my $newsize;
+    if (!defined ($sizefield) || $sizefield eq "") {
+        die __x("{p}: missing size field in {o} option\n",
+                p => $part, o => $option);
+    } elsif ($sizefield =~ /^([.\d]+)([bKMGTPE])$/) {
+        $newsize = sizebytes ($1, $2);
+    } elsif ($sizefield =~ /^\+([.\d]+)([bKMGTPE])$/) {
+        my $incr = sizebytes ($1, $2);
+        $newsize = $oldsize + $incr;
+    } elsif ($sizefield =~ /^-([.\d]+)([bKMGTPE])$/) {
+        my $decr = sizebytes ($1, $2);
+        $newsize = $oldsize - $decr;
+    } elsif ($sizefield =~ /^([.\d]+)%$/) {
+        $newsize = $oldsize * $1 / 100;
+    } elsif ($sizefield =~ /^\+([.\d]+)%$/) {
+        $newsize = $oldsize + $oldsize * $1 / 100;
+    } elsif ($sizefield =~ /^-([.\d]+)%$/) {
+        $newsize = $oldsize - $oldsize * $1 / 100;
+    } else {
+        die __x("{p}: {f}: cannot parse size field\n",
+                p => $part, f => $sizefield)
+    }
+
+    $newsize > 0 or
+        die __x("{p}: new size is zero or negative\n", p => $part);
+
+    mark_partition_for_resize ($part, $oldsize, $newsize, $force, $option);
+}
+
+sub mark_partition_for_resize
+{
+    local $_;
+    my $part = shift;
+    my $oldsize = shift;
+    my $newsize = shift;
+    my $force = shift;
+    my $option = shift;
+
+    # Do nothing if the size is the same.
+    return if $oldsize == $newsize;
+
+    my $bigger = $newsize > $oldsize;
+
+    # Check there is space to shrink this.
+    unless ($bigger || $force) {
+        if (! $partitions{$part}->{fssize} ||
+            $partitions{$part}->{fssize} > $newsize) {
+            die __x("{p}: cannot make this partition smaller because it contains a\nfilesystem, physical volume or other content that is larger than the new size.\nYou have to resize the content first, see virt-resize(1).\n",
+                    p => $part);
+        }
+    }
+
+    $partitions{$part}->{newsize} = $newsize;
+}
+
+# Handle --expand and --shrink.
+my $surplus;
+if (defined $expand && defined $shrink) {
+    die __"virt-resize: you cannot use options --expand and --shrink together\n"
+}
+if (defined $expand || defined $shrink) {
+    calculate_surplus ();
+
+    if ($debug) {
+        print "surplus before --expand or --shrink: $surplus (",
+          human_size ($surplus), ")\n";
+    }
+
+    do_expand () if $expand;
+    do_shrink () if $shrink;
+}
+
+# (Re-)calculate surplus after doing expand or shrink.
+calculate_surplus ();
+
+# Add up the total space required on the target so far, compared
+# to the size of the target.  We end up with a surplus or deficit.
+sub calculate_surplus
+{
+    local $_;
+
+    # We need some overhead for partitioning.  Worst case would be for
+    # EFI partitioning + massive per-partition alignment.
+    my $overhead = $sectsize * (2 * 64 + (64 * (@partitions + 1)) + 128);
+
+    my $required = 0;
+    foreach (@partitions) {
+        if ($partitions{$_}->{newsize}) {
+            $required += $partitions{$_}->{newsize}
+        } else {
+            $required += $partitions{$_}->{part_size}
+        }
+    }
+
+    # Compare that to the actual target disk.
+    $surplus = $outsize - ($required + $overhead);
+}
+
+sub do_expand
+{
+    local $_;
+
+    unless ($surplus > 0) {
+        die __x("virt-resize: error: cannot use --expand when there is no surplus space to\nexpand into.  You need to make the target disk larger by at least {h}.\n",
+                h => human_size (-$surplus));
+    }
+
+    my $part = find_partition ($expand, "--expand");
+    my $oldsize = $partitions{$part}->{part_size};
+    mark_partition_for_resize ($part, $oldsize, $oldsize + $surplus,
+                               0, "--expand");
+}
+
+sub do_shrink
+{
+    local $_;
+
+    unless ($surplus < 0) {
+        die __"virt-resize: error: cannot use --shrink because there is no deficit\n(see 'deficit' in the virt-resize(1) man page)\n"
+    }
+
+    my $part = find_partition ($shrink, "--shrink");
+    my $oldsize = $partitions{$part}->{part_size};
+    mark_partition_for_resize ($part, $oldsize, $oldsize + $surplus,
+                               0, "--shrink");
+}
+
+# Print summary.
+print_summary () unless $quiet;
+
+sub print_summary
+{
+    local $_;
+    print __"Summary of changes:\n";
+
+    foreach my $part (@partitions) {
+        if ($partitions{$part}->{ignore}) {
+            print __x("{p}: partition will be ignored", p => $part);
+        } elsif ($partitions{$part}->{delete}) {
+            print __x("{p}: partition will be deleted", p => $part);
+        } elsif ($partitions{$part}->{newsize}) {
+            print __x("{p}: partition will be resized from {oldsize} to {newsize}",
+                      p => $part,
+                      oldsize => human_size ($partitions{$part}->{part_size}),
+                      newsize => human_size ($partitions{$part}->{newsize}));
+        } else {
+            print __x("{p}: partition will be left alone", p => $part);
+        }
+        print "\n"
+    }
+
+    if ($surplus > 0) {
+        print __x("There is a surplus of {spl} bytes ({h}).\n",
+                  spl => $surplus,
+                  h => human_size ($surplus));
+        if ($extra_partition) {
+            if ($surplus >= $min_extra_partition) {
+                print __"An extra partition will be created for the surplus.\n";
+            } else {
+                print __"The surplus space is not large enough for an extra partition to be created\nand so it will just be ignored.\n";
+            }
+        } else {
+            print __"The surplus space will be ignored.  Run a partitioning program in the guest\nto partition this extra space if you want.\n";
+        }
+    } elsif ($surplus < 0) {
+        die __x("virt-resize: error: there is a deficit of {def} bytes ({h}).\nYou need to make the target disk larger by at least this amount,\nor adjust your resizing requests.\n",
+                def => -$surplus,
+                h => human_size (-$surplus));
+    }
+}
+
+exit 0 if $dryrun;
+
+# Repartition the target disk.
+my $nextpart = 1;
+my $parttype;
+repartition ();
+
+sub repartition
+{
+    local $_;
+
+    if ($copy_boot_loader) {
+        $parttype = $g->part_get_parttype ("/dev/sdb");
+        print "partition table type: $parttype\n" if $debug;
+    } else {
+        # Didn't copy over the initial boot loader, so we need
+        # to make a new partition type here.
+        $parttype = "efi";
+    }
+
+    # Delete any existing partitions on the destination disk.
+    $g->part_init ("/dev/sdb", $parttype);
+
+    my $start = 64;
+
+    # Create the new partitions.
+    foreach my $part (@partitions) {
+        unless ($partitions{$part}->{delete}) {
+            # Size in sectors.
+            my $size;
+            if ($partitions{$part}->{newsize}) {
+                $size = ($partitions{$part}->{newsize} + $sectsize - 1)
+                    / $sectsize;
+            } else {
+                $size = ($partitions{$part}->{part_size} + $sectsize - 1)
+                    / $sectsize;
+            }
+
+            # Create it.
+            my ($target, $end) = add_partition ($start, $size);
+            $partitions{$part}->{target} = $target;
+
+            # Start of next partition + alignment.
+            $start = $end + 1;
+            $start = ($start + 63) & ~63;
+        }
+    }
+
+    # Create surplus partition.
+    if ($extra_partition && $surplus >= $min_extra_partition) {
+        add_partition ($start, $outsize / $sectsize - 64 - $start);
+    }
+}
+
+# Add a partition.
+sub add_partition
+{
+    local $_;
+    my $start = shift;
+    my $size = shift;
+
+    my ($target, $end);
+
+    if ($nextpart <= 3 || $parttype ne "msdos") {
+        $target = "/dev/sdb$nextpart";
+        $end = $start + $size - 1;
+        $g->part_add ("/dev/sdb", "primary", $start, $end);
+        $nextpart++;
+    } else {
+        if ($nextpart == 4) {
+            $g->part_add ("/dev/sdb", "extended", $start, -1);
+            $nextpart++;
+            $start += 64;
+        }
+        $target = "/dev/sdb$nextpart";
+        $end = $start + $size - 1;
+        $g->part_add ("/dev/sdb", "logical", $start, $end);
+        $nextpart++;
+    }
+
+    return ($target, $end);
+}
+
+# Copy over the data.
+copy_data ();
+
+sub copy_data
+{
+    foreach my $part (@partitions)
+    {
+        unless ($partitions{$part}->{ignore}) {
+            my $target = $partitions{$part}->{target};
+            if ($target) {
+                my $oldsize = $partitions{$part}->{part_size};
+                my $newsize;
+                if ($partitions{$part}->{newsize}) {
+                    $newsize = $partitions{$part}->{newsize};
+                } else {
+                    $newsize = $partitions{$part}->{part_size};
+                }
+
+                if (!$quiet && !$debug) {
+                    local $| = 1;
+                    print "Copying $part ...";
+                }
+
+                $g->copy_size ($part, $target,
+                               $newsize < $oldsize ? $newsize : $oldsize);
+
+                if (!$quiet && !$debug) {
+                    print " done\n"
+                }
+            }
+        }
+    }
+}
+
+exit 0;
+
+sub sizebytes
+{
+    local $_ = shift;
+    my $unit = shift;
+
+    $_ *= 1024 if $unit =~ /[KMGTPE]/;
+    $_ *= 1024 if $unit =~ /[MGTPE]/;
+    $_ *= 1024 if $unit =~ /[GTPE]/;
+    $_ *= 1024 if $unit =~ /[TPE]/;
+    $_ *= 1024 if $unit =~ /[PE]/;
+    $_ *= 1024 if $unit =~ /[E]/;
+
+    return floor($_);
+}
+
+# Convert a number of bytes to a human-readable number.
+sub human_size
+{
+    local $_ = shift;
+
+    my $sgn = "";
+    if ($_ < 0) {
+        $sgn = "-";
+        $_ = -$_;
+    }
+
+    $_ /= 1024;
+
+    if ($_ < 1024) {
+        sprintf "%s%dK", $sgn, $_;
+    } elsif ($_ < 1024 * 1024) {
+        sprintf "%s%.1fM", $sgn, ($_ / 1024);
+    } else {
+        sprintf "%s%.1fG", $sgn, ($_ / 1024 / 1024);
+    }
+}
+
+# Return the size in bytes of a HOST block device.
+sub host_blockdevsize
+{
+    local $_;
+    my $dev = shift;
+
+    open BD, "PATH=/usr/sbin:/sbin:\$PATH blockdev --getsize64 $dev |"
+        or die "blockdev: $!";
+    $_ = <BD>;
+    chomp $_;
+    $_;
+}
+
+# The reverse of device name translation, see
+# BLOCK DEVICE NAMING in guestfs(3).
+sub canonicalize
+{
+    local $_ = shift;
+
+    if (m{^/dev/[hv]d([a-z]\d)$}) {
+        return "/dev/sd$1";
+    }
+    $_;
+}
+
+=head1 SEE ALSO
+
+L<virt-list-partitions(1)>,
+L<virt-list-filesystems(1)>,
+L<virt-df(1)>,
+L<guestfs(3)>,
+L<guestfish(1)>,
+L<lvm(8)>,
+L<pvresize(8)>,
+L<lvresize(8)>,
+L<resize2fs(8)>,
+L<virsh(1)>,
+L<Sys::Guestfs(3)>,
+L<http://libguestfs.org/>.
+
+=head1 AUTHOR
+
+Richard W.M. Jones L<http://et.redhat.com/~rjones/>
+
+=head1 COPYRIGHT
+
+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.