Skip to content

Instantly share code, notes, and snippets.

@NotKit
Created October 13, 2025 12:41
Show Gist options
  • Select an option

  • Save NotKit/bef057be3d693d23cd18caafe8a90aaa to your computer and use it in GitHub Desktop.

Select an option

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/
#!/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