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 <name> snapshot <start> <num_sectors> <base_device> \
          <cow_device> <P|N> <chunk_size>
    dmctl create <name> snapshot-merge <start> <num_sectors> <base_device> \
          <cow_device> <chunk_size>
    dmctl create <name> snapshot-origin <start> <num_sectors> <device>

Bug: N/A
Test: libdm_test gtests
Change-Id: I8eef987cb92121e81bedd37b9a66fad04c7a23a3
This commit is contained in:
David Anderson 2019-04-29 13:54:05 -07:00
parent be9c2c0310
commit 29e6bf282f
6 changed files with 494 additions and 23 deletions

View file

@ -210,6 +210,20 @@ bool DeviceMapper::GetAvailableTargets(std::vector<DmTargetTypeInfo>* targets) {
return true;
}
bool DeviceMapper::GetTargetByName(const std::string& name, DmTargetTypeInfo* info) {
std::vector<DmTargetTypeInfo> 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<DmBlockDevice>* devices) {
devices->clear();

View file

@ -18,6 +18,7 @@
#include <android-base/logging.h>
#include <android-base/macros.h>
#include <android-base/parseint.h>
#include <android-base/strings.h>
#include <libdm/dm.h>
@ -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

View file

@ -24,6 +24,7 @@
#include <chrono>
#include <ctime>
#include <iostream>
#include <map>
#include <thread>
@ -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<DmTargetTypeInfo> targets;
ASSERT_TRUE(dm.GetAvailableTargets(&targets));
map<string, DmTargetTypeInfo> 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<LoopDevice> base_loop_;
unique_ptr<LoopDevice> cow_loop_;
unique_ptr<TempDevice> origin_dev_;
unique_ptr<TempDevice> 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<LoopDevice>(base_fd_);
ASSERT_TRUE(base_loop_->valid());
cow_loop_ = std::make_unique<LoopDevice>(cow_fd_);
ASSERT_TRUE(cow_loop_->valid());
DmTable origin_table;
ASSERT_TRUE(origin_table.AddTarget(make_unique<DmTargetSnapshotOrigin>(
0, kBaseDeviceSize / kSectorSize, base_loop_->device())));
ASSERT_TRUE(origin_table.valid());
origin_dev_ = std::make_unique<TempDevice>("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<DmTargetSnapshot>(
0, kBaseDeviceSize / kSectorSize, base_loop_->device(), cow_loop_->device(),
SnapshotStorageMode::Persistent, 8)));
ASSERT_TRUE(snap_table.valid());
snapshot_dev_ = std::make_unique<TempDevice>("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<DmTargetSnapshot>(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<DeviceMapper::TargetInfo> 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<DeviceMapper::TargetInfo> 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");
}
}

View file

@ -96,6 +96,10 @@ class DeviceMapper final {
// successfully read and stored in 'targets'. Returns 'false' otherwise.
bool GetAvailableTargets(std::vector<DmTargetTypeInfo>* 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

View file

@ -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

View file

@ -38,15 +38,7 @@
#include <vector>
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 <dm-name>" << std::endl;
std::cerr << " list <devices | targets> [-v]" << std::endl;
std::cerr << " getpath <dm-name>" << std::endl;
std::cerr << " status <dm-name>" << std::endl;
std::cerr << " table <dm-name>" << 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<DmTargetBow>(start_sector, num_sectors, block_device);
} else if (target_type == "snapshot-origin") {
if (!HasArgs(1)) {
std::cerr << "Expected \"snapshot-origin\" <block_device>" << std::endl;
return nullptr;
}
std::string block_device = NextArg();
return std::make_unique<DmTargetSnapshotOrigin>(start_sector, num_sectors,
block_device);
} else if (target_type == "snapshot") {
if (!HasArgs(4)) {
std::cerr
<< "Expected \"snapshot\" <block_device> <block_device> <mode> <chunk_size>"
<< 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<DmTargetSnapshot>(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\" <block_device> <block_device> <chunk_size>"
<< 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<DmTargetSnapshot>(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<DeviceMapper::TargetInfo> 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<std::string, std::function<int(int, char**)>> cmdmap = {
// clang-format off
{"create", DmCreateCmdHandler},
@ -341,6 +407,7 @@ static std::map<std::string, std::function<int(int, char**)>> cmdmap = {
{"help", HelpCmdHandler},
{"getpath", GetPathCmdHandler},
{"table", TableCmdHandler},
{"status", StatusCmdHandler},
// clang-format on
};