diagnostics: Populate device purpose

Storage team is working on allowing second namespace on new nvme device
to play various roles, such as swap for example. We want to distinguish
the different apparent devices and know their role.

BUG=b:151176984
TEST=unittest and `cros-health-tool telem --category=storage`

Change-Id: I9e5a7b1887a6a12e18d120bacca5d5ee9badbe1c
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/2241226
Commit-Queue: Daniil Lunev <dlunev@chromium.org>
Tested-by: Daniil Lunev <dlunev@chromium.org>
Reviewed-by: Jorge Lucangeli Obes <jorgelo@chromium.org>
Reviewed-by: Paul Moy <pmoy@chromium.org>
diff --git a/diagnostics/cros_healthd/fetchers/disk_fetcher.cc b/diagnostics/cros_healthd/fetchers/disk_fetcher.cc
index dabcf42..df8e329 100644
--- a/diagnostics/cros_healthd/fetchers/disk_fetcher.cc
+++ b/diagnostics/cros_healthd/fetchers/disk_fetcher.cc
@@ -13,6 +13,8 @@
 #include "diagnostics/cros_healthd/utils/error_utils.h"
 #include "diagnostics/cros_healthd/utils/storage/device_lister.h"
 #include "diagnostics/cros_healthd/utils/storage/device_manager.h"
+#include "diagnostics/cros_healthd/utils/storage/device_resolver.h"
+#include "diagnostics/cros_healthd/utils/storage/status_macros.h"
 #include "diagnostics/cros_healthd/utils/storage/statusor.h"
 
 namespace diagnostics {
@@ -44,17 +46,15 @@
 
 }  // namespace
 
-DiskFetcher::DiskFetcher() = default;
-DiskFetcher::~DiskFetcher() = default;
-
-Status DiskFetcher::InitManager() {
+Status DiskFetcher::InitManager(const base::FilePath& root) {
   auto udev = brillo::Udev::Create();
   if (!udev)
     return Status(StatusCode::kInternal, "Unable to create udev interface");
 
-  manager_.reset(
-      new StorageDeviceManager(std::make_unique<StorageDeviceLister>(),
-                               std::move(udev), std::make_unique<Platform>()));
+  ASSIGN_OR_RETURN(auto resolver, StorageDeviceResolver::Create(root));
+  manager_.reset(new StorageDeviceManager(
+      std::make_unique<StorageDeviceLister>(), std::move(resolver),
+      std::move(udev), std::make_unique<Platform>()));
 
   return Status::OkStatus();
 }
@@ -62,13 +62,15 @@
 mojo_ipc::NonRemovableBlockDeviceResultPtr
 DiskFetcher::FetchNonRemovableBlockDevicesInfo(const base::FilePath& root) {
   if (!manager_) {
-    auto status = InitManager();
+    auto status = InitManager(root);
     if (!status.ok())
       return StatusToProbeError(status);
   }
+
   StatusOr<std::vector<
       chromeos::cros_healthd::mojom::NonRemovableBlockDeviceInfoPtr>>
       devices_or = manager_->FetchDevicesInfo(root);
+
   if (devices_or.ok()) {
     return mojo_ipc::NonRemovableBlockDeviceResult::NewBlockDeviceInfo(
         std::move(devices_or.value()));
diff --git a/diagnostics/cros_healthd/fetchers/disk_fetcher.h b/diagnostics/cros_healthd/fetchers/disk_fetcher.h
index 9009597..e03a26a 100644
--- a/diagnostics/cros_healthd/fetchers/disk_fetcher.h
+++ b/diagnostics/cros_healthd/fetchers/disk_fetcher.h
@@ -18,10 +18,10 @@
 // cros_healthd.
 class DiskFetcher {
  public:
-  DiskFetcher();
+  DiskFetcher() = default;
+  ~DiskFetcher() = default;
   DiskFetcher(const DiskFetcher&) = delete;
   DiskFetcher& operator=(const DiskFetcher&) = delete;
-  ~DiskFetcher();
 
   // Returns a structure with either the device's non-removable block device
   // info or the error that occurred fetching the information.
@@ -31,7 +31,7 @@
  private:
   std::unique_ptr<StorageDeviceManager> manager_;
 
-  Status InitManager();
+  Status InitManager(const base::FilePath& root);
 };
 
 }  // namespace diagnostics
diff --git a/diagnostics/cros_healthd/utils/storage/BUILD.gn b/diagnostics/cros_healthd/utils/storage/BUILD.gn
index 1d28ea3..e1bd4d0 100644
--- a/diagnostics/cros_healthd/utils/storage/BUILD.gn
+++ b/diagnostics/cros_healthd/utils/storage/BUILD.gn
@@ -16,13 +16,17 @@
     "//diagnostics/common:libcommon",
     "//diagnostics/mojo:cros_healthd_mojo_bindings",
   ]
-  libs = [ "rootdev" ]
+  libs = [
+    "mount",
+    "rootdev",
+  ]
   sources = [
     "caching_device_adapter.cc",
     "default_device_adapter.cc",
     "device_info.cc",
     "device_lister.cc",
     "device_manager.cc",
+    "device_resolver.cc",
     "disk_iostat.cc",
     "emmc_device_adapter.cc",
     "nvme_device_adapter.cc",
@@ -40,6 +44,7 @@
       "device_info_test.cc",
       "device_lister_test.cc",
       "device_manager_test.cc",
+      "device_resolver_test.cc",
       "disk_iostat_test.cc",
       "emmc_device_adapter_test.cc",
       "nvme_device_adapter_test.cc",
diff --git a/diagnostics/cros_healthd/utils/storage/device_info.cc b/diagnostics/cros_healthd/utils/storage/device_info.cc
index b61e6eb..75511e6 100644
--- a/diagnostics/cros_healthd/utils/storage/device_info.cc
+++ b/diagnostics/cros_healthd/utils/storage/device_info.cc
@@ -74,11 +74,13 @@
     const base::FilePath& dev_sys_path,
     const base::FilePath& dev_node_path,
     const std::string& subsystem,
+    mojo_ipc::StorageDevicePurpose purpose,
     std::unique_ptr<StorageDeviceAdapter> adapter,
     const Platform* platform)
     : dev_sys_path_(dev_sys_path),
       dev_node_path_(dev_node_path),
       subsystem_(subsystem),
+      purpose_(purpose),
       adapter_(std::move(adapter)),
       platform_(platform),
       iostat_(dev_sys_path) {
@@ -90,12 +92,14 @@
     const base::FilePath& dev_sys_path,
     const base::FilePath& dev_node_path,
     const std::string& subsystem,
+    mojo_ipc::StorageDevicePurpose purpose,
     const Platform* platform) {
   auto adapter = CreateAdapter(dev_sys_path, subsystem);
   if (!adapter)
     return nullptr;
-  return std::unique_ptr<StorageDeviceInfo>(new StorageDeviceInfo(
-      dev_sys_path, dev_node_path, subsystem, std::move(adapter), platform));
+  return std::unique_ptr<StorageDeviceInfo>(
+      new StorageDeviceInfo(dev_sys_path, dev_node_path, subsystem, purpose,
+                            std::move(adapter), platform));
 }
 
 Status StorageDeviceInfo::PopulateDeviceInfo(
@@ -104,6 +108,7 @@
 
   output_info->path = dev_node_path_.value();
   output_info->type = subsystem_;
+  output_info->purpose = purpose_;
 
   ASSIGN_OR_RETURN(output_info->size,
                    platform_->GetDeviceSizeBytes(dev_node_path_));
diff --git a/diagnostics/cros_healthd/utils/storage/device_info.h b/diagnostics/cros_healthd/utils/storage/device_info.h
index e158095..e1940fc 100644
--- a/diagnostics/cros_healthd/utils/storage/device_info.h
+++ b/diagnostics/cros_healthd/utils/storage/device_info.h
@@ -30,6 +30,7 @@
       const base::FilePath& dev_sys_path,
       const base::FilePath& dev_node_path,
       const std::string& subsystem,
+      chromeos::cros_healthd::mojom::StorageDevicePurpose purpose,
       const Platform* platform);
 
   // PopulateDeviceInfo fills the fields of Mojo's data structure representing
@@ -48,6 +49,7 @@
   const base::FilePath dev_sys_path_;
   const base::FilePath dev_node_path_;
   const std::string subsystem_;
+  const chromeos::cros_healthd::mojom::StorageDevicePurpose purpose_;
   const std::unique_ptr<const StorageDeviceAdapter> adapter_;
   // platform_ is owned by the StorageDeviceManager.
   const Platform* platform_;
@@ -57,6 +59,7 @@
   StorageDeviceInfo(const base::FilePath& dev_sys_path,
                     const base::FilePath& dev_node_path,
                     const std::string& subsystem,
+                    chromeos::cros_healthd::mojom::StorageDevicePurpose purpose,
                     std::unique_ptr<StorageDeviceAdapter> adapter,
                     const Platform* platform);
   StorageDeviceInfo(const StorageDeviceInfo&) = delete;
diff --git a/diagnostics/cros_healthd/utils/storage/device_info_test.cc b/diagnostics/cros_healthd/utils/storage/device_info_test.cc
index 68498cb..d1127ad 100644
--- a/diagnostics/cros_healthd/utils/storage/device_info_test.cc
+++ b/diagnostics/cros_healthd/utils/storage/device_info_test.cc
@@ -25,13 +25,15 @@
 
 namespace mojo_ipc = ::chromeos::cros_healthd::mojom;
 
-}
+}  // namespace
 
 TEST(StorageDeviceInfoTest, PopulateTest) {
   constexpr char kPath[] =
       "cros_healthd/utils/storage/testdata/sys/block/nvme0n1";
   constexpr char kDevnode[] = "dev/node/path";
   constexpr char kSubsystem[] = "block:nvme";
+  constexpr mojo_ipc::StorageDevicePurpose kPurpose =
+      mojo_ipc::StorageDevicePurpose::kSwapDevice;
   constexpr uint64_t kSize = 16 * 1024;
   constexpr uint64_t kBlockSize = 512;
   auto mock_platform = std::make_unique<StrictMock<MockPlatform>>();
@@ -43,7 +45,7 @@
 
   auto dev_info =
       StorageDeviceInfo::Create(base::FilePath(kPath), base::FilePath(kDevnode),
-                                kSubsystem, mock_platform.get());
+                                kSubsystem, kPurpose, mock_platform.get());
   mojo_ipc::NonRemovableBlockDeviceInfo info;
   EXPECT_TRUE(dev_info->PopulateDeviceInfo(&info).ok());
 
@@ -62,6 +64,7 @@
   EXPECT_EQ(0x13, info.revision->get_nvme_pcie_rev());
   EXPECT_EQ("test_nvme_model", info.name);
   EXPECT_EQ(0x5645525F54534554, info.firmware_version->get_nvme_firmware_rev());
+  EXPECT_EQ(kPurpose, info.purpose);
 }
 
 TEST(StorageDeviceInfoTest, PopulateLegacyTest) {
@@ -69,11 +72,13 @@
       "cros_healthd/utils/storage/testdata/sys/block/mmcblk0";
   constexpr char kDevnode[] = "dev/node/path";
   constexpr char kSubsystem[] = "block:mmc";
+  constexpr mojo_ipc::StorageDevicePurpose kPurpose =
+      mojo_ipc::StorageDevicePurpose::kBootDevice;
   auto mock_platform = std::make_unique<StrictMock<MockPlatform>>();
 
   auto dev_info =
       StorageDeviceInfo::Create(base::FilePath(kPath), base::FilePath(kDevnode),
-                                kSubsystem, mock_platform.get());
+                                kSubsystem, kPurpose, mock_platform.get());
   mojo_ipc::NonRemovableBlockDeviceInfo info;
   dev_info->PopulateLegacyFields(&info);
 
diff --git a/diagnostics/cros_healthd/utils/storage/device_manager.cc b/diagnostics/cros_healthd/utils/storage/device_manager.cc
index f77aa35..9ee654c 100644
--- a/diagnostics/cros_healthd/utils/storage/device_manager.cc
+++ b/diagnostics/cros_healthd/utils/storage/device_manager.cc
@@ -34,12 +34,15 @@
 
 StorageDeviceManager::StorageDeviceManager(
     std::unique_ptr<StorageDeviceLister> device_lister,
+    std::unique_ptr<StorageDeviceResolver> device_resolver,
     std::unique_ptr<brillo::Udev> udev,
     std::unique_ptr<Platform> platform)
     : device_lister_(std::move(device_lister)),
+      device_resolver_(std::move(device_resolver)),
       udev_(std::move(udev)),
       platform_(std::move(platform)) {
   DCHECK(device_lister_);
+  DCHECK(device_resolver_);
   DCHECK(udev_);
   DCHECK(platform_);
 }
@@ -99,6 +102,7 @@
 
     auto dev_info = StorageDeviceInfo::Create(
         sys_path, base::FilePath(dev->GetDeviceNode()), subsystem,
+        device_resolver_->GetDevicePurpose(sys_path.BaseName().value()),
         platform_.get());
 
     if (!dev_info) {
diff --git a/diagnostics/cros_healthd/utils/storage/device_manager.h b/diagnostics/cros_healthd/utils/storage/device_manager.h
index 3d66051..e9cb274 100644
--- a/diagnostics/cros_healthd/utils/storage/device_manager.h
+++ b/diagnostics/cros_healthd/utils/storage/device_manager.h
@@ -18,6 +18,7 @@
 #include "diagnostics/cros_healthd/utils/error_utils.h"
 #include "diagnostics/cros_healthd/utils/storage/device_info.h"
 #include "diagnostics/cros_healthd/utils/storage/device_lister.h"
+#include "diagnostics/cros_healthd/utils/storage/device_resolver.h"
 #include "diagnostics/cros_healthd/utils/storage/statusor.h"
 
 namespace diagnostics {
@@ -26,6 +27,7 @@
 class StorageDeviceManager final {
  public:
   StorageDeviceManager(std::unique_ptr<StorageDeviceLister> device_lister,
+                       std::unique_ptr<StorageDeviceResolver> device_resolver,
                        std::unique_ptr<brillo::Udev> udev,
                        std::unique_ptr<Platform> platform);
   StorageDeviceManager(const StorageDeviceManager&) = delete;
@@ -46,7 +48,8 @@
       const base::FilePath& root) const;
 
   const std::unique_ptr<const StorageDeviceLister> device_lister_;
-  std::unique_ptr<brillo::Udev> udev_;  // Has non-const interface.
+  const std::unique_ptr<const StorageDeviceResolver> device_resolver_;
+  std::unique_ptr<brillo::Udev> udev_;  // Has non-const interface
   const std::unique_ptr<const Platform> platform_;
 
   // fetch_lock_ must be held throughout the whole fetch process.
diff --git a/diagnostics/cros_healthd/utils/storage/device_manager_test.cc b/diagnostics/cros_healthd/utils/storage/device_manager_test.cc
index b5cc513..3c10af7 100644
--- a/diagnostics/cros_healthd/utils/storage/device_manager_test.cc
+++ b/diagnostics/cros_healthd/utils/storage/device_manager_test.cc
@@ -18,10 +18,18 @@
 #include <gtest/gtest.h>
 
 #include "diagnostics/cros_healthd/utils/storage/mock/mock_device_lister.h"
+#include "diagnostics/cros_healthd/utils/storage/mock/mock_device_resolver.h"
 #include "diagnostics/cros_healthd/utils/storage/mock/mock_platform.h"
+#include "mojo/cros_healthd_probe.mojom.h"
 
 namespace diagnostics {
 
+namespace {
+
+namespace mojo_ipc = ::chromeos::cros_healthd::mojom;
+
+}  // namespace
+
 using testing::_;
 using testing::ByMove;
 using testing::DoAll;
@@ -41,6 +49,10 @@
   const std::string kBlockClass = "block";
   const std::string kNvmeClass = "nvme";
   const std::string kEmmcClass = "mmc";
+  constexpr mojo_ipc::StorageDevicePurpose kNvmePurpose =
+      mojo_ipc::StorageDevicePurpose::kSwapDevice;
+  constexpr mojo_ipc::StorageDevicePurpose kEmmcPurpose =
+      mojo_ipc::StorageDevicePurpose::kBootDevice;
   const uint64_t kNvmeSize = 1024;
   const uint64_t kEmmcSize = 768;
   const uint64_t kBlockSize = 512;
@@ -102,12 +114,18 @@
       .WillOnce(Return(ByMove(std::move(mock_emmc_udev))))
       .WillOnce(Return(ByMove(std::move(mock_nvme_udev))));
 
+  auto mock_resolver =
+      std::make_unique<StrictMock<MockStorageDeviceResolver>>();
+  EXPECT_CALL(*mock_resolver, GetDevicePurpose(_))
+      .WillOnce(Return(kNvmePurpose))
+      .WillOnce(Return(kEmmcPurpose));
+
   auto mock_lister = std::make_unique<StrictMock<MockStorageDeviceLister>>();
   EXPECT_CALL(*mock_lister, ListDevices(base::FilePath(kFakeRoot)))
       .WillRepeatedly(Return(listed));
 
-  StorageDeviceManager manager(std::move(mock_lister), std::move(mock_udev),
-                               std::move(mock_platform));
+  StorageDeviceManager manager(std::move(mock_lister), std::move(mock_resolver),
+                               std::move(mock_udev), std::move(mock_platform));
 
   // Do multiple cycles. If the device info preservation is not working,
   // the WillOnce of udev mock will fail.
diff --git a/diagnostics/cros_healthd/utils/storage/device_resolver.cc b/diagnostics/cros_healthd/utils/storage/device_resolver.cc
new file mode 100644
index 0000000..90e3c65
--- /dev/null
+++ b/diagnostics/cros_healthd/utils/storage/device_resolver.cc
@@ -0,0 +1,141 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "diagnostics/cros_healthd/utils/storage/device_resolver.h"
+
+#include <list>
+#include <memory>
+#include <string>
+#include <base/files/file_enumerator.h>
+#include <base/files/file_path.h>
+
+#include <libmount/libmount.h>
+
+#include "diagnostics/cros_healthd/utils/storage/status_macros.h"
+#include "diagnostics/cros_healthd/utils/storage/statusor.h"
+#include "mojo/cros_healthd_probe.mojom.h"
+
+namespace diagnostics {
+
+namespace {
+
+namespace mojo_ipc = ::chromeos::cros_healthd::mojom;
+
+constexpr char kDevFsPrefix[] = "/dev/";
+constexpr char kSysBlockPath[] = "sys/block/";
+constexpr char kProcSwapsPath[] = "proc/swaps";
+constexpr char kDmPrefix[] = "dm-";
+constexpr char kSlavesDir[] = "slaves/";
+
+// Callback for the libmount parser.
+int ParserErrCb(struct libmnt_table* unused, const char* filename, int line) {
+  LOG(ERROR) << filename << ": parser encountered error at line " << line;
+  return 1;
+}
+
+}  // namespace
+
+StatusOr<std::unique_ptr<StorageDeviceResolver>> StorageDeviceResolver::Create(
+    const base::FilePath& rootfs) {
+  ASSIGN_OR_RETURN(auto devs, GetSwapDevices(rootfs));
+  return std::unique_ptr<StorageDeviceResolver>(
+      new StorageDeviceResolver(devs));
+}
+
+StorageDeviceResolver::StorageDeviceResolver(
+    const std::set<std::string>& swap_backing_devices)
+    : swap_backing_devices_(swap_backing_devices) {}
+
+// GetSwapDevices parses /proc/swaps via libmount to retrieve the list of swap
+// devices and then call into a method to find out the backing physical devices.
+StatusOr<std::set<std::string>> StorageDeviceResolver::GetSwapDevices(
+    const base::FilePath& rootfs) {
+  auto table = mnt_new_table();
+  mnt_reset_table(table);
+  mnt_table_set_parser_errcb(table, ParserErrCb);
+
+  auto swaps_path = rootfs.Append(kProcSwapsPath);
+
+  if (mnt_table_parse_swaps(table, swaps_path.value().c_str()) != 0) {
+    return Status(StatusCode::kInvalidArgument,
+                  "Invalid format of " + swaps_path.value());
+  }
+
+  std::list<std::string> swaps;
+  struct libmnt_fs* fs;
+  struct libmnt_iter* itr = mnt_new_iter(MNT_ITER_FORWARD);
+
+  while (mnt_table_next_fs(table, itr, &fs) == 0) {
+    std::string swap_dev = mnt_fs_get_srcpath(fs);
+
+    // We expect devices of the format "/dev/<blah>"
+    if (swap_dev.find(kDevFsPrefix) != 0) {
+      return Status(StatusCode::kUnavailable,
+                    "Unexpected swap device location: " + swap_dev);
+    }
+    swap_dev = swap_dev.substr(std::string(kDevFsPrefix).length());
+    if (swap_dev.find("/") != std::string::npos) {
+      return Status(StatusCode::kUnavailable,
+                    "Swap device name shall not contain slashes: " + swap_dev);
+    }
+    swaps.push_back(swap_dev);
+  }
+
+  mnt_free_iter(itr);
+  mnt_free_table(table);
+
+  return ResolveDevices(rootfs, swaps);
+}
+
+// ResolveDevices determines which physical device is backing the swap device.
+// For now check only for the simplest 0-indirection, or single devmapper layer
+// for encryption and print an error in case any more complicated setup is
+// observed.
+StatusOr<std::set<std::string>> StorageDeviceResolver::ResolveDevices(
+    const base::FilePath& rootfs, const std::list<std::string>& swap_devs) {
+  std::set<std::string> result;
+  for (auto swap_dev : swap_devs) {
+    auto backing_dev = swap_dev;
+    if (swap_dev.find(kDmPrefix) == 0) {
+      auto path =
+          rootfs.Append(kSysBlockPath).Append(swap_dev).Append(kSlavesDir);
+      base::FileEnumerator lister(path, false,
+                                  base::FileEnumerator::DIRECTORIES);
+
+      std::list<std::string> slaves;
+      for (base::FilePath device_path = lister.Next(); !device_path.empty();
+           device_path = lister.Next()) {
+        slaves.push_back(device_path.BaseName().value());
+      }
+
+      if (slaves.size() == 0) {
+        return Status(StatusCode::kUnavailable,
+                      "No physical backing devices found for: " + backing_dev);
+      }
+      if (slaves.size() > 1) {
+        return Status(
+            StatusCode::kUnavailable,
+            "Too many physical backing devices found for: " + backing_dev);
+      }
+
+      backing_dev = slaves.front();
+      if (backing_dev.find(kDmPrefix) == 0) {
+        return Status(StatusCode::kUnavailable,
+                      "Multiple devmapper layers found for: " + backing_dev);
+      }
+    }
+    result.insert(backing_dev);
+  }
+  return result;
+}
+
+mojo_ipc::StorageDevicePurpose StorageDeviceResolver::GetDevicePurpose(
+    const std::string& dev_name) const {
+  if (swap_backing_devices_.find(dev_name) != swap_backing_devices_.end()) {
+    return mojo_ipc::StorageDevicePurpose::kSwapDevice;
+  }
+  return mojo_ipc::StorageDevicePurpose::kBootDevice;
+}
+
+}  // namespace diagnostics
diff --git a/diagnostics/cros_healthd/utils/storage/device_resolver.h b/diagnostics/cros_healthd/utils/storage/device_resolver.h
new file mode 100644
index 0000000..7536187
--- /dev/null
+++ b/diagnostics/cros_healthd/utils/storage/device_resolver.h
@@ -0,0 +1,48 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef DIAGNOSTICS_CROS_HEALTHD_UTILS_STORAGE_DEVICE_RESOLVER_H_
+#define DIAGNOSTICS_CROS_HEALTHD_UTILS_STORAGE_DEVICE_RESOLVER_H_
+
+#include <list>
+#include <memory>
+#include <set>
+#include <string>
+
+#include <base/files/file_path.h>
+
+#include "diagnostics/cros_healthd/utils/storage/statusor.h"
+#include "mojo/cros_healthd_probe.mojom.h"
+
+namespace diagnostics {
+
+// Resolves the purpose of the device.
+class StorageDeviceResolver {
+ public:
+  static StatusOr<std::unique_ptr<StorageDeviceResolver>> Create(
+      const base::FilePath& rootfs);
+
+  virtual ~StorageDeviceResolver() = default;
+
+  virtual chromeos::cros_healthd::mojom::StorageDevicePurpose GetDevicePurpose(
+      const std::string& dev_name) const;
+
+ protected:
+  StorageDeviceResolver() = default;
+
+ private:
+  static StatusOr<std::set<std::string>> GetSwapDevices(
+      const base::FilePath& rootfs);
+  static StatusOr<std::set<std::string>> ResolveDevices(
+      const base::FilePath& rootfs, const std::list<std::string>& swap_devs);
+
+  explicit StorageDeviceResolver(
+      const std::set<std::string>& swap_backing_devices);
+
+  const std::set<std::string> swap_backing_devices_;
+};
+
+}  // namespace diagnostics
+
+#endif  // DIAGNOSTICS_CROS_HEALTHD_UTILS_STORAGE_DEVICE_RESOLVER_H_
diff --git a/diagnostics/cros_healthd/utils/storage/device_resolver_test.cc b/diagnostics/cros_healthd/utils/storage/device_resolver_test.cc
new file mode 100644
index 0000000..97caaf0
--- /dev/null
+++ b/diagnostics/cros_healthd/utils/storage/device_resolver_test.cc
@@ -0,0 +1,41 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <utility>
+
+#include "diagnostics/cros_healthd/utils/storage/device_resolver.h"
+
+#include <base/files/file_path.h>
+#include <gtest/gtest.h>
+
+#include "mojo/cros_healthd_probe.mojom.h"
+
+namespace diagnostics {
+
+namespace {
+
+namespace mojo_ipc = ::chromeos::cros_healthd::mojom;
+
+constexpr char kFakeRoot[] = "cros_healthd/utils/storage/testdata/";
+
+}  //  namespace
+
+TEST(StorageDeviceResolverTest, GoodData) {
+  auto resolver_or = StorageDeviceResolver::Create(base::FilePath(kFakeRoot));
+  ASSERT_TRUE(resolver_or.ok());
+  auto resolver = std::move(resolver_or).value();
+
+  EXPECT_EQ(mojo_ipc::StorageDevicePurpose::kBootDevice,
+            resolver->GetDevicePurpose("nvme0n1"));
+  EXPECT_EQ(mojo_ipc::StorageDevicePurpose::kSwapDevice,
+            resolver->GetDevicePurpose("nvme0n2"));
+}
+
+TEST(StorageDeviceResolverTest, MissingFile) {
+  auto resolver_or =
+      StorageDeviceResolver::Create(base::FilePath("NONSENSE PATH"));
+  ASSERT_FALSE(resolver_or.ok());
+}
+
+}  // namespace diagnostics
diff --git a/diagnostics/cros_healthd/utils/storage/mock/mock_device_resolver.h b/diagnostics/cros_healthd/utils/storage/mock/mock_device_resolver.h
new file mode 100644
index 0000000..ff4a959
--- /dev/null
+++ b/diagnostics/cros_healthd/utils/storage/mock/mock_device_resolver.h
@@ -0,0 +1,38 @@
+
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef DIAGNOSTICS_CROS_HEALTHD_UTILS_STORAGE_MOCK_MOCK_DEVICE_RESOLVER_H_
+#define DIAGNOSTICS_CROS_HEALTHD_UTILS_STORAGE_MOCK_MOCK_DEVICE_RESOLVER_H_
+
+#include <string>
+#include <vector>
+
+#include <base/files/file_path.h>
+#include <gmock/gmock.h>
+
+#include "diagnostics/cros_healthd/utils/storage/device_resolver.h"
+#include "mojo/cros_healthd_probe.mojom.h"
+
+namespace diagnostics {
+
+class MockStorageDeviceResolver : public StorageDeviceResolver {
+ public:
+  MockStorageDeviceResolver() = default;
+  MockStorageDeviceResolver(const MockStorageDeviceResolver&) = delete;
+  MockStorageDeviceResolver(MockStorageDeviceResolver&&) = delete;
+  MockStorageDeviceResolver& operator=(const MockStorageDeviceResolver&) =
+      delete;
+  MockStorageDeviceResolver& operator=(MockStorageDeviceResolver&&) = delete;
+  ~MockStorageDeviceResolver() override = default;
+
+  MOCK_METHOD(chromeos::cros_healthd::mojom::StorageDevicePurpose,
+              GetDevicePurpose,
+              (const std::string&),
+              (const, override));
+};
+
+}  // namespace diagnostics
+
+#endif  // DIAGNOSTICS_CROS_HEALTHD_UTILS_STORAGE_MOCK_MOCK_DEVICE_RESOLVER_H_
diff --git a/diagnostics/cros_healthd/utils/storage/testdata/proc/swaps b/diagnostics/cros_healthd/utils/storage/testdata/proc/swaps
new file mode 100644
index 0000000..33efc88
--- /dev/null
+++ b/diagnostics/cros_healthd/utils/storage/testdata/proc/swaps
@@ -0,0 +1,2 @@
+Filename				Type		Size	Used	Priority
+/dev/dm-2                               partition	399728636	0	-2
diff --git a/diagnostics/cros_healthd/utils/storage/testdata/sys/block/dm-2/slaves/nvme0n2 b/diagnostics/cros_healthd/utils/storage/testdata/sys/block/dm-2/slaves/nvme0n2
new file mode 120000
index 0000000..3206929
--- /dev/null
+++ b/diagnostics/cros_healthd/utils/storage/testdata/sys/block/dm-2/slaves/nvme0n2
@@ -0,0 +1 @@
+../../nvme0n2
\ No newline at end of file
diff --git a/diagnostics/mojo/cros_healthd_probe.mojom b/diagnostics/mojo/cros_healthd_probe.mojom
index 5ff52bc..fdf01a0 100644
--- a/diagnostics/mojo/cros_healthd_probe.mojom
+++ b/diagnostics/mojo/cros_healthd_probe.mojom
@@ -198,6 +198,14 @@
   uint16 other;
 };
 
+// Purpose of the storage device.
+[Extensible]
+enum StorageDevicePurpose {
+  kUnknown,
+  kBootDevice,
+  kSwapDevice,
+};
+
 // Information related to a specific non-removable block device.
 struct NonRemovableBlockDeviceInfo {
   // IO statistics
@@ -234,6 +242,8 @@
   BlockDeviceFirmware firmware_version;
   // Storage type, could be MMC / NVMe / ATA, based on udev subsystem.
   string type;
+  // Purpose of the devicem e.g. "boot", "swap"
+  StorageDevicePurpose purpose;
 
   // Additional identification.