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