blob: 6b2d3cc90556ecf562cb118c99c1c9d83a5ca585 [file] [log] [blame] [edit]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <spaced/disk_usage_impl.h>
#include <fcntl.h>
#include <sys/quota.h>
#include <sys/statvfs.h>
#include <optional>
#include <string>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <base/logging.h>
#include <base/strings/stringprintf.h>
#include <brillo/blkdev_utils/mock_lvm.h>
#include <spaced/proto_bindings/spaced.pb.h>
extern "C" {
#include <linux/fs.h>
}
using brillo::FakeRunDmStatusIoctl;
using testing::_;
using testing::DoAll;
using testing::Return;
using testing::SetArgPointee;
namespace spaced {
namespace {
// ~3% of blocks are allocated.
constexpr const char kSampleReport[] =
"3 1/24 8/256 - rw discard_passdown "
"queue_if_no_space - 1024";
constexpr char kQuotaSamplePath[] = "/home/user/chronos";
} // namespace
class DiskUsageUtilMock : public DiskUsageUtilImpl {
public:
DiskUsageUtilMock(struct statvfs st, std::optional<brillo::Thinpool> thinpool)
: DiskUsageUtilImpl(base::FilePath("/dev/foo"), thinpool), st_(st) {}
protected:
int StatVFS(const base::FilePath& path, struct statvfs* st) override {
memcpy(st, &st_, sizeof(struct statvfs));
return !st_.f_fsid;
}
private:
struct statvfs st_;
};
TEST(DiskUsageUtilTest, FailedVfsCall) {
struct statvfs st = {};
DiskUsageUtilMock disk_usage_mock(st, std::nullopt);
base::FilePath path("/foo/bar");
EXPECT_EQ(disk_usage_mock.GetFreeDiskSpace(path), -1);
EXPECT_EQ(disk_usage_mock.GetTotalDiskSpace(path), -1);
}
TEST(DiskUsageUtilTest, FilesystemData) {
struct statvfs st = {};
st.f_fsid = 1;
st.f_bavail = 1024;
st.f_blocks = 2048;
st.f_frsize = 4096;
DiskUsageUtilMock disk_usage_mock(st, std::nullopt);
base::FilePath path("/foo/bar");
EXPECT_EQ(disk_usage_mock.GetFreeDiskSpace(path), 4194304);
EXPECT_EQ(disk_usage_mock.GetTotalDiskSpace(path), 8388608);
}
TEST(DiskUsageUtilTest, ThinProvisionedVolume) {
struct statvfs st = {};
st.f_fsid = 1;
st.f_bavail = 1024;
st.f_blocks = 2048;
st.f_frsize = 4096;
auto lvm_command_runner = std::make_shared<brillo::MockLvmCommandRunner>();
brillo::Thinpool thinpool("thinpool", "STATEFUL", lvm_command_runner);
DiskUsageUtilMock disk_usage_mock(st, thinpool);
base::FilePath path("/foo/bar");
std::vector<std::string> cmd = {"/sbin/dmsetup", "status", "--noflush",
"STATEFUL-thinpool-tpool"};
auto fn = FakeRunDmStatusIoctl(0, 32768, kSampleReport);
EXPECT_CALL(*lvm_command_runner.get(), RunDmIoctl(_, _))
.WillRepeatedly(testing::Invoke(fn));
// With only 3% of the thinpool occupied, disk usage should use the
// filesystem data for free space.
EXPECT_EQ(disk_usage_mock.GetFreeDiskSpace(path), 4194304);
EXPECT_EQ(disk_usage_mock.GetTotalDiskSpace(path), 8388608);
}
TEST(DiskUsageUtilTest, ThinProvisionedVolumeLowDiskSpace) {
struct statvfs st = {};
st.f_fsid = 1;
st.f_bavail = 1024;
st.f_blocks = 2048;
st.f_frsize = 4096;
auto lvm_command_runner = std::make_shared<brillo::MockLvmCommandRunner>();
brillo::Thinpool thinpool("thinpool", "STATEFUL", lvm_command_runner);
DiskUsageUtilMock disk_usage_mock(st, thinpool);
base::FilePath path("/foo/bar");
std::vector<std::string> cmd = {"/sbin/dmsetup", "status", "--noflush",
"STATEFUL-thinpool-tpool"};
auto fn = FakeRunDmStatusIoctl(0, 32768, kSampleReport);
EXPECT_CALL(*lvm_command_runner.get(), RunDmIoctl(_, _))
.WillRepeatedly(testing::Invoke(fn));
EXPECT_EQ(disk_usage_mock.GetFreeDiskSpace(path), 4194304);
EXPECT_EQ(disk_usage_mock.GetTotalDiskSpace(path), 8388608);
}
TEST(DiskUsageUtilTest, OverprovisionedVolumeSpace) {
struct statvfs st = {};
st.f_fsid = 1;
st.f_bavail = 1024;
st.f_blocks = 9192;
st.f_frsize = 4096;
auto lvm_command_runner = std::make_shared<brillo::MockLvmCommandRunner>();
brillo::Thinpool thinpool("thinpool", "STATEFUL", lvm_command_runner);
DiskUsageUtilMock disk_usage_mock(st, thinpool);
base::FilePath path("/foo/bar");
std::vector<std::string> cmd = {"/sbin/dmsetup", "status", "--noflush",
"STATEFUL-thinpool-tpool"};
auto fn = FakeRunDmStatusIoctl(0, 32768, kSampleReport);
EXPECT_CALL(*lvm_command_runner.get(), RunDmIoctl(_, _))
.WillRepeatedly(testing::Invoke(fn));
EXPECT_EQ(disk_usage_mock.GetFreeDiskSpace(path), 4194304);
EXPECT_EQ(disk_usage_mock.GetTotalDiskSpace(path), 16777216);
}
class DiskUsageRootdevMock : public DiskUsageUtilImpl {
public:
DiskUsageRootdevMock(int64_t size, const base::FilePath& path)
: DiskUsageUtilImpl(path, std::nullopt),
rootdev_size_(size),
rootdev_path_(path) {}
protected:
int64_t GetBlockDeviceSize(const base::FilePath& device) override {
// At the moment, only the root device size is queried from spaced.
// Once more block devices are queried, move this into a map.
if (device == rootdev_path_)
return rootdev_size_;
return -1;
}
private:
int64_t rootdev_size_;
base::FilePath rootdev_path_;
};
TEST(DiskUsageUtilTest, InvalidRootDeviceTest) {
DiskUsageRootdevMock disk_usage_mock(0, base::FilePath("/dev/foo"));
EXPECT_EQ(disk_usage_mock.GetRootDeviceSize(), 0);
}
TEST(DiskUsageUtilTest, RootDeviceSizeTest) {
DiskUsageRootdevMock disk_usage_mock(500, base::FilePath("/dev/foo"));
EXPECT_EQ(disk_usage_mock.GetRootDeviceSize(), 500);
}
class DiskUsageQuotaMock : public DiskUsageUtilImpl {
public:
explicit DiskUsageQuotaMock(const base::FilePath& home_device)
: DiskUsageUtilImpl(base::FilePath("/dev/foo"), std::nullopt),
home_device_(home_device) {}
void set_current_space_for_uid(uint32_t uid, uint64_t space) {
uid_to_current_space_[uid] = space;
}
void set_current_space_for_gid(uint32_t gid, uint64_t space) {
gid_to_current_space_[gid] = space;
}
void set_current_space_for_project_id(uint32_t project_id, uint64_t space) {
project_id_to_current_space_[project_id] = space;
}
int Ioctl(int fd, uint32_t request, void* ptr) override {
switch (request) {
case FS_IOC_FSGETXATTR: {
auto iter = fd_to_project_id_.find(fd);
if (iter == fd_to_project_id_.end()) {
(reinterpret_cast<struct fsxattr*>(ptr))->fsx_projid = 0;
return 0;
}
(reinterpret_cast<struct fsxattr*>(ptr))->fsx_projid = iter->second;
return 0;
}
case FS_IOC_FSSETXATTR:
fd_to_project_id_[fd] =
(reinterpret_cast<struct fsxattr*>(ptr))->fsx_projid;
return 0;
case FS_IOC_GETFLAGS: {
auto iter = fd_to_ext_flags_.find(fd);
if (iter == fd_to_ext_flags_.end()) {
*(reinterpret_cast<int*>(ptr)) = 0;
return 0;
}
*(reinterpret_cast<int*>(ptr)) = iter->second;
return 0;
}
case FS_IOC_SETFLAGS:
fd_to_ext_flags_[fd] = *(reinterpret_cast<int*>(ptr));
return 0;
default:
LOG(ERROR) << "Unsupported request in ioctl: " << request;
return -1;
}
}
protected:
base::FilePath GetDevice(const base::FilePath& path) override {
return home_device_;
}
int QuotaCtl(int cmd,
const base::FilePath& device,
int id,
struct dqblk* dq) override {
dq->dqb_curspace = 0;
std::map<uint32_t, uint64_t>* current_space_map;
if (cmd == QCMD(Q_GETQUOTA, USRQUOTA)) {
current_space_map = &uid_to_current_space_;
} else if (cmd == QCMD(Q_GETQUOTA, GRPQUOTA)) {
current_space_map = &gid_to_current_space_;
} else if (cmd == QCMD(Q_GETQUOTA, PRJQUOTA)) {
current_space_map = &project_id_to_current_space_;
} else {
return -1;
}
auto iter = current_space_map->find(id);
if (iter == current_space_map->end()) {
return -1;
}
dq->dqb_curspace = iter->second;
return 0;
}
private:
base::FilePath home_device_;
std::map<uint32_t, uint64_t> uid_to_current_space_;
std::map<uint32_t, uint64_t> gid_to_current_space_;
std::map<uint32_t, uint64_t> project_id_to_current_space_;
std::map<int, int> fd_to_project_id_;
std::map<int, int> fd_to_ext_flags_;
};
TEST(DiskUsageUtilTest, QuotaNotSupportedWhenPathNotMounted) {
DiskUsageQuotaMock disk_usage_mock(base::FilePath(""));
base::FilePath path(kQuotaSamplePath);
EXPECT_FALSE(disk_usage_mock.IsQuotaSupported(path));
EXPECT_EQ(disk_usage_mock.GetQuotaCurrentSpaceForUid(path, 0), -1);
EXPECT_EQ(disk_usage_mock.GetQuotaCurrentSpaceForUid(path, 1), -1);
EXPECT_EQ(disk_usage_mock.GetQuotaCurrentSpaceForGid(path, 2), -1);
EXPECT_EQ(disk_usage_mock.GetQuotaCurrentSpaceForProjectId(path, 3), -1);
GetQuotaCurrentSpacesForIdsReply reply =
disk_usage_mock.GetQuotaCurrentSpacesForIds(path, {1, 2}, {10, 11},
{100, 101});
EXPECT_TRUE(reply.curspaces_for_uids().empty());
EXPECT_TRUE(reply.curspaces_for_gids().empty());
EXPECT_TRUE(reply.curspaces_for_project_ids().empty());
}
TEST(DiskUsageUtilTest, QuotaNotSupportedWhenPathNotMountedWithQuotaOption) {
DiskUsageQuotaMock disk_usage_mock(base::FilePath("/dev/bar"));
base::FilePath path(kQuotaSamplePath);
// Current disk space for UID 0 is undefined.
EXPECT_FALSE(disk_usage_mock.IsQuotaSupported(path));
}
TEST(DiskUsageUtilTest, QuotaSupported) {
DiskUsageQuotaMock disk_usage_mock(base::FilePath("/dev/bar"));
base::FilePath path(kQuotaSamplePath);
disk_usage_mock.set_current_space_for_uid(0, 1);
disk_usage_mock.set_current_space_for_uid(1, 10);
disk_usage_mock.set_current_space_for_gid(10, 20);
disk_usage_mock.set_current_space_for_project_id(100, 30);
EXPECT_TRUE(disk_usage_mock.IsQuotaSupported(path));
EXPECT_EQ(disk_usage_mock.GetQuotaCurrentSpaceForUid(path, 1), 10);
EXPECT_EQ(disk_usage_mock.GetQuotaCurrentSpaceForUid(path, 2), -1);
EXPECT_EQ(disk_usage_mock.GetQuotaCurrentSpaceForGid(path, 10), 20);
EXPECT_EQ(disk_usage_mock.GetQuotaCurrentSpaceForGid(path, 11), -1);
EXPECT_EQ(disk_usage_mock.GetQuotaCurrentSpaceForProjectId(path, 100), 30);
EXPECT_EQ(disk_usage_mock.GetQuotaCurrentSpaceForProjectId(path, 101), -1);
GetQuotaCurrentSpacesForIdsReply reply =
disk_usage_mock.GetQuotaCurrentSpacesForIds(path, {1, 2}, {10, 11},
{100, 101});
EXPECT_EQ(reply.curspaces_for_uids().count(0), 0);
EXPECT_EQ(reply.curspaces_for_uids().at(1), 10);
EXPECT_EQ(reply.curspaces_for_uids().at(2), -1);
EXPECT_EQ(reply.curspaces_for_gids().count(1), 0);
EXPECT_EQ(reply.curspaces_for_gids().at(10), 20);
EXPECT_EQ(reply.curspaces_for_gids().at(11), -1);
EXPECT_EQ(reply.curspaces_for_project_ids().count(2), 0);
EXPECT_EQ(reply.curspaces_for_project_ids().at(100), 30);
EXPECT_EQ(reply.curspaces_for_project_ids().at(101), -1);
}
TEST(DiskUsageUtilTest, SetProjectId) {
DiskUsageQuotaMock disk_usage_mock(base::FilePath("/dev/foo"));
const base::ScopedFD fd(open("/dev/null", O_RDONLY));
constexpr int kProjectId = 1003;
// Set the project ID.
int error = 0;
ASSERT_TRUE(disk_usage_mock.SetProjectId(fd, kProjectId, &error));
// Verify that the fd has the expected project ID.
struct fsxattr fsx_out = {};
ASSERT_EQ(disk_usage_mock.Ioctl(fd.get(), FS_IOC_FSGETXATTR, &fsx_out), 0);
EXPECT_EQ(fsx_out.fsx_projid, kProjectId);
}
TEST(DiskUsageUtilTest, SetProjectInheritanceFlag) {
DiskUsageQuotaMock disk_usage_mock(base::FilePath("/dev/foo"));
const base::ScopedFD fd(open("/dev/null", O_RDONLY));
constexpr int kOriginalFlags = FS_ENCRYPT_FL | FS_EXTENT_FL;
int old_flags = kOriginalFlags;
// Set FS_ENCRYPT_FL and FS_EXTENT_FL to the file.
ASSERT_EQ(disk_usage_mock.Ioctl(fd.get(), FS_IOC_SETFLAGS, &old_flags), 0);
// Set the project inheritance flag.
int error = 0;
ASSERT_TRUE(
disk_usage_mock.SetProjectInheritanceFlag(fd, true /* enable */, &error));
// Check that the flag is enabled and the original flags are preserved.
int flags = 0;
ASSERT_EQ(disk_usage_mock.Ioctl(fd.get(), FS_IOC_GETFLAGS, &flags), 0);
EXPECT_EQ(flags, kOriginalFlags | FS_PROJINHERIT_FL);
// Unset the project inheritance flag and check the flags.
ASSERT_TRUE(disk_usage_mock.SetProjectInheritanceFlag(fd, false /* enable */,
&error));
flags = 0;
ASSERT_EQ(disk_usage_mock.Ioctl(fd.get(), FS_IOC_GETFLAGS, &flags), 0);
EXPECT_EQ(flags, kOriginalFlags);
}
} // namespace spaced