Created
September 17, 2025 07:59
-
-
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/
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
| #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