chromium/chrome/browser/ash/crostini/crostini_disk.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/crostini/crostini_disk.h"

#include <cmath>
#include <utility>

#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/ash/crostini/crostini_features.h"
#include "chrome/browser/ash/crostini/crostini_manager.h"
#include "chrome/browser/ash/crostini/crostini_simple_types.h"
#include "chrome/browser/ash/crostini/crostini_types.mojom.h"
#include "chromeos/ash/components/dbus/concierge/concierge_client.h"
#include "chromeos/ash/components/dbus/spaced/spaced_client.h"
#include "chromeos/ash/components/dbus/vm_concierge/concierge_service.pb.h"
#include "ui/base/text/bytes_formatting.h"

using DiskImageStatus = vm_tools::concierge::DiskImageStatus;

namespace {
ash::ConciergeClient* GetConciergeClient() {
  return ash::ConciergeClient::Get();
}

std::string FormatBytes(const int64_t value) {
  return base::UTF16ToUTF8(ui::FormatBytes(value));
}

void EmitResizeResultMetric(DiskImageStatus status) {
  base::UmaHistogramEnumeration(
      "Crostini.DiskResize.Result", status,
      static_cast<DiskImageStatus>(vm_tools::concierge::DiskImageStatus_MAX +
                                   1));
}

int64_t round_up(int64_t n, double increment) {
  return std::ceil(n / increment) * increment;
}

int64_t round_down(int64_t n, double increment) {
  return std::floor(n / increment) * increment;
}
}  // namespace

namespace crostini {
CrostiniDiskInfo::CrostiniDiskInfo() = default;
CrostiniDiskInfo::CrostiniDiskInfo(CrostiniDiskInfo&&) = default;
CrostiniDiskInfo& CrostiniDiskInfo::operator=(CrostiniDiskInfo&&) = default;
CrostiniDiskInfo::~CrostiniDiskInfo() = default;

namespace disk {

void GetDiskInfo(OnceDiskInfoCallback callback,
                 Profile* profile,
                 std::string vm_name,
                 bool full_info) {
  if (!CrostiniFeatures::Get()->IsEnabled(profile)) {
    std::move(callback).Run(nullptr);
    VLOG(1) << "Crostini not enabled. Nothing to do.";
    return;
  }
  if (full_info) {
    ash::SpacedClient::Get()->GetFreeDiskSpace(
        crostini::kHomeDirectory,
        base::BindOnce(&OnAmountOfFreeDiskSpace, std::move(callback), profile,
                       std::move(vm_name)));
  } else {
    // Since we only care about the disk's current size and whether it's a
    // sparse disk, we claim there's plenty of free space available to prevent
    // error conditions in |OnCrostiniSufficientlyRunning|.
    constexpr int64_t kFakeAvailableDiskBytes =
        kDiskHeadroomBytes + kRecommendedDiskSizeBytes;

    OnCrostiniSufficientlyRunning(std::move(callback), profile,
                                  std::move(vm_name), kFakeAvailableDiskBytes,
                                  CrostiniResult::SUCCESS);
  }
}

void OnAmountOfFreeDiskSpace(OnceDiskInfoCallback callback,
                             Profile* profile,
                             std::string vm_name,
                             std::optional<int64_t> free_space) {
  if (!free_space.has_value() || free_space.value() <= 0) {
    LOG(ERROR) << "Failed to get amount of free disk space";
    std::move(callback).Run(nullptr);
  } else {
    VLOG(1) << "Starting vm " << vm_name;
    auto container_id = guest_os::GuestId(kCrostiniDefaultVmType, vm_name,
                                          kCrostiniDefaultContainerName);
    CrostiniManager::RestartOptions options;
    options.start_vm_only = true;
    CrostiniManager::GetForProfile(profile)->RestartCrostiniWithOptions(
        std::move(container_id), std::move(options),
        base::BindOnce(&OnCrostiniSufficientlyRunning, std::move(callback),
                       profile, std::move(vm_name), free_space.value()));
  }
}

void OnCrostiniSufficientlyRunning(OnceDiskInfoCallback callback,
                                   Profile* profile,
                                   std::string vm_name,
                                   int64_t free_space,
                                   CrostiniResult result) {
  if (result != CrostiniResult::SUCCESS) {
    LOG(ERROR) << "Start VM: error " << static_cast<int>(result);
    std::move(callback).Run(nullptr);
  } else {
    vm_tools::concierge::ListVmDisksRequest request;
    request.set_cryptohome_id(CryptohomeIdForProfile(profile));
    request.set_storage_location(vm_tools::concierge::STORAGE_CRYPTOHOME_ROOT);
    request.set_vm_name(vm_name);
    GetConciergeClient()->ListVmDisks(
        std::move(request), base::BindOnce(&OnListVmDisks, std::move(callback),
                                           std::move(vm_name), free_space));
  }
}

void OnListVmDisks(
    OnceDiskInfoCallback callback,
    std::string vm_name,
    int64_t free_space,
    std::optional<vm_tools::concierge::ListVmDisksResponse> response) {
  if (!response) {
    LOG(ERROR) << "Failed to get response from concierge";
    std::move(callback).Run(nullptr);
    return;
  }
  if (!response->success()) {
    LOG(ERROR) << "Failed to get successful response from concierge "
               << response->failure_reason();
    std::move(callback).Run(nullptr);
    return;
  }
  auto disk_info = std::make_unique<CrostiniDiskInfo>();
  auto image = base::ranges::find(response->images(), vm_name,
                                  &vm_tools::concierge::VmDiskInfo::name);
  if (image == response->images().end()) {
    // No match found for the VM:
    LOG(ERROR) << "No VM found with name " << vm_name;
    std::move(callback).Run(nullptr);
    return;
  }
  VLOG(1) << "name: " << image->name();
  VLOG(1) << "image_type: " << image->image_type();
  VLOG(1) << "size: " << image->size();
  VLOG(1) << "user_chosen_size: " << image->user_chosen_size();
  VLOG(1) << "free_space: " << free_space;
  VLOG(1) << "min_size: " << image->min_size();

  if (image->image_type() !=
      vm_tools::concierge::DiskImageType::DISK_IMAGE_RAW) {
    // Can't resize qcow2 images and don't know how to handle auto or pluginvm
    // images.
    disk_info->can_resize = false;
    std::move(callback).Run(std::move(disk_info));
    return;
  }
  if (image->min_size() == 0) {
    VLOG(1) << "Unable to get minimum disk size. VM not running yet?";
  }
  // User has to leave at least kDiskHeadroomBytes for the host system.
  // In some cases we can be over-provisioned (e.g. we increased the headroom
  // required), when that happens the user can still go up to their currently
  // allocated size.
  int64_t max_size =
      std::max(free_space - kDiskHeadroomBytes + image->size(), image->size());
  disk_info->is_user_chosen_size = image->user_chosen_size();
  disk_info->can_resize =
      image->image_type() == vm_tools::concierge::DiskImageType::DISK_IMAGE_RAW;
  disk_info->is_low_space_available = max_size < kRecommendedDiskSizeBytes;

  const int64_t min_size =
      std::max(static_cast<int64_t>(image->min_size()), kMinimumDiskSizeBytes);
  std::vector<crostini::mojom::DiskSliderTickPtr> ticks =
      GetTicks(min_size, image->size(), max_size, &(disk_info->default_index));
  if (ticks.size() == 0) {
    LOG(ERROR) << "Unable to calculate the number of ticks for min: "
               << min_size << " current: " << image->size()
               << " max: " << max_size;
    std::move(callback).Run(nullptr);
    return;
  }
  disk_info->ticks = std::move(ticks);

  std::move(callback).Run(std::move(disk_info));
}

std::vector<crostini::mojom::DiskSliderTickPtr>
GetTicks(int64_t min, int64_t current, int64_t max, int* out_default_index) {
  if (current < min) {
    // btrfs is conservative, sometimes it won't let us resize to what the user
    // currently has. In those cases act like the current size is the same as
    // the minimum.
    VLOG(1) << "Minimum size is larger than the current, setting current = min";
    current = min;
  }
  if (current > max) {
    LOG(ERROR) << "current (" << current << ") > max (" << max << ")";
    return {};
  }
  std::vector<int64_t> values = GetTicksForDiskSize(min, max);
  DCHECK(!values.empty());

  // If the current size isn't on one of the ticks insert an extra tick for it.
  // It's possible for the current size to be greater than the maximum tick,
  // in which case we go up to whatever that size is.
  auto it = std::lower_bound(begin(values), end(values), current);
  *out_default_index = std::distance(begin(values), it);
  if (it == end(values) || *it != current) {
    values.insert(it, current);
  }

  std::vector<crostini::mojom::DiskSliderTickPtr> ticks;
  ticks.reserve(values.size());
  for (const auto& val : values) {
    std::string formatted_val = FormatBytes(val);
    ticks.emplace_back(crostini::mojom::DiskSliderTick::New(val, formatted_val,
                                                            formatted_val));
  }
  return ticks;
}

class Observer : public ash::ConciergeClient::DiskImageObserver {
 public:
  Observer(std::string uuid, base::OnceCallback<void(bool)> callback)
      : uuid_(std::move(uuid)), callback_(std::move(callback)) {}
  ~Observer() override { GetConciergeClient()->RemoveDiskImageObserver(this); }

  // ash::ConciergeClient::DiskImageObserver:
  void OnDiskImageProgress(
      const vm_tools::concierge::DiskImageStatusResponse& signal) override {
    if (signal.command_uuid() != uuid_ ||
        signal.status() == DiskImageStatus::DISK_STATUS_IN_PROGRESS) {
      return;
    }

    EmitResizeResultMetric(signal.status());
    bool resized = signal.status() == DiskImageStatus::DISK_STATUS_RESIZED;
    if (!resized) {
      LOG(ERROR) << "Failed or unrecognised status when resizing: "
                 << signal.status() << " " << signal.failure_reason();
    }
    std::move(callback_).Run(resized);
    delete this;
  }

 private:
  std::string uuid_;
  base::OnceCallback<void(bool)> callback_;
};

void ResizeCrostiniDisk(Profile* profile,
                        std::string vm_name,
                        uint64_t size_bytes,
                        base::OnceCallback<void(bool)> callback) {
  guest_os::GuestId container_id(kCrostiniDefaultVmType, vm_name,
                                 kCrostiniDefaultContainerName);
  CrostiniManager::RestartOptions options;
  options.start_vm_only = true;
  CrostiniManager::GetForProfile(profile)->RestartCrostiniWithOptions(
      std::move(container_id), std::move(options),
      base::BindOnce(&OnVMRunning, std::move(callback), profile,
                     std::move(vm_name), size_bytes));
}

void OnVMRunning(base::OnceCallback<void(bool)> callback,
                 Profile* profile,
                 std::string vm_name,
                 int64_t size_bytes,
                 CrostiniResult result) {
  if (result != CrostiniResult::SUCCESS) {
    LOG(ERROR) << "Failed to launch VM: error " << static_cast<int>(result);
    std::move(callback).Run(false);
  } else {
    vm_tools::concierge::ResizeDiskImageRequest request;
    request.set_cryptohome_id(CryptohomeIdForProfile(profile));
    request.set_vm_name(std::move(vm_name));
    request.set_disk_size(size_bytes);

    base::UmaHistogramBoolean("Crostini.DiskResize.Started", true);
    GetConciergeClient()->ResizeDiskImage(
        request, base::BindOnce(&OnResize, std::move(callback)));
  }
}

void OnResize(
    base::OnceCallback<void(bool)> callback,
    std::optional<vm_tools::concierge::ResizeDiskImageResponse> response) {
  if (!response) {
    LOG(ERROR) << "Got null response from concierge";
    EmitResizeResultMetric(DiskImageStatus::DISK_STATUS_UNKNOWN);
    std::move(callback).Run(false);
  } else if (response->status() == DiskImageStatus::DISK_STATUS_RESIZED) {
    EmitResizeResultMetric(response->status());
    std::move(callback).Run(true);
  } else if (response->status() == DiskImageStatus::DISK_STATUS_IN_PROGRESS) {
    // The newly created Observer is self-deleting.
    GetConciergeClient()->AddDiskImageObserver(
        new Observer(response->command_uuid(), std::move(callback)));
  } else {
    LOG(ERROR) << "Got unexpected or error status from concierge: "
               << response->status();
    EmitResizeResultMetric(response->status());
    std::move(callback).Run(false);
  }
}

std::vector<int64_t> GetTicksForDiskSize(int64_t min_size,
                                         int64_t available_space,
                                         int num_ticks) {
  if (min_size < 0 || available_space < 0 || min_size > available_space) {
    return {};
  }
  std::vector<int64_t> ticks;

  int64_t delta = (available_space - min_size) / num_ticks;
  double increments[] = {1 * kGiB, 0.5 * kGiB, 0.2 * kGiB, 0.1 * kGiB};
  double increment;
  if (delta > increments[0]) {
    increment = increments[0];
  } else if (delta > increments[1]) {
    increment = increments[1];
  } else if (delta > increments[2]) {
    increment = increments[2];
  } else {
    increment = increments[3];
  }

  int64_t start = round_up(min_size, increment);
  int64_t end = round_down(available_space, increment);

  if (end <= start) {
    // We have less than 1 tick between min_size and available space, so the
    // only option is to give all the space.
    return std::vector<int64_t>{min_size};
  }

  ticks.emplace_back(start);
  for (int n = 1; std::ceil(n * increment) < (end - start); n++) {
    ticks.emplace_back(start + std::round(n * increment));
  }
  ticks.emplace_back(end);
  return ticks;
}
}  // namespace disk
}  // namespace crostini