virt-resize: Be much more conservative about moving first partition.
[libguestfs.git] / resize / resize.ml
index 295d121..3c7a633 100644 (file)
@@ -28,8 +28,10 @@ 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,
+type align_first_t = [ `Never | `Always | `Auto ]
+
+let infile, outfile, align_first, alignment, copy_boot_loader, debug, 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 () =
@@ -42,6 +44,8 @@ let infile, outfile, copy_boot_loader, debug, deletes, dryrun,
 
   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 deletes = ref [] in
@@ -71,6 +75,8 @@ let infile, outfile, copy_boot_loader, debug, deletes, dryrun,
   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,             " -\"-";
@@ -118,6 +124,7 @@ read the man page virt-resize(1).
   );
 
   (* 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
@@ -135,6 +142,18 @@ read the man page virt-resize(1).
   let resizes_force = List.rev !resizes_force in
   let shrink = match !shrink with "" -> None | str -> Some str in
 
+  if alignment < 1 then
+    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
@@ -145,6 +164,8 @@ read the man page virt-resize(1).
     printf "ntfsresize-force\n";
     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 ();
@@ -162,8 +183,8 @@ read the man page virt-resize(1).
     | _ ->
         error "usage is: %s [--options] indisk outdisk" prog in
 
-  infile, outfile, copy_boot_loader, debug, deletes, dryrun,
-  expand, expand_content, extra_partition, format, ignores,
+  infile, outfile, align_first, alignment, copy_boot_loader, debug, deletes,
+  dryrun, expand, expand_content, extra_partition, format, ignores,
   lv_expands, machine_readable, ntfsresize_force, output_format,
   quiet, resizes, resizes_force, shrink
 
@@ -237,7 +258,26 @@ let () =
     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. *)
@@ -255,6 +295,7 @@ 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 *)
 and partition_operation =
   | OpCopy                       (* copy it as-is, no resizing *)
   | OpIgnore                     (* ignore it (create on target, but don't
@@ -275,10 +316,12 @@ 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"
 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
@@ -307,12 +350,26 @@ let get_partition_content =
     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) ->
@@ -322,7 +379,9 @@ let partitions : partition list =
         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;
@@ -374,7 +433,8 @@ type logvol = {
   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 *)
@@ -389,7 +449,10 @@ let lvs =
   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
@@ -422,6 +485,7 @@ let can_expand_content =
     | ContentFS (("ntfs"), _) when !ntfs_available -> true
     | ContentFS (("btrfs"), _) when !btrfs_available -> true
     | ContentFS (_, _) -> false
+    | ContentExtendedPartition -> false
   else
     fun _ -> false
 
@@ -434,6 +498,7 @@ let expand_content_method =
     | ContentFS (("ntfs"), _) when !ntfs_available -> NTFSResize
     | ContentFS (("btrfs"), _) when !btrfs_available -> BtrfsFilesystemResize
     | ContentFS (_, _) -> assert false
+    | ContentExtendedPartition -> assert false
   else
     fun _ -> assert false
 
@@ -525,6 +590,9 @@ let mark_partition_for_resize ~option ?(force = false) p 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 _ -> ()
+      | 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
@@ -570,7 +638,7 @@ let calculate_surplus () =
   let nr_partitions = List.length partitions in
   let overhead = (Int64.of_int sectsize) *^ (
     2L *^ 64L +^                                 (* GPT start and end *)
-    (128L *^ (Int64.of_int (nr_partitions + 1))) (* Maximum alignment *)
+    (alignment *^ (Int64.of_int (nr_partitions + 1))) (* Maximum alignment *)
   ) +^
   (Int64.of_int (max_bootloader - 64 * 512)) in  (* Bootloader *)
 
@@ -740,10 +808,7 @@ let () =
  * 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.
    *)
@@ -753,7 +818,7 @@ let g, parttype =
   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 (
@@ -771,7 +836,7 @@ let g, parttype =
   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.
@@ -786,7 +851,7 @@ let () =
     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?
@@ -799,6 +864,43 @@ let () =
     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
@@ -809,6 +911,9 @@ let () =
 let partitions =
   let sectsize = Int64.of_int sectsize in
 
+  (* Return 'i' rounded up to the next multiple of 'a'. *)
+  let roundup64 i a = let a = a -^ 1L in (i +^ a) &^ (~^ a) in
+
   let rec loop partnum start = function
     | p :: ps ->
       (match p.p_operation with
@@ -819,7 +924,7 @@ let partitions =
          let size = (p.p_part.G.part_size +^ sectsize -^ 1L) /^ sectsize in
          (* Start of next partition + alignment. *)
          let end_ = start +^ size in
-         let next = (end_ +^ 127L) &^ (~^ 127L) in
+         let next = roundup64 end_ alignment in
 
          { p with p_target_start = start; p_target_end = end_ -^ 1L;
            p_target_partnum = partnum } :: loop (partnum+1) next ps
@@ -829,7 +934,7 @@ let partitions =
          let size = (newsize +^ sectsize -^ 1L) /^ sectsize in
          (* Start of next partition + alignment. *)
          let next = start +^ size in
-         let next = (next +^ 127L) &^ (~^ 127L) in
+         let next = roundup64 next alignment in
 
          { p with p_target_start = start; p_target_end = next -^ 1L;
            p_target_partnum = partnum } :: loop (partnum+1) next ps
@@ -858,29 +963,25 @@ let partitions =
         []
   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. *)
@@ -906,11 +1007,74 @@ let () =
         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