From 29e6bf282f3d7ce3bba98653bdf81d7fc4b83ede Mon Sep 17 00:00:00 2001 From: David Anderson Date: Mon, 29 Apr 2019 13:54:05 -0700 Subject: [PATCH] Add dm-snapshot targets to libdm and dmctl. This adds DmTargetSnapshotOrigin and DmTargetSnapshot. The latter target can handle both "snapshot" and "snapshot-merge" targets. The syntax for dmctl is as follows: dmctl create snapshot \ dmctl create snapshot-merge \ dmctl create snapshot-origin Bug: N/A Test: libdm_test gtests Change-Id: I8eef987cb92121e81bedd37b9a66fad04c7a23a3 --- fs_mgr/libdm/dm.cpp | 14 ++ fs_mgr/libdm/dm_target.cpp | 78 ++++++++ fs_mgr/libdm/dm_test.cpp | 257 ++++++++++++++++++++++++- fs_mgr/libdm/include/libdm/dm.h | 4 + fs_mgr/libdm/include/libdm/dm_target.h | 71 +++++++ fs_mgr/tools/dmctl.cpp | 93 +++++++-- 6 files changed, 494 insertions(+), 23 deletions(-) diff --git a/fs_mgr/libdm/dm.cpp b/fs_mgr/libdm/dm.cpp index a4614d02f..c2917a4b0 100644 --- a/fs_mgr/libdm/dm.cpp +++ b/fs_mgr/libdm/dm.cpp @@ -210,6 +210,20 @@ bool DeviceMapper::GetAvailableTargets(std::vector* targets) { return true; } +bool DeviceMapper::GetTargetByName(const std::string& name, DmTargetTypeInfo* info) { + std::vector targets; + if (!GetAvailableTargets(&targets)) { + return false; + } + for (const auto& target : targets) { + if (target.name() == name) { + if (info) *info = target; + return true; + } + } + return false; +} + bool DeviceMapper::GetAvailableDevices(std::vector* devices) { devices->clear(); diff --git a/fs_mgr/libdm/dm_target.cpp b/fs_mgr/libdm/dm_target.cpp index cb33eeae3..f440e6dc7 100644 --- a/fs_mgr/libdm/dm_target.cpp +++ b/fs_mgr/libdm/dm_target.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -115,5 +116,82 @@ std::string DmTargetAndroidVerity::GetParameterString() const { return keyid_ + " " + block_device_; } +std::string DmTargetSnapshot::name() const { + if (mode_ == SnapshotStorageMode::Merge) { + return "snapshot-merge"; + } + return "snapshot"; +} + +std::string DmTargetSnapshot::GetParameterString() const { + std::string mode; + switch (mode_) { + case SnapshotStorageMode::Persistent: + case SnapshotStorageMode::Merge: + // Note: "O" lets us query for overflow in the status message. This + // is only supported on kernels 4.4+. On earlier kernels, an overflow + // will be reported as "Invalid" in the status string. + mode = "P"; + if (ReportsOverflow(name())) { + mode += "O"; + } + break; + case SnapshotStorageMode::Transient: + mode = "N"; + break; + default: + LOG(ERROR) << "DmTargetSnapshot unknown mode"; + break; + } + return base_device_ + " " + cow_device_ + " " + mode + " " + std::to_string(chunk_size_); +} + +bool DmTargetSnapshot::ReportsOverflow(const std::string& target_type) { + DeviceMapper& dm = DeviceMapper::Instance(); + DmTargetTypeInfo info; + if (!dm.GetTargetByName(target_type, &info)) { + return false; + } + if (target_type == "snapshot") { + return info.IsAtLeast(1, 15, 0); + } + if (target_type == "snapshot-merge") { + return info.IsAtLeast(1, 4, 0); + } + return false; +} + +bool DmTargetSnapshot::ParseStatusText(const std::string& text, Status* status) { + auto sections = android::base::Split(text, " "); + if (sections.size() == 1) { + // This is probably an error code, "Invalid" is possible as is "Overflow" + // on 4.4+. + status->error = text; + return true; + } + if (sections.size() != 2) { + LOG(ERROR) << "snapshot status should have two components"; + return false; + } + auto sector_info = android::base::Split(sections[0], "/"); + if (sector_info.size() != 2) { + LOG(ERROR) << "snapshot sector info should have two components"; + return false; + } + if (!android::base::ParseUint(sections[1], &status->metadata_sectors)) { + LOG(ERROR) << "could not parse metadata sectors"; + return false; + } + if (!android::base::ParseUint(sector_info[0], &status->sectors_allocated)) { + LOG(ERROR) << "could not parse sectors allocated"; + return false; + } + if (!android::base::ParseUint(sector_info[1], &status->total_sectors)) { + LOG(ERROR) << "could not parse total sectors"; + return false; + } + return true; +} + } // namespace dm } // namespace android diff --git a/fs_mgr/libdm/dm_test.cpp b/fs_mgr/libdm/dm_test.cpp index 70823c619..72a0e119f 100644 --- a/fs_mgr/libdm/dm_test.cpp +++ b/fs_mgr/libdm/dm_test.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -35,21 +36,15 @@ #include "test_util.h" using namespace std; +using namespace std::chrono_literals; using namespace android::dm; using unique_fd = android::base::unique_fd; TEST(libdm, HasMinimumTargets) { + DmTargetTypeInfo info; + DeviceMapper& dm = DeviceMapper::Instance(); - vector targets; - ASSERT_TRUE(dm.GetAvailableTargets(&targets)); - - map by_name; - for (const auto& target : targets) { - by_name[target.name()] = target; - } - - auto iter = by_name.find("linear"); - EXPECT_NE(iter, by_name.end()); + ASSERT_TRUE(dm.GetTargetByName("linear", &info)); } // Helper to ensure that device mapper devices are released. @@ -201,3 +196,245 @@ TEST(libdm, DmVerityArgsAvb2) { "2 fec_blocks 126955 fec_start 126955 restart_on_corruption ignore_zero_blocks"; EXPECT_EQ(target.GetParameterString(), expected); } + +TEST(libdm, DmSnapshotArgs) { + DmTargetSnapshot target1(0, 512, "base", "cow", SnapshotStorageMode::Persistent, 8); + if (DmTargetSnapshot::ReportsOverflow("snapshot")) { + EXPECT_EQ(target1.GetParameterString(), "base cow PO 8"); + } else { + EXPECT_EQ(target1.GetParameterString(), "base cow P 8"); + } + EXPECT_EQ(target1.name(), "snapshot"); + + DmTargetSnapshot target2(0, 512, "base", "cow", SnapshotStorageMode::Transient, 8); + EXPECT_EQ(target2.GetParameterString(), "base cow N 8"); + EXPECT_EQ(target2.name(), "snapshot"); + + DmTargetSnapshot target3(0, 512, "base", "cow", SnapshotStorageMode::Merge, 8); + if (DmTargetSnapshot::ReportsOverflow("snapshot-merge")) { + EXPECT_EQ(target3.GetParameterString(), "base cow PO 8"); + } else { + EXPECT_EQ(target3.GetParameterString(), "base cow P 8"); + } + EXPECT_EQ(target3.name(), "snapshot-merge"); +} + +TEST(libdm, DmSnapshotOriginArgs) { + DmTargetSnapshotOrigin target(0, 512, "base"); + EXPECT_EQ(target.GetParameterString(), "base"); + EXPECT_EQ(target.name(), "snapshot-origin"); +} + +class SnapshotTestHarness final { + public: + bool Setup(); + bool Merge(); + + std::string origin_dev() const { return origin_dev_->path(); } + std::string snapshot_dev() const { return snapshot_dev_->path(); } + + int base_fd() const { return base_fd_; } + + static const uint64_t kBaseDeviceSize = 1024 * 1024; + static const uint64_t kCowDeviceSize = 1024 * 64; + static const uint64_t kSectorSize = 512; + + private: + void SetupImpl(); + void MergeImpl(); + + unique_fd base_fd_; + unique_fd cow_fd_; + unique_ptr base_loop_; + unique_ptr cow_loop_; + unique_ptr origin_dev_; + unique_ptr snapshot_dev_; + bool setup_ok_ = false; + bool merge_ok_ = false; +}; + +bool SnapshotTestHarness::Setup() { + SetupImpl(); + return setup_ok_; +} + +void SnapshotTestHarness::SetupImpl() { + base_fd_ = CreateTempFile("base_device", kBaseDeviceSize); + ASSERT_GE(base_fd_, 0); + cow_fd_ = CreateTempFile("cow_device", kCowDeviceSize); + ASSERT_GE(cow_fd_, 0); + + base_loop_ = std::make_unique(base_fd_); + ASSERT_TRUE(base_loop_->valid()); + cow_loop_ = std::make_unique(cow_fd_); + ASSERT_TRUE(cow_loop_->valid()); + + DmTable origin_table; + ASSERT_TRUE(origin_table.AddTarget(make_unique( + 0, kBaseDeviceSize / kSectorSize, base_loop_->device()))); + ASSERT_TRUE(origin_table.valid()); + + origin_dev_ = std::make_unique("libdm-test-dm-snapshot-origin", origin_table); + ASSERT_TRUE(origin_dev_->valid()); + ASSERT_FALSE(origin_dev_->path().empty()); + ASSERT_TRUE(origin_dev_->WaitForUdev()); + + // chunk size = 4K blocks. + DmTable snap_table; + ASSERT_TRUE(snap_table.AddTarget(make_unique( + 0, kBaseDeviceSize / kSectorSize, base_loop_->device(), cow_loop_->device(), + SnapshotStorageMode::Persistent, 8))); + ASSERT_TRUE(snap_table.valid()); + + snapshot_dev_ = std::make_unique("libdm-test-dm-snapshot", snap_table); + ASSERT_TRUE(snapshot_dev_->valid()); + ASSERT_FALSE(snapshot_dev_->path().empty()); + ASSERT_TRUE(snapshot_dev_->WaitForUdev()); + + setup_ok_ = true; +} + +bool SnapshotTestHarness::Merge() { + MergeImpl(); + return merge_ok_; +} + +void SnapshotTestHarness::MergeImpl() { + DmTable merge_table; + ASSERT_TRUE(merge_table.AddTarget( + make_unique(0, kBaseDeviceSize / kSectorSize, base_loop_->device(), + cow_loop_->device(), SnapshotStorageMode::Merge, 8))); + ASSERT_TRUE(merge_table.valid()); + + DeviceMapper& dm = DeviceMapper::Instance(); + ASSERT_TRUE(dm.LoadTableAndActivate("libdm-test-dm-snapshot", merge_table)); + + while (true) { + vector status; + ASSERT_TRUE(dm.GetTableStatus("libdm-test-dm-snapshot", &status)); + ASSERT_EQ(status.size(), 1); + ASSERT_EQ(strncmp(status[0].spec.target_type, "snapshot-merge", strlen("snapshot-merge")), + 0); + + DmTargetSnapshot::Status merge_status; + ASSERT_TRUE(DmTargetSnapshot::ParseStatusText(status[0].data, &merge_status)); + ASSERT_TRUE(merge_status.error.empty()); + if (merge_status.sectors_allocated == merge_status.metadata_sectors) { + break; + } + + std::this_thread::sleep_for(250ms); + } + + merge_ok_ = true; +} + +bool CheckSnapshotAvailability() { + DmTargetTypeInfo info; + + DeviceMapper& dm = DeviceMapper::Instance(); + if (!dm.GetTargetByName("snapshot", &info)) { + cout << "snapshot module not enabled; skipping test" << std::endl; + return false; + } + if (!dm.GetTargetByName("snapshot-merge", &info)) { + cout << "snapshot-merge module not enabled; skipping test" << std::endl; + return false; + } + if (!dm.GetTargetByName("snapshot-origin", &info)) { + cout << "snapshot-origin module not enabled; skipping test" << std::endl; + return false; + } + return true; +} + +TEST(libdm, DmSnapshot) { + if (!CheckSnapshotAvailability()) { + return; + } + + SnapshotTestHarness harness; + ASSERT_TRUE(harness.Setup()); + + // Open the dm devices. + unique_fd origin_fd(open(harness.origin_dev().c_str(), O_RDONLY | O_CLOEXEC)); + ASSERT_GE(origin_fd, 0); + unique_fd snapshot_fd(open(harness.snapshot_dev().c_str(), O_RDWR | O_CLOEXEC | O_SYNC)); + ASSERT_GE(snapshot_fd, 0); + + // Write to the first block of the snapshot device. + std::string data("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"); + ASSERT_TRUE(android::base::WriteFully(snapshot_fd, data.data(), data.size())); + ASSERT_EQ(lseek(snapshot_fd, 0, SEEK_SET), 0); + + // We should get the same data back from the snapshot device. + std::string read(data.size(), '\0'); + ASSERT_TRUE(android::base::ReadFully(snapshot_fd, read.data(), read.size())); + ASSERT_EQ(read, data); + + // We should see the original data from the origin device. + std::string zeroes(data.size(), '\0'); + ASSERT_TRUE(android::base::ReadFully(origin_fd, read.data(), read.size())); + ASSERT_EQ(lseek(snapshot_fd, 0, SEEK_SET), 0); + ASSERT_EQ(read, zeroes); + + // We should also see the original data from the base device. + ASSERT_TRUE(android::base::ReadFully(harness.base_fd(), read.data(), read.size())); + ASSERT_EQ(lseek(harness.base_fd(), 0, SEEK_SET), 0); + ASSERT_EQ(read, zeroes); + + // Now, perform the merge and wait. + ASSERT_TRUE(harness.Merge()); + + // Reading from the base device should give us the modified data. + ASSERT_TRUE(android::base::ReadFully(harness.base_fd(), read.data(), read.size())); + ASSERT_EQ(lseek(harness.base_fd(), 0, SEEK_SET), 0); + ASSERT_EQ(read, data); +} + +TEST(libdm, DmSnapshotOverflow) { + if (!CheckSnapshotAvailability()) { + return; + } + + SnapshotTestHarness harness; + ASSERT_TRUE(harness.Setup()); + + // Open the dm devices. + unique_fd snapshot_fd(open(harness.snapshot_dev().c_str(), O_RDWR | O_CLOEXEC)); + ASSERT_GE(snapshot_fd, 0); + + // Fill the copy-on-write device until it overflows. + uint64_t bytes_remaining = SnapshotTestHarness::kCowDeviceSize; + uint8_t byte = 1; + while (bytes_remaining) { + std::string data(4096, char(byte)); + if (!android::base::WriteFully(snapshot_fd, data.data(), data.size())) { + ASSERT_EQ(errno, EIO); + break; + } + bytes_remaining -= data.size(); + } + + // If writes succeed (because they are buffered), then we should expect an + // fsync to fail with EIO. + if (!bytes_remaining) { + ASSERT_EQ(fsync(snapshot_fd), -1); + ASSERT_EQ(errno, EIO); + } + + DeviceMapper& dm = DeviceMapper::Instance(); + + vector target_status; + ASSERT_TRUE(dm.GetTableStatus("libdm-test-dm-snapshot", &target_status)); + ASSERT_EQ(target_status.size(), 1); + ASSERT_EQ(strncmp(target_status[0].spec.target_type, "snapshot", strlen("snapshot")), 0); + + DmTargetSnapshot::Status status; + ASSERT_TRUE(DmTargetSnapshot::ParseStatusText(target_status[0].data, &status)); + if (DmTargetSnapshot::ReportsOverflow("snapshot")) { + ASSERT_EQ(status.error, "Overflow"); + } else { + ASSERT_EQ(status.error, "Invalid"); + } +} diff --git a/fs_mgr/libdm/include/libdm/dm.h b/fs_mgr/libdm/include/libdm/dm.h index 28e6e011c..d7e8aa91c 100644 --- a/fs_mgr/libdm/include/libdm/dm.h +++ b/fs_mgr/libdm/include/libdm/dm.h @@ -96,6 +96,10 @@ class DeviceMapper final { // successfully read and stored in 'targets'. Returns 'false' otherwise. bool GetAvailableTargets(std::vector* targets); + // Finds a target by name and returns its information if found. |info| may + // be null to check for the existence of a target. + bool GetTargetByName(const std::string& name, DmTargetTypeInfo* info); + // Return 'true' if it can successfully read the list of device mapper block devices // currently created. 'devices' will be empty if the kernel interactions // were successful and there are no block devices at the moment. Returns diff --git a/fs_mgr/libdm/include/libdm/dm_target.h b/fs_mgr/libdm/include/libdm/dm_target.h index 175b0f05c..fce1175b5 100644 --- a/fs_mgr/libdm/include/libdm/dm_target.h +++ b/fs_mgr/libdm/include/libdm/dm_target.h @@ -40,6 +40,18 @@ class DmTargetTypeInfo { return std::to_string(major_) + "." + std::to_string(minor_) + "." + std::to_string(patch_); } + uint32_t major_version() const { return major_; } + uint32_t minor_version() const { return minor_; } + uint32_t patch_level() const { return patch_; } + + bool IsAtLeast(uint32_t major, uint32_t minor, uint32_t patch) const { + if (major_ > major) return true; + if (major_ < major) return false; + if (minor_ > minor) return true; + if (minor_ < minor) return false; + return patch_ >= patch; + } + private: std::string name_; uint32_t major_; @@ -170,6 +182,65 @@ class DmTargetBow final : public DmTarget { std::string target_string_; }; +enum class SnapshotStorageMode { + // The snapshot will be persisted to the COW device. + Persistent, + // The snapshot will be lost on reboot. + Transient, + // The snapshot will be merged from the COW device into the base device, + // in the background. + Merge +}; + +// Writes to a snapshot device will be written to the given COW device. Reads +// will read from the COW device or base device. The chunk size is specified +// in sectors. +class DmTargetSnapshot final : public DmTarget { + public: + DmTargetSnapshot(uint64_t start, uint64_t length, const std::string& base_device, + const std::string& cow_device, SnapshotStorageMode mode, uint64_t chunk_size) + : DmTarget(start, length), + base_device_(base_device), + cow_device_(cow_device), + mode_(mode), + chunk_size_(chunk_size) {} + + std::string name() const override; + std::string GetParameterString() const override; + bool Valid() const override { return true; } + + struct Status { + uint64_t sectors_allocated; + uint64_t total_sectors; + uint64_t metadata_sectors; + std::string error; + }; + + static bool ParseStatusText(const std::string& text, Status* status); + static bool ReportsOverflow(const std::string& target_type); + + private: + std::string base_device_; + std::string cow_device_; + SnapshotStorageMode mode_; + uint64_t chunk_size_; +}; + +// snapshot-origin will read/write directly to the backing device, updating any +// snapshot devices with a matching origin. +class DmTargetSnapshotOrigin final : public DmTarget { + public: + DmTargetSnapshotOrigin(uint64_t start, uint64_t length, const std::string& device) + : DmTarget(start, length), device_(device) {} + + std::string name() const override { return "snapshot-origin"; } + std::string GetParameterString() const override { return device_; } + bool Valid() const override { return true; } + + private: + std::string device_; +}; + } // namespace dm } // namespace android diff --git a/fs_mgr/tools/dmctl.cpp b/fs_mgr/tools/dmctl.cpp index 9309aadf8..7e6ad5bf8 100644 --- a/fs_mgr/tools/dmctl.cpp +++ b/fs_mgr/tools/dmctl.cpp @@ -38,15 +38,7 @@ #include using namespace std::literals::string_literals; - -using DeviceMapper = ::android::dm::DeviceMapper; -using DmTable = ::android::dm::DmTable; -using DmTarget = ::android::dm::DmTarget; -using DmTargetLinear = ::android::dm::DmTargetLinear; -using DmTargetZero = ::android::dm::DmTargetZero; -using DmTargetAndroidVerity = ::android::dm::DmTargetAndroidVerity; -using DmTargetBow = ::android::dm::DmTargetBow; -using DmTargetTypeInfo = ::android::dm::DmTargetTypeInfo; +using namespace android::dm; using DmBlockDevice = ::android::dm::DeviceMapper::DmBlockDevice; static int Usage(void) { @@ -57,6 +49,7 @@ static int Usage(void) { std::cerr << " delete " << std::endl; std::cerr << " list [-v]" << std::endl; std::cerr << " getpath " << std::endl; + std::cerr << " status " << std::endl; std::cerr << " table " << std::endl; std::cerr << " help" << std::endl; std::cerr << std::endl; @@ -122,6 +115,62 @@ class TargetParser final { } std::string block_device = NextArg(); return std::make_unique(start_sector, num_sectors, block_device); + } else if (target_type == "snapshot-origin") { + if (!HasArgs(1)) { + std::cerr << "Expected \"snapshot-origin\" " << std::endl; + return nullptr; + } + std::string block_device = NextArg(); + return std::make_unique(start_sector, num_sectors, + block_device); + } else if (target_type == "snapshot") { + if (!HasArgs(4)) { + std::cerr + << "Expected \"snapshot\" " + << std::endl; + return nullptr; + } + std::string base_device = NextArg(); + std::string cow_device = NextArg(); + std::string mode_str = NextArg(); + std::string chunk_size_str = NextArg(); + + SnapshotStorageMode mode; + if (mode_str == "P") { + mode = SnapshotStorageMode::Persistent; + } else if (mode_str == "N") { + mode = SnapshotStorageMode::Transient; + } else { + std::cerr << "Unrecognized mode: " << mode_str << "\n"; + return nullptr; + } + + uint32_t chunk_size; + if (!android::base::ParseUint(chunk_size_str, &chunk_size)) { + std::cerr << "Chunk size must be an unsigned integer.\n"; + return nullptr; + } + return std::make_unique(start_sector, num_sectors, base_device, + cow_device, mode, chunk_size); + } else if (target_type == "snapshot-merge") { + if (!HasArgs(3)) { + std::cerr + << "Expected \"snapshot-merge\" " + << std::endl; + return nullptr; + } + std::string base_device = NextArg(); + std::string cow_device = NextArg(); + std::string chunk_size_str = NextArg(); + SnapshotStorageMode mode = SnapshotStorageMode::Merge; + + uint32_t chunk_size; + if (!android::base::ParseUint(chunk_size_str, &chunk_size)) { + std::cerr << "Chunk size must be an unsigned integer.\n"; + return nullptr; + } + return std::make_unique(start_sector, num_sectors, base_device, + cow_device, mode, chunk_size); } else { std::cerr << "Unrecognized target type: " << target_type << std::endl; return nullptr; @@ -308,7 +357,7 @@ static int GetPathCmdHandler(int argc, char** argv) { return 0; } -static int TableCmdHandler(int argc, char** argv) { +static int DumpTable(const std::string& mode, int argc, char** argv) { if (argc != 1) { std::cerr << "Invalid arguments, see \'dmctl help\'" << std::endl; return -EINVAL; @@ -316,9 +365,18 @@ static int TableCmdHandler(int argc, char** argv) { DeviceMapper& dm = DeviceMapper::Instance(); std::vector table; - if (!dm.GetTableInfo(argv[0], &table)) { - std::cerr << "Could not query table status of device \"" << argv[0] << "\"." << std::endl; - return -EINVAL; + if (mode == "status") { + if (!dm.GetTableStatus(argv[0], &table)) { + std::cerr << "Could not query table status of device \"" << argv[0] << "\"." + << std::endl; + return -EINVAL; + } + } else if (mode == "table") { + if (!dm.GetTableInfo(argv[0], &table)) { + std::cerr << "Could not query table status of device \"" << argv[0] << "\"." + << std::endl; + return -EINVAL; + } } std::cout << "Targets in the device-mapper table for " << argv[0] << ":" << std::endl; for (const auto& target : table) { @@ -333,6 +391,14 @@ static int TableCmdHandler(int argc, char** argv) { return 0; } +static int TableCmdHandler(int argc, char** argv) { + return DumpTable("table", argc, argv); +} + +static int StatusCmdHandler(int argc, char** argv) { + return DumpTable("status", argc, argv); +} + static std::map> cmdmap = { // clang-format off {"create", DmCreateCmdHandler}, @@ -341,6 +407,7 @@ static std::map> cmdmap = { {"help", HelpCmdHandler}, {"getpath", GetPathCmdHandler}, {"table", TableCmdHandler}, + {"status", StatusCmdHandler}, // clang-format on };