Richard W.M. Jones
2011-Apr-08 13:23 UTC
[Libguestfs] [PATCH] Rewrite virt-resize in OCaml.
-- Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones libguestfs lets you edit virtual machines. Supports shell scripting, bindings from many languages. http://libguestfs.org -------------- next part -------------->From 7e6d023165462d87f8f42d90dd86df0effd98dd5 Mon Sep 17 00:00:00 2001From: Richard W.M. Jones <rjones at redhat.com> Date: Fri, 8 Apr 2011 14:07:26 +0100 Subject: [PATCH] Rewrite virt-resize in OCaml. This is a fairly straightforward translation of Perl virt-resize into OCaml. It is bug-for-bug and feature-for-feature identical to the Perl version, except as noted below. The motivation is to have a more solid, high-level, statically safe compiled language to go forwards with fixing some of the harder bugs in virt-resize. In particular contracts between different parts of the program are now handled by statically typed structures checked at compile time, instead of the very ad-hoc unchecked hash tables used by the Perl version. OCaml and the ocaml-pcre library (Perl-Compatible Regular Expressions bindings for OCaml) are required. Extra features in this version: - 32 bit hosts are now supported. - We try hard to handle the case where the target disk is not "clean" (ie. all zeroes). It usually works for this case, whereas the previous version would usually fail. However it is still recommended that the system administrator creates a fresh blank disk for the target before running the program. - User messages are a bit more verbose and helpful. You can turn these off with the -q (--quiet) option. There is one lost feature: - Ability to specify >= T (terabytes) sizes in command line size expressions has been removed. This probably didn't work in the Perl version. Other differences: - The order in which operations are performed has been changed to make it more logical. The user should not notice any functional difference, but debug messages will be quite a bit different. - virt-resize is a compiled binary, not a script. --- .gitignore | 2 + Makefile.am | 7 + README | 2 + configure.ac | 13 +- po/POTFILES.in | 1 - resize/.depend | 6 + resize/Makefile.am | 101 +++ resize/progress.ml | 49 ++ resize/resize.ml | 964 ++++++++++++++++++++++++++++ resize/run-resize-locally | 53 ++ resize/test-virt-resize.sh | 47 ++ resize/utils.ml | 154 +++++ resize/virt-resize.pod | 557 ++++++++++++++++ tools/Makefile.am | 4 +- tools/test-virt-resize.sh | 38 -- tools/virt-resize | 1530 -------------------------------------------- 16 files changed, 1954 insertions(+), 1574 deletions(-) create mode 100644 resize/.depend create mode 100644 resize/Makefile.am create mode 100644 resize/progress.ml create mode 100644 resize/resize.ml create mode 100755 resize/run-resize-locally create mode 100755 resize/test-virt-resize.sh create mode 100644 resize/utils.ml create mode 100644 resize/virt-resize.pod delete mode 100755 tools/test-virt-resize.sh delete mode 100755 tools/virt-resize diff --git a/.gitignore b/.gitignore index 9a844fa..67fd7a9 100644 --- a/.gitignore +++ b/.gitignore @@ -281,6 +281,8 @@ regressions/test.out rescue/stamp-virt-rescue.pod rescue/virt-rescue rescue/virt-rescue.1 +resize/virt-resize +resize/virt-resize.1 ruby/bindtests.rb ruby/doc/site/api ruby/examples/guestfs-ruby.3 diff --git a/Makefile.am b/Makefile.am index 96c4d35..3d629e9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -64,6 +64,13 @@ endif # Unconditional because nothing is built yet. SUBDIRS += csharp +# virt-resize 2.0 is written in OCaml. +if HAVE_OCAML +if HAVE_OCAML_PCRE +SUBDIRS += resize +endif +endif + # Perl tools and guestmount. if HAVE_TOOLS SUBDIRS += tools diff --git a/README b/README index fbff025..659feb9 100644 --- a/README +++ b/README @@ -59,6 +59,8 @@ Requirements - (Optional) OCaml if you want to rebuild the generated files, and also to build the OCaml bindings +- (Optional) OCaml PCRE bindings (ocaml-pcre). + - (Optional) Perl if you want to build the perl bindings - (Optional) Python if you want to build the python bindings diff --git a/configure.ac b/configure.ac index 268119a..5b69c9d 100644 --- a/configure.ac +++ b/configure.ac @@ -476,9 +476,14 @@ AS_IF([test "x$enable_ocaml" != "xno"], OCAMLFIND AC_PROG_OCAML AC_PROG_FINDLIB + + AS_IF([test "x$OCAMLC" != "xno" && test "x$OCAMLFIND" != "xno"], + [AC_CHECK_OCAML_PKG([pcre])]) ]) AM_CONDITIONAL([HAVE_OCAML], [test "x$OCAMLC" != "xno" && test "x$OCAMLFIND" != "xno"]) +AM_CONDITIONAL([HAVE_OCAML_PCRE], + [test "x$OCAML_PKG_pcre" != "xno"]) dnl Check for Perl (optional, for Perl bindings). PERL=no @@ -843,7 +848,8 @@ AC_CONFIG_FILES([Makefile df/Makefile rescue/Makefile debian/changelog - ocaml/META perl/Makefile.PL]) + ocaml/META perl/Makefile.PL + resize/Makefile]) AC_OUTPUT dnl Produce summary. @@ -871,8 +877,11 @@ echo -n "Haskell bindings .................... " if test "x$HAVE_HASKELL_TRUE" = "x"; then echo "yes"; else echo "no"; fi echo -n "PHP bindings ........................ " if test "x$HAVE_PHP_TRUE" = "x"; then echo "yes"; else echo "no"; fi -echo -n "virt-* tools ........................ " +echo "guestfish and C virt tools .......... yes" +echo -n "Perl virt tools ..................... " if test "x$HAVE_TOOLS_TRUE" = "x"; then echo "yes"; else echo "no"; fi +echo -n "virt-resize ......................... " +if test "x$HAVE_OCAML" = "x" && test "x$HAVE_OCAML_PCRE" = "x"; then echo "yes"; else echo "no"; fi echo "FUSE filesystem ..................... $enable_fuse" echo echo "If any optional component is configured 'no' when you expected 'yes'" diff --git a/po/POTFILES.in b/po/POTFILES.in index 5f5919a..e0b2b29 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -147,6 +147,5 @@ tools/virt-edit.pl tools/virt-list-filesystems.pl tools/virt-list-partitions.pl tools/virt-make-fs.pl -tools/virt-resize.pl tools/virt-tar.pl tools/virt-win-reg.pl diff --git a/resize/.depend b/resize/.depend new file mode 100644 index 0000000..96f38cc --- /dev/null +++ b/resize/.depend @@ -0,0 +1,6 @@ +progress.cmo: utils.cmo +progress.cmx: utils.cmx +resize.cmo: utils.cmo progress.cmo +resize.cmx: utils.cmx progress.cmx +utils.cmo: +utils.cmx: diff --git a/resize/Makefile.am b/resize/Makefile.am new file mode 100644 index 0000000..8ef16ff --- /dev/null +++ b/resize/Makefile.am @@ -0,0 +1,101 @@ +# libguestfs virt-resize 2.0 tools +# Copyright (C) 2011 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 = \ + $(SOURCES) \ + run-resize-locally \ + virt-resize.pod \ + test-virt-resize.sh + +CLEANFILES = *~ *.cmi *.cmo *.cmx *.cmxa *.o virt-resize test.img + +if HAVE_OCAML +if HAVE_OCAML_PCRE + +# Alphabetical order. +SOURCES = \ + progress.ml \ + resize.ml \ + utils.ml + +# Note this list must be in dependency order. +OBJECTS = \ + utils.cmx \ + progress.cmx \ + resize.cmx + +bin_SCRIPTS = virt-resize + +OCAMLPACKAGES = -package guestfs,pcre +OCAMLCFLAGS = -g -warn-error CDEFLMPSUVYZX $(OCAMLPACKAGES) +OCAMLOPTFLAGS = $(OCAMLCFLAGS) + +virt-resize: $(OBJECTS) + $(OCAMLFIND) ocamlopt $(OCAMLOPTFLAGS) -linkpkg $^ -o $@ + +.mli.cmi: + $(OCAMLFIND) ocamlc $(OCAMLCFLAGS) -c $< -o $@ +.ml.cmo: + $(OCAMLFIND) ocamlc $(OCAMLCFLAGS) -c $< -o $@ +.ml.cmx: + $(OCAMLFIND) ocamlopt $(OCAMLCFLAGS) -c $< -o $@ + +# Manual pages and HTML files for the website. + +man_MANS = virt-resize.1 + +noinst_DATA = $(top_builddir)/html/virt-resize.1.html + +virt-%.1: virt-%.pod + $(top_srcdir)/podwrapper.sh \ + --man $@ \ + $< + +$(top_builddir)/html/virt-%.1.html: virt-% + $(top_srcdir)/podwrapper.sh \ + --html $@ \ + $< + +# 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-resize.sh + +# Dependencies. +depend: .depend + +.depend: $(wildcard *.mli) $(wildcard *.ml) + rm -f $@ $@-t + $(OCAMLFIND) ocamldep $(OCAMLPACKAGES) $^ | \ + $(SED) -e :a -e '/ *\\$$/N; s/ *\\\n */ /; ta' | \ + sort > $@-t + mv $@-t $@ + +include .depend + +.PHONY: depend docs + +endif +endif diff --git a/resize/progress.ml b/resize/progress.ml new file mode 100644 index 0000000..a4eff0f --- /dev/null +++ b/resize/progress.ml @@ -0,0 +1,49 @@ +(* virt-resize + * Copyright (C) 2010-2011 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + *) + +open Printf + +open Utils + +module G = Guestfs + +let set_up_progress_bar (g : Guestfs.guestfs) + let progress_callback g event evh buf array + if event = G.EVENT_PROGRESS && Array.length array >= 4 then ( + (*let proc_nr = array.(0) + and serial = array.(1)*) + let position = array.(2) + and total = array.(3) in + + let ratio + if total <> 0L then Int64.to_float position /. Int64.to_float total + else 0. in + let ratio + if ratio < 0. then 0. else if ratio > 1. then 1. else ratio in + + let dots = int_of_float (ratio *. 72.) in + + print_string "["; + for i = 0 to dots-1 do print_char '#' done; + for i = dots to 71 do print_char '-' done; + print_string "]\r"; + if ratio = 1. then print_string "\n"; + flush stdout + ) + in + ignore (g#set_event_callback progress_callback [G.EVENT_PROGRESS]) diff --git a/resize/resize.ml b/resize/resize.ml new file mode 100644 index 0000000..c7321ee --- /dev/null +++ b/resize/resize.ml @@ -0,0 +1,964 @@ +(* virt-resize + * Copyright (C) 2010-2011 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + *) + +open Printf + +module G = Guestfs + +open Utils + +(* Minimum surplus before we create an extra partition. *) +let min_extra_partition = 10L *^ 1024L *^ 1024L + +(* Command line argument parsing. *) +let prog = Filename.basename Sys.executable_name + +let infile, outfile, copy_boot_loader, debug, deletes, dryrun, + expand, expand_content, extra_partition, format, ignores, + lv_expands, output_format, + quiet, resizes, resizes_force, shrink + let display_version () + let g = new G.guestfs () in + let version = g#version () in + printf "virt-resize %Ld.%Ld.%Ld%s" + version.G.major version.G.minor version.G.release version.G.extra; + exit 0 + in + + let add xs s = xs := s :: !xs in + + let copy_boot_loader = ref true in + let debug = ref false in + let deletes = ref [] in + let dryrun = ref false in + let expand = ref "" in + let set_expand s + if s = "" then error "%s: empty --expand option" prog + else if !expand <> "" then error "--expand option given twice" + else expand := s + in + let expand_content = ref true in + let extra_partition = ref true in + let format = ref "" in + let ignores = ref [] in + let lv_expands = ref [] in + let output_format = ref "" in + let quiet = ref false in + let resizes = ref [] in + let resizes_force = ref [] in + let shrink = ref "" in + let set_shrink s + if s = "" then error "empty --shrink option" + else if !shrink <> "" then error "--shrink option given twice" + else shrink := s + in + + let argspec = Arg.align [ + "--no-copy-boot-loader", Arg.Clear copy_boot_loader, " Don't copy boot loader"; + "-d", Arg.Set debug, " Enable debugging messages"; + "--debug", Arg.Set debug, " -\"-"; + "--delete", Arg.String (add deletes), "dev Delete partition"; + "--expand", Arg.String set_expand, "dev Expand partition"; + "--no-expand-content", Arg.Clear expand_content, " Don't expand content"; + "--no-extra-partition", Arg.Clear extra_partition, " Don't create extra partition"; + "--format", Arg.Set_string format, "format Format of input disk"; + "--ignore", Arg.String (add ignores), "dev Ignore partition"; + "--lv-expand", Arg.String (add lv_expands), "lv Expand logical volume"; + "--LV-expand", Arg.String (add lv_expands), "lv -\"-"; + "--lvexpand", Arg.String (add lv_expands), "lv -\"-"; + "--LVexpand", Arg.String (add lv_expands), "lv -\"-"; + "-n", Arg.Set dryrun, " Don't perform changes"; + "--dryrun", Arg.Set dryrun, " -\"-"; + "--dry-run", Arg.Set dryrun, " -\"-"; + "--output-format", Arg.Set_string format, "format Format of output disk"; + "-q", Arg.Set quiet, " Don't print the summary"; + "--quiet", Arg.Set quiet, " -\"-"; + "--resize", Arg.String (add resizes), "part=size Resize partition"; + "--resize-force", Arg.String (add resizes_force), "part=size Forcefully resize partition"; + "--shrink", Arg.String set_shrink, "dev Shrink partition"; + "-V", Arg.Unit display_version, " Display version and exit"; + "--version", Arg.Unit display_version, " -\"-"; + ] in + let disks = ref [] in + let anon_fun s = disks := s :: !disks in + let usage_msg + sprintf "\ +%s: resize a virtual machine disk + +A short summary of the options is given below. For detailed help please +read the man page virt-resize(1). +" + prog in + Arg.parse argspec anon_fun usage_msg; + + let debug = !debug in + if debug then ( + eprintf "command line:"; + List.iter (eprintf " %s") (Array.to_list Sys.argv); + prerr_newline () + ); + + (* Dereference the rest of the args. *) + let copy_boot_loader = !copy_boot_loader in + let deletes = List.rev !deletes in + let dryrun = !dryrun in + let expand = match !expand with "" -> None | str -> Some str in + let expand_content = !expand_content in + let extra_partition = !extra_partition in + let format = match !format with "" -> None | str -> Some str in + let ignores = List.rev !ignores in + let lv_expands = List.rev !lv_expands in + let output_format = match !output_format with "" -> None | str -> Some str in + let quiet = !quiet in + let resizes = List.rev !resizes in + let resizes_force = List.rev !resizes_force in + let shrink = match !shrink with "" -> None | str -> Some str in + + (* Verify we got exactly 2 disks. *) + let infile, outfile + match List.rev !disks with + | [infile; outfile] -> infile, outfile + | _ -> + error "usage is: %s [--options] indisk outdisk" prog in + + infile, outfile, copy_boot_loader, debug, deletes, dryrun, + expand, expand_content, extra_partition, format, ignores, + lv_expands, output_format, + quiet, resizes, resizes_force, shrink + +(* Add in and out disks to the handle and launch. *) +let connect_both_disks () + let g = new G.guestfs () in + if debug then g#set_trace true; + g#add_drive_opts ?format ~readonly:true infile; + g#add_drive_opts ?format:output_format ~readonly:false outfile; + if not quiet then Progress.set_up_progress_bar g; + g#launch (); + + (* Set the filter to /dev/sda, in case there are any rogue + * PVs lying around on the target disk. + *) + g#lvm_set_filter [|"/dev/sda"|]; + + g + +let g + if not quiet then + printf "Examining %s ...\n%!" infile; + + let g = connect_both_disks () in + + g + +(* 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. + *) +let sectsize, insize, outsize + let sectsize = g#blockdev_getss "/dev/sdb" in + let insize = g#blockdev_getsize64 "/dev/sda" in + let outsize = g#blockdev_getsize64 "/dev/sdb" in + if debug then ( + eprintf "%s size %Ld bytes\n" infile insize; + eprintf "%s size %Ld bytes\n" outfile outsize + ); + sectsize, insize, outsize + +let max_bootloader + (* 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). + * + * However make this large enough to be sure that we have copied over + * the boot loader. We could also do this by looking for the sector + * offset of the first partition. + * + * It doesn't matter if we copy too much. + *) + 4096 * 512 + +(* Check the disks are at least as big as the bootloader. *) +let () + if insize < Int64.of_int max_bootloader then + error "%s: file is too small to be a disk image (%Ld bytes)" + infile insize; + if outsize < Int64.of_int max_bootloader then + error "%s: file is too small to be a disk image (%Ld bytes)" + outfile outsize + +(* Build a data structure describing the source disk's partition layout. *) +type partition = { + p_name : string; (* Device name, like /dev/sda1. *) + p_size : int64; (* Current size of this partition. *) + p_part : G.partition; (* Partition data from libguestfs. *) + p_bootable : bool; (* Is it bootable? *) + p_mbr_id : int option; (* MBR ID, if it has one. *) + p_type : partition_content; (* Content type and content size. *) + mutable p_operation : partition_operation; (* What we're going to do. *) + mutable p_target_partnum : int; (* Partition number on target. *) +} +and partition_content + | ContentUnknown (* undetermined *) + | ContentPV of int64 (* physical volume (size of PV) *) + | ContentFS of string * int64 (* mountable filesystem (FS type, FS size) *) +and partition_operation + | OpCopy (* copy it as-is, no resizing *) + | OpIgnore (* ignore it (create on target, but don't + copy any content) *) + | OpDelete (* delete it *) + | OpResize of int64 (* resize it to the new size *) + +let rec debug_partition p + eprintf "%s:\n" p.p_name; + eprintf "\tpartition data: %ld %Ld-%Ld (%Ld bytes)\n" + p.p_part.G.part_num p.p_part.G.part_start p.p_part.G.part_end + p.p_part.G.part_size; + eprintf "\tbootable: %b\n" p.p_bootable; + eprintf "\tpartition ID: %s\n" + (match p.p_mbr_id with None -> "(none)" | Some i -> sprintf "0x%x" i); + eprintf "\tcontent: %s\n" (string_of_partition_content p.p_type) +and string_of_partition_content = function + | ContentUnknown -> "unknown data" + | ContentPV sz -> sprintf "LVM PV (%Ld bytes)" sz + | ContentFS (fs, sz) -> sprintf "filesystem %s (%Ld bytes)" fs sz +and string_of_partition_content_no_size = function + | ContentUnknown -> "unknown data" + | ContentPV _ -> sprintf "LVM PV" + | ContentFS (fs, _) -> sprintf "filesystem %s" fs + +let get_partition_content + let pvs_full = Array.to_list (g#pvs_full ()) in + fun dev -> + try + let fs = g#vfs_type dev in + if fs = "unknown" then + ContentUnknown + else if fs = "LVM2_member" then ( + let rec loop = function + | [] -> + error "%s: physical volume not returned by pvs_full" + dev + | pv :: _ when canonicalize pv.G.pv_name = dev -> + ContentPV pv.G.pv_size + | _ :: pvs -> loop pvs + in + loop pvs_full + ) + else ( + g#mount_ro dev "/"; + let stat = g#statvfs "/" in + let size = stat.G.bsize *^ stat.G.blocks in + ContentFS (fs, size) + ) + with + G.Error _ -> ContentUnknown + +let partitions : partition list + let parts = Array.to_list (g#part_list "/dev/sda") in + + if List.length parts = 0 then + error "the source disk has no partitions"; + + let partitions + List.map ( + fun ({ G.part_num = part_num } as part) -> + let part_num = Int32.to_int part_num in + let name = sprintf "/dev/sda%d" part_num in + let bootable = g#part_get_bootable "/dev/sda" part_num in + let mbr_id + try Some (g#part_get_mbr_id "/dev/sda" part_num) + with G.Error _ -> None in + let typ = get_partition_content name in + + { p_name = name; p_size = part.G.part_size; p_part = part; + p_bootable = bootable; p_mbr_id = mbr_id; p_type = typ; + p_operation = OpCopy; p_target_partnum = 0 } + ) parts in + + if debug then ( + eprintf "%d partitions found\n" (List.length partitions); + List.iter debug_partition partitions + ); + + (* Check content isn't larger than partitions. If it is then + * something has gone wrong and we shouldn't continue. Old + * virt-resize didn't do these checks. + *) + List.iter ( + function + | { p_name = name; p_size = size; p_type = ContentPV pv_size } + when size < pv_size -> + error "%s: partition size %Ld < physical volume size %Ld" + name size pv_size + | { p_name = name; p_size = size; p_type = ContentFS (_, fs_size) } + when size < fs_size -> + error "%s: partition size %Ld < filesystem size %Ld" + name size fs_size + | _ -> () + ) partitions; + + (* Check partitions don't overlap. *) + let rec loop end_of_prev = function + | [] -> () + | { p_name = name; p_part = { G.part_start = part_start } } :: _ + when end_of_prev > part_start -> + error "%s: this partition overlaps the previous one" name + | { p_part = { G.part_end = part_end } } :: parts -> loop part_end parts + in + loop 0L partitions; + + partitions + +(* Build a data structure describing LVs on the source disk. + * This is only used if the user gave the --lv-expand option. + *) +type logvol = { + lv_name : string; + lv_type : logvol_content; + mutable lv_operation : logvol_operation +} +and logvol_content = partition_content (* except ContentPV cannot occur *) +and logvol_operation + | LVOpNone (* nothing *) + | LVOpExpand (* expand it *) + +let debug_logvol lv + eprintf "%s:\n" lv.lv_name; + eprintf "\tcontent: %s\n" (string_of_partition_content lv.lv_type) + +let lvs + let lvs = Array.to_list (g#lvs ()) in + + let lvs = List.map ( + fun name -> + let typ = get_partition_content name in + assert (match typ with ContentPV _ -> false | _ -> true); + + { lv_name = name; lv_type = typ; lv_operation = LVOpNone } + ) lvs in + + if debug then ( + eprintf "%d logical volumes found\n" (List.length lvs); + List.iter debug_logvol lvs + ); + + lvs + +(* These functions tell us if we know how to expand the content of + * a particular partition or LV, and what method to use. + *) +type expand_content_method = PVResize | Resize2fs | NTFSResize + +let string_of_expand_content_method = function + | PVResize -> "pvresize" + | Resize2fs -> "resize2fs" + | NTFSResize -> "ntfsresize" + +let can_expand_content + if expand_content then + function + | ContentUnknown -> false + | ContentPV _ -> true + | ContentFS (("ext2"|"ext3"|"ext4"), _) -> true + | ContentFS (("ntfs"), _) when feature_available g "ntfsprogs" -> true + | ContentFS (_, _) -> false + else + fun _ -> false + +let expand_content_method + if expand_content then + function + | ContentUnknown -> assert false + | ContentPV _ -> PVResize + | ContentFS (("ext2"|"ext3"|"ext4"), _) -> Resize2fs + | ContentFS (("ntfs"), _) when feature_available g "ntfsprogs" -> NTFSResize + | ContentFS (_, _) -> assert false + else + fun _ -> assert false + +(* Helper function to locate a partition given what the user might + * type on the command line. It also gives errors for partitions + * that the user has asked to be ignored or deleted. + *) +let find_partition + let hash = Hashtbl.create 13 in + List.iter (fun ({ p_name = name } as p) -> Hashtbl.add hash name p) + partitions; + fun ~option name -> + let name + if String.length name < 5 || String.sub name 0 5 <> "/dev/" then + "/dev/" ^ name + else + name in + let name = canonicalize name in + + let partition + try Hashtbl.find hash name + with Not_found -> + error "%s: partition not found in the source disk image (this error came from '%s' option on the command line). Try running this command: virt-filesystems --partitions --long -a %s" + name option infile in + + if partition.p_operation = OpIgnore then + error "%s: partition already ignored, you cannot use it in '%s' option" + name option; + + if partition.p_operation = OpDelete then + error "%s: partition already deleted, you cannot use it in '%s' option" + name option; + + partition + +(* Handle --ignore option. *) +let () + List.iter ( + fun dev -> + let p = find_partition ~option:"--ignore" dev in + p.p_operation <- OpIgnore + ) ignores + +(* Handle --delete option. *) +let () + List.iter ( + fun dev -> + let p = find_partition ~option:"--delete" dev in + p.p_operation <- OpDelete + ) deletes + +(* Helper function to mark a partition for resizing. It prevents the + * user from trying to mark the same partition twice. If the force + * flag is given, then we will allow the user to shrink the partition + * even if we think that would destroy the content. + *) +let mark_partition_for_resize ~option ?(force = false) p newsize + let name = p.p_name in + let oldsize = p.p_size in + + (match p.p_operation with + | OpResize _ -> + error "%s: this partition has already been marked for resizing" + name + | OpIgnore | OpDelete -> + (* This error should have been caught already by find_partition ... *) + error "%s: this partition has already been ignored or deleted" + name + | OpCopy -> () + ); + + (* Only do something if the size will change. *) + if oldsize <> newsize then ( + let bigger = newsize > oldsize in + + if not bigger && not force then ( + (* Check if this contains filesystem content, and how big that is + * and whether we will destroy any content by shrinking this. + *) + match p.p_type with + | ContentUnknown -> + error "%s: This partition has unknown content which might be damaged by shrinking it. If you want to shrink this partition, you need to use the '--resize-force' option, but that could destroy any data on this partition. (This error came from '%s' option on the command line.)" + name option + | ContentPV size when size > newsize -> + error "%s: This partition has contains an LVM physical volume which will be damaged by shrinking it below %Ld bytes (user asked to shrink it to %Ld bytes). If you want to shrink this partition, you need to use the '--resize-force' option, but that could destroy any data on this partition. (This error came from '%s' option on the command line.)" + name size newsize option + | ContentPV _ -> () + | ContentFS (fstype, size) when size > newsize -> + error "%s: This partition has contains a %s filesystem which will be damaged by shrinking it below %Ld bytes (user asked to shrink it to %Ld bytes). If you want to shrink this partition, you need to use the '--resize-force' option, but that could destroy any data on this partition. (This error came from '%s' option on the command line.)" + name fstype size newsize option + | ContentFS _ -> () + ); + + p.p_operation <- OpResize newsize + ) + +(* Handle --resize and --resize-force options. *) +let () + let do_resize ~option ?(force = false) arg + (* Argument is "dev=size". *) + let dev, sizefield + try + let i = String.index arg '=' in + let n = String.length arg - (i+1) in + if n == 0 then raise Not_found; + String.sub arg 0 i, String.sub arg (i+1) n + with Not_found -> + error "%s: missing size field in '%s' option" arg option in + + let p = find_partition ~option dev in + + (* Parse the size field. *) + let oldsize = p.p_size in + let newsize = parse_size oldsize sizefield in + + if newsize <= 0L then + error "%s: new partition size is zero or negative" dev; + + mark_partition_for_resize ~option ~force p newsize + in + + List.iter (do_resize ~option:"--resize") resizes; + List.iter (do_resize ~option:"--resize-force" ~force:true) resizes_force + +(* Helper function calculates the surplus space, given the total + * required so far for the current partition layout, compared to + * the size of the target disk. If the return value >= 0 then it's + * a surplus, if it is < 0 then it's a deficit. + *) +let calculate_surplus () + (* We need some overhead for partitioning. Worst case would be for + * EFI partitioning + massive per-partition alignment. + *) + let nr_partitions = List.length partitions in + let overhead = (Int64.of_int sectsize) *^ ( + 2L *^ 64L +^ (* GPT start and end *) + (64L *^ (Int64.of_int (nr_partitions + 1))) (* Maximum alignment *) + ) +^ + (Int64.of_int (max_bootloader - 64 * 512)) in (* Bootloader *) + + let required = List.fold_left ( + fun total p -> + let newsize + match p.p_operation with + | OpCopy | OpIgnore -> p.p_size + | OpDelete -> 0L + | OpResize newsize -> newsize in + total +^ newsize + ) 0L partitions in + + outsize -^ (required +^ overhead) + +(* Handle --expand and --shrink options. *) +let () + if expand <> None && shrink <> None then + error "you cannot use options --expand and --shrink together"; + + if expand <> None || shrink <> None then ( + let surplus = calculate_surplus () in + + if debug then + eprintf "surplus before --expand or --shrink: %Ld\n" surplus; + + (match expand with + | None -> () + | Some dev -> + if surplus < 0L then + error "You cannot use --expand when there is no surplus space to expand into. You need to make the target disk larger by at least %s." + (human_size (Int64.neg surplus)); + + let option = "--expand" in + let p = find_partition ~option dev in + let oldsize = p.p_size in + mark_partition_for_resize ~option p (oldsize +^ surplus) + ); + (match shrink with + | None -> () + | Some dev -> + if surplus > 0L then + error "You cannot use --shrink when there is no deficit (see 'deficit' in the virt-resize(1) man page)."; + + let option = "--shrink" in + let p = find_partition ~option dev in + let oldsize = p.p_size in + mark_partition_for_resize ~option p (oldsize +^ surplus) + ) + ) + +(* Calculate the final surplus. + * At this point, this number must be >= 0. + *) +let surplus + let surplus = calculate_surplus () in + + if surplus < 0L then ( + let deficit = Int64.neg surplus in + error "There is a deficit of %Ld bytes (%s). You need to make the target disk larger by at least this amount or adjust your resizing requests." + deficit (human_size deficit) + ); + + surplus + +(* Mark the --lv-expand LVs. *) +let () + let hash = Hashtbl.create 13 in + List.iter (fun ({ lv_name = name } as lv) -> Hashtbl.add hash name lv) lvs; + + List.iter ( + fun name -> + let lv + try Hashtbl.find hash name + with Not_found -> + error "%s: logical volume not found in the source disk image (this error came from '--lv-expand' option on the command line). Try running this command: virt-filesystems --logical-volumes --long -a %s" + name infile in + lv.lv_operation <- LVOpExpand + ) lv_expands + +(* Print a summary of what we will do. *) +let () + flush stderr; + + if not quiet then ( + printf "**********\n\n"; + printf "Summary of changes:\n\n"; + + List.iter ( + fun ({ p_name = name; p_size = oldsize } as p) -> + let text + match p.p_operation with + | OpCopy -> + sprintf "%s: This partition will be left alone." name + | OpIgnore -> + sprintf "%s: This partition will be created, but the contents will be ignored (ie. not copied to the target)." name + | OpDelete -> + sprintf "%s: This partition will be deleted." name + | OpResize newsize -> + sprintf "%s: This partition will be resized from %s to %s." + name (human_size oldsize) (human_size newsize) ^ + if can_expand_content p.p_type then ( + sprintf " The %s on %s will be expanded using the '%s' method." + (string_of_partition_content_no_size p.p_type) + name + (string_of_expand_content_method + (expand_content_method p.p_type)) + ) else "" in + + wrap ~hanging:4 (text ^ "\n\n") + ) partitions; + + List.iter ( + fun ({ lv_name = name } as lv) -> + match lv.lv_operation with + | LVOpNone -> () + | LVOpExpand -> + let text + sprintf "%s: This logical volume will be expanded to maximum size." + name ^ + if can_expand_content lv.lv_type then ( + sprintf " The %s on %s will be expanded using the '%s' method." + (string_of_partition_content_no_size lv.lv_type) + name + (string_of_expand_content_method + (expand_content_method lv.lv_type)) + ) else "" in + + wrap ~hanging:4 (text ^ "\n\n") + ) lvs; + + if surplus > 0L then ( + let text + sprintf "There is a surplus of %s." (human_size surplus) ^ + if extra_partition then ( + if surplus >= min_extra_partition then + sprintf " An extra partition will be created for the surplus." + else + sprintf " The surplus space is not large enough for an extra partition to be created and so it will just be ignored." + ) else + sprintf " The surplus space will be ignored. Run a partitioning program in the guest to partition this extra space if you want." in + + wrap (text ^ "\n\n") + ); + + printf "**********\n"; + flush stdout + ); + + if dryrun then exit 0 + +(* 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. + *) +let g, parttype + let parttype = g#part_get_parttype "/dev/sda" in + if debug then eprintf "partition table type: %s\n%!" parttype; + + (* Try hard to initialize the partition table. This might involve + * relaunching another handle. + *) + if not quiet then + printf "Setting up initial partition table on %s ...\n%!" outfile; + + let last_error = ref "" in + let rec initialize_partition_table g attempts + let ok + try g#part_init "/dev/sdb" parttype; true + with G.Error error -> last_error := error; false in + if ok then g, true + else if attempts > 0 then ( + g#zero "/dev/sdb"; + g#sync (); + g#close (); + + let g = connect_both_disks () in + initialize_partition_table g (attempts-1) + ) + else g, false + in + + let g, ok = initialize_partition_table g 5 in + if not ok then + error "Failed to initialize the partition table on the target disk. You need to wipe or recreate the target disk and then run virt-resize again.\n\nThe underlying error was: %s" !last_error; + + g, parttype + +(* Copy the bootloader across. + * 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 + *) +let () + if copy_boot_loader then ( + let bootsect = g#pread_device "/dev/sda" 446 0L in + if String.length bootsect < 446 then + error "pread-device: short read"; + ignore (g#pwrite_device "/dev/sdb" bootsect 0L); + + let start + if parttype <> "gpt" then 512L + else + (* XXX With 4K sectors does GPT just fit more entries in a + * sector, or does it always use 34 sectors? + *) + 17408L in + + let loader = g#pread_device "/dev/sda" max_bootloader start in + if String.length loader < max_bootloader then + error "pread-device: short read"; + ignore (g#pwrite_device "/dev/sdb" loader start) + ) + +(* Repartition the target disk. *) +let () + (* The first partition must start at the same position as the old + * first partition. Old virt-resize used to align this to 64 + * sectors, but I suspect this is the cause of boot failures, so + * let's not do this. + *) + let sectsize = Int64.of_int sectsize in + let start = ref ((List.hd partitions).p_part.G.part_start /^ sectsize) in + + (* This counts the partition numbers on the target disk. *) + let nextpart = ref 1 in + + let rec repartition = function + | [] -> () + | p :: ps -> + let target_partnum + match p.p_operation with + | OpDelete -> None (* do nothing *) + | OpIgnore | OpCopy -> (* new partition, same size *) + (* Size in sectors. *) + let size = (p.p_size +^ sectsize -^ 1L) /^ sectsize in + Some (add_partition size) + | OpResize newsize -> (* new partition, resized *) + (* Size in sectors. *) + let size = (newsize +^ sectsize -^ 1L) /^ sectsize in + Some (add_partition size) in + + (match target_partnum with + | None -> (* OpDelete *) + () + | Some target_partnum -> (* not OpDelete *) + p.p_target_partnum <- target_partnum; + + (* Set bootable and MBR IDs *) + if p.p_bootable then + g#part_set_bootable "/dev/sdb" target_partnum true; + + (match p.p_mbr_id with + | None -> () + | Some mbr_id -> + g#part_set_mbr_id "/dev/sdb" target_partnum mbr_id + ); + ); + + repartition ps + + (* Add a partition, returns the partition number on the target. *) + and add_partition size (* in SECTORS *) + let target_partnum, end_ + if !nextpart <= 3 || parttype <> "msdos" then ( + let target_partnum = !nextpart in + let end_ = !start +^ size -^ 1L in + g#part_add "/dev/sdb" "primary" !start end_; + incr nextpart; + target_partnum, end_ + ) else ( + if !nextpart = 4 then ( + g#part_add "/dev/sdb" "extended" !start (-1L); + incr nextpart; + start := !start +^ 64L + ); + let target_partnum = !nextpart in + let end_ = !start +^ size -^ 1L in + g#part_add "/dev/sdb" "logical" !start end_; + incr nextpart; + target_partnum, end_ + ) in + + (* Start of next partition + alignment to 64 sectors. *) + start := ((end_ +^ 1L) +^ 63L) &^ (~^ 63L); + + target_partnum + in + + repartition partitions; + + (* Create the surplus partition. *) + if extra_partition && surplus >= min_extra_partition then ( + let size = outsize /^ sectsize -^ 64L -^ !start in + ignore (add_partition size) + ) + +(* Copy over the data. *) +let () + let rec copy_data = function + | [] -> () + + | ({ p_name = source; p_target_partnum = target_partnum } as p) :: ps + when target_partnum > 0 -> + let oldsize = p.p_size in + let newsize + match p.p_operation with OpResize s -> s | _ -> oldsize in + + let copysize = if newsize < oldsize then newsize else oldsize in + + let target = sprintf "/dev/sdb%d" target_partnum in + + if not quiet then + printf "Copying %s ...\n%!" source; + + g#copy_size source target copysize; + + copy_data ps + + | _ :: ps -> + copy_data ps + in + + copy_data partitions + +(* After copying the data over we must shut down and restart the + * appliance in order to expand the content. The reason for this may + * not be obvious, but it's because otherwise we'll have duplicate VGs + * (the old VG(s) and the new VG(s)) which breaks LVM. + * + * The restart is only required if we're going to expand something. + *) +let to_be_expanded + List.exists ( + function + | ({ p_operation = OpResize _ } as p) -> can_expand_content p.p_type + | _ -> false + ) partitions + || List.exists ( + function + | ({ lv_operation = LVOpExpand } as lv) -> can_expand_content lv.lv_type + | _ -> false + ) lvs + +let g + if to_be_expanded then ( + g#umount_all (); + g#sync (); + g#close (); + + let g = new G.guestfs () in + if debug then g#set_trace true; + g#add_drive_opts ?format:output_format ~readonly:false outfile; + if not quiet then Progress.set_up_progress_bar g; + g#launch (); + + g (* Return new handle. *) + ) + else g (* Return existing handle. *) + +(* Helper function to expand partition or LV content. *) +let do_expand_content target = function + | PVResize -> g#pvresize target + | Resize2fs -> + g#e2fsck_f target; + g#resize2fs target + | NTFSResize -> g#ntfsresize target + +let () + if to_be_expanded then ( + (* Expand partition content as required. *) + List.iter ( + function + | ({ p_operation = OpResize _ } as p) when can_expand_content p.p_type -> + let source = p.p_name in + let target = sprintf "/dev/sda%d" p.p_target_partnum in + let meth = expand_content_method p.p_type in + + if not quiet then + printf "Expanding %s%s using the '%s' method ...\n%!" + source + (if source <> target then sprintf " (now %s)" target else "") + (string_of_expand_content_method meth); + + do_expand_content target meth + | _ -> () + ) partitions; + + (* Expand logical volume content as required. *) + List.iter ( + function + | ({ lv_operation = LVOpExpand } as lv) when can_expand_content lv.lv_type -> + let name = lv.lv_name in + let meth = expand_content_method lv.lv_type in + + if not quiet then + printf "Expanding %s using the '%s' method ...\n%!" + name + (string_of_expand_content_method meth); + + (* First expand the LV itself to maximum size. *) + g#lvresize_free name 100; + + (* Then expand the content in the LV. *) + do_expand_content name meth + | _ -> () + ) lvs + ) + +(* Finished. Unmount disks and exit. *) +let () + g#umount_all (); + g#sync (); + g#close (); + + if not quiet then ( + print_newline (); + wrap "Resize operation completed with no errors. Before deleting the old disk, carefully check that the resized disk boots and works correctly.\n"; + ); + + exit 0 diff --git a/resize/run-resize-locally b/resize/run-resize-locally new file mode 100755 index 0000000..0d840dd --- /dev/null +++ b/resize/run-resize-locally @@ -0,0 +1,53 @@ +#!/usr/bin/perl +# Copyright (C) 2009-2011 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-resize-locally [usual virt-resize args ...] + +use strict; +use warnings; + +use File::Basename qw(dirname); +use File::Spec; +use Cwd qw(abs_path); + +my $path = $0; +my $tool = "resize"; + +# 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/$tool/virt-$tool", @ARGV), "\n"); +exec("$path/$tool/virt-$tool", @ARGV); diff --git a/resize/test-virt-resize.sh b/resize/test-virt-resize.sh new file mode 100755 index 0000000..2d5cce9 --- /dev/null +++ b/resize/test-virt-resize.sh @@ -0,0 +1,47 @@ +#!/bin/bash - +# libguestfs virt-resize 2.0 test script +# Copyright (C) 2010-2011 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. + +export LANG=C +set -e + +# Test expanding. +# +# This exercises a number of interesting codepaths including resizing +# LV content, handling GPT, and using qcow2 as a target. + +../fish/guestfish -N bootrootlv:/dev/VG/LV:ext2:ext4:400M:32M:gpt </dev/null + +qemu-img create -f qcow2 test2.img 500M +./virt-resize -d --expand /dev/sda2 --lv-expand /dev/VG/LV test1.img test2.img + +# Test shrinking in a semi-realistic scenario. Although the disk +# image created above contains no data, we will nevertheless use +# similar operations to ones that might be used by a real admin. + +../fish/guestfish -a test1.img <<EOF +run +resize2fs-size /dev/VG/LV 190M +lvresize /dev/VG/LV 190 +pvresize-size /dev/sda2 200M +fsck ext4 /dev/VG/LV +EOF + +rm -f test2.img; truncate -s 300M test2.img +./virt-resize -d --shrink /dev/sda2 test1.img test2.img + +rm -f test1.img test2.img diff --git a/resize/utils.ml b/resize/utils.ml new file mode 100644 index 0000000..875f790 --- /dev/null +++ b/resize/utils.ml @@ -0,0 +1,154 @@ +(* virt-resize + * Copyright (C) 2010-2011 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + *) + +open Printf + +module G = Guestfs + +let ( +^ ) = Int64.add +let ( -^ ) = Int64.sub +let ( *^ ) = Int64.mul +let ( /^ ) = Int64.div +let ( &^ ) = Int64.logand +let ( ~^ ) = Int64.lognot + +let output_spaces chan n = for i = 0 to n-1 do output_char chan ' ' done + +let wrap ?(chan = stdout) ?(hanging = 0) str + let rec _wrap col str + let n = String.length str in + let i = try String.index str ' ' with Not_found -> n in + let col + if col+i >= 72 then ( + output_char chan '\n'; + output_spaces chan hanging; + i+hanging+1 + ) else col+i+1 in + output_string chan (String.sub str 0 i); + if i < n then ( + output_char chan ' '; + _wrap col (String.sub str (i+1) (n-(i+1))) + ) + in + _wrap 0 str + +let error fs + let display str + wrap ~chan:stderr ("virt-resize: error: " ^ str); + prerr_newline (); + prerr_newline (); + wrap ~chan:stderr + "If reporting bugs, run virt-resize with the '-d' option and include the complete output."; + prerr_newline (); + exit 1 + in + ksprintf display fs + +(* The reverse of device name translation, see + * BLOCK DEVICE NAMING in guestfs(3). + *) +let canonicalize dev + if String.length dev >= 8 && + dev.[0] = '/' && dev.[1] = 'd' && dev.[2] = 'e' && dev.[3] = 'v' && + dev.[4] = '/' && (dev.[5] = 'h' || dev.[5] = 'v') && dev.[6] = 'd' then ( + let dev = String.copy dev in + dev.[5] <- 's'; + dev + ) + else + dev + +let feature_available (g : Guestfs.guestfs) name + try g#available [|name|]; true + with G.Error _ -> false + +(* Parse the size field from --resize and --resize-force options. *) +let parse_size + let const_re = Pcre.regexp "^([.\\d]+)([bKMG])$" + and plus_const_re = Pcre.regexp "^\\+([.\\d]+)([bKMG])$" + and minus_const_re = Pcre.regexp "^-([.\\d]+)([bKMG])$" + and percent_re = Pcre.regexp "^([.\\d]+)%$" + and plus_percent_re = Pcre.regexp "^\\+([.\\d]+)%$" + and minus_percent_re = Pcre.regexp "^-([.\\d]+)%$" + in + fun oldsize field -> + let subs = ref None in + let matches rex + try subs := Some (Pcre.exec ~rex field); true + with Not_found -> false + in + let sub i + match !subs with None -> assert false + | Some subs -> Pcre.get_substring subs i + in + let size_scaled f = function + | "b" -> Int64.of_float f + | "K" -> Int64.of_float (f *. 1024.) + | "M" -> Int64.of_float (f *. 1024. *. 1024.) + | "G" -> Int64.of_float (f *. 1024. *. 1024. *. 1024.) + | _ -> assert false + in + + if matches const_re then ( + size_scaled (float_of_string (sub 1)) (sub 2) + ) + else if matches plus_const_re then ( + let incr = size_scaled (float_of_string (sub 1)) (sub 2) in + oldsize +^ incr + ) + else if matches minus_const_re then ( + let incr = size_scaled (float_of_string (sub 1)) (sub 2) in + oldsize -^ incr + ) + else if matches percent_re then ( + let percent = Int64.of_float (10. *. float_of_string (sub 1)) in + oldsize *^ percent /^ 1000L + ) + else if matches plus_percent_re then ( + let percent = Int64.of_float (10. *. float_of_string (sub 1)) in + oldsize +^ oldsize *^ percent /^ 1000L + ) + else if matches minus_percent_re then ( + let percent = Int64.of_float (10. *. float_of_string (sub 1)) in + oldsize -^ oldsize *^ percent /^ 1000L + ) + else + error "virt-resize: %s: cannot parse size field" field + +let human_size i + let sign, i = if i < 0L then "-", Int64.neg i else "", i in + + if i < 1024L then + sprintf "%s%Ld" sign i + else ( + let f = Int64.to_float i /. 1024. in + let i = i /^ 1024L in + if i < 1024L then + sprintf "%s%.1fK" sign f + else ( + let f = Int64.to_float i /. 1024. in + let i = i /^ 1024L in + if i < 1024L then + sprintf "%s%.1fM" sign f + else ( + let f = Int64.to_float i /. 1024. in + (*let i = i /^ 1024L in*) + sprintf "%s%.1fG" sign f + ) + ) + ) diff --git a/resize/virt-resize.pod b/resize/virt-resize.pod new file mode 100644 index 0000000..5be09cf --- /dev/null +++ b/resize/virt-resize.pod @@ -0,0 +1,557 @@ +=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-filesystems(1)> and L<virt-df(1)>, we recommend you go and read +those manual pages first. + +=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-filesystems --long -h --all -a 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 + +=over 4 + +=item 1. Shut down the virtual machine + +=item 2. Locate input disk image + +Locate the input disk image (ie. the file or device on the host +containing the guest's disk). 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 3. Look at current sizing + +Use L<virt-filesystems(1)> to display the current partitions and +sizes: + + # virt-filesystems --long --parts --blkdevs -h -a /dev/vg/lv_guest + Name Type Size Parent + /dev/sda1 partition 101M /dev/sda + /dev/sda2 partition 7.9G /dev/sda + /dev/sda device 8.0G - + +(This example is a virtual machine with an 8 GB disk which we would +like to expand up to 10 GB). + +=item 4. Create output disk + +Virt-resize cannot do in-place disk modifications. You have to have +space to store the resized output disk. + +To store the resized disk image in a file, create a file of a suitable +size: + + # rm -f outdisk + # truncate -s 10G outdisk + +Or 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 5. Resize + +virt-resize takes two mandatory parameters, the input disk (eg. device +or file) and the output disk. The output disk is the one created in +the previous step. + + # 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. + +More realistically you'd want to expand existing partitions in the +disk image by passing 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). + +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 + +If the expanded partition in the image contains a filesystem or LVM +PV, then if virt-resize knows how, it will resize the contents, the +equivalent of calling a command such as L<pvresize(8)>, +L<resize2fs(8)> or L<ntfsresize(8)>. However virt-resize does not +know how to resize some filesystems, so you would have to online +resize them after booting the guest. + +Other options are covered below. + +=item 6. 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. See also the L</NOTES> section below +for additional information. + +=item 7. Resize LVs etc inside the guest + +(This can also be done offline using L<guestfish(1)>) + +Once the guest has booted you should see the new space available, at +least for filesystems that virt-resize knows how to resize, and for +PVs. The user may need to resize LVs inside PVs, and also resize +filesystem types that virt-resize does not know how to expand. + +=back + +=head2 SHRINKING A VIRTUAL MACHINE DISK + +Shrinking is somewhat more complex than expanding, and only an +overview is given here. + +Firstly virt-resize will not attempt to shrink any partition content +(PVs, filesystems). The user has to shrink content before passing the +disk image to virt-resize, and virt-resize will check that the content +has been shrunk properly. + +(Shrinking can also be done offline using L<guestfish(1)>) + +After shrinking PVs and filesystems, shut down the guest, and proceed +with steps 3 and 4 above to allocate a new disk image. + +Then run virt-resize with any of the C<--shrink> and/or C<--resize> +options. + +=head2 IGNORING OR DELETING PARTITIONS + +virt-resize also gives a convenient way to ignore or delete partitions +when copying from the input disk to the output disk. Ignoring a +partition speeds up the copy where you don't care about the existing +contents of a partition. Deleting a partition removes it completely, +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 + +=item B<--help> + +Display help. + +=item B<-V> + +=item B<--version> + +Display version number and exit. + +=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 to mean bytes, Kilobytes, Megabytes, or Gigabytes; +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. Virt-resize will expand +the direct content of the partition if it knows how (see C<--expand> +below). + +You can only I<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. + +=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. + +=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). + +If virt-resize knows how, it will expand the direct content of the +partition. For example, if the partition is an LVM PV, it will expand +the PV to fit (like calling L<pvresize(8)>). Virt-resize leaves any +other content it doesn't know about alone. + +Currently virt-resize can resize: + +=over 4 + +=item * + +ext2, ext3 and ext4 filesystems when they are contained +directly inside a partition. + +=item * + +NTFS filesystems contained directly in a partition, if libguestfs was +compiled with support for NTFS. + +The filesystem must have been shut down consistently last time it was +used. Additionally, L<ntfsresize(8)> marks the resized filesystem as +requiring a consistency check, so at the first boot after resizing +Windows will check the disk. + +=item * + +LVM PVs (physical volumes). virt-resize does not usually resize +anything inside the PV, but see the C<--LV-expand> option. The user +could also resize LVs as desired after boot. + +=back + +Note that you cannot use C<--expand> and C<--shrink> together. + +=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. + +=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. + +=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. + +=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-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. + +=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. + +=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. + +=item B<--no-expand-content> + +By default, virt-resize will try to expand the direct contents +of partitions, if it knows how (see C<--expand> option above). + +If you give the C<--no-expand-content> option then virt-resize +will not attempt this. + +=item B<-d> + +=item B<--debug> + +Enable debugging messages. + +=item B<-n> + +=item B<--dryrun> + +Print a summary of what would be done, but don't do anything. + +=item B<-q> + +=item B<--quiet> + +Don't print the summary. + +=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>. + +=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 this option I<does not create> the output format. This +option just tells libguestfs what it is so it doesn't try to guess it. +You still need to create the output disk with the right format. See +L</QCOW2 AND NON-SPARSE RAW FORMATS>. + +=back + +=head1 NOTES + +=head2 "Partition 1 does not end on cylinder boundary." + +Virt-resize aligns partitions to multiples of 64 sectors. Usually +this means the partitions will not be aligned to the ancient CHS +geometry. However CHS geometry is meaningless for disks manufactured +since the early 1990s, and doubly so for virtual hard drives. +Alignment of partitions to cylinders is not required by any modern +operating system. + +=head2 RESIZING WINDOWS VIRTUAL MACHINES + +In Windows Vista and later versions, Microsoft switched to using a +separate boot partition. In these VMs, typically C</dev/sda1> is the +boot partition and C</dev/sda2> is the main (C:) drive. We have not +had any luck resizing the boot partition. Doing so seems to break the +guest completely. However expanding the second partition (ie. C: +drive) should work. + +Windows may initiate a lengthy "chkdsk" on first boot after a resize, +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 +won't mention any here. + +L<parted(8)> and its graphical shell gparted can do some types of +resizing operations on disk images. They can resize and move +partitions, but I don't think they can do anything with the contents, +and they certainly don't understand LVM. + +L<guestfish(1)> can do everything that virt-resize can do and a lot +more, but at a much lower level. You will probably end up +hand-calculating sector offsets, which is something that virt-resize +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-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<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/>. + +=head1 AUTHOR + +Richard W.M. Jones L<http://people.redhat.com/~rjones/> + +=head1 COPYRIGHT + +Copyright (C) 2010-2011 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/Makefile.am b/tools/Makefile.am index 1803049..806f244 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -1,5 +1,5 @@ # libguestfs virt-* tools -# Copyright (C) 2009-2010 Red Hat Inc. +# Copyright (C) 2009-2011 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 @@ -22,7 +22,6 @@ tools = \ list-filesystems \ list-partitions \ make-fs \ - resize \ tar \ win-reg @@ -68,7 +67,6 @@ TESTS_ENVIRONMENT = \ TESTS = test-virt-list-filesystems.sh \ test-virt-make-fs.sh \ - test-virt-resize.sh \ test-virt-tar.sh endif diff --git a/tools/test-virt-resize.sh b/tools/test-virt-resize.sh deleted file mode 100755 index 2c343cb..0000000 --- a/tools/test-virt-resize.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - - -# virt-resize does not work on 32 bit because of limitations in Perl -# so short-circuit this test on a 32 bit host. -perl -e 'exit 1 if ~1 == 4294967294' || { - echo "$0: Skipping this test on 32 bit." - exit 0 -} - -export LANG=C -set -e - -# Test expanding. -# -# This exercises a number of interesting codepaths including resizing -# LV content, handling GPT, and using qcow2 as a target. - -../fish/guestfish -N bootrootlv:/dev/VG/LV:ext2:ext4:400M:32M:gpt </dev/null - -qemu-img create -f qcow2 test2.img 500M -./virt-resize -d --expand /dev/sda2 --lv-expand /dev/VG/LV test1.img test2.img - -# Test shrinking in a semi-realistic scenario. Although the disk -# image created above contains no data, we will nevertheless use -# similar operations to ones that might be used by a real admin. - -../fish/guestfish -a test1.img <<EOF -run -resize2fs-size /dev/VG/LV 190M -lvresize /dev/VG/LV 190 -pvresize-size /dev/sda2 200M -fsck ext4 /dev/VG/LV -EOF - -rm -f test2.img; truncate -s 300M test2.img -./virt-resize -d --shrink /dev/sda2 test1.img test2.img - -rm -f test1.img test2.img diff --git a/tools/virt-resize b/tools/virt-resize deleted file mode 100755 index 5d0673a..0000000 --- a/tools/virt-resize +++ /dev/null @@ -1,1530 +0,0 @@ -#!/usr/bin/perl -w -# virt-resize -# Copyright (C) 2010-2011 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 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; - -$| = 1; - -=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-filesystems(1)> and L<virt-df(1)>, we recommend you go and read -those manual pages first. - -=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-filesystems --long -h --all -a 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 - -=over 4 - -=item 1. Shut down the virtual machine - -=item 2. Locate input disk image - -Locate the input disk image (ie. the file or device on the host -containing the guest's disk). 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 3. Look at current sizing - -Use L<virt-filesystems(1)> to display the current partitions and -sizes: - - # virt-filesystems --long --parts --blkdevs -h -a /dev/vg/lv_guest - Name Type Size Parent - /dev/sda1 partition 101M /dev/sda - /dev/sda2 partition 7.9G /dev/sda - /dev/sda device 8.0G - - -(This example is a virtual machine with an 8 GB disk which we would -like to expand up to 10 GB). - -=item 4. Create output disk - -Virt-resize cannot do in-place disk modifications. You have to have -space to store the resized output disk. - -To store the resized disk image in a file, create a file of a suitable -size: - - # rm -f outdisk - # truncate -s 10G outdisk - -Or 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 5. Resize - -virt-resize takes two mandatory parameters, the input disk (eg. device -or file) and the output disk. The output disk is the one created in -the previous step. - - # 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. - -More realistically you'd want to expand existing partitions in the -disk image by passing 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). - -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 - -If the expanded partition in the image contains a filesystem or LVM -PV, then if virt-resize knows how, it will resize the contents, the -equivalent of calling a command such as L<pvresize(8)>, -L<resize2fs(8)> or L<ntfsresize(8)>. However virt-resize does not -know how to resize some filesystems, so you would have to online -resize them after booting the guest. - -Other options are covered below. - -=item 6. 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. See also the L</NOTES> section below -for additional information. - -=item 7. Resize LVs etc inside the guest - -(This can also be done offline using L<guestfish(1)>) - -Once the guest has booted you should see the new space available, at -least for filesystems that virt-resize knows how to resize, and for -PVs. The user may need to resize LVs inside PVs, and also resize -filesystem types that virt-resize does not know how to expand. - -=back - -=head2 SHRINKING A VIRTUAL MACHINE DISK - -Shrinking is somewhat more complex than expanding, and only an -overview is given here. - -Firstly virt-resize will not attempt to shrink any partition content -(PVs, filesystems). The user has to shrink content before passing the -disk image to virt-resize, and virt-resize will check that the content -has been shrunk properly. - -(Shrinking can also be done offline using L<guestfish(1)>) - -After shrinking PVs and filesystems, shut down the guest, and proceed -with steps 3 and 4 above to allocate a new disk image. - -Then run virt-resize with any of the C<--shrink> and/or C<--resize> -options. - -=head2 IGNORING OR DELETING PARTITIONS - -virt-resize also gives a convenient way to ignore or delete partitions -when copying from the input disk to the output disk. Ignoring a -partition speeds up the copy where you don't care about the existing -contents of a partition. Deleting a partition removes it completely, -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 $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. Virt-resize will expand -the direct content of the partition if it knows how (see C<--expand> -below). - -You can only I<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). - -If virt-resize knows how, it will expand the direct content of the -partition. For example, if the partition is an LVM PV, it will expand -the PV to fit (like calling L<pvresize(8)>). Virt-resize leaves any -other content it doesn't know about alone. - -Currently virt-resize can resize: - -=over 4 - -=item * - -ext2, ext3 and ext4 filesystems when they are contained -directly inside a partition. - -=item * - -NTFS filesystems contained directly in a partition, if libguestfs was -compiled with support for NTFS. - -The filesystem must have been shut down consistently last time it was -used. Additionally, L<ntfsresize(8)> marks the resized filesystem as -requiring a consistency check, so at the first boot after resizing -Windows will check the disk. - -=item * - -LVM PVs (physical volumes). virt-resize does not usually resize -anything inside the PV, but see the C<--LV-expand> option. The user -could also resize LVs as desired after boot. - -=back - -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 @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-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> - -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 $expand_content = 1; - -=item B<--no-expand-content> - -By default, virt-resize will try to expand the direct contents -of partitions, if it knows how (see C<--expand> option above). - -If you give the C<--no-expand-content> option then virt-resize -will not attempt this. - -=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. - -=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 - -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, - "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) { - 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; - -# 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_event_callback (\&progress_callback, $Sys::Guestfs::EVENT_PROGRESS) - 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). -# -# However make this large enough to be sure that we have copied over -# the boot loader. We could also do this by looking for the sector -# offset of the first partition. -# -# It doesn't matter if we copy too much. -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 < $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 < $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; - - # 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 - - 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 $to_be_expanded = 0; - -# 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; - $h{bootable} = $g->part_get_bootable ("/dev/sda", $h{part_num}); - eval { $h{mbr_id} = $g->part_get_mbr_id ("/dev/sda", $h{part_num}); }; - $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; - - # Is it partition content that we know how to expand? - $partitions{$part}->{can_expand_content} = 0; - if ($expand_content) { - if ($type eq "LVM2_member") { - $partitions{$part}->{can_expand_content} = 1; - $partitions{$part}->{expand_content_method} = "pvresize"; - } elsif ($type =~ /^ext[234]$/) { - $partitions{$part}->{can_expand_content} = 1; - $partitions{$part}->{expand_content_method} = "resize2fs"; - } elsif ($type eq "ntfs" && feature_available ($g, "ntfsprogs")) { - $partitions{$part}->{can_expand_content} = 1; - $partitions{$part}->{expand_content_method} = "ntfsresize"; - } - } -} - -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"); - } - } -} - -# 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; - 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; - - if ($partitions{$part}->{can_expand_content} && $bigger) { - $partitions{$part}->{will_expand_content} = 1; - $to_be_expanded++; - } -} - -# 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 + # GPT start and end - (64 * (@partitions + 1)) # Maximum alignment - ) + - ($max_bootloader - 64 * 512); # boot loader - - 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\n", p => $part); - } elsif ($partitions{$part}->{delete}) { - print __x("{p}: partition will be deleted\n", p => $part); - } elsif ($partitions{$part}->{newsize}) { - print __x("{p}: partition will be resized from {oldsize} to {newsize}\n", - p => $part, - oldsize => human_size ($partitions{$part}->{part_size}), - newsize => human_size ($partitions{$part}->{newsize})); - if ($partitions{$part}->{will_expand_content}) { - print __x("{p}: content will be expanded using the '{meth}' method\n", - p => $part, - meth => $partitions{$part}->{expand_content_method}); - } - } else { - print __x("{p}: partition will be left alone\n", p => $part); - } - } - - 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, - 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; -repartition (); - -sub repartition -{ - local $_; - - # 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"}); - my $start = $partitions{"/dev/sda1"}->{part_start} / $sectsize; - - # Align to 64. - $start = ($start + 63) & ~63; - - print "starting to partition from $start\n" if $debug; - - # 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, $part_num) = add_partition ($start, $size); - $partitions{$part}->{target} = $target; - - if ($partitions{$part}->{bootable}) { - $g->part_set_bootable ("/dev/sdb", $part_num, 1); - } - - if ($partitions{$part}->{mbr_id}) { - $g->part_set_mbr_id ("/dev/sdb", $part_num, - $partitions{$part}->{mbr_id}); - } - - # 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, $part_num); - - if ($nextpart <= 3 || $parttype ne "msdos") { - $target = "/dev/sdb$nextpart"; - $end = $start + $size - 1; - $g->part_add ("/dev/sdb", "primary", $start, $end); - $part_num = $nextpart++; - } else { - if ($nextpart == 4) { - $g->part_add ("/dev/sdb", "extended", $start, -1); - $part_num = $nextpart++; - $start += 64; - } - $target = "/dev/sdb$nextpart"; - $end = $start + $size - 1; - $g->part_add ("/dev/sdb", "logical", $start, $end); - $part_num = $nextpart++; - } - - return ($target, $end, $part_num); -} - -# 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) { - print __x("Copying {p} ...\n", p => $part); - } - - $g->copy_size ($part, $target, - $newsize < $oldsize ? $newsize : $oldsize); - } - } - } -} - -# After copying the data over we must shut down and restart the -# appliance in order to expand the content. The reason for this may -# not be obvious, but it's because otherwise we'll have duplicate VGs -# (the old VG(s) and the new VG(s)) which breaks LVM. -# -# The restart is only required if we're going to expand something. - -if ($to_be_expanded > 0) { - restart_appliance (); - expand_partitions (); - expand_lvs (); - expand_lvs_content (); -} - -sub restart_appliance -{ - # Sync disk and exit. - $g->umount_all (); - $g->sync (); - undef $g; - - $g = Sys::Guestfs->new (); - $g->set_trace (1) if $debug; - 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, - # so change them. - foreach my $part (@partitions) - { - my $target = $partitions{$part}->{target}; - if ($target) { - if ($target =~ m{/dev/(.)db(.*)}) { - $partitions{$part}->{target} = "/dev/$1da$2"; - } else { - die "internal error: unexpected partition target: $target"; - } - } - } -} - -sub expand_partitions -{ - foreach my $part (@partitions) - { - unless ($partitions{$part}->{ignore}) { - my $target = $partitions{$part}->{target}; - if ($target) { - # Expand if requested. - if ($partitions{$part}->{will_expand_content}) { - if (!$quiet && !$debug) { - print __x("Expanding {p} using the '{meth}' method\n", - p => $part, - meth => $partitions{$part}->{expand_content_method}); - } - expand_target_partition ($part) - } - } - } - } -} - -sub expand_target_partition -{ - local $_; - my $part = shift; - - # Assertions. - die unless $part; - die unless $partitions{$part}->{can_expand_content}; - die unless $partitions{$part}->{will_expand_content}; - die unless $partitions{$part}->{expand_content_method}; - die unless $partitions{$part}->{target}; - die unless $expand_content; - - my $target = $partitions{$part}->{target}; - my $method = $partitions{$part}->{expand_content_method}; - if ($method eq "pvresize") { - $g->pvresize ($target); - } - elsif ($method eq "resize2fs") { - $g->e2fsck_f ($target); - $g->resize2fs ($target); - } - elsif ($method eq "ntfsresize") { - $g->ntfsresize ($target); - } - else { - die "internal error: unknown method: $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 (); -undef $g; - -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); - } -} - -# 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"; - } - $_; -} - -# 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 $event = shift; - my $event_handle = shift; - my $buf = shift; - my $array = shift; - - my $proc_nr = $array->[0]; - my $serial = $array->[1]; - my $position = $array->[2]; - my $total = $array->[3]; - - 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." - -Virt-resize aligns partitions to multiples of 64 sectors. Usually -this means the partitions will not be aligned to the ancient CHS -geometry. However CHS geometry is meaningless for disks manufactured -since the early 1990s, and doubly so for virtual hard drives. -Alignment of partitions to cylinders is not required by any modern -operating system. - -=head2 RESIZING WINDOWS VIRTUAL MACHINES - -In Windows Vista and later versions, Microsoft switched to using a -separate boot partition. In these VMs, typically C</dev/sda1> is the -boot partition and C</dev/sda2> is the main (C:) drive. We have not -had any luck resizing the boot partition. Doing so seems to break the -guest completely. However expanding the second partition (ie. C: -drive) should work. - -Windows may initiate a lengthy "chkdsk" on first boot after a resize, -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 -won't mention any here. - -L<parted(8)> and its graphical shell gparted can do some types of -resizing operations on disk images. They can resize and move -partitions, but I don't think they can do anything with the contents, -and they certainly don't understand LVM. - -L<guestfish(1)> can do everything that virt-resize can do and a lot -more, but at a much lower level. You will probably end up -hand-calculating sector offsets, which is something that virt-resize -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-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<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/>. - -=head1 AUTHOR - -Richard W.M. Jones L<http://people.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. -- 1.7.4.1
Possibly Parallel Threads
- [PATCH 0/9] Enhance virt-resize so it can really expand Linux and Windows guests
- [PATCH FOR DISCUSSION ONLY] virt-resize tool for resizing virtual machines.
- [PATCH] virt-resize: Document qcow2 output format (RHBZ#642826).
- Re: virt-resize error on RHEL 7 (was: Re: Regarding libguestfs)
- Re: error in virt-resize