(* Command line argument parsing. *)
let prog = Filename.basename Sys.executable_name
-let infile, outfile, alignment, copy_boot_loader, debug, deletes, dryrun,
- expand, expand_content, extra_partition, format, ignores,
+type align_first_t = [ `Never | `Always | `Auto ]
+
+let infile, outfile, align_first, alignment, copy_boot_loader,
+ debug, debug_gc, deletes,
+ dryrun, expand, expand_content, extra_partition, format, ignores,
lv_expands, machine_readable, ntfsresize_force, output_format,
quiet, resizes, resizes_force, shrink =
let display_version () =
let add xs s = xs := s :: !xs in
+ let align_first = ref "auto" in
let alignment = ref 128 in
let copy_boot_loader = ref true in
let debug = ref false in
+ let debug_gc = ref false in
let deletes = ref [] in
let dryrun = ref false in
let expand = ref "" in
in
let argspec = Arg.align [
+ "--align-first", Arg.Set_string align_first, "never|always|auto Align first partition (default: auto)";
"--alignment", Arg.Set_int alignment, "sectors Set partition alignment (default: 128 sectors)";
"--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, " -\"-";
+ "--debug-gc",Arg.Set debug_gc, " Debug GC and memory allocations";
"--delete", Arg.String (add deletes), "part Delete partition";
"--expand", Arg.String set_expand, "part Expand partition";
"--no-expand-content", Arg.Clear expand_content, " Don't expand content";
(* Dereference the rest of the args. *)
let alignment = !alignment in
let copy_boot_loader = !copy_boot_loader in
+ let debug_gc = !debug_gc in
let deletes = List.rev !deletes in
let dryrun = !dryrun in
let expand = match !expand with "" -> None | str -> Some str in
error "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 "unknown --align-first option: use never|always|auto" 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
printf "32bitok\n";
printf "128-sector-alignment\n";
printf "alignment\n";
+ printf "align-first\n";
let g = new G.guestfs () in
g#add_drive_opts "/dev/null";
g#launch ();
| _ ->
error "usage is: %s [--options] indisk outdisk" prog in
- infile, outfile, alignment, copy_boot_loader, debug, deletes, dryrun,
- expand, expand_content, extra_partition, format, ignores,
+ infile, outfile, align_first, alignment, copy_boot_loader,
+ debug, debug_gc, deletes,
+ dryrun, expand, expand_content, extra_partition, format, ignores,
lv_expands, machine_readable, ntfsresize_force, output_format,
quiet, resizes, resizes_force, shrink
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. *)
+(* Get the source partition type. *)
+type parttype = MBR | GPT (* Only these are supported by virt-resize. *)
+
+let parttype, parttype_string =
+ let pt = g#part_get_parttype "/dev/sda" in
+ if debug then eprintf "partition table type: %s\n%!" pt;
+
+ match pt with
+ | "msdos" -> MBR, "msdos"
+ | "gpt" -> GPT, "gpt"
+ | _ ->
+ error "%s: unknown partition table type\nvirt-resize only supports MBR (DOS) and GPT partition tables." infile
+
+(* Build a 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. *)
| ContentUnknown (* undetermined *)
| ContentPV of int64 (* physical volume (size of PV) *)
| ContentFS of string * int64 (* mountable filesystem (FS type, FS size) *)
+ | ContentExtendedPartition (* MBR extended partition *)
and partition_operation =
| OpCopy (* copy it as-is, no resizing *)
| OpIgnore (* ignore it (create on target, but don't
| ContentUnknown -> "unknown data"
| ContentPV sz -> sprintf "LVM PV (%Ld bytes)" sz
| ContentFS (fs, sz) -> sprintf "filesystem %s (%Ld bytes)" fs sz
+ | ContentExtendedPartition -> "extended partition"
and string_of_partition_content_no_size = function
| ContentUnknown -> "unknown data"
| ContentPV _ -> sprintf "LVM PV"
| ContentFS (fs, _) -> sprintf "filesystem %s" fs
+ | ContentExtendedPartition -> "extended partition"
let get_partition_content =
let pvs_full = Array.to_list (g#pvs_full ()) in
with
G.Error _ -> ContentUnknown
+let is_extended_partition = function
+ | Some (0x05|0x0f) -> true
+ | _ -> false
+
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";
+ (* Filter out logical partitions. See note above. *)
+ let parts =
+ match parttype with
+ | GPT -> parts
+ | MBR ->
+ List.filter (function
+ | { G.part_num = part_num } when part_num >= 5_l -> false
+ | _ -> true
+ ) parts in
+
let partitions =
List.map (
fun ({ G.part_num = part_num } as part) ->
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
+ let typ =
+ if is_extended_partition mbr_id then ContentExtendedPartition
+ else get_partition_content name in
{ p_name = name; p_part = part;
p_bootable = bootable; p_mbr_id = mbr_id; p_type = typ;
lv_type : logvol_content;
mutable lv_operation : logvol_operation
}
-and logvol_content = partition_content (* except ContentPV cannot occur *)
+ (* ContentPV, ContentExtendedPartition cannot occur here *)
+and logvol_content = partition_content
and logvol_operation =
| LVOpNone (* nothing *)
| LVOpExpand (* expand it *)
let lvs = List.map (
fun name ->
let typ = get_partition_content name in
- assert (match typ with ContentPV _ -> false | _ -> true);
+ assert (
+ match typ with ContentPV _ | ContentExtendedPartition -> false
+ | _ -> true
+ );
{ lv_name = name; lv_type = typ; lv_operation = LVOpNone }
) lvs in
| ContentFS (("ntfs"), _) when !ntfs_available -> true
| ContentFS (("btrfs"), _) when !btrfs_available -> true
| ContentFS (_, _) -> false
+ | ContentExtendedPartition -> false
else
fun _ -> false
| ContentFS (("ntfs"), _) when !ntfs_available -> NTFSResize
| ContentFS (("btrfs"), _) when !btrfs_available -> BtrfsFilesystemResize
| ContentFS (_, _) -> assert false
+ | ContentExtendedPartition -> assert false
else
fun _ -> assert false
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 _ -> ()
+ | ContentExtendedPartition ->
+ error "%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
);
p.p_operation <- OpResize newsize
* 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;
-
+let g =
(* Try hard to initialize the partition table. This might involve
* relaunching another handle.
*)
let last_error = ref "" in
let rec initialize_partition_table g attempts =
let ok =
- try g#part_init "/dev/sdb" parttype; true
+ try g#part_init "/dev/sdb" parttype_string; true
with G.Error error -> last_error := error; false in
if ok then g, true
else if attempts > 0 then (
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
+ g
(* Copy the bootloader across.
* Don't disturb the partition table that we just wrote.
ignore (g#pwrite_device "/dev/sdb" bootsect 0L);
let start =
- if parttype <> "gpt" then 512L
+ 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?
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
+
+let () =
+ if debug then
+ eprintf "align_first_partition_and_fix_bootloader = %b\n%!"
+ align_first_partition_and_fix_bootloader
+
(* Repartition the target disk. *)
(* Calculate the location of the partitions on the target disk. This
[]
in
- (* 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.
+ (* 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 = (List.hd partitions).p_part.G.part_start /^ sectsize in
+ 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
(* Now partition the target disk. *)
let () =
List.iter (
fun p ->
- g#part_add "/dev/sdb" "primary" p.p_target_start p.p_target_end;
-
- (* Set bootable and MBR IDs *)
- if p.p_bootable then
- g#part_set_bootable "/dev/sdb" p.p_target_partnum true;
-
- (match p.p_mbr_id with
- | None -> ()
- | Some mbr_id ->
- g#part_set_mbr_id "/dev/sdb" p.p_target_partnum mbr_id
- );
+ g#part_add "/dev/sdb" "primary" p.p_target_start p.p_target_end
) partitions
(* Copy over the data. *)
if not quiet then
printf "Copying %s ...\n%!" source;
- g#copy_size source target copysize;
-
+ (match p.p_type with
+ | ContentUnknown | ContentPV _ | ContentFS _ ->
+ g#copy_device_to_device ~size:copysize 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).
+ *)
+ let srcoffset = p.p_part.G.part_start in
+ g#copy_device_to_device ~srcoffset ~size:copysize "/dev/sda" target
+ )
| _ -> ()
) partitions
+(* Set bootable and MBR IDs. Do this *after* copying over the data,
+ * so that we can magically change the primary partition to an extended
+ * partition if necessary.
+ *)
+let () =
+ List.iter (
+ fun p ->
+ if p.p_bootable then
+ g#part_set_bootable "/dev/sdb" p.p_target_partnum true;
+
+ (match p.p_mbr_id with
+ | None -> ()
+ | Some mbr_id ->
+ g#part_set_mbr_id "/dev/sdb" p.p_target_partnum mbr_id
+ );
+ ) partitions
+
+(* Fix the bootloader if we aligned the first partition. *)
+let () =
+ 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
+ eprintf "warning: first partition is NTFS but does not contain NTFS boot loader magic\n%!"
+ else (
+ if not quiet then
+ printf "Fixing first NTFS partition boot record ...\n%!";
+
+ if debug then (
+ let old_hidden = int_of_le32 (g#pread_device target 4 0x1c_L) in
+ eprintf "old hidden sectors value: 0x%Lx\n%!" old_hidden
+ );
+
+ let new_hidden = le32_of_int start in
+ ignore (g#pwrite_device target new_hidden 0x1c_L)
+ )
+
+ | _ -> ()
+ )
+
(* 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
wrap "Resize operation completed with no errors. Before deleting the old disk, carefully check that the resized disk boots and works correctly.\n";
);
+ if debug_gc then
+ Gc.compact ();
+
exit 0