mirror of
https://github.com/libguestfs/libguestfs.git
synced 2026-03-22 07:03:38 +00:00
However some existing functions had names which shadowed existing functions in the List module, so I had to rename them: assoc -> List.assoc_lbl append -> List.push_back_list prepend -> List.push_front_list This is an extension of the previous commit.
1425 lines
52 KiB
OCaml
1425 lines
52 KiB
OCaml
(* virt-resize
|
||
* Copyright (C) 2010-2017 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 Std_utils
|
||
open Tools_utils
|
||
open Common_gettext.Gettext
|
||
open Unix_utils
|
||
open Getopt.OptionName
|
||
|
||
module G = Guestfs
|
||
|
||
(* Minimum surplus before we create an extra partition. *)
|
||
let min_extra_partition = 10L *^ 1024L *^ 1024L
|
||
|
||
(* Command line argument parsing. *)
|
||
type align_first_t = [ `Never | `Always | `Auto ]
|
||
|
||
(* Source partition type. *)
|
||
type parttype = MBR | GPT
|
||
|
||
(* Data structure describing the source disk's partition layout.
|
||
*
|
||
* NOTE: For MBR, only primary/extended partitions are tracked here.
|
||
* Logical partitions are contained within an extended partition, and
|
||
* we don't track them (they are just copied within the extended
|
||
* partition). For the same reason we cannot resize logical partitions.
|
||
*)
|
||
type partition = {
|
||
p_name : string; (* Device name, like /dev/sda1. *)
|
||
p_part : G.partition; (* SOURCE partition data from libguestfs. *)
|
||
p_bootable : bool; (* Is it bootable? *)
|
||
p_id : partition_id; (* Partition (MBR/GPT) ID. *)
|
||
p_type : partition_content; (* Content type and content size. *)
|
||
p_label : string option; (* Label/name. *)
|
||
p_guid : string option; (* Partition GUID (GPT only). *)
|
||
|
||
(* What we're going to do: *)
|
||
mutable p_operation : partition_operation;
|
||
p_target_partnum : int; (* TARGET partition number. *)
|
||
p_target_start : int64; (* TARGET partition start (sector num). *)
|
||
p_target_end : int64; (* TARGET partition end (sector num). *)
|
||
}
|
||
and partition_content =
|
||
| ContentUnknown (* undetermined *)
|
||
| ContentPV of int64 (* physical volume (size of PV) *)
|
||
| ContentFS of string * int64 (* mountable filesystem (FS type, FS size) *)
|
||
| ContentExtendedPartition (* MBR extended partition *)
|
||
| ContentSwap (* Swap partition *)
|
||
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 *)
|
||
and partition_id =
|
||
| No_ID (* No identifier. *)
|
||
| MBR_ID of int (* MBR ID. *)
|
||
| GPT_Type of string (* GPT UUID. *)
|
||
|
||
let rec debug_partition ?(sectsize=512L) p =
|
||
printf "%s:\n" p.p_name;
|
||
printf "\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;
|
||
printf "\tpartition sector data: %Ld-%Ld (%Ld sectors)\n"
|
||
(p.p_part.G.part_start /^ sectsize) (p.p_part.G.part_end /^ sectsize)
|
||
((p.p_part.G.part_end +^ 1L -^ p.p_part.G.part_start) /^ sectsize);
|
||
printf "\ttarget partition sector data: %Ld-%Ld (%Ld sectors)\n"
|
||
p.p_target_start p.p_target_end (p.p_target_end +^ 1L -^ p.p_target_start);
|
||
printf "\tbootable: %b\n" p.p_bootable;
|
||
printf "\tpartition ID: %s\n"
|
||
(match p.p_id with
|
||
| No_ID -> "(none)"
|
||
| MBR_ID i -> sprintf "0x%x" i
|
||
| GPT_Type i -> i
|
||
);
|
||
printf "\tcontent: %s\n" (string_of_partition_content p.p_type);
|
||
printf "\tlabel: %s\n"
|
||
(match p.p_label with
|
||
| Some label -> label
|
||
| None -> "(none)"
|
||
);
|
||
printf "\tGUID: %s\n"
|
||
(match p.p_guid with
|
||
| Some guid -> guid
|
||
| None -> "(none)"
|
||
)
|
||
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
|
||
| ContentExtendedPartition -> "extended partition"
|
||
| ContentSwap -> "swap"
|
||
and string_of_partition_content_no_size = function
|
||
| ContentUnknown -> "unknown data"
|
||
| ContentPV _ -> "LVM PV"
|
||
| ContentFS (fs, _) -> sprintf "filesystem %s" fs
|
||
| ContentExtendedPartition -> "extended partition"
|
||
| ContentSwap -> "swap"
|
||
|
||
(* 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
|
||
}
|
||
(* ContentPV, ContentExtendedPartition cannot occur here *)
|
||
and logvol_content = partition_content
|
||
and logvol_operation =
|
||
| LVOpNone (* nothing *)
|
||
| LVOpExpand (* expand it *)
|
||
|
||
let debug_logvol lv =
|
||
printf "%s:\n" lv.lv_name;
|
||
printf "\tcontent: %s\n" (string_of_partition_content lv.lv_type)
|
||
|
||
type expand_content_method =
|
||
| PVResize | Resize2fs | NTFSResize | BtrfsFilesystemResize | XFSGrowFS
|
||
| Mkswap
|
||
|
||
let string_of_expand_content_method = function
|
||
| PVResize -> s_"pvresize"
|
||
| Resize2fs -> s_"resize2fs"
|
||
| NTFSResize -> s_"ntfsresize"
|
||
| BtrfsFilesystemResize -> s_"btrfs-filesystem-resize"
|
||
| XFSGrowFS -> s_"xfs_growfs"
|
||
| Mkswap -> s_"mkswap"
|
||
|
||
type unknown_filesystems_mode =
|
||
| UnknownFsIgnore
|
||
| UnknownFsWarn
|
||
| UnknownFsError
|
||
|
||
(* Main program. *)
|
||
let main () =
|
||
let infile, outfile, align_first, alignment, copy_boot_loader,
|
||
deletes,
|
||
dryrun, expand, expand_content, extra_partition, format, ignores,
|
||
lv_expands, machine_readable, ntfsresize_force, output_format,
|
||
resizes, resizes_force, shrink, sparse, unknown_fs_mode =
|
||
|
||
let add xs s = List.push_front s xs in
|
||
|
||
let align_first = ref "auto" in
|
||
let alignment = ref 128 in
|
||
let copy_boot_loader = ref true in
|
||
let deletes = ref [] in
|
||
let dryrun = ref false in
|
||
let expand = ref "" in
|
||
let set_expand s =
|
||
if s = "" then error (f_"empty --expand option")
|
||
else if !expand <> "" then error (f_"--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 machine_readable = ref false in
|
||
let ntfsresize_force = ref false in
|
||
let output_format = ref "" in
|
||
let resizes = ref [] in
|
||
let resizes_force = ref [] in
|
||
let shrink = ref "" in
|
||
let set_shrink s =
|
||
if s = "" then error (f_"empty --shrink option")
|
||
else if !shrink <> "" then error (f_"--shrink option given twice")
|
||
else shrink := s
|
||
in
|
||
let sparse = ref true in
|
||
let unknown_fs_mode = ref "warn" in
|
||
|
||
let argspec = [
|
||
[ L"align-first" ], Getopt.Set_string (s_"never|always|auto", align_first), s_"Align first partition (default: auto)";
|
||
[ L"alignment" ], Getopt.Set_int (s_"sectors", alignment), s_"Set partition alignment (default: 128 sectors)";
|
||
[ L"no-copy-boot-loader" ], Getopt.Clear copy_boot_loader, s_"Don’t copy boot loader";
|
||
[ S 'd'; L"debug" ], Getopt.Unit set_verbose, s_"Enable debugging messages";
|
||
[ L"delete" ], Getopt.String (s_"part", add deletes), s_"Delete partition";
|
||
[ L"expand" ], Getopt.String (s_"part", set_expand), s_"Expand partition";
|
||
[ L"no-expand-content" ], Getopt.Clear expand_content, s_"Don’t expand content";
|
||
[ L"no-extra-partition" ], Getopt.Clear extra_partition, s_"Don’t create extra partition";
|
||
[ L"format" ], Getopt.Set_string (s_"format", format), s_"Format of input disk";
|
||
[ L"ignore" ], Getopt.String (s_"part", add ignores), s_"Ignore partition";
|
||
[ L"lv-expand"; L"LV-expand"; L"lvexpand"; L"LVexpand" ], Getopt.String (s_"lv", add lv_expands), s_"Expand logical volume";
|
||
[ L"machine-readable" ], Getopt.Set machine_readable, s_"Make output machine readable";
|
||
[ S 'n'; L"dry-run"; L"dryrun" ], Getopt.Set dryrun, s_"Don’t perform changes";
|
||
[ L"ntfsresize-force" ], Getopt.Set ntfsresize_force, s_"Force ntfsresize";
|
||
[ L"output-format" ], Getopt.Set_string (s_"format", output_format), s_"Format of output disk";
|
||
[ L"resize" ], Getopt.String (s_"part=size", add resizes), s_"Resize partition";
|
||
[ L"resize-force" ], Getopt.String (s_"part=size", add resizes_force), s_"Forcefully resize partition";
|
||
[ L"shrink" ], Getopt.String (s_"part", set_shrink), s_"Shrink partition";
|
||
[ L"no-sparse" ], Getopt.Clear sparse, s_"Turn off sparse copying";
|
||
[ L"unknown-filesystems" ], Getopt.Set_string (s_"ignore|warn|error", unknown_fs_mode),
|
||
s_"Behaviour on expand unknown filesystems (default: warn)";
|
||
] in
|
||
let disks = ref [] in
|
||
let anon_fun s = List.push_front s disks in
|
||
let usage_msg =
|
||
sprintf (f_"\
|
||
%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
|
||
let opthandle = create_standard_options argspec ~anon_fun usage_msg in
|
||
Getopt.parse opthandle;
|
||
|
||
if verbose () then (
|
||
printf "command line:";
|
||
List.iter (printf " %s") (Array.to_list Sys.argv);
|
||
print_newline ()
|
||
);
|
||
|
||
(* Dereference the rest of the args. *)
|
||
let alignment = !alignment in
|
||
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 machine_readable = !machine_readable in
|
||
let ntfsresize_force = !ntfsresize_force in
|
||
let output_format = match !output_format with "" -> None | str -> Some str 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
|
||
let sparse = !sparse in
|
||
let unknown_fs_mode = !unknown_fs_mode in
|
||
|
||
if alignment < 1 then
|
||
error (f_"alignment cannot be < 1");
|
||
let alignment = Int64.of_int alignment in
|
||
|
||
let align_first =
|
||
match !align_first with
|
||
| "never" -> `Never
|
||
| "always" -> `Always
|
||
| "auto" -> `Auto
|
||
| _ ->
|
||
error (f_"unknown --align-first option: use never|always|auto") in
|
||
|
||
let unknown_fs_mode =
|
||
match unknown_fs_mode with
|
||
| "ignore" -> UnknownFsIgnore
|
||
| "warn" -> UnknownFsWarn
|
||
| "error" -> UnknownFsError
|
||
| _ ->
|
||
error (f_"unknown --unknown-filesystems: use ignore|warn|error") in
|
||
|
||
(* No arguments and machine-readable mode? Print out some facts
|
||
* about what this binary supports. We only need to print out new
|
||
* things added since this option, or things which depend on features
|
||
* of the appliance.
|
||
*)
|
||
if !disks = [] && machine_readable then (
|
||
printf "virt-resize\n";
|
||
printf "ntfsresize-force\n";
|
||
printf "32bitok\n";
|
||
printf "128-sector-alignment\n";
|
||
printf "alignment\n";
|
||
printf "align-first\n";
|
||
printf "infile-uri\n";
|
||
let g = open_guestfs () in
|
||
g#add_drive "/dev/null";
|
||
g#launch ();
|
||
if g#feature_available [| "ntfsprogs"; "ntfs3g" |] then
|
||
printf "ntfs\n";
|
||
if g#feature_available [| "btrfs" |] then
|
||
printf "btrfs\n";
|
||
if g#feature_available [| "xfs" |] then
|
||
printf "xfs\n";
|
||
exit 0
|
||
);
|
||
|
||
(* Verify we got exactly 2 disks. *)
|
||
let infile, outfile =
|
||
match List.rev !disks with
|
||
| [infile; outfile] -> infile, outfile
|
||
| _ ->
|
||
error (f_"usage is: %s [--options] indisk outdisk") prog in
|
||
|
||
(* Simple-minded check that the user isn't trying to use the
|
||
* same disk for input and output.
|
||
*)
|
||
if infile = outfile then
|
||
error (f_"you cannot use the same disk image for input and output");
|
||
|
||
(* infile can be a URI. *)
|
||
let infile =
|
||
try (infile, URI.parse_uri infile)
|
||
with URI.Parse_failed ->
|
||
error (f_"error parsing URI ‘%s’. Look for error messages printed above.")
|
||
infile in
|
||
|
||
(* outfile can be a URI. *)
|
||
let outfile =
|
||
try (outfile, URI.parse_uri outfile)
|
||
with URI.Parse_failed ->
|
||
error (f_"error parsing URI ‘%s’. Look for error messages printed above.")
|
||
outfile in
|
||
|
||
infile, outfile, align_first, alignment, copy_boot_loader,
|
||
deletes,
|
||
dryrun, expand, expand_content, extra_partition, format, ignores,
|
||
lv_expands, machine_readable, ntfsresize_force, output_format,
|
||
resizes, resizes_force, shrink, sparse, unknown_fs_mode in
|
||
|
||
(* Default to true, since NTFS/btrfs/XFS support are usually available. *)
|
||
let ntfs_available = ref true in
|
||
let btrfs_available = ref true in
|
||
let xfs_available = ref true in
|
||
|
||
(* Add a drive to an handle using the elements of the URI,
|
||
* and few additional parameters.
|
||
*)
|
||
let add_drive_uri (g : Guestfs.guestfs) ?format ?readonly ?cachemode
|
||
{ URI.path; protocol; server; username; password } =
|
||
g#add_drive ?format ?readonly ?cachemode
|
||
~protocol ?server ?username ?secret:password path
|
||
in
|
||
|
||
(* Add in and out disks to the handle and launch. *)
|
||
let connect_both_disks () =
|
||
let g = open_guestfs () in
|
||
add_drive_uri g ?format ~readonly:true (snd infile);
|
||
(* The output disk is being created, so use cache=unsafe here. *)
|
||
add_drive_uri g ?format:output_format ~readonly:false ~cachemode:"unsafe"
|
||
(snd outfile);
|
||
if not (quiet ()) then Progress.set_up_progress_bar ~machine_readable 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 := g#feature_available [|"ntfsprogs"; "ntfs3g"|];
|
||
btrfs_available := g#feature_available [|"btrfs"|];
|
||
xfs_available := g#feature_available [|"xfs"|];
|
||
|
||
g
|
||
in
|
||
|
||
let g =
|
||
message (f_"Examining %s") (fst infile);
|
||
let g = connect_both_disks () in
|
||
g in
|
||
|
||
(* 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 = Int64.of_int (g#blockdev_getss "/dev/sdb") in
|
||
let insize = g#blockdev_getsize64 "/dev/sda" in
|
||
let outsize = g#blockdev_getsize64 "/dev/sdb" in
|
||
debug "%s size %Ld bytes" (fst infile) insize;
|
||
debug "%s size %Ld bytes" (fst outfile) outsize;
|
||
sectsize, insize, outsize in
|
||
|
||
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 in
|
||
|
||
(* Check the disks are at least as big as the bootloader. *)
|
||
if insize < Int64.of_int max_bootloader then
|
||
error (f_"%s: file is too small to be a disk image (%Ld bytes)")
|
||
(fst infile) insize;
|
||
if outsize < Int64.of_int max_bootloader then
|
||
error (f_"%s: file is too small to be a disk image (%Ld bytes)")
|
||
(fst outfile) outsize;
|
||
|
||
(* Get the source partition type. *)
|
||
let parttype, parttype_string =
|
||
let pt = g#part_get_parttype "/dev/sda" in
|
||
debug "partition table type: %s" pt;
|
||
|
||
match pt with
|
||
| "msdos" -> MBR, "msdos"
|
||
| "gpt" -> GPT, "gpt"
|
||
| _ ->
|
||
error (f_"%s: unknown partition table type\nvirt-resize only supports MBR (DOS) and GPT partition tables.")
|
||
(fst infile) in
|
||
|
||
let disk_guid =
|
||
match parttype with
|
||
| MBR -> None
|
||
| GPT ->
|
||
try Some (g#part_get_disk_guid "/dev/sda")
|
||
with G.Error _ -> None in
|
||
|
||
(* Build a data structure describing the source disk's partition layout. *)
|
||
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 = "swap" then
|
||
ContentSwap
|
||
else if fs = "LVM2_member" then (
|
||
let rec loop = function
|
||
| [] ->
|
||
error (f_"%s: physical volume not returned by pvs_full") dev
|
||
| pv :: _ when g#canonical_device_name 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
|
||
g#umount "/";
|
||
let size = stat.G.bsize *^ stat.G.blocks in
|
||
ContentFS (fs, size)
|
||
)
|
||
with
|
||
G.Error _ -> ContentUnknown
|
||
in
|
||
|
||
let is_extended_partition = function
|
||
| MBR_ID (0x05|0x0f) -> true
|
||
| MBR_ID _ | GPT_Type _ | No_ID -> false
|
||
in
|
||
|
||
let partitions : partition list =
|
||
let parts = Array.to_list (g#part_list "/dev/sda") in
|
||
|
||
if List.length parts = 0 then
|
||
error (f_"the source disk has no partitions");
|
||
|
||
(* Filter out logical partitions. See note above. *)
|
||
let parts =
|
||
List.filter (fun p -> parttype <> MBR || p.G.part_num <= 4_l)
|
||
parts in
|
||
|
||
let partitions =
|
||
List.map (
|
||
fun ({ G.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 id =
|
||
match parttype with
|
||
| GPT ->
|
||
(try GPT_Type (g#part_get_gpt_type "/dev/sda" part_num)
|
||
with G.Error _ -> No_ID)
|
||
| MBR ->
|
||
(try MBR_ID (g#part_get_mbr_id "/dev/sda" part_num)
|
||
with G.Error _ -> No_ID) in
|
||
let typ =
|
||
if is_extended_partition id then ContentExtendedPartition
|
||
else get_partition_content name in
|
||
let label =
|
||
try Some (g#part_get_name "/dev/sda" part_num)
|
||
with G.Error _ -> None in
|
||
let guid =
|
||
match parttype with
|
||
| MBR -> None
|
||
| GPT ->
|
||
try Some (g#part_get_gpt_guid "/dev/sda" part_num)
|
||
with G.Error _ -> None in
|
||
|
||
{ p_name = name; p_part = part;
|
||
p_bootable = bootable; p_id = id; p_type = typ;
|
||
p_label = label; p_guid = guid;
|
||
p_operation = OpCopy; p_target_partnum = 0;
|
||
p_target_start = 0L; p_target_end = 0L }
|
||
) parts in
|
||
|
||
if verbose () 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_part = { G.part_size = size };
|
||
p_type = ContentPV pv_size }
|
||
when size < pv_size ->
|
||
error (f_"%s: partition size %Ld < physical volume size %Ld")
|
||
name size pv_size
|
||
| { p_name = name; p_part = { G.part_size = size };
|
||
p_type = ContentFS (_, fs_size) }
|
||
when size < fs_size ->
|
||
error (f_"%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 } } :: _
|
||
when end_of_prev > part_start ->
|
||
error (f_"%s: this partition overlaps the previous one") name
|
||
| { p_part = { G.part_end } } :: parts -> loop part_end parts
|
||
in
|
||
loop 0L partitions;
|
||
|
||
partitions in
|
||
|
||
(* Build a data structure describing LVs on the source disk. *)
|
||
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 _ | ContentExtendedPartition -> false
|
||
| ContentUnknown | ContentFS _ | ContentSwap -> true
|
||
);
|
||
|
||
{ lv_name = name; lv_type = typ; lv_operation = LVOpNone }
|
||
) lvs in
|
||
|
||
if verbose () then (
|
||
printf "%d logical volumes found\n" (List.length lvs);
|
||
List.iter debug_logvol lvs
|
||
);
|
||
|
||
lvs in
|
||
|
||
(* These functions tell us if we know how to expand the content of
|
||
* a particular partition or LV, and what method to use.
|
||
*)
|
||
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 (("btrfs"), _) when !btrfs_available -> true
|
||
| ContentFS (("xfs"), _) when !xfs_available -> true
|
||
| ContentFS _ -> false
|
||
| ContentExtendedPartition -> false
|
||
| ContentSwap -> true
|
||
else
|
||
fun _ -> false
|
||
|
||
and 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 (("btrfs"), _) when !btrfs_available -> BtrfsFilesystemResize
|
||
| ContentFS (("xfs"), _) when !xfs_available -> XFSGrowFS
|
||
| ContentFS _ -> assert false
|
||
| ContentExtendedPartition -> assert false
|
||
| ContentSwap -> Mkswap
|
||
else
|
||
fun _ -> assert false
|
||
in
|
||
|
||
(* 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 = g#canonical_device_name name in
|
||
|
||
let partition =
|
||
try Hashtbl.find hash name
|
||
with Not_found ->
|
||
error (f_"%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 (fst infile) in
|
||
|
||
if partition.p_operation = OpIgnore then
|
||
error (f_"%s: partition already ignored, you cannot use it in ‘%s’ option")
|
||
name option;
|
||
|
||
if partition.p_operation = OpDelete then
|
||
error (f_"%s: partition already deleted, you cannot use it in ‘%s’ option")
|
||
name option;
|
||
|
||
partition in
|
||
|
||
(* Handle --ignore option. *)
|
||
List.iter (
|
||
fun dev ->
|
||
let p = find_partition ~option:"--ignore" dev in
|
||
p.p_operation <- OpIgnore
|
||
) ignores;
|
||
|
||
(* Handle --delete option. *)
|
||
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_part.G.part_size in
|
||
|
||
(match p.p_operation with
|
||
| OpResize _ ->
|
||
error (f_"%s: this partition has already been marked for resizing")
|
||
name
|
||
| OpIgnore | OpDelete ->
|
||
(* This error should have been caught already by find_partition ... *)
|
||
error (f_"%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 (f_"%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 (f_"%s: This partition 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 (f_"%s: This partition 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 _ -> ()
|
||
| ContentExtendedPartition ->
|
||
error (f_"%s: This extended partition contains logical partitions 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 logical partitions within this partition. (This error came from ‘%s’ option on the command line.)")
|
||
name option
|
||
| ContentSwap -> ()
|
||
);
|
||
|
||
p.p_operation <- OpResize newsize
|
||
)
|
||
in
|
||
|
||
(* Handle --resize and --resize-force options. *)
|
||
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 (f_"%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_part.G.part_size in
|
||
let newsize = parse_resize oldsize sizefield in
|
||
|
||
if newsize <= 0L then
|
||
error (f_"%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. *)
|
||
let overhead =
|
||
let maxl64 = List.fold_left max 0L in
|
||
|
||
let nr_partitions = List.length partitions in
|
||
|
||
let gpt_start_sects = 64L in
|
||
let gpt_end_sects = gpt_start_sects in
|
||
|
||
let first_part_start_sects =
|
||
match partitions with
|
||
| { p_part = { G.part_start = start }} :: _ ->
|
||
start /^ sectsize
|
||
| [] -> 0L in
|
||
|
||
let max_bootloader_sects = Int64.of_int max_bootloader /^ 512L in
|
||
|
||
(* Size of the unpartitioned space before the first partition. *)
|
||
let start_overhead_sects =
|
||
maxl64 [gpt_start_sects; max_bootloader_sects; first_part_start_sects] in
|
||
|
||
(* Maximum space lost because of alignment of partitions. *)
|
||
let alignment_sects = alignment *^ Int64.of_int (nr_partitions + 1) in
|
||
|
||
(* Add up the total max. overhead. *)
|
||
let overhead_sects =
|
||
start_overhead_sects +^ alignment_sects +^ gpt_end_sects in
|
||
sectsize *^ overhead_sects in
|
||
|
||
let required = List.fold_left (
|
||
fun total p ->
|
||
let newsize =
|
||
match p.p_operation with
|
||
| OpCopy | OpIgnore -> p.p_part.G.part_size
|
||
| OpDelete -> 0L
|
||
| OpResize newsize -> newsize in
|
||
total +^ newsize
|
||
) 0L partitions in
|
||
|
||
let surplus = outsize -^ (required +^ overhead) in
|
||
|
||
debug "calculate surplus: outsize=%Ld required=%Ld overhead=%Ld surplus=%Ld"
|
||
outsize required overhead surplus;
|
||
|
||
surplus
|
||
in
|
||
|
||
(* Handle --expand and --shrink options. *)
|
||
if expand <> None && shrink <> None then
|
||
error (f_"you cannot use options --expand and --shrink together");
|
||
|
||
if expand <> None || shrink <> None then (
|
||
let surplus = calculate_surplus () in
|
||
|
||
debug "surplus before --expand or --shrink: %Ld" surplus;
|
||
|
||
(match expand with
|
||
| None -> ()
|
||
| Some dev ->
|
||
if surplus < 0L then
|
||
error (f_"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_part.G.part_size in
|
||
mark_partition_for_resize ~option p (oldsize +^ surplus)
|
||
);
|
||
(match shrink with
|
||
| None -> ()
|
||
| Some dev ->
|
||
if surplus > 0L then
|
||
error (f_"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_part.G.part_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 (f_"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 in
|
||
|
||
(* Mark the --lv-expand LVs. *)
|
||
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 (f_"%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 (fst infile) in
|
||
lv.lv_operation <- LVOpExpand
|
||
) lv_expands;
|
||
|
||
(* In case we need to error out on unknown/unhandled filesystems,
|
||
* iterate on what we need to resize/expand.
|
||
*)
|
||
(match unknown_fs_mode with
|
||
| UnknownFsIgnore -> ()
|
||
| UnknownFsWarn -> ()
|
||
| UnknownFsError ->
|
||
List.iter (
|
||
fun p ->
|
||
match p.p_operation with
|
||
| OpCopy
|
||
| OpIgnore
|
||
| OpDelete -> ()
|
||
| OpResize _ ->
|
||
if not (can_expand_content p.p_type) then (
|
||
(match p.p_type with
|
||
| ContentUnknown
|
||
| ContentPV _
|
||
| ContentExtendedPartition
|
||
| ContentSwap -> ()
|
||
| ContentFS (fs, _) ->
|
||
error (f_"unknown/unavailable method for expanding the %s filesystem on %s")
|
||
fs p.p_name
|
||
);
|
||
)
|
||
) partitions;
|
||
|
||
List.iter (
|
||
fun lv ->
|
||
match lv.lv_operation with
|
||
| LVOpNone -> ()
|
||
| LVOpExpand ->
|
||
if not (can_expand_content lv.lv_type) then (
|
||
(match lv.lv_type with
|
||
| ContentUnknown
|
||
| ContentPV _
|
||
| ContentExtendedPartition
|
||
| ContentSwap -> ()
|
||
| ContentFS (fs, _) ->
|
||
error (f_"unknown/unavailable method for expanding the %s filesystem on %s")
|
||
fs lv.lv_name;
|
||
);
|
||
)
|
||
) lvs;
|
||
);
|
||
|
||
(* Print a summary of what we will do. *)
|
||
flush stderr;
|
||
|
||
if not (quiet ()) then (
|
||
printf "**********\n\n";
|
||
printf "Summary of changes:\n\n";
|
||
|
||
let rec print_summary p =
|
||
let text =
|
||
match p.p_operation with
|
||
| OpCopy ->
|
||
sprintf (f_"%s: This partition will be left alone.") p.p_name
|
||
| OpIgnore ->
|
||
sprintf (f_"%s: This partition will be created, but the contents will be ignored (ie. not copied to the target).") p.p_name
|
||
| OpDelete ->
|
||
sprintf (f_"%s: This partition will be deleted.") p.p_name
|
||
| OpResize newsize ->
|
||
sprintf (f_"%s: This partition will be resized from %s to %s.")
|
||
p.p_name (human_size p.p_part.G.part_size) (human_size newsize) ^
|
||
if can_expand_content p.p_type then (
|
||
sprintf (f_" The %s on %s will be expanded using the ‘%s’ method.")
|
||
(string_of_partition_content_no_size p.p_type)
|
||
p.p_name
|
||
(string_of_expand_content_method
|
||
(expand_content_method p.p_type))
|
||
) else (
|
||
(match p.p_type with
|
||
| ContentUnknown
|
||
| ContentPV _
|
||
| ContentExtendedPartition
|
||
| ContentSwap -> ()
|
||
| ContentFS (fs, _) ->
|
||
warning (f_"unknown/unavailable method for expanding the %s filesystem on %s")
|
||
fs p.p_name;
|
||
);
|
||
""
|
||
) in
|
||
|
||
wrap (text ^ "\n\n") in
|
||
|
||
List.iter print_summary partitions;
|
||
|
||
List.iter (
|
||
fun ({ lv_name = name } as lv) ->
|
||
match lv.lv_operation with
|
||
| LVOpNone -> ()
|
||
| LVOpExpand ->
|
||
let text =
|
||
sprintf (f_"%s: This logical volume will be expanded to maximum size.")
|
||
name ^
|
||
if can_expand_content lv.lv_type then (
|
||
sprintf (f_" 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 (
|
||
(match lv.lv_type with
|
||
| ContentUnknown
|
||
| ContentPV _
|
||
| ContentExtendedPartition
|
||
| ContentSwap -> ()
|
||
| ContentFS (fs, _) ->
|
||
warning (f_"unknown/unavailable method for expanding the %s filesystem on %s")
|
||
fs name;
|
||
);
|
||
""
|
||
) in
|
||
|
||
wrap (text ^ "\n\n")
|
||
) lvs;
|
||
|
||
if surplus > 0L then (
|
||
let text =
|
||
sprintf (f_"There is a surplus of %s.") (human_size surplus) ^
|
||
if extra_partition then (
|
||
if surplus >= min_extra_partition then
|
||
s_" An extra partition will be created for the surplus."
|
||
else
|
||
s_" The surplus space is not large enough for an extra partition to be created and so it will just be ignored."
|
||
) else
|
||
s_" 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 =
|
||
(* Try hard to initialize the partition table. This might involve
|
||
* relaunching another handle.
|
||
*)
|
||
message (f_"Setting up initial partition table on %s") (fst outfile);
|
||
|
||
let last_error = ref "" in
|
||
let rec initialize_partition_table g attempts =
|
||
let ok =
|
||
try
|
||
g#part_init "/dev/sdb" parttype_string;
|
||
Option.may (g#part_set_disk_guid "/dev/sdb") disk_guid;
|
||
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#shutdown ();
|
||
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 (f_"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 in
|
||
|
||
(* 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
|
||
*)
|
||
if copy_boot_loader then (
|
||
let bootsect = g#pread_device "/dev/sda" 446 0L in
|
||
if String.length bootsect < 446 then
|
||
error (f_"pread-device: short read");
|
||
ignore (g#pwrite_device "/dev/sdb" bootsect 0L);
|
||
|
||
let start =
|
||
if parttype <> GPT then 512L
|
||
else
|
||
(* With 512 byte sectors, GPT looks like:
|
||
* 512 bytes sector 0 protective MBR
|
||
* 1024 bytes sector 1 GPT header
|
||
* 17408 bytes sectors 2-33 GPT entries (up to 128 x 128 bytes)
|
||
*
|
||
* With 4K sectors, GPT puts more entries in each sector, so
|
||
* the partition table looks like this:
|
||
* 4096 bytes sector 0 protective MBR
|
||
* 8192 bytes sector 1 GPT header
|
||
* 24576 bytes sectors 2-5 GPT entries (up to 128 x 128 bytes)
|
||
*
|
||
* qemu doesn't support 4k sectors yet, so let's just use the
|
||
* 512 sector number for now.
|
||
*)
|
||
17408L in
|
||
|
||
let loader = g#pread_device "/dev/sda" max_bootloader start in
|
||
if String.length loader < max_bootloader then
|
||
error (f_"pread-device: short read");
|
||
ignore (g#pwrite_device "/dev/sdb" loader start)
|
||
);
|
||
|
||
(* Are we going to align the first partition and fix the bootloader? *)
|
||
let align_first_partition_and_fix_bootloader =
|
||
(* Bootloaders that we know how to fix:
|
||
* - first partition is NTFS, and
|
||
* - first partition is bootable, and
|
||
* - only one partition (ie. not Win Vista and later), and
|
||
* - it's not already aligned to some small value (no point
|
||
* moving it around unnecessarily)
|
||
*)
|
||
let rec can_fix_boot_loader () =
|
||
match partitions with
|
||
| [ { p_part = { G.part_start = start };
|
||
p_type = ContentFS ("ntfs", _);
|
||
p_bootable = true;
|
||
p_operation = OpCopy | OpIgnore | OpResize _ } ]
|
||
when not_aligned_enough start -> true
|
||
| _ -> false
|
||
and not_aligned_enough start =
|
||
let alignment = alignment_of start in
|
||
alignment < 12 (* < 4K alignment *)
|
||
and alignment_of = function
|
||
| 0L -> 64
|
||
| n when n &^ 1L = 1L -> 0
|
||
| n -> 1 + alignment_of (n /^ 2L)
|
||
in
|
||
|
||
match align_first, can_fix_boot_loader () with
|
||
| `Never, _
|
||
| `Auto, false -> false
|
||
| `Always, _
|
||
| `Auto, true -> true in
|
||
|
||
debug "align_first_partition_and_fix_bootloader = %b"
|
||
align_first_partition_and_fix_bootloader;
|
||
|
||
(* Repartition the target disk. *)
|
||
|
||
(* Calculate the location of the partitions on the target disk. This
|
||
* also removes from the list any partitions that will be deleted, so
|
||
* the final list just contains partitions that need to be created
|
||
* on the target.
|
||
*)
|
||
let partitions =
|
||
let rec loop partnum start = function
|
||
| p :: ps ->
|
||
(match p.p_operation with
|
||
| OpDelete -> loop partnum start ps (* skip p *)
|
||
|
||
| OpIgnore | OpCopy -> (* same size *)
|
||
(* Size in sectors. *)
|
||
let size = div_roundup64 p.p_part.G.part_size sectsize in
|
||
(* Start of next partition + alignment. *)
|
||
let end_ = start +^ size in
|
||
let next = roundup64 end_ alignment in
|
||
|
||
debug "target partition %d: ignore or copy: start=%Ld end=%Ld"
|
||
partnum start (end_ -^ 1L);
|
||
|
||
{ p with p_target_start = start; p_target_end = end_ -^ 1L;
|
||
p_target_partnum = partnum } :: loop (partnum+1) next ps
|
||
|
||
| OpResize newsize -> (* resized partition *)
|
||
(* New size in sectors. *)
|
||
let size = div_roundup64 newsize sectsize in
|
||
(* Start of next partition + alignment. *)
|
||
let next = start +^ size in
|
||
let next = roundup64 next alignment in
|
||
|
||
debug "target partition %d: resize: newsize=%Ld start=%Ld end=%Ld"
|
||
partnum newsize start (next -^ 1L);
|
||
|
||
{ p with p_target_start = start; p_target_end = next -^ 1L;
|
||
p_target_partnum = partnum } :: loop (partnum+1) next ps
|
||
)
|
||
|
||
| [] ->
|
||
(* Create the surplus partition if there is room for it. *)
|
||
if extra_partition && surplus >= min_extra_partition then (
|
||
[ {
|
||
(* Since this partition has no source, this data is
|
||
* meaningless and not used since the operation is
|
||
* OpIgnore.
|
||
*)
|
||
p_name = "";
|
||
p_part = { G.part_num = 0l; part_start = 0L; part_end = 0L;
|
||
part_size = 0L };
|
||
p_bootable = false; p_id = No_ID; p_type = ContentUnknown;
|
||
p_label = None; p_guid = None;
|
||
|
||
(* Target information is meaningful. *)
|
||
p_operation = OpIgnore;
|
||
p_target_partnum = partnum;
|
||
p_target_start = start; p_target_end = ~^ 64L
|
||
} ]
|
||
)
|
||
else
|
||
[] in
|
||
|
||
(* Choose the alignment of the first partition based on the
|
||
* '--align-first' option. Old virt-resize used to always align this
|
||
* to 64 sectors, but this causes boot failures unless we are able to
|
||
* adjust the bootloader accordingly.
|
||
*)
|
||
let start =
|
||
if align_first_partition_and_fix_bootloader then
|
||
alignment
|
||
else
|
||
(* Preserve the existing start, but convert to sectors. *)
|
||
(List.hd partitions).p_part.G.part_start /^ sectsize in
|
||
|
||
loop 1 start partitions in
|
||
|
||
if verbose () then (
|
||
printf "After calculate target partitions:\n";
|
||
List.iter (debug_partition ~sectsize) partitions
|
||
);
|
||
|
||
(* Now partition the target disk. *)
|
||
List.iter (
|
||
fun p ->
|
||
g#part_add "/dev/sdb" "primary" p.p_target_start p.p_target_end
|
||
) partitions;
|
||
|
||
(* Set bootable and MBR IDs. Do this *before* copying over the data,
|
||
* because the rewritten sfdisk "helpfully" overwrites the partition
|
||
* table in the first sector of an extended partition if a partition
|
||
* is changed from primary to extended. Thus we need to set the
|
||
* MBR ID before doing the copy so sfdisk doesn't corrupt things.
|
||
*)
|
||
let set_partition_bootable_and_id p =
|
||
if p.p_bootable then
|
||
g#part_set_bootable "/dev/sdb" p.p_target_partnum true;
|
||
|
||
Option.may (g#part_set_name "/dev/sdb" p.p_target_partnum) p.p_label;
|
||
Option.may (g#part_set_gpt_guid "/dev/sdb" p.p_target_partnum) p.p_guid;
|
||
|
||
match parttype, p.p_id with
|
||
| GPT, GPT_Type gpt_type ->
|
||
g#part_set_gpt_type "/dev/sdb" p.p_target_partnum gpt_type
|
||
| MBR, MBR_ID mbr_id ->
|
||
g#part_set_mbr_id "/dev/sdb" p.p_target_partnum mbr_id
|
||
| GPT, (No_ID|MBR_ID _) | MBR, (No_ID|GPT_Type _) -> ()
|
||
in
|
||
List.iter set_partition_bootable_and_id partitions;
|
||
|
||
(* Copy over the data. *)
|
||
let copy_partition p =
|
||
match p.p_operation with
|
||
| OpCopy | OpResize _ ->
|
||
(* XXX Old code had 'when target_partnum > 0', but it appears
|
||
* to have served no purpose since the field could never be 0
|
||
* at this point.
|
||
*)
|
||
|
||
let oldsize = p.p_part.G.part_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 source = p.p_name in
|
||
let target = sprintf "/dev/sdb%d" p.p_target_partnum in
|
||
|
||
message (f_"Copying %s") source;
|
||
|
||
(match p.p_type with
|
||
| ContentUnknown | ContentPV _ | ContentFS _ | ContentSwap ->
|
||
g#copy_device_to_device ~size:copysize ~sparse source target
|
||
|
||
| ContentExtendedPartition ->
|
||
(* You can't just copy an extended partition by name, eg.
|
||
* source = "/dev/sda2", because the device name only covers
|
||
* the first 1K of the partition. Instead, copy the
|
||
* source bytes from the parent disk (/dev/sda).
|
||
*
|
||
* You can't write directly to the extended partition,
|
||
* because the size of it reported by Linux is always 1024
|
||
* bytes. Instead, write to the offset of the extended
|
||
* partition in the destination disk (/dev/sdb).
|
||
*)
|
||
let srcoffset = p.p_part.G.part_start in
|
||
let destoffset = p.p_target_start *^ 512L in
|
||
g#copy_device_to_device ~srcoffset ~destoffset ~size:copysize
|
||
~sparse
|
||
"/dev/sda" "/dev/sdb"
|
||
)
|
||
| OpIgnore | OpDelete -> ()
|
||
in
|
||
List.iter copy_partition partitions;
|
||
|
||
(* Fix the bootloader if we aligned the first partition. *)
|
||
if align_first_partition_and_fix_bootloader then (
|
||
(* See can_fix_boot_loader above. *)
|
||
match partitions with
|
||
| { p_type = ContentFS ("ntfs", _); p_bootable = true;
|
||
p_target_partnum = partnum; p_target_start = start } :: _ ->
|
||
(* If the first partition is NTFS and bootable, set the "Number of
|
||
* Hidden Sectors" field in the NTFS Boot Record so that the
|
||
* filesystem is still bootable.
|
||
*)
|
||
|
||
(* Should always be /dev/sdb1? *)
|
||
let target = sprintf "/dev/sdb%d" partnum in
|
||
|
||
(* Sanity check: it contains the NTFS magic. *)
|
||
let magic = g#pread_device target 8 3L in
|
||
if magic <> "NTFS " then
|
||
warning (f_"first partition is NTFS but does not contain NTFS boot loader magic")
|
||
else (
|
||
message (f_"Fixing first NTFS partition boot record");
|
||
|
||
if verbose () then (
|
||
let old_hidden = int_of_le32 (g#pread_device target 4 0x1c_L) in
|
||
debug "old hidden sectors value: 0x%Lx" old_hidden
|
||
);
|
||
|
||
let new_hidden = le32_of_int start in
|
||
ignore (g#pwrite_device target new_hidden 0x1c_L)
|
||
)
|
||
|
||
| { p_type =
|
||
(ContentFS _|ContentUnknown|ContentPV _
|
||
|ContentExtendedPartition|ContentSwap) } :: _
|
||
| [] -> ()
|
||
);
|
||
|
||
(* 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
|
||
| { p_operation = (OpCopy | OpIgnore | OpDelete) } -> false
|
||
) partitions
|
||
|| List.exists (
|
||
function
|
||
| ({ lv_operation = LVOpExpand } as lv) ->
|
||
can_expand_content lv.lv_type
|
||
| { lv_operation = LVOpNone } -> false
|
||
) lvs in
|
||
|
||
let g =
|
||
if to_be_expanded then (
|
||
g#shutdown ();
|
||
g#close ();
|
||
|
||
let g = open_guestfs () in
|
||
(* The output disk is being created, so use cache=unsafe here. *)
|
||
add_drive_uri g ?format:output_format ~readonly:false ~cachemode:"unsafe"
|
||
(snd outfile);
|
||
if not (quiet ()) then Progress.set_up_progress_bar ~machine_readable g;
|
||
g#launch ();
|
||
|
||
g (* Return new handle. *)
|
||
)
|
||
else g (* Return existing handle. *) in
|
||
|
||
if to_be_expanded then (
|
||
(* Helper function to expand partition or LV content. *)
|
||
let do_expand_content target =
|
||
let with_mounted dev (resize : string -> unit) =
|
||
(* Btrfs and XFS need to mount the filesystem to resize it. *)
|
||
assert (Array.length (g#mounts ()) = 0);
|
||
g#mount dev "/";
|
||
resize "/";
|
||
g#umount "/"
|
||
in
|
||
function
|
||
| PVResize -> g#pvresize target
|
||
| Resize2fs -> g#resize2fs target
|
||
| NTFSResize -> g#ntfsresize ~force:ntfsresize_force target
|
||
| BtrfsFilesystemResize -> with_mounted target g#btrfs_filesystem_resize
|
||
| XFSGrowFS -> with_mounted target g#xfs_growfs
|
||
| Mkswap ->
|
||
(* Rebuild the swap using the UUID and label of the existing
|
||
* swap partition.
|
||
*)
|
||
let orig_uuid = g#vfs_uuid target in
|
||
let uuid =
|
||
match orig_uuid with
|
||
| "" -> None
|
||
| uuid -> Some uuid in
|
||
let label = g#vfs_label target in
|
||
g#mkswap ?uuid ~label target;
|
||
(* Check whether the UUID could be set, and warn in case it
|
||
* changed.
|
||
*)
|
||
let new_uuid = g#vfs_uuid target in
|
||
if new_uuid <> orig_uuid then
|
||
warning (f_"UUID in swap partition %s changed from ‘%s’ to ‘%s’")
|
||
target orig_uuid new_uuid;
|
||
in
|
||
|
||
(* Expand partition content as required. *)
|
||
let expand_partition_content = 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
|
||
|
||
message (f_"Expanding %s%s using the ‘%s’ method")
|
||
source
|
||
(if source <> target then sprintf " (now %s)" target else "")
|
||
(string_of_expand_content_method meth);
|
||
|
||
do_expand_content target meth
|
||
| { p_operation = (OpCopy | OpIgnore | OpDelete | OpResize _) }
|
||
-> ()
|
||
in
|
||
List.iter expand_partition_content 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
|
||
|
||
message (f_"Expanding %s using the ‘%s’ method")
|
||
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
|
||
| { lv_operation = (LVOpExpand | LVOpNone) } -> ()
|
||
) lvs
|
||
);
|
||
|
||
(* Finished. Unmount disks and exit. *)
|
||
g#shutdown ();
|
||
g#close ();
|
||
|
||
(* Try to sync the destination disk only if it is a local file. *)
|
||
(match outfile with
|
||
| _, { URI.protocol = (""|"file"); path } ->
|
||
(* Because we used cache=unsafe when writing the output file, the
|
||
* file might not be committed to disk. This is a problem if qemu is
|
||
* immediately used afterwards with cache=none (which uses O_DIRECT
|
||
* and therefore bypasses the host cache). In general you should not
|
||
* use cache=none.
|
||
*)
|
||
Fsync.file path
|
||
| _ -> ());
|
||
|
||
if not (quiet ()) then (
|
||
print_newline ();
|
||
wrap (s_"Resize operation completed with no errors. Before deleting the old disk, carefully check that the resized disk boots and works correctly.\n");
|
||
)
|
||
|
||
let () = run_main_and_handle_errors main
|