Created
October 13, 2025 12:41
-
-
Save NotKit/bef057be3d693d23cd18caafe8a90aaa to your computer and use it in GitHub Desktop.
Migrate existing ext4 partition to LVM physical volume in-place. Inspired by https://chromium.googlesource.com/chromiumos/platform2/+/main/thinpool_migrator/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| # Copyright (c) 2025 TheKit <thekit@disroot.org> | |
| # | |
| # Redistribution and use in source and binary forms, with or without | |
| # modification, are permitted provided that the following conditions are met: | |
| # | |
| # 1. Redistributions of source code must retain the above copyright notice, | |
| # this list of conditions and the following disclaimer. | |
| # | |
| # 2. Redistributions in binary form must reproduce the above copyright notice, | |
| # this list of conditions and the following disclaimer in the documentation | |
| # and/or other materials provided with the distribution. | |
| # | |
| # 3. Neither the name of the copyright holder nor the names of its | |
| # contributors may be used to endorse or promote products derived from | |
| # this software without specific prior written permission. | |
| # | |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
| # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | |
| # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
| # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
| # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
| # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
| # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
| # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| # POSSIBILITY OF SUCH DAMAGE. | |
| # Constants | |
| STARTING_PHYSICAL_EXTENT_ADDRESS=1048576 # 1MB | |
| PARTITION_HEADER_SIZE=1048576 # 1MB | |
| REGULAR_LVM_PHYSICAL_EXTENT_SIZE=1048576 # 1MB | |
| # Default values | |
| RESERVED_SIZE_MB=0 | |
| # Function to print usage | |
| print_usage() { | |
| echo "Usage: $0 --device=<device> --vg-name=<name> --lv-name=<name> [--reserve=<mb>]" | |
| echo " --device=<device> Path to the device to migrate" | |
| echo " --vg-name=<name> Volume group name (required)" | |
| echo " --lv-name=<name> Logical volume name (required)" | |
| echo " --reserve=<mb> Reserve space in MB for additional logical volumes (optional)" | |
| echo " --help Show this help message" | |
| } | |
| # Function to generate random ID (POSIX compatible) | |
| generate_id() { | |
| # Use /dev/urandom if available, otherwise use date + process ID | |
| if [ -r /dev/urandom ]; then | |
| hexdump -n 16 -e '4/4 "%08X" "\n"' /dev/urandom | sed 's/\([0-9A-F]\{8\}\)\([0-9A-F]\{4\}\)\([0-9A-F]\{4\}\)\([0-9A-F]\{4\}\)\([0-9A-F]\{12\}\)/\1-\2-\3-\4-\5/' | |
| else | |
| # Fallback: use date and process ID | |
| date +%s | od -tx4 -N4 | awk '{printf "%08X", $2}' | sed 's/\([0-9A-F]\{8\}\)\([0-9A-F]\{4\}\)\([0-9A-F]\{4\}\)\([0-9A-F]\{4\}\)\([0-9A-F]\{12\}\)/\1-\2-\3-\4-\5/' | |
| fi | |
| } | |
| # Function to get device size | |
| get_device_size() { | |
| device="$1" | |
| blockdev --getsize64 "$device" 2>/dev/null || { | |
| echo "Failed to get device size for $device" >&2 | |
| exit 1 | |
| } | |
| } | |
| # Function to resize filesystem | |
| resize_filesystem() { | |
| device="$1" | |
| target_size_bytes="$2" | |
| target_size_blocks=$((target_size_bytes / 4096)) | |
| resize2fs "$device" "$target_size_blocks" | |
| } | |
| # Function to copy header | |
| copy_header() { | |
| device="$1" | |
| src_offset="$2" | |
| dst_offset="$3" | |
| dd if="$device" of="$device" bs=512 count=2048 \ | |
| seek=$((dst_offset / 512)) skip=$((src_offset / 512)) \ | |
| conv=notrunc | |
| } | |
| # Function to create LVM metadata | |
| create_lvm_metadata() { | |
| device="$1" | |
| device_size="$2" | |
| vg_name="$3" | |
| lv_name="$4" | |
| reserved_size_mb="$5" | |
| # Calculate extents | |
| total_extents=$(((device_size - STARTING_PHYSICAL_EXTENT_ADDRESS) / REGULAR_LVM_PHYSICAL_EXTENT_SIZE)) | |
| header_extents=1 # 1MB header | |
| reserved_extents="$reserved_size_mb" # Reserved space in MB = extents (since 1 extent = 1MB) | |
| main_data_extents=$((total_extents - header_extents - reserved_extents)) | |
| # Generate random IDs | |
| vg_id=$(generate_id) | |
| pv_id=$(generate_id) | |
| lv_id=$(generate_id) | |
| # Get current time | |
| creation_time=$(date +%s) | |
| # Generate metadata | |
| cat > /tmp/vgcfgrestore.txt << EOF | |
| contents = "Text Format Volume Group" | |
| version = 1 | |
| description = "Generated by lvm_migrator" | |
| creation_host = "localhost" | |
| creation_time = $creation_time | |
| $vg_name { | |
| id = "$vg_id" | |
| seqno = 0 | |
| format = "lvm2" | |
| status = ["READ", "WRITE", "RESIZEABLE"] | |
| flags = [] | |
| extent_size = 2048 | |
| max_lv = 0 | |
| max_pv = 1 | |
| metadata_copies = 0 | |
| physical_volumes { | |
| pv0 { | |
| id = "$pv_id" | |
| device = $device | |
| status = ["ALLOCATABLE"] | |
| flags = [] | |
| dev_size = $((total_extents * 2048)) | |
| pe_start = 2048 | |
| pe_count = $total_extents | |
| } | |
| } | |
| logical_volumes { | |
| $lv_name { | |
| id = "$lv_id" | |
| status = ["READ", "WRITE", "VISIBLE"] | |
| flags = [] | |
| creation_time = $creation_time | |
| creation_host = "localhost" | |
| segment_count = 2 | |
| segment1 { | |
| start_extent = 0 | |
| extent_count = 1 | |
| type = "striped" | |
| stripe_count = 1 | |
| stripes = [ | |
| "pv0", $((total_extents - 1)) | |
| ] | |
| } | |
| segment2 { | |
| start_extent = 1 | |
| extent_count = $main_data_extents | |
| type = "striped" | |
| stripe_count = 1 | |
| stripes = [ | |
| "pv0", 0 | |
| ] | |
| } | |
| } | |
| } | |
| } | |
| EOF | |
| # Create physical volume | |
| pvcreate --uuid "$pv_id" --restorefile /tmp/vgcfgrestore.txt "$device" | |
| # Restore volume group | |
| vgcfgrestore -f /tmp/vgcfgrestore.txt "$vg_name" | |
| echo "Created volume group: $vg_name" | |
| echo "Created logical volume: $vg_name-$lv_name" | |
| } | |
| # Parse command line arguments | |
| while [ $# -gt 0 ]; do | |
| case "$1" in | |
| --help) | |
| print_usage | |
| exit 0 | |
| ;; | |
| --device=*) | |
| DEVICE_PATH="${1#*=}" | |
| shift | |
| ;; | |
| --vg-name=*) | |
| VG_NAME="${1#*=}" | |
| shift | |
| ;; | |
| --lv-name=*) | |
| LV_NAME="${1#*=}" | |
| shift | |
| ;; | |
| --reserve=*) | |
| RESERVED_SIZE_MB="${1#*=}" | |
| shift | |
| ;; | |
| *) | |
| echo "Unknown argument: $1" >&2 | |
| print_usage | |
| exit 1 | |
| ;; | |
| esac | |
| done | |
| # Validate required arguments | |
| if [ -z "${DEVICE_PATH:-}" ]; then | |
| echo "Error: --device argument is required" >&2 | |
| print_usage | |
| exit 1 | |
| fi | |
| if [ -z "${VG_NAME:-}" ]; then | |
| echo "Error: --vg-name argument is required" >&2 | |
| print_usage | |
| exit 1 | |
| fi | |
| if [ -z "${LV_NAME:-}" ]; then | |
| echo "Error: --lv-name argument is required" >&2 | |
| print_usage | |
| exit 1 | |
| fi | |
| # Validate reserve size (POSIX compatible number check) | |
| case "$RESERVED_SIZE_MB" in | |
| ''|*[!0-9]*) | |
| echo "Error: Invalid reserve size: $RESERVED_SIZE_MB" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| # Main migration logic | |
| echo "Starting LVM migration for device: $DEVICE_PATH" | |
| # Get device size | |
| DEVICE_SIZE=$(get_device_size "$DEVICE_PATH") | |
| echo "Device size: $DEVICE_SIZE bytes ($((DEVICE_SIZE / 1048576)) MB)" | |
| # Calculate filesystem size (reserve 1MB for LVM header + optional reserved space) | |
| RESERVED_SIZE_BYTES=$((RESERVED_SIZE_MB * 1048576)) | |
| FILESYSTEM_SIZE=$((DEVICE_SIZE - STARTING_PHYSICAL_EXTENT_ADDRESS - RESERVED_SIZE_BYTES)) | |
| echo "Resizing filesystem to: $FILESYSTEM_SIZE bytes ($((FILESYSTEM_SIZE / 1048576)) MB)" | |
| if [ "$RESERVED_SIZE_MB" -gt 0 ]; then | |
| echo "Reserving $RESERVED_SIZE_MB MB for additional logical volumes" | |
| fi | |
| # Resize filesystem | |
| echo "Resizing filesystem..." | |
| resize_filesystem "$DEVICE_PATH" "$FILESYSTEM_SIZE" | |
| # Copy header to end | |
| HEADER_OFFSET="$FILESYSTEM_SIZE" | |
| echo "Copying 1MB header to offset: $HEADER_OFFSET" | |
| copy_header "$DEVICE_PATH" 0 "$HEADER_OFFSET" | |
| # Create LVM metadata | |
| echo "Creating LVM metadata..." | |
| create_lvm_metadata "$DEVICE_PATH" "$DEVICE_SIZE" "$VG_NAME" "$LV_NAME" "$RESERVED_SIZE_MB" | |
| echo "Migration completed successfully!" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment