Skip to content

Instantly share code, notes, and snippets.

@NotKit
Created September 17, 2025 07:59
Show Gist options
  • Select an option

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

Select an option

Save NotKit/d123fcf7cd54fc43812c43aaec5c2aa8 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/
#include <iostream>
#include <string>
#include <cstdint>
#include <fstream>
#include <random>
#include <format>
#include <chrono>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/fs.h>
#include <cstdlib>
#include <cstring>
#include <sstream>
#include <vector>
// Constants
constexpr uint64_t kStartingPhysicalExtentAddress = 1 * 1024 * 1024; // 1MB
constexpr uint64_t kPartitionHeaderSize = 1 * 1024 * 1024; // 1MB
constexpr uint64_t kRegularLvmPhysicalExtentSize = 1 * 1024 * 1024; // 1MB
// Simple process execution function
int run_command(const std::vector<std::string>& args) {
// Build command string from arguments
std::ostringstream cmd_stream;
for (size_t i = 0; i < args.size(); ++i) {
if (i > 0) cmd_stream << " ";
// Escape arguments that contain spaces
if (args[i].find(' ') != std::string::npos) {
cmd_stream << "\"" << args[i] << "\"";
} else {
cmd_stream << args[i];
}
}
std::string command = cmd_stream.str();
// Execute the command using system()
int result = std::system(command.c_str());
// Extract exit code from system() return value
if (WIFEXITED(result)) {
return WEXITSTATUS(result);
} else {
return -1; // Process didn't exit normally
}
}
// LVM metadata templates
constexpr const char* kVolumeGroupMetadataTemplate = R"(contents = "Text Format Volume Group"
version = 1
description = "Generated by lvm_migrator"
creation_host = "localhost"
creation_time = {}
{} {{
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 = "{}"
device = {}
status = ["ALLOCATABLE"]
flags = []
dev_size = {}
pe_start = 2048
pe_count = {}
}}
}}
logical_volumes {{
{} {{
id = "{}"
status = ["READ", "WRITE", "VISIBLE"]
flags = []
creation_time = {}
creation_host = "localhost"
segment_count = 2
segment1 {{
start_extent = 0
extent_count = 1
type = "striped"
stripe_count = 1
stripes = [
"pv0", {}
]
}}
segment2 {{
start_extent = 1
extent_count = {}
type = "striped"
stripe_count = 1
stripes = [
"pv0", 0
]
}}
}}
}}
}})";
class LvmMigrator {
public:
LvmMigrator(const std::string& device_path,
const std::string& vg_name,
const std::string& lv_name,
uint64_t reserved_size_mb = 0)
: device_path_(device_path), vg_name_(vg_name),
lv_name_(lv_name), reserved_size_mb_(reserved_size_mb) {}
bool Migrate() {
std::cout << "Starting LVM migration for device: " << device_path_ << std::endl;
// Get device size
uint64_t device_size = GetDeviceSize();
if (device_size == 0) {
std::cerr << "Failed to get device size" << std::endl;
return false;
}
std::cout << "Device size: " << device_size << " bytes (" << device_size / (1024*1024) << " MB)" << std::endl;
// Calculate filesystem size (reserve 1MB for LVM header + optional reserved space)
uint64_t reserved_size_bytes = reserved_size_mb_ * 1024 * 1024;
uint64_t filesystem_size = device_size - kStartingPhysicalExtentAddress - reserved_size_bytes;
std::cout << "Resizing filesystem to: " << filesystem_size << " bytes (" << filesystem_size / (1024*1024) << " MB)" << std::endl;
if (reserved_size_mb_ > 0) {
std::cout << "Reserving " << reserved_size_mb_ << " MB for additional logical volumes" << std::endl;
}
// Resize filesystem
if (!ResizeFilesystem(filesystem_size)) {
std::cerr << "Failed to resize filesystem" << std::endl;
return false;
}
// Copy header to end
uint64_t header_offset = filesystem_size;
std::cout << "Copying 1MB header to offset: " << header_offset << std::endl;
if (!CopyHeader(0, header_offset)) {
std::cerr << "Failed to copy header" << std::endl;
return false;
}
// Create LVM metadata
if (!CreateLvmMetadata(device_size, filesystem_size)) {
std::cerr << "Failed to create LVM metadata" << std::endl;
return false;
}
std::cout << "Migration completed successfully!" << std::endl;
return true;
}
private:
std::string device_path_;
std::string vg_name_;
std::string lv_name_;
uint64_t reserved_size_mb_;
uint64_t GetDeviceSize() {
int fd = open(device_path_.c_str(), O_RDONLY);
if (fd < 0) {
return 0;
}
uint64_t size = 0;
if (ioctl(fd, BLKGETSIZE64, &size) == 0) {
close(fd);
return size;
}
close(fd);
return 0;
}
bool ResizeFilesystem(uint64_t target_size_bytes) {
// Convert bytes to 4KB filesystem blocks
uint64_t target_size_blocks = target_size_bytes / 4096;
return run_command({"resize2fs", device_path_, std::to_string(target_size_blocks)}) == 0;
}
bool CopyHeader(uint64_t src_offset, uint64_t dst_offset) {
return run_command({"dd",
"if=" + device_path_,
"of=" + device_path_,
"bs=512",
"count=2048", // 1MB in 512-byte blocks
"seek=" + std::to_string(dst_offset / 512),
"skip=" + std::to_string(src_offset / 512),
"conv=notrunc"}) == 0;
}
bool CreateLvmMetadata(uint64_t device_size, uint64_t /* filesystem_size */) {
// Calculate extents
uint64_t total_extents = (device_size - kStartingPhysicalExtentAddress) / kRegularLvmPhysicalExtentSize;
uint64_t header_extents = 1; // 1MB header
uint64_t reserved_extents = reserved_size_mb_; // Reserved space in MB = extents (since 1 extent = 1MB)
uint64_t main_data_extents = total_extents - header_extents - reserved_extents;
// Generate random IDs
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(0, 15);
auto generate_id = [&]() {
std::string id;
for (int i = 0; i < 32; ++i) {
id += "0123456789ABCDEF"[dis(gen)];
if (i == 7 || i == 11 || i == 15 || i == 19) id += "-";
}
return id;
};
std::string vg_id = generate_id();
std::string pv_id = generate_id();
std::string lv_id = generate_id();
// Use provided volume group name
std::string final_vg_name = vg_name_;
// Get current time
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
// Generate metadata
std::string metadata = std::format(kVolumeGroupMetadataTemplate,
time_t, // creation_time
final_vg_name, // volume group name
vg_id, // VG ID
pv_id, // PV ID
device_path_, // device path
total_extents * 2048, // dev_size in sectors
total_extents, // pe_count
lv_name_, // logical volume name
lv_id, // LV ID
time_t, // LV creation_time
total_extents - 1, // header physical extent (at end)
main_data_extents // main data extent count
);
// Write metadata to file
std::ofstream metadata_file("/tmp/vgcfgrestore.txt");
if (!metadata_file) {
std::cerr << "Failed to create metadata file" << std::endl;
return false;
}
metadata_file << metadata;
metadata_file.close();
// Create physical volume
if (run_command({"pvcreate", "--uuid", pv_id, "--restorefile", "/tmp/vgcfgrestore.txt", device_path_}) != 0) {
std::cerr << "Failed to create physical volume" << std::endl;
return false;
}
// Restore volume group
if (run_command({"vgcfgrestore", "-f", "/tmp/vgcfgrestore.txt", final_vg_name}) != 0) {
std::cerr << "Failed to restore volume group" << std::endl;
return false;
}
std::cout << "Created volume group: " << final_vg_name << std::endl;
std::cout << "Created logical volume: " << final_vg_name << "-" << lv_name_ << std::endl;
return true;
}
};
void print_usage(const char* program_name) {
std::cout << "Usage: " << program_name << " --device=<device> --vg-name=<name> --lv-name=<name> [--reserve=<mb>]" << std::endl;
std::cout << " --device=<device> Path to the device to migrate" << std::endl;
std::cout << " --vg-name=<name> Volume group name (required)" << std::endl;
std::cout << " --lv-name=<name> Logical volume name (required)" << std::endl;
std::cout << " --reserve=<mb> Reserve space in MB for additional logical volumes (optional)" << std::endl;
std::cout << " --help Show this help message" << std::endl;
}
int main(int argc, char* argv[]) {
std::string device_path;
std::string vg_name;
std::string lv_name;
uint64_t reserved_size_mb = 0;
// Parse command line arguments
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--help") {
print_usage(argv[0]);
return 0;
} else if (arg.substr(0, 9) == "--device=") {
device_path = arg.substr(9);
} else if (arg.substr(0, 10) == "--vg-name=") {
vg_name = arg.substr(10);
} else if (arg.substr(0, 10) == "--lv-name=") {
lv_name = arg.substr(10);
} else if (arg.substr(0, 10) == "--reserve=") {
try {
reserved_size_mb = std::stoull(arg.substr(10));
} catch (const std::exception&) {
std::cerr << "Error: Invalid reserve size: " << arg.substr(10) << std::endl;
return 1;
}
} else {
std::cerr << "Unknown argument: " << arg << std::endl;
print_usage(argv[0]);
return 1;
}
}
if (device_path.empty()) {
std::cerr << "Error: --device argument is required" << std::endl;
print_usage(argv[0]);
return 1;
}
if (vg_name.empty()) {
std::cerr << "Error: --vg-name argument is required" << std::endl;
print_usage(argv[0]);
return 1;
}
if (lv_name.empty()) {
std::cerr << "Error: --lv-name argument is required" << std::endl;
print_usage(argv[0]);
return 1;
}
LvmMigrator migrator(device_path, vg_name, lv_name, reserved_size_mb);
if (!migrator.Migrate()) {
std::cerr << "Migration failed!" << std::endl;
return 1;
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment