diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Makefile.am | 7 | ||||
-rw-r--r-- | README | 2 | ||||
-rw-r--r-- | configure.ac | 13 | ||||
-rw-r--r-- | po/POTFILES.in | 1 | ||||
-rw-r--r-- | resize/.depend | 6 | ||||
-rw-r--r-- | resize/Makefile.am | 101 | ||||
-rw-r--r-- | resize/progress.ml | 49 | ||||
-rw-r--r-- | resize/resize.ml | 972 | ||||
-rwxr-xr-x | resize/run-resize-locally | 53 | ||||
-rwxr-xr-x | resize/test-virt-resize.sh (renamed from tools/test-virt-resize.sh) | 23 | ||||
-rw-r--r-- | resize/utils.ml | 154 | ||||
-rw-r--r-- | resize/virt-resize.pod | 556 | ||||
-rw-r--r-- | tools/Makefile.am | 4 | ||||
-rwxr-xr-x | tools/virt-resize | 1530 |
15 files changed, 1930 insertions, 1543 deletions
@@ -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 96c4d354..3d629e9c 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 @@ -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 268119a0..05db6e0c 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$OCAMLC" != "xno" && test "x$OCAMLFIND" != "xno" && 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 5f5919ad..e0b2b295 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 00000000..96f38ccd --- /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 00000000..8ef16fff --- /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 00000000..a4eff0fe --- /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 00000000..9a4c86bb --- /dev/null +++ b/resize/resize.ml @@ -0,0 +1,972 @@ +(* 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 + +(* Default to true, since NTFS support is usually available. *) +let ntfs_available = ref true + +(* 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"|]; + + (* Update features available in the daemon. *) + ntfs_available := feature_available g [|"ntfsprogs"; "ntfs3g"|]; + + 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 !ntfs_available -> 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 !ntfs_available -> 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; + p_operation = (OpCopy | OpResize _) } 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. *) + +let () = + if to_be_expanded then ( + (* 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 + in + + (* 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 00000000..0d840dd8 --- /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/tools/test-virt-resize.sh b/resize/test-virt-resize.sh index 2c343cb4..2d5cce90 100755 --- a/tools/test-virt-resize.sh +++ b/resize/test-virt-resize.sh @@ -1,11 +1,20 @@ #!/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 -} +# 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 diff --git a/resize/utils.ml b/resize/utils.ml new file mode 100644 index 00000000..38519757 --- /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) names = + try g#available names; 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 00000000..1a32f25e --- /dev/null +++ b/resize/virt-resize.pod @@ -0,0 +1,556 @@ +=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<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 18030496..806f2444 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/virt-resize b/tools/virt-resize deleted file mode 100755 index 5d0673a3..00000000 --- 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. |