1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "chrome/browser/chromeos/crostini/crostini_disk.h"
6 
7 #include <algorithm>
8 #include <cmath>
9 #include <utility>
10 
11 #include "base/bind.h"
12 #include "base/metrics/histogram_functions.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "base/system/sys_info.h"
15 #include "base/task/post_task.h"
16 #include "base/task/thread_pool.h"
17 #include "chrome/browser/chromeos/crostini/crostini_features.h"
18 #include "chrome/browser/chromeos/crostini/crostini_manager.h"
19 #include "chrome/browser/chromeos/crostini/crostini_simple_types.h"
20 #include "chrome/browser/chromeos/crostini/crostini_types.mojom.h"
21 #include "chromeos/dbus/concierge/concierge_service.pb.h"
22 #include "chromeos/dbus/concierge_client.h"
23 #include "chromeos/dbus/dbus_thread_manager.h"
24 #include "ui/base/text/bytes_formatting.h"
25 
26 namespace {
GetConciergeClient()27 chromeos::ConciergeClient* GetConciergeClient() {
28   return chromeos::DBusThreadManager::Get()->GetConciergeClient();
29 }
30 
FormatBytes(const int64_t value)31 std::string FormatBytes(const int64_t value) {
32   return base::UTF16ToUTF8(ui::FormatBytes(value));
33 }
34 
EmitResizeResultMetric(vm_tools::concierge::DiskImageStatus status)35 void EmitResizeResultMetric(vm_tools::concierge::DiskImageStatus status) {
36   base::UmaHistogramEnumeration(
37       "Crostini.DiskResize.Result", status,
38       static_cast<vm_tools::concierge::DiskImageStatus>(
39           vm_tools::concierge::DiskImageStatus_MAX + 1));
40 }
41 
round_up(int64_t n,double increment)42 int64_t round_up(int64_t n, double increment) {
43   return std::ceil(n / increment) * increment;
44 }
45 
round_down(int64_t n,double increment)46 int64_t round_down(int64_t n, double increment) {
47   return std::floor(n / increment) * increment;
48 }
49 }  // namespace
50 
51 namespace crostini {
52 CrostiniDiskInfo::CrostiniDiskInfo() = default;
53 CrostiniDiskInfo::CrostiniDiskInfo(CrostiniDiskInfo&&) = default;
54 CrostiniDiskInfo& CrostiniDiskInfo::operator=(CrostiniDiskInfo&&) = default;
55 CrostiniDiskInfo::~CrostiniDiskInfo() = default;
56 
57 namespace disk {
58 
GetDiskInfo(OnceDiskInfoCallback callback,Profile * profile,std::string vm_name,bool full_info)59 void GetDiskInfo(OnceDiskInfoCallback callback,
60                  Profile* profile,
61                  std::string vm_name,
62                  bool full_info) {
63   if (!CrostiniFeatures::Get()->IsEnabled(profile)) {
64     std::move(callback).Run(nullptr);
65     VLOG(1) << "Crostini not enabled. Nothing to do.";
66     return;
67   }
68   if (full_info) {
69     base::ThreadPool::PostTaskAndReplyWithResult(
70         FROM_HERE, {base::MayBlock()},
71         base::BindOnce(&base::SysInfo::AmountOfFreeDiskSpace,
72                        base::FilePath(crostini::kHomeDirectory)),
73         base::BindOnce(&OnAmountOfFreeDiskSpace, std::move(callback), profile,
74                        std::move(vm_name)));
75   } else {
76     // Since we only care about the disk's current size and whether it's a
77     // sparse disk, we claim there's plenty of free space available to prevent
78     // error conditions in |OnCrostiniSufficientlyRunning|.
79     constexpr int64_t kFakeAvailableDiskBytes =
80         kDiskHeadroomBytes + kRecommendedDiskSizeBytes;
81 
82     OnCrostiniSufficientlyRunning(std::move(callback), profile,
83                                   std::move(vm_name), kFakeAvailableDiskBytes,
84                                   CrostiniResult::SUCCESS);
85   }
86 }
87 
OnAmountOfFreeDiskSpace(OnceDiskInfoCallback callback,Profile * profile,std::string vm_name,int64_t free_space)88 void OnAmountOfFreeDiskSpace(OnceDiskInfoCallback callback,
89                              Profile* profile,
90                              std::string vm_name,
91                              int64_t free_space) {
92   if (free_space == 0) {
93     LOG(ERROR) << "Failed to get amount of free disk space";
94     std::move(callback).Run(nullptr);
95   } else {
96     VLOG(1) << "Starting vm " << vm_name;
97     auto container_id = ContainerId(vm_name, kCrostiniDefaultContainerName);
98     CrostiniManager::GetForProfile(profile)->EnsureVmRunning(
99         std::move(container_id),
100         base::BindOnce(&OnCrostiniSufficientlyRunning, std::move(callback),
101                        profile, std::move(vm_name), free_space));
102   }
103 }
104 
OnCrostiniSufficientlyRunning(OnceDiskInfoCallback callback,Profile * profile,std::string vm_name,int64_t free_space,CrostiniResult result)105 void OnCrostiniSufficientlyRunning(OnceDiskInfoCallback callback,
106                                    Profile* profile,
107                                    std::string vm_name,
108                                    int64_t free_space,
109                                    CrostiniResult result) {
110   if (result != CrostiniResult::SUCCESS) {
111     LOG(ERROR) << "Start VM: error " << static_cast<int>(result);
112     std::move(callback).Run(nullptr);
113   } else {
114     vm_tools::concierge::ListVmDisksRequest request;
115     request.set_cryptohome_id(CryptohomeIdForProfile(profile));
116     request.set_storage_location(vm_tools::concierge::STORAGE_CRYPTOHOME_ROOT);
117     request.set_vm_name(vm_name);
118     GetConciergeClient()->ListVmDisks(
119         std::move(request), base::BindOnce(&OnListVmDisks, std::move(callback),
120                                            std::move(vm_name), free_space));
121   }
122 }
123 
OnListVmDisks(OnceDiskInfoCallback callback,std::string vm_name,int64_t free_space,base::Optional<vm_tools::concierge::ListVmDisksResponse> response)124 void OnListVmDisks(
125     OnceDiskInfoCallback callback,
126     std::string vm_name,
127     int64_t free_space,
128     base::Optional<vm_tools::concierge::ListVmDisksResponse> response) {
129   if (!response) {
130     LOG(ERROR) << "Failed to get response from concierge";
131     std::move(callback).Run(nullptr);
132     return;
133   }
134   if (!response->success()) {
135     LOG(ERROR) << "Failed to get successful response from concierge "
136                << response->failure_reason();
137     std::move(callback).Run(nullptr);
138     return;
139   }
140   auto disk_info = std::make_unique<CrostiniDiskInfo>();
141   auto image =
142       std::find_if(response->images().begin(), response->images().end(),
143                    [&vm_name](const auto& a) { return a.name() == vm_name; });
144   if (image == response->images().end()) {
145     // No match found for the VM:
146     LOG(ERROR) << "No VM found with name " << vm_name;
147     std::move(callback).Run(nullptr);
148     return;
149   }
150   VLOG(1) << "name: " << image->name();
151   VLOG(1) << "image_type: " << image->image_type();
152   VLOG(1) << "size: " << image->size();
153   VLOG(1) << "user_chosen_size: " << image->user_chosen_size();
154   VLOG(1) << "free_space: " << free_space;
155   VLOG(1) << "min_size: " << image->min_size();
156 
157   if (image->image_type() !=
158       vm_tools::concierge::DiskImageType::DISK_IMAGE_RAW) {
159     // Can't resize qcow2 images and don't know how to handle auto or pluginvm
160     // images.
161     disk_info->can_resize = false;
162     std::move(callback).Run(std::move(disk_info));
163     return;
164   }
165   if (image->min_size() == 0) {
166     VLOG(1) << "Unable to get minimum disk size. VM not running yet?";
167   }
168   // User has to leave at least kDiskHeadroomBytes for the host system.
169   // In some cases we can be over-provisioned (e.g. we increased the headroom
170   // required), when that happens the user can still go up to their currently
171   // allocated size.
172   int64_t max_size =
173       std::max(free_space - kDiskHeadroomBytes + image->size(), image->size());
174   disk_info->is_user_chosen_size = image->user_chosen_size();
175   disk_info->can_resize =
176       image->image_type() == vm_tools::concierge::DiskImageType::DISK_IMAGE_RAW;
177   disk_info->is_low_space_available = max_size < kRecommendedDiskSizeBytes;
178 
179   const int64_t min_size =
180       std::max(static_cast<int64_t>(image->min_size()), kMinimumDiskSizeBytes);
181   std::vector<crostini::mojom::DiskSliderTickPtr> ticks =
182       GetTicks(min_size, image->size(), max_size, &(disk_info->default_index));
183   if (ticks.size() == 0) {
184     LOG(ERROR) << "Unable to calculate the number of ticks for min: "
185                << min_size << " current: " << image->size()
186                << " max: " << max_size;
187     std::move(callback).Run(nullptr);
188     return;
189   }
190   disk_info->ticks = std::move(ticks);
191 
192   std::move(callback).Run(std::move(disk_info));
193 }
194 
GetTicks(int64_t min,int64_t current,int64_t max,int * out_default_index)195 std::vector<crostini::mojom::DiskSliderTickPtr> GetTicks(
196     int64_t min,
197     int64_t current,
198     int64_t max,
199     int* out_default_index) {
200   if (current < min) {
201     // btrfs is conservative, sometimes it won't let us resize to what the user
202     // currently has. In those cases act like the current size is the same as
203     // the minimum.
204     VLOG(1) << "Minimum size is larger than the current, setting current = min";
205     current = min;
206   }
207   if (current > max) {
208     LOG(ERROR) << "current (" << current << ") > max (" << max << ")";
209     return {};
210   }
211   std::vector<int64_t> values = GetTicksForDiskSize(min, max);
212   DCHECK(!values.empty());
213 
214   // If the current size isn't on one of the ticks insert an extra tick for it.
215   // It's possible for the current size to be greater than the maximum tick,
216   // in which case we go up to whatever that size is.
217   auto it = std::lower_bound(begin(values), end(values), current);
218   *out_default_index = std::distance(begin(values), it);
219   if (it == end(values) || *it != current) {
220     values.insert(it, current);
221   }
222 
223   std::vector<crostini::mojom::DiskSliderTickPtr> ticks;
224   ticks.reserve(values.size());
225   for (const auto& val : values) {
226     std::string formatted_val = FormatBytes(val);
227     ticks.emplace_back(crostini::mojom::DiskSliderTick::New(val, formatted_val,
228                                                             formatted_val));
229   }
230   return ticks;
231 }
232 
233 class Observer : public chromeos::ConciergeClient::DiskImageObserver {
234  public:
Observer(std::string uuid,base::OnceCallback<void (bool)> callback)235   Observer(std::string uuid, base::OnceCallback<void(bool)> callback)
236       : uuid_(std::move(uuid)), callback_(std::move(callback)) {}
~Observer()237   ~Observer() override { GetConciergeClient()->RemoveDiskImageObserver(this); }
OnDiskImageProgress(const vm_tools::concierge::DiskImageStatusResponse & signal)238   void OnDiskImageProgress(
239       const vm_tools::concierge::DiskImageStatusResponse& signal) override {
240     if (signal.command_uuid() != uuid_) {
241       return;
242     }
243     switch (signal.status()) {
244       case vm_tools::concierge::DiskImageStatus::DISK_STATUS_IN_PROGRESS:
245         break;
246       case vm_tools::concierge::DiskImageStatus::DISK_STATUS_RESIZED:
247         EmitResizeResultMetric(signal.status());
248         std::move(callback_).Run(true);
249         break;
250       default:
251         LOG(ERROR) << "Failed or unrecognised status when resizing: "
252                    << signal.status() << " " << signal.failure_reason();
253         EmitResizeResultMetric(signal.status());
254         std::move(callback_).Run(false);
255         delete this;
256     }
257   }
258 
259  private:
260   std::string uuid_;
261   base::OnceCallback<void(bool)> callback_;
262 };
263 
ResizeCrostiniDisk(Profile * profile,std::string vm_name,uint64_t size_bytes,base::OnceCallback<void (bool)> callback)264 void ResizeCrostiniDisk(Profile* profile,
265                         std::string vm_name,
266                         uint64_t size_bytes,
267                         base::OnceCallback<void(bool)> callback) {
268   ContainerId container_id(vm_name, kCrostiniDefaultContainerName);
269   CrostiniManager::GetForProfile(profile)->EnsureVmRunning(
270       std::move(container_id),
271       base::BindOnce(&OnVMRunning, std::move(callback), profile,
272                      std::move(vm_name), size_bytes));
273 }
274 
OnVMRunning(base::OnceCallback<void (bool)> callback,Profile * profile,std::string vm_name,int64_t size_bytes,CrostiniResult result)275 void OnVMRunning(base::OnceCallback<void(bool)> callback,
276                  Profile* profile,
277                  std::string vm_name,
278                  int64_t size_bytes,
279                  CrostiniResult result) {
280   if (result != CrostiniResult::SUCCESS) {
281     LOG(ERROR) << "Failed to launch VM: error " << static_cast<int>(result);
282     std::move(callback).Run(false);
283   } else {
284     vm_tools::concierge::ResizeDiskImageRequest request;
285     request.set_cryptohome_id(CryptohomeIdForProfile(profile));
286     request.set_vm_name(std::move(vm_name));
287     request.set_disk_size(size_bytes);
288 
289     base::UmaHistogramBoolean("Crostini.DiskResize.Started", true);
290     GetConciergeClient()->ResizeDiskImage(
291         request, base::BindOnce(&OnResize, std::move(callback)));
292   }
293 }
294 
OnResize(base::OnceCallback<void (bool)> callback,base::Optional<vm_tools::concierge::ResizeDiskImageResponse> response)295 void OnResize(
296     base::OnceCallback<void(bool)> callback,
297     base::Optional<vm_tools::concierge::ResizeDiskImageResponse> response) {
298   if (!response) {
299     LOG(ERROR) << "Got null response from concierge";
300     EmitResizeResultMetric(
301         vm_tools::concierge::DiskImageStatus::DISK_STATUS_UNKNOWN);
302     std::move(callback).Run(false);
303   } else if (response->status() ==
304              vm_tools::concierge::DiskImageStatus::DISK_STATUS_RESIZED) {
305     EmitResizeResultMetric(response->status());
306     std::move(callback).Run(true);
307   } else if (response->status() ==
308              vm_tools::concierge::DiskImageStatus::DISK_STATUS_IN_PROGRESS) {
309     GetConciergeClient()->AddDiskImageObserver(
310         new Observer(response->command_uuid(), std::move(callback)));
311   } else {
312     LOG(ERROR) << "Got unexpected or error status from concierge: "
313                << response->status();
314     EmitResizeResultMetric(response->status());
315     std::move(callback).Run(false);
316   }
317 }
318 
GetTicksForDiskSize(int64_t min_size,int64_t available_space,int num_ticks)319 std::vector<int64_t> GetTicksForDiskSize(int64_t min_size,
320                                          int64_t available_space,
321                                          int num_ticks) {
322   if (min_size < 0 || available_space < 0 || min_size > available_space) {
323     return {};
324   }
325   std::vector<int64_t> ticks;
326 
327   int64_t delta = (available_space - min_size) / num_ticks;
328   double increments[] = {1 * kGiB, 0.5 * kGiB, 0.2 * kGiB, 0.1 * kGiB};
329   double increment;
330   if (delta > increments[0]) {
331     increment = increments[0];
332   } else if (delta > increments[1]) {
333     increment = increments[1];
334   } else if (delta > increments[2]) {
335     increment = increments[2];
336   } else {
337     increment = increments[3];
338   }
339 
340   int64_t start = round_up(min_size, increment);
341   int64_t end = round_down(available_space, increment);
342 
343   if (end <= start) {
344     // We have less than 1 tick between min_size and available space, so the
345     // only option is to give all the space.
346     return std::vector<int64_t>{min_size};
347   }
348 
349   ticks.emplace_back(start);
350   for (int n = 1; std::ceil(n * increment) < (end - start); n++) {
351     ticks.emplace_back(start + std::round(n * increment));
352   }
353   ticks.emplace_back(end);
354   return ticks;
355 }
356 }  // namespace disk
357 }  // namespace crostini
358