diff options
Diffstat (limited to 'resize')
-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 | 47 | ||||
-rw-r--r-- | resize/utils.ml | 154 | ||||
-rw-r--r-- | resize/virt-resize.pod | 556 |
8 files changed, 1938 insertions, 0 deletions
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/resize/test-virt-resize.sh b/resize/test-virt-resize.sh new file mode 100755 index 00000000..2d5cce90 --- /dev/null +++ b/resize/test-virt-resize.sh @@ -0,0 +1,47 @@ +#!/bin/bash - +# libguestfs virt-resize 2.0 test script +# Copyright (C) 2010-2011 Red Hat Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +export LANG=C +set -e + +# Test expanding. +# +# This exercises a number of interesting codepaths including resizing +# LV content, handling GPT, and using qcow2 as a target. + +../fish/guestfish -N bootrootlv:/dev/VG/LV:ext2:ext4:400M:32M:gpt </dev/null + +qemu-img create -f qcow2 test2.img 500M +./virt-resize -d --expand /dev/sda2 --lv-expand /dev/VG/LV test1.img test2.img + +# Test shrinking in a semi-realistic scenario. Although the disk +# image created above contains no data, we will nevertheless use +# similar operations to ones that might be used by a real admin. + +../fish/guestfish -a test1.img <<EOF +run +resize2fs-size /dev/VG/LV 190M +lvresize /dev/VG/LV 190 +pvresize-size /dev/sda2 200M +fsck ext4 /dev/VG/LV +EOF + +rm -f test2.img; truncate -s 300M test2.img +./virt-resize -d --shrink /dev/sda2 test1.img test2.img + +rm -f test1.img test2.img diff --git a/resize/utils.ml b/resize/utils.ml new file mode 100644 index 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. |