From: Richard Jones Date: Mon, 22 Mar 2010 18:38:31 +0000 (+0000) Subject: New tools: virt-resize and virt-list-partitions. X-Git-Tag: 1.0.87~1 X-Git-Url: http://git.annexia.org/?p=libguestfs.git;a=commitdiff_plain;h=def627e4daf2d32cebf91f5e01f44a0b7e512129 New tools: virt-resize and virt-list-partitions. 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. --- diff --git a/.gitignore b/.gitignore index 5914499..6a47234 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Makefile.am b/Makefile.am index 87da5a8..3ed7815 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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/' | \ diff --git a/po/POTFILES.in b/po/POTFILES.in index c88abc5..92106b6 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -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 diff --git a/tools/Makefile.am b/tools/Makefile.am index 6e6872c..943b1eb 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -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 \ diff --git a/tools/virt-list-filesystems b/tools/virt-list-filesystems index 0d52745..f0ee45f 100755 --- a/tools/virt-list-filesystems +++ b/tools/virt-list-filesystems @@ -194,6 +194,7 @@ L, L, L, L, +L, L, L, L, diff --git a/tools/virt-list-partitions b/tools/virt-list-partitions new file mode 100755 index 0000000..b8bc0cc --- /dev/null +++ b/tools/virt-list-partitions @@ -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 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. + +C is just a simple wrapper around +L functionality. For more complex cases you should +look at the L 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. 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 displays the type +and size of each partition too (where "type" means C, C 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, +L, +L, +L, +L, +L, +L, +L. + +=head1 AUTHOR + +Richard W.M. Jones L + +=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 index 0000000..1c4006a --- /dev/null +++ b/tools/virt-resize @@ -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=[+/-][%]] [--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 resize disk images in-place. Virt-resize +B 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, +L and +L, +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 like this to find the disk image name: + + # virsh dumpxml guestname | xpath /domain/devices/disk/source + Found 1 nodes: + -- NODE -- + + +=item 2. Look at current sizing + +Use L 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 to create a logical volume: + + # lvcreate -L 10G -n lv_name vg_name + +Or use L 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 to disk image C +I resizing or changing any existing partitions. If +C is larger, then an extra, empty partition is created at the +end of the disk covering the extra space. If C is smaller, +then it will give an error. + +To resize, you need to pass extra options (for the full list see the +L section below). + +L 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 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 automatically resized. You can resize it +afterwards either using L (offline) or using commands +inside the guest (online resizing). + +L 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 discarding the old one. + +If you are using libvirt, edit the XML to point at the new disk: + + # virsh edit guestname + +Change Esource ...E, see +L + +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, L and L. It is +also possible to do this offline (eg. for scripting changes) using +L. + +=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 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 B 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 expanded. You will need +to expand the filesystem (or PV) to fit the extra space either using +L (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 contain a filesystem or PV +which has already been shrunk using another tool (eg. L +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, 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: $!"; + $_ = ; + 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, +L, +L, +L, +L, +L, +L, +L, +L, +L, +L, +L. + +=head1 AUTHOR + +Richard W.M. Jones L + +=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.