Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 29e6bf28 authored by David Anderson's avatar David Anderson
Browse files

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
parent be9c2c03
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -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();

+78 −0
Original line number Diff line number Diff line
@@ -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
+247 −10
Original line number Diff line number Diff line
@@ -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) {
    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;
    }
    DmTargetTypeInfo info;

    auto iter = by_name.find("linear");
    EXPECT_NE(iter, by_name.end());
    DeviceMapper& dm = DeviceMapper::Instance();
    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");
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -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
+71 −0
Original line number Diff line number Diff line
@@ -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

Loading