blob: 9963e1b458aca45989f6c34012397ee6e5bc61d6 [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 <errno.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/quota.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sys/sysmacros.h>
#include <sys/types.h>
#include <algorithm>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include <base/check.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/files/scoped_file.h>
#include <base/json/json_reader.h>
#include <base/logging.h>
#include <base/posix/eintr_wrapper.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <base/values.h>
#include <brillo/blkdev_utils/get_backing_block_device.h>
#include <brillo/userdb_utils.h>
#include <rootdev/rootdev.h>
#include <spaced/proto_bindings/spaced.pb.h>
extern "C" {
#include <linux/fs.h>
}
namespace spaced {
constexpr char kProjectIdJson[] = "/etc/spaced/projects.json";
constexpr char kProcPrefix[] = "/proc";
constexpr char kDiskstatsFilename[] = "diskstats";
constexpr char kSysDevBlockPrefix[] = "/sys/dev/block";
constexpr char kStatFilename[] = "stat";
constexpr int kNumIOStatsEntries = 17;
DiskUsageUtilImpl::DiskUsageUtilImpl(const base::FilePath& rootdev,
std::optional<brillo::Thinpool> thinpool)
: rootdev_(rootdev),
thinpool_(thinpool),
proc_dir_(kProcPrefix),
sys_dev_block_dir_(kSysDevBlockPrefix) {}
DiskUsageUtilImpl::DiskUsageUtilImpl(const base::FilePath& rootdev,
std::optional<brillo::Thinpool> thinpool,
std::string proc_dir,
std::string sys_dev_block_dir)
: rootdev_(rootdev),
thinpool_(thinpool),
proc_dir_(proc_dir),
sys_dev_block_dir_(sys_dev_block_dir) {}
int DiskUsageUtilImpl::StatVFS(const base::FilePath& path, struct statvfs* st) {
return HANDLE_EINTR(statvfs(path.value().c_str(), st));
}
int DiskUsageUtilImpl::Stat(const std::string& path, struct stat* st) {
return HANDLE_EINTR(stat(path.c_str(), st));
}
int DiskUsageUtilImpl::QuotaCtl(int cmd,
const base::FilePath& device,
int id,
struct dqblk* dq) {
return quotactl(cmd, device.value().c_str(), id, reinterpret_cast<char*>(dq));
}
int DiskUsageUtilImpl::Ioctl(int fd, uint32_t request, void* ptr) {
return ioctl(fd, request, ptr);
}
base::FilePath DiskUsageUtilImpl::GetDevice(const base::FilePath& path) {
return brillo::GetBackingLogicalDeviceForFile(path);
}
int64_t DiskUsageUtilImpl::GetFreeDiskSpace(const base::FilePath& path) {
// Use statvfs() to get the free space for the given path.
struct statvfs stat;
if (StatVFS(path, &stat) != 0) {
PLOG(ERROR) << "Failed to run statvfs() on " << path;
return -1;
}
int64_t free_disk_space = static_cast<int64_t>(stat.f_bavail) * stat.f_frsize;
return free_disk_space;
}
int64_t DiskUsageUtilImpl::GetTotalDiskSpace(const base::FilePath& path) {
// Use statvfs() to get the total space for the given path.
struct statvfs stat;
if (StatVFS(path, &stat) != 0) {
PLOG(ERROR) << "Failed to run statvfs() on " << path;
return -1;
}
int64_t total_disk_space =
static_cast<int64_t>(stat.f_blocks) * stat.f_frsize;
int64_t thinpool_total_space;
if (thinpool_ && thinpool_->IsValid() &&
thinpool_->GetTotalSpace(&thinpool_total_space)) {
total_disk_space = std::min(total_disk_space, thinpool_total_space);
}
return total_disk_space;
}
int64_t DiskUsageUtilImpl::GetBlockDeviceSize(const base::FilePath& device) {
base::ScopedFD fd(HANDLE_EINTR(
open(device.value().c_str(), O_RDONLY | O_NOFOLLOW | O_CLOEXEC)));
if (!fd.is_valid()) {
PLOG(ERROR) << "open " << device.value();
return -1;
}
int64_t size;
if (Ioctl(fd.get(), BLKGETSIZE64, &size)) {
PLOG(ERROR) << "ioctl(BLKGETSIZE): " << device.value();
return -1;
}
return size;
}
int64_t DiskUsageUtilImpl::GetRootDeviceSize() {
if (rootdev_.empty()) {
LOG(WARNING) << "Failed to get root device";
return -1;
}
return GetBlockDeviceSize(rootdev_);
}
bool DiskUsageUtilImpl::IsQuotaSupported(const base::FilePath& path) {
return GetQuotaCurrentSpaceForUid(path, 0) >= 0;
}
int64_t DiskUsageUtilImpl::GetQuotaCurrentSpaceForUid(
const base::FilePath& path, uint32_t uid) {
return GetQuotaCurrentSpaceForId(path, uid, USRQUOTA);
}
int64_t DiskUsageUtilImpl::GetQuotaCurrentSpaceForGid(
const base::FilePath& path, uint32_t gid) {
return GetQuotaCurrentSpaceForId(path, gid, GRPQUOTA);
}
int64_t DiskUsageUtilImpl::GetQuotaCurrentSpaceForProjectId(
const base::FilePath& path, uint32_t project_id) {
return GetQuotaCurrentSpaceForId(path, project_id, PRJQUOTA);
}
int64_t DiskUsageUtilImpl::GetQuotaCurrentSpaceForId(const base::FilePath& path,
uint32_t id,
int quota_type) {
DCHECK(0 <= quota_type && quota_type < MAXQUOTAS)
<< "Invalid quota_type: " << quota_type;
const base::FilePath device = GetDevice(path);
if (device.empty()) {
LOG(ERROR) << "Failed to find logical device for home directory";
return -1;
}
struct dqblk dq = {};
if (QuotaCtl(QCMD(Q_GETQUOTA, quota_type), device, id, &dq) != 0) {
PLOG(ERROR) << "quotactl failed: quota_type=" << quota_type << ", id=" << id
<< ", device=" << device.value();
return -1;
}
return dq.dqb_curspace;
}
GetQuotaCurrentSpacesForIdsReply DiskUsageUtilImpl::GetQuotaCurrentSpacesForIds(
const base::FilePath& path,
const std::vector<uint32_t>& uids,
const std::vector<uint32_t>& gids,
const std::vector<uint32_t>& project_ids) {
GetQuotaCurrentSpacesForIdsReply reply;
const base::FilePath device = GetDevice(path);
if (device.empty()) {
LOG(ERROR) << "Failed to find logical device for home directory";
return reply;
}
SetQuotaCurrentSpacesForIdsMap(device, uids, USRQUOTA,
reply.mutable_curspaces_for_uids());
SetQuotaCurrentSpacesForIdsMap(device, gids, GRPQUOTA,
reply.mutable_curspaces_for_gids());
SetQuotaCurrentSpacesForIdsMap(device, project_ids, PRJQUOTA,
reply.mutable_curspaces_for_project_ids());
return reply;
}
void DiskUsageUtilImpl::SetQuotaCurrentSpacesForIdsMap(
const base::FilePath& device,
const std::vector<uint32_t>& ids,
int quota_type,
google::protobuf::Map<uint32_t, int64_t>* curspaces_for_ids) {
DCHECK(0 <= quota_type && quota_type <= MAXQUOTAS)
<< "Invalid quota_type: " << quota_type;
for (const auto& id : ids) {
struct dqblk dq = {};
if (QuotaCtl(QCMD(Q_GETQUOTA, quota_type), device, id, &dq) != 0) {
PLOG(ERROR) << "quotactl(GETQUOTA) failed: quota_type=" << quota_type
<< ", id=" << id << ", device=" << device;
(*curspaces_for_ids)[id] = -1;
} else {
(*curspaces_for_ids)[id] = dq.dqb_curspace;
}
}
}
std::vector<uid_t> DiskUsageUtilImpl::GetUsers() {
return brillo::userdb::GetUsers();
}
std::vector<gid_t> DiskUsageUtilImpl::GetGroups() {
return brillo::userdb::GetGroups();
}
std::vector<uint32_t> DiskUsageUtilImpl::GetProjectIds() {
std::vector<uint32_t> project_ids;
std::string json_string;
if (!base::ReadFileToString(base::FilePath(kProjectIdJson), &json_string)) {
PLOG(ERROR) << "Unable to read json file: " << kProjectIdJson;
return project_ids;
}
auto prj =
base::JSONReader::Read(json_string, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
if (!prj) {
LOG(ERROR) << "Failed to read json file";
return project_ids;
}
if (!prj->is_dict()) {
LOG(ERROR) << "Failed to read json file as a dictionary";
return project_ids;
}
base::Value::List* projects = prj->GetDict().FindList("projects");
if (projects == nullptr) {
LOG(ERROR) << "Failed to get project ids";
return project_ids;
}
for (const auto& prj : *projects) {
if (!prj.is_dict()) {
LOG(ERROR) << "Failed to get project information";
continue;
}
const auto& prj_dict = prj.GetDict();
const std::string* value = prj_dict.FindString("id");
const std::string* name = prj_dict.FindString("name");
if (value != nullptr && name != nullptr) {
int num = 0;
CHECK(base::StringToInt(*value, &num));
project_ids.push_back(num);
projects_[num] = *name;
}
}
return project_ids;
}
GetQuotaCurrentSpacesForIdsReply DiskUsageUtilImpl::GetQuotaOverallUsage(
const base::FilePath& path) {
GetQuotaCurrentSpacesForIdsReply reply;
std::vector<uid_t> users = GetUsers();
std::vector<gid_t> groups = GetGroups();
std::vector<uint32_t> projects = GetProjectIds();
reply = GetQuotaCurrentSpacesForIds(path, users, groups, projects);
return reply;
}
std::string DiskUsageUtilImpl::GetQuotaOverallUsagePrettyPrint(
const base::FilePath& path) {
GetQuotaCurrentSpacesForIdsReply reply;
std::string output;
reply = GetQuotaOverallUsage(path);
output.append("Users:\n");
for (auto const& usr : reply.curspaces_for_uids()) {
if (usr.second != 0) {
std::string s =
std::to_string(usr.first) + ": " + std::to_string(usr.second) + "\n";
output.append(s);
}
}
output.append("\nGroups:\n");
for (auto const& grp : reply.curspaces_for_gids()) {
if (grp.second != 0) {
std::string s =
std::to_string(grp.first) + ": " + std::to_string(grp.second) + "\n";
output.append(s);
}
}
output.append("\nProjects:\n");
for (auto const& prj : reply.curspaces_for_project_ids()) {
if (prj.second != 0) {
std::string s =
std::to_string(prj.first) + ": " + std::to_string(prj.second) + "\n";
output.append(s);
}
}
return output;
}
std::map<std::pair<uint32_t, uint32_t>, std::string>
DiskUsageUtilImpl::GetDeviceMap(const std::vector<base::FilePath>& paths) {
// Use stat() to get the major/minor numbers for the specified path.
struct stat stat;
std::map<std::pair<uint32_t, uint32_t>, std::string> dev_map;
for (auto const& path : paths) {
if (Stat(path.value(), &stat) != 0) {
PLOG(ERROR) << "Failed to run stat() on:" << path;
continue;
}
if (!S_ISDIR(stat.st_mode)) {
PLOG(ERROR) << path << " is not a directory";
continue;
}
dev_t dev_num = stat.st_dev;
unsigned int major_num = major(dev_num);
unsigned int minor_num = minor(dev_num);
if (dev_map.count({major_num, minor_num})) {
PLOG(WARNING) << "Skipping duplicate entry: " << path;
continue;
}
dev_map[{major_num, minor_num}] = path.value();
}
return dev_map;
}
void DiskUsageUtilImpl::ParseDiskIOStatsAndUpdateReply(
const std::string& name,
const std::string& stats,
GetDiskIOStatsForPathsReply* reply) {
std::stringstream stats_stream(stats);
std::vector<uint64_t> stats_list;
uint64_t value;
while (stats_stream >> value) {
stats_list.push_back(value);
}
if (stats_list.size() != kNumIOStatsEntries) {
PLOG(ERROR) << "Unable to parse I/O stats file for " << name;
return;
}
auto reply_entry = reply->add_stats_for_path();
*reply_entry->mutable_path() = name;
int index = 0;
reply_entry->mutable_stats()->set_read_ios(stats_list[index++]);
reply_entry->mutable_stats()->set_read_merges(stats_list[index++]);
reply_entry->mutable_stats()->set_read_sectors(stats_list[index++]);
reply_entry->mutable_stats()->set_read_ticks(stats_list[index++]);
reply_entry->mutable_stats()->set_write_ios(stats_list[index++]);
reply_entry->mutable_stats()->set_write_merges(stats_list[index++]);
reply_entry->mutable_stats()->set_write_sectors(stats_list[index++]);
reply_entry->mutable_stats()->set_write_ticks(stats_list[index++]);
reply_entry->mutable_stats()->set_in_flight(stats_list[index++]);
reply_entry->mutable_stats()->set_io_ticks(stats_list[index++]);
reply_entry->mutable_stats()->set_time_in_queue(stats_list[index++]);
reply_entry->mutable_stats()->set_discard_ios(stats_list[index++]);
reply_entry->mutable_stats()->set_discard_merges(stats_list[index++]);
reply_entry->mutable_stats()->set_discard_sectors(stats_list[index++]);
reply_entry->mutable_stats()->set_discard_ticks(stats_list[index++]);
reply_entry->mutable_stats()->set_flush_ios(stats_list[index++]);
reply_entry->mutable_stats()->set_flush_ticks(stats_list[index++]);
}
GetDiskIOStatsForPathsReply DiskUsageUtilImpl::GetDiskIOStatsForPaths(
const std::vector<base::FilePath>& paths) {
// Map each specified path to the corresponding device major:minor numbers.
std::map<std::pair<uint32_t, uint32_t>, std::string> dev_map =
GetDeviceMap(paths);
GetDiskIOStatsForPathsReply reply;
for (const auto& p : dev_map) {
unsigned int major_num = p.first.first;
unsigned int minor_num = p.first.second;
const std::string& name = p.second;
std::string sysfspath =
base::StringPrintf("%s/%d:%d/%s", sys_dev_block_dir_.c_str(), major_num,
minor_num, kStatFilename);
std::string stats;
if (!base::ReadFileToString(base::FilePath(sysfspath), &stats)) {
PLOG(ERROR) << "Unable to read sysfs file: " << sysfspath;
continue;
}
ParseDiskIOStatsAndUpdateReply(name, stats, &reply);
}
return reply;
}
std::string DiskUsageUtilImpl::GetDiskIOStatsForPathsPrettyPrint(
const std::string& paths) {
std::vector<base::FilePath> file_paths;
std::stringstream path_stream(paths);
std::string path;
while (std::getline(path_stream, path, ',')) {
file_paths.push_back(base::FilePath(path));
}
GetDiskIOStatsForPathsReply reply = GetDiskIOStatsForPaths(file_paths);
std::string output;
std::stringstream stats_stream;
for (auto const& entry : reply.stats_for_path()) {
stats_stream
<< std::endl
<< "Disk I/O stats for " << entry.path() << ":" << std::endl
<< "Read IOs: " << entry.stats().read_ios() << std::endl
<< "Read Merges: " << entry.stats().read_merges() << std::endl
<< "Read Sectors: " << entry.stats().read_sectors() << std::endl
<< "Read Ticks: " << entry.stats().read_ticks() << std::endl
<< "Writes IOs: " << entry.stats().write_ios() << std::endl
<< "Write Merges: " << entry.stats().write_merges() << std::endl
<< "Write Sectors: " << entry.stats().write_sectors() << std::endl
<< "Write Ticks: " << entry.stats().write_ticks() << std::endl
<< "In Flight: " << entry.stats().in_flight() << std::endl
<< "IO Ticks: " << entry.stats().io_ticks() << std::endl
<< "Time In Queue: " << entry.stats().time_in_queue() << std::endl
<< "Discard IOs: " << entry.stats().discard_ios() << std::endl
<< "Discard Merges: " << entry.stats().discard_merges() << std::endl
<< "Discard Sectors: " << entry.stats().discard_sectors() << std::endl
<< "Discard Ticks: " << entry.stats().discard_ticks() << std::endl
<< "Flush IOs: " << entry.stats().flush_ios() << std::endl
<< "Flush Ticks: " << entry.stats().flush_ticks() << std::endl;
output.append(stats_stream.str());
stats_stream.str("");
}
return output;
}
std::string DiskUsageUtilImpl::GetDiskIOStats() {
std::string output;
std::string diskstats_path =
base::StringPrintf("%s/%s", proc_dir_.c_str(), kDiskstatsFilename);
if (!base::ReadFileToString(base::FilePath(diskstats_path), &output)) {
PLOG(ERROR) << "Unable to read diskstats file: " << diskstats_path;
return "";
}
output.insert(0, "\nI/O stats for all block devices:\n");
return output;
}
bool DiskUsageUtilImpl::SetProjectId(const base::ScopedFD& fd,
uint32_t project_id,
int* out_error) {
if (!fd.is_valid()) {
*out_error = EBADF;
LOG(ERROR) << "SetProjectId: Invalid fd";
return false;
}
struct fsxattr fsx = {};
if (Ioctl(fd.get(), FS_IOC_FSGETXATTR, &fsx) < 0) {
*out_error = errno;
PLOG(ERROR) << "ioctl(FS_IOC_FSGETXATTR) failed";
return false;
}
fsx.fsx_projid = project_id;
if (Ioctl(fd.get(), FS_IOC_FSSETXATTR, &fsx) < 0) {
*out_error = errno;
PLOG(ERROR) << "ioctl(FS_IOC_FSSETXATTR) failed: project_id=" << project_id;
return false;
}
return true;
}
bool DiskUsageUtilImpl::SetProjectInheritanceFlag(const base::ScopedFD& fd,
bool enable,
int* out_error) {
if (!fd.is_valid()) {
*out_error = EBADF;
LOG(ERROR) << "SetProjectInheritanceFlag: Invalid fd";
return false;
}
uint32_t flags = 0;
if (Ioctl(fd.get(), FS_IOC_GETFLAGS, &flags) < 0) {
*out_error = errno;
PLOG(ERROR) << "ioctl(FS_IOC_GETFLAGS) failed";
return false;
}
if (enable) {
flags |= FS_PROJINHERIT_FL;
} else {
flags &= ~FS_PROJINHERIT_FL;
}
if (Ioctl(fd.get(), FS_IOC_SETFLAGS, reinterpret_cast<void*>(&flags)) < 0) {
*out_error = errno;
PLOG(ERROR) << "ioctl(FS_IOC_SETFLAGS) failed: flags=" << std::hex << flags;
return false;
}
return true;
}
} // namespace spaced