X-Git-Url: http://git.annexia.org/?a=blobdiff_plain;f=resize%2Fresize.ml;h=3c7a6335b4025988daaf9b91ccf548a243815e7c;hb=19005b2cfc6b077aafd16cb5b97a08180e4e39f8;hp=aed0e439d4005ba122057cf1ca2597aeba53f4b4;hpb=37cdd39ada139956f237b55c87c095bed622b5e3;p=libguestfs.git diff --git a/resize/resize.ml b/resize/resize.ml index aed0e43..3c7a633 100644 --- a/resize/resize.ml +++ b/resize/resize.ml @@ -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, 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, 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,7 @@ let infile, outfile, alignment, 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 @@ -72,6 +75,7 @@ let infile, outfile, alignment, 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"; @@ -142,6 +146,14 @@ read the man page virt-resize(1). 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 @@ -153,6 +165,7 @@ read the man page virt-resize(1). 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 (); @@ -170,8 +183,8 @@ read the man page virt-resize(1). | _ -> 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, deletes, + dryrun, expand, expand_content, extra_partition, format, ignores, lv_expands, machine_readable, ntfsresize_force, output_format, quiet, resizes, resizes_force, shrink @@ -245,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. *) @@ -263,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 @@ -283,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 @@ -315,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) -> @@ -330,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; @@ -382,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 *) @@ -397,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 @@ -430,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 @@ -442,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 @@ -533,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 @@ -748,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. *) @@ -761,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 ( @@ -779,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. @@ -794,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? @@ -807,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 @@ -869,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. *) @@ -917,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