die __"virt-resize: sorry this program does not work on a 32 bit host\n"
if ~1 == 4294967294;
+$| = 1;
+
=encoding utf8
=head1 NAME
L<virt-df(1)>,
we recommend you go and read those manual pages first.
-=head1 BASIC USAGE
+=head1 EXAMPLES
+
+Copy C<olddisk> to C<newdisk>, extending one of the guest's partitions
+to fill the extra 5GB of space.
+
+ truncate -r olddisk newdisk; truncate -s +5G newdisk
+ virt-list-partitions -lht olddisk
+ # Note "/dev/sda2" is a partition inside the "olddisk" file.
+ virt-resize --expand /dev/sda2 olddisk newdisk
+
+As above, but make the /boot partition 200MB bigger, while giving the
+remaining space to /dev/sda2:
+
+ virt-resize --resize /dev/sda1=+200M --expand /dev/sda2 olddisk newdisk
+
+As above, but the output format will be uncompressed qcow2:
+
+ qemu-img create -f qcow2 newdisk.qcow2 15G
+ virt-resize --expand /dev/sda2 olddisk newdisk.qcow2
+
+=head1 DETAILED USAGE
=head2 EXPANDING A VIRTUAL MACHINE DISK
but note that it also renumbers any partitions after the one which is
deleted, which can leave some guests unbootable.
+=head2 QCOW2 AND NON-SPARSE RAW FORMATS
+
+If the input disk is in qcow2 format, then you may prefer that the
+output is in qcow2 format as well. Alternately, virt-resize can
+convert the format on the fly. The output format is simply determined
+by the format of the empty output container that you provide. Thus to
+create qcow2 output, use:
+
+ qemu-img create [-c] -f qcow2 outdisk [size]
+
+instead of the truncate command (use C<-c> for a compressed disk).
+
+Similarly, to get non-sparse raw output use:
+
+ fallocate -l size outdisk
+
+(on older systems that don't have the L<fallocate(1)> command use
+C<dd if=/dev/zero of=outdisk bs=1M count=..>)
+
=head1 OPTIONS
=over 4
=cut
+my @lv_expand;
+
+=item B<--LV-expand logvol>
+
+This takes the logical volume and, as a final step, expands it to fill
+all the space available in its volume group. A typical usage,
+assuming a Linux guest with a single PV C</dev/sda2> and a root device
+called C</dev/vg_guest/lv_root> would be:
+
+ virt-resize indisk outdisk \
+ --expand /dev/sda2 --LV-expand /dev/vg_guest/lv_root
+
+This would first expand the partition (and PV), and then expand the
+root device to fill the extra space in the PV.
+
+The contents of the LV are also resized if virt-resize knows how to do
+that. You can stop virt-resize from trying to expand the content by
+using the option C<--no-expand-content>.
+
+Use L<virt-list-filesystems(1)> to list the filesystems in
+the guest.
+
+You can give this option multiple times, I<but> it doesn't
+make sense to do this unless the logical volumes you specify
+are all in different volume groups.
+
+=cut
+
my $copy_boot_loader = 1;
=item B<--no-copy-boot-loader>
Don't print the summary.
+=cut
+
+my $format;
+
+=item B<--format> raw
+
+Specify the format of the input disk image. If this flag is not
+given then it is auto-detected from the image itself.
+
+If working with untrusted raw-format guest disk images, you should
+ensure the format is always specified.
+
+Note that this option I<does not> affect the output format.
+See L</QCOW2 AND NON-SPARSE RAW FORMATS>.
+
+=cut
+
+my $output_format;
+
+=item B<--output-format> raw
+
+Specify the format of the output disk image. If this flag is not
+given then it is auto-detected from the image itself.
+
+If working with untrusted raw-format guest disk images, you should
+ensure the format is always specified.
+
+Note that you still need to create the output disk with the right
+format. See L</QCOW2 AND NON-SPARSE RAW FORMATS>.
+
=back
=cut
"shrink=s" => \$shrink,
"ignore=s" => \@ignore,
"delete=s" => \@delete,
+ "lv-expand=s" => \@lv_expand,
"copy-boot-loader!" => \$copy_boot_loader,
"extra-partition!" => \$extra_partition,
"expand-content!" => \$expand_content,
"d|debug" => \$debug,
"n|dryrun|dry-run" => \$dryrun,
"q|quiet" => \$quiet,
+ "format=s" => \$format,
+ "output-format=s" => \$output_format,
) or pod2usage (2);
pod2usage (1) if $help;
if ($version) {
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);
+# 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;
+ my @args = ($infile);
+ push @args, readonly => 1;
+ push @args, format => $format if defined $format;
+ $g->add_drive_opts (@args);
+ @args = ($outfile);
+ push @args, format => $output_format if defined $output_format;
+ $g->add_drive_opts (@args);
+ $g->set_progress_callback (\&progress_callback) unless $quiet;
+ $g->launch ();
+}
+
+my $sectsize = $g->blockdev_getss ("/dev/sdb");
+
+# Get the size in bytes of each disk.
+#
+# Originally we computed this by looking at the same of the host file,
+# but of course this failed for qcow2 images (RHBZ#633096). The right
+# way to do it is with $g->blockdev_getsize64.
+my $insize = $g->blockdev_getsize64 ("/dev/sda");
+my $outsize = $g->blockdev_getsize64 ("/dev/sdb");
if ($debug) {
print "$infile size $insize bytes\n";
print "$outfile size $outsize bytes\n";
}
+# Create a partition table.
+#
+# We *must* do this before copying the bootloader across, and copying
+# the bootloader must be careful not to disturb this partition table
+# (RHBZ#633766). There are two reasons for this:
+#
+# (1) The 'parted' library is stupid and broken. In many ways. In
+# this particular instance the stupid and broken bit is that it
+# overwrites the whole boot sector when initializating a partition
+# table. (Upstream don't consider this obvious problem to be a bug).
+#
+# (2) GPT has a backup partition table located at the end of the disk.
+# It's non-movable, because the primary GPT contains fixed references
+# to both the size of the disk and the backup partition table at the
+# end. This would be a problem for any resize that didn't either
+# carefully move the backup GPT (and rewrite those references) or
+# recreate the whole partition table from scratch.
+
+my $parttype;
+create_partition_table ();
+
+sub create_partition_table
+{
+ local $_;
+
+ $parttype = $g->part_get_parttype ("/dev/sda");
+ print "partition table type: $parttype\n" if $debug;
+
+ $g->part_init ("/dev/sdb", $parttype);
+}
+
# In reality the number of sectors containing boot loader data will be
# less than this (although Windows 7 defaults to putting the first
# partition on sector 2048, and has quite a large boot loader).
# offset of the first partition.
#
# It doesn't matter if we copy too much.
-my $boot_sectors = 4096;
+my $max_bootloader = 4096 * 512;
die __x("virt-resize: {file}: file is too small to be a disk image ({sz} bytes)\n",
file => $infile, sz => $insize)
- if $insize < $boot_sectors * 512;
+ if $insize < $max_bootloader;
die __x("virt-resize: {file}: file is too small to be a disk image ({sz} bytes)\n",
file => $outfile, sz => $outsize)
- if $outsize < $boot_sectors * 512;
+ if $outsize < $max_bootloader;
# 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, $boot_sectors * 512) or die "$infile: $!";
- die "$infile: short read" if $r < $boot_sectors * 512;
- open OFILE, "+<$outfile" or die "$outfile: $!";
- sysseek OFILE, 0, SEEK_SET or die "$outfile: seek: $!";
- $r = syswrite (OFILE, $s, $boot_sectors * 512) or die "$outfile: $!";
- die "$outfile: short write" if $r < $boot_sectors * 512;
-}
-# Add them to the handle and launch the appliance.
-my $g;
-launch_guestfs ();
+ # Don't disturb the partition table that we just wrote.
+ # https://secure.wikimedia.org/wikipedia/en/wiki/Master_Boot_Record
+ # https://secure.wikimedia.org/wikipedia/en/wiki/GUID_Partition_Table
-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 $bootsect = $g->pread_device ("/dev/sda", 446, 0);
+ die __"virt-resize: short read" if length ($bootsect) < 446;
+
+ $g->pwrite_device ("/dev/sdb", $bootsect, 0);
+
+ my $start = 512;
+ if ($parttype eq "gpt") {
+ # XXX With 4K sectors does GPT just fit more entries in a
+ # sector, or does it always use 34 sectors?
+ $start = 17408;
+ }
+
+ my $loader = $g->pread_device ("/dev/sda", $max_bootloader, $start);
+ die __"virt-resize: short read" if length ($loader) < $max_bootloader;
+
+ $g->pwrite_device ("/dev/sdb", $loader, $start);
}
-my $sectsize = $g->blockdev_getss ("/dev/sdb");
+my $to_be_expanded = 0;
# Get the partitions on the source disk.
my @partitions;
}
}
+# Examine the LVs (for --lv-expand option).
+my @lvs = $g->lvs ();
+my %lvs;
+examine_lv ($_) foreach @lvs;
+mark_lvs_to_expand ();
+
+sub examine_lv
+{
+ local $_ = shift;
+
+ $lvs{$_}->{name} = $_;
+
+ my $type = "unknown";
+ eval {
+ $type = $g->vfs_type ($_);
+ };
+ $lvs{$_}->{type} = $type;
+
+ if ($expand_content) {
+ if ($type =~ /^ext[234]$/) {
+ $lvs{$_}->{can_expand_content} = 1;
+ $lvs{$_}->{expand_content_method} = "resize2fs";
+ } elsif ($type eq "ntfs" && feature_available ($g, "ntfsprogs")) {
+ $lvs{$_}->{can_expand_content} = 1;
+ $lvs{$_}->{expand_content_method} = "ntfsresize";
+ }
+ }
+}
+
+sub mark_lvs_to_expand {
+ local $_;
+
+ foreach (@lv_expand) {
+ die __x("virt-resize: no logical volume called {n}\n",
+ n => $_)
+ unless exists $lvs{$_};
+
+ if ($lvs{$_}->{can_expand_content}) {
+ $lvs{$_}->{will_expand_content} = 1;
+ $to_be_expanded++;
+ }
+ }
+}
+
sub find_partition
{
local $_ = shift;
}
# Handle --resize and --resize-force.
-my $to_be_expanded = 0;
-
do_resize ($_, 0, "--resize") foreach @resize;
do_resize ($_, 1, "--resize-force") foreach @resize_force;
# EFI partitioning + massive per-partition alignment.
my $overhead = $sectsize * (
2 * 64 + # GPT start and end
- (64 * (@partitions + 1)) + # Maximum alignment
- ($boot_sectors - 64) # Boot loader
- );
+ (64 * (@partitions + 1)) # Maximum alignment
+ ) +
+ ($max_bootloader - 64 * 512); # boot loader
my $required = 0;
foreach (@partitions) {
}
}
+ foreach my $lv (@lv_expand) {
+ print __x("{n}: LV will be expanded to maximum size\n",
+ n => $lv);
+ }
+
+ foreach my $lv (@lvs) {
+ if ($lvs{$lv}->{will_expand_content}) {
+ print __x("{n}: content will be expanded using the '{meth}' method\n",
+ n => $lv,
+ meth => $lvs{$lv}->{expand_content_method});
+ }
+ }
+
if ($surplus > 0) {
print __x("There is a surplus of {spl} bytes ({h}).\n",
spl => $surplus,
# Repartition the target disk.
my $nextpart = 1;
-my $parttype;
repartition ();
sub repartition
{
local $_;
- if ($copy_boot_loader) {
- $parttype = $g->part_get_parttype ("/dev/sdb");
- } else {
- $parttype = "efi";
- }
- print "partition table type: $parttype\n" if $debug;
-
- # Delete any existing partitions on the destination disk,
- # but leave the bootloader that we copied over intact.
- if ($copy_boot_loader) {
- # Delete in reverse as an easy way to deal with extended
- # partitions.
- foreach (sort { $b cmp $a } $g->list_partitions ()) {
- if (m{^/dev/.db(\d+)$}) {
- $g->part_del ("/dev/sdb", $1);
- }
- }
- } else {
- # Didn't copy over the initial boot loader, so we need
- # to make a new partition table here.
- $g->part_init ("/dev/sdb", $parttype);
- }
-
# Work out where to start the first partition.
die __"virt-resize: source disk does not have a first partition\n"
unless exists ($partitions{"/dev/sda1"});
}
if (!$quiet && !$debug) {
- local $| = 1;
- print __x("Copying {p} ...", p => $part);
+ print __x("Copying {p} ...\n", p => $part);
}
$g->copy_size ($part, $target,
$newsize < $oldsize ? $newsize : $oldsize);
-
- if (!$quiet && !$debug) {
- print " ", __"done", "\n";
- }
}
}
}
if ($to_be_expanded > 0) {
restart_appliance ();
expand_partitions ();
+ expand_lvs ();
+ expand_lvs_content ();
}
sub restart_appliance
$g = Sys::Guestfs->new ();
$g->set_trace (1) if $debug;
- $g->add_drive ($outfile);
+ my @args = ($outfile);
+ push @args, format => $output_format if defined $output_format;
+ $g->add_drive_opts (@args);
$g->launch ();
# Target partitions have changed from /dev/sdb to /dev/sda,
# Expand if requested.
if ($partitions{$part}->{will_expand_content}) {
if (!$quiet && !$debug) {
- print __x("Expanding {p} using the '{meth}' method",
+ print __x("Expanding {p} using the '{meth}' method\n",
p => $part,
meth => $partitions{$part}->{expand_content_method});
}
}
}
+sub expand_lvs
+{
+ local $_;
+
+ foreach (@lv_expand) {
+ $g->lvresize_free ($_, 100);
+ }
+}
+
+sub expand_lvs_content
+{
+ local $_;
+
+ foreach (@lvs) {
+ if ($lvs{$_}->{will_expand_content}) {
+ my $method = $lvs{$_}->{expand_content_method};
+ if (!$quiet && !$debug) {
+ print __x("Expanding {p} using the '{meth}' method\n",
+ p => $_, meth => $method);
+ }
+ if ($method eq "resize2fs") {
+ $g->e2fsck_f ($_);
+ $g->resize2fs ($_);
+ } elsif ($method eq "ntfsresize") {
+ $g->ntfsresize ($_);
+ } else {
+ die "internal error: unknown method: $method";
+ }
+ }
+ }
+}
+
# Sync disk and exit.
$g->umount_all ();
$g->sync ();
}
}
-# 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
$_;
}
+# Not as sophisticated as the guestfish progress bar, because
+# I intend to use an external library for this at some point (XXX).
+sub progress_callback
+{
+ my $proc_nr = shift;
+ my $serial = shift;
+ my $position = shift;
+ my $total = shift;
+
+ my $ratio = $position / $total;
+ if ($ratio < 0) { $ratio = 0 }
+ elsif ($ratio > 1) { $ratio = 1 }
+
+ my $dots = int ($ratio * 76);
+
+ print "[", "#"x$dots, "-"x(76-$dots), "]\r";
+ print "\n" if $ratio == 1;
+}
+
=head1 NOTES
=head2 "Partition 1 does not end on cylinder boundary."
if NTFS partitions have been expanded. This is just a safety check
and (unless it find errors) is nothing to worry about.
+=head2 GUEST BOOT STUCK AT "GRUB"
+
+If a Linux guest does not boot after resizing, and the boot is stuck
+after printing C<GRUB> on the console, try reinstalling grub. This
+sometimes happens on older (RHEL 5-era) guests, for reasons we don't
+fully understand, although we think is to do with partition alignment.
+
+ guestfish -i -a newdisk
+ ><fs> cat /boot/grub/device.map
+ # check the contents of this file are sensible or
+ # edit the file if necessary
+ ><fs> grub-install / /dev/vda
+ ><fs> exit
+
+For more flexible guest reconfiguration, including if you need to
+specify other parameters to grub-install, use L<virt-rescue(1)>.
+
=head1 ALTERNATIVE TOOLS
There are several proprietary tools for resizing partitions. We
was designed to avoid. If you want to see the guestfish-equivalent
commands that virt-resize runs, use the C<--debug> flag.
+=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<sh(1)> for details.
+
=head1 SEE ALSO
L<virt-list-partitions(1)>,
L<ntfsresize(8)>,
L<virsh(1)>,
L<parted(8)>,
+L<truncate(1)>,
+L<fallocate(1)>,
+L<grub(8)>,
+L<grub-install(8)>,
+L<virt-rescue(1)>,
L<Sys::Guestfs(3)>,
L<http://libguestfs.org/>.