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
diff --git a/fs_mgr/libdm/dm.cpp b/fs_mgr/libdm/dm.cpp
index a4614d0..c2917a4 100644
--- a/fs_mgr/libdm/dm.cpp
+++ b/fs_mgr/libdm/dm.cpp
@@ -210,6 +210,20 @@
     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();
 
diff --git a/fs_mgr/libdm/dm_target.cpp b/fs_mgr/libdm/dm_target.cpp
index cb33eea..f440e6d 100644
--- a/fs_mgr/libdm/dm_target.cpp
+++ b/fs_mgr/libdm/dm_target.cpp
@@ -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 @@
     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 70823c6..72a0e11 100644
--- a/fs_mgr/libdm/dm_test.cpp
+++ b/fs_mgr/libdm/dm_test.cpp
@@ -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 @@
             "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");
+    }
+}
diff --git a/fs_mgr/libdm/include/libdm/dm.h b/fs_mgr/libdm/include/libdm/dm.h
index 28e6e01..d7e8aa9 100644
--- a/fs_mgr/libdm/include/libdm/dm.h
+++ b/fs_mgr/libdm/include/libdm/dm.h
@@ -96,6 +96,10 @@
     // 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
diff --git a/fs_mgr/libdm/include/libdm/dm_target.h b/fs_mgr/libdm/include/libdm/dm_target.h
index 175b0f0..fce1175 100644
--- a/fs_mgr/libdm/include/libdm/dm_target.h
+++ b/fs_mgr/libdm/include/libdm/dm_target.h
@@ -40,6 +40,18 @@
         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 @@
     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 9309aad..7e6ad5b 100644
--- a/fs_mgr/tools/dmctl.cpp
+++ b/fs_mgr/tools/dmctl.cpp
@@ -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 @@
     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 @@
             }
             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 @@
     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 @@
 
     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 @@
     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 @@
         {"help", HelpCmdHandler},
         {"getpath", GetPathCmdHandler},
         {"table", TableCmdHandler},
+        {"status", StatusCmdHandler},
         // clang-format on
 };